š”ļø SFDX Assign Permissions ā
This repository implements a simple GitHub composite action for assigning permission sets, permission set groups and permission set licenses to Salesforce org users from within CI/CD automations. It is the missing step right after a deployment: a freshly created scratch org contains the metadata, but the admin user has none of the permissions the deployed permission sets are supposed to grant.
Usage ā
After installing the SF CLI, authorizing the relevant org and deploying the metadata, the permissions can be assigned as follows:
# Least-privilege token: the workflow only reads the repository and talks to
# Salesforce via the CLI, it never writes back to GitHub.
permissions:
contents: read
jobs:
scratch-org-ci:
name: Scratch Org CI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.3
- name: Authorize DevHub
uses: svierk/sfdx-login@v1.4.3
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
set-default-dev-hub: true
alias: devhub
- name: Create Scratch Org
id: scratch
uses: svierk/sfdx-create-scratch-org@v1.3.2
with:
target-dev-hub: devhub
definition-file: config/project-scratch-def.json
duration-days: 1
- name: Deploy Metadata
uses: svierk/sfdx-deploy@v1.2.2
with:
source-dir: force-app
target-org: ${{ steps.scratch.outputs.username }}
destructive-changes: false # a fresh scratch org has nothing to delete
- name: Assign Permissions
uses: svierk/sfdx-assign-permissions@v1.0.0
with:
permission-sets: 'DreamHouse,CloudHouse'
permission-set-groups: 'Sales_Manager'
target-org: ${{ steps.scratch.outputs.username }}
# ... run tests, etc. ...
- name: Delete Scratch Org
if: always() # release the org even when a previous step failed
uses: svierk/sfdx-delete-scratch-org@v1.1.3
with:
target-org: ${{ steps.scratch.outputs.username }}All four list inputs - permission-sets, permission-set-groups, permission-set-licenses and on-behalf-of - accept a comma-separated string as well as a multi-line block, and the two notations can be mixed freely within one step. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
- name: Assign Permissions
uses: svierk/sfdx-assign-permissions@v1.0.0
with:
permission-set-licenses: 'SalesforceCPQ' # a single value is simply a one-element list
permission-set-groups: 'Sales_Manager,Service_Agent' # comma-separated
permission-sets: | # multi-line block
DreamHouse
CloudHouse
mypkg__Package_Admin
on-behalf-of: 'integration-user@example.com,test-user@example.com'
target-org: awesome-orgSurrounding whitespace and blank lines are ignored, so 'DreamHouse, CloudHouse' is read exactly like the tightly packed 'DreamHouse,CloudHouse'. Every permission in the example above is assigned to every user in on-behalf-of.
The following actions were also used in the example on this page:
- SFDX CLI Setup | Installs the Salesforce CLI and related plugins
- SFDX Login | Handles Salesforce login using a SFDX auth URL or a JSON web token (JWT)
- SFDX Create Scratch Org | Creates the scratch org the permissions are assigned in
- SFDX Deploy | Deploys the metadata that contains the permission sets and groups
- SFDX Delete Scratch Org | Releases the scratch org again at the end of the run
Inputs ā
| Name | Required | Default | Description |
|---|---|---|---|
permission-sets | no | Permission sets to assign, comma-separated or multi-line. Packaged permission sets can be referenced as namespace__PermissionSet. | |
permission-set-groups | no | Permission set groups to assign, comma-separated or multi-line, referenced by their API name (DeveloperName). | |
permission-set-licenses | no | Permission set licenses to assign, comma-separated or multi-line, referenced by their API name (DeveloperName), e.g. SalesforceCPQ. | |
on-behalf-of | no | Org users that receive the permissions, comma-separated or multi-line. Defaults to the user the target org is authorized as - the admin user in a typical scratch org run. | |
target-org | no | Username or alias of the target org. Not required if the default org is set. | |
api-version | no | Override the api version used for api requests, e.g. 59.0. | |
wait | no | 5 | Minutes to wait for permission set groups to finish recalculating before they are assigned. Set to 0 to assign them immediately. |
ignore-existing | no | true | Treat "already assigned" as success, so re-runs stay green. Set to false to let duplicate assignments fail the step. |
step-summary | no | true | Write a result section to the GitHub Actions job summary. Set to false to avoid collisions with a custom workflow summary. |
At least one of permission-sets, permission-set-groups or permission-set-licenses must be provided - an invocation that would assign nothing fails the step instead of silently passing.
Assignment order ā
The three types are assigned in a fixed order, because they build on each other:
- Permission set licenses - a permission set can only grant what the user is licensed for, so the licenses are assigned first.
- Permission sets - assigned via
sf org assign permset. - Permission set groups - assigned last, after the groups have finished recalculating.
Permission set groups ā
The Salesforce CLI has no command for permission set groups. sf org assign permset resolves every name against the PermissionSet object and fails with "Permission set ⦠not found in target org" when handed the API name of a group. This action therefore resolves groups itself: it looks up the PermissionSetGroup record via SOQL and creates the PermissionSetAssignment record through sf data create record.
Two things follow from that, and both are handled by the action:
- Recalculation. A group that was just deployed is recalculated asynchronously and only grants its permissions once its
StatusisUpdated. Assigning it while it is stillUpdatingorOutdatedcan leave the user without the very permissions the pipeline just deployed. The action polls the status for up towaitminutes, aborts early with a clear error if the recalculation ends inFailed, and - if the group is still not ready when the wait elapses - emits a warning and assigns it anyway rather than failing the pipeline on the timeout alone. - User resolution. Because the assignment record is created directly, the assignee has to be resolved to a
Userrecord. Values passed toon-behalf-ofare treated the same way the CLI treats them: a value matching a CLI alias is resolved to its username, anything else is assumed to be a username already. Do not use theAliasfield of theUserobject. Withouton-behalf-of, the user the target org is authorized as is used.
Re-runs and idempotency ā
Assigning a permission a user already has makes Salesforce return a duplicate value found error. The CLI swallows this for permission set licenses, but not for permission sets - which makes any re-run against a long-lived sandbox or dev org fail on the second attempt.
With the default ignore-existing: true the action closes that gap: existing permission set group assignments are detected up front and skipped, and duplicate errors reported by sf org assign permset are downgraded to a notice. Every other failure - a permission set that does not exist in the org, a missing license, an authorization problem - still fails the step. Set ignore-existing: false to get the raw CLI behaviour back.
Security & versioning ā
Every uses: reference in the snippets above is pinned to an exact release tag, e.g. svierk/sfdx-assign-permissions@v1.0.0. Do the same in your own pipelines:
- Never reference a mutable ref such as
@mainor@v1. It runs whatever code sits on that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed. - Good - pin to an exact release tag (
@v1.0.0). Readable, concrete, and bumped through reviewed pull requests. - Strictest - pin to a full-length commit SHA (
@a1b2c3dā¦) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability. Worth it for actions from publishers you don't control. - Enable Dependabot for
github-actionsin the repository that hosts the workflow, so those pins are bumped for you instead of silently ageing.
Beyond pinning, a few rules are worth following in the workflow that calls this action:
- Secrets travel as secrets - hand the SFDX Auth URL straight into the
sfdx-urlinput of sfdx-login. Never interpolate a secret into arun:script via${{ ... }}- that allows command injection and can leak the value into the log. This action follows the same rule internally: every input is exposed to the shell as an environment variable and referenced as"$PERMISSION_SETS","$TARGET_ORG"and so on. Values that end up in a SOQL query are escaped before they are interpolated. - Least-privilege
GITHUB_TOKEN- declare apermissions:block granting only what the job needs. Assigning permissions needs no write access at all, socontents: readis enough. persist-credentials: falseon checkout - the token is not written to.git/config, so later steps (SF CLI, third-party actions) cannot reuse it.- Validate pull requests with
pull_request, neverpull_request_target- the latter runs with the base repository's secrets, which would let a fork grant itself permissions in your org.
Resolving the default assignee reads the org details, whose payload contains a live access token. The token is registered as a masked secret before anything is written to the log.
References ā
The assignment options supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
- org assign permset
- org assign permsetlicense
- data create record (used for permission set groups)
- data query
Background on the objects involved:
Releases ā
Latest release notes can be found on the release page.
License ā
The scripts and documentation in this project are released under the MIT License.
ā”ļø Full source & releases: svierk/sfdx-assign-permissions
