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 ADDED
@@ -0,0 +1,346 @@
1
+ # sf-plugin-permission-sets
2
+
3
+ [![NPM](https://img.shields.io/npm/v/sf-plugin-permission-sets.svg?label=sf-plugin-permission-sets)](https://www.npmjs.com/package/sf-plugin-permission-sets) [![Downloads/week](https://img.shields.io/npm/dw/sf-plugin-permission-sets.svg)](https://npmjs.org/package/sf-plugin-permission-sets) [![License](https://img.shields.io/badge/License-BSD%203--Clause-brightgreen.svg)](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.