Organization-based hierarchical access control
Achieve logical multi-tenancy with Aidbox
Hierarchical organization-based access control in Aidbox allows for the restriction of access to data based on the organization to which it belongs. When this feature is enabled, the FHIR Organization resource in Aidbox gains new semantics and functionality.
This means that when users interact with the Organizational FHIR API, they are only able to access the resources that belong to their organization or tenant. The hierarchical organization-based access control ensures that data is logically isolated and accessible only within the appropriate organizational context.
Problem
FHIR resources must be separated per organizations. Organizations can be nested. Every organization has access to their own resources and to the nested organization resources.
Solution
Let's consider the next organization structure. There are two independent organizations Org A & Org D, each of them has nested, dependent organizations. Org B & Org C are nested to Org A, and Org E is nested to Org D.

Organization hierarchy structure
To achieve such a behavior, you may consider an Aidbox feature called organization-based access control.
Let's create the organization structure in Aidbox:
POST /fhir
resourceType: Bundle
type: batch
entry:
- request:
method: PUT
url: Organization/org-a
resource:
name: Organization A
resourceType: Organization
- request:
method: PUT
url: Organization/org-b
resource:
name: Organization B
resourceType: Organization
partOf:
reference: Organization/org-a
- request:
method: PUT
url: Organization/org-c
resource:
name: Organization C
resourceType: Organization
partOf:
reference: Organization/org-a
- request:
method: PUT
url: Organization/org-d
resource:
name: Organization D
resourceType: Organization
- request:
method: PUT
url: Organization/org-E
resource:
name: Organization E
resourceType: Organization
partOf:
reference: Organization/org-d
When an Organization resource is created, a dedicated FHIR API is deployed for that organization. This API provides access to the associated FHIR resources. Nested organization FHIR resources are accessible through the parent Organization API.
The Organization-based FHIR API base url:
<AIDBOX_BASE_URL>/Organization/<org-id>/fhir
The Organization-based Aidbox API base url:
<AIDBOX_BASE_URL>/Organization/<org-id>/aidbox

