fix: versions not loading properly (#11256)

### What?
The admin panel was not respecting where constraints returned from the
readAccess function.

### Why?
`getEntityPolicies` was always using `find` when looping over the
operations, but `readVersions` should be using `findVersions`.

### How?
When the operation is `readVersions` run the `findVersions` operation.

Fixes https://github.com/payloadcms/payload/issues/11240
This commit is contained in:
Jarrod Flesch
2025-02-19 10:22:31 -05:00
committed by GitHub
parent 618624e110
commit 0651ae0727
9 changed files with 186 additions and 133 deletions

View File

@@ -1,12 +1,13 @@
import type {
Payload,
SanitizedCollectionConfig,
SanitizedDocumentPermissions,
SanitizedGlobalConfig,
TypedUser,
} from 'payload'
import { sanitizeID } from '@payloadcms/ui/shared' import { sanitizeID } from '@payloadcms/ui/shared'
import {
combineQueries,
extractAccessFromPermission,
type Payload,
type SanitizedCollectionConfig,
type SanitizedDocumentPermissions,
type SanitizedGlobalConfig,
type TypedUser,
} from 'payload'
type Args = { type Args = {
collectionConfig?: SanitizedCollectionConfig collectionConfig?: SanitizedCollectionConfig
@@ -134,7 +135,8 @@ export const getVersions = async ({
autosave: true, autosave: true,
}, },
user, user,
where: { where: combineQueries(
{
and: [ and: [
{ {
parent: { parent: {
@@ -143,6 +145,8 @@ export const getVersions = async ({
}, },
], ],
}, },
extractAccessFromPermission(docPermissions.readVersions),
),
}) })
if ( if (
@@ -158,7 +162,8 @@ export const getVersions = async ({
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({ ;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
collection: collectionConfig.slug, collection: collectionConfig.slug,
user, user,
where: { where: combineQueries(
{
and: [ and: [
{ {
parent: { parent: {
@@ -177,6 +182,8 @@ export const getVersions = async ({
}, },
], ],
}, },
extractAccessFromPermission(docPermissions.readVersions),
),
})) }))
} }
} }
@@ -185,7 +192,8 @@ export const getVersions = async ({
collection: collectionConfig.slug, collection: collectionConfig.slug,
depth: 0, depth: 0,
user, user,
where: { where: combineQueries(
{
and: [ and: [
{ {
parent: { parent: {
@@ -194,6 +202,8 @@ export const getVersions = async ({
}, },
], ],
}, },
extractAccessFromPermission(docPermissions.readVersions),
),
})) }))
} }
@@ -242,7 +252,8 @@ export const getVersions = async ({
depth: 0, depth: 0,
global: globalConfig.slug, global: globalConfig.slug,
user, user,
where: { where: combineQueries(
{
and: [ and: [
{ {
'version._status': { 'version._status': {
@@ -256,6 +267,8 @@ export const getVersions = async ({
}, },
], ],
}, },
extractAccessFromPermission(docPermissions.readVersions),
),
})) }))
} }
} }

View File

@@ -0,0 +1,17 @@
import type { AccessResult } from '../config/types.js'
import type { Permission } from './index.js'
export const extractAccessFromPermission = (hasPermission: boolean | Permission): AccessResult => {
if (typeof hasPermission === 'boolean') {
return hasPermission
}
const { permission, where } = hasPermission
if (!permission) {
return false
}
if (where && typeof where === 'object') {
return where
}
return true
}

View File

