feat: sanitise access endpoint (#7335)
Protects the `/api/access` endpoint behind authentication and sanitizes the result, making it more secure and significantly smaller. To do this: 1. The `permission` keyword is completely omitted from the result 2. Only _truthy_ access results are returned 3. All nested permissions are consolidated when possible --------- Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com> Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com> Co-authored-by: James <james@trbl.design>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
||||
|
||||
@@ -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<any>
|
||||
navGroups?: ReturnType<typeof groupNavItems>
|
||||
permissions: Permissions
|
||||
permissions: SanitizedPermissions
|
||||
visibleEntities: VisibleEntities
|
||||
} & ServerProps
|
||||
|
||||
@@ -94,7 +94,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
path: `/collections/${slug}/create`,
|
||||
})
|
||||
|
||||
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
|
||||
hasCreatePermission = permissions?.collections?.[slug]?.create
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
|
||||
@@ -35,14 +35,13 @@ export const Dashboard: React.FC<AdminViewProps> = 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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -65,7 +65,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<TField extends ClientField = ClientField> = {
|
||||
readonly isRichText?: boolean
|
||||
readonly locale?: string
|
||||
readonly locales?: string[]
|
||||
readonly permissions?: Record<string, FieldPermissions>
|
||||
readonly permissions?:
|
||||
| {
|
||||
[key: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| true
|
||||
readonly version: any
|
||||
}
|
||||
|
||||
@@ -50,11 +50,13 @@ const RenderFieldsToDiff: React.FC<Props> = ({
|
||||
? 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, any>
|
||||
readonly diffComponents: DiffComponents
|
||||
readonly fieldPermissions: Record<string, FieldPermissions>
|
||||
readonly fieldPermissions:
|
||||
| {
|
||||
[key: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| true
|
||||
readonly fields: ClientField[]
|
||||
readonly i18n: I18nClient
|
||||
readonly locales: string[]
|
||||
|
||||
@@ -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<EditViewComponent> = async
|
||||
|
||||
const { localization } = config
|
||||
|
||||
let docPermissions: CollectionPermission | GlobalPermission
|
||||
let docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
|
||||
let slug: string
|
||||
|
||||
let doc: Document
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Permissions> {
|
||||
export async function getAccessResults({
|
||||
req,
|
||||
}: GetAccessResultsArgs): Promise<SanitizedPermissions> {
|
||||
const results = {} as Permissions
|
||||
const { payload, user } = req
|
||||
|
||||
@@ -74,5 +77,5 @@ export async function getAccessResults({ req }: GetAccessResultsArgs): Promise<P
|
||||
}),
|
||||
)
|
||||
|
||||
return results
|
||||
return sanitizePermissions(results)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { Permissions } from '../types.js'
|
||||
import type { SanitizedPermissions } from '../types.js'
|
||||
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit.js'
|
||||
@@ -9,7 +9,7 @@ type Arguments = {
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
export const accessOperation = async (args: Arguments): Promise<Permissions> => {
|
||||
export const accessOperation = async (args: Arguments): Promise<SanitizedPermissions> => {
|
||||
const { req } = args
|
||||
|
||||
adminInitTelemetry(req)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<T extends Args>(args: T): Promise<Return
|
||||
}) => {
|
||||
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<T extends Args>(args: T): Promise<Return
|
||||
})
|
||||
}
|
||||
|
||||
if ('blocks' in field && field?.blocks) {
|
||||
if ('blocks' in field && field.blocks) {
|
||||
if (!mutablePolicies[field.name]?.blocks) {
|
||||
mutablePolicies[field.name].blocks = {}
|
||||
}
|
||||
|
||||
450
packages/payload/src/utilities/sanitizePermissions.spec.ts
Normal file
450
packages/payload/src/utilities/sanitizePermissions.spec.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import type { CollectionPermission, Permissions } from '../auth/types.js'
|
||||
|
||||
import { recursivelySanitizePermissions, sanitizePermissions } from './sanitizePermissions.js'
|
||||
|
||||
/* eslint-disable perfectionist/sort-objects */
|
||||
describe('recursivelySanitizePermissions', () => {
|
||||
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<CollectionPermission> = {
|
||||
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<CollectionPermission> = {
|
||||
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({})
|
||||
})
|
||||
})
|
||||
187
packages/payload/src/utilities/sanitizePermissions.ts
Normal file
187
packages/payload/src/utilities/sanitizePermissions.ts
Normal file
@@ -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
|
||||
}
|
||||
@@ -61,18 +61,15 @@ function removeUndefinedAndNullAndEmptyArraysRecursively(obj: object) {
|
||||
* not the whole document.
|
||||
*/
|
||||
export const BlockContent: React.FC<Props> = (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<boolean>()
|
||||
|
||||
@@ -232,7 +229,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
parentIndexPath=""
|
||||
parentPath={''}
|
||||
parentSchemaPath=""
|
||||
permissions={permissions} // TODO: Pass field permissions
|
||||
permissions={true}
|
||||
/>
|
||||
</Collapsible>
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export const BlockComponent: React.FC<Props> = (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> = (props) => {
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
docPermissions,
|
||||
// DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing.
|
||||
])
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ export const UploadDrawer: React.FC<{
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath=""
|
||||
permissions={{}}
|
||||
permissions={docPermissions.fields}
|
||||
readOnly={false}
|
||||
/>
|
||||
<FormSubmit>{t('fields:saveChanges')}</FormSubmit>
|
||||
|
||||
@@ -96,11 +96,11 @@ export const AddNewRelation: React.FC<Props> = ({
|
||||
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<Props> = ({
|
||||
render={({ close: closePopup }) => (
|
||||
<PopupList.ButtonGroup>
|
||||
{relatedCollections.map((relatedCollection) => {
|
||||
if (permissions.collections[relatedCollection?.slug].create.permission) {
|
||||
if (permissions.collections[relatedCollection?.slug].create) {
|
||||
return (
|
||||
<PopupList.Button
|
||||
className={`${baseClass}__relation-button--${relatedCollection?.slug}`}
|
||||
@@ -207,10 +207,9 @@ export const AddNewRelation: React.FC<Props> = ({
|
||||
)}
|
||||
size="medium"
|
||||
/>
|
||||
{collectionConfig &&
|
||||
permissions.collections[collectionConfig?.slug]?.create?.permission && (
|
||||
<DocumentDrawer onSave={onSave} />
|
||||
)}
|
||||
{collectionConfig && permissions.collections[collectionConfig?.slug]?.create && (
|
||||
<DocumentDrawer onSave={onSave} />
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<void>
|
||||
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<DocumentSlots>({})
|
||||
const [hasSubmitted, setHasSubmitted] = React.useState(false)
|
||||
const [docPermissions, setDocPermissions] = React.useState<DocumentPermissions>()
|
||||
const [docPermissions, setDocPermissions] = React.useState<SanitizedDocumentPermissions>()
|
||||
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])
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
const hasDeletePermission = collectionPermissions?.delete?.permission
|
||||
const hasDeletePermission = collectionPermissions?.delete
|
||||
|
||||
const modalSlug = `delete-${slug}`
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -132,7 +132,7 @@ export const EditMany: React.FC<EditManyProps> = (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<EditManyProps> = (props) => {
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath={slug}
|
||||
permissions={permissions?.collections?.[slug]?.fields}
|
||||
permissions={collectionPermissions?.fields}
|
||||
readOnly={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
const { stringifyParams } = useSearchParams()
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
const hasPermission = collectionPermissions?.update?.permission
|
||||
const hasPermission = collectionPermissions?.update
|
||||
|
||||
const modalSlug = `publish-${slug}`
|
||||
|
||||
|
||||
@@ -172,8 +172,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
|
||||
const preferenceKey = `${relationTo}-list`
|
||||
|
||||
const canCreate =
|
||||
allowCreate !== false && permissions?.collections?.[relationTo]?.create?.permission
|
||||
const canCreate = allowCreate !== false && permissions?.collections?.[relationTo]?.create
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
||||
@@ -138,7 +138,7 @@ export const Status: React.FC = () => {
|
||||
],
|
||||
)
|
||||
|
||||
const canUpdate = docPermissions?.update?.permission
|
||||
const canUpdate = docPermissions?.update
|
||||
|
||||
if (statusToRender) {
|
||||
return (
|
||||
|
||||
@@ -45,7 +45,7 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
const hasPermission = collectionPermissions?.update?.permission
|
||||
const hasPermission = collectionPermissions?.update
|
||||
|
||||
const modalSlug = `unpublish-${slug}`
|
||||
|
||||
|
||||
@@ -206,9 +206,7 @@ export const Upload: React.FC<UploadProps> = (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)
|
||||
|
||||
@@ -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<ArrayRowProps> = ({
|
||||
parentIndexPath=""
|
||||
parentPath={path}
|
||||
parentSchemaPath={schemaPath}
|
||||
permissions={permissions?.fields}
|
||||
permissions={permissions === true ? permissions : permissions?.fields}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Collapsible>
|
||||
|
||||
@@ -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<BlocksFieldProps> = ({
|
||||
parentIndexPath=""
|
||||
parentPath={path}
|
||||
parentSchemaPath={schemaPath}
|
||||
permissions={permissions?.blocks?.[block.slug]?.fields}
|
||||
permissions={
|
||||
permissions === true ? permissions : permissions?.blocks?.[block.slug]?.fields
|
||||
}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Collapsible>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className={baseClass}>
|
||||
|
||||
@@ -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 (
|
||||
<SelectComponents.SingleValue {...props} className={baseClass}>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ClientComponentProps, 'forceRender' | 'readOnly' | 'schemaPath'>
|
||||
|
||||
|
||||
@@ -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> = (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> = (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> = (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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FieldPaths> = {
|
||||
customComponents: fieldState?.customComponents || {},
|
||||
field: clientField,
|
||||
path,
|
||||
readOnly: permissions?.[operation]?.permission === false,
|
||||
readOnly: permissions !== true && !permissions?.[operation],
|
||||
schemaPath,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T = ClientUser> = {
|
||||
export type AuthContext<T = ClientUser> = {
|
||||
fetchFullUser: () => Promise<null | User>
|
||||
logOut: () => Promise<boolean>
|
||||
permissions?: Permissions
|
||||
permissions?: SanitizedPermissions
|
||||
refreshCookie: (forceRefresh?: boolean) => void
|
||||
refreshCookieAsync: () => Promise<ClientUser>
|
||||
refreshPermissions: () => Promise<void>
|
||||
setPermissions: (permissions: Permissions) => void
|
||||
setPermissions: (permissions: SanitizedPermissions) => void
|
||||
setUser: (user: null | UserWithToken<T>) => 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<Permissions>(initialPermissions)
|
||||
const [permissions, setPermissions] = useState<SanitizedPermissions>(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}`)
|
||||
|
||||
@@ -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<DocumentPermissions>(docPermissionsFromProps)
|
||||
const [docPermissions, setDocPermissions] =
|
||||
useState<SanitizedDocumentPermissions>(docPermissionsFromProps)
|
||||
|
||||
const [hasSavePermission, setHasSavePermission] = useState<boolean>(hasSavePermissionFromProps)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<React.SetStateAction<DocumentPermissions>>
|
||||
setDocPermissions: React.Dispatch<React.SetStateAction<SanitizedDocumentPermissions>>
|
||||
setHasPublishPermission: React.Dispatch<React.SetStateAction<boolean>>
|
||||
setHasSavePermission: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}) =>
|
||||
@@ -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
|
||||
|
||||
@@ -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<void>
|
||||
readonly theme: Theme
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -57,24 +57,25 @@ export const Auth: React.FC<Props> = (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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user