mirror of
https://github.com/payloadcms/payload.git
synced 2026-09-14 20:07:19 +08:00
ef69ab17ae
## Summary Adds a top-level `baseAccess` config with separate `collections` and `globals` access maps. This gives applications and plugins one core-owned policy layer without iterating over resources or hardcoding Payload-managed slugs. ## Motivation This originated from Figma Make's need to enforce a read-only policy for viewer users across every Payload resource. Its plugin currently has to find resources and wrap their access callbacks, which is order-dependent and can miss Payload-managed resources created during sanitization. Plugins can currently apply a shared policy only by finding every Collection and Global and wrapping each access callback. That is order-dependent and can miss resources added by later plugins or by Payload during sanitization, making omissions difficult to detect and potentially security-sensitive. `baseAccess` moves that composition into Payload so the same policy is applied consistently to configured, plugin-provided, and Payload-generated resources. ## Alternatives considered - **Plugin ordering and callback wrapping:** still depends on when resources are registered and cannot reliably cover resources created during sanitization. - **One boolean-only function:** simpler, but cannot express the `Where` constraints supported by existing Collection and Global Access Control. - **A new policies API:** more flexible, but introduces a larger new abstraction when the existing access shapes already describe the required operations. ## Changes - Reuses `CollectionAccess` and `GlobalAccess` directly for `baseAccess.collections` and `baseAccess.globals` - Adds the resource `slug` to all Collection and Global Access Control callback arguments, including `access.admin` - Combines each configured base function with the matching resource access function using AND semantics - Applies the policy to resources created during sanitization, including Payload-generated resources - Resolves the base function from the current request so reused sanitized resources cannot retain another config's policy - Fails closed when either access function returns an invalid falsy result - Preserves `overrideAccess` behavior and existing resource-specific access - Keeps `baseAccess` out of the client config - Rejects query constraints returned by collection base access for create operations, which only support boolean access - Documents the API, composition rules, scope, and Payload 4 migration note --------- Co-authored-by: German Jablonski <GermanJablo@users.noreply.github.com>
112 lines
2.5 KiB
TypeScript
112 lines
2.5 KiB
TypeScript
import type { CollectionAccess } from '../collections/config/types.js'
|
|
import type { Access, AccessArgs, AccessResult } from '../config/types.js'
|
|
import type { GlobalAccess } from '../globals/config/types.js'
|
|
|
|
type CommonArgs = {
|
|
access?: Access
|
|
slug: string
|
|
}
|
|
|
|
type AdminAccess = NonNullable<CollectionAccess['admin']>
|
|
|
|
type Args = (
|
|
| {
|
|
entityType: 'collection'
|
|
operation: Exclude<keyof CollectionAccess, 'admin'>
|
|
}
|
|
| {
|
|
entityType: 'global'
|
|
operation: keyof GlobalAccess
|
|
}
|
|
) &
|
|
CommonArgs
|
|
|
|
const authenticatedAccess: Access = ({ req }) => Boolean(req.user)
|
|
|
|
export const withBaseAccess = (options: Args): Access => {
|
|
const documentAccess = options.access ?? authenticatedAccess
|
|
|
|
return async (args: AccessArgs): Promise<AccessResult> => {
|
|
const accessArgs = {
|
|
...args,
|
|
slug: options.slug,
|
|
}
|
|
const { baseAccess } = args.req.payload.config
|
|
const baseAccessFunction =
|
|
options.entityType === 'collection'
|
|
? baseAccess?.collections?.[options.operation]
|
|
: baseAccess?.globals?.[options.operation]
|
|
|
|
if (!baseAccessFunction) {
|
|
return documentAccess(accessArgs)
|
|
}
|
|
|
|
const baseResult = await baseAccessFunction(accessArgs)
|
|
|
|
if (!baseResult) {
|
|
return false
|
|
}
|
|
|
|
if (
|
|
options.entityType === 'collection' &&
|
|
options.operation === 'create' &&
|
|
typeof baseResult === 'object'
|
|
) {
|
|
throw new Error('baseAccess must return a boolean for collection create operations.')
|
|
}
|
|
|
|
const documentResult = await documentAccess(accessArgs)
|
|
|
|
if (!documentResult) {
|
|
return false
|
|
}
|
|
|
|
if (baseResult === true) {
|
|
return documentResult
|
|
}
|
|
|
|
if (documentResult === true) {
|
|
return baseResult
|
|
}
|
|
|
|
return {
|
|
and: [baseResult, documentResult],
|
|
}
|
|
}
|
|
}
|
|
|
|
export const withBaseAdminAccess = ({
|
|
slug,
|
|
access,
|
|
}: {
|
|
access?: AdminAccess
|
|
slug: string
|
|
}): AdminAccess => {
|
|
const documentAccess =
|
|
access ?? (({ req }) => Boolean(req.user && req.payload.config.admin.user === slug))
|
|
|
|
return async (args) => {
|
|
const accessArgs = {
|
|
...args,
|
|
slug,
|
|
}
|
|
const baseAccessFunction = args.req.payload.config.baseAccess?.collections?.admin
|
|
|
|
if (!baseAccessFunction) {
|
|
return documentAccess(accessArgs)
|
|
}
|
|
|
|
const baseResult = await baseAccessFunction(accessArgs)
|
|
|
|
if (!baseResult) {
|
|
return false
|
|
}
|
|
|
|
if (typeof baseResult === 'object') {
|
|
throw new Error('baseAccess must return a boolean for collection admin operations.')
|
|
}
|
|
|
|
return documentAccess(accessArgs)
|
|
}
|
|
}
|