Build Your Own Role-Based Access Control in Payload
Tags: javascript, node, webdev, tutorial
Published: 2021-07-14 12:55:02

---
title: Build Your Own Role-Based Access Control in Payload
published: true
description: Learn how to build your own role-based access control in Payload
tags: javascript, node, webdev, tutorial
cover_image: https://payloadcms.com/images/blog/build-your-own-rbac-dev-to.jpg
canonical_url: https://payloadcms.com/blog/build-your-own-rbac
---

Payload comes with open-ended [access control](https://payloadcms.com/docs/access-control/overview). You can define whatever type of pattern that you can dream up, and best of all—it's all done with simple JavaScript.

A common pattern is Role-Based Access Control. Here, we'll walk you through how to create your own RBAC pattern on both the collection-level and field-level.

In more detail, here are the pieces that we will be building:

- Users collection with `role` field
- Orders collection
  - A `beforeChange` hook to save which user created the order to a `createdBy` field
  - Access Control functions to restrict Admin Panel access to `admin` roles or the creator of the order
  - admin-only field level access

#### Initialize Project

We'll be using `create-payload-app` to build out the initial project.

1. Run `npx create-payload-app payload-rbac`
2. Select `javascript` for language
3. Select `blank` for our template
4. Follow all other prompts

This will give us a simple project with a Payload config and Users collection. The structure of the project will be:

```
├─ payload.config.js
└─ collections/
  └─ Users.js
  └─ Orders.js
```

#### Modify Users Collection

First, we will add the `role` field to our Users collection with 2 options: `admin` and `user`.

```js
const Users = {
  slug: 'users',
  auth: true,
  admin: {
    useAsTitle: 'email',
  },
  fields: [
    {
      name: 'role',
      type: 'select',
      options: [
        { label: 'Admin', value: 'admin' },
        { label: 'User', value: 'user' },
      ],
      required: true,
      defaultValue: 'user',
    },
  ],
};

export default Users;
```

#### Create Orders Collection

Next, we will create a new `Orders.js` collection in our `collections/` directory and scaffold out basic fields and values - including the `createdBy` relationship to the user.

```js
const Orders = {
  slug: 'orders',
  fields: [
    {
      name: 'items',
      type: 'array',
      fields: [
        {
          name: 'item',
          type: 'text',
        }
      ]
    },
    {
      name: 'createdBy',
      type: 'relationship',
      relationTo: 'users',
      access: {
        update: () => false,
      },
      admin: {
        readOnly: true,
        position: 'sidebar',
        condition: data => Boolean(data?.createdBy)
      },
    },
  ]
}

export default Orders;
```

The Orders collection has an `array` field for items and a `createdBy` field which is a relationship to our `Users` collection. The `createdBy` field will feature a strict `update` access control function so that it can never be changed.

Notice we also have a `condition` function under the `createdBy` field's access. This will hide `createdBy` until it has a value.

#### Set the `createdBy` Attribute Using a Hook

Next, we'll add a hook that will run before any order is created. This is done by adding a `beforeChange` hook to our collection definition.

```js
const Orders = {
  slug: 'orders',
  fields: [
    // Collapsed
  ],
  hooks: {
    beforeChange: [
      ({ req, operation, data }) => {
        if (operation === 'create') {
          if (req.user) {
            data.createdBy = req.user.id;
            return data;
          }
        }
      },
    ],
  },
}
```

The logic in this hook sets the `createdBy` field to be the current user's `id` value, only if it is on a `create` operation. This will create a relationship between an order and the user who created it.

#### Access Control

Next, the access control for the collection can be defined. Payload's access control is based on functions. An access control function returns either a `boolean` value to allow/disallow access _or_ it returns a query constraint that filters the data.

We want our function to handle a few scenarios:

1. A user has the 'admin' role - access _all_ orders
2. A user created the order - allow access to only those orders
3. Any other user - disallow access

```js
const isAdminOrCreatedBy = ({ req: { user } }) => {
  // Scenario #1 - Check if user has the 'admin' role
  if (user && user.role === 'admin') {
    return true;
  }

  // Scenario #2 - Allow only documents with the current user set to the 'createdBy' field
  if (user) {

    // Will return access for only documents that were created by the current user
    return {
      createdBy: {
        equals: user.id,
      },
    };
  }

  // Scenario #3 - Disallow all others
  return false;
};
```

Once defined, this function is added to the `access` property of the collection definition:

```js
const Orders = {
  slug: 'orders',
  fields: [
    // Collapsed
  ],
  access: {
    read: isAdminOrCreatedBy,
    update: isAdminOrCreatedBy,
    delete: isAdminOrCreatedBy,
  },
  hooks: {
    // Collapsed
  },
}
```

With this function added to the `read`, `update`, and `delete` access properties, the function will run whenever these operations are attempted on the collection.

> Note: Access functions default to requiring a logged-in user. This is why `create` does not need to be defined for this example, since we want any logged in user to be able to create an order.

#### Put It All Together

The last step is to add the collection to our `payload.config.js`

```js
import { buildConfig } from 'payload/config';
import Orders from './collections/Orders';
import Users from './collections/Users';

export default buildConfig({
  serverURL: 'http://localhost:3000',
  admin: {
    user: Users.slug,
  },
  collections: [
    Users,
    Orders,
  ],
});
```

Let's verify the functionality:

Start up the project by running `npm run dev` or `yarn dev` and navigate to `http://localhost:3000/admin`

Create your initial user with the `admin` role.
![Payload login](https://payloadcms.com/images/blog/rbac/1-login.png)

Create an Order with the `admin` user.
![Create Order as admin](https://payloadcms.com/images/blog/rbac/2-create-order-1.png)
![Save Order as admin](https://payloadcms.com/images/blog/rbac/3-save-order-as-admin.png)

Create an additional user with the `user` role by navigating to the Users collection, selecting Create New, entering an email/password, then saving.
![Create Normal User](https://payloadcms.com/images/blog/rbac/4-create-normal-user.png)
![Save Normal User](https://payloadcms.com/images/blog/rbac/5-create-normal-user-after.png)

Log out of your `admin` user by selecting the icon in the bottom left, then log in with the second user.
![Normal User Login](https://payloadcms.com/images/blog/rbac/6-normal-user-login.png)

You'll notice if we go to the Orders collection, no Orders will be shown. This indicates that the access control is working properly.
![View Orders as Normal User](https://payloadcms.com/images/blog/rbac/7-view-orders-1.png)
![No Orders Show](https://payloadcms.com/images/blog/rbac/8-no-orders-shown.png)

Create another Order. Note that the current user will be saved to `Created By` in the sidebar.
![Create Order as Normal User](https://payloadcms.com/images/blog/rbac/9-create-order-as-normal-user.png)

Navigate back to Orders list on the dashboard. There will only be the single order created by the current user.
![View Single Order As Normal User](https://payloadcms.com/images/blog/rbac/10-view-single-order-as-normal-user.png)

Log out, then back in with your `admin` user. You should be able to see the original Order as well as the Order created by the second user.
![View All Orders As Admin](https://payloadcms.com/images/blog/rbac/11-viewing-all-orders-as-admin.png)

#### Field Level Access Control
With everything working at the collection level, we can carry the concepts further and see how they can be applied at the field level. Suppose we wanted to add a `paymentID` field only for Admin users. Create an `isAdmin` function that checks the role as we did earlier.
```js
const isAdmin = ({ req: { user } }) => (user && user.role === 'admin');
```

Add a new field to Orders and set `create`, `read` or `update` access calls to use the isAdmin function.

```js
const Orders = {
  slug: 'orders',
  fields: [
    // Collapsed
    {
      name: 'paymentId',
      type: 'text',
      access: {
        create: isAdmin,
        read: isAdmin,
        update: isAdmin,
      },
    }
  ],
  // Collapsed
}
```

The new paymentID field is not available to the users even on one's own Order. Field level access controls allow for greater granularity over document level access for Collections and Globals. This shows how easy it is to manage exact permissions throughout the admin UI, GraphQL and REST endpoints; it even works when querying relationships to keep data secure.

#### What Other Improvements Can Be Made?

Now that we have a basic example working. What are some ways that this could be improved?

- Ideally, we'd want to use both the hook and the access control function across multiple collections in our application. Since it's just JavaScript, we can extract each of these functions into their own file for re-use.
- Add additional roles, such as an `editor` role which allows reading and editing, _but disallows creating_. This all can be customized specifically to your needs.

#### Questions or Comments? Join us on GitHub Discussions

I hope you enjoyed the introduction to doing role-based access control with Payload!

Come join the Payload [discussions](https://github.com/payloadcms/payload/discussions) on GitHub.

#### Further Reading
- [Source Code for this post](https://github.com/payloadcms/blog-rbac)
- Documentation
  - [Access Control](https://payloadcms.com/docs/access-control/overview)
  - [Hooks](https://payloadcms.com/docs/hooks/overview)
  - [Collections](https://payloadcms.com/docs/configuration/collections)