Patient Chart Service — Security Model
Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · 13 Security/Compliance/Tenancy · 14 Extended compliance
1. AuthN / AuthZ
All requests carry a Bearer JWT from identity-service. The JWT tid claim identifies the active tenant; downstream RLS uses set_config('app.tenant_id', tid). Scopes ride in the scope claim.
2. RBAC / ABAC matrix
| Scope | Allows | Typical roles |
|---|---|---|
chart:read | Read banner / summary / timeline / specific aggregates (except sensitive segments) | provider, nurse, pharmacist, care-coordinator, medical-assistant |
chart:problems:write | Create / update / resolve / inactivate problems | provider, resident (cosign gate may apply), advanced-practice |
chart:problems:admin | Entered-in-error | provider-admin / HIM lead |
chart:allergies:write | Create / update allergies + NKA/NKDA | provider, nurse, pharmacist |
chart:allergies:admin | Entered-in-error | provider-admin |
chart:allergies:advisory | Call internal advisory (service-to-service) | orders-service, medication-service |
chart:vitals:write | Record / correct vitals | nurse, medical-assistant, provider |
chart:notes:write | Create / edit draft / addenda | provider, resident |
chart:notes:sign | Sign own notes; request cosign | provider, resident |
chart:notes:cosign | Attest cosign | attending / advanced-practice |
chart:notes:admin | Entered-in-error | HIM |
chart:export | Snapshot export | provider, care-coordinator (policy-gated) |
chart:breakglass | Invoke break-glass | any clinician with reason |
chart:sensitive:{segment} | Read a sensitive segment (e.g., mental-health, sexual-health) | role-bound per policy |
ABAC rules:
- Own-encounter preference: Some edit actions require the actor be on the encounter's
participantlist or the patient's current care-team. - Role cosign policy:
SignNotefromrole=residentroutes to cosign unless policy disables it for the template / specialty. - Sensitive segment policy: Evaluated via
ConsentPolicyClientper patient + segment + actor; result determines 200 vs 403 vs 200-with-redaction.
3. Tenant isolation
- Row Level Security on every table with
tenant_id. - All repository queries pass
tenantId; a CI check fails the build if any repository method lacks the tenant predicate in SQL. test/integration/tenant-isolation.spec.tsis a mandatory test (per TEMPLATE).
4. Encryption
| Class | At rest | In transit |
|---|---|---|
| Class A (IDs, tenant) | Disk-level | TLS 1.2+ |
| Class B (codings, category) | Disk-level + TDE | TLS |
| Class C (narrative: note bodies, free-text notes, addenda, reasons) | Disk-level + application-level KMS envelope for note body bytes if kms.enabled=true | TLS |
KMS envelope keying: per-tenant KEK in AWS KMS / platform KMS; DEKs cached in-memory with 15-min TTL.
5. Audit events
Every mutation and sensitive read emits an audit.clinical.* event through AuditPublisher:
| Trigger | Audit event |
|---|---|
| Any aggregate create/update/lifecycle | audit.clinical.mutation |
| Break-glass invocation | audit.clinical.breakglass |
| Sensitive segment access | audit.clinical.sensitive_access |
| Snapshot export | audit.clinical.snapshot_exported |
| Failed authorization | audit.auth.denied |
| AI acceptance | audit.clinical.ai_accepted |
| Note read (first time per user) | audit.clinical.note_read |
All audit events include tenantId, actorId, patientId, correlationId, outcome, purposeOfUse (where provided).
6. Break-glass
- Clinician calls
POST /v1/chart/{patientId}/breakglasswith reason + duration. - Service evaluates policy (eligibility, max duration per role), creates a
ChartAccessrecord, emitsbreakglass.invoked.v1, and issues a short-lived elevated scope (via identity-service refresh exchange) for the requested duration (bounded to 240 minutes). - Every subsequent sensitive access during the elevation is also audited.
7. Sensitive segments
| Segment | Default policy |
|---|---|
mental-health | Require chart:sensitive:mental-health scope OR break-glass |
sexual-health | Require chart:sensitive:sexual-health scope OR break-glass; redact from summary widgets by default |
genetic | Explicit consent required; default deny |
addiction | Require segment scope |
Policy evaluation delegated to ConsentPolicyClient backed by tenant-service + config-service.
8. GDPR / local privacy
| Right | Handling |
|---|---|
| Access | GET /v1/chart/{patientId}/... with patient-scoped token (patient-portal-service proxy) |
| Rectification | PATCH on product REST with audit |
| Erasure | Participate in platform GDPR saga: anonymize author display fields, retain clinical content per retention policy (jurisdictional); do not delete clinical record without policy flag |
| Portability | /v1/chart/{patientId}/snapshot/export?format=json — FHIR-bundle-compatible export |
| Restriction | Block access via policy engine; retain record |
9. Data residency
Deployment is configured per tenant to pin Postgres and object-storage to an in-country region. Cross-region reads are forbidden at the data path; cross-region reporting goes through anonymized aggregates in population-health-service.
10. Threat model (summary)
| Threat | Mitigation |
|---|---|
| Cross-tenant data leakage | RLS + scope guard + integration test |
| Unauthorized break-glass abuse | Audit all + SIEM alert + supervisor review queue |
| Signed-note tampering | DB constraint + content hash + KMS envelope on note body bytes |
| AI injection via input | Gateway refuses uncompliant content; service validates output before write; contentHash stored |
| Enumeration of problems/allergies via id scan | ULIDs + tenant scope on all queries; 404 for cross-tenant id |
| Stolen refresh / break-glass session | Short-lived elevated scope, 240-min max; revocation channel |
11. Open Questions
- Sensitive segment taxonomy — platform-wide fixed list vs per-tenant extensible? Default: fixed list + tenant opt-in extension.
- Rotation cadence for per-tenant KEK? Platform baseline is annual; chart notes may need 180d — decision deferred.