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) and cases.investigations.assigned_worker (NOT NULL).

  • CREATE INDEX on 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_worker becomes type-correct

  • IdP username changes no longer silently break authz on cases/investigations

  • Devstack onboarding requires cargo xtask dev reseed after this migration lands

  • Pre-1.0 status justifies destructive shape; expand-contract dance avoided as over-engineering for non-existent legacy data

  • ADR-023 — Authz architecture this enables

  • ADR-026 — IdP-neutral identity layer

Edit this page · latest