fix: corrects permission access reading for disabling fields (#6815)
Fixes issues where access control was not properly affecting the read-only setting on fields.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { ArrayField as ArrayFieldType, FieldPermissions } from 'payload'
|
import type { ArrayField as ArrayFieldType } from 'payload'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
@@ -40,7 +40,6 @@ export type ArrayFieldProps = FormFieldBase & {
|
|||||||
maxRows?: ArrayFieldType['maxRows']
|
maxRows?: ArrayFieldType['maxRows']
|
||||||
minRows?: ArrayFieldType['minRows']
|
minRows?: ArrayFieldType['minRows']
|
||||||
name?: string
|
name?: string
|
||||||
permissions: FieldPermissions
|
|
||||||
width?: string
|
width?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,13 +62,17 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
maxRows,
|
maxRows,
|
||||||
minRows: minRowsProp,
|
minRows: minRowsProp,
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
permissions,
|
|
||||||
readOnly: readOnlyFromProps,
|
readOnly: readOnlyFromProps,
|
||||||
required,
|
required,
|
||||||
validate,
|
validate,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { indexPath, readOnly: readOnlyFromContext } = useFieldProps()
|
const {
|
||||||
|
indexPath,
|
||||||
|
path: pathFromContext,
|
||||||
|
permissions,
|
||||||
|
readOnly: readOnlyFromContext,
|
||||||
|
} = useFieldProps()
|
||||||
const minRows = minRowsProp ?? required ? 1 : 0
|
const minRows = minRowsProp ?? required ? 1 : 0
|
||||||
|
|
||||||
const { setDocFieldPreferences } = useDocumentInfo()
|
const { setDocFieldPreferences } = useDocumentInfo()
|
||||||
@@ -110,8 +113,6 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
[maxRows, minRows, required, validate, editingDefaultLocale],
|
[maxRows, minRows, required, validate, editingDefaultLocale],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext } = useFieldProps()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
errorPaths,
|
errorPaths,
|
||||||
formInitializing,
|
formInitializing,
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import type { BlockField } from 'payload'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import React, { Fragment, useCallback } from 'react'
|
import React, { Fragment, useCallback } from 'react'
|
||||||
|
|
||||||
|
import type { ReducedBlock } from '../../providers/ComponentMap/buildComponentMap/types.js'
|
||||||
|
import type { FormFieldBase } from '../shared/index.js'
|
||||||
|
|
||||||
import { Banner } from '../../elements/Banner/index.js'
|
import { Banner } from '../../elements/Banner/index.js'
|
||||||
import { Button } from '../../elements/Button/index.js'
|
import { Button } from '../../elements/Button/index.js'
|
||||||
import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js'
|
import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js'
|
||||||
@@ -9,14 +14,19 @@ import { DraggableSortable } from '../../elements/DraggableSortable/index.js'
|
|||||||
import { DrawerToggler } from '../../elements/Drawer/index.js'
|
import { DrawerToggler } from '../../elements/Drawer/index.js'
|
||||||
import { useDrawerSlug } from '../../elements/Drawer/useDrawerSlug.js'
|
import { useDrawerSlug } from '../../elements/Drawer/useDrawerSlug.js'
|
||||||
import { ErrorPill } from '../../elements/ErrorPill/index.js'
|
import { ErrorPill } from '../../elements/ErrorPill/index.js'
|
||||||
|
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
||||||
import { useForm, useFormSubmitted } from '../../forms/Form/context.js'
|
import { useForm, useFormSubmitted } from '../../forms/Form/context.js'
|
||||||
import { NullifyLocaleField } from '../../forms/NullifyField/index.js'
|
import { NullifyLocaleField } from '../../forms/NullifyField/index.js'
|
||||||
import { useField } from '../../forms/useField/index.js'
|
import { useField } from '../../forms/useField/index.js'
|
||||||
|
import { withCondition } from '../../forms/withCondition/index.js'
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
import { useLocale } from '../../providers/Locale/index.js'
|
import { useLocale } from '../../providers/Locale/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { scrollToID } from '../../utilities/scrollToID.js'
|
import { scrollToID } from '../../utilities/scrollToID.js'
|
||||||
|
import { FieldDescription } from '../FieldDescription/index.js'
|
||||||
|
import { FieldError } from '../FieldError/index.js'
|
||||||
|
import { FieldLabel } from '../FieldLabel/index.js'
|
||||||
import { fieldBaseClass } from '../shared/index.js'
|
import { fieldBaseClass } from '../shared/index.js'
|
||||||
import { BlockRow } from './BlockRow.js'
|
import { BlockRow } from './BlockRow.js'
|
||||||
import { BlocksDrawer } from './BlocksDrawer/index.js'
|
import { BlocksDrawer } from './BlocksDrawer/index.js'
|
||||||
@@ -24,17 +34,6 @@ import './index.scss'
|
|||||||
|
|
||||||
const baseClass = 'blocks-field'
|
const baseClass = 'blocks-field'
|
||||||
|
|
||||||
import type { BlockField, FieldPermissions } from 'payload'
|
|
||||||
|
|
||||||
import type { ReducedBlock } from '../../providers/ComponentMap/buildComponentMap/types.js'
|
|
||||||
import type { FormFieldBase } from '../shared/index.js'
|
|
||||||
|
|
||||||
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
|
||||||
import { withCondition } from '../../forms/withCondition/index.js'
|
|
||||||
import { FieldDescription } from '../FieldDescription/index.js'
|
|
||||||
import { FieldError } from '../FieldError/index.js'
|
|
||||||
import { FieldLabel } from '../FieldLabel/index.js'
|
|
||||||
|
|
||||||
export type BlocksFieldProps = FormFieldBase & {
|
export type BlocksFieldProps = FormFieldBase & {
|
||||||
blocks?: ReducedBlock[]
|
blocks?: ReducedBlock[]
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
@@ -43,7 +42,6 @@ export type BlocksFieldProps = FormFieldBase & {
|
|||||||
maxRows?: number
|
maxRows?: number
|
||||||
minRows?: number
|
minRows?: number
|
||||||
name?: string
|
name?: string
|
||||||
permissions: FieldPermissions
|
|
||||||
slug?: string
|
slug?: string
|
||||||
width?: string
|
width?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { FieldDescription } from '../FieldDescription/index.js'
|
|||||||
export type CollapsibleFieldProps = FormFieldBase & {
|
export type CollapsibleFieldProps = FormFieldBase & {
|
||||||
fieldMap: FieldMap
|
fieldMap: FieldMap
|
||||||
initCollapsed?: boolean
|
initCollapsed?: boolean
|
||||||
permissions: FieldPermissions
|
|
||||||
width?: string
|
width?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FieldPermissions } from 'payload'
|
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
@@ -33,7 +32,6 @@ export type GroupFieldProps = FormFieldBase & {
|
|||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
hideGutter?: boolean
|
hideGutter?: boolean
|
||||||
name?: string
|
name?: string
|
||||||
permissions: FieldPermissions
|
|
||||||
width?: string
|
width?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { DocumentPreferences, FieldPermissions } from 'payload'
|
import type { DocumentPreferences } from 'payload'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { toKebabCase } from 'payload/shared'
|
import { toKebabCase } from 'payload/shared'
|
||||||
@@ -29,7 +29,6 @@ export type TabsFieldProps = FormFieldBase & {
|
|||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
name?: string
|
name?: string
|
||||||
path?: string
|
path?: string
|
||||||
permissions: FieldPermissions
|
|
||||||
tabs?: MappedTab[]
|
tabs?: MappedTab[]
|
||||||
width?: string
|
width?: string
|
||||||
}
|
}
|
||||||
@@ -49,9 +48,9 @@ const TabsField: React.FC<TabsFieldProps> = (props) => {
|
|||||||
const {
|
const {
|
||||||
indexPath,
|
indexPath,
|
||||||
path: pathFromContext,
|
path: pathFromContext,
|
||||||
permissions,
|
|
||||||
readOnly: readOnlyFromContext,
|
readOnly: readOnlyFromContext,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
|
siblingPermissions,
|
||||||
} = useFieldProps()
|
} = useFieldProps()
|
||||||
|
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
const readOnly = readOnlyFromProps || readOnlyFromContext
|
||||||
@@ -180,9 +179,9 @@ const TabsField: React.FC<TabsFieldProps> = (props) => {
|
|||||||
margins="small"
|
margins="small"
|
||||||
path={generateTabPath()}
|
path={generateTabPath()}
|
||||||
permissions={
|
permissions={
|
||||||
'name' in activeTabConfig && permissions?.fields?.[activeTabConfig.name]?.fields
|
'name' in activeTabConfig && siblingPermissions?.[activeTabConfig.name]?.fields
|
||||||
? permissions?.fields?.[activeTabConfig.name]?.fields
|
? siblingPermissions[activeTabConfig.name]?.fields
|
||||||
: permissions?.fields
|
: siblingPermissions
|
||||||
}
|
}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
schemaPath={`${schemaPath ? `${schemaPath}` : ''}${activeTabConfig.name ? `.${activeTabConfig.name}` : ''}`}
|
schemaPath={`${schemaPath ? `${schemaPath}` : ''}${activeTabConfig.name ? `.${activeTabConfig.name}` : ''}`}
|
||||||
|
|||||||
42
test/access-control/collections/Disabled/index.ts
Normal file
42
test/access-control/collections/Disabled/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { CollectionConfig, Field } from 'payload/types'
|
||||||
|
|
||||||
|
import { disabledSlug } from '../../shared.js'
|
||||||
|
|
||||||
|
const disabledFromUpdateAccessControl = (fieldName = 'text'): Field => ({
|
||||||
|
type: 'text',
|
||||||
|
name: fieldName,
|
||||||
|
access: {
|
||||||
|
update: () => {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Disabled: CollectionConfig = {
|
||||||
|
slug: disabledSlug,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
name: 'group',
|
||||||
|
fields: [disabledFromUpdateAccessControl()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'namedTab',
|
||||||
|
fields: [disabledFromUpdateAccessControl()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'unnamedTab',
|
||||||
|
fields: [disabledFromUpdateAccessControl('unnamedTab')],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
name: 'array',
|
||||||
|
fields: [disabledFromUpdateAccessControl()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import type { FieldAccess } from 'payload'
|
|||||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
import { devUser } from '../credentials.js'
|
import { devUser } from '../credentials.js'
|
||||||
import { TestButton } from './TestButton.js'
|
import { TestButton } from './TestButton.js'
|
||||||
|
import { Disabled } from './collections/Disabled/index.js'
|
||||||
import {
|
import {
|
||||||
createNotUpdateCollectionSlug,
|
createNotUpdateCollectionSlug,
|
||||||
docLevelAccessSlug,
|
docLevelAccessSlug,
|
||||||
@@ -533,6 +534,7 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Disabled,
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
|||||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||||
import {
|
import {
|
||||||
createNotUpdateCollectionSlug,
|
createNotUpdateCollectionSlug,
|
||||||
|
disabledSlug,
|
||||||
docLevelAccessSlug,
|
docLevelAccessSlug,
|
||||||
fullyRestrictedSlug,
|
fullyRestrictedSlug,
|
||||||
noAdminAccessEmail,
|
noAdminAccessEmail,
|
||||||
@@ -67,6 +68,7 @@ describe('access control', () => {
|
|||||||
let restrictedVersionsUrl: AdminUrlUtil
|
let restrictedVersionsUrl: AdminUrlUtil
|
||||||
let userRestrictedCollectionURL: AdminUrlUtil
|
let userRestrictedCollectionURL: AdminUrlUtil
|
||||||
let userRestrictedGlobalURL: AdminUrlUtil
|
let userRestrictedGlobalURL: AdminUrlUtil
|
||||||
|
let disabledFields: AdminUrlUtil
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let context: BrowserContext
|
let context: BrowserContext
|
||||||
let logoutURL: string
|
let logoutURL: string
|
||||||
@@ -83,6 +85,7 @@ describe('access control', () => {
|
|||||||
restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug)
|
restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug)
|
||||||
userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug)
|
userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug)
|
||||||
userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug)
|
userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug)
|
||||||
|
disabledFields = new AdminUrlUtil(serverURL, disabledSlug)
|
||||||
|
|
||||||
context = await browser.newContext()
|
context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -521,6 +524,34 @@ describe('access control', () => {
|
|||||||
await expect(page.locator('.next-error-h1')).toBeVisible()
|
await expect(page.locator('.next-error-h1')).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('read-only from access control', () => {
|
||||||
|
test('should be read-only when update returns false', async () => {
|
||||||
|
await page.goto(disabledFields.create)
|
||||||
|
|
||||||
|
// group field
|
||||||
|
await page.locator('#field-group__text').fill('group')
|
||||||
|
|
||||||
|
// named tab
|
||||||
|
await page.locator('#field-namedTab__text').fill('named tab')
|
||||||
|
|
||||||
|
// unnamed tab
|
||||||
|
await page.locator('.tabs-field__tab-button').nth(1).click()
|
||||||
|
await page.locator('#field-unnamedTab').fill('unnamed tab')
|
||||||
|
|
||||||
|
// array field
|
||||||
|
await page.locator('#field-array button').click()
|
||||||
|
await page.locator('#field-array__0__text').fill('array row 0')
|
||||||
|
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
await expect(page.locator('#field-group__text')).toBeDisabled()
|
||||||
|
await expect(page.locator('#field-namedTab__text')).toBeDisabled()
|
||||||
|
await page.locator('.tabs-field__tab-button').nth(1).click()
|
||||||
|
await expect(page.locator('#field-unnamedTab')).toBeDisabled()
|
||||||
|
await expect(page.locator('#field-array__0__text')).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
|||||||
@@ -24,3 +24,5 @@ export const noAdminAccessEmail = 'no-admin-access@payloadcms.com'
|
|||||||
export const nonAdminUserEmail = 'non-admin-user@payloadcms.com'
|
export const nonAdminUserEmail = 'non-admin-user@payloadcms.com'
|
||||||
|
|
||||||
export const nonAdminUserSlug = 'non-admin-user'
|
export const nonAdminUserSlug = 'non-admin-user'
|
||||||
|
|
||||||
|
export const disabledSlug = 'disabled'
|
||||||
|
|||||||
Reference in New Issue
Block a user