diff --git a/packages/next/src/elements/DocumentHeader/Tabs/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/index.tsx index 987f193b8..3e000515b 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/index.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/index.tsx @@ -1,9 +1,9 @@ import type { I18n } from '@payloadcms/translations' import type { Payload, - Permissions, SanitizedCollectionConfig, SanitizedGlobalConfig, + SanitizedPermissions, } from 'payload' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' @@ -23,7 +23,7 @@ export const DocumentTabs: React.FC<{ globalConfig: SanitizedGlobalConfig i18n: I18n payload: Payload - permissions: Permissions + permissions: SanitizedPermissions }> = (props) => { const { collectionConfig, globalConfig, i18n, payload, permissions } = props const { config } = payload diff --git a/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx index 0bcffe4e0..fbe9c33cb 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx @@ -72,9 +72,8 @@ export const tabs: Record< condition: ({ collectionConfig, globalConfig, permissions }) => Boolean( (collectionConfig?.versions && - permissions?.collections?.[collectionConfig?.slug]?.readVersions?.permission) || - (globalConfig?.versions && - permissions?.globals?.[globalConfig?.slug]?.readVersions?.permission), + permissions?.collections?.[collectionConfig?.slug]?.readVersions) || + (globalConfig?.versions && permissions?.globals?.[globalConfig?.slug]?.readVersions), ), href: '/versions', label: ({ t }) => t('version:versions'), diff --git a/packages/next/src/elements/DocumentHeader/index.tsx b/packages/next/src/elements/DocumentHeader/index.tsx index 67e984a2f..9cf68a2c8 100644 --- a/packages/next/src/elements/DocumentHeader/index.tsx +++ b/packages/next/src/elements/DocumentHeader/index.tsx @@ -1,9 +1,9 @@ import type { I18n } from '@payloadcms/translations' import type { Payload, - Permissions, SanitizedCollectionConfig, SanitizedGlobalConfig, + SanitizedPermissions, } from 'payload' import { Gutter, RenderTitle } from '@payloadcms/ui' @@ -20,7 +20,7 @@ export const DocumentHeader: React.FC<{ hideTabs?: boolean i18n: I18n payload: Payload - permissions: Permissions + permissions: SanitizedPermissions }> = (props) => { const { collectionConfig, globalConfig, hideTabs, i18n, payload, permissions } = props diff --git a/packages/next/src/utilities/initReq.ts b/packages/next/src/utilities/initReq.ts index e907ee728..c0a8f1c18 100644 --- a/packages/next/src/utilities/initReq.ts +++ b/packages/next/src/utilities/initReq.ts @@ -1,5 +1,5 @@ import type { I18n, I18nClient } from '@payloadcms/translations' -import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload' +import type { PayloadRequest, SanitizedConfig, SanitizedPermissions, User } from 'payload' import { initI18n } from '@payloadcms/translations' import { headers as getHeaders } from 'next/headers.js' @@ -11,7 +11,7 @@ import { getRequestLanguage } from './getRequestLanguage.js' type Result = { i18n: I18nClient - permissions: Permissions + permissions: SanitizedPermissions req: PayloadRequest user: User } diff --git a/packages/next/src/views/CreateFirstUser/index.client.tsx b/packages/next/src/views/CreateFirstUser/index.client.tsx index 156d134a8..31b821719 100644 --- a/packages/next/src/views/CreateFirstUser/index.client.tsx +++ b/packages/next/src/views/CreateFirstUser/index.client.tsx @@ -2,10 +2,10 @@ import type { FormProps, UserWithToken } from '@payloadcms/ui' import type { ClientCollectionConfig, - DocumentPermissions, DocumentPreferences, FormState, LoginWithUsernameOptions, + SanitizedDocumentPermissions, } from 'payload' import { @@ -24,7 +24,7 @@ import { abortAndIgnore } from '@payloadcms/ui/shared' import React, { useEffect } from 'react' export const CreateFirstUserClient: React.FC<{ - docPermissions: DocumentPermissions + docPermissions: SanitizedDocumentPermissions docPreferences: DocumentPreferences initialState: FormState loginWithUsername?: false | LoginWithUsernameOptions @@ -114,7 +114,7 @@ export const CreateFirstUserClient: React.FC<{ parentIndexPath="" parentPath="" parentSchemaPath={userSlug} - permissions={null} + permissions={true} readOnly={false} /> {t('general:create')} diff --git a/packages/next/src/views/Dashboard/Default/index.tsx b/packages/next/src/views/Dashboard/Default/index.tsx index 7b0d1674f..34d0022b2 100644 --- a/packages/next/src/views/Dashboard/Default/index.tsx +++ b/packages/next/src/views/Dashboard/Default/index.tsx @@ -1,5 +1,5 @@ import type { groupNavItems } from '@payloadcms/ui/shared' -import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload' +import type { ClientUser, SanitizedPermissions, ServerProps, VisibleEntities } from 'payload' import { getTranslation } from '@payloadcms/translations' import { Button, Card, Gutter, Locked } from '@payloadcms/ui' @@ -19,7 +19,7 @@ export type DashboardProps = { }> Link: React.ComponentType navGroups?: ReturnType - permissions: Permissions + permissions: SanitizedPermissions visibleEntities: VisibleEntities } & ServerProps @@ -94,7 +94,7 @@ export const DefaultDashboard: React.FC = (props) => { path: `/collections/${slug}/create`, }) - hasCreatePermission = permissions?.collections?.[slug]?.create?.permission + hasCreatePermission = permissions?.collections?.[slug]?.create } if (type === EntityType.global) { diff --git a/packages/next/src/views/Dashboard/index.tsx b/packages/next/src/views/Dashboard/index.tsx index d8fa8711b..b342813c4 100644 --- a/packages/next/src/views/Dashboard/index.tsx +++ b/packages/next/src/views/Dashboard/index.tsx @@ -35,14 +35,13 @@ export const Dashboard: React.FC = async ({ const collections = config.collections.filter( (collection) => - permissions?.collections?.[collection.slug]?.read?.permission && + permissions?.collections?.[collection.slug]?.read && visibleEntities.collections.includes(collection.slug), ) const globals = config.globals.filter( (global) => - permissions?.globals?.[global.slug]?.read?.permission && - visibleEntities.globals.includes(global.slug), + permissions?.globals?.[global.slug]?.read && visibleEntities.globals.includes(global.slug), ) // Query locked global documents only if there are globals in the config diff --git a/packages/next/src/views/Document/getDocumentPermissions.tsx b/packages/next/src/views/Document/getDocumentPermissions.tsx index 8fb1ac7d4..eccdbf900 100644 --- a/packages/next/src/views/Document/getDocumentPermissions.tsx +++ b/packages/next/src/views/Document/getDocumentPermissions.tsx @@ -3,6 +3,7 @@ import type { DocumentPermissions, PayloadRequest, SanitizedCollectionConfig, + SanitizedDocumentPermissions, SanitizedGlobalConfig, } from 'payload' @@ -10,7 +11,7 @@ import { hasSavePermission as getHasSavePermission, isEditing as getIsEditing, } from '@payloadcms/ui/shared' -import { docAccessOperation, docAccessOperationGlobal } from 'payload' +import { docAccessOperation, docAccessOperationGlobal, sanitizePermissions } from 'payload' export const getDocumentPermissions = async (args: { collectionConfig?: SanitizedCollectionConfig @@ -19,7 +20,7 @@ export const getDocumentPermissions = async (args: { id?: number | string req: PayloadRequest }): Promise<{ - docPermissions: DocumentPermissions + docPermissions: SanitizedDocumentPermissions hasPublishPermission: boolean hasSavePermission: boolean }> => { @@ -91,9 +92,13 @@ export const getDocumentPermissions = async (args: { } } + // TODO: do this in a better way. Only doing this bc this is how the fn was written (mutates the original object) + const sanitizedDocPermissions = { ...docPermissions } as any as SanitizedDocumentPermissions + sanitizePermissions(sanitizedDocPermissions) + const hasSavePermission = getHasSavePermission({ collectionSlug: collectionConfig?.slug, - docPermissions, + docPermissions: sanitizedDocPermissions, globalSlug: globalConfig?.slug, isEditing: getIsEditing({ id, @@ -103,7 +108,7 @@ export const getDocumentPermissions = async (args: { }) return { - docPermissions, + docPermissions: sanitizedDocPermissions, hasPublishPermission, hasSavePermission, } diff --git a/packages/next/src/views/Document/getVersions.ts b/packages/next/src/views/Document/getVersions.ts index 0e4649c08..d35557875 100644 --- a/packages/next/src/views/Document/getVersions.ts +++ b/packages/next/src/views/Document/getVersions.ts @@ -1,14 +1,14 @@ import type { - DocumentPermissions, Payload, SanitizedCollectionConfig, + SanitizedDocumentPermissions, SanitizedGlobalConfig, TypedUser, } from 'payload' type Args = { collectionConfig?: SanitizedCollectionConfig - docPermissions: DocumentPermissions + docPermissions: SanitizedDocumentPermissions globalConfig?: SanitizedGlobalConfig id?: number | string locale?: string @@ -43,7 +43,7 @@ export const getVersions = async ({ const entityConfig = collectionConfig || globalConfig const versionsConfig = entityConfig?.versions - const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions?.permission) + const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions) if (!shouldFetchVersions) { const hasPublishedDoc = Boolean((collectionConfig && id) || globalConfig) diff --git a/packages/next/src/views/Document/getViewsFromConfig.tsx b/packages/next/src/views/Document/getViewsFromConfig.tsx index 22fb196eb..3cf0494e1 100644 --- a/packages/next/src/views/Document/getViewsFromConfig.tsx +++ b/packages/next/src/views/Document/getViewsFromConfig.tsx @@ -1,11 +1,11 @@ import type { AdminViewProps, - CollectionPermission, - GlobalPermission, PayloadComponent, SanitizedCollectionConfig, + SanitizedCollectionPermission, SanitizedConfig, SanitizedGlobalConfig, + SanitizedGlobalPermission, ServerSideEditViewProps, } from 'payload' import type React from 'react' @@ -38,7 +38,7 @@ export const getViewsFromConfig = ({ routeSegments: string[] } & ( | { - docPermissions: CollectionPermission | GlobalPermission + docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission overrideDocPermissions?: false | undefined } | { @@ -78,7 +78,7 @@ export const getViewsFromConfig = ({ const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] = routeSegments - if (!overrideDocPermissions && !docPermissions?.read?.permission) { + if (!overrideDocPermissions && !docPermissions?.read) { throw new Error('not-found') } else { // `../:id`, or `../create` @@ -86,11 +86,7 @@ export const getViewsFromConfig = ({ case 3: { switch (segment3) { case 'create': { - if ( - !overrideDocPermissions && - 'create' in docPermissions && - docPermissions?.create?.permission - ) { + if (!overrideDocPermissions && 'create' in docPermissions && docPermissions.create) { CustomView = { ComponentConfig: getCustomViewByKey(views, 'default'), } @@ -176,7 +172,7 @@ export const getViewsFromConfig = ({ } case 'versions': { - if (!overrideDocPermissions && docPermissions?.readVersions?.permission) { + if (!overrideDocPermissions && docPermissions?.readVersions) { CustomView = { ComponentConfig: getCustomViewByKey(views, 'versions'), } @@ -229,7 +225,7 @@ export const getViewsFromConfig = ({ // `../:id/versions/:version`, etc default: { if (segment4 === 'versions') { - if (!overrideDocPermissions && docPermissions?.readVersions?.permission) { + if (!overrideDocPermissions && docPermissions?.readVersions) { CustomView = { ComponentConfig: getCustomViewByKey(views, 'version'), } @@ -281,7 +277,7 @@ export const getViewsFromConfig = ({ if (globalConfig) { const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments - if (!overrideDocPermissions && !docPermissions?.read?.permission) { + if (!overrideDocPermissions && !docPermissions?.read) { throw new Error('not-found') } else { switch (routeSegments.length) { @@ -323,7 +319,7 @@ export const getViewsFromConfig = ({ } case 'versions': { - if (!overrideDocPermissions && docPermissions?.readVersions?.permission) { + if (!overrideDocPermissions && docPermissions?.readVersions) { CustomView = { ComponentConfig: getCustomViewByKey(views, 'versions'), } @@ -340,7 +336,7 @@ export const getViewsFromConfig = ({ } default: { - if (!overrideDocPermissions && docPermissions?.read?.permission) { + if (!overrideDocPermissions && docPermissions?.read) { const baseRoute = [adminRoute, globalEntity, globalSlug, segment3] .filter(Boolean) .join('/') @@ -381,7 +377,7 @@ export const getViewsFromConfig = ({ default: { // `../:slug/versions/:version`, etc if (segment3 === 'versions') { - if (!overrideDocPermissions && docPermissions?.readVersions?.permission) { + if (!overrideDocPermissions && docPermissions?.readVersions) { CustomView = { ComponentConfig: getCustomViewByKey(views, 'version'), } diff --git a/packages/next/src/views/Document/renderDocumentSlots.tsx b/packages/next/src/views/Document/renderDocumentSlots.tsx index 4c836462b..bce86f376 100644 --- a/packages/next/src/views/Document/renderDocumentSlots.tsx +++ b/packages/next/src/views/Document/renderDocumentSlots.tsx @@ -1,9 +1,9 @@ import type { DefaultServerFunctionArgs, - DocumentPermissions, DocumentSlots, PayloadRequest, SanitizedCollectionConfig, + SanitizedDocumentPermissions, SanitizedGlobalConfig, StaticDescription, } from 'payload' @@ -18,7 +18,7 @@ export const renderDocumentSlots: (args: { collectionConfig?: SanitizedCollectionConfig globalConfig?: SanitizedGlobalConfig hasSavePermission: boolean - permissions: DocumentPermissions + permissions: SanitizedDocumentPermissions req: PayloadRequest }) => DocumentSlots = (args) => { const { collectionConfig, globalConfig, hasSavePermission, req } = args diff --git a/packages/next/src/views/List/index.tsx b/packages/next/src/views/List/index.tsx index af1e378f4..f0ea89990 100644 --- a/packages/next/src/views/List/index.tsx +++ b/packages/next/src/views/List/index.tsx @@ -66,7 +66,7 @@ export const renderListView = async ( visibleEntities, } = initPageResult - if (!permissions?.collections?.[collectionSlug]?.read?.permission) { + if (!permissions?.collections?.[collectionSlug]?.read) { throw new Error('not-found') } @@ -190,7 +190,7 @@ export const renderListView = async ( const sharedClientProps: ListComponentClientProps = { collectionSlug, - hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission, + hasCreatePermission: permissions?.collections?.[collectionSlug]?.create, newDocumentURL: formatAdminURL({ adminRoute, path: `/collections/${collectionSlug}/create`, diff --git a/packages/next/src/views/Version/Default/index.tsx b/packages/next/src/views/Version/Default/index.tsx index 208cb29a5..029ec67da 100644 --- a/packages/next/src/views/Version/Default/index.tsx +++ b/packages/next/src/views/Version/Default/index.tsx @@ -65,7 +65,7 @@ export const DefaultVersionView: React.FC = ({ const comparison = compareValue?.value && currentComparisonDoc?.version // the `version` key is only present on `versions` documents - const canUpdate = docPermissions?.update?.permission + const canUpdate = docPermissions?.update const localeValues = locales && locales.map((locale) => locale.value) diff --git a/packages/next/src/views/Version/Default/types.ts b/packages/next/src/views/Version/Default/types.ts index 64736c2c4..08ed5fad1 100644 --- a/packages/next/src/views/Version/Default/types.ts +++ b/packages/next/src/views/Version/Default/types.ts @@ -1,4 +1,9 @@ -import type { CollectionPermission, Document, GlobalPermission, OptionObject } from 'payload' +import type { + Document, + OptionObject, + SanitizedCollectionPermission, + SanitizedGlobalPermission, +} from 'payload' export type CompareOption = { label: React.ReactNode | string @@ -9,7 +14,7 @@ export type CompareOption = { export type DefaultVersionsViewProps = { readonly doc: Document - readonly docPermissions: CollectionPermission | GlobalPermission + readonly docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission readonly initialComparisonDoc: Document readonly latestDraftVersion?: string readonly latestPublishedVersion?: string diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts b/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts index e555712c2..452f3c232 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts @@ -1,5 +1,5 @@ import type { I18nClient } from '@payloadcms/translations' -import type { ClientField, FieldPermissions } from 'payload' +import type { ClientField, SanitizedFieldPermissions } from 'payload' import type React from 'react' import type { DiffMethod } from 'react-diff-viewer-continued' @@ -16,6 +16,10 @@ export type DiffComponentProps = { readonly isRichText?: boolean readonly locale?: string readonly locales?: string[] - readonly permissions?: Record + readonly permissions?: + | { + [key: string]: SanitizedFieldPermissions + } + | true readonly version: any } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx index 0bba5e8fb..2fcb392c9 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx @@ -50,11 +50,13 @@ const RenderFieldsToDiff: React.FC = ({ ? JSON.stringify(comparison?.[fieldName]) : comparison?.[fieldName] - const hasPermission = fieldPermissions?.[fieldName]?.read?.permission + const hasPermission = + fieldPermissions?.[fieldName] === true || fieldPermissions?.[fieldName]?.read - const subFieldPermissions = fieldPermissions?.[fieldName]?.fields + const subFieldPermissions = + fieldPermissions?.[fieldName] === true || fieldPermissions?.[fieldName]?.fields - if (hasPermission === false) { + if (!hasPermission) { return null } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/types.ts b/packages/next/src/views/Version/RenderFieldsToDiff/types.ts index 65e12b688..634e6eb8b 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/types.ts +++ b/packages/next/src/views/Version/RenderFieldsToDiff/types.ts @@ -1,5 +1,5 @@ import type { I18nClient } from '@payloadcms/translations' -import type { ClientField, FieldPermissions } from 'payload' +import type { ClientField, SanitizedFieldPermissions } from 'payload' import type { DiffMethod } from 'react-diff-viewer-continued' import type { DiffComponents } from './fields/types.js' @@ -7,7 +7,11 @@ import type { DiffComponents } from './fields/types.js' export type Props = { readonly comparison: Record readonly diffComponents: DiffComponents - readonly fieldPermissions: Record + readonly fieldPermissions: + | { + [key: string]: SanitizedFieldPermissions + } + | true readonly fields: ClientField[] readonly i18n: I18nClient readonly locales: string[] diff --git a/packages/next/src/views/Version/index.tsx b/packages/next/src/views/Version/index.tsx index 0c777880b..384d72a22 100644 --- a/packages/next/src/views/Version/index.tsx +++ b/packages/next/src/views/Version/index.tsx @@ -1,10 +1,10 @@ import type { - CollectionPermission, Document, EditViewComponent, - GlobalPermission, OptionObject, PayloadServerReactComponent, + SanitizedCollectionPermission, + SanitizedGlobalPermission, } from 'payload' import { notFound } from 'next/navigation.js' @@ -33,7 +33,7 @@ export const VersionView: PayloadServerReactComponent = async const { localization } = config - let docPermissions: CollectionPermission | GlobalPermission + let docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission let slug: string let doc: Document diff --git a/packages/payload/src/admin/elements/Tab.ts b/packages/payload/src/admin/elements/Tab.ts index c28903bb2..06b6e7dca 100644 --- a/packages/payload/src/admin/elements/Tab.ts +++ b/packages/payload/src/admin/elements/Tab.ts @@ -1,6 +1,6 @@ import type { I18n } from '@payloadcms/translations' -import type { Permissions } from '../../auth/types.js' +import type { SanitizedPermissions } from '../../auth/types.js' import type { SanitizedCollectionConfig } from '../../collections/config/types.js' import type { PayloadComponent, SanitizedConfig } from '../../config/types.js' import type { SanitizedGlobalConfig } from '../../globals/config/types.js' @@ -12,14 +12,14 @@ export type DocumentTabProps = { readonly globalConfig?: SanitizedGlobalConfig readonly i18n: I18n readonly payload: Payload - readonly permissions: Permissions + readonly permissions: SanitizedPermissions } export type DocumentTabCondition = (args: { collectionConfig: SanitizedCollectionConfig config: SanitizedConfig globalConfig: SanitizedGlobalConfig - permissions: Permissions + permissions: SanitizedPermissions }) => boolean // Everything is optional because we merge in the defaults diff --git a/packages/payload/src/admin/forms/Field.ts b/packages/payload/src/admin/forms/Field.ts index 795ed991c..e08d3a9ed 100644 --- a/packages/payload/src/admin/forms/Field.ts +++ b/packages/payload/src/admin/forms/Field.ts @@ -1,7 +1,7 @@ import type { I18nClient } from '@payloadcms/translations' import type { MarkOptional } from 'ts-essentials' -import type { FieldPermissions, User } from '../../auth/types.js' +import type { SanitizedFieldPermissions, User } from '../../auth/types.js' import type { ClientBlock, ClientField, Field } from '../../fields/config/types.js' import type { Payload } from '../../types/index.js' import type { @@ -79,7 +79,7 @@ export type ServerComponentProps = { formState: FormState i18n: I18nClient payload: Payload - permissions: FieldPermissions + permissions: SanitizedFieldPermissions siblingData: Data user: User } diff --git a/packages/payload/src/admin/forms/Form.ts b/packages/payload/src/admin/forms/Form.ts index 978e40d52..bd96fadb5 100644 --- a/packages/payload/src/admin/forms/Form.ts +++ b/packages/payload/src/admin/forms/Form.ts @@ -1,6 +1,6 @@ import { type SupportedLanguages } from '@payloadcms/translations' -import type { DocumentPermissions } from '../../auth/types.js' +import type { SanitizedDocumentPermissions } from '../../auth/types.js' import type { Field, Validate } from '../../fields/config/types.js' import type { TypedLocale } from '../../index.js' import type { DocumentPreferences } from '../../preferences/types.js' @@ -61,7 +61,7 @@ export type FormStateWithoutComponents = { export type BuildFormStateArgs = { data?: Data - docPermissions: DocumentPermissions | undefined + docPermissions: SanitizedDocumentPermissions | undefined docPreferences: DocumentPreferences fallbackLocale?: false | TypedLocale formState?: FormState diff --git a/packages/payload/src/admin/views/types.ts b/packages/payload/src/admin/views/types.ts index 0aae8c3c6..bdf2d6f57 100644 --- a/packages/payload/src/admin/views/types.ts +++ b/packages/payload/src/admin/views/types.ts @@ -1,6 +1,6 @@ import type { ClientTranslationsObject } from '@payloadcms/translations' -import type { Permissions } from '../../auth/index.js' +import type { SanitizedPermissions } from '../../auth/index.js' import type { ImportMap } from '../../bin/generateImportMap/index.js' import type { SanitizedCollectionConfig } from '../../collections/config/types.js' import type { ClientConfig } from '../../config/client.js' @@ -52,7 +52,7 @@ export type InitPageResult = { globalConfig?: SanitizedGlobalConfig languageOptions: LanguageOptions locale?: Locale - permissions: Permissions + permissions: SanitizedPermissions redirectTo?: string req: PayloadRequest translations: ClientTranslationsObject diff --git a/packages/payload/src/auth/getAccessResults.ts b/packages/payload/src/auth/getAccessResults.ts index 56753ffef..32d0f5e35 100644 --- a/packages/payload/src/auth/getAccessResults.ts +++ b/packages/payload/src/auth/getAccessResults.ts @@ -1,12 +1,15 @@ import type { AllOperations, PayloadRequest } from '../types/index.js' -import type { Permissions } from './types.js' +import type { Permissions, SanitizedPermissions } from './types.js' import { getEntityPolicies } from '../utilities/getEntityPolicies.js' +import { sanitizePermissions } from '../utilities/sanitizePermissions.js' type GetAccessResultsArgs = { req: PayloadRequest } -export async function getAccessResults({ req }: GetAccessResultsArgs): Promise { +export async function getAccessResults({ + req, +}: GetAccessResultsArgs): Promise { const results = {} as Permissions const { payload, user } = req @@ -74,5 +77,5 @@ export async function getAccessResults({ req }: GetAccessResultsArgs): Promise

=> { +export const accessOperation = async (args: Arguments): Promise => { const { req } = args adminInitTelemetry(req) diff --git a/packages/payload/src/auth/operations/auth.ts b/packages/payload/src/auth/operations/auth.ts index 110abaab5..035d4b3f1 100644 --- a/packages/payload/src/auth/operations/auth.ts +++ b/packages/payload/src/auth/operations/auth.ts @@ -1,6 +1,5 @@ -import type { TypedUser } from '../../index.js' +import type { SanitizedPermissions, TypedUser } from '../../index.js' import type { PayloadRequest } from '../../types/index.js' -import type { Permissions } from '../types.js' import { killTransaction } from '../../utilities/killTransaction.js' import { executeAuthStrategies } from '../executeAuthStrategies.js' @@ -12,7 +11,7 @@ export type AuthArgs = { } export type AuthResult = { - permissions: Permissions + permissions: SanitizedPermissions responseHeaders?: Headers user: null | TypedUser } diff --git a/packages/payload/src/auth/types.ts b/packages/payload/src/auth/types.ts index 987370778..570dad10d 100644 --- a/packages/payload/src/auth/types.ts +++ b/packages/payload/src/auth/types.ts @@ -3,9 +3,12 @@ import type { DeepRequired } from 'ts-essentials' import type { CollectionSlug, GlobalSlug, Payload } from '../index.js' import type { PayloadRequest, Where } from '../types/index.js' +/** + * A permission object that can be used to determine if a user has access to a specific operation. + */ export type Permission = { permission: boolean - where?: Record + where?: Where } export type FieldPermissions = { @@ -30,6 +33,24 @@ export type FieldPermissions = { } } +export type SanitizedFieldPermissions = + | { + blocks?: { + [blockSlug: string]: { + fields: { + [fieldName: string]: SanitizedFieldPermissions + } + } + } + create: true + fields?: { + [fieldName: string]: SanitizedFieldPermissions + } + read: true + update: true + } + | true + export type CollectionPermission = { create: Permission delete: Permission @@ -41,6 +62,19 @@ export type CollectionPermission = { update: Permission } +export type SanitizedCollectionPermission = { + create?: true + delete?: true + fields: + | { + [fieldName: string]: SanitizedFieldPermissions + } + | true + read?: true + readVersions?: true + update?: true +} + export type GlobalPermission = { fields: { [fieldName: string]: FieldPermissions @@ -50,7 +84,21 @@ export type GlobalPermission = { update: Permission } +export type SanitizedGlobalPermission = { + fields: + | { + [fieldName: string]: SanitizedFieldPermissions + } + | true + read?: true + readVersions?: true + update?: true +} + export type DocumentPermissions = CollectionPermission | GlobalPermission + +export type SanitizedDocumentPermissions = SanitizedCollectionPermission | SanitizedGlobalPermission + export type Permissions = { canAccessAdmin: boolean collections: { @@ -61,6 +109,32 @@ export type Permissions = { } } +export type SanitizedPermissions = { + canAccessAdmin?: boolean + collections?: { + [collectionSlug: string]: { + create?: true + delete?: true + fields: { + [fieldName: string]: SanitizedFieldPermissions + } + read?: true + readVersions?: true + update?: true + } + } + globals?: { + [globalSlug: string]: { + fields: { + [fieldName: string]: SanitizedFieldPermissions + } + read?: true + readVersions?: true + update?: true + } + } +} + type BaseUser = { collection: string email?: string diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index be1c53b71..190e1e4ca 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -20,7 +20,7 @@ import type { ServerSideEditViewProps, VisibleEntities, } from '../admin/views/types.js' -import type { Permissions } from '../auth/index.js' +import type { SanitizedPermissions } from '../auth/index.js' import type { AddToImportMap, ImportMap, @@ -398,7 +398,7 @@ export type ServerProps = { readonly locale?: Locale readonly params?: { [key: string]: string | string[] | undefined } readonly payload: Payload - readonly permissions?: Permissions + readonly permissions?: SanitizedPermissions readonly searchParams?: { [key: string]: string | string[] | undefined } readonly user?: TypedUser readonly visibleEntities?: VisibleEntities diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 2be723410..385579858 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -778,6 +778,7 @@ export { registerFirstUserOperation } from './auth/operations/registerFirstUser. export { resetPasswordOperation } from './auth/operations/resetPassword.js' export { unlockOperation } from './auth/operations/unlock.js' export { verifyEmailOperation } from './auth/operations/verifyEmail.js' + export type { AuthStrategyFunction, AuthStrategyFunctionArgs, @@ -788,9 +789,15 @@ export type { IncomingAuthType, Permission, Permissions, + SanitizedCollectionPermission, + SanitizedDocumentPermissions, + SanitizedFieldPermissions, + SanitizedGlobalPermission, + SanitizedPermissions, User, VerifyConfig, } from './auth/types.js' + export { generateImportMap } from './bin/generateImportMap/index.js' export type { ImportMap } from './bin/generateImportMap/index.js' @@ -1218,6 +1225,7 @@ export { isValidID } from './utilities/isValidID.js' export { killTransaction } from './utilities/killTransaction.js' export { mapAsync } from './utilities/mapAsync.js' export { sanitizeFallbackLocale } from './utilities/sanitizeFallbackLocale.js' +export { recursivelySanitizePermissions as sanitizePermissions } from './utilities/sanitizePermissions.js' export { traverseFields } from './utilities/traverseFields.js' export type { TraverseFieldsCallback } from './utilities/traverseFields.js' export { buildVersionCollectionFields } from './versions/buildCollectionFields.js' diff --git a/packages/payload/src/preferences/preferencesCollection.ts b/packages/payload/src/preferences/preferencesCollection.ts index 82f6c4078..f74c1ddd6 100644 --- a/packages/payload/src/preferences/preferencesCollection.ts +++ b/packages/payload/src/preferences/preferencesCollection.ts @@ -5,11 +5,15 @@ import { deleteHandler } from './requestHandlers/delete.js' import { findByIDHandler } from './requestHandlers/findOne.js' import { updateHandler } from './requestHandlers/update.js' -const preferenceAccess: Access = ({ req }) => ({ - 'user.value': { - equals: req?.user?.id, - }, -}) +const preferenceAccess: Access = ({ req }) => { + if (!req.user) return false + + return { + 'user.value': { + equals: req?.user?.id, + }, + } +} const getPreferencesCollection = (config: Config): CollectionConfig => ({ slug: 'payload-preferences', diff --git a/packages/payload/src/utilities/getEntityPolicies.ts b/packages/payload/src/utilities/getEntityPolicies.ts index 477d6a912..0a91a67bf 100644 --- a/packages/payload/src/utilities/getEntityPolicies.ts +++ b/packages/payload/src/utilities/getEntityPolicies.ts @@ -6,7 +6,7 @@ import type { SanitizedGlobalConfig } from '../globals/config/types.js' import type { AllOperations, Document, PayloadRequest, Where } from '../types/index.js' import { combineQueries } from '../database/combineQueries.js' -import { tabHasName } from '../fields/config/types.js' +import { fieldAffectsData, tabHasName } from '../fields/config/types.js' type Args = { entity: SanitizedCollectionConfig | SanitizedGlobalConfig @@ -132,6 +132,11 @@ export async function getEntityPolicies(args: T): Promise { const mutablePolicies = policiesObj.fields + // Fields don't have all operations of a collection + if (operation === 'delete' || operation === 'readVersions' || operation === 'unlock') { + return + } + await Promise.all( fields.map(async (field) => { if ('name' in field && field.name) { @@ -166,7 +171,7 @@ export async function getEntityPolicies(args: T): Promise { + it('should sanitize a basic collection', () => { + const permissions: CollectionPermission = { + fields: { + text: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + }, + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + delete: { + permission: false, + }, + readVersions: { + permission: true, + }, + } + + recursivelySanitizePermissions(permissions) + + expect(permissions).toStrictEqual({ + fields: true, + create: true, + read: true, + update: true, + readVersions: true, + }) + }) + + it('should sanitize a collection with where queries', () => { + const permissions: CollectionPermission = { + fields: {}, + create: { + permission: true, + where: { + user: { + equals: 2, + }, + }, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + delete: { + permission: false, + }, + readVersions: { + permission: true, + where: { + user: { + equals: 1, + }, + }, + }, + } + + recursivelySanitizePermissions(permissions) + + expect(permissions).toStrictEqual({ + create: { + permission: true, + where: { + user: { + equals: 2, + }, + }, + }, + read: true, + update: true, + readVersions: { + permission: true, + where: { + user: { + equals: 1, + }, + }, + }, + }) + }) + + it('should sanitize a collection with nested fields in blocks', () => { + const permissions: CollectionPermission = { + create: { + permission: true, + }, + delete: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + fields: { + layout: { + create: { + permission: true, + }, + blocks: { + blockWithTitle: { + fields: { + blockTitle: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + id: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + blockName: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + }, + }, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + }, + } + + recursivelySanitizePermissions(permissions) + + expect(permissions).toStrictEqual({ + create: true, + delete: true, + fields: true, + read: true, + update: true, + }) + }) + + it('should sanitize a collection with nested fields in blocks without truncating', () => { + const permissions: CollectionPermission = { + create: { + permission: true, + }, + delete: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + fields: { + layout: { + create: { + permission: true, + }, + blocks: { + blockWithTitle: { + fields: { + blockTitle: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + id: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + blockName: { + create: { + permission: false, + }, + read: { + permission: true, + }, + update: { + permission: false, + }, + }, + }, + }, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + }, + } + + recursivelySanitizePermissions(permissions) + + expect(permissions).toStrictEqual({ + create: true, + delete: true, + read: true, + update: true, + fields: { + layout: { + create: true, + blocks: { + blockWithTitle: { + fields: { + blockTitle: true, + id: true, + blockName: { + read: true, + }, + }, + }, + }, + read: true, + update: true, + }, + }, + }) + }) + + it('should sanitize a collection with nested fields in arrays', () => { + const permissions: Partial = { + fields: { + arrayOfText: { + create: { + permission: true, + }, + fields: { + text: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + hiddenText: { + create: { + permission: true, + }, + read: { + permission: false, + }, + update: { + permission: true, + }, + }, + id: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + }, + } + + recursivelySanitizePermissions(permissions) + + expect(permissions).toStrictEqual({ + fields: { + arrayOfText: { + create: true, + fields: { + text: true, + hiddenText: { + create: true, + update: true, + }, + id: true, + }, + read: true, + update: true, + }, + }, + }) + }) + + it('should sanitize a collection with nested fields in richText', () => { + const permissions: Partial = { + fields: { + text: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + richText: { + create: { + permission: true, + }, + read: { + permission: true, + }, + update: { + permission: true, + }, + }, + }, + } + + recursivelySanitizePermissions(permissions) + + expect(permissions).toStrictEqual({ + fields: true, + }) + }) +}) + +describe('sanitizePermissions', () => { + it('should return nothing for unauthenticated user', () => { + const permissions: Permissions = { + canAccessAdmin: false, + collections: { + 'payload-preferences': { + fields: { + user: { + create: { + permission: false, + }, + read: { + permission: false, + }, + update: { + permission: false, + }, + }, + }, + create: { + permission: false, + }, + read: { + permission: false, + }, + update: { + permission: false, + }, + delete: { + permission: false, + }, + }, + }, + globals: { + menu: { + fields: { + globalText: { + create: { + permission: false, + }, + read: { + permission: false, + }, + update: { + permission: false, + }, + }, + }, + read: { + permission: false, + }, + update: { + permission: false, + }, + }, + }, + } + + const sanitizedPermissions = sanitizePermissions(permissions) + + expect(sanitizedPermissions).toStrictEqual({}) + }) +}) diff --git a/packages/payload/src/utilities/sanitizePermissions.ts b/packages/payload/src/utilities/sanitizePermissions.ts new file mode 100644 index 000000000..474cad438 --- /dev/null +++ b/packages/payload/src/utilities/sanitizePermissions.ts @@ -0,0 +1,187 @@ +import type { Permissions, SanitizedPermissions } from '../auth/types.js' + +type PermissionObject = { + [key: string]: any +} + +/** + * Check if all permissions in a FieldPermissions object are true on the condition that no nested blocks or fields are present. + */ +function areAllPermissionsTrue(data: PermissionObject): boolean { + if (data.blocks) { + for (const key in data.blocks) { + if (typeof data.blocks[key] === 'object') { + // If any recursive call returns false, the whole function returns false + if (key === 'fields' && !areAllPermissionsTrue(data.blocks[key].fields)) { + return false + } + if (data.blocks[key].fields && !areAllPermissionsTrue(data.blocks[key].fields)) { + return false + } + } else if (data.blocks[key] !== true) { + // If any value is not true, return false + return false + } + } + // If all values are true or it's an empty object, return true + return true + } + + if (data.fields) { + for (const key in data.fields) { + if (typeof data.fields[key] === 'object') { + // If any recursive call returns false, the whole function returns false + if (!areAllPermissionsTrue(data.fields[key])) { + return false + } + } else if (data.fields[key] !== true) { + // If any value is not true, return false + return false + } + } + // If all values are true or it's an empty object, return true + return true + } + + for (const key in data) { + if (typeof data[key] === 'object') { + // If any recursive call returns false, the whole function returns false + if (!areAllPermissionsTrue(data[key])) { + return false + } + } else if (data[key] !== true) { + // If any value is not true, return false + return false + } + } + // If all values are true or it's an empty object, return true + return true +} + +/** + * Check if an object is a permission object. + */ +function isPermissionObject(data: unknown): boolean { + return typeof data === 'object' && 'permission' in data && typeof data['permission'] === 'boolean' +} + +/** + * Recursively remove empty objects from an object. + */ +function cleanEmptyObjects(obj: any): void { + Object.keys(obj).forEach((key) => { + if (typeof obj[key] === 'object' && obj[key] !== null) { + // Recursive call + cleanEmptyObjects(obj[key]) + if (Object.keys(obj[key]).length === 0) { + // Delete the key if the object is empty + delete obj[key] + } + } else if (obj[key] === null || obj[key] === undefined) { + delete obj[key] + } + }) +} + +/** + * Recursively resolve permissions in an object. + */ +export function recursivelySanitizePermissions(obj: PermissionObject): void { + if (typeof obj !== 'object') { + return + } + + const entries = Object.entries(obj) + + for (let i = 0; i < entries.length; i++) { + const [key, value] = entries[i] + // Check if it's a 'fields' key + if (key === 'fields') { + // Check if fields is empty + if (Object.keys(obj[key]).length === 0) { + delete obj[key] + continue + } + // Otherwise set fields to true if all permissions are true + else if (areAllPermissionsTrue(value)) { + obj[key] = true + continue + } + } else if (key === 'blocks') { + // Check if fields is empty + if (Object.keys(obj[key]).length === 0) { + delete obj[key] + continue + } + // Otherwise set fields to true if all permissions are true + else if (areAllPermissionsTrue(value)) { + obj[key] = true + continue + } + } + + // Check if the whole object is a permission object + const isFullPermissionObject = Object.keys(value).every( + (subKey) => + subKey !== 'blocks' && + typeof value?.[subKey] === 'object' && + 'permission' in value[subKey] && + !('where' in value[subKey]) && + typeof value[subKey]['permission'] === 'boolean', + ) + + if (isFullPermissionObject) { + if (areAllPermissionsTrue(value)) { + obj[key] = true + continue + } else { + for (const subKey in value) { + if (value[subKey]['permission'] === true && !('where' in value[subKey])) { + value[subKey] = true + continue + } else if (value[subKey]['permission'] === true && 'where' in value[subKey]) { + // do nothing + } else { + delete value[subKey] + continue + } + } + } + } else if (isPermissionObject(value)) { + if (value['permission'] === true && !('where' in value)) { + // If the permission is true and there is no where clause, set the key to true + obj[key] = true + continue + } else if (value['permission'] === true && 'where' in value) { + // otherwise do nothing so we can keep the where clause + } else { + delete obj[key] + continue + } + } else { + recursivelySanitizePermissions(value) + } + } +} + +/** + * Recursively remove empty objects and false values from an object. + */ +export function sanitizePermissions(data: Permissions): SanitizedPermissions { + if (data.canAccessAdmin === false) { + delete data.canAccessAdmin + } + + if (data.collections) { + recursivelySanitizePermissions(data.collections) + } + + if (data.globals) { + recursivelySanitizePermissions(data.globals) + } + + // Run clean up of empty objects at the end + cleanEmptyObjects(data) + + return data as unknown as SanitizedPermissions +} diff --git a/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx b/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx index 4f30ff283..559904934 100644 --- a/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx @@ -61,18 +61,15 @@ function removeUndefinedAndNullAndEmptyArraysRecursively(obj: object) { * not the whole document. */ export const BlockContent: React.FC = (props) => { - const { baseClass, clientBlock, field, formSchema, Label, nodeKey, path, schemaPath } = props + const { baseClass, clientBlock, field, formSchema, Label, nodeKey } = props let { formData } = props - const { - fieldProps: { permissions }, - } = useEditorConfigContext() const { i18n } = useTranslation() const [editor] = useLexicalComposerContext() // Used for saving collapsed to preferences (and gettin' it from there again) // Remember, these preferences are scoped to the whole document, not just this form. This // is important to consider for the data path used in setDocFieldPreferences - const { docPermissions, getDocPreferences, setDocFieldPreferences } = useDocumentInfo() + const { getDocPreferences, setDocFieldPreferences } = useDocumentInfo() const [isCollapsed, setIsCollapsed] = React.useState() @@ -232,7 +229,7 @@ export const BlockContent: React.FC = (props) => { parentIndexPath="" parentPath={''} parentSchemaPath="" - permissions={permissions} // TODO: Pass field permissions + permissions={true} /> diff --git a/packages/richtext-lexical/src/features/blocks/client/component/index.tsx b/packages/richtext-lexical/src/features/blocks/client/component/index.tsx index 4a230a2d5..af9c0dd1f 100644 --- a/packages/richtext-lexical/src/features/blocks/client/component/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/component/index.tsx @@ -68,7 +68,7 @@ export const BlockComponent: React.FC = (props) => { id, collectionSlug, data: formData, - docPermissions, + docPermissions: { fields: true }, docPreferences: await getDocPreferences(), globalSlug, operation: 'update', @@ -103,7 +103,6 @@ export const BlockComponent: React.FC = (props) => { collectionSlug, globalSlug, getDocPreferences, - docPermissions, // DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing. ]) diff --git a/packages/richtext-slate/src/field/elements/upload/Element/UploadDrawer/index.tsx b/packages/richtext-slate/src/field/elements/upload/Element/UploadDrawer/index.tsx index 21aff2e2e..e005a0215 100644 --- a/packages/richtext-slate/src/field/elements/upload/Element/UploadDrawer/index.tsx +++ b/packages/richtext-slate/src/field/elements/upload/Element/UploadDrawer/index.tsx @@ -150,7 +150,7 @@ export const UploadDrawer: React.FC<{ parentIndexPath="" parentPath="" parentSchemaPath="" - permissions={{}} + permissions={docPermissions.fields} readOnly={false} /> {t('fields:saveChanges')} diff --git a/packages/ui/src/elements/AddNewRelation/index.tsx b/packages/ui/src/elements/AddNewRelation/index.tsx index 37eb22f14..77bb80cc1 100644 --- a/packages/ui/src/elements/AddNewRelation/index.tsx +++ b/packages/ui/src/elements/AddNewRelation/index.tsx @@ -96,11 +96,11 @@ export const AddNewRelation: React.FC = ({ useEffect(() => { if (permissions) { if (relatedCollections.length === 1) { - setShow(permissions.collections[relatedCollections[0]?.slug]?.create?.permission) + setShow(permissions.collections[relatedCollections[0]?.slug]?.create) } else { setShow( relatedCollections.some( - (collection) => permissions.collections[collection?.slug]?.create?.permission, + (collection) => permissions.collections[collection?.slug]?.create, ), ) } @@ -186,7 +186,7 @@ export const AddNewRelation: React.FC = ({ render={({ close: closePopup }) => ( {relatedCollections.map((relatedCollection) => { - if (permissions.collections[relatedCollection?.slug].create.permission) { + if (permissions.collections[relatedCollection?.slug].create) { return ( = ({ )} size="medium" /> - {collectionConfig && - permissions.collections[collectionConfig?.slug]?.create?.permission && ( - - )} + {collectionConfig && permissions.collections[collectionConfig?.slug]?.create && ( + + )} )} diff --git a/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx b/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx index debdb899f..50e4cb233 100644 --- a/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx +++ b/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx @@ -1,6 +1,6 @@ 'use client' -import type { Data, DocumentPermissions, DocumentSlots, FormState } from 'payload' +import type { Data, DocumentSlots, FormState, SanitizedDocumentPermissions } from 'payload' import { useModal } from '@faceless-ui/modal' import * as qs from 'qs-esm' @@ -26,7 +26,7 @@ type FormsManagerContext = { readonly activeIndex: State['activeIndex'] readonly addFiles: (filelist: FileList) => Promise readonly collectionSlug: string - readonly docPermissions?: DocumentPermissions + readonly docPermissions?: SanitizedDocumentPermissions readonly documentSlots: DocumentSlots readonly forms: State['forms'] getFormDataRef: React.RefObject<() => Data> @@ -91,7 +91,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) { const [documentSlots, setDocumentSlots] = React.useState({}) const [hasSubmitted, setHasSubmitted] = React.useState(false) - const [docPermissions, setDocPermissions] = React.useState() + const [docPermissions, setDocPermissions] = React.useState() const [hasSavePermission, setHasSavePermission] = React.useState(false) const [hasPublishPermission, setHasPublishPermission] = React.useState(false) const [hasInitializedState, setHasInitializedState] = React.useState(false) @@ -162,7 +162,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) { method: 'post', }) - const json: DocumentPermissions = await res.json() + const json: SanitizedDocumentPermissions = await res.json() const publishedAccessJSON = await fetch( `${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, { @@ -188,7 +188,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) { }), ) - setHasPublishPermission(publishedAccessJSON?.update?.permission) + setHasPublishPermission(publishedAccessJSON?.update) setHasInitializedDocPermissions(true) }, [api, code, collectionSlug, i18n.language, serverURL]) diff --git a/packages/ui/src/elements/DeleteMany/index.tsx b/packages/ui/src/elements/DeleteMany/index.tsx index 188789d35..4df7ff834 100644 --- a/packages/ui/src/elements/DeleteMany/index.tsx +++ b/packages/ui/src/elements/DeleteMany/index.tsx @@ -44,7 +44,7 @@ export const DeleteMany: React.FC = (props) => { const { clearRouteCache } = useRouteCache() const collectionPermissions = permissions?.collections?.[slug] - const hasDeletePermission = collectionPermissions?.delete?.permission + const hasDeletePermission = collectionPermissions?.delete const modalSlug = `delete-${slug}` diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index dbefb242f..7f4339f18 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -3,9 +3,9 @@ import type { ClientCollectionConfig, ClientGlobalConfig, ClientUser, - CollectionPermission, - GlobalPermission, SanitizedCollectionConfig, + SanitizedCollectionPermission, + SanitizedGlobalPermission, } from 'payload' import { getTranslation } from '@payloadcms/translations' @@ -57,7 +57,7 @@ export const DocumentControls: React.FC<{ readonly onDuplicate?: DocumentDrawerContextType['onDuplicate'] readonly onSave?: DocumentDrawerContextType['onSave'] readonly onTakeOver?: () => void - readonly permissions: CollectionPermission | GlobalPermission | null + readonly permissions: null | SanitizedCollectionPermission | SanitizedGlobalPermission readonly readOnlyForIncomingUser?: boolean readonly redirectAfterDelete?: boolean readonly redirectAfterDuplicate?: boolean @@ -118,11 +118,9 @@ export const DocumentControls: React.FC<{ } }, [data, i18n, dateFormat]) - const hasCreatePermission = - permissions && 'create' in permissions && permissions.create?.permission + const hasCreatePermission = permissions && 'create' in permissions && permissions.create - const hasDeletePermission = - permissions && 'delete' in permissions && permissions.delete?.permission + const hasDeletePermission = permissions && 'delete' in permissions && permissions.delete const showDotMenu = Boolean( collectionConfig && id && !disableActions && (hasCreatePermission || hasDeletePermission), diff --git a/packages/ui/src/elements/DocumentFields/index.tsx b/packages/ui/src/elements/DocumentFields/index.tsx index 36d068b6a..fc73a9961 100644 --- a/packages/ui/src/elements/DocumentFields/index.tsx +++ b/packages/ui/src/elements/DocumentFields/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { ClientField, DocumentPermissions } from 'payload' +import type { ClientField, SanitizedDocumentPermissions } from 'payload' import { fieldIsSidebar } from 'payload/shared' import React from 'react' @@ -15,7 +15,7 @@ type Args = { readonly AfterFields?: React.ReactNode readonly BeforeFields?: React.ReactNode readonly Description?: React.ReactNode - readonly docPermissions: DocumentPermissions + readonly docPermissions: SanitizedDocumentPermissions readonly fields: ClientField[] readonly forceSidebarWrap?: boolean readonly readOnly?: boolean diff --git a/packages/ui/src/elements/EditMany/index.tsx b/packages/ui/src/elements/EditMany/index.tsx index b0208c42c..a765ddedc 100644 --- a/packages/ui/src/elements/EditMany/index.tsx +++ b/packages/ui/src/elements/EditMany/index.tsx @@ -132,7 +132,7 @@ export const EditMany: React.FC = (props) => { const { clearRouteCache } = useRouteCache() const collectionPermissions = permissions?.collections?.[slug] - const hasUpdatePermission = collectionPermissions?.update?.permission + const hasUpdatePermission = collectionPermissions?.update const drawerSlug = `edit-${slug}` @@ -261,7 +261,7 @@ export const EditMany: React.FC = (props) => { parentIndexPath="" parentPath="" parentSchemaPath={slug} - permissions={permissions?.collections?.[slug]?.fields} + permissions={collectionPermissions?.fields} readOnly={false} /> )} diff --git a/packages/ui/src/elements/EmailAndUsername/index.tsx b/packages/ui/src/elements/EmailAndUsername/index.tsx index 57e8148d7..d29fb7229 100644 --- a/packages/ui/src/elements/EmailAndUsername/index.tsx +++ b/packages/ui/src/elements/EmailAndUsername/index.tsx @@ -1,10 +1,10 @@ 'use client' import type { TFunction } from '@payloadcms/translations' -import type { FieldPermissions, LoginWithUsernameOptions } from 'payload' +import type { LoginWithUsernameOptions, SanitizedFieldPermissions } from 'payload' import { email, username } from 'payload/shared' -import React, { Fragment } from 'react' +import React from 'react' import { EmailField } from '../../fields/Email/index.js' import { TextField } from '../../fields/Text/index.js' @@ -13,9 +13,11 @@ type RenderEmailAndUsernameFieldsProps = { className?: string loginWithUsername?: false | LoginWithUsernameOptions operation?: 'create' | 'update' - permissions?: { - [fieldName: string]: FieldPermissions - } + permissions?: + | { + [fieldName: string]: SanitizedFieldPermissions + } + | true readOnly: boolean t: TFunction } diff --git a/packages/ui/src/elements/HydrateAuthProvider/index.tsx b/packages/ui/src/elements/HydrateAuthProvider/index.tsx index 9967e7c43..378eefddd 100644 --- a/packages/ui/src/elements/HydrateAuthProvider/index.tsx +++ b/packages/ui/src/elements/HydrateAuthProvider/index.tsx @@ -1,6 +1,6 @@ 'use client' -import type { Permissions } from 'payload' +import type { SanitizedPermissions } from 'payload' import { useEffect } from 'react' @@ -14,8 +14,9 @@ import { useAuth } from '../../providers/Auth/index.js' */ type Props = { - permissions: Permissions + permissions: SanitizedPermissions } + export function HydrateAuthProvider({ permissions }: Props) { const { setPermissions } = useAuth() diff --git a/packages/ui/src/elements/PublishMany/index.tsx b/packages/ui/src/elements/PublishMany/index.tsx index 7e90e605d..0e532fe0b 100644 --- a/packages/ui/src/elements/PublishMany/index.tsx +++ b/packages/ui/src/elements/PublishMany/index.tsx @@ -44,7 +44,7 @@ export const PublishMany: React.FC = (props) => { const { stringifyParams } = useSearchParams() const collectionPermissions = permissions?.collections?.[slug] - const hasPermission = collectionPermissions?.update?.permission + const hasPermission = collectionPermissions?.update const modalSlug = `publish-${slug}` diff --git a/packages/ui/src/elements/RelationshipTable/index.tsx b/packages/ui/src/elements/RelationshipTable/index.tsx index 1a98039c8..2f3b24723 100644 --- a/packages/ui/src/elements/RelationshipTable/index.tsx +++ b/packages/ui/src/elements/RelationshipTable/index.tsx @@ -172,8 +172,7 @@ export const RelationshipTable: React.FC = (pro const preferenceKey = `${relationTo}-list` - const canCreate = - allowCreate !== false && permissions?.collections?.[relationTo]?.create?.permission + const canCreate = allowCreate !== false && permissions?.collections?.[relationTo]?.create return (

diff --git a/packages/ui/src/elements/Status/index.tsx b/packages/ui/src/elements/Status/index.tsx index ff1bb1aac..eea5c4866 100644 --- a/packages/ui/src/elements/Status/index.tsx +++ b/packages/ui/src/elements/Status/index.tsx @@ -138,7 +138,7 @@ export const Status: React.FC = () => { ], ) - const canUpdate = docPermissions?.update?.permission + const canUpdate = docPermissions?.update if (statusToRender) { return ( diff --git a/packages/ui/src/elements/UnpublishMany/index.tsx b/packages/ui/src/elements/UnpublishMany/index.tsx index 4bfc0a237..27428213c 100644 --- a/packages/ui/src/elements/UnpublishMany/index.tsx +++ b/packages/ui/src/elements/UnpublishMany/index.tsx @@ -45,7 +45,7 @@ export const UnpublishMany: React.FC = (props) => { const { clearRouteCache } = useRouteCache() const collectionPermissions = permissions?.collections?.[slug] - const hasPermission = collectionPermissions?.update?.permission + const hasPermission = collectionPermissions?.update const modalSlug = `unpublish-${slug}` diff --git a/packages/ui/src/elements/Upload/index.tsx b/packages/ui/src/elements/Upload/index.tsx index 571e92d33..e96d46d5b 100644 --- a/packages/ui/src/elements/Upload/index.tsx +++ b/packages/ui/src/elements/Upload/index.tsx @@ -206,9 +206,7 @@ export const Upload: React.FC = (props) => { }, [showUrlInput]) const canRemoveUpload = - docPermissions?.update?.permission && - 'delete' in docPermissions && - docPermissions?.delete?.permission + docPermissions?.update && 'delete' in docPermissions && docPermissions?.delete const hasImageSizes = uploadConfig?.imageSizes?.length > 0 const hasResizeOptions = Boolean(uploadConfig?.resizeOptions) diff --git a/packages/ui/src/fields/Array/ArrayRow.tsx b/packages/ui/src/fields/Array/ArrayRow.tsx index 351e854bb..834f68ce8 100644 --- a/packages/ui/src/fields/Array/ArrayRow.tsx +++ b/packages/ui/src/fields/Array/ArrayRow.tsx @@ -1,5 +1,5 @@ 'use client' -import type { ArrayField, ClientField, FieldPermissions, Row } from 'payload' +import type { ArrayField, ClientField, Row, SanitizedFieldPermissions } from 'payload' import { getTranslation } from '@payloadcms/translations' import React from 'react' @@ -30,7 +30,7 @@ type ArrayRowProps = { readonly moveRow: (fromIndex: number, toIndex: number) => void readonly parentPath: string readonly path: string - readonly permissions: FieldPermissions + readonly permissions: SanitizedFieldPermissions readonly readOnly?: boolean readonly removeRow: (rowIndex: number) => void readonly row: Row @@ -144,7 +144,7 @@ export const ArrayRow: React.FC = ({ parentIndexPath="" parentPath={path} parentSchemaPath={schemaPath} - permissions={permissions?.fields} + permissions={permissions === true ? permissions : permissions?.fields} readOnly={readOnly} /> diff --git a/packages/ui/src/fields/Blocks/BlockRow.tsx b/packages/ui/src/fields/Blocks/BlockRow.tsx index f5bade67e..a553376d1 100644 --- a/packages/ui/src/fields/Blocks/BlockRow.tsx +++ b/packages/ui/src/fields/Blocks/BlockRow.tsx @@ -1,5 +1,5 @@ 'use client' -import type { ClientBlock, ClientField, FieldPermissions, Labels, Row } from 'payload' +import type { ClientBlock, ClientField, Labels, Row, SanitizedFieldPermissions } from 'payload' import { getTranslation } from '@payloadcms/translations' import React from 'react' @@ -31,7 +31,7 @@ type BlocksFieldProps = { moveRow: (fromIndex: number, toIndex: number) => void parentPath: string path: string - permissions: FieldPermissions + permissions: SanitizedFieldPermissions readOnly: boolean removeRow: (rowIndex: number) => void row: Row @@ -147,7 +147,9 @@ export const BlockRow: React.FC = ({ parentIndexPath="" parentPath={path} parentSchemaPath={schemaPath} - permissions={permissions?.blocks?.[block.slug]?.fields} + permissions={ + permissions === true ? permissions : permissions?.blocks?.[block.slug]?.fields + } readOnly={readOnly} /> diff --git a/packages/ui/src/fields/Group/index.tsx b/packages/ui/src/fields/Group/index.tsx index 8a84ae6ca..5760d46cf 100644 --- a/packages/ui/src/fields/Group/index.tsx +++ b/packages/ui/src/fields/Group/index.tsx @@ -104,7 +104,7 @@ export const GroupFieldComponent: GroupFieldClientComponent = (props) => { parentIndexPath="" parentPath={path} parentSchemaPath={schemaPath} - permissions={permissions?.fields} + permissions={permissions === true ? permissions : permissions?.fields} readOnly={readOnly} />
diff --git a/packages/ui/src/fields/Relationship/select-components/MultiValueLabel/index.tsx b/packages/ui/src/fields/Relationship/select-components/MultiValueLabel/index.tsx index 6bd4eb575..03cb247a5 100644 --- a/packages/ui/src/fields/Relationship/select-components/MultiValueLabel/index.tsx +++ b/packages/ui/src/fields/Relationship/select-components/MultiValueLabel/index.tsx @@ -31,7 +31,7 @@ export const MultiValueLabel: React.FC< const { permissions } = useAuth() const [showTooltip, setShowTooltip] = useState(false) const { t } = useTranslation() - const hasReadPermission = Boolean(permissions?.collections?.[relationTo]?.read?.permission) + const hasReadPermission = Boolean(permissions?.collections?.[relationTo]?.read) return (
diff --git a/packages/ui/src/fields/Relationship/select-components/SingleValue/index.tsx b/packages/ui/src/fields/Relationship/select-components/SingleValue/index.tsx index 29c59f80a..0741da61b 100644 --- a/packages/ui/src/fields/Relationship/select-components/SingleValue/index.tsx +++ b/packages/ui/src/fields/Relationship/select-components/SingleValue/index.tsx @@ -32,7 +32,7 @@ export const SingleValue: React.FC< const [showTooltip, setShowTooltip] = useState(false) const { t } = useTranslation() const { permissions } = useAuth() - const hasReadPermission = Boolean(permissions?.collections?.[relationTo]?.read?.permission) + const hasReadPermission = Boolean(permissions?.collections?.[relationTo]?.read) return ( diff --git a/packages/ui/src/fields/Tabs/index.tsx b/packages/ui/src/fields/Tabs/index.tsx index ccbeb5893..9a6ee3cd9 100644 --- a/packages/ui/src/fields/Tabs/index.tsx +++ b/packages/ui/src/fields/Tabs/index.tsx @@ -3,7 +3,7 @@ import type { ClientField, ClientTab, DocumentPreferences, - FieldPermissions, + SanitizedFieldPermissions, StaticDescription, TabsFieldClientComponent, } from 'payload' @@ -222,7 +222,7 @@ type ActiveTabProps = { parentPath: string parentSchemaPath: string path: string - permissions: FieldPermissions + permissions: SanitizedFieldPermissions readOnly: boolean } function ActiveTabContent({ diff --git a/packages/ui/src/fields/Upload/Input.tsx b/packages/ui/src/fields/Upload/Input.tsx index 02776cc7d..5f3c12047 100644 --- a/packages/ui/src/fields/Upload/Input.tsx +++ b/packages/ui/src/fields/Upload/Input.tsx @@ -164,9 +164,7 @@ export function UploadInput(props: UploadInputProps) { if (typeof activeRelationTo === 'string') { if (permissions?.collections && permissions.collections?.[activeRelationTo]?.create) { - if (permissions.collections[activeRelationTo].create?.permission === true) { - return true - } + return true } } diff --git a/packages/ui/src/forms/RenderFields/RenderField.tsx b/packages/ui/src/forms/RenderFields/RenderField.tsx index 4d2071d9f..8b485786c 100644 --- a/packages/ui/src/forms/RenderFields/RenderField.tsx +++ b/packages/ui/src/forms/RenderFields/RenderField.tsx @@ -1,6 +1,11 @@ 'use client' -import type { ClientComponentProps, ClientField, FieldPaths, FieldPermissions } from 'payload' +import type { + ClientComponentProps, + ClientField, + FieldPaths, + SanitizedFieldPermissions, +} from 'payload' import React from 'react' @@ -31,7 +36,7 @@ import { useFormFields } from '../../forms/Form/index.js' type RenderFieldProps = { clientFieldConfig: ClientField - permissions: FieldPermissions + permissions: SanitizedFieldPermissions } & FieldPaths & Pick diff --git a/packages/ui/src/forms/RenderFields/index.tsx b/packages/ui/src/forms/RenderFields/index.tsx index ec53803c4..93922471a 100644 --- a/packages/ui/src/forms/RenderFields/index.tsx +++ b/packages/ui/src/forms/RenderFields/index.tsx @@ -1,7 +1,5 @@ 'use client' -import type { FieldPermissions } from 'payload' - import { getFieldPaths } from 'payload/shared' import React from 'react' @@ -51,18 +49,17 @@ export const RenderFields: React.FC = (props) => { return null } - const fieldPermissions: FieldPermissions = - 'name' in field ? permissions?.[field.name] : permissions - // If the user cannot read the field, then filter it out // This is different from `admin.readOnly` which is executed based on `operation` - const lacksReadPermission = - fieldPermissions && - 'read' in fieldPermissions && - 'permission' in fieldPermissions.read && - fieldPermissions?.read?.permission === false + const hasReadPermission = + permissions === true || + ('name' in field && + typeof permissions === 'object' && + permissions?.[field.name] && + (permissions[field.name] === true || + ('read' in permissions[field.name] && permissions[field.name].read))) - if (lacksReadPermission) { + if ('name' in field && !hasReadPermission) { return null } @@ -75,13 +72,15 @@ export const RenderFields: React.FC = (props) => { } // If the user does not have access control to begin with, force it to be read-only - const lacksOperationPermission = - fieldPermissions && - operation in fieldPermissions && - 'permission' in fieldPermissions[operation] && - fieldPermissions[operation]?.permission === false + const hasOperationPermission = + permissions === true || + ('name' in field && + typeof permissions === 'object' && + permissions?.[field.name] && + (permissions[field.name] === true || + (operation in permissions[field.name] && permissions[field.name][operation]))) - if (lacksOperationPermission) { + if ('name' in field && !hasOperationPermission) { isReadOnly = true } @@ -102,7 +101,13 @@ export const RenderFields: React.FC = (props) => { parentPath={parentPath} parentSchemaPath={parentSchemaPath} path={path} - permissions={fieldPermissions} + permissions={ + permissions === null || permissions === true + ? true + : 'name' in field + ? permissions?.[field.name] + : permissions + } readOnly={isReadOnly} schemaPath={schemaPath} /> diff --git a/packages/ui/src/forms/RenderFields/types.ts b/packages/ui/src/forms/RenderFields/types.ts index deead9d3e..f2df60381 100644 --- a/packages/ui/src/forms/RenderFields/types.ts +++ b/packages/ui/src/forms/RenderFields/types.ts @@ -1,4 +1,4 @@ -import type { ClientField, FieldPermissions } from 'payload' +import type { ClientField, SanitizedFieldPermissions } from 'payload' export type Props = { readonly className?: string @@ -17,9 +17,8 @@ export type Props = { readonly parentSchemaPath: string readonly permissions: | { - [fieldName: string]: FieldPermissions + [fieldName: string]: SanitizedFieldPermissions } - | FieldPermissions - | null + | SanitizedFieldPermissions readonly readOnly?: boolean } diff --git a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts index 1b99bcbbe..4e7666d2f 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts @@ -1,6 +1,5 @@ import type { Data, - DocumentPermissions, DocumentPreferences, Field, FieldSchemaMap, @@ -8,6 +7,7 @@ import type { FormState, FormStateWithoutComponents, PayloadRequest, + SanitizedFieldPermissions, } from 'payload' import ObjectIdImport from 'bson-objectid' @@ -61,7 +61,12 @@ export type AddFieldStatePromiseArgs = { parentPath: string parentSchemaPath: string passesCondition: boolean - permissions: DocumentPermissions['fields'] + permissions: + | { + [fieldName: string]: SanitizedFieldPermissions + } + | null + | SanitizedFieldPermissions preferences: DocumentPreferences previousFormState: FormState renderAllFields: boolean @@ -131,8 +136,9 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom const disabledFromAdmin = field?.admin && 'disabled' in field.admin && field.admin.disabled if (fieldAffectsData(field) && !(isHiddenField || disabledFromAdmin)) { - let hasPermission = - typeof permissions?.[field.name]?.read === 'boolean' ? permissions[field.name].read : true + const fieldPermissions = permissions[field.name] + + let hasPermission: boolean = fieldPermissions === true || fieldPermissions?.read if (typeof field?.access?.read === 'function') { hasPermission = await field.access.read({ doc: fullData, req, siblingData: data }) @@ -243,7 +249,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom parentPassesCondition: passesCondition, parentPath, parentSchemaPath: schemaPath, - permissions: permissions?.[field.name]?.fields || {}, + permissions: + fieldPermissions === true ? fieldPermissions : fieldPermissions?.fields || {}, preferences, previousFormState, renderAllFields: requiresRender, diff --git a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx index 16caca0c2..7c5d983af 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx @@ -1,12 +1,12 @@ import type { Data, - DocumentPermissions, DocumentPreferences, Field, FieldSchemaMap, FormState, FormStateWithoutComponents, PayloadRequest, + SanitizedDocumentPermissions, } from 'payload' import type { RenderFieldMethod } from './types.js' @@ -26,7 +26,7 @@ type Args = { fieldSchemaMap: FieldSchemaMap | undefined id?: number | string operation?: 'create' | 'update' - permissions: DocumentPermissions['fields'] + permissions: SanitizedDocumentPermissions['fields'] preferences: DocumentPreferences /** * Optionally accept the previous form state, diff --git a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts index 0933d0659..5a25d1f4f 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts @@ -1,12 +1,12 @@ import type { Data, - DocumentPermissions, DocumentPreferences, Field as FieldSchema, FieldSchemaMap, FormState, FormStateWithoutComponents, PayloadRequest, + SanitizedFieldPermissions, } from 'payload' import type { AddFieldStatePromiseArgs } from './addFieldStatePromise.js' @@ -47,7 +47,12 @@ type Args = { parentPassesCondition?: boolean parentPath: string parentSchemaPath: string - permissions: DocumentPermissions['fields'] + permissions: + | { + [fieldName: string]: SanitizedFieldPermissions + } + | null + | SanitizedFieldPermissions preferences?: DocumentPreferences previousFormState: FormState renderAllFields: boolean diff --git a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx index cb6c04d7f..16935173e 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx @@ -2,8 +2,8 @@ import type { ClientComponentProps, ClientField, FieldPaths, - FieldPermissions, PayloadComponent, + SanitizedFieldPermissions, ServerComponentProps, } from 'payload' @@ -42,15 +42,18 @@ export const renderField: RenderFieldMethod = ({ i18n: req.i18n, }) - const permissions = fieldAffectsData(fieldConfig) - ? incomingPermissions?.[fieldConfig.name] - : ({} as FieldPermissions) + const permissions = + incomingPermissions === true + ? true + : fieldAffectsData(fieldConfig) + ? incomingPermissions?.[fieldConfig.name] + : ({} as SanitizedFieldPermissions) const clientProps: ClientComponentProps & Partial = { customComponents: fieldState?.customComponents || {}, field: clientField, path, - readOnly: permissions?.[operation]?.permission === false, + readOnly: permissions !== true && !permissions?.[operation], schemaPath, } diff --git a/packages/ui/src/forms/fieldSchemasToFormState/types.ts b/packages/ui/src/forms/fieldSchemasToFormState/types.ts index fb9d0b940..7388f56f1 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/types.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/types.ts @@ -1,12 +1,12 @@ import type { Data, - DocumentPermissions, Field, FieldSchemaMap, FieldState, FormState, Operation, PayloadRequest, + SanitizedFieldPermissions, } from 'payload' export type RenderFieldArgs = { @@ -20,7 +20,12 @@ export type RenderFieldArgs = { parentPath: string parentSchemaPath: string path: string - permissions: DocumentPermissions['fields'] + permissions: + | { + [fieldName: string]: SanitizedFieldPermissions + } + | null + | SanitizedFieldPermissions previousFieldState: FieldState req: PayloadRequest schemaPath: string diff --git a/packages/ui/src/providers/Auth/index.tsx b/packages/ui/src/providers/Auth/index.tsx index 8e04ad979..1b5e6b60a 100644 --- a/packages/ui/src/providers/Auth/index.tsx +++ b/packages/ui/src/providers/Auth/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { ClientUser, Permissions, User } from 'payload' +import type { ClientUser, SanitizedPermissions, User } from 'payload' import { useModal } from '@faceless-ui/modal' import { usePathname, useRouter } from 'next/navigation.js' @@ -23,11 +23,11 @@ export type UserWithToken = { export type AuthContext = { fetchFullUser: () => Promise logOut: () => Promise - permissions?: Permissions + permissions?: SanitizedPermissions refreshCookie: (forceRefresh?: boolean) => void refreshCookieAsync: () => Promise refreshPermissions: () => Promise - setPermissions: (permissions: Permissions) => void + setPermissions: (permissions: SanitizedPermissions) => void setUser: (user: null | UserWithToken) => void strategy?: string token?: string @@ -41,9 +41,10 @@ const maxTimeoutTime = 2147483647 type Props = { children: React.ReactNode - permissions?: Permissions + permissions?: SanitizedPermissions user?: ClientUser | null } + export function AuthProvider({ children, permissions: initialPermissions, @@ -66,7 +67,7 @@ export function AuthProvider({ serverURL, } = config - const [permissions, setPermissions] = useState(initialPermissions) + const [permissions, setPermissions] = useState(initialPermissions) const { i18n } = useTranslation() const { closeAllModals, openModal } = useModal() @@ -224,7 +225,7 @@ export function AuthProvider({ }) if (request.status === 200) { - const json: Permissions = await request.json() + const json: SanitizedPermissions = await request.json() setPermissions(json) } else { throw new Error(`Fetching permissions failed with status code ${request.status}`) diff --git a/packages/ui/src/providers/DocumentInfo/index.tsx b/packages/ui/src/providers/DocumentInfo/index.tsx index 64f3e6458..fe717cc92 100644 --- a/packages/ui/src/providers/DocumentInfo/index.tsx +++ b/packages/ui/src/providers/DocumentInfo/index.tsx @@ -3,8 +3,8 @@ import type { ClientCollectionConfig, ClientGlobalConfig, ClientUser, - DocumentPermissions, DocumentPreferences, + SanitizedDocumentPermissions, } from 'payload' import * as qs from 'qs-esm' @@ -51,7 +51,8 @@ const DocumentInfo: React.FC< versionCount: versionCountFromProps, } = props - const [docPermissions, setDocPermissions] = useState(docPermissionsFromProps) + const [docPermissions, setDocPermissions] = + useState(docPermissionsFromProps) const [hasSavePermission, setHasSavePermission] = useState(hasSavePermissionFromProps) diff --git a/packages/ui/src/providers/DocumentInfo/types.ts b/packages/ui/src/providers/DocumentInfo/types.ts index b7e9e034f..9116b70ea 100644 --- a/packages/ui/src/providers/DocumentInfo/types.ts +++ b/packages/ui/src/providers/DocumentInfo/types.ts @@ -3,11 +3,11 @@ import type { ClientGlobalConfig, ClientUser, Data, - DocumentPermissions, DocumentPreferences, FormState, InsideFieldsPreferences, SanitizedCollectionConfig, + SanitizedDocumentPermissions, SanitizedGlobalConfig, TypedUser, } from 'payload' @@ -24,7 +24,7 @@ export type DocumentInfoProps = { readonly disableActions?: boolean readonly disableCreate?: boolean readonly disableLeaveWithoutSaving?: boolean - readonly docPermissions?: DocumentPermissions + readonly docPermissions?: SanitizedDocumentPermissions readonly globalSlug?: SanitizedGlobalConfig['slug'] readonly hasPublishedDoc: boolean readonly hasPublishPermission?: boolean diff --git a/packages/ui/src/providers/DocumentInfo/useGetDocPermissions.tsx b/packages/ui/src/providers/DocumentInfo/useGetDocPermissions.tsx index 6706a9766..14fdfbed3 100644 --- a/packages/ui/src/providers/DocumentInfo/useGetDocPermissions.tsx +++ b/packages/ui/src/providers/DocumentInfo/useGetDocPermissions.tsx @@ -1,4 +1,4 @@ -import type { Data, DocumentPermissions, Permissions } from 'payload' +import type { Data, SanitizedDocumentPermissions, SanitizedPermissions } from 'payload' import * as qs from 'qs-esm' import React from 'react' @@ -25,9 +25,9 @@ export const useGetDocPermissions = ({ i18n: any id: string locale: string - permissions: Permissions + permissions: SanitizedPermissions serverURL: string - setDocPermissions: React.Dispatch> + setDocPermissions: React.Dispatch> setHasPublishPermission: React.Dispatch> setHasSavePermission: React.Dispatch> }) => @@ -61,7 +61,7 @@ export const useGetDocPermissions = ({ method: 'post', }) - const json: DocumentPermissions = await res.json() + const json: SanitizedDocumentPermissions = await res.json() const publishedAccessJSON = await fetch( `${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, @@ -90,7 +90,7 @@ export const useGetDocPermissions = ({ }), ) - setHasPublishPermission(publishedAccessJSON?.update?.permission) + setHasPublishPermission(publishedAccessJSON?.update) } } else { // when creating new documents, there is no permissions saved for this document yet diff --git a/packages/ui/src/providers/Root/index.tsx b/packages/ui/src/providers/Root/index.tsx index 0a6e56d54..62a680a99 100644 --- a/packages/ui/src/providers/Root/index.tsx +++ b/packages/ui/src/providers/Root/index.tsx @@ -3,7 +3,7 @@ import type { I18nClient, Language } from '@payloadcms/translations' import type { ClientConfig, LanguageOptions, - Permissions, + SanitizedPermissions, ServerFunctionClient, User, } from 'payload' @@ -41,7 +41,7 @@ type Props = { readonly isNavOpen?: boolean readonly languageCode: string readonly languageOptions: LanguageOptions - readonly permissions: Permissions + readonly permissions: SanitizedPermissions readonly serverFunction: ServerFunctionClient readonly switchLanguageServerAction?: (lang: string) => Promise readonly theme: Theme diff --git a/packages/ui/src/utilities/groupNavItems.ts b/packages/ui/src/utilities/groupNavItems.ts index 6f52b3d02..863223d86 100644 --- a/packages/ui/src/utilities/groupNavItems.ts +++ b/packages/ui/src/utilities/groupNavItems.ts @@ -1,8 +1,8 @@ import type { I18nClient } from '@payloadcms/translations' import type { - Permissions, SanitizedCollectionConfig, SanitizedGlobalConfig, + SanitizedPermissions, StaticLabel, } from 'payload' @@ -34,15 +34,12 @@ export type NavGroupType = { export function groupNavItems( entities: EntityToGroup[], - permissions: Permissions, + permissions: SanitizedPermissions, i18n: I18nClient, ): NavGroupType[] { const result = entities.reduce( (groups, entityToGroup) => { - if ( - permissions?.[entityToGroup.type.toLowerCase()]?.[entityToGroup.entity.slug]?.read - .permission - ) { + if (permissions?.[entityToGroup.type.toLowerCase()]?.[entityToGroup.entity.slug]?.read) { const translatedGroup = getTranslation(entityToGroup.entity.admin.group, i18n) if (entityToGroup.entity.admin.group) { diff --git a/packages/ui/src/utilities/hasSavePermission.ts b/packages/ui/src/utilities/hasSavePermission.ts index 14d53c997..d89f66313 100644 --- a/packages/ui/src/utilities/hasSavePermission.ts +++ b/packages/ui/src/utilities/hasSavePermission.ts @@ -1,11 +1,15 @@ -import type { CollectionPermission, DocumentPermissions, GlobalPermission } from 'payload' +import type { + SanitizedCollectionPermission, + SanitizedDocumentPermissions, + SanitizedGlobalPermission, +} from 'payload' export const hasSavePermission = (args: { /* * Pass either `collectionSlug` or `globalSlug` */ collectionSlug?: string - docPermissions: DocumentPermissions + docPermissions: SanitizedDocumentPermissions /* * Pass either `collectionSlug` or `globalSlug` */ @@ -16,13 +20,13 @@ export const hasSavePermission = (args: { if (collectionSlug) { return Boolean( - (isEditing && docPermissions?.update?.permission) || - (!isEditing && (docPermissions as CollectionPermission)?.create?.permission), + (isEditing && docPermissions?.update) || + (!isEditing && (docPermissions as SanitizedCollectionPermission)?.create), ) } if (globalSlug) { - return Boolean((docPermissions as GlobalPermission)?.update?.permission) + return Boolean((docPermissions as SanitizedGlobalPermission)?.update) } return false diff --git a/packages/ui/src/views/Edit/Auth/index.tsx b/packages/ui/src/views/Edit/Auth/index.tsx index befa160d3..dd32abc54 100644 --- a/packages/ui/src/views/Edit/Auth/index.tsx +++ b/packages/ui/src/views/Edit/Auth/index.tsx @@ -57,24 +57,25 @@ export const Auth: React.FC = (props) => { const collection = permissions?.collections?.[collectionSlug] if (collection) { - const unlock = 'unlock' in collection ? collection.unlock : undefined - - if (unlock) { - // current types for permissions do not include auth permissions, this will be fixed in another branch soon, for now we need to ignore the types - // @todo: fix types - // @ts-expect-error - return unlock.permission - } + return Boolean('unlock' in collection ? collection.unlock : undefined) } return false }, [permissions, collectionSlug]) - const apiKeyReadOnly = readOnly || !docPermissions?.fields?.apiKey?.update?.permission - const enableAPIKeyReadOnly = readOnly || !docPermissions?.fields?.enableAPIKey?.update?.permission + const apiKeyPermissions = + docPermissions?.fields === true ? true : docPermissions?.fields?.enableAPIKey - const canReadApiKey = docPermissions?.fields?.apiKey?.read?.permission - const canReadEnableAPIKey = docPermissions?.fields?.enableAPIKey?.read?.permission + const apiKeyReadOnly = + readOnly || + apiKeyPermissions === true || + (apiKeyPermissions && typeof apiKeyPermissions === 'object' && !apiKeyPermissions?.update) + + const enableAPIKeyReadOnly = + readOnly || (apiKeyPermissions !== true && !apiKeyPermissions?.update) + + const canReadApiKey = apiKeyPermissions === true || apiKeyPermissions?.read + const canReadEnableAPIKey = apiKeyPermissions === true || apiKeyPermissions?.read const handleChangePassword = useCallback( (showPasswordFields: boolean) => { diff --git a/packages/ui/src/views/List/types.ts b/packages/ui/src/views/List/types.ts index e65489100..a2d6e9865 100644 --- a/packages/ui/src/views/List/types.ts +++ b/packages/ui/src/views/List/types.ts @@ -3,8 +3,8 @@ import type { AdminViewProps, Locale, Payload, - Permissions, SanitizedCollectionConfig, + SanitizedPermissions, User, } from 'payload' @@ -38,7 +38,7 @@ export type ListComponentServerProps = { locale: Locale params: AdminViewProps['params'] payload: Payload - permissions: Permissions + permissions: SanitizedPermissions searchParams: AdminViewProps['searchParams'] user: User }