User-defined roles and keys

You can also build keys that rely on user-defined roles rather than the built-in roles. Use the Role collection to create documents that define privileges for database resources. You can then create a key that uses any role you might create:

new key role

Roles are part of a native collection, so the document structure is system defined and immutable. Each document has a required, unique name and optional membership and privileges arrays. When a user-defined role is used with a key, Fauna ignores any membership and evaluates only the privileges.

Overlapping roles

When a caller is a member of two or more roles, Fauna does its best to optimize for the most common access pattern. The first access granted wins. No further evaluation is done after Fauna authorizes a privilege for action on a resource. In this way, Fauna avoids evaluating roles unnecessarily, especially predicates.

When all actions for a resource involve predicates, Fauna sequentially evaluates the predicates until one of them returns true or access is denied. Fauna keeps track of which predicates are most successful and evaluates them first.

The maximum number of overlapping roles is 64. Attempts to create overlapping role 65 results in error.

Role stacking

Typically, when making a query, Fauna only applies the role of the key or secret of the caller. Fauna can apply multiple roles if the caller makes a query that uses a UDF. UDFs have a role field that Fauna uses when executing the function body. If a UDF calls another UDF, it can result in a role stack. Consider a situation where a query from a caller requires a role stack like this:

Level Role

Caller token

humanResources

myUDF()

writeSocial

otherUDF()

admin

The caller makes the query and Fauna first evaluates the humanResources role. When it runs myUDF() in the query, Fauna first pops the stack to the writeSocial role. While executing myUDF(), Fauna comes across otherUDF(). Again, the Fauna pops the stack and runs the otherUDF() under the admin role. As it returns from each function, Fauna rebuilds the original stack by pushing each role back onto it.

Best practices

Fauna evaluates role privileges for every query. The role membership is only evaluated when a Token document is included in the query. Membership is ignored if a caller accesses the database with a Key or an AccessProvider.

The role document architecture is such that a role configuration can authorize all callers regardless of how they enter the database. This design allows applications to use multiple ways of accessing data. For example, consider this role:

Role.create({
  name: 'CanReadUsers',
  privileges: [{ resource: 'Users', actions: { read: true }}],
  membership: [{ resource: 'Users', predicate: 'user => user.isAdmin'}]
})

An admin user logging in through the Credential.login() function can read Users because the CanReadUsers.membership.predicate evaluates as true making the user a member with privileges. At the same time, a caller with a key created by Key.create({ role: "CanReadUsers" }) can also read Users documents because the CanReadUsers.privileges allow it.

Fauna does not favor a single attribute-based access control approach, but the following suggestions might be useful:

  • Define only as many roles as you need.

  • Follow the principle of least privilege by granting role membership to only those users that need it.

  • While roles can filter out documents that shouldn’t be readable by the current caller, such filtering can involve evaluating every document in a collection. Instead, use indexes for filtering.

  • Limit the scope of operations that use membership and privileges predicates wherever possible. Use predicates only when needed.

Create Collection data

In this procedure, you create some data for the user roles to manage.

  1. Open the CoffeeStore database that you previously created.

  2. In the Shell, make sure you set the run menu to Admin or an equivalent key secret.

  3. Create a People collection.

    Collection.create({ name: "People" })
    This is the content of create-people.fql.out
  4. Add a person to the People collection.

    People.create({
        "name": "Janine Labrune",
        "email": "jlabrune@gmail.com",
        "address": {
          "street": "67, rue des Cinquante Otages",
          "city": "Nantes",
          "country": "France",
          "zip": 44000
        },
      employment: "active"
      })
    {
      id: "372643256462213153",
      coll: People,
      ts: Time("2023-08-10T03:45:52.910Z"),
      name: "Janine Labrune",
      email: "jlabrune@gmail.com",
      address: {
        street: "67, rue des Cinquante Otages",
        city: "Nantes",
        country: "France",
        zip: 44000
      },
      employment: "active"
    }
  5. Add these two as well:

    People.create({
        "name": "Gail Philbert",
        "email": "gphilbert@gmail.com",
        "address": {
          "street": "98 Cowbell Lane",
          "city": "San Mateo",
          "country": "USA",
          "zip": 94403
        },
      employment: "inactive"
      })
    
      People.create({
        "name": "Bob Hamstead",
        "email": "bhamstead@gmail.com",
        "address": {
          "street": "90B South 7th Street",
          "city": "Redwood City",
          "country": "CA",
          "zip": 94803
        },
      employment: "active"
      })

Create a role-based query

