Add configuration for BMad, Claude, OpenCode, and other AI agent tools and workflows.
4.7 KiB
4.7 KiB
Access Control Reference
Overview
Access control functions determine WHO can do WHAT with documents:
type Access = (args: AccessArgs) => boolean | Where | Promise<boolean | Where>
Returns:
true- Full accessfalse- No accessWherequery - Filtered access (row-level security)
Collection-Level Access
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
create: isLoggedIn,
read: isPublishedOrAdmin,
update: isAdminOrAuthor,
delete: isAdmin,
},
fields: [...],
}
Common Patterns
Public Read, Admin Write
const isAdmin: Access = ({ req }) => {
return req.user?.roles?.includes('admin') ?? false
}
const isLoggedIn: Access = ({ req }) => {
return !!req.user
}
access: {
create: isLoggedIn,
read: () => true, // Public
update: isAdmin,
delete: isAdmin,
}
Row-Level Security (User's Own Documents)
const ownDocsOnly: Access = ({ req }) => {
if (!req.user) return false
// Admins see everything
if (req.user.roles?.includes('admin')) return true
// Others see only their own
return {
author: { equals: req.user.id },
}
}
access: {
read: ownDocsOnly,
update: ownDocsOnly,
delete: ownDocsOnly,
}
Complex Queries
const publishedOrOwn: Access = ({ req }) => {
// Not logged in: published only
if (!req.user) {
return { status: { equals: 'published' } }
}
// Admin: see all
if (req.user.roles?.includes('admin')) return true
// Others: published OR own drafts
return {
or: [
{ status: { equals: 'published' } },
{ author: { equals: req.user.id } },
],
}
}
Field-Level Access
Control access to specific fields:
{
name: 'internalNotes',
type: 'textarea',
access: {
read: ({ req }) => req.user?.roles?.includes('admin'),
update: ({ req }) => req.user?.roles?.includes('admin'),
},
}
Hide Field Completely
{
name: 'secretKey',
type: 'text',
access: {
read: () => false, // Never returned in API
update: ({ req }) => req.user?.roles?.includes('admin'),
},
}
Access Control Arguments
type AccessArgs = {
req: PayloadRequest
id?: string | number // Document ID (for update/delete)
data?: Record<string, unknown> // Incoming data (for create/update)
}
RBAC (Role-Based Access Control)
// Define roles
type Role = 'admin' | 'editor' | 'author' | 'subscriber'
// Helper functions
const hasRole = (req: PayloadRequest, role: Role): boolean => {
return req.user?.roles?.includes(role) ?? false
}
const hasAnyRole = (req: PayloadRequest, roles: Role[]): boolean => {
return roles.some(role => hasRole(req, role))
}
// Use in access control
const canEdit: Access = ({ req }) => {
return hasAnyRole(req, ['admin', 'editor'])
}
const canPublish: Access = ({ req }) => {
return hasAnyRole(req, ['admin', 'editor'])
}
const canDelete: Access = ({ req }) => {
return hasRole(req, 'admin')
}
Multi-Tenant Access
// Users belong to organizations
const sameOrgOnly: Access = ({ req }) => {
if (!req.user) return false
// Super admin sees all
if (req.user.roles?.includes('super-admin')) return true
// Others see only their org's data
return {
organization: { equals: req.user.organization },
}
}
// Apply to collection
access: {
create: ({ req }) => !!req.user,
read: sameOrgOnly,
update: sameOrgOnly,
delete: sameOrgOnly,
}
Global Access
For singleton documents:
export const Settings: GlobalConfig = {
slug: 'settings',
access: {
read: () => true,
update: ({ req }) => req.user?.roles?.includes('admin'),
},
fields: [...],
}
Important: Local API Access Control
Local API bypasses access control by default!
// ❌ SECURITY BUG: Access control bypassed
await payload.find({
collection: 'posts',
user: someUser,
})
// ✅ SECURE: Explicitly enforce access control
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: false, // REQUIRED
})
Access Control with req.context
Share state between access checks and hooks:
const conditionalAccess: Access = ({ req }) => {
// Check context set by middleware or previous operation
if (req.context?.bypassAuth) return true
return req.user?.roles?.includes('admin')
}
Best Practices
- Default to restrictive - Start with
false, add permissions - Use query constraints for row-level - More efficient than filtering after
- Keep logic in reusable functions - DRY across collections
- Test with different user types - Admin, regular user, anonymous
- Remember Local API default - Always use
overrideAccess: falsefor user-facing operations - Document your access rules - Complex logic needs comments