Attribute-based access control (ABAC)

Attribute-based access control (ABAC) is a flexible, fine-grained strategy for managing identity-based operations within Fauna. ABAC extends the default Fauna authentication and authorization mechanisms.

ABAC is an extension of traditional role-based access control (RBAC), where roles can define privileges that can be dynamically determined based on any attribute of the actor attempting to access or modify data (e.g. have they completed a specific training?), any attribute of the data to be accessed or modified (for example, is the document in "review" state?), or contextual information available during a transaction (e.g. is the current time between 10am and 2pm?).

A role defines a set of privileges — specific actions that can be executed on specific resources — and membership — specific identities that should have the specified privileges.

ABAC role overview

Changes to identities or resources are reflected immediately, as ABAC is evaluated for every query. For example, if Carol’s manager membership is removed, she can no longer access manager-specific resources.

The identity of an actor is determined by the token included in the query sent to Fauna. When the token is valid, and the token’s associated identity is listed in a role’s membership, the query’s operations are evaluated against the privileges defined by the role; if the required privileges are granted, the query is permitted to execute.

ABAC roles configure membership and privileges for the current database. It is not possible to define roles that affect child databases.

If you have defined ABAC roles that should also apply to child databases, you must establish those roles in each child database that requires them.

Beginning with Fauna 2.11.0, the Fauna access control logic has been changed to use attribute-based access control (ABAC) roles, or the key-based permission system, but never both.

If a resource is a member of an ABAC role, the ABAC role specifies all privileges for that resource. Otherwise, the key-based permission system determines whether read/write/execute privileges are enabled.

For example, when an ABAC role includes a user-defined function as a member, that function cannot be called unless the ABAC privileges permit the call action.

Privileges

A privilege specifies a resource in Fauna, where the resource could be a database, collection, document, key, index, function, etc., and a set of pre-defined actions to permit.

Privileges associated with containers (e.g. databases, collections, etc.) also apply to the resources within the container.

The available actions are:

  • create: permits the creation of new documents.

  • delete: permits the deletion of existing documents.

  • read: permits the reading of documents, from collections or indexes.

  • write: permits writing to existing documents within a collection.

  • history_read: permits reading historical versions of documents, from collections or indexes.

  • history_write: permits inserting events into the history for an existing document. Required when creating documents with specified document IDs.

  • unrestricted_read: permits the reading of an index without considering any other read permissions.

  • call: permits the calling of user-defined functions.

The actions available vary according to the target resource:

  • Core schemas (Databases, Collections, Indexes, Functions, Keys, etc.): create and delete

  • Documents: create, read, write, delete, history_read, history_write

  • User-defined functions: call

For example:

CreateRole({
  name: "access_todos",
  membership: [{ resource: Collection("users") }],
  privileges: [{
    resource: Collection("todos"),
    actions: {
      create: true,
      delete: true,
      write: true
    }
  }]
})

Action permissions operate as a whitelist: they define permitted actions. The default is that no actions are permitted.

Action permissions can use a predicate function to determine permissions dynamically. For example, an action might be permissible only during a specific period of the day.

Action permissions only apply to the specified resource and its contents, and not to the resource contents' contents. For example, specifying Collections() as a resource can be used to grant or deny privileges on all of the collection documents that exist, but not to the documents within those collections. To grant permissions to documents in a collection, you have to specify a particular collection as a resource (there is no "wildcard" collection expression that can be used).

For a given resource, there may be multiple privileges defined in separate roles. When a query attempts to operate on a resource, permission is granted to process the action if any privilege grants the action.

User-defined functions (UDFs) can be created to have a role, such as role: "admin". This allows UDFs to execute with specific privileges, which might be higher than those of the caller.

This is useful in many scenarios. For example, a "complete_order" function, that completes a financial transaction, might access collections and documents that the user calling the function does not otherwise have access to.

You should be very careful when granting create and call privileges for UDFs, as an otherwise unprivileged user might create a function with role: "admin" and then call it to create a new key with the admin role. This situation exists for the server role too, since it has implicit access to the create and call privileges.

Membership

Membership describes the set of documents that should have the role’s privileges.

