access control.docx

Access Control Decision Guide

Companion to the Authorization and Access Control docs.

Section 1: Use Case & Participants

1.1 Who are the users?

1.2 What is your organizational structure?

Why: biggest driver of downstream design. If single org with no internal isolation, skip Section 2.

Section 2: Multi-tenancy

Skip if single-organization with no internal isolation.

Isolation uses compartments (meta.compartment) matched by parameterized access policies.

Design principle (from Medplum docs): Push complexity into enrollment Bots, not policies. Each patient is tagged with their tenant(s); each practitioner’s ProjectMembership.access lists every tenant they work in. See the MSO demo enrollment.ts for reference.

2a Tenant Modeling

Questions:

Situation Approach
Clinics, practices, locations (MSO) Organization
Service lines, departments HealthcareService
Dedicated team per patient CareTeam per patient

Hierarchy pattern (if tenants nest, e.g., region → clinic):

2b Patient and Practitioner Sharing

Questions:

Situation Approach
One tenant per patient Single compartment on the Patient
Patient shared across tenants Multiple compartments, one per tenant
Practitioners shared (global directory) Leave unassigned; policy reads without _compartment filter
Practitioners isolated per tenant Tag with tenant compartment; policy filters by _compartment

Section 3: Roles & Permissions

For each role: name it, scope it (which tenants, if multi-tenant), say what resources it touches, and narrow as needed.

3a Role Inventory & Scope

Questions:

Role scope Modeling approach
One tenant Single ProjectMembership.access entry parameterized with that tenant
Multiple tenants, combined view One access entry per tenant, same policy
Org-wide / cross-tenant oversight Unparameterized policy, no _compartment filter
Tenants contractually can’t share data (e.g., locum across competitor clinics) Separate logins per tenant — see callout below

⚠ One-way door: stacking is additive only. Policies compose by union — you can’t narrow by composition. For genuinely narrower API access in one context (not just a filtered UI view), use separate logins, not stacked policies.

3b Permission Mechanics

Questions:

Situation Approach
Full CRUD on a resource Standard resource entry, no interaction restriction
Read-only readonly: true
Partial interactions Explicit interaction array

3c Narrowing What a Role Can Do

Cover only the knobs that apply.

Field-level — partial field access (e.g., billing sees Patient minus diagnoses):

Situation Approach
Readable but not writable readonlyFields
Not visible hiddenFields (masks output only — for decluttering, do it in the UI)

Write constraints — enforce state transitions or required fields at the policy layer:

Situation Approach
Prevent field change in terminal state writeConstraint on %before
Require field at transition writeConstraint on %after + field presence
Update-only Prefix with %before.exists() implies ...
Create-only Prefix with %before.exists().not() implies ...

Keep FHIRPath simple; complex logic belongs in a Bot.

Criteria-based filtering — filter by resource attribute (geography, code, status):

Situation Approach
State license Patient?address-state=%licensed_state, parameterized
Multi-state One access entry per state
Specialty or status filter criteria with code/status filter
Criteria + tenant Single criteria string combining both

criteria supports only :not / :missing — no chained searches. Denormalize or tag via an enrollment Bot.

Section 4: Admin Structure

Questions:

Situation Approach
Standard admin ProjectMembership.admin
Admin with narrowed clinical access Admin flag + dedicated AccessPolicy
Admin blocked from clinical content Admin flag + hiddenFields on clinical resources
Project creation, overwriting protected fields Super admin — server operators only
Separate “user manager” vs. “tech admin” Two AccessPolicies stacked on admin flag (see 3b)

⚠ Note: Admin flag covers admin resources only (Project, ProjectMembership, User) — admins still need a policy for clinical data.

Appendix: Additional Decisions

Narrower decisions that apply only in specific situations. Cover each only if relevant.

A. Open Registration & Caregivers

If the product has a patient portal.

Questions:

Situation Approach
Invite-only Standard ProjectMembership with patient policy
Open self-registration Set Project.defaultPatientAccessPolicy; enable open registration
Patient sees own data Parameterized policy with %patient on clinical resources
Single caregiver, single patient One access entry with %patient
One caregiver, multiple patients Multiple access entries, different %patient
Multiple caregivers per patient Each caregiver has their own ProjectMembership
Revoke caregiver Remove the corresponding access entry
Time-bound caregiver access (e.g., expires at age 18) Application-layer enforcement — not native

B. SMART on FHIR

If third-party apps (not your own frontend) request access.

Questions:

Situation Approach
Your own frontend Not SMART — standard ClientApplication + AccessPolicy
Third-party, patient-context SMART 2.0.0, launch/patient
Third-party, provider-context SMART 2.0.0, launch
Standalone patient app patient/*.rs or narrower
Offline access Add offline_access

⚠ Note: Effective access = SMART scopes ∩ AccessPolicy.

C. IP Access Rules

If access should be restricted by IP range.

Questions:

Situation Approach
VPN-only Allow VPN ranges + wildcard block
On-prem only Allow clinic network + wildcard block
Mixed Different AccessPolicies per role

⚠ Note: IPv4 only. Rules evaluate sequentially — always terminate with a wildcard block.

D. Enrollment & Reassignment Workflows

If Section 2 multi-tenancy applies.

Questions:

Situation Approach
Patient enrollment Bot calls $set-accounts on the triggering event; use propagate: true to tag related resources
Transfer, records follow Update compartment with propagate: true; old tenant loses access
Transfer, records stay / shared Add new compartment (don’t replace); both tenants retain access

Reference implementation: MSO demo enrollment.ts — canonical example patterns.

E. Shared vs. Tenant-Specific Resources

If Section 2 multi-tenancy applies.

Questions:

Situation Approach
Shared standard terminology Leave unassigned; unfiltered read in policy
Tenant-specific Questionnaires $set-accounts tags with tenant; filter by _compartment
Mixed (some global, some per-tenant) Two resource entries in policy: one filtered, one unfiltered
Global Bots Leave unassigned; Bots run as ClientApplication
Reference data shared across multiple projects Super admin creates a linked project (read-only view)

⚠ Note: Forgetting unfiltered read on standard terminology causes empty dropdowns — a common multi-tenant misconfiguration.

F. Role-Aware UI

If your frontend needs to show / hide UI based on the user’s role

Situation Approach
UI changes based on role AccessPolicy.basedOn + /auth/me

Using AccessPolicy.basedOn keeps the UI in sync with what the API enforces. The alternative (hardcoded role field, custom endpoint) tends to drift.