It is useful during development or debugging to fetch all the assigned roles for a user, or to verify if the current user has a certain role. There is currently no out-of-the-box solution to achieve this.
In this article, we discuss how it can be achieved using FQL.
The approach described here leverages how Fauna treats read permissions. If a Role specifies a predicate for read permissions on a collection, Fauna will filter the results for only those Documents allowed by the current roles.
Here are the steps we take:
- Create a new Collection called "RoleCheck".
- Create one RoleCheck Document for each Role which uniquely represents that role.
- Add an Index on RoleCheck Collection to search by Role name.
- Create two Functions: "HasRole" and "CurrentRoles".
- Update each Role with privileges to read the RoleCheck Collection and call the new Functions.
With that in place, when you read the Collection, only those documents that the roles have access to will be read. You can interpret which documents are read as the roles that you have. And we can add Functions that make it convenient to use.
Solution
We are discussing a step-by-step approach here, but there is also a single script further below that does all of these steps at once.
Create a new Collection
Collection.create({ name: "RoleCheck" })
Create one Document for each Role
This query will read all Roles and create a Document with a name
field that matches the Role name.
let roles = Role.all().toArray() roles.map(role => { RoleCheck.create({ name: role.name }) })
Create the required Indexes to read the new Collection
We will create a byName
Index that we will use for the HasRole
Function.
RoleCheck.definition.update({ indexes: { byName: { terms: [{field: "name" }] } } })
Create HasRole
and CurrentRoles
Functions
We want two functions
CurrentRoles
Function will return an array of Role name, like["customer", "manager"]
HasRole
Function that will take a Role name as an argument and returntrue
orfalse
.
These Functions will not have a role
field specified. That means they will rely on the Roles of the calling Key/Token.
Function.create({ name: "currentRoles", body: "() => RoleCheck.all().map(.name).toArray()" })
Function.create({ name: "hasRole", body: "(name) => RoleCheck.byName(name) != null" })
Update each Role
For each Role, we will add the privilege to read the Indexes, read the respective Document, and if we want, call the Functions.
let roles = Role.all().toArray() roles.map(role => { let newPrivileges = role.privileges.concat([ { resource: "RoleCheck", actions: { read: "doc => doc.name == '#{role.name}'" } }, { resource: "HasRole", actions: { call: true } }, { resource: "CurrentRoles", actions: { call: true } } ]) role.update({ privileges: newPrivileges }) })
Test it out!
After running all of this on the Demo database we can try out our new functions
// as admin
currentRoles() // ["customer", "manager"]
hasRole("customer") // true
hasRole("manager") // true
// as a customer
currentRoles() // ["customer"]
hasRole("customer") // true
hasRole("manager") // false
// as a manager
currentRoles() // ["manager"]
hasRole("customer") // false
hasRole("manager") // true
Put it all together
FQL is quite powerful, and you can actually perform all of these steps in a single FQL query. The following actually needs to be at least twice so it will create the Collection then later when it exists the Documents will be created.
This query is also idempotent, so it will still continue to work if you run it more than twice. You can make changes (e.g. add/remove Roles or update privileges), and running the query will reapply the necessary privileges.
if (Collection.byName("RoleCheck") == null) { Collection.create({ name: "RoleCheck" }) "Created 'RoleCheck' Collection" } else { // Do some clean-up first RoleCheck.all().forEach(doc => doc.delete()) // Add or update the byName Index RoleCheck.definition.update({ indexes: { byName: { terms: [{field: "name" }] } } }) // Create UDFs let currentRolesBody = "() => RoleCheck.all().map(.name).toArray()" let hasRoleBody = "(name) => RoleCheck.byName(name) != null" if (Function.byName("currentRoles") == null) { Function.create({ name: "currentRoles", body: currentRolesBody }) } else { currentRoles.definition.update({ body: currentRolesBody }) } if (Function.byName("hasRole") == null) { Function.create({ name: "hasRole", body: hasRoleBody }) } else { hasRole.definition.update({ body: hasRoleBody }) } Role.all().toArray().forEach(role => { // create one RoleCheck doc for each Role RoleCheck.create({ name: role.name }) // update all existing roles with new privileges let basePrivileges = role.privileges.where( .resource != "RoleCheck" && .resource != "currentRoles" && .resource != "hasRole" ) let newPrivileges = basePrivileges.concat([ { resource: "RoleCheck", actions: { read: "doc => doc.name == '#{role.name}'" }, }, { resource: "hasRole", actions: { call: true } }, { resource: "currentRoles", actions: { call: true } }, ]) role.update({ privileges: newPrivileges }) }) "Enabled Role detecting functions" }