ADR-023: Multi-Jurisdictional Authorization Architecture

On this page

Status

Accepted (2026-05-09). Implementation lands across Plan A Steps 4–11 (Multi-Jurisdictional Authorization plan). Step 4 ships the crates/craig-authz skeleton (AuthzEngine trait, ZenAuthzEngine impl, types, fail-closed semantics) the rest of the plan integrates into handlers.

Context

The original 2026-04-21 audit flagged record-level authorization (BOLA/IDOR) as the headline P0 federal-compliance event in CRAIG. A 2026-05-07 jurisdiction-coupling audit run during plan-shaping found that hardcoding any specific authz policy (e.g., "supervisors are realm-flat" or "supervisors are case-scoped") forces every adopting jurisdiction to either fork CRAIG or eat the chosen policy. CRAIG is open-source CCWIS — states, tribal nations, counties, and other jurisdictions must be able to onboard without code changes.

Decision

Authorization is data-driven through a policy engine, not hardcoded in handlers. Jurisdiction is a first-class field in the engine’s evaluation context. Each (jurisdiction, role, resource_type, action) tuple maps to a policy expression that the engine evaluates per request. Roles are defined in the deployer’s IdP realm; CRAIG consumes them via JWT claims.

The engine is built on zen-engine (gorules JDM evaluator) — already CRAIG’s house pattern for jurisdiction-specific decision logic ({jurisdiction}-screening-policy, {jurisdiction}-person-match, {jurisdiction}-safety-assessment). Authz policies join the same pattern: {jurisdiction}-authz-{resource} rulesets. No new DSL, no new parser, no new evaluator.

Policies are stored in the existing craig-rules rule_sets table; replicated to consuming services via the platform-stab-2 outbox/inbox + RMQ subscribe_exclusive cache-invalidation primitives.

Consequences

  • CRAIG is genuinely multi-jurisdictional: states/tribes/counties onboard with rulesets + CSVs + env vars, no code changes

  • Per-jurisdiction policy edits (admin role added, supervisor scope changed, custom role with new scope predicate) are runtime config, not code releases

  • The engine is fail-closed: missing policy at boot → service bails; missing policy at request time → 403 + audit event

  • Performance: in-memory policy cache loaded at boot from craig-rules; per-request evaluation is sub-millisecond (zen-engine cached eval)

  • Cross-service authz needs ownership-data denormalization (Plan A §D5) so consuming services can evaluate without a per-request HTTP hop to cases

  • ADR-024 — Policy engine design + RMQ cache invalidation

  • ADR-025 — Identity normalization (preferred_username → sub UUID)

  • ADR-026 — IdP-neutral identity layer

  • ADR-027 — Architectural principles

Edit this page · latest