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:
Jarrod Flesch
2024-06-17 18:33:45 -04:00
committed by GitHub
parent 45871489d0
commit cedd916816
9 changed files with 99 additions and 27 deletions

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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}` : ''}`}

View 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()],
},
],
}

View File

@@ -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({

View File

@@ -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

View File

@@ -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'