FHIR APIs reflection in organization-based access control
Try Org-BAC
Let's play with new APIs.
We will create a Patient resource in Org B:
PUT /Organization/org-b/fhir/Patient/pt-1
content-type: text/yaml
accept: text/yaml
name: [{given: [John], family: Smith}]
gender: male
Now we can read it:
GET /Organization/org-b/fhir/Patient/pt-1
Note, that patient has a https://aidbox.app/tenant-organization-id
extension, which references org-b
.
id: >-
pt-1
meta:
extension:
- url: https://aidbox.app/tenant-organization-id
valueReference:
reference: Organization/org-b
- url: ex:createdAt
valueInstant: '2024-10-03T15:02:09.039005Z'
lastUpdated: '2024-10-03T15:02:09.039005Z'
versionId: '336'
name:
- given:
- John
family: Smith
gender: male
resourceType: Patient
The resource is also accessible through Org A API:
GET /Organization/org-a/fhir/Patient/pt-1
But this resource is not accessible through Org C, Org D and Org E API:
GET /Organization/org-c/fhir/Patient/pt-1
# 403 Forbidden
Limitations
Some Aidbox features do not respect Organization-based access control. The resources managing these features are inaccessible under the Organization API.
For example, there is SubsSubscription
resource.
Any request to the SubsSubscription
resource will return OperationOutcome
with the 422
HTTP code and issue code not-supported
.
If SubsSubscription
resource is created using regular API (not Organization API), Aidbox Subscriptions will send notifications irrespectively of Organization hierarchy.
Topic-Based Subscriptions with Organization Hierarchy
Organization-based hierarchical filtering is available starting from version 2509.
AidboxSubscriptionTopic
and AidboxTopicDestination
support organization-based hierarchical filtering. For more details, see Aidbox Topic-Based Subscriptions.
FHIR API over Organization resources
Create
POST <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>
Conditional Create
Conditional operations are available starting from version 2509.
Create the resource only if no existing resource matches the given search criteria.
POST /Organization/<org-id>/fhir/Observation
If-None-Exist: identifier=http://acme.org/obs|12345
Content-Type: application/fhir+json
{
"resourceType": "Observation",
"status": "final",
"identifier": [{ "system": "http://acme.org/obs", "value": "12345" }],
"code": { "text": "Example observation" }
}
Read
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>/<id>
Update
Put
PUT <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>/<id>
Conditional Put
Conditional operations are available starting from version 2509.
Update a resource that matches the search; create if none exists.
PUT /Organization/<org-id>/fhir/Observation?identifier=http://acme.org/obs|12345
Content-Type: application/fhir+json
{
"resourceType": "Observation",
"status": "final",
"identifier": [{ "system": "http://acme.org/obs", "value": "12345" }],
"code": { "text": "Example observation (updated)" }
}
Patch
PATCH <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>/<id>?[_method={ json-patch | merge-patch | fhirpath-patch }]
All PATCH methods are supported under org-scoped API. See also patch
Conditional Patch
Conditional operations are available starting from version 2509.
Patch a resource matched by a search expression.
PATCH /Organization/<org-id>/fhir/Observation?identifier=http://acme.org/obs|12345&_method=merge-patch
Content-Type: application/merge-patch+json
{ "status": "final" }
Delete
DELETE <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>/<id>
Conditional Delete
Conditional operations are available starting from version 2509.
Delete resource(s) matching a search expression at the type endpoint under the organization.
DELETE /Organization/<org-id>/fhir/Observation?identifier=http://acme.org/obs|12345
Search
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>
The search API does not support search parameters:
_assoc
_with
Since 2505, _has search parameter is supported.
$everything
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/Patient/$everything
See also $everything on Patient
$document
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/Composition/$document
See also $document endpoint
History
Resource full history
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>/<id>/_history
Specific version history entry
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>/<id>/_history/<vid>
Bundle
Supported transaction
and batch
bundle types.
POST /Organization/org-a/fhir/
Accept: text/yaml
Content-Type: text/yaml
resourceType: Bundle
# transaction | batch
type: transaction
entry:
- request:
method: POST
url: 'Patient'
resource:
birthDate: '2021-01-01'
id: 'pt-1'
meta:
organization:
id: 'org-c'
resourceType: 'Organization'
- request:
method: POST
url: 'Patient'
resource:
birthDate: '2021-01-01'
id: 'pt-2'
- request:
method: PATCH
url: 'Patient/pt-3?_method=json-patch'
resource:
- op: replace
path: birthDate
value: '2021-01-01'
It is also possible to use org-based url in a request.url
:
POST /
Accept: text/yaml
Content-Type: text/yaml
resourceType: Bundle
# transaction | batch
type: transaction
entry:
- request:
method: GET
url: '/Organization/org-a/fhir/Patient/pt-1'
- request:
method: PUT
url: '/Organization/org-b/fhir/Patient/pt-3'
resource:
birthDate: '2021-01-01'
- request:
method: POST
url: '/Organization/org-a/fhir/Patient'
resource:
birthDate: '2021-01-01'
id: 'pt-4'
See also Transactions page
Conditional Create with Bundle
Conditional operations are available starting from version 2509.
POST /Organization/org-a/fhir/
Content-Type: application/fhir+json
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "POST",
"url": "Observation",
"ifNoneExist": "identifier=http://acme.org/obs|12345"
},
"resource": {
"resourceType": "Observation",
"status": "final",
"identifier": [{ "system": "http://acme.org/obs", "value": "12345" }],
"code": { "text": "Example observation" }
}
}
]
}
Conditional Update with Bundle
Conditional operations are available starting from version 2509.
POST /Organization/org-a/fhir/
Content-Type: application/fhir+json
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "PUT",
"url": "Observation?identifier=http://acme.org/obs|12345"
},
"resource": {
"resourceType": "Observation",
"status": "final",
"identifier": [{ "system": "http://acme.org/obs", "value": "12345" }],
"code": { "text": "Example observation" }
}
}
]
}
Conditional Delete with Bundle
Conditional operations are available starting from version 2509.
POST /Organization/org-a/fhir/
Content-Type: application/fhir+json
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "DELETE",
"url": "Observation?identifier=http://acme.org/obs|12345"
}
}
]
}
Metadata
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/metadata
AidboxQuery
To use $query
endpoint under organization-based hierarchical access control, it is necessary to create explicitly organization
param in AidboxQuery
.
PUT /AidboxQuery/<query-name>
params:
organization:
type: string
query: "SELECT * from patient pt WHERE pt.resource#>>'{meta,organization,id}' = {{params.organization}}"
count-query: "SELECT count(*) from patient pt WHERE pt.resource#>>'{meta,organization,id}' = {{params.organization}}"
type: query
Now org-id
is automatically available in the query in {{params.organization}}
.
GET /Organization/<org-id>/$query/<query-name>
GraphQL
POST /Organization/<org-id>/aidbox/$graphql
Since version 2503 GraphQL is supported in OrgBAC mode. Note that it can be accessed only on the non-FHIR endpoint, because our GraphQL implementation is slightly different from FHIR.
See also: graphql-api.md
Shared resource mode
By default, nested API has no access to a resource that belongs to the upper organizations. Sometimes it is necessary to have resources that can be accessed by the nested APIs. To achieve it the resource should be marked as shared
.
Update and delete operations are not allowed from nested organizations' APIs. To update or delete shared
resource use its root organization API.
Create a shared resource
To create a shared resource, use the https://aidbox.app/tenant-resource-mode
extension.
PUT /Organization/org-a/fhir/Practitioner/prac-1
content-type: text/yaml
meta:
extension:
- url: https://aidbox.app/tenant-resource-mode
valueString: "shared"
Access shared resource from a nested API
Now, if org-b
is a child organization of org-a
, (Organization.partOf references org-a
), we get the access to the shared resource:
GET /Organization/org-b/fhir/Practitioner/prac-1