diff --git a/packages/next/src/views/Document/getVersions.ts b/packages/next/src/views/Document/getVersions.ts index 44ef27edf7..d969073271 100644 --- a/packages/next/src/views/Document/getVersions.ts +++ b/packages/next/src/views/Document/getVersions.ts @@ -1,12 +1,13 @@ -import type { - Payload, - SanitizedCollectionConfig, - SanitizedDocumentPermissions, - SanitizedGlobalConfig, - TypedUser, -} from 'payload' - import { sanitizeID } from '@payloadcms/ui/shared' +import { + combineQueries, + extractAccessFromPermission, + type Payload, + type SanitizedCollectionConfig, + type SanitizedDocumentPermissions, + type SanitizedGlobalConfig, + type TypedUser, +} from 'payload' type Args = { collectionConfig?: SanitizedCollectionConfig @@ -134,15 +135,18 @@ export const getVersions = async ({ autosave: true, }, user, - where: { - and: [ - { - parent: { - equals: id, + where: combineQueries( + { + and: [ + { + parent: { + equals: id, + }, }, - }, - ], - }, + ], + }, + extractAccessFromPermission(docPermissions.readVersions), + ), }) if ( @@ -158,25 +162,28 @@ export const getVersions = async ({ ;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({ collection: collectionConfig.slug, user, - where: { - and: [ - { - parent: { - equals: id, + where: combineQueries( + { + and: [ + { + parent: { + equals: id, + }, }, - }, - { - 'version._status': { - equals: 'draft', + { + 'version._status': { + equals: 'draft', + }, }, - }, - { - updatedAt: { - greater_than: publishedDoc.updatedAt, + { + updatedAt: { + greater_than: publishedDoc.updatedAt, + }, }, - }, - ], - }, + ], + }, + extractAccessFromPermission(docPermissions.readVersions), + ), })) } } @@ -185,15 +192,18 @@ export const getVersions = async ({ collection: collectionConfig.slug, depth: 0, user, - where: { - and: [ - { - parent: { - equals: id, + where: combineQueries( + { + and: [ + { + parent: { + equals: id, + }, }, - }, - ], - }, + ], + }, + extractAccessFromPermission(docPermissions.readVersions), + ), })) } @@ -242,20 +252,23 @@ export const getVersions = async ({ depth: 0, global: globalConfig.slug, user, - where: { - and: [ - { - 'version._status': { - equals: 'draft', + where: combineQueries( + { + and: [ + { + 'version._status': { + equals: 'draft', + }, }, - }, - { - updatedAt: { - greater_than: publishedDoc.updatedAt, + { + updatedAt: { + greater_than: publishedDoc.updatedAt, + }, }, - }, - ], - }, + ], + }, + extractAccessFromPermission(docPermissions.readVersions), + ), })) } } diff --git a/packages/payload/src/auth/extractAccessFromPermission.ts b/packages/payload/src/auth/extractAccessFromPermission.ts new file mode 100644 index 0000000000..fa55c07bfa --- /dev/null +++ b/packages/payload/src/auth/extractAccessFromPermission.ts @@ -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 +} diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 1f9f9a11f2..1a5dff2640 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -968,6 +968,7 @@ interface RequestContext { export interface DatabaseAdapter extends BaseDatabaseAdapter {} export type { Payload, RequestContext } export { executeAuthStrategies } from './auth/executeAuthStrategies.js' +export { extractAccessFromPermission } from './auth/extractAccessFromPermission.js' export { getAccessResults } from './auth/getAccessResults.js' export { getFieldsToSign } from './auth/getFieldsToSign.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 { verifyEmailOperation } from './auth/operations/verifyEmail.js' export { JWTAuthentication } from './auth/strategies/jwt.js' + export type { AuthStrategyFunction, AuthStrategyFunctionArgs, @@ -1004,8 +1006,8 @@ export type { } from './auth/types.js' export { generateImportMap } from './bin/generateImportMap/index.js' - export type { ImportMap } from './bin/generateImportMap/index.js' + export { genImportMapIterateFields } from './bin/generateImportMap/iterateFields.js' export { @@ -1052,7 +1054,6 @@ export type { TypeWithID, TypeWithTimestamps, } from './collections/config/types.js' - export { createDataloaderCacheKey, getDataLoader } from './collections/dataloader.js' export { countOperation } from './collections/operations/count.js' export { createOperation } from './collections/operations/create.js' @@ -1075,8 +1076,8 @@ export { serverOnlyConfigProperties, type UnsanitizedClientConfig, } from './config/client.js' -export { defaults } from './config/defaults.js' +export { defaults } from './config/defaults.js' export { sanitizeConfig } from './config/sanitize.js' export type * from './config/types.js' export { combineQueries } from './database/combineQueries.js' diff --git a/packages/payload/src/utilities/getEntityPolicies.ts b/packages/payload/src/utilities/getEntityPolicies.ts index c3bf367c8c..104a6ead84 100644 --- a/packages/payload/src/utilities/getEntityPolicies.ts +++ b/packages/payload/src/utilities/getEntityPolicies.ts @@ -48,7 +48,10 @@ export async function getEntityPolicies(args: T): Promise | undefined - async function getEntityDoc({ where }: { where?: Where } = {}): Promise { + async function getEntityDoc({ + operation, + where, + }: { operation?: AllOperations; where?: Where } = {}): Promise { if (!entity.slug) { return undefined } @@ -66,18 +69,28 @@ export async function getEntityPolicies(args: T): Promise(args: T): Promise({ fieldName, userHasAccessToAllTenants, }: Args): void => { - if (!collection?.access) { - collection.access = {} - } - collectionAccessKeys.reduce<{ - [key in (typeof collectionAccessKeys)[number]]?: Access - }>((acc, key) => { + collectionAccessKeys.forEach((key) => { if (!collection.access) { - return acc + collection.access = {} } collection.access[key] = withTenantAccess({ accessFunction: collection.access?.[key], - fieldName, + fieldName: key === 'readVersions' ? `version.${fieldName}` : fieldName, userHasAccessToAllTenants, }) - - return acc - }, {}) + }) } diff --git a/test/access-control/config.ts b/test/access-control/config.ts index 54b93a381a..17a3a271ff 100644 --- a/test/access-control/config.ts +++ b/test/access-control/config.ts @@ -22,14 +22,13 @@ import { hiddenAccessSlug, hiddenFieldsSlug, nonAdminEmail, - nonAdminUserEmail, - nonAdminUserSlug, publicUserEmail, publicUsersSlug, readNotUpdateGlobalSlug, readOnlyGlobalSlug, readOnlySlug, relyOnRequestHeadersSlug, + restrictedVersionsAdminPanelSlug, restrictedVersionsSlug, secondArrayText, siblingDataSlug, @@ -324,6 +323,35 @@ export default buildConfigWithDefaults( ], 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, access: openAccess, diff --git a/test/access-control/e2e.spec.ts b/test/access-control/e2e.spec.ts index d7eeb97b7c..90edf52996 100644 --- a/test/access-control/e2e.spec.ts +++ b/test/access-control/e2e.spec.ts @@ -35,6 +35,7 @@ import { readNotUpdateGlobalSlug, readOnlyGlobalSlug, readOnlySlug, + restrictedVersionsAdminPanelSlug, restrictedVersionsSlug, slug, unrestrictedSlug, @@ -63,6 +64,7 @@ describe('Access Control', () => { let richTextUrl: AdminUrlUtil let readOnlyGlobalUrl: AdminUrlUtil let restrictedVersionsUrl: AdminUrlUtil + let restrictedVersionsAdminPanelUrl: AdminUrlUtil let userRestrictedCollectionURL: AdminUrlUtil let userRestrictedGlobalURL: AdminUrlUtil let disabledFields: AdminUrlUtil @@ -81,6 +83,7 @@ describe('Access Control', () => { readOnlyCollectionUrl = new AdminUrlUtil(serverURL, readOnlySlug) readOnlyGlobalUrl = new AdminUrlUtil(serverURL, readOnlySlug) restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug) + restrictedVersionsAdminPanelUrl = new AdminUrlUtil(serverURL, restrictedVersionsAdminPanelSlug) userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug) userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug) disabledFields = new AdminUrlUtil(serverURL, disabledSlug) @@ -557,16 +560,26 @@ describe('Access Control', () => { beforeAll(async () => { existingDoc = await payload.create({ - collection: restrictedVersionsSlug, + collection: restrictedVersionsAdminPanelSlug, data: { name: 'name', }, }) + + await payload.update({ + collection: restrictedVersionsAdminPanelSlug, + id: existingDoc.id, + data: { + hidden: true, + }, + }) }) - test('versions sidebar should not show', async () => { - await page.goto(restrictedVersionsUrl.edit(existingDoc.id)) - await expect(page.locator('.versions-count')).toBeHidden() + test('versions tab should not show', async () => { + await page.goto(restrictedVersionsAdminPanelUrl.edit(existingDoc.id)) + await page.locator('.doc-tabs__tabs').getByLabel('Versions').click() + const rows = page.locator('.versions table tbody tr') + await expect(rows).toHaveCount(1) }) }) diff --git a/test/access-control/payload-types.ts b/test/access-control/payload-types.ts index d84dae9ede..ed98d40347 100644 --- a/test/access-control/payload-types.ts +++ b/test/access-control/payload-types.ts @@ -6,66 +6,11 @@ * 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 { auth: { users: UserAuthOperations; 'public-users': PublicUserAuthOperations; }; - blocks: {}; collections: { users: User; 'public-users': PublicUser; @@ -77,6 +22,7 @@ export interface Config { 'user-restricted-collection': UserRestrictedCollection; 'create-not-update-collection': CreateNotUpdateCollection; 'restricted-versions': RestrictedVersion; + 'restricted-versions-admin-panel': RestrictedVersionsAdminPanel; 'sibling-data': SiblingDatum; 'rely-on-request-headers': RelyOnRequestHeader; 'doc-level-access': DocLevelAccess; @@ -104,6 +50,7 @@ export interface Config { 'user-restricted-collection': UserRestrictedCollectionSelect | UserRestrictedCollectionSelect; 'create-not-update-collection': CreateNotUpdateCollectionSelect | CreateNotUpdateCollectionSelect; 'restricted-versions': RestrictedVersionsSelect | RestrictedVersionsSelect; + 'restricted-versions-admin-panel': RestrictedVersionsAdminPanelSelect | RestrictedVersionsAdminPanelSelect; 'sibling-data': SiblingDataSelect | SiblingDataSelect; 'rely-on-request-headers': RelyOnRequestHeadersSelect | RelyOnRequestHeadersSelect; 'doc-level-access': DocLevelAccessSelect | DocLevelAccessSelect; @@ -309,6 +256,17 @@ export interface RestrictedVersion { updatedAt: 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 * via the `definition` "sibling-data". @@ -714,6 +672,10 @@ export interface PayloadLockedDocument { relationTo: 'restricted-versions'; value: string | RestrictedVersion; } | null) + | ({ + relationTo: 'restricted-versions-admin-panel'; + value: string | RestrictedVersionsAdminPanel; + } | null) | ({ relationTo: 'sibling-data'; value: string | SiblingDatum; @@ -924,6 +886,16 @@ export interface RestrictedVersionsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "restricted-versions-admin-panel_select". + */ +export interface RestrictedVersionsAdminPanelSelect { + name?: T; + hidden?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "sibling-data_select". diff --git a/test/access-control/shared.ts b/test/access-control/shared.ts index 8706453b28..0de87257e8 100644 --- a/test/access-control/shared.ts +++ b/test/access-control/shared.ts @@ -12,6 +12,7 @@ export const createNotUpdateCollectionSlug = 'create-not-update-collection' export const userRestrictedGlobalSlug = 'user-restricted-global' export const readNotUpdateGlobalSlug = 'read-not-update-global' export const restrictedVersionsSlug = 'restricted-versions' +export const restrictedVersionsAdminPanelSlug = 'restricted-versions-admin-panel' export const siblingDataSlug = 'sibling-data' export const relyOnRequestHeadersSlug = 'rely-on-request-headers' export const docLevelAccessSlug = 'doc-level-access'