226 lines
5.2 KiB
Markdown
226 lines
5.2 KiB
Markdown
---
|
|
title: Access Control
|
|
description: Collection, field, and global access control patterns
|
|
tags: [payload, access-control, security, permissions, rbac]
|
|
---
|
|
|
|
# Payload CMS Access Control
|
|
|
|
## Access Control Layers
|
|
|
|
1. **Collection-Level**: Controls operations on entire documents (create, read, update, delete, admin)
|
|
2. **Field-Level**: Controls access to individual fields (create, read, update)
|
|
3. **Global-Level**: Controls access to global documents (read, update)
|
|
|
|
## Collection Access Control
|
|
|
|
```typescript
|
|
import type { Access } from 'payload'
|
|
|
|
export const Posts: CollectionConfig = {
|
|
slug: 'posts',
|
|
access: {
|
|
// Boolean: Only authenticated users can create
|
|
create: ({ req: { user } }) => Boolean(user),
|
|
|
|
// Query constraint: Public sees published, users see all
|
|
read: ({ req: { user } }) => {
|
|
if (user) return true
|
|
return { status: { equals: 'published' } }
|
|
},
|
|
|
|
// User-specific: Admins or document owner
|
|
update: ({ req: { user }, id }) => {
|
|
if (user?.roles?.includes('admin')) return true
|
|
return { author: { equals: user?.id } }
|
|
},
|
|
|
|
// Async: Check related data
|
|
delete: async ({ req, id }) => {
|
|
const hasComments = await req.payload.count({
|
|
collection: 'comments',
|
|
where: { post: { equals: id } },
|
|
})
|
|
return hasComments === 0
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
## Common Access Patterns
|
|
|
|
```typescript
|
|
// Anyone
|
|
export const anyone: Access = () => true
|
|
|
|
// Authenticated only
|
|
export const authenticated: Access = ({ req: { user } }) => Boolean(user)
|
|
|
|
// Admin only
|
|
export const adminOnly: Access = ({ req: { user } }) => {
|
|
return user?.roles?.includes('admin')
|
|
}
|
|
|
|
// Admin or self
|
|
export const adminOrSelf: Access = ({ req: { user } }) => {
|
|
if (user?.roles?.includes('admin')) return true
|
|
return { id: { equals: user?.id } }
|
|
}
|
|
|
|
// Published or authenticated
|
|
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
|
|
if (user) return true
|
|
return { _status: { equals: 'published' } }
|
|
}
|
|
```
|
|
|
|
## Row-Level Security
|
|
|
|
```typescript
|
|
// Organization-scoped access
|
|
export const organizationScoped: Access = ({ req: { user } }) => {
|
|
if (user?.roles?.includes('admin')) return true
|
|
|
|
// Users see only their organization's data
|
|
return {
|
|
organization: {
|
|
equals: user?.organization,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Team-based access
|
|
export const teamMemberAccess: Access = ({ req: { user } }) => {
|
|
if (!user) return false
|
|
if (user.roles?.includes('admin')) return true
|
|
|
|
return {
|
|
'team.members': {
|
|
contains: user.id,
|
|
},
|
|
}
|
|
}
|
|
```
|
|
|
|
## Field Access Control
|
|
|
|
**Field access ONLY returns boolean** (no query constraints).
|
|
|
|
```typescript
|
|
{
|
|
name: 'salary',
|
|
type: 'number',
|
|
access: {
|
|
read: ({ req: { user }, doc }) => {
|
|
// Self can read own salary
|
|
if (user?.id === doc?.id) return true
|
|
// Admin can read all
|
|
return user?.roles?.includes('admin')
|
|
},
|
|
update: ({ req: { user } }) => {
|
|
// Only admins can update
|
|
return user?.roles?.includes('admin')
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
## RBAC Pattern
|
|
|
|
Payload does NOT provide a roles system by default. Add a `roles` field to your auth collection:
|
|
|
|
```typescript
|
|
export const Users: CollectionConfig = {
|
|
slug: 'users',
|
|
auth: true,
|
|
fields: [
|
|
{
|
|
name: 'roles',
|
|
type: 'select',
|
|
hasMany: true,
|
|
options: ['admin', 'editor', 'user'],
|
|
defaultValue: ['user'],
|
|
required: true,
|
|
saveToJWT: true, // Include in JWT for fast access checks
|
|
access: {
|
|
update: ({ req: { user } }) => user?.roles?.includes('admin'),
|
|
},
|
|
},
|
|
],
|
|
}
|
|
```
|
|
|
|
## Multi-Tenant Access Control
|
|
|
|
```typescript
|
|
interface User {
|
|
id: string
|
|
tenantId: string
|
|
roles?: string[]
|
|
}
|
|
|
|
const tenantAccess: Access = ({ req: { user } }) => {
|
|
if (!user) return false
|
|
if (user.roles?.includes('super-admin')) return true
|
|
|
|
return {
|
|
tenant: {
|
|
equals: (user as User).tenantId,
|
|
},
|
|
}
|
|
}
|
|
|
|
export const Posts: CollectionConfig = {
|
|
slug: 'posts',
|
|
access: {
|
|
create: tenantAccess,
|
|
read: tenantAccess,
|
|
update: tenantAccess,
|
|
delete: tenantAccess,
|
|
},
|
|
fields: [
|
|
{
|
|
name: 'tenant',
|
|
type: 'text',
|
|
required: true,
|
|
access: {
|
|
update: ({ req: { user } }) => user?.roles?.includes('super-admin'),
|
|
},
|
|
hooks: {
|
|
beforeChange: [
|
|
({ req, operation, value }) => {
|
|
if (operation === 'create' && !value) {
|
|
return (req.user as User)?.tenantId
|
|
}
|
|
return value
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
}
|
|
```
|
|
|
|
## Important Notes
|
|
|
|
1. **Local API Default**: Access control is **skipped by default** in Local API (`overrideAccess: true`). When passing a `user` parameter, you must set `overrideAccess: false`:
|
|
|
|
```typescript
|
|
// ❌ WRONG: Passes user but bypasses access control
|
|
await payload.find({
|
|
collection: 'posts',
|
|
user: someUser,
|
|
})
|
|
|
|
// ✅ CORRECT: Respects the user's permissions
|
|
await payload.find({
|
|
collection: 'posts',
|
|
user: someUser,
|
|
overrideAccess: false, // Required to enforce access control
|
|
})
|
|
```
|
|
|
|
2. **Field Access Limitations**: Field-level access does NOT support query constraints - only boolean returns.
|
|
|
|
3. **Admin Panel Visibility**: The `admin` access control determines if a collection appears in the admin panel for a user.
|