Create user-defined roles

Use the Role collection to create documents that define privileges for database resources.

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 membership and evaluates privileges.

Overlapping roles

When a user 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 the most successful predicates and evaluates them first.

The maximum number of overlapping roles is 64. Trying to create more overlapping roles result in an error.

Role stacking

Typically, when making a query, Fauna applies the role of the user key Fauna can apply multiple roles if the user 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 user requires a role stack like this:

Given:

Level Role

User token

humanResources

myUDF()

writeSocial

otherUDF()

admin

  1. The user makes the query, and Fauna first evaluates the humanResources role.

  2. When the myUDF() function is executed, Fauna pushes the writeSocial role on the stack.

    If the UDF returns a Document Fauna verifies that the caller has read permission on the document and permission to call the UDF.

  3. While executing myUDF(), Fauna invokes the otherUDF() function. Again, the Fauna pushes the admin role on the stack and runs otherUDF().

  4. Returning from each function, Fauna pops each role from the stack in turn.

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 these persons 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"
    })
    
    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. Without privileges, this role can’t 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 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 predicate. For example, you might want to ensure that humanResources roles can create only active people but not inactive people.

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

  1. Set your Shell 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 to Role humanResources.

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

    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 but pass an employment: "inactive" status:

    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 with the create predicate and returns false. There are no privileges to do this query.

Best practices

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

The role document architecture is such that a role configuration can authorize all users 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 user with a key created by Key.create({ role: "CanReadUsers" }) can also read Users documents because the CanReadUsers.privileges allow it.

Fauna doesn’t favor a single attribute-based access control approach, but the following suggestions might be useful:

  • Define only the roles you need.

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

  • While roles can filter out documents that shouldn’t be readable by the current user, 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.

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!