Control access with ABAC
In Fauna, you implement attribute-based access control (ABAC) using role-related predicates. These predicates let you assign roles and grant privileges based on attributes.
You can further limit access using user-defined functions (UDFs). UDFs give you granular control over the way data is queried and returned.
In this tutorial, you’ll use ABAC and UDFs to dynamically control access to medical appointment data.
Before you start
To complete this tutorial, you’ll need:
-
The Fauna CLI
-
Familiarity with the following Fauna features:
Setup
Set up a database with sample data and a user-defined role.
-
Log in to Fauna using the CLI:
fauna cloud-login
When prompted, enter:
-
Endpoint name:
cloud
(Press Enter)An endpoint defines the settings the CLI uses to run API requests against a Fauna account or database. See Endpoints.
-
Email address: The email address for your Fauna account.
-
Password: The password for your Fauna account.
-
Which endpoint would you like to set as default? The
cloud-*
endpoint for your preferred Region Group. For example, to use the US Region Group, usecloud-us
.
fauna cloud-login
requires an email and password login. If you log in to Fauna using GitHub or Netlify, you can enable email and password login using the Forgot Password workflow. -
-
Create an
abac
directory and navigate to it:mkdir abac cd abac
-
Create an
abac
database:fauna create-database abac
-
Initialize a project directory:
fauna project init
When prompted, use:
-
schema
as the schema directory. The command creates the directory. -
dev
as the environment name. -
The default endpoint.
-
abac
as the database.
-
-
Navigate to the
schema
directory:cd schema
-
Create the
collections.fsl
file and add the following schema to it:// Stores identity documents for staff end users. collection Staff { // Each `Staff` collection document must have a // unique `name` field value. unique [.name] // Defines the `byName()` index. // Use the index to get `Staff` documents by `name` value. index byName { terms [.name] } } // Stores medical office data. collection Office { unique [.name] index byName { terms [.name] } } // Stores appointment data. collection Appointment { // Defines the `byDateAndOffice()` index. // Use the index to get `Appointment` documents by // `date` and `office` values. index byDateAndOffice { terms [.date, .office] } // Adds a computed `date` field. compute date = (appt => { // The `date` value is derived from each `Appointment` // document's `time` field. let timeStr = appt.time.toString() let dateStr = timeStr.split("T")[0] Date(dateStr) }) }
-
Create
roles.fsl
and add the following schema to it:// User-defined role for frontdesk staff. role frontdesk { // Assigns the `frontdesk` role to tokens with // an identity document in the `Staff` collection. membership Staff // Grants `read` access to // `Appointment` collection documents. privileges Appointment { read } // Grants `read` access to // `Office` collection documents. privileges Office { read } }
-
Save
collections.fsl
androles.fsl
. Then push the schema to Fauna:fauna schema push
When prompted, accept and stage the schema.
-
Check the status of the staged schema:
fauna schema status
-
When the status is
ready
, commit the staged schema to the database:fauna schema commit
The commit applies the staged schema to the database. This creates the collections and role.
-
Start a shell session in the Fauna CLI:
fauna shell
The shell runs with the default
admin
key you created when you logged in usingfauna cloud-login
. -
Enter editor mode to run multi-line queries:
> .editor
-
Run the following FQL query:
// Creates an `Office` collection document. // Stores a reference to the document in // the `acmeOffice` variable. let acmeOffice = Office.create({ name: "Acme Medical" }) let baysideOffice = Office.create({ name: "Bayside Hospital" }) // Creates a `Staff` collection document. Staff.create({ name: "John Doe" }) // Creates an `Appointment` collection document. Appointment.create({ patient: "Alice Appleseed", // Set appointment for now time: Time.now(), reason: "Allergy test", // Sets the previous `Office` collection document // as the `office` field value. office: acmeOffice }) Appointment.create({ patient: "Bob Brown", // Set the appointment for 30 minutes from now. time: Time.now().add(30, "minutes"), reason: "Fever", office: acmeOffice }) Appointment.create({ patient: "Carol Clark", // Set the appointment for 1 hour from now. time: Time.now().add(1, "hours"), reason: "Foot x-ray", office: baysideOffice }) Appointment.create({ patient: "Dave Dennis", // Set the appointment for tomorrow. time: Time.now().add(1, "days"), reason: "Foot x-ray", office: acmeOffice })
The query creates all documents but only returns the last document.
Create an authentication token
Create a token tied to a user’s Staff
identity document.
You typically create tokens using
credentials and login-related
UDFs. For an example, see Build an end-user authentication system. For simplicity, this
tutorial uses Token.create()
to create the
token instead.
-
Run the following query in the shell’s editor mode:
// Gets `Staff` collection documents with a `name` of `John Doe`. // Because `name` values are unique, use the first document. let document = Staff.byName("John Doe").first() // Create a token using the `Staff` document // as the identity document. Token.create({ document: document })
Copy the returned token’s
secret
. You’ll use the secret later in the tutorial. -
Press Ctrl+D to exit the shell.
Test user access
Use the token’s secret to run queries on the user’s behalf. Fauna assigns and evaluates the secret’s roles and privileges, including any predicates, at query time for every query.
-
Start a new shell session using the token secret:
fauna shell --secret <TOKEN_SECRET>
-
Run the following query in editor mode:
// Use the `byDateAndOffice()` index to get `Appointment` documents // with a: // - `date` value of today // - `office` value containing an `Office` collection document // with a `name` of "Acme Medical" Appointment.byDateAndOffice( Date.today(), Office.byName("Acme Medical").first() )
If successful, the query returns
Appointment
documents with adate
of today:{ data: [ { id: "400593000902688845", coll: Appointment, ts: Time("2099-06-13T15:55:06.320Z"), date: Date("2099-06-13"), patient: "Alice Appleseed", time: Time("2099-06-13T15:55:06.239185Z"), reason: "Allergy test", office: Office("400593000896397389") }, ... ] }
-
Press Ctrl+D to exit the shell.
Check environmental attributes
Add a membership predicate to only assign the frontdesk
role to
Staff
users during their scheduled work hours.
-
Edit
roles.fsl
as follows:role frontdesk { membership Staff { predicate (staff => { // Allow access after the user's // scheduled start hour in UTC. Time.now().hour >= staff.schedule[0] && // Disallow access after the user's // scheduled end hour in UTC. Time.now().hour < staff.schedule[1] }) } privileges Appointment { read } privileges Office { read } }
-
Save
roles.fsl
and push the schema to Fauna:fauna schema push
When prompted, accept and stage the schema.
-
Check the status of the staged schema:
fauna schema status
-
When the status is
ready
, commit the staged schema to the database:fauna schema commit
The commit applies the staged schema to the database.
-
The query returns a privileges-related error. The token’s identity document doesn’t have the required
schedule
field. -
Next, add the
schedule
field to the identity document. Start a shell session using youradmin
key:fauna shell
-
Run the following query in editor mode:
// Gets the token's identity document. let user = Staff.byName("John Doe").first() // Adds a `schedule` field to the above document. user?.update({ schedule: [8, 18] // 08:00 (8 AM) to 18:00 (6 PM) UTC })
-
Exit the admin shell session and test the user’s access.
If the current UTC time is within the user’s scheduled hours, the query should run successfully. If needed, you can repeat the previous step and adjust the user’s schedule.
Check identity-based attributes
Add a privilege predicate to only allow the frontdesk
role to read
Appointment
documents with the same office
as the user’s identity
document.
Fauna evaluates the privilege predicate at query time for every query. This
lets you tread the user’s identity document as "living" document. If the
office
value in the user’s identity document changes, their access to
Appointment
documents also changes.
-
Edit
roles.fsl
as follows:role frontdesk { membership Staff { predicate (staff => { Time.now().hour >= staff.schedule[0] && Time.now().hour < staff.schedule[1] }) } // If the predicate is `true`, // grant `read` access to `Appointment` collection documents. privileges Appointment { read { predicate (doc => // `Query.identity()` gets the token's `Staff` identity // document. The predicate checks that `Appointment` // document's `office` value is the same as the `Staff` // user's `office` value. doc.office == Query.identity()?.office ) } } privileges Office { read } }
-
Save
roles.fsl
and push the schema to Fauna:fauna schema push
When prompted, accept and stage the schema.
-
Check the status of the staged schema:
fauna schema status
-
When the status is
ready
, commit the staged schema to the database:fauna schema commit
The commit applies the staged schema to the database.
-
Test user access using the steps in Test user access.
The query returns an empty Set:
{ data: [] }
The token’s identity document doesn’t have the required
office
value. -
Next, add the
office
field to the identity document. Start a shell session using youradmin
key:fauna shell
-
Run the following query in editor mode:
// Gets the token's identity document. let user = Staff.byName("John Doe").first() // Get the first `Office` collection document with a // `name` of "Acme Medical". let acmeOffice = Office.byName("Acme Medical").first() // Updates the identity document to add the previous // `Office` document as the `office` field value. user!.update({ office: acmeOffice })
-
Exit the admin shell session and test the user’s access.
The query should run successfully.
-
Run the following query in editor mode:
// Use the `byDateAndOffice()` index to get `Appointment` documents // with a: // - `date` of tomorrow // - `office` containing the "Bayside Hospital" document in the // `Office` collection Appointment.byDateAndOffice( Date.today().add(1, "days"), Office.byName("Bayside Hospital").first() )
Although the sample data includes
Appointment
documents with the requestedoffice
, the query returns an empty Set. Thefrontdesk
role can only readAppointment
documents with the sameoffice
as the user.
Check data attributes
Update the privilege predicate to only allow the frontdesk
role to access
Appointment
documents with a date
of today’s date.
-
Edit
roles.fsl
as follows:role frontdesk { membership Staff { predicate (staff => { Time.now().hour >= staff.schedule[0] && Time.now().hour < staff.schedule[1] }) } // If the predicate is `true`, // grant `read` access to `Appointment` collection documents. privileges Appointment { read { predicate (doc => doc.office == Query.identity()?.office && // Only allow access to `Appointment` documents with // today's `date` doc.date == Date.today() ) } } privileges Office { read } }
-
Save
roles.fsl
and push the schema to Fauna:fauna schema push
When prompted, accept and stage the schema.
-
Check the status of the staged schema:
fauna schema status
-
When the status is
ready
, commit the staged schema to the database:fauna schema commit
The commit applies the staged schema to the database.
Re-test user access
Use the token’s secret to run queries on the user’s behalf. The user should only
be able to read Appointment
documents with a date
of today.
-
Start a shell session with the user’s token secret:
fauna shell --secret <TOKEN_SECRET>
-
Run the following query in editor mode:
// Use the `byDateAndOffice()` index to get `Appointment` documents // with a: // - `date` value of tomorrow // - `office` value containing an `Office` collection document // with a `name` of "Acme Medical" Appointment.byDateAndOffice( Date.today().add(1, "days"), Office.byName("Acme Medical").first() )
Although the sample data includes
Appointment
documents with adate
of tomorrow, the query returns an empty Set. The user can only readAppointment
documents with adate
of today. -
Run the following query in editor mode to get
Appointment
documents with adate
of today:// Use the `byDateAndOffice()` index to get `Appointment` documents // with a: // - `date` value of today // - `office` value containing an `Office` collection document // with a `name` of "Acme Medical" Appointment.byDateAndOffice( Date.today(), Office.byName("Acme Medical").first() )
The query should run successfully.
Limit access with UDFs
For more control, you could only allow users to access Appointment
data using
a UDF.
With a UDF, you can remove direct access to Appointment
collection documents.
The UDF also lets you customize the format of returned data. In this case, we’ll
exclude the reason
field from results.
First, define a frontdeskAppts()
UDF. Then update the frontdesk
role to:
-
Remove privileges for the
Appointment
collection. -
Add a
call
privilege for thefrontdeskAppts()
function. -
Add a privilege predicate to only allow users to call
frontdeskAppts()
with today’s date and the user’s office.
-
In the
schema
directory, createfunctions.fsl
and add the following schema to it:// Defines the `frontdeskAppts()` function. // The function gets appointments for a specific date. // Runs with the built-in `server-readonly` role's privileges. @role("server-readonly") function frontdeskAppts(date, office) { // Uses the `byDateAndOffice()` index to get `Appointment` // documents by date and office. Returned documents only // contain the `patient` and `date` fields. let appt = Appointment.byDateAndOffice(date, office) { patient, date } // Output results as an Array appt.toArray() }
-
Edit
roles.fsl
as follows:role frontdesk { membership Staff { predicate (staff => { Time.now().hour >= staff.schedule[0] && Time.now().hour < staff.schedule[1] }) } // Removed `Appointment` collection privileges // If the predicate is `true`, // grant the ability to call the `frondeskAppts` function. privileges frontdeskAppts { call { predicate ((date, office) => // Only allow users to pass today's date to the function. date == Date.today() && // Only allow users to pass their office to the function. office == Query.identity()?.office ) } } privileges Office { read } }
-
Save
functions.fsl
androles.fsl
. Then push the schema to Fauna:fauna schema push
When prompted, accept and stage the schema.
-
Check the status of the staged schema:
fauna schema status
-
When the status is
ready
, commit the staged schema to the database:fauna schema commit
The commit applies the staged schema to the database.
Re-test user access
Use the token’s secret to run queries on the user’s behalf. The user should only
be able to call frontdeskAppts()
with today’s date.
-
Start a shell session with the user’s token secret:
fauna shell --secret <TOKEN_SECRET>
-
Run the following query in editor mode. The query calls
frontdeskAppts()
with today’s date and the user’s office.// Get today's date. let date = Date.today() // Get the first `Office` collection document with a // `name` of "Acme Medical". let office = Office.byName("Acme Medical").first() // Pass tomorrow's date to the // `frondeskAppts` function call. frontdeskAppts(date, office)
The returned Array only contains the
patient
anddate
fields:[ { patient: "Alice Appleseed", date: Date("2099-06-13") }, { patient: "Bob Brown", date: Date("2099-06-13") } ]
-
Run the following query in editor mode:
// Get tomorrow's date. let date = Date.today().add(1, 'days') // Get the first `Office` collection document with a // `name` of "Acme Medical". let office = Office.byName("Acme Medical").first() // Pass tomorrow's date to the // `frondeskAppts` function call. frontdeskAppts(date, office)
The query returns a privileges-related error. The
frontdesk
role only allows you to callfrontdeskAppts()
with today’s date. -
Run the following query in editor mode:
// Get tomorrow's date. let date = Date.today() // Get the first `Office` collection document with a // `name` of "Bayside Hospital". let office = Office.byName("Bayside Hospital").first() // Pass tomorrow's date to the // `frondeskAppts` function call. frontdeskAppts(date, office)
The query returns a privileges-related error. The
frontdesk
role only allows you to callfrontdeskAppts()
with user’s office. -
Run the following query in editor mode:
// Attempt to directly read `Appointment` collection documents. Appointment.byDateAndOffice( Date.today(), Office.byName("Acme Medical").first() )
The query returns a privileges-related error. The
frontdesk
role does not grant direct access toAppointment
collection documents.
Is this article helpful?
Tell Fauna how the article can be improved:
Visit Fauna's forums
or email docs@fauna.com
Thank you for your feedback!