sf-plugin-permission-sets 0.4.0 → 0.6.0
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 +135 -79
- package/lib/adapters/connection-org-client.d.ts +8 -6
- package/lib/adapters/connection-org-client.d.ts.map +1 -1
- package/lib/adapters/connection-org-client.js +187 -96
- package/lib/adapters/connection-org-client.js.map +1 -1
- package/lib/commands/ps/apply.d.ts +1 -15
- package/lib/commands/ps/apply.d.ts.map +1 -1
- package/lib/commands/ps/apply.js +11 -81
- package/lib/commands/ps/apply.js.map +1 -1
- package/lib/commands/ps/check.d.ts.map +1 -1
- package/lib/commands/ps/check.js +3 -3
- package/lib/commands/ps/check.js.map +1 -1
- package/lib/commands/ps/export.js +2 -2
- package/lib/commands/ps/export.js.map +1 -1
- package/lib/commands/ps/plan.d.ts +2 -8
- package/lib/commands/ps/plan.d.ts.map +1 -1
- package/lib/commands/ps/plan.js +31 -84
- package/lib/commands/ps/plan.js.map +1 -1
- package/lib/commands/ps/validate.d.ts.map +1 -1
- package/lib/commands/ps/validate.js +3 -3
- package/lib/commands/ps/validate.js.map +1 -1
- package/lib/core/diff.d.ts +37 -1
- package/lib/core/diff.d.ts.map +1 -1
- package/lib/core/diff.js +68 -4
- package/lib/core/diff.js.map +1 -1
- package/lib/core/finding.d.ts +9 -7
- package/lib/core/finding.d.ts.map +1 -1
- package/lib/core/finding.js +16 -8
- package/lib/core/finding.js.map +1 -1
- package/lib/core/index.d.ts +4 -4
- package/lib/core/index.d.ts.map +1 -1
- package/lib/core/index.js +3 -3
- package/lib/core/index.js.map +1 -1
- package/lib/core/load.js +1 -1
- package/lib/core/load.js.map +1 -1
- package/lib/core/model.d.ts +24 -25
- package/lib/core/model.d.ts.map +1 -1
- package/lib/core/normalize.d.ts.map +1 -1
- package/lib/core/normalize.js +13 -8
- package/lib/core/normalize.js.map +1 -1
- package/lib/core/report.d.ts +2 -1
- package/lib/core/report.d.ts.map +1 -1
- package/lib/core/report.js +30 -17
- package/lib/core/report.js.map +1 -1
- package/lib/core/resolve.d.ts +8 -6
- package/lib/core/resolve.d.ts.map +1 -1
- package/lib/core/resolve.js +18 -14
- package/lib/core/resolve.js.map +1 -1
- package/lib/core/serialize.d.ts.map +1 -1
- package/lib/core/serialize.js +29 -13
- package/lib/core/serialize.js.map +1 -1
- package/lib/core/target-name.d.ts +20 -0
- package/lib/core/target-name.d.ts.map +1 -0
- package/lib/core/target-name.js +32 -0
- package/lib/core/target-name.js.map +1 -0
- package/lib/core/username.d.ts +23 -0
- package/lib/core/username.d.ts.map +1 -0
- package/lib/core/username.js +35 -0
- package/lib/core/username.js.map +1 -0
- package/lib/services/adapters/org-client.d.ts +9 -5
- package/lib/services/adapters/org-client.d.ts.map +1 -1
- package/lib/services/apply.d.ts +1 -18
- package/lib/services/apply.d.ts.map +1 -1
- package/lib/services/apply.js +9 -38
- package/lib/services/apply.js.map +1 -1
- package/lib/services/export.d.ts.map +1 -1
- package/lib/services/export.js +3 -2
- package/lib/services/export.js.map +1 -1
- package/lib/services/index.d.ts +0 -1
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +0 -1
- package/lib/services/index.js.map +1 -1
- package/lib/services/plan.d.ts +4 -14
- package/lib/services/plan.d.ts.map +1 -1
- package/lib/services/plan.js +9 -22
- package/lib/services/plan.js.map +1 -1
- package/lib/services/resolution.d.ts +17 -9
- package/lib/services/resolution.d.ts.map +1 -1
- package/lib/services/resolution.js +43 -16
- package/lib/services/resolution.js.map +1 -1
- package/lib/services/validate.d.ts +2 -1
- package/lib/services/validate.d.ts.map +1 -1
- package/lib/services/validate.js +10 -1
- package/lib/services/validate.js.map +1 -1
- package/messages/ps.apply.md +2 -34
- package/messages/ps.plan.md +0 -8
- package/messages/ps.validate.md +1 -1
- package/package.json +10 -6
- package/lib/core/mode.d.ts +0 -18
- package/lib/core/mode.d.ts.map +0 -1
- package/lib/core/mode.js +0 -16
- package/lib/core/mode.js.map +0 -1
- package/lib/core/plan-file.d.ts +0 -61
- package/lib/core/plan-file.d.ts.map +0 -1
- package/lib/core/plan-file.js +0 -74
- package/lib/core/plan-file.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# sf-plugin-permission-sets
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/sf-plugin-permission-sets) [](https://npmjs.org/package/sf-plugin-permission-sets) [](https://semver.org/#spec-item-4) [](https://raw.githubusercontent.com/zaclummys/sf-plugin-permission-sets/main/LICENSE.
|
|
3
|
+
[](https://www.npmjs.com/package/sf-plugin-permission-sets) [](https://npmjs.org/package/sf-plugin-permission-sets) [](https://semver.org/#spec-item-4) [](https://raw.githubusercontent.com/zaclummys/sf-plugin-permission-sets/main/LICENSE.md)
|
|
4
4
|
|
|
5
5
|
> Declarative, GitOps-style management of **permission set assignments** for Salesforce orgs.
|
|
6
6
|
> Define who gets what in version-controlled YAML. The plugin reconciles your org to match it: `plan` then `apply`, just like Terraform.
|
|
@@ -15,6 +15,7 @@ Stop clicking through Setup to grant access. Commit a YAML file, open a PR, let
|
|
|
15
15
|
|
|
16
16
|
- [Why](#why)
|
|
17
17
|
- [Install](#install)
|
|
18
|
+
- [Org permissions](#org-permissions)
|
|
18
19
|
- [Quick start](#quick-start)
|
|
19
20
|
- [Permission files](#permission-files)
|
|
20
21
|
- [Organizing files](#organizing-files)
|
|
@@ -22,9 +23,10 @@ Stop clicking through Setup to grant access. Commit a YAML file, open a PR, let
|
|
|
22
23
|
- [Validations](#validations)
|
|
23
24
|
- [Commands](#commands)
|
|
24
25
|
- [GitHub Actions](#github-actions)
|
|
25
|
-
- [Inspiration & equivalents](#inspiration--equivalents)
|
|
26
26
|
- [Versioning](#versioning)
|
|
27
27
|
- [Architecture](#architecture)
|
|
28
|
+
- [Development](#development)
|
|
29
|
+
- [License](#license)
|
|
28
30
|
|
|
29
31
|
---
|
|
30
32
|
|
|
@@ -54,28 +56,47 @@ Or pin a version:
|
|
|
54
56
|
sf plugins install sf-plugin-permission-sets@x.y.z
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
Requires Salesforce CLI (`sf`) and Node.js
|
|
59
|
+
Requires Salesforce CLI (`sf`) and Node.js 22.13+.
|
|
60
|
+
|
|
61
|
+
## Org permissions
|
|
62
|
+
|
|
63
|
+
What the user behind `--target-org` needs:
|
|
64
|
+
|
|
65
|
+
| Command | API Enabled | View Setup and Configuration | View Roles and Role Hierarchy | Assign Permission Sets |
|
|
66
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
67
|
+
| `sf ps check` | - | - | - | - |
|
|
68
|
+
| `sf ps validate` | ✓ | ✓ | ✓ | - |
|
|
69
|
+
| `sf ps plan` | ✓ | ✓ | ✓ | - |
|
|
70
|
+
| `sf ps export` | ✓ | ✓ | ✓ | - |
|
|
71
|
+
| `sf ps apply` | ✓ | ✓ | ✓ | ✓ |
|
|
72
|
+
|
|
73
|
+
✓ Required, - Not required. `Manage Users` is not required, not even for permission set licenses.
|
|
74
|
+
|
|
75
|
+
Two permission sets, so a pull request job cannot change the org:
|
|
76
|
+
|
|
77
|
+
- [PS_Plugin_Read](setup/permissionsets/PS_Plugin_Read.permissionset-meta.xml): reads permission set assignments.
|
|
78
|
+
- [PS_Plugin_Write](setup/permissionsets/PS_Plugin_Write.permissionset-meta.xml): reads and modifies permission set assignments.
|
|
58
79
|
|
|
59
80
|
## Quick start
|
|
60
81
|
|
|
61
82
|
```bash
|
|
62
83
|
# 1. Bootstrap YAML from an existing org (so you don't start from scratch)
|
|
63
|
-
sf ps export --target-org dev --output-file permissions.yml
|
|
84
|
+
sf ps export --target-org dev --output-file permissions/dev.yml
|
|
64
85
|
|
|
65
86
|
# 2. Edit the files, commit, open a PR. Check them, no org needed:
|
|
66
|
-
sf ps check --file "
|
|
87
|
+
sf ps check --file "permissions/*.yml"
|
|
67
88
|
|
|
68
89
|
# 3. Validate against a real org (do the users/permission sets exist?)
|
|
69
|
-
sf ps validate --file "
|
|
90
|
+
sf ps validate --file "permissions/*.yml" --target-org dev
|
|
70
91
|
|
|
71
92
|
# 4. See what would change
|
|
72
|
-
sf ps plan --file "
|
|
93
|
+
sf ps plan --file "permissions/*.yml" --target-org dev
|
|
73
94
|
|
|
74
95
|
# 5. Apply it (additive by default, only adds)
|
|
75
|
-
sf ps apply --file "
|
|
96
|
+
sf ps apply --file "permissions/*.yml" --target-org dev
|
|
76
97
|
|
|
77
98
|
# 6. Full reconcile, including removals (opt-in)
|
|
78
|
-
sf ps apply --file "
|
|
99
|
+
sf ps apply --file "permissions/*.yml" --target-org prod --mode sync
|
|
79
100
|
```
|
|
80
101
|
|
|
81
102
|
## Permission files
|
|
@@ -115,6 +136,12 @@ users:
|
|
|
115
136
|
- Sales_Manager
|
|
116
137
|
```
|
|
117
138
|
|
|
139
|
+
### Names are matched case-insensitively
|
|
140
|
+
|
|
141
|
+
Salesforce compares usernames and API names without regard to case, and so does this plugin. `JDoe@acme.com` and `jdoe@acme.com` are the same user, `Sales_Manager` and `sales_manager` are the same permission set, and every comparison the plugin makes (merging files, de-duplicating, diffing against the org, filtering an export) follows that rule. The spelling you write is the spelling that gets displayed and written back, so a file is never rewritten just to normalize case.
|
|
142
|
+
|
|
143
|
+
The practical consequence is that declaring the same user twice under different spellings merges the two blocks rather than creating two users, and listing a target twice under different spellings is the same duplicate `check` already warns about.
|
|
144
|
+
|
|
118
145
|
### Timed access (expiration)
|
|
119
146
|
|
|
120
147
|
A permission set or permission set group entry can be a plain name or an object with an `expiration`. The expiration is an ISO 8601 datetime, and Salesforce removes access automatically when it passes. Plain names never expire.
|
|
@@ -197,7 +224,8 @@ Every run checks the files first. `check` runs the file checks with no org, and
|
|
|
197
224
|
| Situation | Checked by | Severity | Result |
|
|
198
225
|
| --- | --- | :---: | --- |
|
|
199
226
|
| Same user in two files with different targets | `check` | ✅ ok | Merged into one model, the point of slicing |
|
|
200
|
-
| Same
|
|
227
|
+
| Same user under two spellings that differ only in case | `check` | ✅ ok | Merged into one user, matching how the org compares usernames |
|
|
228
|
+
| Same target listed twice for a user (case-insensitively) | `check` | ⚠️ warning | Deduped |
|
|
201
229
|
| A user with no scopes, or an empty list | `check` | ⚠️ warning | Ignored as a no-op |
|
|
202
230
|
| Same username key appears twice in one file | `check` | ❌ error | Rejected, the intent is ambiguous |
|
|
203
231
|
| Declared user, permission set, group, or license missing or not unique | `validate` | ❌ error | Run fails before any change |
|
|
@@ -209,8 +237,8 @@ Every run checks the files first. `check` runs the file checks with no org, and
|
|
|
209
237
|
| `sf ps check` | Static analysis of the files alone: schema, duplicates, conflicts, identifier shape. No org, no auth. |
|
|
210
238
|
| `sf ps validate` | Everything `check` does, plus resolving every user/permission set against the org. |
|
|
211
239
|
| `sf ps plan` | Compute and display the change set: a read-only preview of what `apply` would do. |
|
|
212
|
-
| `sf ps apply` | Reconcile the org. Honors `--mode`, prompts before deletes, enforces guardrails. |
|
|
213
240
|
| `sf ps export` | Generate YAML from the current org state to bootstrap adoption. |
|
|
241
|
+
| `sf ps apply` | Reconcile the org. Honors `--mode`, prompts before deletes, enforces guardrails. |
|
|
214
242
|
|
|
215
243
|
### `sf ps check`
|
|
216
244
|
|
|
@@ -250,19 +278,15 @@ group, and license referenced actually exists and resolves uniquely.
|
|
|
250
278
|
|
|
251
279
|
```
|
|
252
280
|
USAGE
|
|
253
|
-
$ sf ps plan -o <org> -f <glob>... [--mode <value>] [--show-unchanged]
|
|
254
|
-
[--out <file>] [--json]
|
|
281
|
+
$ sf ps plan -o <org> -f <glob>... [--mode <value>] [--show-unchanged] [--json]
|
|
255
282
|
|
|
256
283
|
FLAGS
|
|
257
284
|
-o, --target-org=<org> (required)
|
|
258
285
|
-f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
|
|
259
286
|
--mode=<value> additive | destructive | sync [default: additive]
|
|
260
287
|
--show-unchanged List assignments that already match, instead of only counting them.
|
|
261
|
-
--out=<file> Write the computed change set to a plan file that `apply` can run verbatim.
|
|
262
288
|
```
|
|
263
289
|
|
|
264
|
-
With `--out` the plan is also saved to a file: the resolved change set (adds, updates, removes with their record ids), the mode it was computed for, and the org it targets. Feed that file to `apply --plan` to execute exactly what you reviewed, with no recomputation. See [Saved plans](#saved-plans).
|
|
265
|
-
|
|
266
290
|
The body shows only what the mode will do, and unchanged assignments are summarized as a count (pass `--show-unchanged` to list them). The default `additive` run previews only what it grants, and reports the undeclared assignment it won't remove as drift:
|
|
267
291
|
|
|
268
292
|
```text
|
|
@@ -307,46 +331,6 @@ Unchanged: 4 assignments (--show-unchanged to list).
|
|
|
307
331
|
Next: sf ps apply -o prod -f "permissions/*.yml" --mode sync
|
|
308
332
|
```
|
|
309
333
|
|
|
310
|
-
### `sf ps apply`
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
USAGE
|
|
314
|
-
$ sf ps apply -o <org> (-f <glob>... | --plan <file>) [--mode <value>]
|
|
315
|
-
[--max-deletes <n>] [--dry-run] [--show-unchanged] [--no-prompt] [--json]
|
|
316
|
-
|
|
317
|
-
FLAGS
|
|
318
|
-
-o, --target-org=<org> (required)
|
|
319
|
-
-f, --file=<glob>... YAML file(s) to read. Repeatable, globs expanded by the plugin.
|
|
320
|
-
--plan=<file> Apply a saved plan file from `plan --out` instead of re-reading YAML.
|
|
321
|
-
--mode=<value> additive | destructive | sync [default: additive]
|
|
322
|
-
--max-deletes=<n> Abort if a run would remove more than n assignments. [default: 50]
|
|
323
|
-
--dry-run Resolve and diff, print what would happen, change nothing.
|
|
324
|
-
--show-unchanged List assignments that already match, instead of only counting them.
|
|
325
|
-
--no-prompt Skip the deletion confirmation prompt (for CI).
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Provide exactly one source: `--file` (read and diff the YAML now) or `--plan` (run a saved plan). They cannot be combined, and `--mode` cannot accompany `--plan` (the plan already carries its mode). `--max-deletes`, `--dry-run`, and `--no-prompt` apply to both.
|
|
329
|
-
|
|
330
|
-
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.
|
|
331
|
-
|
|
332
|
-
#### Saved plans
|
|
333
|
-
|
|
334
|
-
`plan --out` and `apply --plan` split review from execution, so what you approve is exactly what runs:
|
|
335
|
-
|
|
336
|
-
```bash
|
|
337
|
-
sf ps plan -o prod -f "permissions/*.yml" --mode sync --out prod.plan
|
|
338
|
-
# ... review prod.plan, get sign-off ...
|
|
339
|
-
sf ps apply -o prod --plan prod.plan
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
Without a saved plan, `apply` recomputes from the files: it re-reads the YAML, re-resolves every reference to an org id, and re-diffs against live state. Anything that changed since you ran `plan` (an edited file, a renamed permission set, another admin's assignment) silently changes what `apply` does. A saved plan freezes the resolved change set, so `apply --plan` executes those exact records with no recomputation.
|
|
343
|
-
|
|
344
|
-
Guardrails:
|
|
345
|
-
|
|
346
|
-
- `apply --plan` refuses a plan built for a different org (the plan records the org id).
|
|
347
|
-
- It refuses a plan file it cannot parse or whose format version it does not recognize.
|
|
348
|
-
- The plan is executed as recorded. If the org drifted after the plan was written, individual records may fail (a removed target, an already-deleted assignment); those surface as per-record failures in the outcome report rather than aborting the run. Re-run `plan` to get a fresh plan when in doubt.
|
|
349
|
-
|
|
350
334
|
### `sf ps export`
|
|
351
335
|
|
|
352
336
|
Read-only. Snapshots the org's current assignments as YAML you can commit and then feed back into the other commands. Writes to a file with `--output-file`, or to stdout when that flag is omitted.
|
|
@@ -359,7 +343,7 @@ USAGE
|
|
|
359
343
|
FLAGS
|
|
360
344
|
-o, --target-org=<org> (required) Org to read assignments from.
|
|
361
345
|
-f, --output-file=<file> Path of the YAML file to write. Parent directories are created; an existing file is overwritten. Omit to write to stdout.
|
|
362
|
-
--user=<username>... Only export these users. Repeatable, matched
|
|
346
|
+
--user=<username>... Only export these users. Repeatable, matched case-insensitively.
|
|
363
347
|
--kind=<scope>... Only export these scopes: permissionSets | permissionSetGroups | permissionSetLicenses. Repeatable.
|
|
364
348
|
```
|
|
365
349
|
|
|
@@ -375,7 +359,7 @@ sf ps export -o prod | diff - permissions/prod.yml
|
|
|
375
359
|
sf ps export -o prod --user jdoe@acme.com > jdoe.yml
|
|
376
360
|
```
|
|
377
361
|
|
|
378
|
-
By default the whole org is exported. `--user` and `--kind` narrow the snapshot: pass either to scope it down, and pass both to intersect (the named users, restricted to the named scopes). Values within a flag are a union, so `--user jdoe@acme.com --user asmith@acme.com` exports both. The `--kind` values are the same scope keys the file uses, so `--kind permissionSetLicenses` reads back exactly the `permissionSetLicenses:` block.
|
|
362
|
+
By default the whole org is exported. `--user` and `--kind` narrow the snapshot: pass either to scope it down, and pass both to intersect (the named users, restricted to the named scopes). Values within a flag are a union, so `--user jdoe@acme.com --user asmith@acme.com` exports both. A `--user` value is matched case-insensitively, so only a user the org really does not hold assignments for is reported as unmatched. The `--kind` values are the same scope keys the file uses, so `--kind permissionSetLicenses` reads back exactly the `permissionSetLicenses:` block.
|
|
379
363
|
|
|
380
364
|
```bash
|
|
381
365
|
# Snapshot one team's permission sets and groups only
|
|
@@ -386,9 +370,35 @@ sf ps export -o prod --output-file team.yml \
|
|
|
386
370
|
|
|
387
371
|
A requested `--user` that has no matching assignments (a typo, or a user who genuinely holds nothing in scope) is reported as a warning and the export continues with whoever matched, so a mistyped username never masquerades as a clean empty file.
|
|
388
372
|
|
|
373
|
+
### `sf ps apply`
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
USAGE
|
|
377
|
+
$ sf ps apply -o <org> -f <glob>... [--mode <value>]
|
|
378
|
+
[--max-deletes <n>] [--dry-run] [--show-unchanged] [--no-prompt] [--json]
|
|
379
|
+
|
|
380
|
+
FLAGS
|
|
381
|
+
-o, --target-org=<org> (required)
|
|
382
|
+
-f, --file=<glob>... (required) YAML file(s) to read. Repeatable, globs expanded by the plugin.
|
|
383
|
+
--mode=<value> additive | destructive | sync [default: additive]
|
|
384
|
+
--max-deletes=<n> Abort if a run would remove more than n assignments. [default: 50]
|
|
385
|
+
--dry-run Resolve and diff, print what would happen, change nothing.
|
|
386
|
+
--show-unchanged List assignments that already match, instead of only counting them.
|
|
387
|
+
--no-prompt Skip the deletion confirmation prompt (for CI).
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`apply` recomputes from the files every run: it re-reads the YAML, re-resolves every reference to an org id, and re-diffs against live state, then acts per `--mode`. Run `plan` shortly before `apply` so the preview you review reflects what `apply` will do (an edited file, a renamed permission set, or another admin's change between the two shifts the outcome).
|
|
391
|
+
|
|
392
|
+
> [!CAUTION]
|
|
393
|
+
> `--mode sync` and `--mode destructive` revoke access. 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.
|
|
394
|
+
|
|
395
|
+
DML is executed with the sObject Collections API and reports partial successes/failures per record.
|
|
396
|
+
|
|
389
397
|
## GitHub Actions
|
|
390
398
|
|
|
391
|
-
|
|
399
|
+
Three small workflows that build on each other: check pull requests with no org at all, validate the same files against the real org, then apply on merge.
|
|
400
|
+
|
|
401
|
+
Point the pull request workflows at a `PS_Plugin_Read` user and only the merge workflow at `PS_Plugin_Write` (see [Org permissions](#org-permissions)).
|
|
392
402
|
|
|
393
403
|
**1. Check pull requests to main** (no org, no secrets):
|
|
394
404
|
|
|
@@ -407,7 +417,7 @@ jobs:
|
|
|
407
417
|
- uses: actions/checkout@v7
|
|
408
418
|
- uses: actions/setup-node@v7
|
|
409
419
|
with:
|
|
410
|
-
node-version:
|
|
420
|
+
node-version: 22
|
|
411
421
|
|
|
412
422
|
- name: Install Salesforce CLI
|
|
413
423
|
run: npm install --global @salesforce/cli
|
|
@@ -419,7 +429,41 @@ jobs:
|
|
|
419
429
|
run: sf ps check --file "permissions/*.yml"
|
|
420
430
|
```
|
|
421
431
|
|
|
422
|
-
**2.
|
|
432
|
+
**2. Validate pull requests against the org** (needs org auth):
|
|
433
|
+
|
|
434
|
+
```yaml
|
|
435
|
+
# .github/workflows/permissions-validate.yml
|
|
436
|
+
name: permissions-validate
|
|
437
|
+
|
|
438
|
+
on:
|
|
439
|
+
pull_request:
|
|
440
|
+
branches: [main]
|
|
441
|
+
paths:
|
|
442
|
+
- "permissions/**"
|
|
443
|
+
|
|
444
|
+
jobs:
|
|
445
|
+
validate:
|
|
446
|
+
runs-on: ubuntu-latest
|
|
447
|
+
steps:
|
|
448
|
+
- uses: actions/checkout@v7
|
|
449
|
+
- uses: actions/setup-node@v7
|
|
450
|
+
with:
|
|
451
|
+
node-version: 22
|
|
452
|
+
|
|
453
|
+
- name: Install Salesforce CLI
|
|
454
|
+
run: npm install --global @salesforce/cli
|
|
455
|
+
|
|
456
|
+
- name: Install the plugin
|
|
457
|
+
run: sf plugins install sf-plugin-permission-sets
|
|
458
|
+
|
|
459
|
+
- name: Log in to the org
|
|
460
|
+
run: echo '${{ secrets.SFDX_AUTH_URL }}' | sf org login sfdx-url --sfdx-url-stdin --alias prod
|
|
461
|
+
|
|
462
|
+
- name: Validate the permission files
|
|
463
|
+
run: sf ps validate --file "permissions/*.yml" --target-org prod
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**3. Apply on merge to main** (needs org auth):
|
|
423
467
|
|
|
424
468
|
```yaml
|
|
425
469
|
# .github/workflows/permissions-apply.yml
|
|
@@ -436,7 +480,7 @@ jobs:
|
|
|
436
480
|
- uses: actions/checkout@v7
|
|
437
481
|
- uses: actions/setup-node@v7
|
|
438
482
|
with:
|
|
439
|
-
node-version:
|
|
483
|
+
node-version: 22
|
|
440
484
|
|
|
441
485
|
- name: Install Salesforce CLI
|
|
442
486
|
run: npm install --global @salesforce/cli
|
|
@@ -451,18 +495,9 @@ jobs:
|
|
|
451
495
|
run: sf ps apply --file "permissions/*.yml" --target-org prod --mode sync --no-prompt
|
|
452
496
|
```
|
|
453
497
|
|
|
454
|
-
Get the auth URL once with `sf org display --verbose --target-org prod`, copy the `Sfdx Auth Url` value, and save it as a repository secret named `SFDX_AUTH_URL`.
|
|
455
|
-
|
|
456
|
-
Want the diff on the PR before merging? Add a `sf ps plan --file "permissions/*.yml" --target-org prod` step (it needs the same org auth) to the check workflow.
|
|
457
|
-
|
|
458
|
-
## Inspiration & equivalents
|
|
459
|
-
|
|
460
|
-
This plugin's command surface borrows ideas from tools you already know:
|
|
498
|
+
Workflows 2 and 3 share one secret. Get the auth URL once with `sf org display --verbose --target-org prod`, copy the `Sfdx Auth Url` value, and save it as a repository secret named `SFDX_AUTH_URL`.
|
|
461
499
|
|
|
462
|
-
-
|
|
463
|
-
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/)
|
|
464
|
-
- [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/)
|
|
465
|
-
- [Salesforce CLI](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
|
|
500
|
+
Want the diff on the PR before merging? Add a `sf ps plan --file "permissions/*.yml" --target-org prod` step to workflow 2, right after the login step, so reviewers see the change set that `apply` will carry out on merge.
|
|
466
501
|
|
|
467
502
|
## Versioning
|
|
468
503
|
|
|
@@ -502,13 +537,14 @@ gh release create v0.2.0 --target main --title v0.2.0 --notes "Add ps export"
|
|
|
502
537
|
The plugin is layered so every command reuses the same core. Commands stay thin, services hold the orchestration, core holds the reusable primitives, and a thin adapter layer isolates the Salesforce SDK.
|
|
503
538
|
|
|
504
539
|
- **Commands** (`src/commands/ps/`): oclif only. They parse flags, construct the service (wiring in the org adapter when the command needs one), render output, and set the exit code.
|
|
505
|
-
- **Services** (`src/services/`): one per command (`check`, `validate`, `export`, `apply`, and `plan`). Each is a class
|
|
540
|
+
- **Services** (`src/services/`): one per command (`check`, `validate`, `export`, `apply`, and `plan`), plus `resolution`, which the org-facing ones share. Each is a class whose constructor takes only its dependencies (the org client, a confirmation callback), while the per-invocation inputs are `run()` parameters, so one instance serves any number of runs. A service also declares the ports it needs from the outside, like the `OrgClient` interface its adapter implements.
|
|
506
541
|
- **Core** (`src/core/`): the reusable building blocks. Pure, with no `@salesforce/*` imports, so every piece is unit-testable on its own.
|
|
507
542
|
- **Adapters** (`src/adapters/`): the boundary to the outside world. `ConnectionOrgClient` implements the `OrgClient` port (declared in services) with a Salesforce `Connection`, and owns all the SOQL and SObject detail. Services depend on the port, not the SDK, so they test against a fake and stay free of connection detail.
|
|
508
543
|
|
|
509
544
|
| Core module | Responsibility |
|
|
510
545
|
| --- | --- |
|
|
511
|
-
| `model` | Shared domain types (assignment, org
|
|
546
|
+
| `model` | Shared domain types (assignment, org). |
|
|
547
|
+
| `username`, `target-name` | The identifiers, owning the org's case-insensitive comparison so no caller has to remember it. |
|
|
512
548
|
| `finding` | The finding type and code vocabulary, plus constructors, formatting, and counting. |
|
|
513
549
|
| `schema` | The zod contract for a file, plus validation. |
|
|
514
550
|
| `parse` | File text to an object, with YAML and duplicate-key errors. |
|
|
@@ -516,12 +552,32 @@ The plugin is layered so every command reuses the same core. Commands stay thin,
|
|
|
516
552
|
| `serialize` | Canonical tuples back to a user-keyed YAML document (the inverse of `normalize`). |
|
|
517
553
|
| `load` | Expand globs, run parse then validate then normalize per file, and merge by union. |
|
|
518
554
|
| `resolve` | Pure rules that turn declared references and the org's answers into findings, plus id lookups for assigning. No SOQL: the adapter owns that. |
|
|
519
|
-
| `diff` | The desired model vs. the org's current state, producing adds, removes, and unchanged. |
|
|
520
|
-
| `mode` | Scope a diff to what a reconcile mode acts on, plus the drift it leaves alone. |
|
|
555
|
+
| `diff` | The desired model vs. the org's current state, producing adds, removes, and unchanged. The `Diff` it returns also scopes itself to a reconcile mode, reporting what that mode acts on and the drift it leaves alone. |
|
|
521
556
|
| `report` | Format a diff as a plan. |
|
|
522
|
-
| `plan-file` | Serialize and parse a saved plan: the frozen change set `apply --plan` runs. |
|
|
523
557
|
|
|
524
|
-
Commands are slices of one pipeline. `check` runs the **load** stage only, with no org. `validate` adds **resolve**: it looks the declared references up through the `OrgClient` port (the adapter builds the SOQL) and evaluates the org's answers with resolve's pure rules. `export` runs in the opposite direction: it **fetch**es the org's current assignments through the port and **serialize**s them straight back to YAML, skipping load entirely. `apply` is the full pipeline: load, resolve to ids, **fetch** current state, **diff**, then insert and delete through the Collections API per the mode (guarded by `--max-deletes` and a confirmation). `plan` is that same pipeline stopping before the DML: load, resolve to ids, **fetch** current state, **diff**, and report, the same preview `apply --dry-run` produces.
|
|
558
|
+
Commands are slices of one pipeline. `check` runs the **load** stage only, with no org. `validate` adds **resolve**: it looks the declared references up through the `OrgClient` port (the adapter builds the SOQL) and evaluates the org's answers with resolve's pure rules. `export` runs in the opposite direction: it **fetch**es the org's current assignments through the port and **serialize**s them straight back to YAML, skipping load entirely. `apply` is the full pipeline: load, resolve to ids, **fetch** current state, **diff**, then insert and delete through the Collections API per the mode (guarded by `--max-deletes` and a confirmation). `plan` is that same pipeline stopping before the DML: load, resolve to ids, **fetch** current state, **diff**, and report, the same preview `apply --dry-run` produces.
|
|
559
|
+
|
|
560
|
+
## Development
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
npm ci
|
|
564
|
+
npm run build # compile and lint
|
|
565
|
+
npm test # compile, then run the suite
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
The suite is black-box: every spec spawns the real `sf ps ...` binary. Install the Salesforce CLI first (`npm install -g @salesforce/cli`). The test run links this plugin into `sf` before the first spec and unlinks it at the end.
|
|
569
|
+
|
|
570
|
+
### Test environment
|
|
571
|
+
|
|
572
|
+
Tests take their parameters from the environment, never from a committed file. Copy the template and fill it in:
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
cp .env.example .env
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
| Variable | Required | What it is |
|
|
579
|
+
| --- | --- | --- |
|
|
580
|
+
| `PS_TARGET_ORG` | yes | Username or alias of an already-authenticated org. The `plan`, `apply`, and `export` specs run against it. |
|
|
525
581
|
|
|
526
582
|
## License
|
|
527
583
|
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { Connection } from '@salesforce/core';
|
|
2
|
-
import { ActualAssignment, AssignmentFilter, AssignmentOutcome, AssignmentUpdate, DesiredAssignment,
|
|
2
|
+
import { ActualAssignment, AssignmentFilter, AssignmentOutcome, AssignmentUpdate, DesiredAssignment, OrgTarget, OrgUser, ResolvedAddition, TargetName, TargetRef, Username } from '../core/index.js';
|
|
3
3
|
import { OrgClient } from '../services/adapters/index.js';
|
|
4
4
|
/** Adapter backing OrgClient with a Salesforce Connection. autoFetchQuery pages past 2000 rows. */
|
|
5
5
|
export declare class ConnectionOrgClient implements OrgClient {
|
|
6
6
|
private readonly connection;
|
|
7
7
|
constructor(connection: Connection);
|
|
8
|
-
findUsers(usernames:
|
|
9
|
-
|
|
8
|
+
findUsers(usernames: Username[]): Promise<OrgUser[]>;
|
|
9
|
+
findPermissionSets(names: TargetName[]): Promise<OrgTarget[]>;
|
|
10
|
+
findPermissionSetGroups(names: TargetName[]): Promise<OrgTarget[]>;
|
|
11
|
+
findPermissionSetLicenses(names: TargetName[]): Promise<OrgTarget[]>;
|
|
10
12
|
listAssignments(filter?: AssignmentFilter): Promise<DesiredAssignment[]>;
|
|
11
13
|
private listMemberships;
|
|
12
14
|
private listLicenses;
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
private
|
|
15
|
+
listCurrentAssignments(targets: TargetRef[]): Promise<ActualAssignment[]>;
|
|
16
|
+
private listMembershipAssignments;
|
|
17
|
+
private listLicenseAssignments;
|
|
16
18
|
addAssignments(additions: ResolvedAddition[]): Promise<AssignmentOutcome[]>;
|
|
17
19
|
updateAssignments(updates: AssignmentUpdate[]): Promise<AssignmentOutcome[]>;
|
|
18
20
|
removeAssignments(removals: ActualAssignment[]): Promise<AssignmentOutcome[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-org-client.d.ts","sourceRoot":"","sources":["../../src/adapters/connection-org-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACH,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"connection-org-client.d.ts","sourceRoot":"","sources":["../../src/adapters/connection-org-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACH,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EAEjB,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,QAAQ,EACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AA4R1D,mGAAmG;AACnG,qBAAa,mBAAoB,YAAW,SAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,UAAU;IAE7C,SAAS,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAUpD,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAM7D,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAMlE,yBAAyB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAMpE,eAAe,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;YAkBvE,eAAe;YAoBf,YAAY;IAYb,sBAAsB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAqBxE,yBAAyB;YAezB,sBAAsB;IAWvB,cAAc,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAkB3E,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAkB5E,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;YAmB5E,KAAK;CAItB"}
|