---
description: Implement multi-tenancy with hierarchical organization-based access control to isolate FHIR data per tenant.
keywords: [orgbac, OBAC, multi-tenant, tenant isolation, org-level access, data partitioning]
---

# Organization-based hierarchical access control

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.

<figure><img src="../../../../assets/d31fcf9c-4ae7-4932-87ac-0b725ce1c7a3.avif" alt="Organization hierarchy diagram showing nested organizations: Org A with child Org B and Org C, Org D with child Org E"><figcaption><p>Organization hierarchy structure</p></figcaption></figure>

To achieve such a behavior, you may consider an Aidbox feature called organization-based access control.

Let's create the organization structure in Aidbox:

```http
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](../../../api/rest-api/other/aidbox-and-fhir-formats.md) base url:

```
<AIDBOX_BASE_URL>/Organization/<org-id>/aidbox
```

<figure><img src="../../../../assets/4ec1a26c-d58c-4ff9-9d3b-87f4698f41d8.avif" alt="Diagram showing FHIR API endpoints for each organization in the hierarchy"><figcaption><p>FHIR APIs reflection in organization-based access control</p></figcaption></figure>

### Try Org-BAC

Let's play with new APIs.

We will create a Patient resource in Org B:

```http
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:

```http
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:

```http
GET /Organization/org-a/fhir/Patient/pt-1
```

But this resource is not accessible through Org C, Org D and Org E API:

```http
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

{% hint style="warning" %}
Organization-based hierarchical filtering is available starting from version 2509.
{% endhint %}

