Skip to main content

Consent Model

See Architecture → Auth + Consent Flow for the full sequence diagrams + resolution-order flowchart.

When a PHI-route handler carries @ConsentScope("<scope>"), the ConsentGuard resolves in strict order — short-circuit on first ALLOW:

  1. Admin bypassauthSource=admin_bypass + captured rationale
  2. Patient self — the patient accessing their own record
  3. Standing consent — active consents row granting scope to user or care-team member
  4. Break-glass — time-bounded (15 min) emergency access with captured justification + HIGH-severity audit + async care-manager notification + patient alert
  5. Intake bootstrap@IntakeExempt routes only (patient creation + first-visit encounter)

Deny on none-of-the-above → 403 + audit w/ deny reason.

ScopeGrants
patient_selfpatient accessing own record
contact_disclosureprovider can see contact details
referral_readspecialist can see referral chain
family_readfamily member view
care_teamwhole-team visibility
decision_graph_readAI decision lineage access
memory.readretrieval endpoint access
medical_record_viewstandard EHR chart view
break_glass_overridebreak-glass session scope

Roles

admin | provider | nurse | coordinator | patient

Adding a new role requires a spec amendment. Each role's allowed scopes are defined in apps/api/src/modules/consent/scope-matrix.ts.

Key guarantees

  • Per-request evaluation — no cache. Revocation is immediate.
  • No silent broad access — every deny path writes an audit event with deny reason.
  • Break-glass audit — HIGH severity + care-manager notification + patient alert.
  • Patient-id filter at repo — FR-013d defense-in-depth; even if consent guard misconfigured, the repo layer refuses unscoped PHI reads for non-admin callers.

FR-033 synthetic-test patient bypass

Scope: memory-store only (apps/research/memory-store/services/consent/__init__.py). Does not apply to the primary API.

The non-prod fixture convention is that synthetic test patients carry the PAT_TEST_* prefix (FR-033). The /api/consent/check service has no records for synthetic patients and would otherwise deny every benchmark or integration-test request that names one. The memory-store consent shim therefore short-circuits _resolve() for PAT_TEST_* ids and returns a synthesized:

ResolvedScope(
scope=,
role=caller_token.roles[0] or "service",
user_id=caller_token.sub,
patient_id=patient_id,
auth_source="synthetic_test",
is_cache_hit=False,
)

The bypass fires before the network round-trip to /api/consent/check, so the consent service's audit log stays uncontaminated by synthetic traffic. auth_source="synthetic_test" is distinct from standing / break_glass / admin_bypass so downstream code and audits can tell bypass paths from real grants.

Real PHI patient ids do not start with PAT_TEST_ (system-generated MRN-shaped ids), so the bypass is narrow by construction. Adversarial substrings (xxPAT_TEST_999) are rejected.

Cross-link: Compliance → Benchmark Trace Exemption — the trace surface that this bypass enables in turn.

Parents / guardians manage minor accounts (spec 013):

  • Proxy consent scoped to minor's record
  • Age-of-majority graduation — minor's 18th birthday triggers automatic transition prompt; parent loses proxy; minor retains all history
  • Family group can grant each other family_read scope

Revocation path

SPEC-01 Week 7-8 consent hardening (spec 046) added post-resolver ACL check:

  • Entity-level acl.roles[] evaluated AFTER consent resolution
  • Even allowed-by-consent access can be denied by ACL
  • Break-glass does NOT bypass ACL (FR-046-005) — consent-denied-at-ACL still denies
  • get_many silently filters denied entities (FR-046-014)

See specs 046 (memory-store-consent-hardening) for the full ACL model.