Skip to content
Draft
439 changes: 439 additions & 0 deletions docs/superpowers/plans/2026-06-18-field-access-collection-slug.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion packages/payload/src/collections/operations/findDistinct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ export const findDistinctOperation = async (
}

if (fieldResult.field.access?.read) {
const hasAccess = await fieldResult.field.access.read({ req })
const hasAccess = await fieldResult.field.access.read({
collection: collectionConfig,
global: null,
req,
})
if (!hasAccess) {
throw new Forbidden(req.t)
}
Expand Down
4 changes: 4 additions & 0 deletions packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ export type FieldAccessArgs<TData extends TypeWithID = any, TSiblingData = any>
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
blockData?: JsonObject | undefined
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
collection?: null | SanitizedCollectionConfig
/**
* The incoming, top-level document data used to `create` or `update` the document with.
*/
Expand All @@ -247,6 +249,8 @@ export type FieldAccessArgs<TData extends TypeWithID = any, TSiblingData = any>
* The original data of the document before the `update` is applied. `doc` is undefined during the `create` operation.
*/
doc?: TData
/** The global which the field belongs to. If the field belongs to a collection, this will be null. */
global?: null | SanitizedGlobalConfig
/**
* The `id` of the current document being read or updated. `id` is undefined during the `create` operation.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/fields/hooks/afterRead/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,10 @@ export const promise = async ({
: await field.access.read({
id: doc.id as number | string,
blockData,
collection,
data: doc,
doc,
global,
req,
siblingData: siblingDoc,
})
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/fields/hooks/beforeValidate/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,10 @@ export const promise = async <T>({
: await field.access[operation]({
id,
blockData,
collection,
data: data as Partial<T>,
doc,
global,
req,
siblingData,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,10 @@ export async function getEntityPermissions<TEntityType extends 'collection' | 'g

populateFieldPermissions({
blockReferencesPermissions,
collection: entityType === 'collection' ? (entity as SanitizedCollectionConfig) : null,
data,
fields: entity.fields,
global: entityType === 'global' ? (entity as SanitizedGlobalConfig) : null,
operations,
parentPermissionsObject: entityPermissions,
permissionsObject: fieldsPermissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
GlobalPermission,
Permission,
} from '../../auth/types.js'
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
import type { DefaultDocumentIDType } from '../../index.js'
import type { AllOperations, JsonObject, PayloadRequest } from '../../types/index.js'
import type { BlockReferencesPermissions } from './getEntityPermissions.js'
Expand Down Expand Up @@ -52,17 +54,23 @@ const setPermission = (
export const populateFieldPermissions = ({
id,
blockReferencesPermissions,
collection,
data,
fields,
global,
operations,
parentPermissionsObject,
permissionsObject,
promises,
req,
}: {
blockReferencesPermissions: BlockReferencesPermissions
/** The collection which the fields belong to. If the fields belong to a global, this will be null. */
collection: null | SanitizedCollectionConfig
data: JsonObject | undefined
fields: Field[]
/** The global which the fields belong to. If the fields belong to a collection, this will be null. */
global: null | SanitizedGlobalConfig
id?: DefaultDocumentIDType
/**
* Operations to check access for
Expand Down Expand Up @@ -94,8 +102,10 @@ export const populateFieldPermissions = ({
if ('access' in field && field.access && typeof field.access[operation] === 'function') {
const accessResult = field.access[operation]({
id,
collection,
data,
doc: data,
global,
req,
// We cannot include siblingData or blockData here, as we do not have siblingData/blockData available once we reach block or array
// rows, as we're calculating schema permissions, which do not include individual rows.
Expand Down Expand Up @@ -128,8 +138,10 @@ export const populateFieldPermissions = ({
populateFieldPermissions({
id,
blockReferencesPermissions,
collection,
data,
fields: field.fields,
global,
operations,
parentPermissionsObject: fieldPermissions,
permissionsObject: fieldPermissions.fields,
Expand Down Expand Up @@ -221,8 +233,10 @@ export const populateFieldPermissions = ({
populateFieldPermissions({
id,
blockReferencesPermissions,
collection,
data,
fields: block.fields,
global,
operations,
parentPermissionsObject: blockPermission,
permissionsObject: blockPermission.fields,
Expand All @@ -238,8 +252,10 @@ export const populateFieldPermissions = ({
// Field does not have a name => same parentPermissionsObject
populateFieldPermissions({
id,
collection,
data,
fields: field.fields,
global,
operations,
// Field does not have a name here => use parent permissions object
blockReferencesPermissions,
Expand Down Expand Up @@ -289,8 +305,10 @@ export const populateFieldPermissions = ({
populateFieldPermissions({
id,
blockReferencesPermissions,
collection,
data,
fields: tab.fields,
global,
operations,
parentPermissionsObject: tabPermissions,
permissionsObject: tabPermissions.fields,
Expand All @@ -301,8 +319,10 @@ export const populateFieldPermissions = ({
// Tab does not have a name => same parentPermissionsObject
populateFieldPermissions({
id,
collection,
data,
fields: tab.fields,
global,
operations,
// Tab does not have a name here => use parent permissions object
blockReferencesPermissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type AddFieldStatePromiseArgs = {
*/
forceFullValue?: boolean
fullData: Data
globalSlug?: string
id: number | string
/**
* Whether the field schema should be included in the state
Expand Down Expand Up @@ -124,6 +125,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
filter,
forceFullValue = false,
fullData,
globalSlug,
includeSchema = false,
indexPath,
mockRSCs,
Expand Down Expand Up @@ -213,7 +215,13 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
hasPermission = await field.access.read({
id,
blockData,
collection: collectionSlug
? (req.payload.collections[collectionSlug]?.config ?? null)
: null,
data: fullData,
global: globalSlug
? (req.payload.globals.config.find((g) => g.slug === globalSlug) ?? null)
: null,
req,
siblingData: data,
})
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/forms/fieldSchemasToFormState/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type Args = {
* then the field schema map is not required.
*/
fieldSchemaMap: FieldSchemaMap | undefined
globalSlug?: string
id?: number | string
/**
* Validation, filterOptions and read access control will receive the `blockData`, which is the data of the nearest parent block. You can pass in
Expand Down Expand Up @@ -88,6 +89,7 @@ export const fieldSchemasToFormState = async ({
documentData,
fields,
fieldSchemaMap,
globalSlug,
initialBlockData,
mockRSCs,
operation,
Expand Down Expand Up @@ -145,6 +147,7 @@ export const fieldSchemasToFormState = async ({
fields,
fieldSchemaMap,
fullData,
globalSlug,
mockRSCs,
operation,
parentIndexPath: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Args = {
*/
forceFullValue?: boolean
fullData: Data
globalSlug?: string
id?: number | string
/**
* Whether the field schema should be included in the state. @default false
Expand Down Expand Up @@ -97,6 +98,7 @@ export const iterateFields = async ({
filter,
forceFullValue = false,
fullData,
globalSlug,
includeSchema = false,
mockRSCs,
omitParents = false,
Expand Down Expand Up @@ -185,6 +187,7 @@ export const iterateFields = async ({
filter,
forceFullValue,
fullData,
globalSlug,
includeSchema,
indexPath,
mockRSCs,
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/utilities/buildFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export const buildFormState = async (
documentData,
fields,
fieldSchemaMap: schemaMap,
globalSlug,
initialBlockData: blockData,
mockRSCs,
operation,
Expand Down
Loading
Loading