πŸŽ“
Sample: Patient can see their own data

About this tutorial

In this tutorial, you will learn how to manage user access to patient resources.

Prerequisites

To complete this tutorial, you should install Postman and get access to the Aidbox Console (see here how to install your Aidbox instance) .
Once you access the Aidbox REST Console, load resources that you need to work with policies:
1
POST /$import
2
​
3
id: patient_import
4
inputFormat: application/fhir+ndjson
5
contentEncoding: gzip
6
mode: bulk
7
inputs:
8
- resourceType: Client
9
url: https://storage.googleapis.com/aidbox-public/demo/client.ndjson.gz
10
- resourceType: User
11
url: https://storage.googleapis.com/aidbox-public/demo/user.ndjson.gz
12
- resourceType: Patient
13
url: https://storage.googleapis.com/aidbox-public/demo/patient.ndjson.gz
14
- resourceType: Encounter
15
url: https://storage.googleapis.com/aidbox-public/demo/encounter.ndjson.gz
16
- resourceType: Observation
17
url: https://storage.googleapis.com/aidbox-public/demo/observation.ndjson.gz
18
- resourceType: Practitioner
19
url: https://storage.googleapis.com/aidbox-public/demo/practitioner.ndjson.gz
Copied!

Structure of imported resources

In the previous step, we have imported a client that will authenticate users and two users with corresponding sets of related resources shown on the picture below. Overlapping outlines indicates the relation between enclosed resources. A similar diagram applies to User-2.

User Loginβ€Œ

Now you can use Postman to log in as a user. In this example, we log in as User-1.
Request
Response
1
POST /auth/token
2
​
3
{
4
"client_id": "myapp"
5
"client_secret": "verysecret"
6
"username": "patient-user"
7
"password": "admin"
8
"grant_type": "password"
9
}
Copied!
1
{
2
"token_type": "Bearer",
3
"userinfo": {
4
"data": {
5
"roles": ["Patient"],
6
"patient_id": "new-patient"
7
},
8
"resourceType": "User",
9
"id": "patient-user",
10
"meta": {
11
"lastUpdated": "2020-11-06T12:18:19.530001Z",
12
"createdAt": "2020-11-03T14:09:03.010136Z",
13
"versionId": "426"
14
}
15
},
16
"access_token": "MjYzOTkyZDEtODg4ZC00NTBlLTgxNDEtNjIzM2Y4NWQ1M2Vk"
17
}
Copied!
Notice the patient_id field of userinfo . This is the id of the Patient resource associated with our user. It will be used further in Access Policies to decide if access should be granted or not. In general, you need to specify data.patient_id: some_patient_id in your User resource to establish a relation to a Patient resource.β€Œ
The access-token field of user-info will be needed to perform requests on behalf of our User. See here how to perform user request with a token.
At this point there are no access policies that allow the user to access any resources. So all attempts to make requests for Resources will be denied.

Patient Resource access

Let's add our first policy that will grant us access to the Patient resource associated with our user.
Request
Response
1
POST /AccessPolicy
2
​
3
id: patient-access
4
engine: matcho
5
matcho:
6
uri: '#/Patient/.*'
7
params:
8
resource/id: .user.data.patient_id
9
request-method: get
Copied!
1
engine: matcho
2
matcho:
3
uri: '#/Patient/.*'
4
params:
5
resource/id: .user.data.patient_id
6
request-method: get
7
id: patient-access
8
resourceType: AccessPolicy
9
meta:
10
lastUpdated: '2020-11-10T15:00:59.497835Z'
11
createdAt: '2020-11-10T15:00:59.497835Z'
12
versionId: '110'
Copied!
Here we specified that Access Policy would grant GET access to a URI that matches #/Patient/.* regex if the request parameter named resource/id matches data.patient value of the user that makes the request.β€Œ
So now we can read our patient. The part of the URL after /Patient/, namely new-patient, is parsed by Access Policy engine as the resource/id parameter of the request:
Request
Response
1
GET /Patient/new-patient
Copied!
1
{
2
"name": [
3
{
4
"given": ["Luke"]
5
},
6
{
7
"family": "Skywalker"
8
}
9
],
10
"gender": "male",
11
"birthDate": "2145-08-12",
12
"id": "new-patient",
13
"resourceType": "Patient",
14
"meta": {
15
"lastUpdated": "2020-11-10T13:51:16.780576Z",
16
"createdAt": "2020-11-10T11:38:52.402256Z",
17
"versionId": "83"
18
}
19
}
Copied!
You can check that access to any other existing Patient resource, for instance, that one with id new-patient1, will be denied.

Encounter access

