Determine Roles of a specific User

I have defined custom roles for my users based on what they have from the Users Collection but at the same time have computed roles based on another collection. My question : Is there a way to know which roles a specific user has?

I wanted to create an FQL where I give it the user then would get admin, user, read-only, etc… but not sure how to get the association between user and role

Hi @rzac and welcome!

There is no out-of-the-box way to do this. There are some things you can do to make this possible though. It’s pretty straightforward to duplicate some logic if there are not many roles, and you save some effort by reusing UDFs. There is also a way to implement generic “CurrentRoles” and “HasRole” functions using some advanced FQL to set things up for your database.

Use a UDF with the same logic as the Role

If you use some FQL predicate to determine how a Role is applied, you can use the same logic as a UDF.

For example, if you have a membership predicate that looks for an admin field to be set to true

CreateRole({
  name: "admin",
  membership: [
    {
      resource: Collection("users"),
      predicate: Query(Lambda("ref", Select(["data", "admin"], Get(Var("ref")))))
    }
  ],
  privileges: [/* ... */]
})

Then you can create an IsAdmin function to check for the same thing.

CreateFunction({
  name: "IsAdmin",
  role: "admin",
  body: Query(Lambda(
    "ref",
    Select(["data", "admin"], Get(Var("ref")))
  ))
})

You couls call it with CurrentIdentity() to check against the current key, like below, or any users Ref.

Call("IsAdmin", CurrentIdentity())

Use a UDF as the predicate function

You can call UDF’s from within predicate functions.

They have to be READ ONLY, so be careful as using disallowed functions will result in permission denied. See some recent discussion.

Using the example UDF from above, we can actually Call that function within the Role we made, passing in the “ref” variable.

Update(Role("admin"), {
  membership: [
    {
      resource: Collection("users"),
      predicate: Query(
        Lambda("ref", Call("IsAdmin", Var("ref"))
      )
    }
  ]
})

Now the logic is not duplicated! If you update the IsAdmin function the permissions will also be updated.

The way to apply this more generally leverages how Fauna treats permissions with reading Indexes Collections. A role can allow you to read an Index, but if there is a predicate on reading the source collection, Fauna will filter the results for only those Documents that are white listed by the current roles.

So how do we use that?

  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

image

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 a customer
Call("CurrentRoles")
// ["customer"]
Call("HasRole", "customer")
// true
Call("HasRole", "manager")
// false

// as a manager
Call("CurrentRoles")
// ["manager"]
Call("HasRole", "customer")
// false
Call("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 work after it is run. You can make changes, add/remove Roles, or update privileges, and running the query should reset everything.

(I also dropped this into a gist, here)

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")])
  )
)

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.