ADR-025: Identity Normalization (preferred_username → sub UUID)
On this page
Status
Accepted (2026-05-08). Implementation lands in Plan A Step 2 (Multi-Jurisdictional Authorization plan).
Context
CRAIG’s CLAUDE.md states "API handlers use claims.sub (UUID), web UI uses preferred_username (display)" — but a 2026-05-07 audit found three high-traffic columns store preferred_username instead of sub:
-
cases.cases.assigned_worker TEXT NOT NULL— populated from request body with display name -
cases.cases.supervisor TEXT NULL— same -
cases.investigations.assigned_worker TEXT NOT NULL— same
The record-level authz predicate claims.sub == row.assigned_worker is type-broken for these tables. Plan A’s policy engine cannot enforce record-level authz on cases/investigations until the identity model is normalized.
Newer CRAIG tables (added 2026-04+) use claims.sub UUID directly (e.g., cases.contacts.recorded_by, cases.report_persons.linked_by, cases.screening_decisions.actor_sub). The audit identified the gap as confined to the three columns above.
Also: preferred_username can change in the IdP. Username-equality predicates silently break on rename. Sub-UUID equality is stable.
Decision
Destructive pre-1.0 migration:
-
ALTER TABLE cases.cases DROP COLUMN assigned_worker; ALTER TABLE cases.cases ADD COLUMN assigned_worker UUID NOT NULL; -
Same for
cases.cases.supervisor(NULL) andcases.investigations.assigned_worker(NOT NULL). -
CREATE INDEXon each new UUID column.
Devstack/test data regenerated via cargo xtask dev reseed. Handlers updated to write claims.sub_uuid()?. DTOs updated.
First-production deployments use per-jurisdiction data-import tooling — NOT this destructive migration. The data-import tool is each jurisdiction’s responsibility (it migrates from their legacy CCWIS, mapping their workers' identities to sub UUIDs at import time).
Consequences
-
Predicate
claims.sub == row.assigned_workerbecomes type-correct -
IdP username changes no longer silently break authz on cases/investigations
-
Devstack onboarding requires
cargo xtask dev reseedafter this migration lands -
Pre-1.0 status justifies destructive shape; expand-contract dance avoided as over-engineering for non-existent legacy data