Skip to content

Instantly share code, notes, and snippets.

@ptpaterson
Created December 28, 2021 23:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ptpaterson/6e75f38e5a1223dc06bc5690a6dcb0c6 to your computer and use it in GitHub Desktop.
Save ptpaterson/6e75f38e5a1223dc06bc5690a6dcb0c6 to your computer and use it in GitHub Desktop.
A Fauna FQL query to setup role-identifying functions for any database

See this original community forums post

How can we create functions that determine which Roles a Key or Token has?

  1. Create a new Collection.
  2. Create one Document for each Role that uniquely represents each role.
  3. Create the required Indexes to read the new Collection.
  4. Update each Role with privileges to read the indexes, but a predicate that lets it only read the one specific Document that represents it.

Then, when you try to read the Indexes 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 create Functions that make it easy to use this stuff.

NOTE: This technique gives you the ability to list all of the roles that a given key has, but it relies on each key being aware of which roles it has. That means that any key can read from this new Collection we are making, so be aware of that.

Example using the demo data

An easy way to try this out is with the demo database that can be created from the Dashboard. Create a new database and make sure to click Use demo data

Create a new Collection

We'll call it "_role_checkers".

// as admin
CreateCollection({ name: "_role_checkers" })

Create the documents for each Role

This query will loop over each Role and create simple Documents with a name field the same as the Role.

// as admin
Let(
  {
    role_names: Select("data", Map(Paginate(Roles()), Lambda("ref", Select("id", Var("ref"))))),
  },
  Map(
    Var("role_names"),
    Lambda(
      "name",
      Create(Collection("_role_checkers"), { data: { name: Var("name") } })
    )
  )
)

Create the required Indexes to read the new Collection

We will create an "_all_role_checkers" Index that we will use for a "CurrentRoles" Function, and a "_role_checker_by_name" Index that we will use for a "HasRole" Function.

// as admin

CreateIndex({
  name: "_all_role_checkers",
  source: Collection("_role_checkers")
})

CreateIndex({
  name: "_role_checker_by_name",
  source: Collection("_role_checkers"),
  terms: [{ field: ["data", "name"] }]
})

IMPORTANT: These Indexes cannot specify values fields. Otherwise, reading the Index entries will count as a read on the Document for permissions. We want to be able to use the read-the-index-but-filter-the-documents trick.

Create functions to use the Indexes

We want two functions

  1. "CurrentRoles" function that will return an array of Role name, like ["customer", "manager"]
  2. "HasRole" function that will take a Role name as an argument and return true or false.

These Functions will not have a role field specified. That means they will rely on the Roles of the calling Key/Token.

// as admin

CreateFunction({
  name: "CurrentRoles",
  body: Query(
    Lambda([], Map(
      Select("data", Paginate(Match(Index("_all_role_checkers")))),
      Lambda("ref", Select(["data", "name"], Get(Var("ref"))))
    ))
  )
})

CreateFunction({
  name: "HasRole",
  body: Query(
    Lambda("name", Let(
      {
        page: Paginate(Match(Index("_role_checker_by_name"), Var("name")))
      },
      Not(IsEmpty(Select("data", Var("page"))))
    ))
  )
})

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.

