FHIR authorization
FHIR Authorization and RBAC
Healthcare access control follows clinical relationships. Fire Arrow expresses those relationships as FHIR resources and resolves each request through rules that combine role, operation, and a validator into an allow, deny, or filter decision.
Who this is for
Security architects and product teams designing patient, clinician, sponsor, and service-account access.
Clinical applicability
A patient sees their own data via PatientCompartment. A clinician sees patients in their organisation via LegitimateInterest. A sponsor sees anonymized records via property filters. A service account uses a durable API token, all from the same backend.
The rule shape
A rule matches three things: `client-role`, `resource`, and `operation`. When all three match the request, the rule's validator decides whether to allow it. The full operation set includes `read`, `search`, `create`, `update`, `delete`, `graphql-read`, `graphql-search`, `subscribe`, `binary-upload`, `generate-durable-token`, `generate-one-time-token`, and `transaction`.
Set `default-validator: Forbidden` and grant access explicitly. A request with no matching rule is denied. This eliminates the failure mode where a missing rule silently grants access.
Validators
On Server: `Allowed` (always grant), `Forbidden` (always deny), `PatientCompartment`, `PractitionerCompartment`, `RelatedPersonCompartment`, `DeviceCompartment`, `LegitimateInterest` (covering Task and organisation-scoped access), and `CareTeam` (for cross-organisational, patient-scoped access). Compartment validators support the full FHIR R4 compartment inclusion criteria. Multiple validators can apply to the same role/resource pair and resolve additively.
On Core: the same compartment validators plus `GeneralPractitioner` and `OrganizationCompartment`. Compartment validators on Core support one inclusion parameter per resource. Rules are exclusive on Core (one validator per role/resource pair).
Search narrowing and side-channel protection
When a Patient role searches Observation under PatientCompartment, the underlying query gets `&patient=Patient/{id}` appended at the database layer. The unauthorized rows are never selected, not filtered after the fact. GraphQL search uses alternative search parameter maps to achieve the same narrowing without changes on the client.
Property filters redact values from authorized responses, but redacted fields stay indexed and searchable. To prevent reverse-engineering through `_sort`, `_include`, `_filter`, or chained search parameters, declare `blocked-search-params` on the rule. Any search using a listed parameter is rejected with 403 Forbidden. The safest pattern is to grant `read` (or `graphql-read` with reference expansion) without `search` for property-filtered roles.
Identity filters
An identity filter is an optional FHIRPath expression on a rule. Before the validator runs, the expression is evaluated against the caller's identity resource. If it returns false or empty, the rule is skipped and the next matching rule is tried. This lets you express conditions like `qualification.code.coding.where(system='...' and code='MD').exists()` or `telecom.where(system='email').value.contains('@admin.example.org')` without splitting the role.
Related pages
FAQ
Do I need separate rules for REST and GraphQL?
Yes. A rule for `read` does not grant `graphql-read`, and a rule for `search` does not grant `graphql-search`. This keeps the GraphQL surface explicit. If a role uses both APIs, configure both pairs of operations.
How do I debug a denied request?
Send the request with the `X-Fire-Arrow-Debug` header. Server returns the rule chain it evaluated, which rule matched, and why the validator decided as it did. The Server admin UI also surfaces this output.
Can a rule be conditional on the caller's identity?
Yes, through identity filters. The filter is a FHIRPath expression on the caller's resolved FHIR resource (Patient, Practitioner, RelatedPerson, or Device). If the expression returns true, the rule applies; otherwise it is skipped and the next matching rule is tried.
What happens if no rule matches?
The `default-validator` decides. Configure it as `Forbidden` so unmatched requests are denied. This is the recommended setting and the foundation of the deny-by-default model.