Introduce a dynamic RBAC system with a new 'roles' collection that grants granular permissions. Add hasPermission/requirePermission/loadUserPermissions utilities and a central permissions registry. Register the Roles collection in payload.config and add roleDocs relationship to Users.
113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
import type { Payload, PayloadRequest } from "payload";
|
|
import { loadUserPermissions } from "@/utils/access-control/loadUserPermissions";
|
|
import type { Permission } from "@/permissions";
|
|
|
|
/**
|
|
* Minimal user shape for permission checks.
|
|
* Accepts the full Payload User or a stripped-down { id } from server actions.
|
|
*/
|
|
interface UserLike {
|
|
id: number | string;
|
|
roleDocs?: unknown;
|
|
}
|
|
|
|
/**
|
|
* Check whether a user has a specific permission.
|
|
*
|
|
* Resolution order:
|
|
* 1. No user → false.
|
|
* 2. User has a role with `isSuperuser: true` → true (bypasses all checks).
|
|
* 3. User has a role whose `permissions` array includes the given permission → true.
|
|
* 4. Otherwise → false.
|
|
*
|
|
* Results are cached per-user for 30s (see `loadUserPermissions`).
|
|
*
|
|
* @example
|
|
* const canCreate = await hasPermission(payload, user, "users:create");
|
|
*/
|
|
export async function hasPermission(
|
|
payload: Payload,
|
|
user: UserLike | null | undefined,
|
|
permission: Permission,
|
|
): Promise<boolean> {
|
|
if (!user) return false;
|
|
|
|
const { permissions, isSuperuser } = await loadUserPermissions(payload, user);
|
|
if (isSuperuser) return true;
|
|
return permissions.has(permission);
|
|
}
|
|
|
|
/**
|
|
* Check whether a user has a superuser role (bypasses all permission checks).
|
|
*
|
|
* Use this for the legacy `isDeveloper` replacement where the check was
|
|
* "can do anything" rather than a specific permission.
|
|
*/
|
|
export async function isSuperuser(
|
|
payload: Payload,
|
|
user: UserLike | null | undefined,
|
|
): Promise<boolean> {
|
|
if (!user) return false;
|
|
const { isSuperuser: su } = await loadUserPermissions(payload, user);
|
|
return su;
|
|
}
|
|
|
|
/**
|
|
* Check whether a user has ANY of the given permissions.
|
|
*/
|
|
export async function hasAnyPermission(
|
|
payload: Payload,
|
|
user: UserLike | null | undefined,
|
|
...permissions: Permission[]
|
|
): Promise<boolean> {
|
|
if (!user) return false;
|
|
const { permissions: userPerms, isSuperuser: su } = await loadUserPermissions(payload, user);
|
|
if (su) return true;
|
|
return permissions.some((p) => userPerms.has(p));
|
|
}
|
|
|
|
/**
|
|
* Check whether a user has ALL of the given permissions.
|
|
*/
|
|
export async function hasAllPermissions(
|
|
payload: Payload,
|
|
user: UserLike | null | undefined,
|
|
...permissions: Permission[]
|
|
): Promise<boolean> {
|
|
if (!user) return false;
|
|
const { permissions: userPerms, isSuperuser: su } = await loadUserPermissions(payload, user);
|
|
if (su) return true;
|
|
return permissions.every((p) => userPerms.has(p));
|
|
}
|
|
|
|
/**
|
|
* Factory that creates a Payload collection access function requiring a specific
|
|
* permission. Drop-in replacement for `isDeveloper` / `isAdmin` in collection
|
|
* `access` blocks.
|
|
*
|
|
* @example
|
|
* access: {
|
|
* create: requirePermission("users:create"),
|
|
* update: requirePermission("users:update"),
|
|
* }
|
|
*/
|
|
export function requirePermission(permission: Permission) {
|
|
return async ({ req }: { req: PayloadRequest }): Promise<boolean> => {
|
|
return hasPermission(req.payload, req.user, permission);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Factory that creates a Payload collection access function requiring ANY of
|
|
* the given permissions.
|
|
*
|
|
* @example
|
|
* access: {
|
|
* update: requireAnyPermission("tickets:update", "tickets:staff"),
|
|
* }
|
|
*/
|
|
export function requireAnyPermission(...permissions: Permission[]) {
|
|
return async ({ req }: { req: PayloadRequest }): Promise<boolean> => {
|
|
return hasAnyPermission(req.payload, req.user, ...permissions);
|
|
};
|
|
}
|