Identity-based authentication

User-defined roles can have a membership attribute which describes the set of documents that should have the role’s privileges. You can use the membership attribute to control which identities can perform actions on particular collections, as well as other restrictions.

A membership definition can include a predicate function which is evaluated whenever a query is made against the covered documents. The following example procedure demonstrates how to use a membership attribute to restrict data access.

This examples imagines a company database with two collections called Users and Customers. The Users collection contains documents with information about company employees, including name, role, and department. The Customers collection contains information about the company’s customers, including name, location, and year-to-date revenue generated. Different employees have different levels of access to the two collections based on their department and role.

Step 1: Create a new database

Navigate to the Fauna Dashboard and create a new database called company_data.

Once your database is created, you can interact with it either with a driver, with the Dashboard’s Shell, or with the fauna-shell command-line tool. All of the subsequent steps in this example provide FQL queries in each supported language, well as Fauna Query Language for use in the Dashboard’s Shell or fauna-shell reference.

Step 2: Create two new collections

Create a new collection called Users and another one called Customers:

Copied!
[
  ({ name: 'Users' }),
  ({ name: 'Customers' })
]
[
  {
    ref: ("Users"),
    ts: 1649797461320000,
    history_days: 30,
    name: 'Users'
  },
  {
    ref: ("Customers"),
    ts: 1649797461320000,
    history_days: 30,
    name: 'Customers'
  }
]
Query metrics:
  •    bytesIn:  105

  •   bytesOut:  282

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    2

  •  readBytes:  818

  • writeBytes:  662

  •  queryTime: 36ms

  •    retries:    0

Step 3: Create new users

The following example creates four documents in the Users collection, each with personal information and login credentials for each user.

Copied!
[
  (
    (("Users"), "1"),
    {
      data: {
        name: 'Donna Merrick',
        role: 'manager',
        department: 'HR'
      },
      credentials: { password: 'abc123' }
    }
  ),
  (
    (("Users"), "2"),
    {
      data: {
        name: 'John Morales',
        role: 'recruiter',
        department: 'HR'
      },
      credentials: { password: 'def123' }
    }
  ),
  (
    (("Users"), "3"),
    {
      data: {
        name: 'Sam Grant',
        role: 'manager',
        department: 'Data Analytics'
      },
      credentials: { password: 'ghi123' }
    }
  ),
  (
    (("Users"), "4"),
    {
      data: {
        name: 'Arlene Lee',
        role: 'analyst',
        department: 'Data Analytics'
      },
      credentials: { password: 'jkl123' }
    }
  )
]
[
  {
    ref: (("Users"), "1"),
    ts: 1649803776970000,
    data: { name: 'Donna Merrick', role: 'manager', department: 'HR' }
  },
  {
    ref: (("Users"), "2"),
    ts: 1649803776970000,
    data: { name: 'John Morales', role: 'recruiter', department: 'HR' }
  },
  {
    ref: (("Users"), "3"),
    ts: 1649803776970000,
    data: {
      name: 'Sam Grant',
      role: 'manager',
      department: 'Data Analytics'
    }
  },
  {
    ref: (("Users"), "4"),
    ts: 1649803776970000,
    data: {
      name: 'Arlene Lee',
      role: 'analyst',
      department: 'Data Analytics'
    }
  }
]
Query metrics:
  •    bytesIn:   803

  •   bytesOut:   820

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     8

  •  readBytes:   248

  • writeBytes: 2,606

  •  queryTime:  28ms

  •    retries:     0

Step 4: Create customer data

The next example creates a document in the Customers collection.

Copied!
(("Customers"),
  {
    data: {
      name: 'Acme Widgets, Inc.',
      location: 'Hoboken, NJ',
      YTD_orders: 35 
    }
  }
)
{
  ref: (("Customers"), "328775685877268992"),
  ts: 1649803777480000,
  data: {
    name: 'Acme Widgets, Inc.',
    location: 'Hoboken, NJ',
    YTD_orders: 35
  }
}
Query metrics:
  •    bytesIn:  148

  •   bytesOut:  241

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:    0

  • writeBytes:  251

  •  queryTime: 18ms

  •    retries:    0

Step 5: Create roles

This is where we set up our data access rules. First, set up two roles which govern access to the Customers collection:

  • Any user who is a member of the Data Analytics department can perform read operations.

  • Users who are managers in the Data Analytics department can perform read, write, create, and delete operations.

The first role is called DA-reader. It covers documents in the Customers collection and contains a predicate function which checks to see if the caller belongs to the Data Analytics department.

