Payload Adsign Plugin

Access Control Setup

Type-safe role-based access control

Setup

  1. Add roles field to Users:
collections/Users/index.ts
import { rolesField } from '@adsign/payload-adsign-plugin';

export const Users: CollectionConfig = {
  fields: [
    rolesField({
      name: 'roles',
      roles: [
        {
          label: 'Admin',
          name: 'admin',
        },
        {
          label: 'Editor',
          name: 'editor',
        },
      ],
    }),
  ],
};
  1. Create access control helper:
access.ts
import { createAccessControl } from '@adsign/payload-adsign-plugin';

export const { role } = createAccessControl({
  collection: 'users',
  rolesField: 'roles',
});
  1. Use in collections:
collections/Articles/index.ts
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

collections/Articles/index.ts
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.

access.ts
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([...]):

collections/Users/index.ts
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:

collections/Articles/index.ts
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

  1. No user: denied.
  2. The user has a listed regular role (admin): allowed.
  3. Otherwise the conditional roles decide:
    • Collections: the where filters 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.

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:

access.ts
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:

access.ts
{
  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. author or details.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. Call role([...]) 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.

On this page