RBAC Matrix

On this page

Identity provider

Canopy delegates worker authentication to Keycloak (canopy realm). Worker identities live in Keycloak; service tokens are JWTs validated against the realm’s JWKS. canopy-portal (applicant-facing) does not use Keycloak: reference-number auth (HH-[a-f0-9]{8} code + passcode) is LIVE, verified by canopy-portal against canopy-applications' verify-credential endpoint, and applicant sessions are opaque Redis tokens per ADR-008 / ADR-026 — no applicant JWT.

  • Bearer-token validation: canopy-auth::middleware::require_bearer_auth

  • Role extraction: Claims::roles() reads Keycloak realm-roles claim

  • Role enforcement: Claims::require_* helpers in canopy-auth::claims

Role catalogue

Six realm roles, defined in Keycloak and enforced by canopy-auth::Claims helpers in crates/canopy-auth/src/claims.rs:

Role Audience Typical responsibilities

applicant

Constituent

Submit application, check status, view notices for own household. Reference-number authentication (HH-[a-f0-9]{8} code + passcode) via canopy-portal, verified against canopy-applications. LIVE — applicant sessions are opaque Redis tokens, not Keycloak JWTs (ADR-008 / ADR-026).

caseworker

Frontline DFCS staff

Read case data, run intake, file appeals on behalf of applicant. Read-only on most determination endpoints; cannot run determinations or sign artefacts.

eligibility_specialist

Trained eligibility staff

Run determinations (POST /v1/determine on every program service, POST /v1/eligibility/determine on the orchestrator). Read + write on all case data within their assigned caseload.

supervisor

DFCS supervisor / approval authority

All eligibility_specialist capabilities + report generation, IPV / disqualification approvals, case reassignment, TANF personal-responsibility actions. (Chain-verification endpoints are service-or-admin, not supervisor — see the matrix below.)

quality_control

State QC reviewers

Read-only access to case data, audit-event search, chain-status, FNS-7176 QC universe extracts. Subset of caseworker_or_above — cannot write determinations or reassign cases.

admin

System operators

Full access. Used for system-administration endpoints + override scenarios. Membership tightly controlled at the Keycloak realm level.

Role hierarchy

The Claims::require_*_or_above helpers encode the inclusion lattice. or_above means any of the named roles satisfies the guard:

admin
  ▲
supervisor                       ← require_supervisor_or_above:    {supervisor, admin}
  ▲
eligibility_specialist           ← require_eligibility_specialist_or_above:
  ▲                                {eligibility_specialist, supervisor, admin}
caseworker                       ← require_caseworker_or_above:
  ▲                                {caseworker, eligibility_specialist, supervisor, quality_control, admin}
quality_control (sibling — read)

Note: quality_control is a sibling of caseworker — it satisfies require_caseworker_or_above (read-side) but does not satisfy require_eligibility_specialist_or_above (write-side). This is the key separation-of-duties contract for QC reviewers.

applicant is its own root — require_applicant is mutually exclusive with all worker-side guards.

Endpoint enforcement (representative)

Drawn from grep of claims.require_* call sites across the workspace as of 2026-04-28. The full surface is in each service’s src/api/ directory; this is the auditor-relevant subset.

Endpoint Guard Notes

POST /v1/determine (every program service)

require_eligibility_specialist_or_above

Determination is a write action that produces a signed binding artefact. Caseworker-only roles cannot run determinations.

POST /v1/eligibility/determine (orchestrator)

require_eligibility_specialist_or_above

Same gate as the program services it dispatches to. Bearer token forwarded to each program service per ADR-001 + ADR-002.

