Files
payload/test/access-control/config.ts
Jarrod Flesch 16f5538e12 fix(plugin-multi-tenant): unnecessary modal appearing (#12854)
Fixes #12826 

Leave without saving was being triggered when no changes were made to
the tenant. This should only happen if the value in form state differs
from that of the selected tenant, i.e. after changing tenants.

Adds tenant selector syncing so the selector updates when a tenant is
added or the name is edited.

Also adds E2E for most multi-tenant admin functionality. 

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210562742356842
2025-06-27 16:30:13 -04:00

772 lines
17 KiB
TypeScript

import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { FieldAccess } from 'payload'
import type { Config, User } from './payload-types.js'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { textToLexicalJSON } from '../lexical/collections/LexicalLocalized/textToLexicalJSON.js'
import { Auth } from './collections/Auth/index.js'
import { Disabled } from './collections/Disabled/index.js'
import { Hooks } from './collections/hooks/index.js'
import { Regression1 } from './collections/Regression-1/index.js'
import { Regression2 } from './collections/Regression-2/index.js'
import { RichText } from './collections/RichText/index.js'
import {
createNotUpdateCollectionSlug,
docLevelAccessSlug,
firstArrayText,
fullyRestrictedSlug,
hiddenAccessCountSlug,
hiddenAccessSlug,
hiddenFieldsSlug,
nonAdminEmail,
publicUserEmail,
publicUsersSlug,
readNotUpdateGlobalSlug,
readOnlyGlobalSlug,
readOnlySlug,
relyOnRequestHeadersSlug,
restrictedVersionsAdminPanelSlug,
restrictedVersionsSlug,
secondArrayText,
siblingDataSlug,
slug,
unrestrictedSlug,
userRestrictedCollectionSlug,
userRestrictedGlobalSlug,
} from './shared.js'
const openAccess = {
create: () => true,
delete: () => true,
read: () => true,
update: () => true,
}
const PublicReadabilityAccess: FieldAccess = ({ req: { user }, siblingData }) => {
if (user) {
return true
}
if (siblingData?.allowPublicReadability) {
return true
}
return false
}
export const requestHeaders = new Headers({ authorization: 'Bearer testBearerToken' })
const UseRequestHeadersAccess: FieldAccess = ({ req: { headers } }) => {
return !!headers && headers.get('authorization') === requestHeaders.get('authorization')
}
function isUser(user: Config['user']): user is {
collection: 'users'
} & User {
return user?.collection === 'users'
}
export default buildConfigWithDefaults(
{
admin: {
autoLogin: false,
user: 'users',
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [
{
slug: 'users',
access: {
// admin: () => true,
admin: async ({ req }) => {
if (req.user?.email === nonAdminEmail) {
return false
}
return new Promise((resolve) => {
// Simulate a request to an external service to determine access, i.e. another instance of Payload
setTimeout(resolve, 50, true) // set to 'true' or 'false' here to simulate the response
})
},
},
auth: true,
fields: [
{
name: 'roles',
type: 'select',
access: {
create: ({ req }) => isUser(req.user) && req.user?.roles?.includes('admin'),
read: () => false,
update: ({ req }) => {
return isUser(req.user) && req.user?.roles?.includes('admin')
},
},
defaultValue: ['user'],
hasMany: true,
options: ['admin', 'user'],
},
],
},
{
slug: publicUsersSlug,
auth: true,
fields: [],
},
{
slug,
access: {
...openAccess,
update: () => false,
},
fields: [
{
name: 'restrictedField',
type: 'text',
access: {
read: () => false,
update: () => false,
},
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'restrictedGroupText',
type: 'text',
access: {
create: () => false,
read: () => false,
update: () => false,
},
},
],
},
{
type: 'row',
fields: [
{
name: 'restrictedRowText',
type: 'text',
access: {
create: () => false,
read: () => false,
update: () => false,
},
},
],
},
{
type: 'collapsible',
fields: [
{
name: 'restrictedCollapsibleText',
type: 'text',
access: {
create: () => false,
read: () => false,
update: () => false,
},
},
],
label: 'Access',
},
],
},
{
slug: unrestrictedSlug,
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'userRestrictedDocs',
type: 'relationship',
hasMany: true,
relationTo: userRestrictedCollectionSlug,
},
{
name: 'createNotUpdateDocs',
type: 'relationship',
hasMany: true,
relationTo: createNotUpdateCollectionSlug,
},
],
},
{
slug: 'relation-restricted',
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'post',
type: 'relationship',
relationTo: slug,
},
],
},
{
slug: fullyRestrictedSlug,
access: {
create: () => false,
delete: () => false,
read: () => false,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: readOnlySlug,
access: {
create: () => false,
delete: () => false,
read: () => true,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: userRestrictedCollectionSlug,
access: {
create: () => true,
delete: () => false,
read: () => true,
update: ({ req }) => ({
name: {
equals: req.user?.email,
},
}),
},
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: createNotUpdateCollectionSlug,
access: {
create: () => true,
delete: () => false,
read: () => true,
update: () => false,
},
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: restrictedVersionsSlug,
access: {
read: ({ req: { user } }) => {
if (user) {
return true
}
return {
hidden: {
not_equals: true,
},
}
},
readVersions: ({ req: { user } }) => {
if (user) {
return true
}
return {
'version.hidden': {
not_equals: true,
},
}
},
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'hidden',
type: 'checkbox',
hidden: true,
},
],
versions: true,
},
{
slug: restrictedVersionsAdminPanelSlug,
access: {
read: ({ req: { user } }) => {
if (user) {
return true
}
return false
},
readVersions: () => {
return {
'version.hidden': {
not_equals: true,
},
}
},
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'hidden',
type: 'checkbox',
},
],
versions: true,
},
{
slug: siblingDataSlug,
access: openAccess,
fields: [
{
name: 'array',
type: 'array',
fields: [
{
type: 'row',
fields: [
{
name: 'allowPublicReadability',
type: 'checkbox',
label: 'Allow Public Readability',
},
{
name: 'text',
type: 'text',
access: {
read: PublicReadabilityAccess,
},
},
],
},
],
},
],
},
{
slug: relyOnRequestHeadersSlug,
access: {
create: UseRequestHeadersAccess,
delete: UseRequestHeadersAccess,
read: UseRequestHeadersAccess,
update: UseRequestHeadersAccess,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: docLevelAccessSlug,
access: {
delete: () => ({
and: [
{
approvedForRemoval: {
equals: true,
},
},
],
}),
},
fields: [
{
name: 'approvedForRemoval',
type: 'checkbox',
defaultValue: false,
},
{
name: 'approvedTitle',
type: 'text',
access: {
update: (args) => {
if (args?.doc?.lockTitle) {
return false
}
return true
},
},
localized: true,
},
{
name: 'lockTitle',
type: 'checkbox',
defaultValue: false,
},
],
labels: {
plural: 'Doc Level Access',
singular: 'Doc Level Access',
},
},
{
slug: hiddenFieldsSlug,
access: openAccess,
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'partiallyHiddenGroup',
type: 'group',
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'value',
type: 'text',
hidden: true,
},
],
},
{
name: 'partiallyHiddenArray',
type: 'array',
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'value',
type: 'text',
hidden: true,
},
],
},
{
name: 'hidden',
type: 'checkbox',
hidden: true,
},
],
},
{
slug: hiddenAccessSlug,
access: {
read: ({ req: { user } }) => {
if (user) {
return true
}
return {
hidden: {
not_equals: true,
},
}
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'hidden',
type: 'checkbox',
hidden: true,
},
],
},
{
slug: hiddenAccessCountSlug,
access: {
read: ({ req: { user } }) => {
if (user) {
return true
}
return {
hidden: {
not_equals: true,
},
}
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'hidden',
type: 'checkbox',
hidden: true,
},
],
},
{
slug: 'fields-and-top-access',
access: {
readVersions: () => ({
'version.secret': {
equals: 'will-success-access-read',
},
}),
read: () => ({
secret: {
equals: 'will-success-access-read',
},
}),
},
versions: { drafts: true },
fields: [
{
type: 'text',
name: 'secret',
access: { read: () => false },
},
],
},
Disabled,
RichText,
Regression1,
Regression2,
Hooks,
Auth,
],
globals: [
{
slug: 'settings',
admin: {
components: {
elements: {
SaveButton: '/TestButton.js#TestButton',
},
},
},
fields: [
{
name: 'test',
type: 'checkbox',
label: 'Allow access to test global',
},
],
},
{
slug: 'test',
access: {
read: async ({ req: { payload } }) => {
const access = await payload.findGlobal({ slug: 'settings' })
return Boolean(access.test)
},
},
fields: [],
},
{
slug: readOnlyGlobalSlug,
access: {
read: () => true,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: userRestrictedGlobalSlug,
access: {
read: () => true,
update: ({ data, req }) => data?.name === req.user?.email,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
{
slug: readNotUpdateGlobalSlug,
access: {
read: () => true,
update: () => false,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
],
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
await payload.create({
collection: 'users',
data: {
email: nonAdminEmail,
password: 'test',
},
})
await payload.create({
collection: publicUsersSlug,
data: {
email: publicUserEmail,
password: 'test',
},
})
await payload.create({
collection: slug,
data: {
restrictedField: 'restricted',
},
})
await payload.create({
collection: readOnlySlug,
data: {
name: 'read-only',
},
})
await payload.create({
collection: restrictedVersionsSlug,
data: {
name: 'versioned',
},
})
await payload.create({
collection: siblingDataSlug,
data: {
array: [
{
allowPublicReadability: true,
text: firstArrayText,
},
{
allowPublicReadability: false,
text: secondArrayText,
},
],
},
})
await payload.updateGlobal({
slug: userRestrictedGlobalSlug,
data: {
name: 'dev@payloadcms.com',
},
})
await payload.create({
collection: 'regression1',
data: {
richText4: textToLexicalJSON({ text: 'Text1' }),
array: [{ art: textToLexicalJSON({ text: 'Text2' }) }],
arrayWithAccessFalse: [{ richText6: textToLexicalJSON({ text: 'Text3' }) }],
group1: {
text: 'Text4',
richText1: textToLexicalJSON({ text: 'Text5' }),
},
blocks: [
{
blockType: 'myBlock3',
richText7: textToLexicalJSON({ text: 'Text6' }),
blockName: 'My Block 1',
},
],
blocks3: [
{
blockType: 'myBlock2',
richText5: textToLexicalJSON({ text: 'Text7' }),
blockName: 'My Block 2',
},
],
tab1: {
richText2: textToLexicalJSON({ text: 'Text8' }),
blocks2: [
{
blockType: 'myBlock',
richText3: textToLexicalJSON({ text: 'Text9' }),
blockName: 'My Block 3',
},
],
},
},
})
await payload.create({
collection: 'regression2',
data: {
array: [
{
richText2: textToLexicalJSON({ text: 'Text1' }),
},
],
group: {
text: 'Text2',
richText1: textToLexicalJSON({ text: 'Text3' }),
},
},
})
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
},
{
disableAutoLogin: true,
},
)