Update(
  Role("ROLE_NAME"),
  {
    Privileges: [
        /* all existing privileges */
      {
        // Can only read the Document if the `name` field matches the Role's name
        resource: Collection("_role_checkers"),
        actions: { read: Query(Lambda("ref", Equals(
          "ROLE_NAME",
          Select(["data", "name"], Get(Var("ref")))
        ))) }
      },
      {
        resource: Index("_all_role_checkers"),
        actions: { read: true }
      },
      {
        resource: Index("_role_checker_by_name"),
        actions: { read: true }
      },
      {
        resource: Function("HasRole"),
        actions: { call: true }
      },
      {
        resource: Function("CurrentRoles"),
        actions: { call: true }
      }
    ]
  }

Test it out!

After running all of this on the Demo database we can try out our new functions

// as admin
Call("CurrentRoles")
// ["customer", "manager"]
Call("HasRole", "customer")
// true
Call("HasRole", "manager")
// true

// as customer
Call("CurrentRoles")
// ["customer"]
Call("HasRole", "customer")
// true
Call("HasRole", "manager")
// false

// as manager
Call("CurrentRoles")
// ["manager"]
Call("HasRole", "customer")
// false
Call("HasRole", "manager")
// true
Let(
{
role_checker_collection: If(
Exists(Collection("_role_checker")),
Collection("_role_checker"),
Let(
{
new_coll: CreateCollection({ name: "_role_checker" })
},
Select("ref", Var("new_coll"))
)
),
all_role_checkers_index: If(
Exists(Index("_all_role_checkers")),
Index("_all_role_checkers"),
Let(
{
new_index: CreateIndex({
name: "_all_role_checkers",
source: Var("role_checker_collection")
})
},
Select("ref", Var("new_index"))
)
),
role_checker_by_name_index: If(
Exists(Index("_role_checker_by_name")),
Index("_role_checker_by_name"),
Let(
{
new_index: CreateIndex({
name: "_role_checker_by_name",
source: Var("role_checker_collection"),
terms: [{ field: ["data", "name"] }]
})
},
Select("ref", Var("new_index"))
)
),
has_role_function: If(
Exists(Function("HasRole")),
Function("HasRole"),
Let(
{
new_function: CreateFunction({
name: "HasRole",
body: Query(
Lambda("name", Let(
{
page: Paginate(Match(Index("_role_checker_by_name"), Var("name")))
},
Not(IsEmpty(Select("data", Var("page"))))
))
)
})
},
Select("ref", Var("new_function"))
)
),
current_roles_function: If(
Exists(Function("CurrentRoles")),
Function("CurrentRoles"),
Let(
{
new_function: CreateFunction({
name: "CurrentRoles",
body: Query(
Lambda([], Map(
Select("data", Paginate(Match(Index("_all_role_checkers")))),
Lambda("ref", Select(["data", "name"], Get(Var("ref"))))
))
)
})
},
Select("ref", Var("new_function"))
)
),
role_names: Select("data", Map(Paginate(Roles()), Lambda("ref", Select("id", Var("ref"))))),
delete_existing_role_checkers: Map(
Paginate(Documents(Var("role_checker_collection"))),
Lambda("ref", Delete(Var("ref")))
),
new_role_checkers: Map(
Var("role_names"),
Lambda(
"name",
Create(Var("role_checker_collection"), { data: { name: Var("name") } })
)
),
role_updates: Map(
Var("role_names"),
Lambda(
"name",
Let(
{
role_ref: Role(Var("name")),
role: Get(Var("role_ref")),
privileges: Select("privileges", Var("role")),
privileges_filtered: Filter(
Var("privileges"),
Lambda(
"privilege",
And(
Not(Equals(Select(["resource"], Var("privilege")), Collection("_role_checker"))),
Not(Equals(Select(["resource"], Var("privilege")), Index("_all_role_checkers"))),
Not(Equals(Select(["resource"], Var("privilege")), Index("_role_checker_by_name"))),
Not(Equals(Select(["resource"], Var("privilege")), Function("HasRole"))),
Not(Equals(Select(["resource"], Var("privilege")), Function("CurrentRoles"))),
)
)
),
new_privileges: Append(
[
{
resource: Var("role_checker_collection"),
actions: { read: Query(Lambda("ref", Equals(
Var("name"),
Select(["data", "name"], Get(Var("ref")))
))) }
},
{
resource: Var("all_role_checkers_index"),
actions: { read: true }
},
{
resource: Var("role_checker_by_name_index"),
actions: { read: true }
},
{
resource: Var("has_role_function"),
actions: { call: true }
},
{
resource: Var("current_roles_function"),
actions: { call: true }
}
],
Var("privileges_filtered")
)
},
Update(Var("role_ref"), { privileges: Var("new_privileges") })
)
)
),
},
If(
IsEmpty(Select("data", Paginate(Documents(Var("role_checker_collection"))))),
"No _role_checker Documents created. Run again once you have roles",
Format("updated roles: %@", [Var("role_names")])
)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment