Role-based authentication

The following procedure demonstrates setting up User-defined roles and assigning them to UDFs for better data security.

For purposes of illustration, this tutorial imagines an online service called Weather Data, in which users can read about and report on weather information around the world. The service has three kinds of users:

  • Readers, who can only read the available information.

  • Reporters, who can read from and write to the database.

  • Managers, who can create new users and also have read/write access to the database.

The service has three UDFs:

  • Get_weather_reports, which anyone can call.

  • Create_weather_report, which only Reporters can call.

  • Create_weather_user, which only Managers can call.

To follow along with this procedure, you need a Fauna account. For help with setting up a Fauna account, see the Dashboard quick start.

Step 1: Create a new database

Navigate to the Fauna Dashboard and create a new database called weather_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 weather_reports and another one called weather_users:

Copied!
[
  ({ name: 'weather_reports' }),
  ({ name: 'weather_users' })
]
[
  {
    ref: ("weather_reports"),
    ts: 1648443426630000,
    history_days: 30,
    name: 'weather_reports'
  },
  {
    ref: ("weather_users"),
    ts: 1648443426630000,
    history_days: 30,
    name: 'weather_users'
  }
]
Query metrics:
  •    bytesIn: 119

  •   bytesOut: 310

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   2

  •  readBytes: 880

  • writeBytes: 704

  •  queryTime: 9ms

  •    retries:   0

Step 3: Create UDFs

We need three UDFs to perform the functions of this application. The first one is called Get_weather_reports, and it uses the Documents function to retrieve all the records in the weather_reports collection.

We haven’t created the required roles yet, so these UDFs are created without the role definitions. Once the roles have been created, we can update them to add the role definitions.
Copied!
({
  name: 'Get_weather_reports',
  body: (
    (
      [],
      (
        ((("weather_reports"))),
        ("report", (("report")))
      )
    )
  ),
})
{
  ref: ("Get_weather_reports"),
  ts: 1648443426910000,
  name: 'Get_weather_reports',
  body: (([], (((("weather_reports"))), ("report", (("report"))))))
}
Query metrics:
  •    bytesIn: 232

  •   bytesOut: 340

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   1

  •  readBytes:  32

  • writeBytes: 482

  •  queryTime: 7ms

  •    retries:   0

The next function, Create_weather_report, allows Reporters to create new documents in the weather_reports collection.

Copied!
({
  name: 'Create_weather_report',
  body: (
    (
      ["date", "city", "country", "temperature"],
      (("weather_reports"), {
        data: {
          date: ("date"),
          city: ("city"),
          country: ("country"),
          temperature: ("temperature")
        }
      })
    )
  ),
})
{
  ref: ("Create_weather_report"),
  ts: 1648443427200000,
  name: 'Create_weather_report',
  body: ((["date", "city", "country", "temperature"], (("weather_reports"), {"data": {"date": ("date"), "city": ("city"), "country": ("country"), "temperature": ("temperature")}})))
}
Query metrics:
  •    bytesIn: 332

  •   bytesOut: 442

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   1

  •  readBytes:  34

  • writeBytes: 562

  •  queryTime: 9ms

  •    retries:   0

The last UDF, Create_weather_user, allows Managers to create new documents in the weather_users collection. Note that the password field is passed via the credentials parameter, which causes the password string to be stored as a one-way cryptographic hash that is subsequently used to authenticate the user. For more information, see Credentials.

Copied!
({
  name: 'Create_weather_user',
  body: (
    (
      ["name", "email", "password"],
      (("weather_users"), {
        credentials: { password: ("password") },
        data: { email: ("email"), name: ("name") }
      })
    )
  ),
})
{
  ref: ("Create_weather_user"),
  ts: 1648443427490000,
  name: 'Create_weather_user',
  body: ((["name", "email", "password"], (("weather_users"), {"credentials": {"password": ("password")}, "data": {"email": ("email"), "name": ("name")}})))
}
Query metrics:
  •    bytesIn:  311

  •   bytesOut:  419

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   32

  • writeBytes:  540

  •  queryTime: 16ms

  •    retries:    0

Step 4: Create new user-defined roles

The next step is to create roles with narrowly-defined privileges to execute the UDFs.

First, create a new role called Reader with the read privilege on the weather_reports collection and the call privilege on the Get_weather_reports function:

Copied!
({
  name: "Reader",
  privileges: [
    {
      resource: ("weather_reports"),
      actions: { read: true }
    },
    {
      resource: ("Get_weather_reports"),
      actions: { call: true }
    }
  ]
})
{
  ref: ("Reader"),
  ts: 1648443427780000,
  name: 'Reader',
  privileges: [
    {
      resource: ("weather_reports"),
      actions: { read: true }
    },
    {
      resource: ("Get_weather_reports"),
      actions: { call: true }
    }
  ]
}
Query metrics:
  •    bytesIn:  245

  •   bytesOut:  365

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  296

  • writeBytes:  405

  •  queryTime: 43ms

  •    retries:    0

Next, create a role named Reporter. This role can create new documents in weather_reports, read from weather_reports, and call the Create_weather_report and Get_weather_reports functions.

Copied!
({
  name: "Reporter",
  privileges: [
    {
      resource: ("weather_reports"),
      actions: { read: true, create: true }
    },
    {
      resource: ("Get_weather_reports"),
      actions: { call: true }
    },
    {
      resource: ("Create_weather_report"),
      actions: { call: true }
    }
  ]
})
{
  ref: ("Reporter"),
  ts: 1648443428070000,
  name: 'Reporter',
  privileges: [
    {
      resource: ("weather_reports"),
      actions: { read: true, create: true }
    },
    {
      resource: ("Get_weather_reports"),
      actions: { call: true }
    },
    {
      resource: ("Create_weather_report"),
      actions: { call: true }
    }
  ]
}
Query metrics:
  •    bytesIn:  357

  •   bytesOut:  501

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  572

  • writeBytes:  456

  •  queryTime: 20ms

  •    retries:    0

Lastly, create a role named Manager. This role has read and create privileges on the weather_users collection, read privileges on weather_reports, and can call Create_weather_user and Get_weather_reports.

Copied!
({
  name: "Manager",
  privileges: [
    {
      resource: ("weather_reports"),
      actions: { read: true }
    },
    {
      resource: ("weather_users"),
      actions: { read: true, create: true }
    },
    {
      resource: ("Get_weather_reports"),
      actions: { call: true }
    },
    {
      resource: ("Create_weather_user"),
      actions: { call: true }
    }
  ]
})
{
  ref: ("Manager"),
  ts: 1648443428350000,
  name: 'Manager',
  privileges: [
    {
      resource: ("weather_reports"),
      actions: { read: true }
    },
    {
      resource: ("weather_users"),
      actions: { read: true, create: true }
    },
    {
      resource: ("Get_weather_reports"),
      actions: { call: true }
    },
    {
      resource: ("Create_weather_user"),
      actions: { call: true }
    }
  ]
}
Query metrics:
  •    bytesIn:  540

  •   bytesOut:  727

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  904

  • writeBytes:  527

  •  queryTime: 18ms

  •    retries:    0

Now your roles are all established, and you can assign them to the UDFs you created earlier.

Step 5: Assign roles to UDFs

Use the Update function to update your UDFs to have associated roles.

The following query:

  • Updates the Get_weather_reports function to have the Reader role.

  • Updates the Create_weather_report function to have the Reporter role.

  • Updates the Create_weather_user function to have the Manager role.

