Create user-defined roles
Use the Role
collection to create documents that
define privileges for database resources.
Roles are part of the Role
system 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
.
This tutorial shows how you can create user-defined roles. The examples in this tutorial use the Fauna Dashboard and Fauna’s demo data.
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 API 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 runs the query, and Fauna first evaluates the user-defined
customer
role. This role is defined in Fauna’s Demo database. -
When the
myUDF()
function is executed, Fauna pushes the built-inserver
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 the built-inadmin
role on the stack and runsotherUDF()
. -
Returning from each function, Fauna pops each role from the stack in turn.
Create a user-defined role
You define roles in Fauna Schema Language (FSL) as a schema. You manage schemas using the Dashboard or the Fauna CLI.
Use the Dashboard to add the following owner
role to the Demo
database:
role owner {
privileges Product {
read
}
}
The role grants read
access to documents in the Product
collection.
Test the role
Run some FQL queries to test the owner
role’s privileges.
-
In the Shell for the Demo database, select a Role of owner.
This lets you run queries using the
owner
role’s privileges. -
Run the following query to read documents in the
Product
collection:Product.sortedByPriceHighToLow()
The response contains a list of
Product
documents:{ data: [ { id: "394783030761226306", coll: Product, ts: Time("2099-04-10T12:48:07.090Z"), name: "pinata", description: "Original Classic Donkey Pinata", ... }, ... { id: "394783030767517762", coll: Product, ts: Time("2099-04-10T12:48:07.090Z"), name: "limes", description: "Conventional, 1 ct", ... } ] }
-
Attempt to create a document in the
Product
collection:Product.create({ name: "key limes" })
The query returns an error:
permission_denied error: Insufficient privileges to perform the action. at *query*:1:15 | 1 | Product.create({ name: "key limes" }) | ^^^^^^^^^^^^^^^^^^^^^^^ |
The
owner
role doesn’t havecreate
privileges for theProduct
collection. -
Attempt to read documents in the
Manager
collection:Manager.all()
The query returns an empty set:
{ data: [] }
The
owner
role doesn’t haveread
privileges for theManager
collection.
Add an action predicate
You can also limit an action using a predicate. For example, you might want to
ensure that the owner
role can only create Product
documents with a
backordered
field of false
.
Each permission action
requires distinct function parameters. For example,
write
requires a function with two parameters, while a create
function
requires one parameter.
Use the Dashboard to add the following create
predicate to the owner
role:
role owner {
privileges Product {
read
create {
predicate (data => data.backordered == false)
}
}
}
The predicate limits the create
action on the Product
collection. The
owner
role can only create Product
documents with a backordered
field of
false
.
Test the role
Next, run some FQL queries to test the role’s updated privileges.
-
In the Shell for the Demo database, select a Role of owner.
-
Create a
Product
document with abackordered
field offalse
:Product.create({ name: "key limes", backordered: false })
The query runs successfully:
{ id: "394784162900344898", coll: Product, ts: Time("2024-04-10T13:06:06.690Z"), name: "key limes", backordered: false }
-
Attempt to create a
Product
document with abackordered
field oftrue
:Product.create({ name: "lemons", backordered: true })
The query returns an error:
permission_denied error: Insufficient privileges to perform the action. at *query*:1:15 | 1 | Product.create({ | _______________^ 2 | | name: "lemons", 3 | | backordered: true 4 | | }) | |__^ |
The
owner
role can only create aProduct
document withbackordered
field offalse
. -
Attempt to create a
Product
document without abackordered
field:Product.create({ name: "lemons" })
The query returns an error:
permission_denied error: Insufficient privileges to perform the action. at *query*:12:15 | 12 | Product.create({ name: "lemons" }) | ^^^^^^^^^^^^^^^^^^^^ |
Write predicates
Predicate functions for the write
action accept two arguments: an object for
the original document and an object for the document to write. For example:
role owner {
privileges Product {
...
write {
predicate ((oldDoc, newDoc) => {
oldDoc.backordered == false
&& newDoc.backordered == false
})
}
}
}
Based on the above predicate, users with the owner
role can only update
Product
documents if:
-
The original document has a
backordered
field offalse
, and … -
The updated document also has a
backordered
field offalse
.
If needed, use the _
placeholder for an unused argument:
role owner {
privileges Product {
...
write {
predicate ((oldDoc, _) => {
oldDoc.backordered == false
})
}
}
}
Based on the above predicate, users with the owner
role can only update
Product
documents if the original document has a backordered
field of
false
. The predicate doesn’t evaluate the updated document.
Self-referential predicates
Use Query.identity()
in a predicate
to only allow a user or identity to perform an operation on itself.
For example, based on the following predicate, a user with the manager
role
can only read their own Manager
document:
role manager {
membership Manager
...
privileges Manager {
read {
predicate (ref => Query.identity() == ref)
}
}
}
A similar predicate for the write
action would look like this:
role manager {
membership Manager
...
privileges Manager {
...
write {
predicate ((oldDoc, newDoc) => {
Query.identity() == oldDoc
&& Query.identity() == newDoc
})
}
}
}
call
predicates
The call
action lets a role call a user-defined function (UDF). call
predicates accept the UDF’s parameters as arguments. Use the _
placeholder for
unused arguments.
For example, based on the following predicate, a user with the customer
role
can only call the submitOrder()
function for their own orders:
role customer {
membership Customer
...
privileges submitOrder {
call {
predicate ((customer, _) => Query.identity() == customer)
}
}
...
}
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!