Role-based access control

This tutorial assumes that you have completed the Quick Start with Fauna Cloud tutorial.

Role-based access control (RBAC) is an alternative to an all-or-nothing security model, and is commonly used in applications to restrict access to specific data based on the user’s role. In this tutorial, we introduce FaunaDB’s Role Based Access Control (RBAC) feature by simulating an employee hierarchy, and employing a "smart" role that permits users to see their own salary, and managers to see their own salary and the salaries of users that report to them.

  1. Create a new database

    Open a terminal and run:

    fauna create-database rbac
    creating database rbac
    created database 'rbac'
    To start a shell with your new database, run:
    fauna shell 'rbac'
    Or, to create an application key for your database, run:
    fauna create-key 'rbac'
  2. Connect to the new database using Fauna Shell

    Start a Fauna Shell session:

    fauna shell rbac
    Starting shell for database rbac
    Connected to http://faunadb:8443
    Type Ctrl+D or .exit to exit the shell
     
  3. Create three separate collections (classes)

    CreateClass({ name: "users" })
    CreateClass({ name: "salary" })
    CreateClass({ name: "user_subordinate" })

    The users collection is used to store the user details, while the salary collection is used to collect the salary information. The user_subordinate collection is used to store the information of managers and their subordinates.

  4. Create three indexes

    In FaunaDB, indexes are required for pagination or searching. Here, we create collection indexes on the users and salary collections, and a specific index to retrieve users by name.

    CreateIndex({
      name: "all_users",
      source: Class("users"),
    })
    CreateIndex({
      name: "user_by_name",
      source: Class("users"),
      terms: [{ field: ["data", "name"] }],
    })
    CreateIndex({
      name: "all_salaries",
      source: Class("salary"),
    })
  5. Create user and salary data

    Here, we create some users and salary data. The salary collection stores the user reference as a foreign key. The user collection also stores the user’s credentials, which is just a simple password for this tutorial.

    Map([
      ["Bob", 95000],
      ["Joe", 60000],
      ["John", 70000],
      ["Peter", 97000],
      ["Mary", 120000],
      ["Carol", 150000]
    ], Lambda("data", Let(
        {
          user: Create(Class("users"), {
            data: { name: Select(0, Var("data")) },
            credentials: { password: "123" }
          }),
          salary: Select(1, Var("data"))
        },
        Create(Class("salary"), { data: {
          user: Select("ref", Var("user")),
          salary: Var("salary")
        }})
    )))
  6. Verify that the data is correct

    Now that the data is created, let us query the two collections to check out the usernames and salaries.

    Map(
      Paginate(Match(Index("all_salaries"))),
      Lambda("salaryRef",
        Let({
          salary: Get(Var("salaryRef")),
          user: Get(Select(["data", "user"], Var("salary")))
        },
        {
          user: Select(["data", "name"], Var("user")),
          salary: Select(["data", "salary"], Var("salary"))
        }
       )
      )
    )

    The above query should display the users and their salaries (the order of the results can vary):

    { data:
       [ { user: 'Carol', salary: 150000 },
         { user: 'Peter', salary: 97000 },
         { user: 'Joe', salary: 60000 },
         { user: 'Bob', salary: 95000 },
         { user: 'Mary', salary: 120000 },
         { user: 'John', salary: 70000 } ] }
  7. Create manager→user relationship data

    Now that the basic data is created, we create a similar sample data associating managers and their subordinates

    Map([
      ["Bob", "Mary"],
      ["John", "Mary"],
      ["Peter", "Joe"]
    ], Lambda("data", Let(
      {
        user: Get(Match(Index("user_by_name"), Select(0, Var("data")))),
        manager: Get(Match(Index("user_by_name"), Select(1, Var("data"))))
      },
      Create(Class("user_subordinate"), { data: {
        user: Select("ref", Var("user")),
        reports_to: Select("ref", Var("manager"))
      }})
    )))

    Here, we see that Bob and John work for Mary, while Peter works for Joe. Once our access controls are in place, Bob should only be able to see his salary, but Mary should be able to see her salary as well as the salary for Bob and John.

  8. Create an index for the user_subordinate collection

    CreateIndex({
      name: "is_subordinate",
      source: Class("user_subordinate"),
      terms: [
        { field: ["data", "user"] },
        { field: ["data", "reports_to"] }
      ]
    })
  9. Create a role that provides the appropriate privileges

    CreateRole({
      name: "normal_user",
      membership: {
        resource: Class("users")
      },
      privileges: [
        { resource: Class("users"), actions: { read: true } },
        { resource: Index("all_users"), actions: { read: true } },
        { resource: Index("all_salaries"), actions: { read: true } },
        {
          resource: Class("salary"),
          actions: {
            read: Query(
              Lambda("salaryRef", Let(
                {
                  salary: Get(Var("salaryRef")),
                  userRef: Select( ["data", "user"], Var("salary"))
                },
                Or(
                  Equals(Var("userRef"), Identity()),
                  Exists(
                    Match(Index("is_subordinate"), [Var("userRef"), Identity()])
                  )
                ))
              )
            )
          }
        }
      ]
    })

    This role employs a Lambda function that permits access to a user’s salary, and to the salaries of subordinates.

  10. Verify salary access for a user

    Now we can log in to the database as Bob and run the salary listing query. First we have to create a token for Bob:

    Login(Match(Index("user_by_name"), "Bob"), { password: "123" })

    The output should look similar to:

    { ref: Ref(Tokens(), "231651464569684480"),
      ts: 1557178902130000,
      instance: Ref(Class("users"), "231651384582210048"),
      secret: 'fnEDNv3HmWACAAM2_aC3wAIAGOysa8knR3F3ZzvUkc0sq_O6chQ' }

    Using the secret, we can log in to the database and run the user listing query. In a separate terminal, start a new Fauna Shell session, and be sure to copy the value of the secret field as the value of the --secret argument in the following command:

    fauna shell --secret="fnEDNv3HmWACAAM2_aC3wAIAGOysa8knR3F3ZzvUkc0sq_O6chQ"
    Warning: You didn't specify a database. Starting the shell in the global scope.
    Connected to http://faunadb:8443
    Type Ctrl+D or .exit to exit the shell
     

    Then run this query:

    Map(
      Paginate(Match(Index("all_salaries"))),
      Lambda("salaryRef", Let({
        salary: Get(Var("salaryRef")),
        user: Get(Select(["data", "user"], Var("salary")))
      },
      {
        user: Select(["data", "name"], Var("user")),
        salary: Select(["data", "salary"], Var("salary"))
      })
    ))

    You should see the following output:

    { data: [ { user: 'Bob', salary: 95000 } ] }

    So, we can see that Bob can only query his own salary.

  11. Verify salary access for a manager

    In the original Fauna Shell session, create a login token for Mary:

    Login(Match(Index("user_by_name"), "Mary"), { password: "123" })

    You should see output similar to the following:

    { ref: Ref(Tokens(), "231573285766169088"),
      ts: 1557104345000000,
      instance: Ref(Class("users"), "231573095978109440"),
      secret: 'fnEDNv4fcEACAAM2_aC3wAIANL6untGn8nhY-NK2O90oHyIeWuY' }

    In a new terminal, start a new Fauna Shell session, and be sure to copy the value of the secret field as the value of the --secret argument in the following command:

    fauna shell --secret="fnEDNv4fcEACAAM2_aC3wAIANL6untGn8nhY-NK2O90oHyIeWuY"
    Warning: You didn't specify a database. Starting the shell in the global scope.
    Connected to http://faunadb:8443
    Type Ctrl+D or .exit to exit the shell
     

    Then run the salary lookup query:

    Map(
      Paginate(Match(Index("all_salaries"))),
      Lambda("salaryRef", Let({
        salary: Get(Var("salaryRef")),
        user: Get(Select(["data", "user"], Var("salary")))
      },
      {
        user: Select(["data", "name"], Var("user")),
        salary: Select(["data", "salary"], Var("salary"))
      })
    ))

    You should see the following output (the order may vary):

    { data:
       [ { user: 'Bob', salary: 95000 },
         { user: 'Mary', salary: 120000 },
         { user: 'John', salary: 70000 } ] }

    Mary can see the salaries for herself, Bob, and John.

Was this article helpful?

We're sorry to hear that.
Tell us how we can improve! documentation@fauna.com

Thank you for your feedback!