Skip to content

Commit 93fd43b

Browse files
waleedlatif1claude
andcommitted
feat(scim): SCIM 2.0 directory provisioning with group-to-access projection
Adds a SCIM 2.0 service provider so an organization's identity provider (Okta, Microsoft Entra ID, OneLogin, JumpCloud) can create, update, deactivate, and remove members, and map pushed groups onto permission groups, workspace access, and the organization admin role. Protocol surface (`/api/scim/v2`) - Users and Groups: list with `eq`/`and` filters and paging, get, create, replace, patch, delete; discovery documents for ServiceProviderConfig, ResourceTypes, and Schemas. - A dedicated `defineScimRoute` builder: bearer authentication to a new `scim_connection` principal, per-connection rate limit, RFC 7644 error envelope, `application/scim+json`, 415 on wrong media type. - Tolerances for what providers actually send: Entra's capitalized ops and string booleans, one-element arrays, path-less dotted-key replaces, and filtered email paths that create their target; Okta's path-less `replace {active:false}` and filtered member removal. Identity and safety - Never links by unverified email: resolution is tombstone by externalId, then verified-domain email within this organization, then create. - Every create and email change is refused outside the organization's verified domains, closing the SCIM account-takeover shape. - Deactivation is a new reversible `user.suspendedAt` state, enforced at session creation and both API-key auth paths. It is deliberately not `banned`, which archives owned workspaces and cannot be undone. - The organization owner cannot be deprovisioned; seats are validated with the same policy as SSO admission. Projection - Every grant SCIM makes is recorded, so withdrawing group access touches only what the directory granted and never a manual grant. - Permission groups gain `membershipMode: 'explicit'` so a directory-managed group governs nobody when empty instead of widening to everyone. - An hourly reconcile sweep re-applies mappings idempotently. Shared primitives extracted from routes so UI and SCIM share one implementation: per-user session revocation (with security-version bump), personal API-key revocation, suspend/unsuspend, member role change, workspace access grant/revoke, permission-group add/remove member. The member role route now uses the role-change primitive under the org lock. Admin surface under `/api/organizations/[id]/scim`: connection settings, credential issue/revoke (two active for rotation, digest-only storage), group mappings, activity log, on-demand reconcile. Migration 0323 is expand-only: nine new tables plus nullable/defaulted columns on `user` and `permission_group`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVf3fLVj7iWzED7L2wQvhG
1 parent 776f148 commit 93fd43b

98 files changed