GET /v1/persons/* (canopy-persons)

require_caseworker_or_above

Read-side: caseworker, QC, eligibility_specialist, supervisor, admin all permitted.

POST /v1/persons/* (canopy-persons writes)

require_eligibility_specialist_or_above

Write-side: caseworker + QC excluded (read-only).

POST /v1/applications (canopy-applications)

require_caseworker_or_above

Intake is permitted to caseworker and above — caseworker-side intake is the primary use case.

POST /v1/reporting/* (FNS-388, FNS-7176, ACF-199, T-MSIS, CMS-416)

require_user_only(SUPERVISOR_OR_ABOVE) (#1438, ADR-043 §C)

Federal report generation requires sign-off authority — carried by the worker’s EXCHANGED per-target token (aud=canopy-reporting); service-class bearers and (under enforcement) direct broad-audience worker bearers are 403.

Unified chain-verification namespace (#1205, ADR-014 Amendment 9): GET /v1/security/chain/status, POST /v1/security/chain/verify, GET /v1/security/chain/verify-jobs/{id}, GET /v1/security/chain/attest

service-class token OR require_admin

Pub 1075 §9 reportable surface, BOTH families — family=audit and the family=fti&service=… arm (#1206 MR-3). Replaces the deleted GET /v1/security/verify-chain + POST /v1/security/fti/chain-verify + GET /v1/security/fti/chain-status with the SAME service-or-admin arm those carried. Job polling is additionally requester-scoped: service callers see only their own jobs; admin sees all; unknown/foreign id → 404.

GET /v1/security/events (audit-event search)

require_caseworker_or_above

Read-side; QC reviewers can search audit events.

POST /v1/security/breaches/*

require_admin

Breach disposition is admin-only (incident-response runbook).

TANF personal-responsibility actions (POST /v1/personal-responsibility/*)

require_supervisor_or_above

Sanctions + IPV referrals require supervisor sign-off.

POST /v1/applications/{id}/expedite (expedited SNAP screening)

require_eligibility_specialist_or_above

Expedited determinations 7 CFR 273.2(i).

GET /v1/households/{household_id}/issuances (canopy-enrollment)

require_service_caller + human-context assignment gate (#408, EffectiveUser since #1443)

Pub 1075 AC-6 least-privilege. When the request resolves a HUMAN (an exchanged worker bearer), they must either carry supervisor/admin role OR have an active household_assignments row in canopy-applications for that household. Bare service = system traffic, allowed. Allow + deny paths both emit audit events.

POST /v1/workers/{worker_id}/assignments (canopy-applications, #408)

require_service_caller (service-only provisioning; #1443)

Service-caller-only: post-#1443 a service bearer never transports a human, so the old delegated-supervisor bar is gone (it could no longer fire). Workers of any role are 403 — assignment mutations are system provisioning; a future worker-delegated surface would flip to require_service_or_exchanged with a supervisor bar.

DELETE /v1/assignments/{id} (canopy-applications, #408)

require_service_caller (service-only provisioning; #1443)

Soft-delete via unassigned_at; same gate as create. Audit-preserving.

GET /v1/workers/{worker_id}/assignments, GET /v1/households/{household_id}/assignments (canopy-applications, #408)

require_service_caller

Read-side; any service-class caller. Hot-path consumed by canopy-enrollment’s RBAC gate.

Cross-service token flow

Per ADR-001 program-service isolation, services authenticate to each other via the same Keycloak token the worker presented to the orchestrator:

  1. Worker → canopy-eligibility: bearer token (Keycloak-issued)

  2. canopy-eligibility → canopy-medicaid (and other program services): forwards the worker’s bearer token in the Authorization: Bearer … header (orchestrator.rs:412 — fixed in #338)

  3. Each program service runs its own require_eligibility_specialist_or_above guard against that token

This means the worker’s authority — not the orchestrator’s — gates every downstream action. There is no service-account that programmatically escalates privilege.

Service-to-service calls that do use a separate API key (canopy-snap → canopy-verification’s IEVS adapter, canopy-reporting → upstream services) are scoped to internal endpoints not exposed publicly.

Service-account secrets

Secret Purpose

CANOPY_INTERNAL_API_KEY

Internal HTTP calls between services that don’t carry a worker token (e.g. canopy-reporting → program services). Default canopy-internal-dev-keyoverride in production via env var.

CANOPY_<PROGRAM>__SIGNING_KEY

Per-program ECDSA P-256 private key for determination JWS signing (ADR-002). Production sets via env; dev uses .keys/<program>-private.pem.

CANOPY_VERIFY_KEY_<PROGRAM>

Per-program ECDSA P-256 public key on the orchestrator side. Production sets via env; dev falls back to .keys/<program>-public.pem.

CANOPY_ENCRYPTION_KEY

AES-256-GCM key for SSN field-level encryption in canopy-persons. 32 bytes, base64-encoded.

Drift between docs

The implementation-guide.adoc Authentication & Authorization section once listed roles admin / supervisor / eligibility_worker / intake_worker / fiscal_officer. Those names predate the Keycloak realm-roles convention adopted with canopy-auth. The authoritative list is the one above (driven by the actual Claims::require_* helpers). The implementation guide will be updated in a follow-up doc pass to converge on these names.

Edit this page · default