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:
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 |
|
|
|
|
|
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
andprivileges
predicates wherever possible. Use predicates only when needed.
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 a person to the
People
collection.{ 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" }
-
Add these two as well:
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. Theprivileges
act as a whitelist. Withoutprivileges
this role cannot 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 only 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 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.
-
Make sure your Shell is set the run menu 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 is set the run menu to Role
humanResources
. -
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" } })
-
Try the operation again, only this time pass
employment: "inactive"
.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 returnsfalse
. 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!