|
| 1 | +-- Enterprise SSO + SCIM provisioning (Phase 8, slice 2): the mapping |
| 2 | +-- between SCIM 2.0 /Users resources and platform users. |
| 3 | +-- |
| 4 | +-- Slice 1 created sso_connections + scim_tokens (the CONFIG and the |
| 5 | +-- bearer CREDENTIAL). This slice is the runtime: the SAML/OIDC login |
| 6 | +-- handshake and the SCIM /Users endpoints that CREATE, UPDATE and |
| 7 | +-- DEPROVISION platform users on behalf of an identity provider. |
| 8 | +-- |
| 9 | +-- scim_identities is the join between the two worlds. A SCIM-managed |
| 10 | +-- user is an ordinary users row (it logs in through the normal portal |
| 11 | +-- session exactly like any other account) plus one scim_identities row |
| 12 | +-- that remembers "this user is provisioned from a directory" and holds |
| 13 | +-- the two facts that only the directory knows: |
| 14 | +-- |
| 15 | +-- - external_id — the IdP's own identifier for the person (Okta's |
| 16 | +-- "externalId", Entra's). Nullable: not every provider sends one. |
| 17 | +-- NOT unique here — the same value may be minted by two different |
| 18 | +-- directories, and this table is not scoped to a connection (see |
| 19 | +-- the note on user_id below). Indexed because a client may filter |
| 20 | +-- on it. |
| 21 | +-- |
| 22 | +-- - active — the SCIM "active" attribute. SCIM DELETE is a DEACTIVATE, |
| 23 | +-- not a row removal: the account is preserved (its licences, orders |
| 24 | +-- and audit trail stay intact) and this flag is what the SSO login |
| 25 | +-- path consults before letting the person in. A deactivated identity |
| 26 | +-- therefore blocks sign-in without destroying anything. |
| 27 | +-- |
| 28 | +-- user_id is UNIQUE and a FK to users(id) ON DELETE CASCADE: |
| 29 | +-- |
| 30 | +-- - UNIQUE because one platform user is provisioned at most once. The |
| 31 | +-- SCIM userName IS the user's email, and users.email is already |
| 32 | +-- unique — so "one identity per user" is the same cardinality rule, |
| 33 | +-- expressed as a database constraint so a retry that races past the |
| 34 | +-- handler's pre-check still cannot create a second row. |
| 35 | +-- |
| 36 | +-- - CASCADE so deleting a platform user (admin hard-delete) takes its |
| 37 | +-- provisioning bookkeeping with it instead of orphaning a row no |
| 38 | +-- endpoint can ever resolve. |
| 39 | +-- |
| 40 | +-- The rows here deliberately do NOT carry the user's name or email: those |
| 41 | +-- live on the users row (single source of truth). GivenName/FamilyName in |
| 42 | +-- the SCIM payload are derived from users.name on the way out and folded |
| 43 | +-- back into users.name on the way in (see handler/scim.go), so a name |
| 44 | +-- change via SCIM is a normal user-profile update, not a second copy that |
| 45 | +-- can drift. |
| 46 | +CREATE TABLE IF NOT EXISTS scim_identities ( |
| 47 | + id TEXT PRIMARY KEY, |
| 48 | + -- The IdP's identifier for the user. Nullable; indexed for the |
| 49 | + -- externalId filter. Not unique — see the header note. |
| 50 | + external_id TEXT, |
| 51 | + -- The provisioned platform user. UNIQUE (one identity per user) and |
| 52 | + -- CASCADE on user deletion. |
| 53 | + user_id TEXT NOT NULL |
| 54 | + CONSTRAINT scim_identities_user_id_key UNIQUE |
| 55 | + REFERENCES users(id) ON DELETE CASCADE, |
| 56 | + -- SCIM "active". false = deactivated (SCIM DELETE); the SSO login |
| 57 | + -- path refuses a deactivated identity. |
| 58 | + active BOOLEAN NOT NULL DEFAULT true, |
| 59 | + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), |
| 60 | + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() |
| 61 | +); |
| 62 | + |
| 63 | +-- A client may filter /Users by externalId; keep that lookup indexed. |
| 64 | +CREATE INDEX IF NOT EXISTS scim_identities_external_id_idx |
| 65 | + ON scim_identities (external_id); |
0 commit comments