Define a role and query the CoffeeStore database from the dashboard Shell using that role.

  1. In the CoffeeStore Shell, set the run menu to Admin or an equivalent key secret.

    Only users with an admin key can manage the native Role collection.

  2. Create a Role named humanResources.

    Role.create({ name: "humanResources" })
    {
      name: "humanResources",
      coll: Role,
      ts: Time("2023-07-19T04:45:51.370Z")
    }

    The humanResources role has a name but no privileges defined. The privileges act as a whitelist. Without privileges this role cannot do anything in the CoffeeStore database. Test this.

  3. Set the run menu to Role and enter the humanResources role.

  4. Try listing all the People.

    People.all()
    {
      data: []
    }

    The humanResources role has no privileges to view any CoffeeStore database resources.

  5. Set the run menu back to Admin.

    You need admin privileges to update a Role document.

  6. Update the privileges on the humanResources role to give access to read.

    Role.byName("humanResources")!.update(
        {
            privileges:{
                resource:"People",
                actions:{
                        read: true
                }
            }
        })
    {
      name: "humanResources",
      coll: Role,
      ts: Time("2023-07-19T23:36:01.670Z"),
      privileges: {
        resource: "People",
        actions: {
          read: true
        }
      }
    }

    Privileges on the resource: "Collection" are an exception. They grant permissions only on the user-defined collections, not to their documents.

  7. Set the run menu to Role humanResources and query the People collection again.

    People.all()
    {
      data: [
        {
          id: "372643256462213153",
          coll: People,
          ts: Time("2023-08-10T03:45:52.910Z"),
          name: "Janine Labrune",
          email: "jlabrune@gmail.com",
          address: {
            street: "67, rue des Cinquante Otages",
            city: "Nantes",
            country: "France",
            zip: 44000
          },
          employment: "active"
        },
        {
          id: "372643576946884641",
          coll: People,
          ts: Time("2023-08-10T03:50:58.540Z"),
          name: "Gail Philbert",
          email: "gphilbert@gmail.com",
          address: {
            street: "98 Cowbell Lane",
            city: "San Mateo",
            country: "USA",
            zip: 94403
          },
          employment: "inactive"
        },
        {
          id: "372643576946885665",
          coll: People,
          ts: Time("2023-08-10T03:50:58.540Z"),
          name: "Bob Hamstead",
          email: "bhamstead@gmail.com",
          address: {
            street: "90B South 7th Street",
            city: "Redwood City",
            country: "CA",
            zip: 94803
          },
          employment: "active"
        }
      ]
    }

Write an action predicate

You can also limit an action using a lambda predicate function. For example, you might want to ensure that humanResources roles can only create active people, not inactive.

Each permission action requires distinct function parameters. For example, write requires a function with three parameters, while a create function requires one parameter.

  1. Make sure your Shell is set the run menu to Admin.

  2. Add a write: privilege to humanResources:

    Role.byName("humanResources")!.update(
        { privileges: {
            resource: "People",
            actions: {
                read: true,
                create: "data => data.employment == 'active' "
            }
        }
        }
    )
    {
      name: "humanResources",
      coll: Role,
      ts: Time("2023-07-21T00:22:25.260Z"),
      privileges: {
        resource: "People",
        actions: {
          read: true,
          create: "data => data.employment == \'active\' "
        }
      }
    }
  3. Set your Shell is set the run menu to Role humanResources.

  4. Add a user with a employment: "active".

    People.create({
        "name": "Frank Cribbage",
        "email": "f.cribbage@gmail.com",
        "employment" : "active",
        "address": {
          "street": "4 South Hampstead",
          "city": "York",
          "country": "USA",
          "zip": "56113"
        }
      })
    People.create({
        "name": "Frank Cribbage",
        "email": "f.cribbage@gmail.com",
        "employment" : "active",
        "address": {
          "street": "4 South Hampstead",
          "city": "York",
          "country": "USA",
          "zip": "56113"
        }
      })
  5. Try the operation again, only this time pass employment: "inactive".

    People.create({
        "name": "Frank Cribbage",
        "email": "f.cribbage@gmail.com",
        "employment" : "inactive",
        "address": {
          "street": "4 South Hampstead",
          "city": "York",
          "country": "USA",
          "zip": "56113"
        }
      })
    permission_denied
    
    error: Insufficient privileges to perform the action.
    at *query*:1:14
       |
     1 |   People.create({
       |  ______________^
     2 | |     "name": "Frank Cribbage",
     3 | |     "email": "f.cribbage@gmail.com",
     4 | |     "employment" : "inactive",
     5 | |     "address": {
     6 | |       "street": "4 South Hampstead",
     7 | |       "city": "York",
     8 | |       "country": "USA",
     9 | |       "zip": "56113"
    10 | |     }
    11 | |   })
       | |____^
       |

    This query is tested against the create predicate and returns false. There are no privileges to do this query.

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!