Now let's give our user the ability to retrieve all encounters where they are referred to as a subject:
Request
Response
1
POST /AccessPolicy
2
​
3
id: search-patient-encounter
4
engine: matcho
5
matcho:
6
uri: /Encounter
7
params:
8
patient: .user.data.patient_id
9
request-method: get
Copied!
1
engine: matcho
2
matcho:
3
uri: /Encounter
4
params:
5
patient: .user.data.patient_id
6
resourceType: AccessPolicy
7
id: search-patient-encounter
8
meta:
9
lastUpdated: '2020-11-05T15:28:58.054136Z'
10
createdAt: '2020-11-05T15:28:58.054136Z'
11
versionId: '0'
Copied!
And this policy is a bit tricky. The allowed URI is /Encounter and it doesn't contain any additional parts that could be identified as request parameters as in the previous case. So, in order to provide the required request parameter patient to the Access Policy matching engine, we have to specify it as the query parameter of our request. And after the Access Policy engine allows such a request, the Search Engine comes into play. It filters out encounters that do not match the condition of patient = our-patient-id. To know more about how the AidBox Search works, see the Search section. To know more about the available search parameters, refer to the Search Parameters section of the FHIR documentation for the resource of interest.
Finally, we can make a request for the list of patient encounters.
Request
Response
1
GET /Encounter?patient=new-patient
Copied!
1
{
2
"query-time": 7,
3
"meta": {
4
"versionId": "155"
5
},
6
"type": "searchset",
7
"resourceType": "Bundle",
8
"total": 1,
9
"link": [
10
{
11
"relation": "first",
12
"url": "/Encounter?patient=new-patient&page=1"
13
},
14
{
15
"relation": "self",
16
"url": "/Encounter?patient=new-patient&page=1"
17
}
18
],
19
"query-timeout": 60000,
20
"entry": [
21
{
22
"resource": {
23
"class": {
24
"code": "AMB"
25
},
26
"status": "planned",
27
"subject": {
28
"id": "new-patient",
29
"resourceType": "Patient"
30
},
31
"participant": [
32
{
33
"individual": {
34
"id": "practitioner-1",
35
"resourceType": "Practitioner"
36
}
37
}
38
],
39
"id": "enc1",
40
"resourceType": "Encounter",
41
"meta": {
42
"lastUpdated": "2020-11-10T11:11:39.464261Z",
43
"createdAt": "2020-11-06T19:14:46.247628Z",
44
"versionId": "150"
45
}
46
},
47
"fullUrl": "/Encounter/enc1",
48
"link": [
49
{
50
"relation": "self",
51
"url": "/Encounter/enc1"
52
}
53
]
54
}
55
],
56
"query-sql": [
57
"SELECT \"encounter\".* FROM \"encounter\" WHERE \"encounter\".resource @> ? LIMIT ? OFFSET ? ",
58
"{\"subject\":{\"id\":\"new-patient\",\"resourceType\":\"Patient\"}}",
59
100,
60
0
61
]
62
}
Copied!

Observation access

Read access

Granting access to observations is similar to the previous case. We just add another policy that looks just like the previous one, but matches against another URI. It is so similar that we should stop there and think a little about what happens if we want to grant read access to more resources β€” we will end up with a bunch of almost indistinguishable policies. A better approach, in this case, is to use the CompartmentDefinition resource.
Request
1
POST /fhir/CompartmentDefinition
2
​
3
id: patient
4
url: http://hl7.org/fhir/CompartmentDefinition/patient
5
code: Patient
6
search: true
7
status: draft
8
resource:
9
- code: Encounter
10
param:
11
- patient
12
- code: Observation
13
param:
14
- subject
15
- performer
Copied!
Now, when we've created a CompartmentDefinition resource, we can access patient-related resources with such requests: GET /Patient/{patient-id}/{resource}. To know in detail about how compartments work, see the Compartments tutorial.
And that's it! We don't even need to add more policies, since we already have the policy that allows the user to access URIs that match /Patient/.* regex.
Request
Response
1
GET /Patient/new-patient/Observation
Copied!
1
{
2
"query-time": 7,
3
"meta": {
4
"versionId": "171"
5
},
6
"type": "searchset",
7
"resourceType": "Bundle",
8
"total": 2,
9
"link": [
10
{
11
"relation": "first",
12
"url": "/Observation?_filter=subject eq 'new-patient' or performer eq 'new-patient'&page=1"
13
},
14
{
15
"relation": "self",
16
"url": "/Observation?_filter=subject eq 'new-patient' or performer eq 'new-patient'&page=1"
17
}
18
],
19
"query-timeout": 60000,
20
"entry": [
21
{
22
"resource": {
23
"class": {
24
"coding": [
25
{
26
"code": "11557-6"
27
}
28
]
29
},
30
"status": "registered",
31
"subject": {
32
"id": "new-patient",
33
"resourceType": "Patient"
34
},
35
"performer": [
36
{
37
"id": "practitioner-1",
38
"resourceType": "Practitioner"
39
}
40
],
41
"resourceType": "Observation",
42
"id": "observation-1",
43
"meta": {
44
"lastUpdated": "2020-11-06T19:14:46.078643Z",
45
"createdAt": "2020-11-06T19:14:46.078643Z",
46
"versionId": "0"
47
}
48
},
49
"fullUrl": "/Observation/observation-1",
50
"link": [
51
{
52
"relation": "self",
53
"url": "/Observation/observation-1"
54
}
55
]
56
},
57
{
58
"resource": {
59
"class": {
60
"coding": [
61
{
62
"code": "11557-6"
63
}
64
]
65
},
66
"status": "registered",
67
"subject": {
68
"id": "new-patient",
69
"resourceType": "Patient"
70
},
71
"performer": [
72
{
73
"id": "new-patient",
74
"resourceType": "Patient"
75
}
76
],
77
"resourceType": "Observation",
78
"id": "observation-3",
79
"meta": {
80
"lastUpdated": "2020-11-06T19:14:46.078643Z",
81
"createdAt": "2020-11-06T19:14:46.078643Z",
82
"versionId": "0"
83
}
84
},
85
"fullUrl": "/Observation/observation-3",
86
"link": [
87
{
88
"relation": "self",
89
"url": "/Observation/observation-3"
90
}
91
]
92
}
93
],
94
"query-sql": [
95
"SELECT \"observation\".* FROM \"observation\" WHERE (\"observation\".resource @> ? OR \"observation\".resource @> ?) LIMIT ? OFFSET ? ",
96
"{\"subject\":{\"id\":\"new-patient\"}}",
97
"{\"performer\":[{\"id\":\"new-patient\"}]}",
98
100,
99
0
100
]
101
}
Copied!
If we want to grant access to some other resource, we just need to add it to the CompartmentDefinition resource that we've created earlier. See FHIR documentation to know what resources can be added to a patient compartment. And we can get rid of the Access Policy that was previously created for encounters.

