Access Control Setup
Type-safe role-based access control
Setup
- Add roles field to Users:
import { rolesField } from '@adsign/payload-adsign-plugin';
export const Users: CollectionConfig = {
fields: [
rolesField({
name: 'roles',
roles: [
{
label: 'Admin',
name: 'admin',
},
{
label: 'Editor',
name: 'editor',
},
],
}),
],
};- Create access control helper:
import { createAccessControl } from '@adsign/payload-adsign-plugin';
export const { role } = createAccessControl({
collection: 'users',
rolesField: 'roles',
});- Use in collections:
import { role } from './access';
export const Articles: CollectionConfig = {
access: {
read: role(['admin', 'editor', 'public']),
create: role(['admin', 'editor']),
update: role(['admin', 'editor']),
delete: role(['admin']),
},
};Special Roles
'public'- Access for everyone (including unauthenticated)'none'- No access for anyone
Field-Level Access
export const Articles: CollectionConfig = {
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'internalNotes',
type: 'textarea',
access: {
read: role(['admin']),
update: role(['admin']),
},
},
],
};Conditional Roles
Conditional roles grant access per document instead of per user, e.g. "authors can edit their own articles". Each has a name and a where that returns a Payload query.
import { createAccessControl, type ConditionalRole } from '@adsign/payload-adsign-plugin';
export const conditionalRoles: ConditionalRole[] = [
{
// Documents where the user is the author
name: 'author',
where: ({ req }) => {
return {
author: {
equals: req.user?.id,
},
};
},
},
{
// The user's own account
name: 'self',
where: ({ req }) => {
return {
id: {
equals: req.user?.id,
},
};
},
},
];
export const { role } = createAccessControl({
collection: 'users',
rolesField: 'roles',
conditionalRoles,
});Pass the same array to the roles field, so the conditional roles type-check in role([...]):
import { conditionalRoles, role } from '../access';
export const Users: CollectionConfig = {
slug: 'users',
access: {
read: role(['admin', 'self']),
update: role(['admin', 'self']),
},
fields: [
rolesField({
name: 'roles',
roles: [
{
label: 'Admin',
name: 'admin',
},
{
label: 'Editor',
name: 'editor',
},
],
conditionalRoles,
// Without this, `self` lets users give themselves any role
access: {
update: role(['admin']),
},
}),
],
};Use them like any other role, on collections and on fields:
export const Articles: CollectionConfig = {
slug: 'articles',
access: {
read: role(['public']),
update: role(['admin', 'author']),
},
fields: [
{
name: 'author',
type: 'relationship',
relationTo: 'users',
},
{
name: 'authorNotes',
type: 'textarea',
access: {
read: role(['admin', 'author']),
update: role(['admin', 'author']),
},
},
],
};How Access Is Decided
- No user: denied.
- The user has a listed regular role (
admin): allowed. - Otherwise the conditional roles decide:
- Collections: the
wherefilters which documents the user can read, update or delete. - Fields: the plugin checks whether this document matches the
where, and allows or denies the field.
- Collections: the
Several Roles
Listing several conditional roles allows users who match any of them:
read: role(['admin', 'author', 'teamMember'])To require all conditions, write one role with and. Type the return as Where, since TypeScript can't infer it when the conditions use different fields:
import type { Where } from 'payload';
{
// The user is the author, and the document belongs to a team
name: 'authorInTeam',
where: ({ req }): Where => {
return {
and: [
{
author: {
equals: req.user?.id,
},
},
{
team: {
exists: true,
},
},
],
};
},
}Lookups in Other Collections
where can be async, e.g. to find the stores a user manages:
{
name: 'storeManager',
where: async ({ req }) => {
const stores = await req.payload.find({
collection: 'stores',
where: {
manager: {
equals: req.user?.id,
},
},
depth: 0,
pagination: false,
req,
});
return {
store: {
in: stores.docs.map((store) => store.id),
},
};
},
}If nothing is found, in: [] matches no documents, so access is denied.
Writing a where
- Paths start at the document, e.g.
authorordetails.owner, even when the protected field is inside a group, array or block. - Base it on
req.user. On fields, one answer is shared by all fields on the same document that use the same roles, within a request. - Drafts and locales are handled. The check uses the version and locale being read.
Limits on Fields
On fields, conditional roles always deny, and only regular roles pass, when:
- creating a document, since there's nothing to match yet;
- the field is on a global;
- the same
role()result is used on fields in several collections. Callrole([...])inline for each field instead. The plugin logs a warning when this happens.
Each document with protected fields costs one database lookup per set of conditional roles, per request.