RBAC with ACL
This guide is based on the Access control lists (ACL)
The goal
When a user with the role OrganizationAdmin
performs read request for some user like GET /User/
, Aidbox checks if organization.id
of the requester and the desired user has the same organization reference. If the organization is the same, Aidbox allows otherwise restricts access.
Init Aidbox configuration project
To set new Aidbox configuration project
It is important to syncronize directory and file name to the :ns
parameter of the configuration
Create an empty directory acl
mkdir acl
Create file system.edn
in new folder
cd acl && touch system.edn
Populate configuration file
In the following configuration project user and client credentials are written as plain text to simplify the topic. In real life scenarios it is important to define credentials with ENVs
{:ns acl.system
:import #{aidbox
aidbox.config
aidbox.rest
aidbox.rest.acl}
search-config
{:zen/tags #{aidbox.config/search}
:zen-fhir :disable
:resource-compat false
:engine :knife
:fhir-comparisons true
:default-params {:timeout 30
:total "none"
:count 100}
:chain {:subselect true}}
compatibility-config
{:zen/tags #{aidbox.config/compatibility}
:validation {:json-schema {:regex #{:fhir-datetime}}}
:auth {:pkce {:code-challenge {:s256 {:conformant true}}}}}
zen-config
{:zen/tags #{aidbox.config/config}
:search search-config
:compatibility compatibility-config
:fhir-version "4.0.1"
:compliant-mode-enabled? true
:override-createdat-url "http://fhir.aidbox.app/extension/createdat"
:correct-aidbox-format true
:disable-legacy-seed true}
seed-data
{:zen/tags #{aidbox/service}
:engine aidbox/seed-v2
:resources
{:Client {:root {:secret "secret"
:first_party true
:grant_types ["client_credentials" "basic"]}
:postman {:secret "secret"
:grant_types ["password" "basic"]}}
:Organization {:org-1 {:resourceType "Organization" :id "org-1"}
:org-2 {:resourceType "Organization" :id "org-2"}}
:User {:admin {:password "password"}
:admin-org-1 {:password "password" :organization {:resourceType "Organization" :id "org-1"}}
:user-org-1 {:organization {:resourceType "Organization" :id "org-1"}}
:user-org-2 {:organization {:resourceType "Organization" :id "org-2"}}}
:AccessPolicy {:admin-seed-policy
{:engine "allow"
:link [{:resourceType "User" :id "admin"}
{:resourceType "Client" :id "root"}]}
:org-admin-can-read-its-users
{:engine "matcho"
:roleName "OrganizationAdmin"
:matcho {:uri "#/User.+" :request-method "get"}}}
:Role {:OrganizationAdmin {:name "OrganizationAdmin"
:user {:resourceType "User" :id "admin-org-1"}}}}}
;; define additional filtering rules based
;; org-id-param value is taken from the requester user resource accroding to the :path
org-id-param
{:zen/tags #{aidbox.rest.acl/request-param}
:source-schema {:type zen/string}
:path [:user :organization :id]}
;; defined SQL statement injecting the org-id-param value
org-condition
{:zen/tags #{aidbox.rest.acl/sql-template}
:params {:org-id org-id-param}
:template "{{target-resource}}#>>'{organization,id}'={{params.org-id}}"}
;; attach injected SQL to the filter
user-filter
{:zen/tags #{aidbox.rest.acl/filter}
:expression org-condition}
;; create user-read operation & apply user-filer
user-read
{:zen/tags #{aidbox.rest/op}
:engine aidbox.rest.acl/read
:resource "User"
:format "aidbox"
:filter user-filter}
;; rebing standard User read operation
user-api
{:zen/tags #{aidbox.rest/api}
"User" {[:id] {:GET user-read}}}
;; attach api to the server
server
{:zen/tags #{aidbox/service}
:engine aidbox/http
:apis #{user-api}}
box
{:zen/tags #{aidbox/system}
:config zen-config
:services {:seed seed-data
:http server}}}
Check configuration works
Use you favorite REST client
Get access token for admin-org-1
user
POST [base-url]/auth/token
Content-Type: text/yaml
{
"client_id": "postman",
"client_secret": "secret",
"username": "admin-org-1",
"password": "password",
"grant_type": "password"
}
token_type: Bearer
userinfo:
organization: { id: org-1, resourceType: Organization }
id: admin-org-1
resourceType: User
sub: admin-org-1
need_patient_banner: true
access_token: MW...Ex
Read user belonging to the requester organization
GET [base-url]/User/user-org-1
Content-Type: text/yaml
authorization: "Bearer MW...Ex"
organization: { id: org-1, resourceType: Organization }
id: user-org-1
resourceType: User
Read attempt user from the different organization
GET [base-url]/User/user-org-2
Content-Type: text/yaml
authorization: "Bearer MW...Ex"
resourceType: OperationOutcome
id: deleted
text:
status: generated
div: "Resource User/user-org-2 not found"
issue:
- severity: fatal
code: deleted
diagnostics: "Resource User/user-org-2 not found"