`AidboxSubscriptionTopic` and `AidboxTopicDestination` support organization-based hierarchical filtering. For more details, see [Aidbox Topic-Based Subscriptions](../../../modules/topic-based-subscriptions/aidbox-topic-based-subscriptions.md#organization-based-hierarchical-filtering).

## FHIR API over Organization resources

### Create

```
POST <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>
```

#### Conditional Create

{% hint style="warning" %}
Conditional operations are available starting from version 2509.
{% endhint %}

Create the resource only if no existing resource matches the given search criteria.

```http
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

{% hint style="warning" %}
Conditional operations are available starting from version 2509.
{% endhint %}

Update a resource that matches the search; create if none exists.

```http
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

<pre><code><strong>PATCH &#x3C;AIDBOX_BASE_URL>/Organization/&#x3C;org-id>/fhir/&#x3C;resource-type>/&#x3C;id>?[_method={ json-patch | merge-patch | fhirpath-patch }]
</strong></code></pre>

All PATCH methods are supported under org-scoped API. See also [patch](../../../api/rest-api/crud/patch.md)

#### Conditional Patch

{% hint style="warning" %}
Conditional operations are available starting from version 2509.
{% endhint %}

Patch a resource matched by a search expression.

```http
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

{% hint style="warning" %}
Conditional operations are available starting from version 2509.
{% endhint %}

Delete resource(s) matching a search expression at the type endpoint under the organization.

```http
DELETE /Organization/<org-id>/fhir/Observation?identifier=http://acme.org/obs|12345
```

### Search

```
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/<resource-type>
```

{% hint style="warning" %}
The search API does not support search parameters:

* `_assoc`
* `_with`
{% endhint %}

Since 2505, [\_has search parameter](../../../api/rest-api/fhir-search/chaining.md) is supported.

### $everything

```
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/Patient/$everything
```

See also [$everything on Patient](../../../api/rest-api/everything-on-patient.md)

### $document

```
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/Composition/$document
```

See also [$document endpoint](../../../api/rest-api/other/document.md)

### 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.

```yaml
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`:

<pre class="language-yaml"><code class="lang-yaml">POST /
Accept: text/yaml
Content-Type: text/yaml

<strong>resourceType: Bundle
</strong># 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'
</code></pre>

See also [Transactions page](../../../api/batch-transaction.md)

#### Conditional Create with Bundle

{% hint style="warning" %}
Conditional operations are available starting from version 2509.
{% endhint %}

```http
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

{% hint style="warning" %}
Conditional operations are available starting from version 2509.
{% endhint %}

```http
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

{% hint style="warning" %}
Conditional operations are available starting from version 2509.
{% endhint %}

```http
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

{% hint style="info" %}
[Learn more about AidboxQuery](../../../api/rest-api/aidbox-search.md#aidboxquery).
{% endhint %}

To use `$query` endpoint under organization-based hierarchical access control, it is necessary to create explicitly `organization` param in `AidboxQuery`.

```yaml
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}}`.

```yaml
GET /Organization/<org-id>/$query/<query-name>
```

### GraphQL

```http
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](../../../api/graphql-api.md)

### Group-level Export

#### Start Export

```
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/Group/<group-id>/$export
```

Starts a group-level export for the specified organization and group.

#### Check Export Status

```
GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/$export-status/<export-id>
```

Checks the status of an export job for the specified organization.

#### Cancel Export

```
DELETE <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/$export-status/<export-id>
```

Cancels an active export job for the specified organization.

See also [$export](../../../api/bulk-api/export.md#group-level-export)

## Authentication

### Login View

```
GET <AIDBOX_BASE_URL>/Organization/<org-id>/auth/login
```

Returns the login view for the specified organization.

### Login

```
POST <AIDBOX_BASE_URL>/Organization/<org-id>/auth/login
```

Performs loginfor the specified organization.

## 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`.

{% hint style="warning" %}
Update and delete operations are not allowed from nested organizations' APIs. To update or delete `shared`resource use its root organization API.
{% endhint %}

### Create a shared resource

To create a shared resource, use the `https://aidbox.app/tenant-resource-mode` extension.

```http
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:

```http
GET /Organization/org-b/fhir/Practitioner/prac-1
```

## System-shared resource mode

While the [shared resource mode](#shared-resource-mode) makes a resource visible only to child organizations of the owning organization, the **system-shared** mode makes a resource visible to **all** organizations globally.

System-shared resources are created at the root level (via the root FHIR API, not through any organization-scoped API) and are automatically visible to every organization in the system.

{% hint style="warning" %}
System-shared resources are **read-only** from organization-scoped APIs. To create, update, or delete a system-shared resource, use the root API. Attempting to modify a system-shared resource from an organization API returns `403 Forbidden`.
{% endhint %}

{% hint style="warning" %}
A system-shared resource **cannot** have an organization binding (`tenant-organization-id` extension). Attempting to create a resource with both `system-shared` mode and an organization reference returns `422 Unprocessable Entity`.
{% endhint %}

### Create a system-shared resource

Use the root FHIR API with the `https://aidbox.app/tenant-resource-mode` extension set to `system-shared`:

```http
PUT /fhir/Practitioner/global-prac-1
content-type: text/yaml

meta:
  extension:
  - url: https://aidbox.app/tenant-resource-mode
    valueString: "system-shared"
name:
- given: [Global]
  family: Practitioner
```

### Access system-shared resource from any organization

The resource is now readable from any organization-scoped API:

```http
GET /Organization/org-a/fhir/Practitioner/global-prac-1
# 200 OK

GET /Organization/org-b/fhir/Practitioner/global-prac-1
# 200 OK

GET /Organization/org-d/fhir/Practitioner/global-prac-1
# 200 OK
```

System-shared resources also appear in search results for all organizations:

```http
GET /Organization/org-b/fhir/Practitioner?_sort=id
# Returns: global-prac-1 + org-b's own practitioners
```

### Comparison: shared vs system-shared

| Behavior | `shared` | `system-shared` |
|---|---|---|
| Visibility | Child organizations only | All organizations |
| Created via | Organization-scoped API | Root API only |
| Organization binding | Required (belongs to an org) | Forbidden (no org binding) |
| Modifiable from org API | No (read-only from children) | No (read-only from all orgs) |
| Use case | Share within org hierarchy | Global templates / references |

## See also

{% content-ref url="../../../tutorials/security-access-control-tutorials/how-to-enable-hierarchical-access-control.md" %}
[how-to-enable-hierarchical-access-control.md](../../../tutorials/security-access-control-tutorials/how-to-enable-hierarchical-access-control.md)
{% endcontent-ref %}