Copied!
[
  (
    ("Get_weather_reports"),
    { role: ("Reader") }
  ),
  (
    ("Create_weather_report"),
    { role: ("Reporter") }
  ),
  (
    ("Create_weather_user"),
    { role: ("Manager") }
  )
]
[
  {
    ref: ("Get_weather_reports"),
    ts: 1648443428630000,
    name: 'Get_weather_reports',
    body: (([], (((("weather_reports"))), ("report", (("report")))))),
    role: ("Reader")
  },
  {
    ref: ("Create_weather_report"),
    ts: 1648443428630000,
    name: 'Create_weather_report',
    body: ((["date", "city", "country", "temperature"], (("weather_reports"), {"data": {"date": ("date"), "city": ("city"), "country": ("country"), "temperature": ("temperature")}}))),
    role: ("Reporter")
  },
  {
    ref: ("Create_weather_user"),
    ts: 1648443428630000,
    name: 'Create_weather_user',
    body: ((["name", "email", "password"], (("weather_users"), {"credentials": {"password": ("password")}, "data": {"email": ("email"), "name": ("name")}}))),
    role: ("Manager")
  }
]
Query metrics:
  •    bytesIn:   285

  •   bytesOut: 1,389

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     3

  •  readBytes: 1,336

  • writeBytes:   833

  •  queryTime:   8ms

  •    retries:     0

Step 6: Create API keys

If you are following along using the Dashboard’s Shell, you can skip ahead to Step 7: Create documents with the UDFs.

Client applications which access your Fauna database with the secret from a key or a token. A secret is a password-equivalent that connects the application to a specific database using role-based privileges.

Each key that you create should be assigned a role with sufficient privileges to do its intended job, and no more. Tokens have no implicit privileges, and require creation of custom roles to grant privileges.

The following query creates three keys, having the roles we need for this example:

Copied!
[
  ({ role: ("Reader") }),
  ({ role: ("Reporter") }),
  ({ role: ("Manager") })
]
[
  {
    ref: ((), "327349257024569856"),
    ts: 1648443428920000,
    role: ("Reader"),
    secret: 'fnAEivpqNnACAEYLR13MyfbKJCtIE89XzF-uVouT',
    hashed_secret: '$2a$05$92XljLnZx617a.1OtwGOM.qiDvCqlX9rBPCsvKCchDgs2SUblJXhO'
  },
  {
    ref: ((), "327349257024570880"),
    ts: 1648443428920000,
    role: ("Reporter"),
    secret: 'fnAEivpqNnAGANdBiR5wMXWfDA7srJboP56W9BJ3',
    hashed_secret: '$2a$05$7h8izslky8S2R7J5k4GkwO2nQy60YjABf/ZZQGV.xd9mPx3AtEyDG'
  },
  {
    ref: ((), "327349257024571904"),
    ts: 1648443428920000,
    role: ("Manager"),
    secret: 'fnAEivpqNnAKAKvpBgWBqozEZ-MUmtysS2ds8Qzi',
    hashed_secret: '$2a$05$IC5MLu9d.shgWt0Mzfe/5uJ5u1myYtj4qUhMI5mFaf.BRtiMqE1I.'
  }
]
Query metrics:
  •    bytesIn:   163

  •   bytesOut:   926

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     3

  •  readBytes:   668

  • writeBytes: 1,017

  •  queryTime:  17ms

  •    retries:     0

A key’s secret is only displayed once, so be sure to save it in a safe place.

If you’d like to do the same in the Fauna Dashboard:

  1. Click the SECURITY link in the left-side navigation.

  2. Click the NEW KEY link.

  3. Select an appropriate role from the ROLE dropdown menu.

  4. Give the key a name which describes its intended function.

  5. Click SAVE.

Step 7: Create documents with the UDFs

So far, neither of the two collections have any documents. Now you can use your UDFs to add some documents.

Create a weather user document

