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
.
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.
Write 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" }) | ^^^^^^^^^^^^^^^^^^^^ |
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!