Skip to content

Commit f9a435a

Browse files
committed
feat(scim): add scim_users and scim_tokens tables
1 parent 1adb274 commit f9a435a

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* auth_migration: 20260821000000 */
2+
-- SCIM Users provisioned into one SSO provider. The resource is stored as a
3+
-- document; queryable columns are generated from it so the two cannot drift.
4+
create table if not exists {{ index .Options "Namespace" }}.scim_users (
5+
id uuid not null default gen_random_uuid(),
6+
sso_provider_id uuid not null references {{ index .Options "Namespace" }}.sso_providers (id) on delete cascade,
7+
user_id uuid references {{ index .Options "Namespace" }}.users (id) on delete set null,
8+
resource jsonb not null,
9+
user_name text not null generated always as (resource->>'userName') stored,
10+
external_id text generated always as (resource->>'externalId') stored,
11+
active boolean not null generated always as (coalesce((resource->>'active')::boolean, true)) stored,
12+
created_at timestamptz not null default now(),
13+
updated_at timestamptz not null default now(),
14+
deleted_at timestamptz,
15+
constraint scim_users_pkey primary key (id)
16+
);
17+
18+
/* auth_migration: 20260821000000 */
19+
-- userName is unique within a provider, case-folded, excluding soft-deleted rows.
20+
create unique index if not exists scim_users_user_name_key
21+
on {{ index .Options "Namespace" }}.scim_users (sso_provider_id, lower(user_name collate "C"))
22+
where deleted_at is null;
23+
24+
/* auth_migration: 20260821000000 */
25+
-- externalId is unique within a provider when set; nulls are unconstrained.
26+
create unique index if not exists scim_users_external_id_key
27+
on {{ index .Options "Namespace" }}.scim_users (sso_provider_id, external_id)
28+
where external_id is not null and deleted_at is null;
29+
30+
/* auth_migration: 20260821000000 */
31+
-- Links a SCIM user to its auth.users row; not partial, so an ON DELETE SET
32+
-- NULL from auth.users can find soft-deleted rows too.
33+
create index if not exists scim_users_user_id_idx
34+
on {{ index .Options "Namespace" }}.scim_users (user_id);
35+
36+
/* auth_migration: 20260821000000 */
37+
-- Sort indexes break ties on id for a total order; user_name uses collate "C"
38+
-- so ordering does not depend on the database's collation.
39+
create index if not exists scim_users_id_idx
40+
on {{ index .Options "Namespace" }}.scim_users (sso_provider_id, id)
41+
where deleted_at is null;
42+
43+
/* auth_migration: 20260821000000 */
44+
create index if not exists scim_users_user_name_idx
45+
on {{ index .Options "Namespace" }}.scim_users (sso_provider_id, lower(user_name collate "C"), id)
46+
where deleted_at is null;
47+
48+
/* auth_migration: 20260821000000 */
49+
create index if not exists scim_users_created_at_idx
50+
on {{ index .Options "Namespace" }}.scim_users (sso_provider_id, created_at, id)
51+
where deleted_at is null;
52+
53+
/* auth_migration: 20260821000000 */
54+
create index if not exists scim_users_updated_at_idx
55+
on {{ index .Options "Namespace" }}.scim_users (sso_provider_id, updated_at, id)
56+
where deleted_at is null;
57+
58+
/* auth_migration: 20260821000000 */
59+
create index if not exists scim_users_sso_provider_id_idx
60+
on {{ index .Options "Namespace" }}.scim_users (sso_provider_id);
61+
62+
/* auth_migration: 20260821000000 */
63+
-- Supports purging soft-deleted rows.
64+
create index if not exists scim_users_deleted_at_idx
65+
on {{ index .Options "Namespace" }}.scim_users (deleted_at);
66+
67+
/* auth_migration: 20260821000000 */
68+
alter table {{ index .Options "Namespace" }}.scim_users enable row level security;
69+
/* auth_migration: 20260821000000 */
70+
grant select on {{ index .Options "Namespace" }}.scim_users to postgres with grant option;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* auth_migration: 20260821010000 */
2+
-- Bearer tokens authorising SCIM requests for one SSO provider. Only the
3+
-- SHA-256 digest is stored; a token carries 160 bits, so the digest needs no salt.
4+
create table if not exists {{ index .Options "Namespace" }}.scim_tokens (
5+
id uuid not null default gen_random_uuid(),
6+
sso_provider_id uuid not null references {{ index .Options "Namespace" }}.sso_providers (id) on delete cascade,
7+
token_hash text not null,
8+
prefix text not null,
9+
created_at timestamptz not null default now(),
10+
expires_at timestamptz,
11+
revoked_at timestamptz,
12+
last_used_at timestamptz,
13+
constraint scim_tokens_pkey primary key (id),
14+
constraint scim_tokens_token_hash_check check (token_hash ~ '^[0-9a-f]{64}$'),
15+
constraint scim_tokens_expires_at_future check (expires_at is null or expires_at > created_at),
16+
constraint scim_tokens_revoked_after_created check (revoked_at is null or revoked_at >= created_at)
17+
);
18+
19+
/* auth_migration: 20260821010000 */
20+
-- The digest resolves a request to a provider, so it is unique across all providers.
21+
create unique index if not exists scim_tokens_token_hash_key
22+
on {{ index .Options "Namespace" }}.scim_tokens (token_hash);
23+
24+
/* auth_migration: 20260821010000 */
25+
-- Not partial, so an ON DELETE CASCADE from sso_providers can find revoked
26+
-- tokens too.
27+
create index if not exists scim_tokens_sso_provider_id_idx
28+
on {{ index .Options "Namespace" }}.scim_tokens (sso_provider_id);
29+
30+
/* auth_migration: 20260821010000 */
31+
-- Supports purging expired tokens.
32+
create index if not exists scim_tokens_expires_at_idx
33+
on {{ index .Options "Namespace" }}.scim_tokens (expires_at);
34+
35+
/* auth_migration: 20260821010000 */
36+
-- Supports purging revoked tokens.
37+
create index if not exists scim_tokens_revoked_at_idx
38+
on {{ index .Options "Namespace" }}.scim_tokens (revoked_at);
39+
40+
/* auth_migration: 20260821010000 */
41+
alter table {{ index .Options "Namespace" }}.scim_tokens enable row level security;
42+
/* auth_migration: 20260821010000 */
43+
grant select on {{ index .Options "Namespace" }}.scim_tokens to postgres with grant option;

0 commit comments

Comments
 (0)