Lines changed: 32321 additions & 72 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/platform/enterprise/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"index",
55
"self-hosted",
66
"sso",
7+
"scim",
78
"verified-domains",
89
"session-policies",
910
"access-control",
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
title: Directory provisioning (SCIM)
3+
description: Create, update, and deactivate Sim members automatically from your identity provider
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
import { Step, Steps } from 'fumadocs-ui/components/steps'
8+
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
9+
10+
Directory provisioning connects your identity provider to Sim over SCIM 2.0. Your provider creates members when someone joins, updates them when their details change, and deactivates them the moment they leave — without anyone touching Sim.
11+
12+
It pairs with [SSO](/platform/enterprise/sso). SSO proves who someone is when they sign in. Directory provisioning decides who exists and what they can reach, before and after that.
13+
14+
<Callout type="info">
15+
Enterprise plans. Requires at least one [verified domain](/platform/enterprise/verified-domains) for your organization.
16+
</Callout>
17+
18+
## What it does
19+
20+
| Your provider does this | Sim does this |
21+
| --- | --- |
22+
| Assigns a person to the Sim app | Creates their account and adds them to your organization as a Member |
23+
| Updates their name or email | Updates the Sim account, and ends their sessions if the address changed |
24+
| Deactivates them | Blocks sign-in, stops their API keys, and withdraws directory-granted access. Everything they own is left untouched |
25+
| Reactivates them | Restores access exactly as it was |
26+
| Removes them from the app | Removes their organization membership and reassigns what they owned |
27+
| Adds them to a group | Grants whatever that group maps to |
28+
29+
Deactivation is reversible and never destructive. Someone on leave keeps their workflows, their credentials, and their workspace history; they simply cannot sign in.
30+
31+
## Turn it on
32+
33+
<Steps>
34+
35+
<Step>
36+
### Verify your domain
37+
38+
Sim only provisions people whose email is in a domain your organization has verified. See [Verified domains](/platform/enterprise/verified-domains).
39+
40+
This is what stops another tenant's directory from claiming an address it does not own.
41+
</Step>
42+
43+
<Step>
44+
### Enable directory provisioning
45+
46+
In **Settings → SSO → Directory provisioning**, turn it on. Sim shows your SCIM base URL:
47+
48+
```
49+
https://<your-sim-domain>/api/scim/v2
50+
```
51+
</Step>
52+
53+
<Step>
54+
### Issue a credential
55+
56+
Select **Issue credential**. The token appears once — copy it straight into your provider.
57+
58+
Two credentials can be active at a time, so you can rotate without downtime: issue the new one, update your provider, confirm a sync succeeds, then revoke the old one.
59+
</Step>
60+
61+
<Step>
62+
### Configure your provider
63+
64+
<Tabs items={['Okta', 'Microsoft Entra ID', 'OneLogin', 'JumpCloud']}>
65+
<Tab value="Okta">
66+
67+
In your Okta app, open **Provisioning → Integration** and select **Configure API Integration**.
68+
69+
- **SCIM connector base URL**: `https://<your-sim-domain>/api/scim/v2`
70+
- **Unique identifier field for users**: `userName`
71+
- **Supported provisioning actions**: Push New Users, Push Profile Updates, Push Groups
72+
- **Authentication Mode**: HTTP Header, with your Sim credential as the token
73+
74+
Select **Test API Credentials**, then save. Under **Provisioning → To App**, enable Create Users, Update User Attributes, and Deactivate Users.
75+
76+
Okta never deletes users over SCIM. Unassigning someone, or deactivating them in Okta, sends a deactivation — which Sim applies as a suspension.
77+
78+
</Tab>
79+
<Tab value="Microsoft Entra ID">
80+
81+
In your enterprise application, open **Provisioning** and set Provisioning Mode to **Automatic**.
82+
83+
- **Tenant URL**: `https://<your-sim-domain>/api/scim/v2`
84+
- **Secret Token**: your Sim credential
85+
86+
Select **Test Connection**, then save and start provisioning.
87+
88+
Entra runs an initial cycle over everyone in scope, then incremental cycles roughly every 40 minutes. Removing someone from the app sends a deactivation; a permanent delete in Entra sends a removal about 30 days later.
89+
90+
</Tab>
91+
<Tab value="OneLogin">
92+
93+
Add a **SCIM Provisioner with SAML** app.
94+
95+
- **SCIM Base URL**: `https://<your-sim-domain>/api/scim/v2`
96+
- **SCIM Bearer Token**: your Sim credential
97+
98+
Enable provisioning and choose what happens when a user is removed. Suspend maps to a Sim suspension; Delete removes their membership.
99+
100+
</Tab>
101+
<Tab value="JumpCloud">
102+
103+
Add a **Custom SCIM** identity management integration.
104+
105+
- **Base URL**: `https://<your-sim-domain>/api/scim/v2`
106+
- **Token Key**: your Sim credential
107+
108+
Enable group sync if you plan to map groups.
109+
110+
</Tab>
111+
</Tabs>
112+
</Step>
113+
114+
<Step>
115+
### Map your groups
116+
117+
Groups mean nothing to Sim until you say what they stand for. In **Settings → SSO → Directory provisioning → Group mappings**, point each pushed group at one or more of:
118+
119+
- a **permission group**, which governs models, integrations, and capabilities
120+
- a **workspace**, at Read, Write, or Admin
121+
- the **organization admin role**
122+
123+
A group can carry several mappings. When two groups grant the same workspace at different levels, the stronger one wins.
124+
125+
</Step>
126+
127+
</Steps>
128+
129+
## How access is withdrawn
130+
131+
Sim records every grant it makes on your behalf. When someone leaves a group, only what the directory granted is taken back — access a workspace administrator granted by hand stays.
132+
133+
The one exception is **managed membership locking**, which is on by default. With it on, the directory is the source of truth: Sim refuses invitations, role changes, and manual grants for provisioned members, because the next sync would revert them anyway. Turn it off if you want to layer manual access on top of directory access.
134+
135+
## Provisioning and SSO together
136+
137+
A member the directory created can sign in with SSO immediately; the two resolve to the same account through your verified domain.
138+
139+
If you want the directory to be the only way in, enable **Disable just-in-time provisioning** in the connection settings. Sim then refuses to create membership for someone signing in who was never provisioned.
140+
141+
## Watching a sync
142+
143+
**Settings → SSO → Directory provisioning → Activity** lists recent requests with their status and, for a failure, what was wrong. Providers report a failed cycle without saying what they sent, so this is usually the fastest way to see the cause.
144+
145+
Sim also re-applies every group mapping on a schedule, so drift cannot persist. You can run it on demand with **Reconcile now**.
146+
147+
## Reference
148+
149+
- Base URL: `https://<your-sim-domain>/api/scim/v2`
150+
- Authentication: `Authorization: Bearer <credential>`
151+
- Resources: `/Users`, `/Groups`, plus `/ServiceProviderConfig`, `/ResourceTypes`, and `/Schemas`
152+
- Filters: `eq`, joined with `and`, on `userName`, `externalId`, `emails.value`, and `displayName`
153+
- Page size: up to 100 per request
154+
155+
<FAQ items={[
156+
{
157+
question: "What happens to someone's workflows when they are deactivated?",
158+
answer: "Nothing. A deactivation blocks sign-in and stops their API keys, and leaves every workspace, workflow, and credential they own exactly as it was. Reactivating them restores access. Only a removal — which your provider sends explicitly — reassigns what they owned."
159+
},
160+
{
161+
question: "Can the directory provision someone outside our verified domains?",
162+
answer: "No. Sim refuses to create or move an account to an address whose domain your organization has not verified. This is what prevents one tenant's directory from claiming an account it does not own."
163+
},
164+
{
165+
question: "What if the email already belongs to a Sim account?",
166+
answer: "If the account is in your organization, or in none, Sim attaches to it. If it belongs to a different organization, Sim returns a conflict — a Sim account can belong to only one organization, so someone has to be removed there first."
167+
},
168+
{
169+
question: "Can the directory remove our organization owner?",
170+
answer: "No. Sim refuses, because it would leave nobody able to administer billing or transfer ownership. Transfer ownership in Sim first."
171+
},
172+
{
173+
question: "What happens when we run out of seats?",
174+
answer: "Sim refuses the new member with a message your provider shows to its administrator. Add seats in Sim and the next sync will pick them up. Team plans grow their seat count automatically; fixed-seat plans do not."
175+
},
176+
{
177+
question: "Does a deactivation sign someone out immediately?",
178+
answer: "Their sessions are deleted at once and their API keys stop authenticating immediately. A browser tab holding a cached session cookie can survive up to five minutes; enabling a session policy shortens that window."
179+
},
180+
{
181+
question: "Can we still invite people manually?",
182+
answer: "Only with managed membership locking turned off. With it on, Sim refuses manual changes for provisioned members, because the directory would revert them on its next sync."
183+
}
184+
]} />

