access control.pdf

Access Control Decision Guide

Companion to the Authorization and Access Control docs.

Section 1: Use Case & Participants

1.1 Who are the users?

• Patients only
• Providers / staff / partners (clients or outside vendors) only
• Both

1.2 What is your organizational structure?

• Single organization
• Multiple locations of one organization
• Multiple independent organizations (MSO-style)
• Departments or service lines within one organization
• Combination

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:

• Internal term for tenants? (clinics, programs, sites, accounts)
• Grouping is organizational, functional, or per-patient?
• Is there an organizational hierarchy (e.g., region clinic)? →

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): →

• Patients are tagged only at the lowest level they belong to.

• Practitioners enrolled at an intermediary level get one
ProjectMembership.access entry per lowest-level tenant under it
(iteratively expanded).

• Adding a new lowest-level tenant requires a workflow to update every
practitioner membership at or above that intermediary.

2b Patient and Practitioner Sharing

Questions:

• Can a patient be enrolled in more than one tenant at a time?

• Are practitioners isolated to their tenant, or visible across all tenants
(global directory)?


Situation Approach
One tenant per patient Single compartment on the Patient
Patient shared across tenants Multiple compartments, one pertenant
Practitioners shared (globaldirectory) 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:

• What are the role names? (physician, RN, MA, front desk, billing, care
coordinator, medical director)

If multi-tenant: for each role, which tenants does it cover? (one, several,
all)

• Any oversight roles that need visibility across all tenants without pertenant
enrollment?

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

Role scope Modeling approach
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.

When separate logins are warranted: contractual or regulatory obligations
requiring the API to never return both scopes’ data in the same session.
Example: a provider works at two independent clinics under separate BAAs —
stacking would let one token read both, violating the BAA. Two memberships
(each parameterized to one clinic) bound each session to one tenant.

Test: if this token leaked, should the attacker reach both scopes? No ––
separate logins. Yes or don’t care stacked; any “switcher” is purely a UI ––> concern.

3b Permission Mechanics

Questions:

• For each role: which resource types does it read / write / search / delete?

• Any partial FHIR interactions? (create-only, read-without-search, etc.)

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:

• Who invites/removes users and manages memberships?
• Who manages Bots, ClientApplications, AccessPolicies?
• Any super admin needs? (project creation via API, overwriting protected
fields)
• Should admins be blocked from clinical data?

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:

• Self-registration or staff-invite?
• Are caregivers / proxies needed? (parents, adult children, guardians)
• One caregiver with multiple patients? One patient with multiple caregivers?
• How is the caregiver relationship established and revoked?

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:

• Patient-context launch, provider-context launch, or standalone?
• Which scopes?
• Offline access needed?

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:

• Restrict by IP range? (VPN, on-prem devices)
• Any roles that bypass?


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:

• When does a patient get enrolled in a tenant? (registration, first
appointment, referral, insurance, geography)
• Who is authorized to move a patient between tenants?
• On transfer: do records follow the patient, stay with the original tenant,
or become shared?

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:

• Terminology (CodeSystems, ValueSets) shared or tenant-specific?
• Questionnaires shared or per-tenant?
• PlanDefinitions / ActivityDefinitions global or per-tenant?
• Any resources shared across projects (not just tenants)?

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.