Copied!
({
  name: "DA-reader",
  membership: [
    {
      resource: ("Customers"),
      predicate: 
        (
          (
            ["ref"],
            ("Data Analytics", (["data", "department"], (("ref"))))
          )
        )
    }
  ],
  privileges: [
    {
      resource: ("Customers"),
      actions: {
        read: true,
      }
    }
  ]
})
{
  ref: ("DA-reader"),
  ts: 1649803777990000,
  name: 'DA-reader',
  membership: [
    {
      resource: ("Customers"),
      predicate: ((["ref"], ("Data Analytics", (["data", "department"], (("ref"))))))
    }
  ],
  privileges: [ { resource: ("Customers"), actions: { read: true } } ]
}
Query metrics:
  •    bytesIn:  354

  •   bytesOut:  507

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  119

  • writeBytes:  642

  •  queryTime: 15ms

  •    retries:    0

The second role is called DA-manager. It also covers documents in the Customers collection, and its predicate function checks to see if the caller is a manager in the Data Analytics department.

Copied!
({
  name: "DA-manager",
  membership: [
    {
      resource: ("Customers"),
      predicate: 
        (
          (
            ["ref"],
            (
              ("manager", (["data", "role"], (("ref")))),
              ("Data analytics", (["data", "department"], (("ref"))))
            )
          )
        )
    }
  ],
  privileges: [
    {
      resource: ("Customers"),
      actions: {
        read: true,
        write: true,
        create: true,
        delete: true,
      }
    }
  ]
})
{
  ref: ("DA-manager"),
  ts: 1649803778490000,
  name: 'DA-manager',
  membership: [
    {
      resource: ("Customers"),
      predicate: ((["ref"], (("manager", (["data", "role"], (("ref")))), ("Data analytics", (["data", "department"], (("ref")))))))
    }
  ],
  privileges: [
    {
      resource: ("Customers"),
      actions: { read: true, write: true, create: true, delete: true }
    }
  ]
}
Query metrics:
  •    bytesIn:  485

  •   bytesOut:  639

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  162

  • writeBytes:  729

  •  queryTime: 15ms

  •    retries:    0

Next, set up a role to govern access to the Users collection. This one is simpler: only managers in the HR department can access the Users collection.

Copied!
({
  name: "HR-manager",
  membership: [
    {
      resource: ("Users"),
      predicate: 
        (
          (
            ["ref"],
            (
              ("manager", (["data", "role"], (("ref")))),
              ("HR", (["data", "department"], (("ref"))))
            )
          )
        )
    }
  ],
  privileges: [
    {
      resource: ("Users"),
      actions: {
        read: true,
        write: true,
        create: true,
        delete: true,
      }
    }
  ]
})
{
  ref: ("HR-manager"),
  ts: 1649803778980000,
  name: 'HR-manager',
  membership: [
    {
      resource: ("Users"),
      predicate: ((["ref"], (("manager", (["data", "role"], (("ref")))), ("HR", (["data", "department"], (("ref")))))))
    }
  ],
  privileges: [
    {
      resource: ("Users"),
      actions: { read: true, write: true, create: true, delete: true }
    }
  ]
}
Query metrics:
  •    bytesIn:  465

  •   bytesOut:  619

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  116

  • writeBytes:  717

  •  queryTime: 13ms

  •    retries:    0

There is, however, a special property of Fauna’s ABAC implementation: a document can always read and modify itself, unless prevented by a higher-priority role definition. Under our current rules, a user such as Sam Grant, who is not a manager in the HR department, would be able to read and modify his own document in the Users collection (but no one else’s). To prevent this, we can create a role with no permissions on the Users collection and no predicate function, so it applies to all callers:

Copied!
({
  name: "HR-default",
  membership: [
    {
      resource: ("Users"),
    },
  ],
  privileges: []
})
{
  ref: ("HR-default"),
  ts: 1649803779480000,
  name: 'HR-default',
  membership: [ { resource: ("Users") } ],
  privileges: []
}
Query metrics:
  •    bytesIn:  126

  •   bytesOut:  239

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  158

  • writeBytes:  509

  •  queryTime: 16ms

  •    retries:    0

This role prevents all users from accessing the Users collection by default. Users who meet the criteria in the predicate function of the HR-manager role can still perform actions according to the privileges attribute of that role. For more information about ABAC behavior when multiple roles apply to the same collection, see overlapping roles.

Step 6: Test the roles

To test the roles, you can use the Login function to log in as any one of the existing users, then check to see that the user can only access the data in the collections as the roles allow.

The following example logs in with the password for John Morales:

{
  ref: ((), "328775688500806144"),
  ts: 1649803779990000,
  instance: (("Users"), "2"),
  secret: 'fnEEkAu_K-ACAASQC7uuwAYABpPNOFZ1S-MxQKGlFZcl_9QcRBg'
}
Query metrics:
  •    bytesIn:   91

  •   bytesOut:  291

  • computeOps:    1

  •    readOps:    1

  •   writeOps:    1

  •  readBytes:  214

  • writeBytes:  433

  •  queryTime: 20ms

  •    retries:    0

Once you have the secret returned by the Login function, you can use it to perform actions as the identity associated with it.

Summary

Creating custom roles with narrowly-defined privileges is the best way to maintain access control in your databases and client applications.

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!