apps/docs/content/docs/platform/enterprise/sso.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ With **Automatic** provisioning, no invitation is required for organization memb
278278
Sign-in must start from Sim. Launching from your identity provider's app portal (Microsoft's **My Apps**, Okta's dashboard tile) sends an unsolicited assertion, which Sim rejects. This is deliberate — accepting them would let anyone replay an assertion into your tenant — but it means an IdP-initiated test fails even when the configuration is correct.
279279
</Callout>
280280

281-
SSO provisioning creates internal organization members but does not grant workspace access. External workspace members are different: they are invited to a specific workspace without joining your organization or consuming one of your seats. Existing invitations and external access take precedence over automatic provisioning so their intended role and workspace grants are preserved.
281+
SSO provisioning creates internal organization members but does not grant workspace access. To grant workspace access from your identity provider, use [directory provisioning](/platform/enterprise/scim) and map a pushed group to a workspace. External workspace members are different: they are invited to a specific workspace without joining your organization or consuming one of your seats. Existing invitations and external access take precedence over automatic provisioning so their intended role and workspace grants are preserved.
282282

283283
<Callout type="info">
284284
Password-based login remains available. Forcing all organization members to use SSO exclusively is not yet supported.
@@ -305,7 +305,7 @@ SSO provisioning creates internal organization members but does not grant worksp
305305
},
306306
{
307307
question: "Does disabling someone in the identity provider remove their Sim access?",
308-
answer: "No. Disabling the IdP account blocks future SSO authentication, but Sim does not currently receive SCIM deprovisioning or IdP logout events to remove membership or revoke active Sim sessions. Remove or suspend the user in Sim as part of offboarding."
308+
answer: "With [directory provisioning](/platform/enterprise/scim) connected, yes: your identity provider sends the deactivation, and Sim blocks sign-in, stops their API keys, and withdraws directory-granted access while leaving everything they own intact. With SSO alone, disabling the IdP account only blocks future SSO authentication — remove or suspend the user in Sim as part of offboarding."
309309
},
310310
{
311311
question: "Can I still use email/password login after enabling SSO?",

apps/docs/openapi-v2-billing.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@
453453
"description": "Human-readable explanation of the error."
454454
},
455455
"details": {
456-
"description": "Structured error details. On a `403` whose cause a caller can act on, this carries a `code` from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the caller’s kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `ORGANIZATION_PLAN_REQUIRED` — The organization has no active organization subscription (Pro for Teams, Max for Teams, or Enterprise).\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `SECRET_ADMIN_ACCESS_REQUIRED` — The caller can write in the workspace but is not an admin of this secret. Ask a workspace admin, or someone holding admin on the secret, to grant access or set the value.\n- `WORKSPACE_RESOURCE_LIMIT_REACHED` — The workspace already holds the maximum number of resources of this kind. Delete one, or contact Sim to raise the limit; the message names the ceiling.\n- `PUBLIC_SHARING_NOT_ALLOWED` — The workspace's organization does not permit sharing this resource publicly. An organization admin controls the policy.\n- `CREDENTIAL_ADMIN_ACCESS_REQUIRED` — The caller can reach the workspace but cannot administer this credential.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\n- `WORKSPACE_PLAN_CAPABILITY_REQUIRED` — The workspace's plan does not include a capability this request depends on. The message names the capability; upgrading the workspace's plan is the remedy.\n- `CHAT_AUTH_MODE_NOT_PERMITTED` — The workspace's permission group does not allow the chat authentication mode the request selected. A mode already saved on the deployment may still be re-saved; changing to a disallowed one cannot.\n- `CONNECTOR_MANAGED_RESOURCE_READ_ONLY` — This resource is managed by a knowledge base connector and cannot be edited directly. Change it at the source and re-sync, or exclude the document from the connector.\n- `PERMISSION_GROUP_CAPABILITY_BLOCKED` — The caller's permission group does not allow this capability. The message names it; an organization admin controls the group.\n- `INTEGRATION_NOT_ALLOWED` — The integration this request names is outside the workspace's allowed set. An organization admin controls the permission group's integration allowlist, and a self-hosted deployment can narrow it further with ALLOWED_INTEGRATIONS."
456+
"description": "Structured error details. On a `403` whose cause a caller can act on, this carries a `code` from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the caller’s kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `ORGANIZATION_PLAN_REQUIRED` — The organization has no active organization subscription (Pro for Teams, Max for Teams, or Enterprise).\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `SECRET_ADMIN_ACCESS_REQUIRED` — The caller can write in the workspace but is not an admin of this secret. Ask a workspace admin, or someone holding admin on the secret, to grant access or set the value.\n- `WORKSPACE_RESOURCE_LIMIT_REACHED` — The workspace already holds the maximum number of resources of this kind. Delete one, or contact Sim to raise the limit; the message names the ceiling.\n- `PUBLIC_SHARING_NOT_ALLOWED` — The workspace's organization does not permit sharing this resource publicly. An organization admin controls the policy.\n- `CREDENTIAL_ADMIN_ACCESS_REQUIRED` — The caller can reach the workspace but cannot administer this credential.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\n- `WORKSPACE_PLAN_CAPABILITY_REQUIRED` — The workspace's plan does not include a capability this request depends on. The message names the capability; upgrading the workspace's plan is the remedy.\n- `CHAT_AUTH_MODE_NOT_PERMITTED` — The workspace's permission group does not allow the chat authentication mode the request selected. A mode already saved on the deployment may still be re-saved; changing to a disallowed one cannot.\n- `CONNECTOR_MANAGED_RESOURCE_READ_ONLY` — This resource is managed by a knowledge base connector and cannot be edited directly. Change it at the source and re-sync, or exclude the document from the connector.\n- `PERMISSION_GROUP_CAPABILITY_BLOCKED` — The caller's permission group does not allow this capability. The message names it; an organization admin controls the group.\n- `INTEGRATION_NOT_ALLOWED` — The integration this request names is outside the workspace's allowed set. An organization admin controls the permission group's integration allowlist, and a self-hosted deployment can narrow it further with ALLOWED_INTEGRATIONS.\n- `SCIM_MANAGED_MEMBERSHIP` — This member is provisioned by the organization's identity provider, which the organization has made the source of truth for membership. Make the change in the identity provider, or turn off managed-membership locking in the organization's SCIM settings."
457457
}
458458
},
459459
"required": ["code", "message"],

0 commit comments

Comments
 (0)