sf-plugin-permission-sets 0.0.0-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +346 -0
- package/messages/hello.world.md +29 -0
- package/oclif.lock +7781 -0
- package/oclif.manifest.json +4 -0
- package/package.json +187 -0
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# sf-plugin-permission-sets
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/sf-plugin-permission-sets) [](https://npmjs.org/package/sf-plugin-permission-sets) [](https://raw.githubusercontent.com/zaclummys/sf-plugin-permission-sets/main/LICENSE.txt)
|
|
4
|
+
|
|
5
|
+
> Declarative, GitOps-style management of **permission set assignments** for Salesforce orgs.
|
|
6
|
+
> Define who gets what in version-controlled YAML. The plugin reconciles your org to match it: `plan` then `apply`, just like Terraform.
|
|
7
|
+
|
|
8
|
+
Stop clicking through Setup to grant access. Commit a YAML file, open a PR, let CI show the diff, and merge to apply. Your git history becomes the audit log of who-had-access-when.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of contents
|
|
13
|
+
|
|
14
|
+
- [Why](#why)
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Permission files](#permission-files)
|
|
18
|
+
- [Organizing files](#organizing-files)
|
|
19
|
+
- [Modes](#modes)
|
|
20
|
+
- [Commands](#commands)
|
|
21
|
+
- [CI/CD](#cicd)
|
|
22
|
+
- [Inspiration & equivalents](#inspiration--equivalents)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why
|
|
27
|
+
|
|
28
|
+
Permission set assignments drift. People get access for a project and keep it forever. Offboarding misses a set. Nobody can answer "who can see X and why?" without a SOQL spelunking session.
|
|
29
|
+
|
|
30
|
+
This plugin makes the desired state **declarative and reviewable**:
|
|
31
|
+
|
|
32
|
+
- ✅ **Single source of truth:** the YAML in git is authoritative, and the org is reconciled to it.
|
|
33
|
+
- ✅ **Plan before apply:** see exactly what will be added/removed before anything changes.
|
|
34
|
+
- ✅ **Safe by default:** deletions are opt-in and guarded by a delete threshold.
|
|
35
|
+
- ✅ **CI-native:** fully offline `check`, exit codes for gating, and `--json` on every command.
|
|
36
|
+
- ✅ **Flexible at the edges:** pick your file layout (by permission set or by user) and your sync mode.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sf plugins install sf-plugin-permission-sets
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or pin a version:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
sf plugins install sf-plugin-permission-sets@x.y.z
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requires Salesforce CLI (`sf`) and Node.js 18+.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 1. Bootstrap YAML from an existing org (so you don't start from scratch)
|
|
56
|
+
sf ps export --target-org dev --output-dir permissions
|
|
57
|
+
|
|
58
|
+
# 2. Edit the files, commit, open a PR. Validate offline, no org needed:
|
|
59
|
+
sf ps check --file "./permissions/*.yml"
|
|
60
|
+
|
|
61
|
+
# 3. Validate against a real org (do the users/permission sets exist?)
|
|
62
|
+
sf ps validate --file "./permissions/*.yml" --target-org dev
|
|
63
|
+
|
|
64
|
+
# 4. See what would change
|
|
65
|
+
sf ps plan --file "./permissions/*.yml" --target-org dev
|
|
66
|
+
|
|
67
|
+
# 5. Apply it (additive by default, only adds)
|
|
68
|
+
sf ps apply --file "./permissions/*.yml" --target-org dev
|
|
69
|
+
|
|
70
|
+
# 6. Full reconcile, including removals (opt-in)
|
|
71
|
+
sf ps apply --file "./permissions/*.yml" --target-org prod --mode sync
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Permission files
|
|
75
|
+
|
|
76
|
+
You point every command at one or more YAML files with `--file` (alias `-f`).
|
|
77
|
+
|
|
78
|
+
Multiple files are merged into one model, so splitting by team is encouraged. The files contain **only declarative data**: knobs like sync mode and exclusions are CLI flags (see [Commands](#commands)), so there's no separate config format to learn yet. Each top-level key is unique within a file, and `check` flags duplicates.
|
|
79
|
+
|
|
80
|
+
Each file is a map of usernames, and every scope key under a user is optional (include only what applies):
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
users:
|
|
84
|
+
<username>:
|
|
85
|
+
permissionSets:
|
|
86
|
+
- <PermissionSet.Name>
|
|
87
|
+
permissionSetGroups:
|
|
88
|
+
- <PermissionSetGroup.DeveloperName>
|
|
89
|
+
permissionSetLicenses:
|
|
90
|
+
- <PermissionSetLicense.DeveloperName>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A worked example:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
users:
|
|
97
|
+
jdoe@acme.com:
|
|
98
|
+
permissionSets:
|
|
99
|
+
- Sales_Manager
|
|
100
|
+
- Report_Builder
|
|
101
|
+
permissionSetGroups:
|
|
102
|
+
- Sales_Team_Bundle
|
|
103
|
+
permissionSetLicenses:
|
|
104
|
+
- SalesforceCRM
|
|
105
|
+
|
|
106
|
+
asmith@acme.com:
|
|
107
|
+
permissionSets:
|
|
108
|
+
- Sales_Manager
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `--file` flag is repeatable and the plugin expands globs itself, so all of these work:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
sf ps plan -o dev --file permissions/sales.yml
|
|
115
|
+
sf ps plan -o dev --file "permissions/*.yml" # quote so the plugin (not the shell) expands it
|
|
116
|
+
sf ps plan -o dev --file permissions/sales.yml --file permissions/support.yml
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Organizing files
|
|
120
|
+
|
|
121
|
+
`--file` takes globs and merges everything it matches, so the folder layout is yours to choose. Two common setups:
|
|
122
|
+
|
|
123
|
+
**Per functional slice.** One file per team or domain. Each squad owns its slice, and `CODEOWNERS` plus PR reviews map to it cleanly. Everything merges into one model.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
permissions/
|
|
127
|
+
sales.yml
|
|
128
|
+
service.yml
|
|
129
|
+
marketing.yml
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
sf ps apply -o prod --file "permissions/*.yml"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Per environment.** Because usernames differ per org (sandbox suffixes, different integration users), keep a directory per environment and reconcile each against its matching org. Each file is org-specific, which sidesteps username portability entirely.
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
permissions/
|
|
140
|
+
prod/
|
|
141
|
+
sales.yml
|
|
142
|
+
service.yml
|
|
143
|
+
qa/
|
|
144
|
+
sales.yml
|
|
145
|
+
dev/
|
|
146
|
+
sales.yml
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
sf ps apply -o prod --file "permissions/prod/*.yml"
|
|
151
|
+
sf ps apply -o qa --file "permissions/qa/*.yml"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The two compose: a directory per environment, each split into functional files.
|
|
155
|
+
|
|
156
|
+
## Modes
|
|
157
|
+
|
|
158
|
+
A run performs two atomic operations: **add** missing assignments and **remove** undeclared ones. The mode selects which it actually executes. Set it with `--mode` (default `additive`):
|
|
159
|
+
|
|
160
|
+
| Mode | Adds missing | Removes undeclared | Use when… |
|
|
161
|
+
| ------------- | :----------: | :----------------: | --------------------------------------------------------------------- |
|
|
162
|
+
| `additive` | ✅ | ❌ | **Default.** Grant access, never revoke. Safe rollout. |
|
|
163
|
+
| `destructive` | ❌ | ✅ | Prune/revoke access that isn't declared, without granting anything new. |
|
|
164
|
+
| `sync` | ✅ | ✅ | Full reconcile: make the org exactly match the YAML (`sync` = `additive` + `destructive`). |
|
|
165
|
+
|
|
166
|
+
`plan` always shows the *full* picture (both adds **and** would-be removes) regardless of mode, so you can preview the impact before running it. Whatever the chosen mode won't act on is surfaced as **drift**. Gate CI on it with `--fail-on-drift`.
|
|
167
|
+
|
|
168
|
+
## Commands
|
|
169
|
+
|
|
170
|
+
| Command | Purpose |
|
|
171
|
+
| ---------------- | ---------------------------------------------------------------------- |
|
|
172
|
+
| `sf ps check` | Static analysis of the files alone: schema, duplicates, conflicts, identifier shape. No org, no auth. |
|
|
173
|
+
| `sf ps validate` | Everything `check` does, plus resolving every user/permission set against the org. |
|
|
174
|
+
| `sf ps plan` | Compute and display the change set. Optionally fail on drift. |
|
|
175
|
+
| `sf ps apply` | Reconcile the org. Honors `--mode`, prompts before deletes, enforces guardrails. |
|
|
176
|
+
| `sf ps export` | Generate YAML from the current org state to bootstrap adoption. |
|
|
177
|
+
|
|
178
|
+
### `sf ps check`
|
|
179
|
+
|
|
180
|
+
Fully offline: runs in any CI job or pre-commit hook without org credentials.
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
USAGE
|
|
184
|
+
$ sf ps check -f <glob>... [--strict] [--json]
|
|
185
|
+
|
|
186
|
+
FLAGS
|
|
187
|
+
-f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs are expanded by the plugin.
|
|
188
|
+
--strict Treat warnings as errors.
|
|
189
|
+
|
|
190
|
+
CHECKS
|
|
191
|
+
• valid YAML & schema (unknown keys rejected)
|
|
192
|
+
• duplicate assignees / duplicate (user, target) pairs
|
|
193
|
+
• conflicting intent across files
|
|
194
|
+
• empty or malformed assignee usernames
|
|
195
|
+
• internal referential integrity
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `sf ps validate`
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
USAGE
|
|
202
|
+
$ sf ps validate -o <org> -f <glob>... [--json]
|
|
203
|
+
|
|
204
|
+
FLAGS
|
|
205
|
+
-o, --target-org=<org> (required) Org to resolve against.
|
|
206
|
+
-f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
|
|
207
|
+
|
|
208
|
+
Runs all offline checks, then verifies that every user (active), permission set,
|
|
209
|
+
group, and license referenced actually exists and resolves uniquely.
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `sf ps plan`
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
USAGE
|
|
216
|
+
$ sf ps plan -o <org> -f <glob>... [--mode <value>] [--fail-on-drift] [--json]
|
|
217
|
+
|
|
218
|
+
FLAGS
|
|
219
|
+
-o, --target-org=<org> (required)
|
|
220
|
+
-f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
|
|
221
|
+
--mode=<value> additive | destructive | sync [default: additive]
|
|
222
|
+
--fail-on-drift Exit non-zero if any change is pending (for CI gates).
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Example output:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
$ sf ps plan -o prod --mode sync
|
|
229
|
+
|
|
230
|
+
Permission Set Assignments Plan
|
|
231
|
+
Org: prod (00D5g0000000abcEAA) Mode: sync
|
|
232
|
+
|
|
233
|
+
permissionSets:
|
|
234
|
+
Sales_Manager
|
|
235
|
+
+ asmith@acme.com
|
|
236
|
+
- bwayne@acme.com (undeclared, will be removed)
|
|
237
|
+
= jdoe@acme.com (no change)
|
|
238
|
+
Report_Builder
|
|
239
|
+
+ jdoe@acme.com
|
|
240
|
+
|
|
241
|
+
permissionSetGroups:
|
|
242
|
+
Sales_Team_Bundle (no changes)
|
|
243
|
+
|
|
244
|
+
Plan: 2 to add, 1 to remove, 1 unchanged.
|
|
245
|
+
► Review, then run: sf ps apply -o prod --mode sync
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### `sf ps apply`
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
USAGE
|
|
252
|
+
$ sf ps apply -o <org> -f <glob>... [--mode <value>] [--max-deletes <n>]
|
|
253
|
+
[--dry-run] [--no-prompt] [--json]
|
|
254
|
+
|
|
255
|
+
FLAGS
|
|
256
|
+
-o, --target-org=<org> (required)
|
|
257
|
+
-f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
|
|
258
|
+
--mode=<value> additive | destructive | sync [default: additive]
|
|
259
|
+
--max-deletes=<n> Abort if a run would remove more than n assignments. [default: 50]
|
|
260
|
+
--dry-run Resolve and diff, print what would happen, change nothing.
|
|
261
|
+
--no-prompt Skip the deletion confirmation prompt (for CI).
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Deletions always prompt for confirmation unless `--no-prompt` is set, and are hard-capped by `--max-deletes` so a bad merge can't unassign your whole org. DML is executed with the sObject Collections API and reports partial successes/failures per record.
|
|
265
|
+
|
|
266
|
+
### `sf ps export`
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
USAGE
|
|
270
|
+
$ sf ps export -o <org> [--output-dir <dir>] [--layout <value>]
|
|
271
|
+
[--permission-sets <names>] [--json]
|
|
272
|
+
|
|
273
|
+
FLAGS
|
|
274
|
+
-o, --target-org=<org> (required)
|
|
275
|
+
--output-dir=<dir> [default: permissions] Where to write the generated YAML.
|
|
276
|
+
--layout=<value> by-permission-set | by-user [default: by-permission-set]
|
|
277
|
+
--permission-sets=<names> Comma-separated list to export (default: all assignable).
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## CI/CD
|
|
281
|
+
|
|
282
|
+
A typical ladder: lint on every PR, plan against a sandbox, apply on merge:
|
|
283
|
+
|
|
284
|
+
```yaml
|
|
285
|
+
# .github/workflows/ps-gitops.yml
|
|
286
|
+
name: ps-gitops
|
|
287
|
+
on:
|
|
288
|
+
pull_request:
|
|
289
|
+
push:
|
|
290
|
+
branches: [main]
|
|
291
|
+
|
|
292
|
+
jobs:
|
|
293
|
+
check:
|
|
294
|
+
runs-on: ubuntu-latest
|
|
295
|
+
steps:
|
|
296
|
+
- uses: actions/checkout@v4
|
|
297
|
+
- run: npm install -g @salesforce/cli
|
|
298
|
+
- run: sf plugins install sf-plugin-permission-sets
|
|
299
|
+
- run: sf ps check --file "permissions/*.yml" --strict
|
|
300
|
+
|
|
301
|
+
plan:
|
|
302
|
+
if: github.event_name == 'pull_request'
|
|
303
|
+
needs: check
|
|
304
|
+
runs-on: ubuntu-latest
|
|
305
|
+
steps:
|
|
306
|
+
- uses: actions/checkout@v4
|
|
307
|
+
- run: npm install -g @salesforce/cli
|
|
308
|
+
- run: sf plugins install sf-plugin-permission-sets
|
|
309
|
+
# Auth via Sfdx auth URL stored in a secrets manager, never hardcode credentials
|
|
310
|
+
- run: echo "$SF_AUTH_URL" | sf org login sfdx-url --sfdx-url-stdin --alias target
|
|
311
|
+
env:
|
|
312
|
+
SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }}
|
|
313
|
+
- run: sf ps plan -o target --file "permissions/*.yml" --mode sync --fail-on-drift
|
|
314
|
+
|
|
315
|
+
apply:
|
|
316
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
317
|
+
needs: check
|
|
318
|
+
runs-on: ubuntu-latest
|
|
319
|
+
steps:
|
|
320
|
+
- uses: actions/checkout@v4
|
|
321
|
+
- run: npm install -g @salesforce/cli
|
|
322
|
+
- run: sf plugins install sf-plugin-permission-sets
|
|
323
|
+
- run: echo "$SF_AUTH_URL" | sf org login sfdx-url --sfdx-url-stdin --alias target
|
|
324
|
+
env:
|
|
325
|
+
SF_AUTH_URL: ${{ secrets.SF_AUTH_URL }}
|
|
326
|
+
- run: sf ps apply -o target --file "permissions/*.yml" --mode sync --no-prompt --max-deletes 25
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
> **Credentials:** the plugin never reads or stores secrets itself. It uses orgs you've already authenticated with `sf`. In CI, inject auth from your platform's secrets store (as above), not from committed files.
|
|
330
|
+
|
|
331
|
+
## Inspiration & equivalents
|
|
332
|
+
|
|
333
|
+
The command surface borrows deliberately from tools you already know:
|
|
334
|
+
|
|
335
|
+
| This plugin | Terraform | CloudFormation / SAM | sf core |
|
|
336
|
+
| -------------------- | ---------------------- | ------------------------------- | -------------------------- |
|
|
337
|
+
| `ps check` | `terraform validate` | `sam validate --lint` | n/a |
|
|
338
|
+
| `ps validate` | `terraform plan` (refresh) | `cfn validate-template` | `project deploy validate` |
|
|
339
|
+
| `ps plan` | `terraform plan` | `cfn create-change-set` | `project deploy preview` |
|
|
340
|
+
| `ps apply` | `terraform apply` | `cfn execute-change-set` / `sam deploy` | `project deploy start` |
|
|
341
|
+
| `ps export` | `terraform import` | n/a | n/a |
|
|
342
|
+
| `--fail-on-drift` | drift in plan exit code | `cfn detect-stack-drift` | n/a |
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
BSD-3-Clause © Isaac Ferreira
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# summary
|
|
2
|
+
|
|
3
|
+
Say hello.
|
|
4
|
+
|
|
5
|
+
# description
|
|
6
|
+
|
|
7
|
+
Say hello either to the world or someone you know.
|
|
8
|
+
|
|
9
|
+
# flags.name.summary
|
|
10
|
+
|
|
11
|
+
The name of the person you'd like to say hello to.
|
|
12
|
+
|
|
13
|
+
# flags.name.description
|
|
14
|
+
|
|
15
|
+
This person can be anyone in the world!
|
|
16
|
+
|
|
17
|
+
# examples
|
|
18
|
+
|
|
19
|
+
- Say hello to the world:
|
|
20
|
+
|
|
21
|
+
<%= config.bin %> <%= command.id %>
|
|
22
|
+
|
|
23
|
+
- Say hello to someone you know:
|
|
24
|
+
|
|
25
|
+
<%= config.bin %> <%= command.id %> --name Astro
|
|
26
|
+
|
|
27
|
+
# info.hello
|
|
28
|
+
|
|
29
|
+
Hello %s at %s.
|