In the Dashboard

  1. Click SHELL in the left-side navigation.

  2. Click the RUN AS button near the bottom of the screen. Select Manager from the dropdown menu.

  3. Enter the following query and click the RUN QUERY AS button:

    shellCopied!
    (
      ("Create_weather_user"),
      "Alice Smith",
      "alice@example.com",
      "mypass123"
    )
    {
      ref: (("weather_users"), "327349258408690176"),
      ts: 1648443430240000,
      data: { email: 'alice@example.com', name: 'Alice Smith' }
    }
    Query metrics:
    •    bytesIn:   103

    •   bytesOut:   225

    • computeOps:     1

    •    readOps:     0

    •   writeOps:     2

    •  readBytes:    48

    • writeBytes:   650

    •  queryTime: 658ms

    •    retries:     0

Note that if you change the RUN AS dropdown menu to Reader and run the query again it fails, because the Reader role does not have the necessary privilege to run the Create_weather_user UDF.

Using a driver

Copied!
(
  ("Create_weather_user"),
  "Alice Smith",
  "alice@example.com",
  "mypass123"
)
{
  ref: (("weather_users"), "327349258408690176"),
  ts: 1648443430240000,
  data: { email: 'alice@example.com', name: 'Alice Smith' }
}
Query metrics:
  •    bytesIn:   103

  •   bytesOut:   225

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     2

  •  readBytes:    48

  • writeBytes:   650

  •  queryTime: 658ms

  •    retries:     0

Create a weather report

In the Dashboard

  1. If the Shell screen is not in view, click SHELL in the left-side navigation.

  2. Change the RUN AS dropdown menu to Reporter and run the following query:

    shellCopied!
    (
      ("Create_weather_report"),
      "2022-02-24",
      "Paris",
      "France",
      "42 degrees F"
    )
    {
      ref: (("weather_reports"), "327349259058807296"),
      ts: 1648443430860000,
      data: {
        date: '2022-02-24',
        city: 'Paris',
        country: 'France',
        temperature: '42 degrees F'
      }
    }
    Query metrics:
    •    bytesIn:  104

    •   bytesOut:  261

    • computeOps:    1

    •    readOps:    0

    •   writeOps:    1

    •  readBytes:    0

    • writeBytes:  260

    •  queryTime: 28ms

    •    retries:    0

Using a driver

Copied!
(
  ("Create_weather_report"),
  "2022-02-24",
  "Paris",
  "France",
  "42 degrees F"
)
{
  ref: (("weather_reports"), "327349259058807296"),
  ts: 1648443430860000,
  data: {
    date: '2022-02-24',
    city: 'Paris',
    country: 'France',
    temperature: '42 degrees F'
  }
}
Query metrics:
  •    bytesIn:  104

  •   bytesOut:  261

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:    0

  • writeBytes:  260

  •  queryTime: 28ms

  •    retries:    0

Step 8: Fetch existing weather reports

In the Dashboard

  1. If the Shell screen is not in view, click SHELL in the left-side navigation.

  2. Change the RUN AS dropdown menu to Reader and run the following query:

    Copied!
    ("Get_weather_reports")
    {
      data: [
        {
          ref: (("weather_reports"), "327349259058807296"),
          ts: 1648443430860000,
          data: {
            date: '2022-02-24',
            city: 'Paris',
            country: 'France',
            temperature: '42 degrees F'
          }
        }
      ]
    }
    Query metrics:
    •    bytesIn:   45

    •   bytesOut:  272

    • computeOps:    1

    •    readOps:    9

    •   writeOps:    0

    •  readBytes:  374

    • writeBytes:    0

    •  queryTime: 22ms

    •    retries:    0

You can also run this query as either of the other roles, because they all have the Call privilege for it.

Using a driver

Copied!
("Get_weather_reports")
{
  data: [
    {
      ref: (("weather_reports"), "327349259058807296"),
      ts: 1648443430860000,
      data: {
        date: '2022-02-24',
        city: 'Paris',
        country: 'France',
        temperature: '42 degrees F'
      }
    }
  ]
}
Query metrics:
  •    bytesIn:   45

  •   bytesOut:  272

  • computeOps:    1

  •    readOps:    9

  •   writeOps:    0

  •  readBytes:  374

  • writeBytes:    0

  •  queryTime: 22ms

  •    retries:    0

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!