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. To install the CLI, see Installation and configuration.
-
Familiarity with the following Fauna features:
Setup
Set up a database with sample data and a user-defined role.
-
Log into Fauna using the CLI:
fauna cloud-login
-
Create an
abac
database:fauna create-database abac
-
Create an
abac
directory and navigate to it:mkdir abac cd abac
-
Initialize a project:
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 appointment data. collection Appointment { // Defines the `byDate()` index. // Use the index to get `Appointment` documents // by `date` value. index byDate { terms [.date] } // 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 } }
-
Save
collections.fsl
androles.fsl
. Then push the schema to Fauna:fauna schema push
When prompted, accept and push the changes. 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 a `Staff` collection document. Staff.create({ name: "John Doe", office: "Acme Medical" }) // Creates an `Appointment` document. Appointment.create({ patient: "Alice Appleseed", // Set appointment for now time: Time.now(), reason: "Allergy test", }) Appointment.create({ patient: "Bob Brown", // Set appointment for 30 minutes from now time: Time.now().add(30, "minutes"), reason: "Fever" }) Appointment.create({ patient: "Carol Clark", // Set appointment for tomorrow time: Time.now().add(1, "days"), reason: "Foot x-ray" })
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
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 `byDate()` index to get `Appointment` // documents with a `date` of today. Appointment.byDate(Date.today())
If successful, the query returns
Appointment
documents with adate
of today:{ data: [ { id: "398798911878201421", coll: Appointment, ts: Time("2099-05-24T20:38:49.670Z"), date: Date("2099-05-24"), patient: "Alice Appleseed", time: Time("2099-05-24T20:48:49.640019Z"), reason: "Allergy test" }, ... ] }
-
Press Ctrl+D to exit the shell.
Check identity-based attributes
Add a membership predicate to the frontdesk
role. The predicate only assigns
the role to Staff
users with a specific job title.
-
Edit
roles.fsl
as follows:role frontdesk { // If the predicate is `true`, // assign the `frontdesk` role to tokens with // identity documents in the `Staff` collection. membership Staff { predicate (staff => { // Check the identity document has // a `title` of `Receptionist`. staff.title == "Receptionist" }) } privileges Appointment { read } }
-
Save
roles.fsl
and push the schema to Fauna:fauna schema push
-
Test user access using the steps in Test user access.
The query returns a permissions-related error. The token’s identity document doesn’t have the required
title
value. -
Next, add the
title
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 `title` of `Receptionist` to // the above document. user!.update({ title: "Receptionist" })
-
Exit the admin shell session and test the user’s access.
The query should run successfully.
Check environmental attributes
Predicates can contain multiple chained statements.
Update the above 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 => { staff.title == "Receptionist" && // 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 } }
-
Save
roles.fsl
and push the schema to Fauna:fauna schema push
-
The query returns a permissions-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 data attributes
Add a privilege predicate to only allow the frontdesk
role to read
Appointment
documents with a date
of today’s date.
-
Edit
roles.fsl
as follows:role frontdesk { membership Staff { predicate (staff => { staff.title == "Receptionist" && 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 { // Only allow access to `Appointment` documents with // today's `date` predicate (doc => doc.date == Date.today()) } } }
-
Save
roles.fsl
and push the schema to Fauna:fauna schema push
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:
// Get `Appointment` documents with a `date` of tomorrow. Appointment.byDate(Date.today().add(1, "days"))
Although the sample data includes
Appointment
documents with adate
of tomorrow, the query returns an empty set. The user can only fetchAppointment
documents with adate
of today.{ data: [] }
-
Run the following query to get
Appointment
documents with adate
of today:Appointment.byDate(Date.today())
The query should run successfully.
Limit access with UDFs
For more control, you could only allow users to access Appointment
data using
a UDF. The UDF 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.
-
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) { // Uses the `byDate()` index to get // `Appointment` documents by date. // Returned documents only contain // the `patient` and `date` fields. let appt = Appointment.byDate(date) { patient, date } // Output results as an array appt.toArray() }
-
Edit
roles.fsl
as follows:role frontdesk { membership Staff { predicate (staff => { staff.title == "Receptionist" && 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 { // Only allow users to pass today's date to the function. predicate (date => date == Date.today()) } } }
-
Save
functions.fsl
androles.fsl
. Then push the schema to Fauna:fauna schema push
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 to call
frontdeskAppts()
with today’s date:frontdeskAppts(Date.today())
The returned array only contains the
patient
anddate
fields:[ { patient: "Alice Appleseed", date: Date("2024-05-24") }, { patient: "Bob Brown", date: Date("2024-05-24") } ]
-
Run the following query in editor mode:
// Get tomorrow's date. let date = Date.today().add(1, 'days') // Pass tomorrow's date to the // `frondeskAppts` function call. frontdeskAppts(date)
The query returns a permissions-related error. The
frontdesk
role only allows you to callfrontdeskAppts()
with today’s date. -
Run the following query in editor mode:
// Attempt to directly read // `Appointment` collection documents Appointment.byDate(Date.today())
The query returns a permissions-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!