Write access

The user should be able to create their own observation, e.g., to report blood sugar level. The following policy manages this case:
Request
1
POST /AccessPolicy
2
​
3
id: create-patient-observation
4
engine: matcho
5
matcho:
6
uri: '/Observation'
7
body:
8
subject:
9
id: .user.data.patient_id
10
resourceType: Patient
11
performer:
12
$contains:
13
id: .user.data.patient_id
14
resourceType: Patient
15
request-method: post
Copied!
With this policy, we can only create observations where the subject and performer must be the user's patient.
Request
1
POST /Observation
2
​
3
{
4
"id": "observation-3",
5
"code": {
6
"coding": [
7
{
8
"code": "11557-6"
9
}
10
]
11
},
12
"status": "registered",
13
"subject": {
14
"id": "new-patient",
15
"resourceType": "Patient"
16
},
17
"performer": [
18
{
19
"id": "new-patient",
20
"resourceType": "Patient"
21
}
22
]
23
}
Copied!
Now it's time to make an important note. In general, it is not possible to use some kind of CompartmentDefinition approach to grant write access to many resources at once, as we did previously for read access. That's because resources may require sophisticated logic to define which part of a resource could have write access and which not. Such logic may even lie beyond the abilities of the Access Control mechanism and in this case custom API is the only resort. But in a quite simple scenario like the creation of observation, Access Policies are helpful.
Let's create a new policy that allows our user to update their observations through the PATCH method. Matcho engine is no longer enough to make a rule for this kind of request since it only relies on the request and the user parameters. Now we need to peek into the requested resource to understand if it is related to our user and could be patched.
TODO: describe the necessity and benefits of the json-schema engine.
Request
1
POST /AccessPolicy
2
​
3
id: patch-observation
4
link:
5
- id: Patch
6
resourceType: Operation
7
engine: complex
8
and:
9
- engine: sql
10
sql:
11
query: >
12
select true from observation
13
where resource#>>'{subject,id}' = {{user.data.patient_id}}
14
and id = {{params.resource/id}}
15
and resource->'performer' @> jsonb_build_array(jsonb_build_object('resourceType', 'Patient', 'id', {{user.data.patient_id}}::text))
16
- engine: json-schema
17
schema:
18
properties:
19
body:
20
properties:
21
subject:
22
optional: true
23
properties:
24
id:
25
constant:
26
$data: '#/user/data/patient_id’
Copied!
Now we can try to update our patient and the patient related to the User-2 and observe the difference in the responses.

Access to the next of kin records

Access policies depend a lot on how we model our resources. FHIR doesn't provide convenient facilities to make relations between patients. The easiest way to add such relations is to enhance a User resource with the list of related patients. Let's define that Patient-2 is related to User-1.
Request
1
PATCH /User/patient-user
2
​
3
data:
4
related_patients:
5
- new-patient1
Copied!
To grant User-1 access to related patients, we should simply update patient-access policy.
Request
1
PUT /AccessPolicy/patient-access
2
​
3
engine: matcho
4
matcho:
5
uri: '#/Patient/.*'
6
user:
7
data:
8
$one-of:
9
- related_patients:
10
$contains: .params.resource/id
11
- patient_id: .params.resource/id
12
request-method: get
Copied!
Last modified 22d ago