Membership is managed with a collection; documents in the collection are members of the role. Typically, a "document" would refer to a "user", but a document can be any record within Fauna. For example, Fauna access keys can be assigned a role, which is useful for background processes.

Membership can also be controlled with a predicate function for dynamic membership evaluation. For example, membership for some users might be available only during a specific period of the day.

Multiple roles can be associated with a Fauna resource, and users can be associated with multiple roles. Attribute-based access is computed for every Fauna transaction (including index filtering), and updates to the role configuration take effect immediately. Additionally, action permissions can be computed dynamically via Lambda functions.

For example:

CreateRole({
  name: "can_manage_todos",
  membership: [
    {
      resource: Collection("users"),
      predicate: Query(Lambda(ref =>
        Select(["data", "vip"], Get(ref))
      ))
    }
  ],
  privileges: [
    // ...
  ]
})

Predicate functions

A predicate function is an FQL Lambda function that operates in a read-only fashion, accepting command-specific arguments, and returning true or false to indicate whether the action is permitted or prohibited.

Predicate functions execute with server-level permissions, regardless of the access level of the identity calling the function. This is because the function needs to be able to determine access to all of the calling identity’s declared resources to make privilege determinations.

The actions and their associated arguments are:

  • create: the new data that is about to be created.

  • read, history_read, delete: the ref to the underlying document.

  • read for indexes: the terms being used to match against the index.

  • write, history_write: the old data, the new data, and a reference to the document to be written.

  • call: the parameters to be passed to the user-defined function.

For example:

CreateRole({
  name: "can_manage_todos",
  membership: [
    // ...
  ],
  privileges: [
    {
      resource: Collection("todos"),
      actions: {
        create: Query(Lambda(newData =>
          Select(["data", "vip"], Get(CurrentIdentity()))
        )),
        // ...
      }
    }
  ]
})

Example

Here is a complete role example:

CreateRole({
  name: "users",
  membership: [
    {
      // This role will be assigned to all users
      // as long as they are active
      resource: Collection("users"),
      predicate: Query(ref =>
        Select(["data", "isActive"], Get(ref), false)
      )
    }
  ],
  privileges: [
    {
      resource: Collection("todos"),
      actions: {
        write:
          // The following function enforces that you can write to your
          // own data but, you can't change the owner of the data
          Query((oldData, newData) =>
            And(
              Equals(
                CurrentIdentity(),
                Select(["data", "owner"], oldData)
              ),
              Equals(
                Select(["data", "owner"], oldData),
                Select(["data", "owner"], newData),
              )
            )
          )
      }
    }
  ]
})

Overlapping roles

When a document is a member of two or more roles, Fauna does its best to optimize for the most common access pattern, to avoid evaluating roles — especially predicates — unnecessarily.

The general approach is that once permission to perform a specific action for a specific resource has been granted, no further determinations for that resource+action need to be performed — the first granted access wins.

Internally, there is a single permission table that is populated during role processing. The table stores the resolution of permissions, keyed on the combination of resource identifier and requested action, involved in the current query.

During role processing, if a resource+action permission evaluates to true, or can be trivially determined to be true, the permission is granted and no further evaluations for that specific resource+action are performed.

However, when all actions for a resource involve predicates, the predicates are evaluated sequentially until one of them returns true, or access is denied. The permission subsystem keeps track of which predicates are most successful and evaluates them first.

The maximum number of overlapping roles is 64. When you attempt to create the 65th overlapping role, you get an error when calling the CreateRole function.

Summary

All of this flexibility can be very useful, and it can also become very complex. Fauna does not favor any particular approach to using ABAC, but we provide the following suggestions that may be useful:

  • It is possible to lock yourself out using your current secret/token. You can recover access via the Fauna Dashboard.

  • Only define as many roles as you need.

  • Only use predicates when necessary.

  • Only provide role membership to those users that need it.

  • Limit the scope of operations used in ABAC role predicates wherever possible. ABAC roles are evaluated for every query, which can impact overall performance.

  • While ABAC roles can be used to filter out documents that should not be readable by the current client, such filtering can involve evaluating every document in a collection. Instead, use indexes for filtering.

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!