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. Attempts to create overlapping role 65 results in 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 |
|
|
|
|
|
-
The user makes the query, and Fauna first evaluates the
humanResources
role. -
When the
myUDF()
function is executed, Fauna pushes thewriteSocial
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.
-
While executing
myUDF()
, Fauna invokes theotherUDF()
function. Again, the Fauna pushes theadmin
role on the stack and runsotherUDF()
. -
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.
-
Open the
CoffeeStore
database that you previously created. -
In the Shell, make sure you set the run menu to Admin or an equivalent key secret.
-
Create a
People
collection:This is the content of create-people.fql.out
-
Add these persons to the
People
collection:
Create a role-based query
Define a role and query the CoffeeStore
database from the dashboard Shell
using that role.
-
In the
CoffeeStore
Shell, set the run menu to Admin or an equivalent key secret.Only users with an
admin
key can manage the nativeRole
collection. -
Create a
Role
namedhumanResources
:{ name: "humanResources", coll: Role, ts: Time("2023-07-19T04:45:51.370Z") }
The
humanResources
role has a name but noprivileges
defined. Without privileges, this role can’t do anything in theCoffeeStore
database. Test this: -
Set the run menu to Role and enter the
humanResources
role. -
Try listing all the
People
:{ data: [] }
The
humanResources
role has no privileges to view anyCoffeeStore
database resources. -
Set the run menu back to Admin.
You need
admin
privileges to update aRole
document. -
Update the
privileges
on thehumanResources
role to give access to read:{ 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. -
Set the run menu to Role
humanResources
and query thePeople
collection again:{ 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 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.
-
Set your Shell to Admin.
-
Add a
write:
privilege tohumanResources
:{ name: "humanResources", coll: Role, ts: Time("2023-07-21T00:22:25.260Z"), privileges: { resource: "People", actions: { read: true, create: "data => data.employment == \'active\' " } } }
-
Set your Shell to Role
humanResources
. -
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" } })
-
Try the operation again but pass an
employment: "inactive"
status: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 returnsfalse
. 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
andprivileges
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!