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:

Setup

Set up a database with sample data and a user-defined role.

  1. Log into Fauna using the CLI:

    fauna cloud-login
  2. Create an abac database:

    fauna create-database abac
  3. Create an abac directory and navigate to it:

    mkdir abac
    cd abac
  4. 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.

  5. Navigate to the schema directory:

    cd schema
  6. 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)
      })
    }
  7. 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
      }
    }
  8. Save collections.fsl and roles.fsl. Then push the schema to Fauna:

    fauna schema push

    When prompted, accept and push the changes. This creates the collections and role.

  9. Start a shell session in the Fauna CLI:

    fauna shell

    The shell runs with the default admin key you created when you logged in using fauna cloud-login.

  10. Enter editor mode to run multi-line queries:

    > .editor
  11. 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.

  1. 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.

  2. 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.

  1. Start a new shell session using the token secret:

    fauna shell --secret <TOKEN_SECRET>
  2. 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 a date 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"
        },
        ...
      ]
    }
  3. 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.

  1. 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
      }
    }
  2. Save roles.fsl and push the schema to Fauna:

    fauna schema push
  3. 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.

  4. Next, add the title field to the identity document. Start a shell session using your admin key:

    fauna shell
  5. 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"
    })
  6. 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.

  1. 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
      }
    }
  2. Save roles.fsl and push the schema to Fauna:

    fauna schema push
  3. Test the user’s access.

    The query returns a permissions-related error. The token’s identity document doesn’t have the required schedule field.

  4. Next, add the schedule field to the identity document. Start a shell session using your admin key:

    fauna shell
  5. 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
    })
  6. 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.

  1. 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())
        }
      }
    }
  2. 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.

  1. Start a shell session with the user’s token secret:

    fauna shell --secret <TOKEN_SECRET>
  2. 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 a date of tomorrow, the query returns an empty set. The user can only fetch Appointment documents with a date of today.

    {
      data: []
    }
  3. Run the following query to get Appointment documents with a date 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 the frontdeskAppts() function.

  • Add a privilege predicate to only allow users to call frontdeskAppts() with today’s date.

  1. In the schema directory, create functions.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()
    }
  2. 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())
        }
      }
    }
  3. Save functions.fsl and roles.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.

  1. Start a shell session with the user’s token secret:

    fauna shell --secret <TOKEN_SECRET>
  2. Run the following query to call frontdeskAppts() with today’s date:

    frontdeskAppts(Date.today())

    The returned array only contains the patient and date fields:

    [
      {
        patient: "Alice Appleseed",
        date: Date("2024-05-24")
      },
      {
        patient: "Bob Brown",
        date: Date("2024-05-24")
      }
    ]
  3. 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 call frontdeskAppts() with today’s date.

  4. 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 to Appointment 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!