Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
05eaa5f
improve test
AlessioGr Nov 14, 2025
5da84e5
improvements
AlessioGr Nov 15, 2025
2976c2e
perf: rewrite getEntityPolicies (permissions object calculation)
AlessioGr Nov 15, 2025
005dd57
use new version
AlessioGr Nov 16, 2025
867d1e4
fix
AlessioGr Nov 16, 2025
845f7b8
debug
AlessioGr Nov 16, 2025
4e3a8e3
fix parallelism
AlessioGr Nov 17, 2025
26ea932
fix the issue
AlessioGr Nov 17, 2025
521b027
delete old function
AlessioGr Nov 17, 2025
b5fd533
Revert "delete old function"
AlessioGr Nov 17, 2025
3f4ae60
fix issue
AlessioGr Nov 17, 2025
d840dde
fix test
AlessioGr Nov 17, 2025
9214f74
fix unlock operation permission calculation
AlessioGr Nov 17, 2025
30331f1
fix faulty test that broke only on postgres
AlessioGr Nov 17, 2025
9cf703c
fix issue
AlessioGr Nov 17, 2025
c3eebd0
fix
AlessioGr Nov 17, 2025
7309114
Merge remote-tracking branch 'origin/main' into fix/update-field-acce…
AlessioGr Nov 17, 2025
fb7674f
fix(db-mongodb): do not return expired sessions
AlessioGr Nov 17, 2025
db077be
fix transaction issues
AlessioGr Nov 17, 2025
584c66b
fix(db-mongodb): do not return expired sessions
AlessioGr Nov 17, 2025
ea90e08
fix transaction issues
AlessioGr Nov 17, 2025
951cc8a
simplify
AlessioGr Nov 17, 2025
6245e47
cleanup
AlessioGr Nov 17, 2025
dd4cb5e
Revert "fix transaction issues"
AlessioGr Nov 18, 2025
d10dfb2
Revert "fix(db-mongodb): do not return expired sessions"
AlessioGr Nov 18, 2025
ee4c8e0
Merge remote-tracking branch 'origin/fix/transaction-issues' into fix…
AlessioGr Nov 18, 2025
744ca19
fix: transaction IDs shared between operations that are supposed to b…
AlessioGr Nov 18, 2025
28d7bc7
Merge remote-tracking branch 'origin/fix/transaction-issues' into fix…
AlessioGr Nov 18, 2025
015591a
fix: move session find as close to db call as possible
AlessioGr Nov 18, 2025
292aa60
more transaction issue fixes
AlessioGr Nov 18, 2025
4b612bc
Merge remote-tracking branch 'origin/fix/transaction-issues' into fix…
AlessioGr Nov 18, 2025
c8ba805
perf: full parallelism
AlessioGr Nov 18, 2025
91f0039
simplify a lot
AlessioGr Nov 18, 2025
bef5367
Revert "Merge remote-tracking branch 'origin/fix/transaction-issues' …
AlessioGr Nov 18, 2025
64a62b4
Revert "Merge remote-tracking branch 'origin/fix/transaction-issues' …
AlessioGr Nov 18, 2025
a68e56c
Revert "Merge remote-tracking branch 'origin/fix/transaction-issues' …
AlessioGr Nov 18, 2025
1c2b764
Merge remote-tracking branch 'origin/main' into fix/update-field-acce…
AlessioGr Nov 18, 2025
ea3cb08
perf: cache access control where query db calls
AlessioGr Nov 19, 2025
fe4ea34
Merge remote-tracking branch 'origin/main' into fix/update-field-acce…
AlessioGr Nov 19, 2025
bd01d9a
chore: skip faulty test
AlessioGr Nov 19, 2025
ba28f99
test: access control tests for db call caching
AlessioGr Nov 19, 2025
89375f6
add another test
AlessioGr Nov 19, 2025
9395c3a
more accurate comments
AlessioGr Nov 19, 2025
f8eef3d
fix: some field permission promises depending on parent promises were…
AlessioGr Nov 19, 2025
8ef54c7
test: add test ensuring fields-parent permission inheritance works
AlessioGr Nov 19, 2025
d8c3085
delete getEntityPolicies
AlessioGr Nov 19, 2025
43e9985
fix: global access control where
AlessioGr Nov 19, 2025
4f143c8
payload/internal export
AlessioGr Nov 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/access-control/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ To accomplish this, Payload exposes the [Access Operation](../authentication/ope

<Banner type="warning">
**Important:** When your access control functions are executed via the [Access
Operation](../authentication/operations#access), the `id` and `data` arguments
will be `undefined`. This is because Payload is executing your functions
without referencing a specific Document.
Operation](../authentication/operations#access), the `id`, `data`, `siblingData`, `blockData` and `doc` arguments
will be `undefined`. Additionally, `Where` queries returned from access control functions will not be run - we'll assume the user does not have access instead.

This is because Payload is executing your functions without referencing a specific Document.

</Banner>

If you use `id` or `data` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your Access Control is being executed via the Access Operation to determine solely what the user can do within the Admin Panel.
If you use `id`, `data`, `siblingData`, `blockData` and `doc` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your Access Control is being executed via the Access Operation to determine solely what the user can do within the Admin Panel.

## Locale Specific Access Control

Expand Down
49 changes: 24 additions & 25 deletions packages/next/src/views/Document/getDocumentPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const getDocumentPermissions = async (args: {
collectionConfig?: SanitizedCollectionConfig
data: Data
globalConfig?: SanitizedGlobalConfig
/**
* When called for creating a new document, id is not provided.
*/
id?: number | string
req: PayloadRequest
}): Promise<{
Expand All @@ -35,29 +38,27 @@ export const getDocumentPermissions = async (args: {
collection: {
config: collectionConfig,
},
req: {
...req,
data: {
...data,
_status: 'draft',
},
data: {
...data,
_status: 'draft',
},
req,
})

if (collectionConfig.versions?.drafts) {
hasPublishPermission = await docAccessOperation({
id,
collection: {
config: collectionConfig,
},
req: {
...req,
hasPublishPermission = (
await docAccessOperation({
id,
collection: {
config: collectionConfig,
},
data: {
...data,
_status: 'published',
},
},
}).then((permissions) => permissions.update)
req,
})
).update
}
} catch (err) {
logError({ err, payload: req.payload })
Expand All @@ -67,24 +68,22 @@ export const getDocumentPermissions = async (args: {
if (globalConfig) {
try {
docPermissions = await docAccessOperationGlobal({
data,
globalConfig,
req: {
...req,
data,
},
req,
})

if (globalConfig.versions?.drafts) {
hasPublishPermission = await docAccessOperationGlobal({
globalConfig,
req: {
...req,
hasPublishPermission = (
await docAccessOperationGlobal({
data: {
...data,
_status: 'published',
},
},
}).then((permissions) => permissions.update)
globalConfig,
req,
})
).update
}
} catch (err) {
logError({ err, payload: req.payload })
Expand Down
10 changes: 10 additions & 0 deletions packages/payload/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./internal": {
"import": "./src/exports/internal.ts",
"types": "./src/exports/internal.ts",
"default": "./src/exports/internal.ts"
},
"./shared": {
"import": "./src/exports/shared.ts",
"types": "./src/exports/shared.ts",
Expand Down Expand Up @@ -150,6 +155,11 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./internal": {
"import": "./dist/exports/internal.js",
"types": "./dist/exports/internal.d.ts",
"default": "./dist/exports/internal.js"
},
"./node": {
"import": "./dist/exports/node.js",
"types": "./dist/exports/node.d.ts",
Expand Down
22 changes: 12 additions & 10 deletions packages/payload/src/auth/getAccessResults.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AllOperations, PayloadRequest } from '../types/index.js'
import type { Permissions, SanitizedPermissions } from './types.js'

import { getEntityPolicies } from '../utilities/getEntityPolicies.js'
import { getEntityPermissions } from '../utilities/getEntityPermissions/getEntityPermissions.js'
import { sanitizePermissions } from '../utilities/sanitizePermissions.js'

type GetAccessResultsArgs = {
Expand All @@ -27,7 +27,7 @@ export async function getAccessResults({
} else {
results.canAccessAdmin = false
}
const blockPolicies = {}
const blockReferencesPermissions = {}

await Promise.all(
payload.config.collections.map(async (collection) => {
Expand All @@ -45,14 +45,15 @@ export async function getAccessResults({
collectionOperations.push('readVersions')
}

const collectionPolicy = await getEntityPolicies({
type: 'collection',
blockPolicies,
const collectionPermissions = await getEntityPermissions({
blockReferencesPermissions,
entity: collection,
entityType: 'collection',
fetchData: false,
operations: collectionOperations,
req,
})
results.collections![collection.slug] = collectionPolicy
results.collections![collection.slug] = collectionPermissions
}),
)

Expand All @@ -64,14 +65,15 @@ export async function getAccessResults({
globalOperations.push('readVersions')
}

const globalPolicy = await getEntityPolicies({
type: 'global',
blockPolicies,
const globalPermissions = await getEntityPermissions({
blockReferencesPermissions,
entity: global,
entityType: 'global',
fetchData: false,
operations: globalOperations,
req,
})
results.globals![global.slug] = globalPolicy
results.globals![global.slug] = globalPermissions
}),
)

Expand Down
30 changes: 16 additions & 14 deletions packages/payload/src/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ export type SanitizedBlockPermissions =
}
| true

export type BlocksPermissions =
| {
[blockSlug: string]: BlockPermissions
}
| true
export type BlocksPermissions = {
[blockSlug: string]: BlockPermissions
}

export type SanitizedBlocksPermissions =
| {
Expand All @@ -42,10 +40,10 @@ export type SanitizedBlocksPermissions =

export type FieldPermissions = {
blocks?: BlocksPermissions
create: Permission
create?: Permission
fields?: FieldsPermissions
read: Permission
update: Permission
read?: Permission
update?: Permission
}

export type SanitizedFieldPermissions =
Expand All @@ -65,12 +63,14 @@ export type SanitizedFieldsPermissions =
| true

export type CollectionPermission = {
create: Permission
delete: Permission
create?: Permission
delete?: Permission
fields: FieldsPermissions
read: Permission
read?: Permission
readVersions?: Permission
update: Permission
// Auth-enabled Collections only
unlock?: Permission
update?: Permission
}

export type SanitizedCollectionPermission = {
Expand All @@ -79,14 +79,16 @@ export type SanitizedCollectionPermission = {
fields: SanitizedFieldsPermissions
read?: true
readVersions?: true
// Auth-enabled Collections only
unlock?: true
update?: true
}

export type GlobalPermission = {
fields: FieldsPermissions
read: Permission
read?: Permission
readVersions?: Permission
update: Permission
update?: Permission
}

export type SanitizedGlobalPermission = {
Expand Down
24 changes: 17 additions & 7 deletions packages/payload/src/collections/operations/docAccess.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import type { SanitizedCollectionPermission } from '../../auth/index.js'
import type { AllOperations, PayloadRequest } from '../../types/index.js'
import type { AllOperations, JsonObject, PayloadRequest } from '../../types/index.js'
import type { Collection } from '../config/types.js'

import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
import { getEntityPermissions } from '../../utilities/getEntityPermissions/getEntityPermissions.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizePermissions } from '../../utilities/sanitizePermissions.js'

const allOperations: AllOperations[] = ['create', 'read', 'update', 'delete']

type Arguments = {
collection: Collection
id: number | string
/**
* If the document data is passed, it will be used to check access instead of fetching the document from the database.
*/
data?: JsonObject
/**
* When called for creating a new document, id is not provided.
*/
id?: number | string
req: PayloadRequest
}

export async function docAccessOperation(args: Arguments): Promise<SanitizedCollectionPermission> {
const {
id,
collection: { config },
data,
req,
} = args

Expand All @@ -36,11 +44,13 @@ export async function docAccessOperation(args: Arguments): Promise<SanitizedColl
}

try {
const result = await getEntityPolicies({
id,
type: 'collection',
blockPolicies: {},
const result = await getEntityPermissions({
id: id!,
blockReferencesPermissions: {},
data,
entity: config,
entityType: 'collection',
fetchData: id ? true : (false as true),
operations: collectionOperations,
req,
})
Expand Down
3 changes: 2 additions & 1 deletion packages/payload/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type { RootFoldersConfiguration } from '../folders/types.js'
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
import type {
Block,
DefaultDocumentIDType,
FlattenedBlock,
JobsConfig,
KVAdapterResult,
Expand Down Expand Up @@ -314,7 +315,7 @@ export type AccessArgs<TData = any> = {
*/
data?: TData
/** ID of the resource being accessed */
id?: number | string
id?: DefaultDocumentIDType
/** If true, the request is for a static file */
isReadingStaticFile?: boolean
/** The original request that requires an access check */
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/database/queryValidation/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CollectionPermission, GlobalPermission } from '../../auth/index.js'
import type { FlattenedField } from '../../fields/config/types.js'

// TODO: Rename to EntityPermissions in 4.0
export type EntityPolicies = {
collections?: {
[collectionSlug: string]: CollectionPermission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { validateSearchParam } from './validateSearchParams.js'
type Args = {
errors?: { path: string }[]
overrideAccess: boolean
// TODO: Rename to permissions or entityPermissions in 4.0
policies?: EntityPolicies
polymorphicJoin?: boolean
req: PayloadRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { PayloadRequest, WhereField } from '../../types/index.js'
import type { EntityPolicies, PathToQuery } from './types.js'

import { fieldAffectsData } from '../../fields/config/types.js'
import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
import { getEntityPermissions } from '../../utilities/getEntityPermissions/getEntityPermissions.js'
import { isolateObjectProperty } from '../../utilities/isolateObjectProperty.js'
import { getLocalizedPaths } from '../getLocalizedPaths.js'
import { validateQueryPaths } from './validateQueryPaths.js'
Expand All @@ -20,6 +20,7 @@ type Args = {
overrideAccess: boolean
parentIsLocalized?: boolean
path: string
// TODO: Rename to permissions or entityPermissions in 4.0
policies: EntityPolicies
polymorphicJoin?: boolean
req: PayloadRequest
Expand Down Expand Up @@ -56,13 +57,14 @@ export async function validateSearchParam({
let paths: PathToQuery[] = []
const { slug } = (collectionConfig || globalConfig)!

const blockPolicies = {}
const blockReferencesPermissions = {}

if (globalConfig && !policies.globals![slug]) {
policies.globals![slug] = await getEntityPolicies({
type: 'global',
blockPolicies,
policies.globals![slug] = await getEntityPermissions({
blockReferencesPermissions,
entity: globalConfig,
entityType: 'global',
fetchData: false,
operations: ['read'],
req,
})
Expand Down Expand Up @@ -123,10 +125,11 @@ export async function validateSearchParam({
if (!overrideAccess && fieldAffectsData(field)) {
if (collectionSlug) {
if (!policies.collections![collectionSlug]) {
policies.collections![collectionSlug] = await getEntityPolicies({
type: 'collection',
blockPolicies,
policies.collections![collectionSlug] = await getEntityPermissions({
blockReferencesPermissions,
entity: req.payload.collections[collectionSlug]!.config,
entityType: 'collection',
fetchData: false,
operations: ['read'],
req: isolateObjectProperty(req, 'transactionID'),
})
Expand Down
6 changes: 6 additions & 0 deletions packages/payload/src/exports/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Modules exported here are not part of the public API and are subject to change without notice and without a major version bump.
*/

export { getEntityPermissions } from '../utilities/getEntityPermissions/getEntityPermissions.js'
export { sanitizePermissions } from '../utilities/sanitizePermissions.js'
Loading
Loading