Access Policies
​Aidbox provides a flexible model to customise request authorization rules. User is allowed to declare a set of checks for all incoming requests. If the incoming request satisfies those checks, it's considered authorised and being processed further. Otherwise the request is denied and the client gets 403 Unauthorized. Such checks are declared with AccessPolicy resource.
AccessPolicy resource has the following structure:
1
resourceType: AccessPolicy
2
description: policy description text
3
​
4
# type of evaluation engine
5
engine: allow | sql | json-schema | complex
6
​
7
# JSON Schema for `json-schema` engine
8
schema: {}
9
​
10
# SQL string for `sql` engine
11
sql:
12
query: "SELECT true FROM ..."
13
​
14
# References to either Client, User or Operation resources
15
link:
16
- { resourceType: Operation, id: op-id }
17
- { resourceType: User, id: user-1 }
18
- { resourceType: Client, id: client-1 }
Copied!
It supports five evaluation modes (engines): SQL, JSON Schema, Allow, Matcho and Complex Engine. Evaluation engine specifies how checks are expressed: with a SQL statement, with a JSON Schema, or as a list of allowed endpoints.

Request Object Structure

Aidbox evaluates AccessPolicy against a request object, a data structure representing incoming HTTP request. It has the following structure:
1
# Request body (for PUT or POST)
2
body: null
3
​
4
# Parsed query-string params merged with URL params extracted by routing engine
5
params: {__debug: policy, type: Patient}
6
​
7
# Request method (get/post/put/delete)
8
request-method: get
9
​
10
# Request URI (no query string)
11
uri: /fhir/Patient
12
​
13
# Object containing current User resource (if any)
14
user:
15
data:
16
patient_id: 42
18
id: b66bc7c5-1a56-422f-8cf8-e64469135ce2
19
meta:
20
lastUpdated: '2018-10-31T13:43:18.566Z'
21
tag:
22
- code: updated
23
system: https://aidbox.io
24
versionId: '6'
25
phone: 123-123-123
26
resourceType: User
27
​
28
# Parsed JWT claims (if any)
29
jwt: {sub: xxxxxxx, jti: xxxxxxx, iss: aidbox,
30
iat: 15409xxxxx, exp: 15409xxxxx}
31
​
32
# Query string
33
query-string: __debug=policy
34
​
35
# Client's IP address
36
remote-addr: 10.128.0.6
37
​
38
# Client resource (if any)
39
client:
40
id: b4930671-410c-462b-8b12-23cdef91af0c
41
meta:
42
lastUpdated: '2018-10-31T13:38:08.982Z'
43
tag:
44
- code: created
45
system: https://aidbox.io
46
versionId: '4'
47
resourceType: Client
48
​
49
# Request scheme (http or https)
50
scheme: http
51
​
52
# Request headers
53
headers: {x-original-uri: '/fhir/Patient?__debug=policy', origin: 'https://ui.aidbox.app',
54
x-forwarded-host: xxxx.aidbox.app, host: xxxx.aidbox.app, user-agent: 'Mozilla/5.0
55
(Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100
56
Safari/537.36', content-type: 'text/yaml, application/json', x-forwarded-port: '443',
57
referer: 'https://ui.aidbox.app/', connection: close, accept: text/yaml, accept-language: 'en-US,en;q=0.9,ru;q=0.8',
58
authorization: Bearer xxxxxx,
59
x-forwarded-for: 10.128.0.6, accept-encoding: 'gzip, deflate, br', x-forwarded-proto: https,
60
x-scheme: https, x-real-ip: 10.128.0.6}
Copied!

Request Authorization Logic

AccessPolicy instance can be linked to User, Client or Operation resources with AccessPolicy.link resource. If AccessPolicy has no links, it's considered as global policy. To authorize a request, Aidbox uses AccessPolicies linked to the current request's User, Client and Operation plus all global policies.
It iterates through them, evaluating each AccessPolicy against current request object. If some policy validates the request (evaluation result is true), the request considered authorized and Aidbox stops further policies evaluation. If all policies denied the request (all of them evaluated to false), then Aidbox denies such request and responds with 403 Unauthorized.
Performance consideration: Link you policy to User, Client or Operation to reduce number of checks per request!
If no AccessPolicy instances exist for a box, all requests will be denied.

Debugging

JSON Schema Engine

JSON Schema engine allows to put JSON Schema under the AccessPolicy.schema element and this schema will be used to validate the request object. Currently supported JSON Schema version is draft-07.
Fields with empty values, such as [], {}, "", null, are removed before passing request into access policy processing. Make sure to add require check of the fields that are validated by a json schema

Example

The following policy requires request.params.resource/type to be present in a request and have the value "Organization":
1
resourceType: AccessPolicy
2
engine: json-schema
3
schema:
4
properties:
5
params:
6
required: [resource/type]
7
properties:
8
resource/type:
9
const:
10
Organization
Copied!

SQL Engine

SQL Engine executes SQL statement and uses its result as an evaluation result. The SQL statement should return a single row with just one column, i.e.:
1
SELECT true FROM patient WHERE id = {{jwt.patient_id}} LIMIT 1;
Copied!
SQL statement can include interpolations in double curly braces, like in the example above. String inside curly braces will be used as a path to get value from the request object.

Example

Assuming that User has a reference to Practitioner resource through User.data.practitioner_id element, the following policy allows requests only to /fhir/Patient/<patient_id> URLs, and only for those patients who have the Patient.generalPractitioner element referencing same practitioner as the current User. In other words, User as a Practitioner is only allowed to see patients who are referencing him with the Patient.generalPractitioner.
1
sql:
2
query: |
3
SELECT
4
{{user}} IS NOT NULL
5
AND {{user.data.practitioner_id}} IS NOT NULL
6
AND {{uri}} LIKE '/fhir/Patient/%'
7
AND resource->'generalPractitioner' @>
8
jsonb_build_array(jsonb_build_object('resourceType',
9
'Practitioner', 'id', {{user.data.practitioner_id}}::text))
10
FROM patient WHERE id = {{params.resource/id}};
11
engine: sql
12
id: practitioner-only-allowed-to-see-his-patients
13
resourceType: AccessPolicy
Copied!

Interpolation Rules

In your SQL query, you can parameterize with attributes from the request object using {{path}} syntax. For example, to get a role from user {data: {role: 'admin'}} you can write {{user.data.role}}. Parameter expressions are escaped by default to protect from SQL injection. If you want to make dynamic queries (parameterize table name, for example), you have to use {{!path}} syntax. For example, the expression SELECT true from {{!params.resource/type}} limit 1 with params = {resource/type: "Patient"} will be transformed into SELECT true from "patient". Such identifier names are double quoted and lower-cased by default.

Allow Engine

Allow Engine constantly evaluates to true regardless the content of the request object.

Complex Engine

Complex engine provides ability to include several checks into a single policy and apply "AND" / "OR" operator on results. It's allowed to use any policy engine to define a check, you can even use "complex" engine to get sub-expression:
1
and:
2
- { engine: "sql", sql: { query: "select true" } } # Check 1
3
- engine: complex
4
or:
5
- { engine: "sql", sql: { query: "select false" } } # Check 2
6
- { engine: "sql", sql: { query: "select false" } } # Check 3
7
​
8
engine: complex
9
id: complex-example-1
10
resourceType: AccessPolicy
Copied!
Policy in the example above represents the following logical expression: check 1 AND (check 2 OR check 3). It's forbidden to have both and and or keys on the same level.

Example

Let's split SQL Policy example into two separate checks and combine them with the AND operator:
1
and:
2
# Check: request.user && request.user.data.practitioner_id are required
3
- engine: json-schema
4
schema:
5
type: object
6
required: ["user"]
7
properties:
8
user:
9
type: object
10
required: ["data"]
11
properties:
12
data:
13
type: object
14
required: ["practitioner_id"]
15
​
16
# Check: Current practitioner is patient's generalPractitioner
17
- engine: sql
18
sql:
19
query: |
20
SELECT
21
{{uri}} LIKE '/fhir/Patient/%'
22
AND resource->'generalPractitioner' @>
23
jsonb_build_array(jsonb_build_object('resourceType',
24
'Practitioner', 'id', {{user.data.practitioner_id}}::text))
25
FROM patient WHERE id = {{params.resource/id}};
26
engine: complex
27
id: complex-example-2
28
resourceType: AccessPolicy
Copied!

Matcho Engine

This custom DSL engine has limited expressiveness, but is very compact and declarative. The general idea is pattern matching with few extensions.
1
resourceType: AccessPolicy
2
engine: matcho
3
matcho:
4
user:
5
# user.role should be equal to admin
6
role: admin
7
# user.data.practitioner_id should be present
8
data: {practitioner_id: present?}
9
# uri match regexp /Encounter/.*
10
uri: '#/Encounter.*'
11
# request method should be one of get or post
12
request-method: {$enum: ['get', 'post']}
13
params:
14
# parameter practitioner should be equal to user.data.practitioner_id
15
practitioner: .user.data.practitioner_id
Copied!
Match DSL definition:
  • If pattern (match) is object, search for inclusion of this object into subject. For example: {x: 1} matches {x: 1, y: 2 ....}. This algorithm is recursive β€” {a: {b: 5}} matches {a: {b: 5, ...}...}
  • Objects with special keys:
    • $enum β€” test subject is equal to one of items in the enumeration. {request-method: {$enum: ['get','post']}} matches {request-method: 'post'}
    • $contains β€” **** if a subject is a collection, then search at least one match. {type: {$contains: {system: 'loinc'}} matches {type: [{system: 'snomed'}, {system: 'loinc'}]}
    • $one-of β€” try to match one of patterns. {a: {$one-of: [{b: present?}, {c: present?}]} matches {a: {c: 5}}
    • $reference - parse Reference or string into aidbox format. Examples:
      • Parse Reference elements
        • parser: {reference: "Patient/pid"} => {id: "pid", resourceType: "Patient"}
        • {resource: {patient: {$reference: {id: '.user.data.patient_id'}}}
      • Parse reference string
        • "Patient/pid" => {id: "pid", resourceType: "Patient"}
        • {params: {subject: {$reference: {id: '.user.data.patient_id'}}}
    • $not - negate nested pattern. Example: $not: {status: private} matches {status: public} and doesn't match {status: private}. Be careful using $not as it is possible to create too permissive policy. See more info below
    • $every - for collection subject check that each item satisfies the specified pattern. Example: $every: {foo: bar} matches [{foo: bar}, {foo: bar, baz: quux}] and doesn't match [{foo: bar}, {foo: baz}]
  • For array, match the first item in the pattern with the first item in the subject. [1,2] matches [1,2,3...]
  • Primitive values (strings, numbers and booleans) are compared by value
  • If a string starts with '#' , it will be transformed into regex and matched as regex. {a: '#\\d+'} matches {a: '2345'}
  • If a string starts with '.' , it's interpreted as a pointer to another path in the subject to compare. For example: {params: {user_id: '.user.id'}} matches {user: {id: 1}, params: {user_id: 1}}, i.e. user.id == param.user_id
  • There are several special string literals postfixed with the ?
    • present? β€” matches the subject if it is not null, i.e. {a: 'present?'} matches {a: 5} or {a: {b: 6}}
    • nil? β€” matches if nil/null β€” {a: nil?} matches {b: 6}
    • not-blank? β€” matches not blank string.
Be careful using $not as it is possible to create too permissive policy, i.e. statement that something is not allowed means that everything else is allowed. Example:
1
resourceType: AccessPolicy
2
engine: matcho
3
matcho:
4
request-method: delete
5
uri: '#^/Patient.*#x27;
6
user: {$not: {data: {role: guest}}}
Copied!
While original intent was to forbid guest users to delete Patient resources this AccessPolicy allows to do DELETE /Patient/<id> for unauthorized users.
In this case it is better to explicitly list possible roles with $enum
Need more rules? Contact us on the telegram chat!
Here are some examples:
1
# only users with role admin
2
​
3
user: {role: {$contains: 'admin'}}
Copied!
1
# only authorized users get patients or encountres
2
​
3
user: present?
4
request-method: get
5
params:
6
'resource/type': {$enum: ['Patient', 'Encounter'}
Copied!
1
# search encounter with required parameter practitioner,
2
# wich should be equal to user.data.pract_id
3
​
4
user: {data: {pract_id: present?}}
5
uri: '/Encounter'
6
params:
7
practitioner: '.user.data.pract_id'
Copied!