SCIM 2.0 provisioning
Zeus OS exposes a SCIM 2.0 (RFC 7643 / RFC 7644) server so an identity provider can create, update, deactivate
and delete users and groups. It is mounted at /scim/v2 (not under /api/v1) and is disabled by
default: every path answers 404 until it is configured.
Implementation: web/src/scim.rs (protocol and semantics), web/src/routes/scim.rs (HTTP layer and bearer
middleware), storage in the local auth user store (ZEUS_OS_LOCAL_USERS_FILE) plus scim-groups.json in
ZEUS_OS_DATA_DIR.
Enabling
| Variable | Purpose |
|---|---|
ZEUS_OS_SCIM_TOKEN |
Static provisioning bearer token. Minimum 32 characters; a shorter value is rejected (SCIM stays disabled and an error is logged). Generate one with openssl rand -hex 32. |
ZEUS_OS_SCIM_API_TOKEN_AUTH |
1/true: also accept a Zeus API token (v9s_...) that belongs to an admin and carries the admin scope. Off by default. |
ZEUS_OS_SCIM |
off disables SCIM even if a token is configured (kill-switch). |
ZEUS_OS_SCIM_GROUP_ADMIN |
Comma-separated group displayNames that grant the platform-wide admin role. Empty by default: no group grants admin unless you list it here. |
ZEUS_OS_SCIM_GROUP_EDITOR |
Group names granting editor (default zeus-os-editors). |
ZEUS_OS_SCIM_GROUP_VIEWER |
Group names granting viewer (default zeus-os-viewers). |
ZEUS_OS_SCIM_TENANT_GROUP_PREFIX |
Prefix of tenant groups (default zeus-os-tenant-). |
The SCIM base URL is https://<zeus-host>/scim/v2.
Authentication
Every request needs Authorization: Bearer <token>. SCIM has its own middleware and never accepts a
normal Zeus user session (Authorization header, cookie, query string or WebSocket sub-protocol): a valid admin
session gets 401. The static token is compared in constant time (SHA-256 digests). Responses:
404 = SCIM not enabled, 401 = missing/wrong token, 403 = an API token that is not an admin token with the
admin scope.
The token is a powerful credential: whoever holds it can create users and place them in groups. Treat it like an
admin password, rotate it by changing the environment variable and restarting, and prefer a network policy
that only allows your IdP to reach /scim/v2. If you list a group in ZEUS_OS_SCIM_GROUP_ADMIN, the token
holder can create platform admins by adding a user to that group; only do so deliberately.
What is (and is not) provisioned
- SCIM manages local accounts in the Zeus user store. Only accounts created through SCIM are visible
to or modifiable by SCIM. The built-in break-glass
adminand any user created manually (Settings or/api/v1/auth/users) are unreachable: they are not listed,GET/PUT/PATCH/DELETE /Users/{id}returns404for them, they cannot be group members, and their user names are reserved. Creating a SCIM user whoseuserNamematches an existing account (case-insensitively) returns409;admin,api_keyanddevreturn403. - Passwords are never accepted or returned. A
passwordattribute (POST/PUT/PATCH) is rejected with400 invalidValue.changePasswordis reported as unsupported. A provisioned user has no local password, so local login (/auth/login) fails for them; an administrator can still set a local password with the existing user-management API, after which they can log in locally as well. Sign-in for SCIM users is intended to be via your SSO. - JIT users are out of scope. Users that OIDC/SAML/LDAP create on first login are not SCIM users and are not
affected. Note that, in this release, SSO session creation does not consult the SCIM
activeflag: SCIM deactivation revokes the user's existing Zeus sessions, OIDC sessions and API tokens (by user name) and blocks local login, but an IdP-side deactivation is what stops new SSO logins. A helper (local_auth::is_scim_deactivated) exists for wiring a deny check into the SSO login paths. - A newly provisioned user is a
vieweruntil a mapped group says otherwise.
Users
Supported attributes: userName (1-128 ASCII characters: letters, digits, _ - . @ +, starting with a letter or
digit; e-mail style names are fine), externalId, displayName, name.givenName, name.familyName, emails
(one address is stored: the primary one, else the first), active. groups is read-only. Other core
attributes (for example phoneNumbers, title, roles) and the enterprise extension are accepted and ignored.
roles are ignored on purpose: access comes only from groups. Some IdPs send booleans as the strings "True" /
"False"; both forms are accepted.
active: false (via PUT or PATCH):
- marks the user inactive (local login refused),
- revokes all their Zeus sessions and OIDC sessions,
- revokes their API tokens.
DELETE removes the user, removes them from all groups and revokes the same credentials. A PUT that omits
active leaves it unchanged (it cannot silently re-enable a deactivated user). Renaming (userName change)
revokes the credentials issued under the old name.
Groups and role / tenant mapping
Groups are stored by Zeus; membership is by SCIM user id. A group's displayName (case-insensitive, exact
match) decides what it grants:
displayName |
Effect |
|---|---|
listed in ZEUS_OS_SCIM_GROUP_ADMIN |
member gets the admin role. As for local admins this is platform-wide (no tenant list). Nothing is listed by default. |
listed in ZEUS_OS_SCIM_GROUP_EDITOR (default zeus-os-editors) |
editor |
listed in ZEUS_OS_SCIM_GROUP_VIEWER (default zeus-os-viewers) |
viewer |
<prefix><tenant-id> (default zeus-os-tenant-acme) |
member belongs to tenant acme (namespace label zeus-os.io/tenant). <tenant-id> must be 1-63 characters of A-Z a-z 0-9 - _ . |
| anything else | stored, but grants nothing |
A user's effective role is the highest role among their groups (default viewer); tenants are the union of
their tenant groups. A non-admin with no tenant group keeps the default scope (a tenant named after their user
name). Whenever membership, a group name, or the mapping changes, the access of every SCIM-managed user is
recomputed on the next SCIM write; users whose role/tenants changed have their sessions revoked (a session is a
snapshot of the old role) and an audit entry is written. Deleting a group demotes its members accordingly.
Nested groups are not supported; group members must be SCIM user ids that exist.
Filtering, pagination, PATCH
GET /UsersandGET /Groupsacceptfilter,startIndex(1-based),count(default 100, max 200;count=0returns onlytotalResults).sortBy,attributesandexcludedAttributesare ignored.- Filter operators:
eq ne co sw ew pr, combined withand,or,not(...)and parentheses.gt/ge/lt/leare rejected (400 invalidFilter). Filterable attributes: usersid externalId userName displayName active emails.value name.givenName name.familyName; groupsid externalId displayName members.value.idandexternalIdcompare case-exactly, other strings case-insensitively. PATCHsupportsadd,replace,remove, with or withoutpath. Paths may be schema-qualified (urn:...:User:userName), dotted (name.givenName) or use a value filter (emails[type eq "work"].value,members[value eq "<id>"]). On groups,memberssupports add, replace, remove-all, remove by filter and remove by value list. A PATCH is atomic: if any operation fails nothing is applied.- Not supported (advertised as such in
/ServiceProviderConfig): bulk, sort, ETags, change-password. - Errors use the SCIM error schema with
application/scim+json.
Audit
Every mutation is written through the auth audit log (auth-audit.jsonl, SIEM export) with the caller as subject
(scim for the static token, scim:<subject> for an API token) and only the userName / group displayName
as detail: scim_user_created, scim_user_updated, scim_user_deactivated, scim_user_reactivated,
scim_user_deleted, scim_group_created, scim_group_updated, scim_group_members_changed,
scim_group_deleted, scim_access_changed (role/tenant changed, with the new role). Tokens and e-mail addresses are never logged. Failed
authentication is logged to the server log only (not the audit file) so unauthenticated callers cannot grow it.
Configuring an IdP
Generic SCIM 2.0 client settings: base URL https://<zeus-host>/scim/v2, authentication "HTTP header / bearer
token" with the value of ZEUS_OS_SCIM_TOKEN, unique identifier field userName. This server has been tested
against its own automated test-suite only, not against live Okta or Entra ID tenants, and this page does not
reproduce vendor UI steps or vendor behaviour claims. What to check in whichever IdP you use:
- Okta (SCIM 2.0 app integration) and Microsoft Entra ID (enterprise application provisioning) both offer a generic SCIM 2.0 mode with bearer-token authentication: point it at the base URL and paste the token.
- Enable create / update / deactivate actions; leave password sync off (a password is rejected with
400). - Map
userNameto a value that satisfies theuserNamerules above (for example the sign-in name or mail). - Push groups whose names match the mapping table (
zeus-os-editors,zeus-os-tenant-acme, ...). Groups with other names are stored but grant nothing. - Zeus accepts the common client request shapes:
filter=userName eq "..."/externalId eq "..."lookups, PATCH with or withoutpath, string booleans ("True"/"False") foractive, member removal bymembers[value eq "<id>"]or by value list, andPUTof a whole resource. - Use the IdP's "test connection" or provisioning log together with
GET /ServiceProviderConfigwhen debugging.
If provisioning fails, the SCIM error body in the IdP's provisioning log states the reason (scimType and
detail); a 409 means the user name or externalId already exists, 403 on a user name means it is reserved.
Operational notes
- SCIM requires the local user store (
ZEUS_OS_LOCAL_USERS_FILE, defaultdata/zeus-os-users.json) to be on persistent storage; if Zeus runs in a mode that does not otherwise initialise it, the first SCIM request creates it (and its break-glassadmin, with the usual generated orZEUS_OS_LOCAL_ADMIN_PASSWORDpassword). - Write operations are serialised in-process; multi-replica deployments must share the data directory (the same requirement as the rest of the local auth store).
- Manual role edits made through
/api/v1/auth/usersto a SCIM-managed user are overwritten by the group mapping the next time SCIM changes anything. ZEUS_OS_SCIM_*variables are read per request, so the kill-switch and mapping changes need no restart when set through the process environment; the static token change also takes effect immediately.- SCIM requests are not subject to the per-actor API rate limiter; put a reverse-proxy limit in front if the endpoint is internet-reachable.