@@ -968,6 +968,7 @@ interface RequestContext {
export interface DatabaseAdapter extends BaseDatabaseAdapter {} export interface DatabaseAdapter extends BaseDatabaseAdapter {}
export type { Payload, RequestContext } export type { Payload, RequestContext }
export { executeAuthStrategies } from './auth/executeAuthStrategies.js' export { executeAuthStrategies } from './auth/executeAuthStrategies.js'
export { extractAccessFromPermission } from './auth/extractAccessFromPermission.js'
export { getAccessResults } from './auth/getAccessResults.js' export { getAccessResults } from './auth/getAccessResults.js'
export { getFieldsToSign } from './auth/getFieldsToSign.js' export { getFieldsToSign } from './auth/getFieldsToSign.js'
export * from './auth/index.js' export * from './auth/index.js'
@@ -984,6 +985,7 @@ export { resetPasswordOperation } from './auth/operations/resetPassword.js'
export { unlockOperation } from './auth/operations/unlock.js' export { unlockOperation } from './auth/operations/unlock.js'
export { verifyEmailOperation } from './auth/operations/verifyEmail.js' export { verifyEmailOperation } from './auth/operations/verifyEmail.js'
export { JWTAuthentication } from './auth/strategies/jwt.js' export { JWTAuthentication } from './auth/strategies/jwt.js'
export type { export type {
AuthStrategyFunction, AuthStrategyFunction,
AuthStrategyFunctionArgs, AuthStrategyFunctionArgs,
@@ -1004,8 +1006,8 @@ export type {
} from './auth/types.js' } from './auth/types.js'
export { generateImportMap } from './bin/generateImportMap/index.js' export { generateImportMap } from './bin/generateImportMap/index.js'
export type { ImportMap } from './bin/generateImportMap/index.js' export type { ImportMap } from './bin/generateImportMap/index.js'
export { genImportMapIterateFields } from './bin/generateImportMap/iterateFields.js' export { genImportMapIterateFields } from './bin/generateImportMap/iterateFields.js'
export { export {
@@ -1052,7 +1054,6 @@ export type {
TypeWithID, TypeWithID,
TypeWithTimestamps, TypeWithTimestamps,
} from './collections/config/types.js' } from './collections/config/types.js'
export { createDataloaderCacheKey, getDataLoader } from './collections/dataloader.js' export { createDataloaderCacheKey, getDataLoader } from './collections/dataloader.js'
export { countOperation } from './collections/operations/count.js' export { countOperation } from './collections/operations/count.js'
export { createOperation } from './collections/operations/create.js' export { createOperation } from './collections/operations/create.js'
@@ -1075,8 +1076,8 @@ export {
serverOnlyConfigProperties, serverOnlyConfigProperties,
type UnsanitizedClientConfig, type UnsanitizedClientConfig,
} from './config/client.js' } from './config/client.js'
export { defaults } from './config/defaults.js'
export { defaults } from './config/defaults.js'
export { sanitizeConfig } from './config/sanitize.js' export { sanitizeConfig } from './config/sanitize.js'
export type * from './config/types.js' export type * from './config/types.js'
export { combineQueries } from './database/combineQueries.js' export { combineQueries } from './database/combineQueries.js'

View File

@@ -48,7 +48,10 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
let docBeingAccessed: EntityDoc | Promise<EntityDoc | undefined> | undefined let docBeingAccessed: EntityDoc | Promise<EntityDoc | undefined> | undefined
async function getEntityDoc({ where }: { where?: Where } = {}): Promise<EntityDoc | undefined> { async function getEntityDoc({
operation,
where,
}: { operation?: AllOperations; where?: Where } = {}): Promise<EntityDoc | undefined> {
if (!entity.slug) { if (!entity.slug) {
return undefined return undefined
} }
@@ -66,18 +69,28 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
if (type === 'collection' && id) { if (type === 'collection' && id) {
if (typeof where === 'object') { if (typeof where === 'object') {
const paginatedRes = await payload.find({ const options = {
collection: entity.slug, collection: entity.slug,
depth: 0, depth: 0,
fallbackLocale: null, fallbackLocale: null,
limit: 1, limit: 1,
locale, locale,
overrideAccess: true, overrideAccess: true,
pagination: false,
req, req,
}
if (operation === 'readVersions') {
const paginatedRes = await payload.findVersions({
...options,
where: combineQueries(where, { parent: { equals: id } }),
})
return paginatedRes?.docs?.[0] || undefined
}
const paginatedRes = await payload.find({
...options,
pagination: false,
where: combineQueries(where, { id: { equals: id } }), where: combineQueries(where, { id: { equals: id } }),
}) })
return paginatedRes?.docs?.[0] || undefined return paginatedRes?.docs?.[0] || undefined
} }
@@ -116,7 +129,9 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
if (typeof accessResult === 'object' && !disableWhere) { if (typeof accessResult === 'object' && !disableWhere) {
mutablePolicies[operation] = { mutablePolicies[operation] = {
permission: permission:
id || type === 'global' ? !!(await getEntityDoc({ where: accessResult })) : true, id || type === 'global'
? !!(await getEntityDoc({ operation, where: accessResult }))
: true,
where: accessResult, where: accessResult,
} }
} else if (mutablePolicies[operation]?.permission !== false) { } else if (mutablePolicies[operation]?.permission !== false) {

View File

@@ -1,4 +1,4 @@
import type { Access, CollectionConfig } from 'payload' import type { CollectionConfig } from 'payload'
import type { MultiTenantPluginConfig } from '../types.js' import type { MultiTenantPluginConfig } from '../types.js'
@@ -34,21 +34,14 @@ export const addCollectionAccess = <ConfigType>({
fieldName, fieldName,
userHasAccessToAllTenants, userHasAccessToAllTenants,
}: Args<ConfigType>): void => { }: Args<ConfigType>): void => {
if (!collection?.access) { collectionAccessKeys.forEach((key) => {
collection.access = {}
}
collectionAccessKeys.reduce<{
[key in (typeof collectionAccessKeys)[number]]?: Access
}>((acc, key) => {
if (!collection.access) { if (!collection.access) {
return acc collection.access = {}
} }
collection.access[key] = withTenantAccess<ConfigType>({ collection.access[key] = withTenantAccess<ConfigType>({
accessFunction: collection.access?.[key], accessFunction: collection.access?.[key],
fieldName, fieldName: key === 'readVersions' ? `version.${fieldName}` : fieldName,
userHasAccessToAllTenants, userHasAccessToAllTenants,
}) })
})
return acc
}, {})
} }

View File

@@ -22,14 +22,13 @@ import {
hiddenAccessSlug, hiddenAccessSlug,
hiddenFieldsSlug, hiddenFieldsSlug,
nonAdminEmail, nonAdminEmail,
nonAdminUserEmail,
nonAdminUserSlug,
publicUserEmail, publicUserEmail,
publicUsersSlug, publicUsersSlug,
readNotUpdateGlobalSlug, readNotUpdateGlobalSlug,
readOnlyGlobalSlug, readOnlyGlobalSlug,
readOnlySlug, readOnlySlug,
relyOnRequestHeadersSlug, relyOnRequestHeadersSlug,
restrictedVersionsAdminPanelSlug,
restrictedVersionsSlug, restrictedVersionsSlug,
secondArrayText, secondArrayText,
siblingDataSlug, siblingDataSlug,
@@ -324,6 +323,35 @@ export default buildConfigWithDefaults(
], ],
versions: true, versions: true,
}, },
{
slug: restrictedVersionsAdminPanelSlug,
access: {
read: ({ req: { user } }) => {
if (user) {
return true
}
return false
},
readVersions: () => {
return {
'version.hidden': {
not_equals: true,
},
}
},
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'hidden',
type: 'checkbox',
},
],
versions: true,
},
{ {
slug: siblingDataSlug, slug: siblingDataSlug,
access: openAccess, access: openAccess,

View File

@@ -35,6 +35,7 @@ import {
readNotUpdateGlobalSlug, readNotUpdateGlobalSlug,
readOnlyGlobalSlug, readOnlyGlobalSlug,
readOnlySlug, readOnlySlug,
restrictedVersionsAdminPanelSlug,
restrictedVersionsSlug, restrictedVersionsSlug,
slug, slug,
unrestrictedSlug, unrestrictedSlug,
@@ -63,6 +64,7 @@ describe('Access Control', () => {
let richTextUrl: AdminUrlUtil let richTextUrl: AdminUrlUtil
let readOnlyGlobalUrl: AdminUrlUtil let readOnlyGlobalUrl: AdminUrlUtil
let restrictedVersionsUrl: AdminUrlUtil let restrictedVersionsUrl: AdminUrlUtil
let restrictedVersionsAdminPanelUrl: AdminUrlUtil
let userRestrictedCollectionURL: AdminUrlUtil let userRestrictedCollectionURL: AdminUrlUtil
let userRestrictedGlobalURL: AdminUrlUtil let userRestrictedGlobalURL: AdminUrlUtil
let disabledFields: AdminUrlUtil let disabledFields: AdminUrlUtil
@@ -81,6 +83,7 @@ describe('Access Control', () => {
readOnlyCollectionUrl = new AdminUrlUtil(serverURL, readOnlySlug) readOnlyCollectionUrl = new AdminUrlUtil(serverURL, readOnlySlug)
readOnlyGlobalUrl = new AdminUrlUtil(serverURL, readOnlySlug) readOnlyGlobalUrl = new AdminUrlUtil(serverURL, readOnlySlug)
restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug) restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug)
restrictedVersionsAdminPanelUrl = new AdminUrlUtil(serverURL, restrictedVersionsAdminPanelSlug)
userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug) userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug)
userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug) userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug)
disabledFields = new AdminUrlUtil(serverURL, disabledSlug) disabledFields = new AdminUrlUtil(serverURL, disabledSlug)
@@ -557,16 +560,26 @@ describe('Access Control', () => {
beforeAll(async () => { beforeAll(async () => {
existingDoc = await payload.create({ existingDoc = await payload.create({
collection: restrictedVersionsSlug, collection: restrictedVersionsAdminPanelSlug,
data: { data: {
name: 'name', name: 'name',
}, },
}) })
await payload.update({
collection: restrictedVersionsAdminPanelSlug,
id: existingDoc.id,
data: {
hidden: true,
},
})
}) })
test('versions sidebar should not show', async () => { test('versions tab should not show', async () => {
await page.goto(restrictedVersionsUrl.edit(existingDoc.id)) await page.goto(restrictedVersionsAdminPanelUrl.edit(existingDoc.id))
await expect(page.locator('.versions-count')).toBeHidden() await page.locator('.doc-tabs__tabs').getByLabel('Versions').click()
const rows = page.locator('.versions table tbody tr')
await expect(rows).toHaveCount(1)
}) })
}) })

View File

@@ -6,66 +6,11 @@
* and re-run `payload generate:types` to regenerate this file. * and re-run `payload generate:types` to regenerate this file.
*/ */
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config { export interface Config {
auth: { auth: {
users: UserAuthOperations; users: UserAuthOperations;
'public-users': PublicUserAuthOperations; 'public-users': PublicUserAuthOperations;
}; };
blocks: {};
collections: { collections: {
users: User; users: User;
'public-users': PublicUser; 'public-users': PublicUser;
@@ -77,6 +22,7 @@ export interface Config {
'user-restricted-collection': UserRestrictedCollection; 'user-restricted-collection': UserRestrictedCollection;
'create-not-update-collection': CreateNotUpdateCollection; 'create-not-update-collection': CreateNotUpdateCollection;
'restricted-versions': RestrictedVersion; 'restricted-versions': RestrictedVersion;
'restricted-versions-admin-panel': RestrictedVersionsAdminPanel;
'sibling-data': SiblingDatum; 'sibling-data': SiblingDatum;
'rely-on-request-headers': RelyOnRequestHeader; 'rely-on-request-headers': RelyOnRequestHeader;
'doc-level-access': DocLevelAccess; 'doc-level-access': DocLevelAccess;
@@ -104,6 +50,7 @@ export interface Config {
'user-restricted-collection': UserRestrictedCollectionSelect<false> | UserRestrictedCollectionSelect<true>; 'user-restricted-collection': UserRestrictedCollectionSelect<false> | UserRestrictedCollectionSelect<true>;
'create-not-update-collection': CreateNotUpdateCollectionSelect<false> | CreateNotUpdateCollectionSelect<true>; 'create-not-update-collection': CreateNotUpdateCollectionSelect<false> | CreateNotUpdateCollectionSelect<true>;
'restricted-versions': RestrictedVersionsSelect<false> | RestrictedVersionsSelect<true>; 'restricted-versions': RestrictedVersionsSelect<false> | RestrictedVersionsSelect<true>;
'restricted-versions-admin-panel': RestrictedVersionsAdminPanelSelect<false> | RestrictedVersionsAdminPanelSelect<true>;
'sibling-data': SiblingDataSelect<false> | SiblingDataSelect<true>; 'sibling-data': SiblingDataSelect<false> | SiblingDataSelect<true>;
'rely-on-request-headers': RelyOnRequestHeadersSelect<false> | RelyOnRequestHeadersSelect<true>; 'rely-on-request-headers': RelyOnRequestHeadersSelect<false> | RelyOnRequestHeadersSelect<true>;
'doc-level-access': DocLevelAccessSelect<false> | DocLevelAccessSelect<true>; 'doc-level-access': DocLevelAccessSelect<false> | DocLevelAccessSelect<true>;
@@ -309,6 +256,17 @@ export interface RestrictedVersion {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted-versions-admin-panel".
*/
export interface RestrictedVersionsAdminPanel {
id: string;
name?: string | null;
hidden?: boolean | null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "sibling-data". * via the `definition` "sibling-data".
@@ -714,6 +672,10 @@ export interface PayloadLockedDocument {
relationTo: 'restricted-versions'; relationTo: 'restricted-versions';
value: string | RestrictedVersion; value: string | RestrictedVersion;
} | null) } | null)
| ({
relationTo: 'restricted-versions-admin-panel';
value: string | RestrictedVersionsAdminPanel;
} | null)
| ({ | ({
relationTo: 'sibling-data'; relationTo: 'sibling-data';
value: string | SiblingDatum; value: string | SiblingDatum;
@@ -924,6 +886,16 @@ export interface RestrictedVersionsSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted-versions-admin-panel_select".
*/
export interface RestrictedVersionsAdminPanelSelect<T extends boolean = true> {
name?: T;
hidden?: T;
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "sibling-data_select". * via the `definition` "sibling-data_select".

View File

@@ -12,6 +12,7 @@ export const createNotUpdateCollectionSlug = 'create-not-update-collection'
export const userRestrictedGlobalSlug = 'user-restricted-global' export const userRestrictedGlobalSlug = 'user-restricted-global'
export const readNotUpdateGlobalSlug = 'read-not-update-global' export const readNotUpdateGlobalSlug = 'read-not-update-global'
export const restrictedVersionsSlug = 'restricted-versions' export const restrictedVersionsSlug = 'restricted-versions'
export const restrictedVersionsAdminPanelSlug = 'restricted-versions-admin-panel'
export const siblingDataSlug = 'sibling-data' export const siblingDataSlug = 'sibling-data'
export const relyOnRequestHeadersSlug = 'rely-on-request-headers' export const relyOnRequestHeadersSlug = 'rely-on-request-headers'
export const docLevelAccessSlug = 'doc-level-access' export const docLevelAccessSlug = 'doc-level-access'