Attribute-based access control (ABAC)

Attribute-based access control (ABAC) is a security model that conditionally grants access to resources based on attributes. In Fauna, these attributes can relate to the:

  • User or system requesting access, if you’re using tokens

  • Resource being accessed

  • Operation to perform and its effect

  • Environment, such as date or time of day

Before you start

Before implementing ABAC in Fauna, you should be familiar with:

RBAC vs. ABAC

ABAC extends traditional role-based access control (RBAC). The models are not mutually exclusive. Fauna supports both RBAC and ABAC.

RBAC

In RBAC, you assign predefined roles with static privileges. If an authentication secret has a role, it’s granted all of the role’s privileges.

RBAC example

Users with the customer role have:

  • Access to Order collection documents, which contain store orders.

  • The ability to call the submitOrder user-defined function (UDF).

Users with the manager role have:

  • Access to Customer collection documents, which contain customer profile data.

  • Access to Order collection documents.

Issues

This approach is simple, but it’s inflexible and lacks granularity:

  • Customers can’t access their own Customer documents.

  • Customers can access any Order document, including orders where they’re not the customer.

  • Managers can access Customer documents for other stores.

  • Managers can access Order documents, even if:

    • They’re logged in at another store.

    • It’s past closing time in their store’s local time.

    • The order has a settled status or it’s 7 days past the settlementDate.

ABAC

With ABAC, you can conditionally assign roles and grant privileges based on multiple attributes.

ABAC example

Users with the customer role have:

  • Access to their own Customer document but no other.

  • Access to Order documents where they’re the customer but no other.

  • The ability to call the submitOrder UDF.

Users with the manager role have:

  • Access to their own store’s Customer documents but no other.

  • Order documents if:

    • The user isn’t logged in at another store.

    • It’s before closing time in their store’s local time.

    • The order doesn’t have a settled status.
      OR
      It’s within 7 days of the settlementDate.

Dynamic ABAC in Fauna

In Fauna, you implement ABAC using role-related predicates. You can use the predicates to:

Fauna evaluates and assigns a secret’s roles and privileges, including any predicates, at query time for every query. This lets you grant access based on real-time user data and the environment.

Dynamically assign roles

You can dynamically assign roles to:

Dynamically assign roles to JWTs

When you define an access provider schema, you can specify one or more roles. Fauna assigns these roles to the provider’s JWTs. Each role can include a predicate:

access provider SomeIssuer {
  ...
  // If the predicate is `true`,
  // assign the `customer` role to the provider's JWTs.
  role customer {
    // Check that JWT's `scope` includes `customer`
    predicate (jwt => jwt!.scope.includes("customer"))
  }
}

The predicate is passed one argument: an object containing the JWT’s payload.

Dynamically assign roles to tokens

Fauna assigns roles to a token based on its identity document’s collection and the role schema's membership properties. Each membership property can include a membership predicate:

role customer {
  // If the predicate is `true`,
  // assign the `customer` role to tokens with
  // identity documents in the `Customer` collection.
  membership Customer {
    // Checks the `accessLevel` field in the
    // token's identity document.
    predicate (idDoc => idDoc.accessLevel == "customer")
  }
}

The predicate is passed one argument: an object containing the token’s identity document.

Dynamically grant privileges

A role schema typically includes several privileges. Each privilege can include a privilege predicate:

role customer {
  privileges Order {
    // If the predicate is `true`,
    // grant `read` access to the `Order` collection.
    read {
      predicate (doc =>
        // Check the order's `status` field.
        doc.status != "Deleted"
      )
    }
    // If the predicate is `true`,
    // grant `write` access to the `Order` collection.
    write {
      predicate ((oldDoc, newDoc) =>
        // Check the existing order's status.
        oldDoc.status != "Deleted" &&
        // Check that `customer` isn't changed by the write.
        oldDoc.customer == newDoc.customer &&
        // Check the current time.
        // Allow access after 07:00 (7 AM).
        Time.now().hour > 7 &&
        // Disallow access after 20:00 (8 PM).
        Time.now().hour < 20
      )
    }
  }
}

Privilege predicates are passed different arguments based on the action the privilege grants. See Privilege predicate arguments.

Identity-based attributes

Tokens are tied to an identity document. You can fetch a token’s identity document using the Query.identity() method:

role customer {
  privileges Order {
    read {
      // `Query.identity()` gets the token's identity document.
      // The identity document typically represents a user or system.
      // In this example, `doc.customer` is the order's customer.
      // The predicate checks that the order belongs to the customer.
      predicate (doc => Query.identity() == doc.customer)
    }
  }
}

Predicates can also check an identity document’s fields:

role customer {
  privileges Order {
    write {
      predicate ((oldDoc, newDoc) =>
        // Check that the user's `country` is in
        // the updated order's `allowedCountries`.
        newDoc.allowedCountries.includes( Query.identity()!.country) &&
        // Disallow `write` access 10 hours after
        // the last document timestamp.
        Time.now().difference(oldDoc!.ts, "hours") < 10
      )
    }
  }
}

For JWTs and keys, Query.identity() returns null. JWTs and keys aren’t tied to an identity document and don’t support identity-based attributes.

Token metadata

Fauna stores tokens as documents in the Token system collection. This token document is distinct from the token’s identity document.

A token document can include metadata in its data field. You later check this metadata in a predicate for ABAC.

Use update() to add metadata to a token document:

// Get an existing credential for a `Manager` collection document.
let credential = Credential.byDocument(Manager.byId("<DOCUMENT_ID>"))

// Use `login()` to create a token using the credential and its password.
// Returns a document in the `Token` system collection.
let token = credential!.login("<PASSWORD>")

// Add metadata to the token document using the `data` property.
// The result doesn't include this property, but it's added.
token!.update({
  data: {
    clientIpAddr: "123.123.12.1"
  }
})

Use Query.token() to fetch the token document for the transaction’s authentication token. You can then access the document’s data field:

role manager {
  membership Manager {
    // Assign the `manager` role if
    // the token document's `clientIpAddr` metadata is
    // in the manager's `approvedIpAddresses`.
    predicate (_ =>
      Query.identity()!.approvedIpAddresses
        .includes(Query.token()!.data!.clientIpAddr)
    )
  }
}

Environmental attributes

You can use predicates to assign roles and grant privileges based on the current date or time.

Use Date.today() to get the current date:

role manager {
  membership Manager {
    // Assign the `manager` role only on weekdays.
    predicate (_ => Date.today().dayOfWeek < 6)
  }
}

Use Time.now() to get the current time:

role manager {
  privileges Order {
    write {
      // Disallow `write` access if the user is logged in for
      // more than 60 minutes.
      predicate ((_, _) =>
        Time.now().difference(Query.identity()!.login, "minutes") < 60
      )
    }
  }
}

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!