DocsOperations
SCIM provisioning runbook (enterprise)
From docs/04-development/scim-runbook.md in the repository.
On this page
SCIM lets an identity provider (Microsoft Entra ID first) create, update and deactivate an organization’s Softwarden
members and groups. It is enterprise only (capability scim; manage.py set_edition <slug> enterprise), built in
apps/api/ee/scim, and described for integrators in docs/API.md “SCIM provisioning” and docs/PERMISSIONS.md
“Provisioning: SCIM”. The decisions behind it are DECISIONS_NEEDED “2026-11-07, Phase 8, SCIM (enterprise)”.
Connect Entra ID
- In Softwarden, as an Admin of the organization: create a SCIM token (
POST /api/v1/scim/tokens {"name": "Entra ID provisioning", "expires_on": "YYYY-MM-DD"}, at most two years ahead; Admin > Provisioning once the SPA has the page). Copyendpointandsecretfrom the answer: the secret is never shown again. - In the Entra admin center: Enterprise applications > New application > Create your own application (“Integrate
any other application you don’t find in the gallery”). Then Provisioning > Get started, Provisioning Mode
Automatic:
- Tenant URL: the
endpointvalue,https://<your host>/scim/v2/<organization slug>; - Secret Token: the
secretvalue (scim_...). Test Connection: Entra asks for a random user and Softwarden answers with an empty list.
- Tenant URL: the
- Mappings: keep
userName,active,emails[type eq "work"].value,name.givenName,name.familyName,displayNameandexternalIdfor users, anddisplayName,externalIdandmembersfor groups. Remove the others (title, phone numbers, addresses, the enterprise extension): Softwarden accepts and ignores them, but Entra may then re-send them every cycle. - Assign users and groups to the application (Users and groups), set Scope “Sync only assigned users and groups”, and start provisioning.
What happens to accounts:
- a new email gets an account (no password: sign-in by single sign-on, or “forgot password”) and a membership with
scim_default_role(Viewer unless an Admin chose Editor in organization settings); - someone already a member here is linked; someone with an account in another organization is never added without their consent: an invitation is made (Users > Invitations; resend it to get a link to send them), and accepting it links them;
- disabling (or deleting) a user deactivates an account that belongs only to this organization, or removes the membership here when the account belongs elsewhere too; Softwarden never deletes an account. Re-enabling restores the solo account, or invites the shared one again;
- a disabled provisioned user whose membership here was removed keeps its link to the account (
user), so re-enabling it finds the same account rather than a namesake; only deleting the provisioned user (or the account) drops the link; - two provisioned users never share one account or one invitation: the second gets
409 uniqueness(fix the duplicate in Entra); - a deactivated account that belongs to no organization is not reactivated: the provisioned user stays unlinked (
link: noneinGET /api/v1/scim/users). Reactivating it is an operator’s decision on the host (docker compose exec app python manage.py reactivate_account <email>, which refuses an account with any membership and is recorded in the audit log of each organization that names the account); provisioning never does it; - an active account that belongs to no organization is invited, never attached (it keeps its own password or way in; PRE_RELEASE_REVIEW_1.0 F-47), as an account in another organization is;
- Admin > Provisioning’s list says whether a provisioned email was invited (
invited: the account belongs to another organization) or left unlinked (none), so it can show you that an account exists elsewhere on this instance. That is accepted (DECISIONS_NEEDED SCIM (h)): only this organization’s Admins configure its provisioning; - the last active Admin and the IT coordinator are never disabled or removed by provisioning: Entra gets a
409 mutabilityerror for that user (shown in its provisioning logs) and Softwarden’s audit log hasSCIM change refused. Make someone else an Admin (or coordinator) first.
Map roles from groups (optional)
Off by default: groups are stored and change nothing. To turn it on, provision the groups, find their ids
(GET /api/v1/scim/groups) and set PATCH /api/v1/organization {"scim_role_groups": {"admin": "<group id>", "editor": "<group id>"}} (either may be null). From then on a provisioned member’s role is the highest mapped group
they are in, otherwise scim_default_role, recomputed whenever a group or a membership changes, and right away for
everyone when the setting changes. Provisioning never demotes the last active Admin or turns the IT coordinator into a
Viewer (audited Role kept despite SCIM group mapping). Roles of members who were not provisioned never change.
Turn it off with {"scim_role_groups": null}; roles then stay as they are.
Rotate or revoke a token
Create a new token, paste its secret into Entra (Provisioning > Admin credentials) and Test Connection, then revoke
the old one (POST /api/v1/scim/tokens/<id>/revoke). A revoked or expired token gets 401, and Entra puts the job
in quarantine until the credentials are fixed. The token list shows last_used_at (to the minute) and status.
Troubleshooting
| Entra shows | Meaning | Fix |
|---|---|---|
| 401 on Test Connection or in the logs | the token is unknown, revoked or expired, or the Tenant URL names another organization’s slug | check the slug in the URL; create a new token |
| 403 | the organization isn’t enterprise (capability scim) |
manage.py set_edition <slug> enterprise |
| 409 uniqueness | another provisioned user (or group) here already has that userName (displayName), compared case-insensitively | rename one of them in Entra |
| 409 mutability | the user is the last active Admin or the IT coordinator | make another Admin or coordinator first |
| 429 | more than SOFTWARDEN_SCIM_RATE requests a minute for the token (per app process) |
raise the rate; Entra retries |
| 413 | a request body over SOFTWARDEN_SCIM_MAX_BODY (1 MiB) |
raise it (and Caddy’s request_body limit if needed) |
Every change provisioning makes is in the audit log with no person as the actor and “(by SCIM (<token name>))” at the
end of its details (via: scim, scim_client: <token name> in the event’s changes).
Optional owner step: the hosted Microsoft SCIM Validator
The pytest replay (tests/test_scim.py) is the phase’s standard test suite. Microsoft also hosts a validator
(https://scimvalidator.microsoft.com, Learn: Tutorial: validate a SCIM endpoint) that calls the endpoint from the
internet, so it needs Softwarden reachable at a public HTTPS address. That is publishing outside this machine, so it
is the owner’s call and doesn’t block the phase. If the owner wants it:
- Run a throwaway instance with synthetic data only (never the pilot’s database), for example the dev stack with
SOFTWARDEN_BASE_URLset to the public address. - Expose it over HTTPS for the session only, with a tunnel the owner trusts, and stop the tunnel afterwards.
- Make an enterprise organization and a token for it (step 1 above), then in the validator enter the Tenant URL and
the token, choose “Discover my schema” (it reads
/Schemas), and run it. - Revoke the token when done and record the result (pass, or the failing tests) in PHASE_STATUS.md.