fix(ui): invalid permissions passed to group and named tab sub-fields (#9366)
Fixes https://github.com/payloadcms/payload/issues/9363 This fixes the following issues that caused fields to be either hidden, or incorrectly set to readOnly in certain configurations: - In some cases, permissions were sanitized incorrectly. This PR rewrites the sanitizePermissions function and adds new unit tests - after a document save, the client was receiving unsanitized permissions. Moving the sanitization logic to the endpoint fixes this - Various incorrect handling of permissions in our form state endpoints / RenderFields
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import type { Collection, CollectionPermission, GlobalPermission, PayloadRequest } from 'payload'
|
||||
import type {
|
||||
Collection,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionPermission,
|
||||
SanitizedGlobalPermission,
|
||||
} from 'payload'
|
||||
|
||||
import { docAccessOperation, isolateObjectProperty } from 'payload'
|
||||
|
||||
@@ -12,7 +17,7 @@ export type Resolver = (
|
||||
context: {
|
||||
req: PayloadRequest
|
||||
},
|
||||
) => Promise<CollectionPermission | GlobalPermission>
|
||||
) => Promise<SanitizedCollectionPermission | SanitizedGlobalPermission>
|
||||
|
||||
export function docAccessResolver(collection: Collection): Resolver {
|
||||
async function resolver(_, args, context: Context) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type {
|
||||
CollectionPermission,
|
||||
GlobalPermission,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionPermission,
|
||||
SanitizedGlobalConfig,
|
||||
SanitizedGlobalPermission,
|
||||
} from 'payload'
|
||||
|
||||
import { docAccessOperationGlobal, isolateObjectProperty } from 'payload'
|
||||
@@ -14,7 +14,7 @@ export type Resolver = (
|
||||
context: {
|
||||
req: PayloadRequest
|
||||
},
|
||||
) => Promise<CollectionPermission | GlobalPermission>
|
||||
) => Promise<SanitizedCollectionPermission | SanitizedGlobalPermission>
|
||||
|
||||
export function docAccessResolver(global: SanitizedGlobalConfig): Resolver {
|
||||
async function resolver(_, context: Context) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {
|
||||
Data,
|
||||
DocumentPermissions,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedDocumentPermissions,
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
hasSavePermission as getHasSavePermission,
|
||||
isEditing as getIsEditing,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { docAccessOperation, docAccessOperationGlobal, sanitizePermissions } from 'payload'
|
||||
import { docAccessOperation, docAccessOperationGlobal } from 'payload'
|
||||
|
||||
export const getDocumentPermissions = async (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
@@ -26,7 +25,7 @@ export const getDocumentPermissions = async (args: {
|
||||
}> => {
|
||||
const { id, collectionConfig, data = {}, globalConfig, req } = args
|
||||
|
||||
let docPermissions: DocumentPermissions
|
||||
let docPermissions: SanitizedDocumentPermissions
|
||||
let hasPublishPermission = false
|
||||
|
||||
if (collectionConfig) {
|
||||
@@ -58,7 +57,7 @@ export const getDocumentPermissions = async (args: {
|
||||
_status: 'published',
|
||||
},
|
||||
},
|
||||
}).then(({ update }) => update?.permission)
|
||||
}).then((permissions) => permissions.update)
|
||||
}
|
||||
} catch (error) {
|
||||
req.payload.logger.error(error)
|
||||
@@ -85,20 +84,16 @@ export const getDocumentPermissions = async (args: {
|
||||
_status: 'published',
|
||||
},
|
||||
},
|
||||
}).then(({ update }) => update?.permission)
|
||||
}).then((permissions) => permissions.update)
|
||||
}
|
||||
} catch (error) {
|
||||
req.payload.logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 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: sanitizedDocPermissions,
|
||||
docPermissions,
|
||||
globalSlug: globalConfig?.slug,
|
||||
isEditing: getIsEditing({
|
||||
id,
|
||||
@@ -108,7 +103,7 @@ export const getDocumentPermissions = async (args: {
|
||||
})
|
||||
|
||||
return {
|
||||
docPermissions: sanitizedDocPermissions,
|
||||
docPermissions,
|
||||
hasPublishPermission,
|
||||
hasSavePermission,
|
||||
}
|
||||
|
||||
@@ -11,61 +11,61 @@ export type Permission = {
|
||||
where?: Where
|
||||
}
|
||||
|
||||
export type FieldPermissions = {
|
||||
blocks?: {
|
||||
[blockSlug: string]: {
|
||||
create: {
|
||||
permission: boolean
|
||||
}
|
||||
fields: {
|
||||
[fieldName: string]: FieldPermissions
|
||||
}
|
||||
read: {
|
||||
permission: boolean
|
||||
}
|
||||
update: {
|
||||
permission: boolean
|
||||
}
|
||||
export type FieldsPermissions = {
|
||||
[fieldName: string]: FieldPermissions
|
||||
}
|
||||
|
||||
export type BlockPermissions = {
|
||||
create: Permission
|
||||
fields: FieldsPermissions
|
||||
read: Permission
|
||||
update: Permission
|
||||
}
|
||||
|
||||
export type SanitizedBlockPermissions =
|
||||
| {
|
||||
fields: SanitizedFieldsPermissions
|
||||
}
|
||||
}
|
||||
create: {
|
||||
permission: boolean
|
||||
}
|
||||
fields?: {
|
||||
[fieldName: string]: FieldPermissions
|
||||
}
|
||||
read: {
|
||||
permission: boolean
|
||||
}
|
||||
update: {
|
||||
permission: boolean
|
||||
}
|
||||
| true
|
||||
|
||||
export type BlocksPermissions = {
|
||||
[blockSlug: string]: BlockPermissions
|
||||
}
|
||||
|
||||
export type SanitizedBlocksPermissions =
|
||||
| {
|
||||
[blockSlug: string]: SanitizedBlockPermissions
|
||||
}
|
||||
| true
|
||||
|
||||
export type FieldPermissions = {
|
||||
blocks?: BlocksPermissions
|
||||
create: Permission
|
||||
fields?: FieldsPermissions
|
||||
read: Permission
|
||||
update: Permission
|
||||
}
|
||||
|
||||
export type SanitizedFieldPermissions =
|
||||
| {
|
||||
blocks?: {
|
||||
[blockSlug: string]: {
|
||||
fields: {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
}
|
||||
}
|
||||
blocks?: SanitizedBlocksPermissions
|
||||
create: true
|
||||
fields?: {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
fields?: SanitizedFieldsPermissions
|
||||
read: true
|
||||
update: true
|
||||
}
|
||||
| true
|
||||
|
||||
export type SanitizedFieldsPermissions =
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| true
|
||||
|
||||
export type CollectionPermission = {
|
||||
create: Permission
|
||||
delete: Permission
|
||||
fields: {
|
||||
[fieldName: string]: FieldPermissions
|
||||
}
|
||||
fields: FieldsPermissions
|
||||
read: Permission
|
||||
readVersions?: Permission
|
||||
update: Permission
|
||||
@@ -74,31 +74,21 @@ export type CollectionPermission = {
|
||||
export type SanitizedCollectionPermission = {
|
||||
create?: true
|
||||
delete?: true
|
||||
fields:
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| true
|
||||
fields: SanitizedFieldsPermissions
|
||||
read?: true
|
||||
readVersions?: true
|
||||
update?: true
|
||||
}
|
||||
|
||||
export type GlobalPermission = {
|
||||
fields: {
|
||||
[fieldName: string]: FieldPermissions
|
||||
}
|
||||
fields: FieldsPermissions
|
||||
read: Permission
|
||||
readVersions?: Permission
|
||||
update: Permission
|
||||
}
|
||||
|
||||
export type SanitizedGlobalPermission = {
|
||||
fields:
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| true
|
||||
fields: SanitizedFieldsPermissions
|
||||
read?: true
|
||||
readVersions?: true
|
||||
update?: true
|
||||
@@ -110,7 +100,7 @@ export type SanitizedDocumentPermissions = SanitizedCollectionPermission | Sanit
|
||||
|
||||
export type Permissions = {
|
||||
canAccessAdmin: boolean
|
||||
collections: {
|
||||
collections?: {
|
||||
[collectionSlug: CollectionSlug]: CollectionPermission
|
||||
}
|
||||
globals?: {
|
||||
@@ -121,26 +111,10 @@ 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
|
||||
}
|
||||
[collectionSlug: string]: SanitizedCollectionPermission
|
||||
}
|
||||
globals?: {
|
||||
[globalSlug: string]: {
|
||||
fields: {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
read?: true
|
||||
readVersions?: true
|
||||
update?: true
|
||||
}
|
||||
[globalSlug: string]: SanitizedGlobalPermission
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { CollectionPermission } from '../../auth/index.js'
|
||||
import type { CollectionPermission, SanitizedCollectionPermission } from '../../auth/index.js'
|
||||
import type { AllOperations, PayloadRequest } from '../../types/index.js'
|
||||
import type { Collection } from '../config/types.js'
|
||||
|
||||
import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { sanitizePermissions } from '../../utilities/sanitizePermissions.js'
|
||||
|
||||
const allOperations: AllOperations[] = ['create', 'read', 'update', 'delete']
|
||||
|
||||
@@ -13,7 +14,7 @@ type Arguments = {
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
export async function docAccessOperation(args: Arguments): Promise<CollectionPermission> {
|
||||
export async function docAccessOperation(args: Arguments): Promise<SanitizedCollectionPermission> {
|
||||
const {
|
||||
id,
|
||||
collection: { config },
|
||||
@@ -43,7 +44,14 @@ export async function docAccessOperation(args: Arguments): Promise<CollectionPer
|
||||
req,
|
||||
})
|
||||
|
||||
return result
|
||||
const sanitizedPermissions = sanitizePermissions({
|
||||
canAccessAdmin: true,
|
||||
collections: {
|
||||
[config.slug]: result,
|
||||
},
|
||||
})
|
||||
|
||||
return sanitizedPermissions.collections[config.slug]
|
||||
} catch (e: unknown) {
|
||||
await killTransaction(req)
|
||||
throw e
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GlobalPermission } from '../../auth/index.js'
|
||||
import type { SanitizedGlobalPermission } from '../../auth/index.js'
|
||||
import type { AllOperations, PayloadRequest } from '../../types/index.js'
|
||||
import type { SanitizedGlobalConfig } from '../config/types.js'
|
||||
|
||||
@@ -6,13 +6,14 @@ import { commitTransaction } from '../../utilities/commitTransaction.js'
|
||||
import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
|
||||
import { initTransaction } from '../../utilities/initTransaction.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { sanitizePermissions } from '../../utilities/sanitizePermissions.js'
|
||||
|
||||
type Arguments = {
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
export const docAccessOperation = async (args: Arguments): Promise<GlobalPermission> => {
|
||||
export const docAccessOperation = async (args: Arguments): Promise<SanitizedGlobalPermission> => {
|
||||
const { globalConfig, req } = args
|
||||
|
||||
const globalOperations: AllOperations[] = ['read', 'update']
|
||||
@@ -32,7 +33,14 @@ export const docAccessOperation = async (args: Arguments): Promise<GlobalPermiss
|
||||
if (shouldCommit) {
|
||||
await commitTransaction(req)
|
||||
}
|
||||
return result
|
||||
const sanitizedPermissions = sanitizePermissions({
|
||||
canAccessAdmin: true,
|
||||
globals: {
|
||||
[globalConfig.slug]: result,
|
||||
},
|
||||
})
|
||||
|
||||
return sanitizedPermissions.globals[globalConfig.slug]
|
||||
} catch (e: unknown) {
|
||||
await killTransaction(req)
|
||||
throw e
|
||||
|
||||
@@ -1341,7 +1341,6 @@ 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'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,61 +1,147 @@
|
||||
import type { Permissions, SanitizedPermissions } from '../auth/types.js'
|
||||
import type {
|
||||
CollectionPermission,
|
||||
FieldPermissions,
|
||||
FieldsPermissions,
|
||||
GlobalPermission,
|
||||
Permissions,
|
||||
SanitizedBlocksPermissions,
|
||||
SanitizedFieldPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
SanitizedPermissions,
|
||||
} from '../auth/types.js'
|
||||
|
||||
type PermissionObject = {
|
||||
[key: string]: any
|
||||
function checkAndSanitizeFieldsPermssions(data: FieldsPermissions): boolean {
|
||||
let allFieldPermissionsTrue = true
|
||||
for (const key in data) {
|
||||
if (typeof data[key] === 'object') {
|
||||
if (!checkAndSanitizePermissions(data[key])) {
|
||||
allFieldPermissionsTrue = false
|
||||
} else {
|
||||
;(data[key] as unknown as SanitizedFieldPermissions) = true
|
||||
}
|
||||
} else if (data[key] !== true) {
|
||||
allFieldPermissionsTrue = false
|
||||
}
|
||||
}
|
||||
|
||||
// If all values are true or it's an empty object, return true
|
||||
return allFieldPermissionsTrue
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all permissions in a FieldPermissions object are true on the condition that no nested blocks or fields are present.
|
||||
* Check if all permissions in a FieldPermissions, CollectionPermission or GlobalPermission object are true.
|
||||
* If nested fields or blocks are present, the function will recursively check those as well.
|
||||
*/
|
||||
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])) {
|
||||
return false
|
||||
function checkAndSanitizePermissions(
|
||||
data: CollectionPermission | FieldPermissions | GlobalPermission,
|
||||
): boolean {
|
||||
/**
|
||||
* Check blocks permissions
|
||||
*/
|
||||
let blocksPermissions = true
|
||||
if ('blocks' in data && data.blocks) {
|
||||
for (const blockSlug in data.blocks) {
|
||||
if (typeof data.blocks[blockSlug] === 'object') {
|
||||
for (const key in data.blocks[blockSlug]) {
|
||||
/**
|
||||
* Check fields in nested blocks
|
||||
*/
|
||||
if (key === 'fields') {
|
||||
if (data.blocks[blockSlug].fields) {
|
||||
if (!checkAndSanitizeFieldsPermssions(data.blocks[blockSlug].fields)) {
|
||||
blocksPermissions = false
|
||||
} else {
|
||||
;(data.blocks[blockSlug].fields as unknown as SanitizedFieldsPermissions) = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (typeof data.blocks[blockSlug][key] === 'object') {
|
||||
/**
|
||||
* Check Permissions in nested blocks
|
||||
*/
|
||||
if (isPermissionObject(data.blocks[blockSlug][key])) {
|
||||
if (
|
||||
data.blocks[blockSlug][key]['permission'] === true &&
|
||||
!('where' in data.blocks[blockSlug][key])
|
||||
) {
|
||||
// If the permission is true and there is no where clause, set the key to true
|
||||
data.blocks[blockSlug][key] = true
|
||||
continue
|
||||
} else if (
|
||||
data.blocks[blockSlug][key]['permission'] === true &&
|
||||
'where' in data.blocks[blockSlug][key]
|
||||
) {
|
||||
// otherwise do nothing so we can keep the where clause
|
||||
blocksPermissions = false
|
||||
} else {
|
||||
blocksPermissions = false
|
||||
data.blocks[blockSlug][key] = false
|
||||
delete data.blocks[blockSlug][key]
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected object in block permissions')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.blocks[key].fields && !areAllPermissionsTrue(data.blocks[key].fields)) {
|
||||
return false
|
||||
}
|
||||
} else if (data.blocks[key] !== true) {
|
||||
} else if (data.blocks[blockSlug] !== true) {
|
||||
// If any value is not true, return false
|
||||
return false
|
||||
blocksPermissions = false
|
||||
delete data.blocks[blockSlug]
|
||||
}
|
||||
}
|
||||
// If all values are true or it's an empty object, return true
|
||||
return true
|
||||
if (blocksPermissions) {
|
||||
;(data.blocks as unknown as SanitizedBlocksPermissions) = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check nested Fields permissions
|
||||
*/
|
||||
let fieldsPermissions = 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 (!checkAndSanitizeFieldsPermssions(data.fields)) {
|
||||
fieldsPermissions = false
|
||||
} else {
|
||||
;(data.fields as unknown as SanitizedFieldsPermissions) = true
|
||||
}
|
||||
// If all values are true or it's an empty object, return true
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check other Permissions objects (e.g. read, write)
|
||||
*/
|
||||
let otherPermissions = true
|
||||
for (const key in data) {
|
||||
if (key === 'fields' || key === 'blocks') {
|
||||
continue
|
||||
}
|
||||
if (typeof data[key] === 'object') {
|
||||
// If any recursive call returns false, the whole function returns false
|
||||
if (!areAllPermissionsTrue(data[key])) {
|
||||
return false
|
||||
if (isPermissionObject(data[key])) {
|
||||
if (data[key]['permission'] === true && !('where' in data[key])) {
|
||||
// If the permission is true and there is no where clause, set the key to true
|
||||
data[key] = true
|
||||
continue
|
||||
} else if (data[key]['permission'] === true && 'where' in data[key]) {
|
||||
// otherwise do nothing so we can keep the where clause
|
||||
otherPermissions = false
|
||||
} else {
|
||||
otherPermissions = false
|
||||
data[key] = false
|
||||
delete data[key]
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected object in fields permissions')
|
||||
}
|
||||
} else if (data[key] !== true) {
|
||||
// If any value is not true, return false
|
||||
return false
|
||||
otherPermissions = false
|
||||
}
|
||||
}
|
||||
|
||||
// If all values are true or it's an empty object, return true
|
||||
return true
|
||||
return fieldsPermissions && blocksPermissions && otherPermissions
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,84 +169,27 @@ function cleanEmptyObjects(obj: any): void {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively resolve permissions in an object.
|
||||
*/
|
||||
export function recursivelySanitizePermissions(obj: PermissionObject): void {
|
||||
export function recursivelySanitizeCollections(obj: Permissions['collections']): void {
|
||||
if (typeof obj !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const entries = Object.entries(obj)
|
||||
const collectionPermissions = Object.values(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
|
||||
}
|
||||
}
|
||||
for (const collectionPermission of collectionPermissions) {
|
||||
checkAndSanitizePermissions(collectionPermission)
|
||||
}
|
||||
}
|
||||
|
||||
// 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',
|
||||
)
|
||||
export function recursivelySanitizeGlobals(obj: Permissions['globals']): void {
|
||||
if (typeof obj !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
const globalPermissions = Object.values(obj)
|
||||
|
||||
for (const globalPermission of globalPermissions) {
|
||||
checkAndSanitizePermissions(globalPermission)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,11 +202,11 @@ export function sanitizePermissions(data: Permissions): SanitizedPermissions {
|
||||
}
|
||||
|
||||
if (data.collections) {
|
||||
recursivelySanitizePermissions(data.collections)
|
||||
recursivelySanitizeCollections(data.collections)
|
||||
}
|
||||
|
||||
if (data.globals) {
|
||||
recursivelySanitizePermissions(data.globals)
|
||||
recursivelySanitizeGlobals(data.globals)
|
||||
}
|
||||
|
||||
// Run clean up of empty objects at the end
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
'use client'
|
||||
import type { ClientBlock, ClientField, Labels, Row, SanitizedFieldPermissions } from 'payload'
|
||||
import type {
|
||||
ClientBlock,
|
||||
ClientField,
|
||||
Labels,
|
||||
Row,
|
||||
SanitizedFieldPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import type { UseDraggableSortableReturn } from '../../elements/DraggableSortable/useDraggableSortable/types.js'
|
||||
import type { RenderFieldsProps } from '../../forms/RenderFields/types.js'
|
||||
|
||||
import { Collapsible } from '../../elements/Collapsible/index.js'
|
||||
import { ErrorPill } from '../../elements/ErrorPill/index.js'
|
||||
@@ -80,6 +88,19 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
let blockPermissions: RenderFieldsProps['permissions'] = undefined
|
||||
|
||||
if (permissions === true) {
|
||||
blockPermissions = true
|
||||
} else {
|
||||
const permissionsBlockSpecific = permissions?.blocks?.[block.slug]
|
||||
if (permissionsBlockSpecific === true) {
|
||||
blockPermissions = true
|
||||
} else {
|
||||
blockPermissions = permissionsBlockSpecific?.fields
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`${parentPath?.split('.').join('-')}-row-${rowIndex}`}
|
||||
@@ -147,9 +168,7 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
|
||||
parentIndexPath=""
|
||||
parentPath={path}
|
||||
parentSchemaPath={schemaPath}
|
||||
permissions={
|
||||
permissions === true ? permissions : permissions?.blocks?.[block.slug]?.fields
|
||||
}
|
||||
permissions={blockPermissions}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Collapsible>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getFieldPaths } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
import type { RenderFieldsProps } from './types.js'
|
||||
|
||||
import { RenderIfInViewport } from '../../elements/RenderIfInViewport/index.js'
|
||||
import { useOperation } from '../../providers/Operation/index.js'
|
||||
@@ -12,9 +12,9 @@ import { RenderField } from './RenderField.js'
|
||||
|
||||
const baseClass = 'render-fields'
|
||||
|
||||
export { Props }
|
||||
export { RenderFieldsProps as Props }
|
||||
|
||||
export const RenderFields: React.FC<Props> = (props) => {
|
||||
export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
|
||||
const {
|
||||
className,
|
||||
fields,
|
||||
@@ -49,10 +49,15 @@ export const RenderFields: React.FC<Props> = (props) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const parentName = parentPath?.includes('.')
|
||||
? parentPath.split('.')[parentPath.split('.').length - 1]
|
||||
: parentPath
|
||||
|
||||
// If the user cannot read the field, then filter it out
|
||||
// This is different from `admin.readOnly` which is executed based on `operation`
|
||||
const hasReadPermission =
|
||||
permissions === true ||
|
||||
permissions?.[parentName] === true ||
|
||||
('name' in field &&
|
||||
typeof permissions === 'object' &&
|
||||
permissions?.[field.name] &&
|
||||
@@ -74,6 +79,7 @@ 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 hasOperationPermission =
|
||||
permissions === true ||
|
||||
permissions?.[parentName] === true ||
|
||||
('name' in field &&
|
||||
typeof permissions === 'object' &&
|
||||
permissions?.[field.name] &&
|
||||
@@ -102,7 +108,7 @@ export const RenderFields: React.FC<Props> = (props) => {
|
||||
parentSchemaPath={parentSchemaPath}
|
||||
path={path}
|
||||
permissions={
|
||||
permissions === null || permissions === true
|
||||
permissions === undefined || permissions === null || permissions === true
|
||||
? true
|
||||
: 'name' in field
|
||||
? permissions?.[field.name]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ClientField, SanitizedFieldPermissions } from 'payload'
|
||||
|
||||
export type Props = {
|
||||
export type RenderFieldsProps = {
|
||||
readonly className?: string
|
||||
readonly fields: ClientField[]
|
||||
/**
|
||||
|
||||
@@ -8,10 +8,12 @@ import type {
|
||||
FormStateWithoutComponents,
|
||||
PayloadRequest,
|
||||
SanitizedFieldPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
} from 'payload'
|
||||
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
import {
|
||||
deepCopyObjectSimple,
|
||||
fieldAffectsData,
|
||||
fieldHasSubFields,
|
||||
fieldIsSidebar,
|
||||
@@ -59,14 +61,9 @@ export type AddFieldStatePromiseArgs = {
|
||||
operation: 'create' | 'update'
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentPermissions: SanitizedFieldsPermissions
|
||||
parentSchemaPath: string
|
||||
passesCondition: boolean
|
||||
permissions:
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| null
|
||||
| SanitizedFieldPermissions
|
||||
preferences: DocumentPreferences
|
||||
previousFormState: FormState
|
||||
renderAllFields: boolean
|
||||
@@ -109,9 +106,9 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentPermissions,
|
||||
parentSchemaPath,
|
||||
passesCondition,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
@@ -135,10 +132,15 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
const isHiddenField = 'hidden' in field && field?.hidden
|
||||
const disabledFromAdmin = field?.admin && 'disabled' in field.admin && field.admin.disabled
|
||||
|
||||
let fieldPermissions: SanitizedFieldPermissions = true
|
||||
if (fieldAffectsData(field) && !(isHiddenField || disabledFromAdmin)) {
|
||||
const fieldPermissions = permissions === true ? permissions : permissions?.[field.name]
|
||||
fieldPermissions =
|
||||
parentPermissions === true
|
||||
? parentPermissions
|
||||
: deepCopyObjectSimple(parentPermissions?.[field.name])
|
||||
|
||||
let hasPermission: boolean = fieldPermissions === true || fieldPermissions?.read
|
||||
let hasPermission: boolean =
|
||||
fieldPermissions === true || deepCopyObjectSimple(fieldPermissions?.read)
|
||||
|
||||
if (typeof field?.access?.read === 'function') {
|
||||
hasPermission = await field.access.read({ doc: fullData, req, siblingData: data })
|
||||
@@ -385,7 +387,9 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
permissions:
|
||||
fieldPermissions === true
|
||||
? fieldPermissions
|
||||
: permissions?.[field.name]?.blocks?.[block.slug]?.fields || {},
|
||||
: parentPermissions?.[field.name]?.blocks?.[block.slug] === true
|
||||
? true
|
||||
: parentPermissions?.[field.name]?.blocks?.[block.slug]?.fields || {},
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields: requiresRender,
|
||||
@@ -470,7 +474,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
parentPassesCondition: passesCondition,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
permissions: fieldPermissions ?? permissions?.[field.name]?.fields ?? {},
|
||||
permissions:
|
||||
typeof fieldPermissions === 'boolean' ? fieldPermissions : fieldPermissions?.fields,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
@@ -614,7 +619,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
parentPassesCondition: passesCondition,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
permissions,
|
||||
permissions: parentPermissions, // TODO: Verify this is correct
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
@@ -643,6 +648,22 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
let childPermissions: SanitizedFieldsPermissions = undefined
|
||||
if (tabHasName(tab)) {
|
||||
if (parentPermissions === true) {
|
||||
childPermissions = true
|
||||
} else {
|
||||
const tabPermissions = parentPermissions?.[tab.name]
|
||||
if (tabPermissions === true) {
|
||||
childPermissions = true
|
||||
} else {
|
||||
childPermissions = tabPermissions?.fields
|
||||
}
|
||||
}
|
||||
} else {
|
||||
childPermissions = parentPermissions
|
||||
}
|
||||
|
||||
return iterateFields({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
@@ -661,11 +682,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
parentPassesCondition: passesCondition,
|
||||
parentPath: tabHasName(tab) ? tabPath : parentPath,
|
||||
parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath,
|
||||
permissions: tabHasName(tab)
|
||||
? typeof permissions === 'boolean'
|
||||
? permissions
|
||||
: permissions?.[tab.name] || {}
|
||||
: permissions,
|
||||
permissions: childPermissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
@@ -727,7 +744,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
permissions,
|
||||
permissions: fieldPermissions,
|
||||
preferences,
|
||||
previousFieldState: previousFormState?.[path],
|
||||
req,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
FormState,
|
||||
FormStateWithoutComponents,
|
||||
PayloadRequest,
|
||||
SanitizedDocumentPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
} from 'payload'
|
||||
|
||||
import type { RenderFieldMethod } from './types.js'
|
||||
@@ -26,7 +26,7 @@ type Args = {
|
||||
fieldSchemaMap: FieldSchemaMap | undefined
|
||||
id?: number | string
|
||||
operation?: 'create' | 'update'
|
||||
permissions: SanitizedDocumentPermissions['fields']
|
||||
permissions: SanitizedFieldsPermissions
|
||||
preferences: DocumentPreferences
|
||||
/**
|
||||
* Optionally accept the previous form state,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
FormState,
|
||||
FormStateWithoutComponents,
|
||||
PayloadRequest,
|
||||
SanitizedFieldPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
} from 'payload'
|
||||
|
||||
import type { AddFieldStatePromiseArgs } from './addFieldStatePromise.js'
|
||||
@@ -47,12 +47,7 @@ type Args = {
|
||||
parentPassesCondition?: boolean
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
permissions:
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| null
|
||||
| SanitizedFieldPermissions
|
||||
permissions: SanitizedFieldsPermissions
|
||||
preferences?: DocumentPreferences
|
||||
previousFormState: FormState
|
||||
renderAllFields: boolean
|
||||
@@ -130,9 +125,9 @@ export const iterateFields = async ({
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentPermissions: permissions,
|
||||
parentSchemaPath,
|
||||
passesCondition,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import type {
|
||||
ClientComponentProps,
|
||||
ClientField,
|
||||
FieldPaths,
|
||||
SanitizedFieldPermissions,
|
||||
ServerComponentProps,
|
||||
} from 'payload'
|
||||
import type { ClientComponentProps, ClientField, FieldPaths, ServerComponentProps } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { createClientField, deepCopyObjectSimple, MissingEditorProp } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
|
||||
import type { RenderFieldMethod } from './types.js'
|
||||
|
||||
@@ -36,15 +29,12 @@ export const renderField: RenderFieldMethod = ({
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
permissions: incomingPermissions,
|
||||
permissions,
|
||||
preferences,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData,
|
||||
}) => {
|
||||
// TODO (ALESSIO): why are we passing the fieldConfig twice?
|
||||
// and especially, why are we deepCopyObject -here- instead of inside the createClientField func,
|
||||
// so no one screws this up in the future?
|
||||
const clientField = createClientField({
|
||||
clientField: deepCopyObjectSimple(fieldConfig) as ClientField,
|
||||
defaultIDType: req.payload.config.db.defaultIDType,
|
||||
@@ -53,18 +43,11 @@ export const renderField: RenderFieldMethod = ({
|
||||
importMap: req.payload.importMap,
|
||||
})
|
||||
|
||||
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 !== true && !permissions?.[operation],
|
||||
readOnly: typeof permissions === 'boolean' ? !permissions : !permissions?.[operation],
|
||||
schemaPath,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,7 @@ export type RenderFieldArgs = {
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
path: string
|
||||
permissions:
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| null
|
||||
| SanitizedFieldPermissions
|
||||
permissions: SanitizedFieldPermissions
|
||||
preferences: DocumentPreferences
|
||||
previousFieldState: FieldState
|
||||
req: PayloadRequest
|
||||
|
||||
Reference in New Issue
Block a user