Redirecting from login to any non-auth collection crashes the page with the following error: ``` Cannot read properties of null (reading 'fields') ``` TL;DR: the page-level config context was threading stale methods from the root config provider. #### Background The client config is now gated behind authentication as of #13714, so it changes based on authentication status. If the root layout is mounted before authentication, it puts the unauthenticated client config into state for the entire app to consume. On login, the root layout does not re-render, so the page itself needs to generate a fresh client config and sync it up. This leads to race conditions, however, where if the login page included a `?redirect=` param, the redirect would take place _before_ the page-level client config could sync to the layout, and ultimately crash the page. This was addressed in #13786. While this fixed redirects to the "users" collection, this collection is _already_ included in the client config (soon to be omitted by #13785). So if you redirect to any other collection, the above error occurs. #### Problem The page-level config context is only overriding the `config` property, keeping stale methods from the root config provider. This means calling `getEntityConfig` during this moment in the time would reference the stale config, although `config` itself would be fresh. #### Solution Wrap the page with an entirely new context provider. Do not thread inherited methods from the root provider, this way all new methods get instantiated using the fresh config. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211332845301596
773 lines
27 KiB
TypeScript
773 lines
27 KiB
TypeScript
import type { BrowserContext, Page } from '@playwright/test'
|
|
import type { TypeWithID } from 'payload'
|
|
|
|
import { expect, test } from '@playwright/test'
|
|
import { devUser } from 'credentials.js'
|
|
import { openDocControls } from 'helpers/e2e/openDocControls.js'
|
|
import { openNav } from 'helpers/e2e/toggleNav.js'
|
|
import path from 'path'
|
|
import { wait } from 'payload/shared'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
|
import type { Config, ReadOnlyCollection, RestrictedVersion } from './payload-types.js'
|
|
|
|
import {
|
|
closeNav,
|
|
ensureCompilationIsDone,
|
|
exactText,
|
|
initPageConsoleErrorCatch,
|
|
saveDocAndAssert,
|
|
} from '../helpers.js'
|
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
|
import { login } from '../helpers/e2e/auth/login.js'
|
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
|
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
|
import {
|
|
authSlug,
|
|
createNotUpdateCollectionSlug,
|
|
disabledSlug,
|
|
docLevelAccessSlug,
|
|
fullyRestrictedSlug,
|
|
nonAdminEmail,
|
|
publicUserEmail,
|
|
publicUsersSlug,
|
|
readNotUpdateGlobalSlug,
|
|
readOnlyGlobalSlug,
|
|
readOnlySlug,
|
|
restrictedVersionsAdminPanelSlug,
|
|
restrictedVersionsSlug,
|
|
slug,
|
|
unrestrictedSlug,
|
|
userRestrictedCollectionSlug,
|
|
userRestrictedGlobalSlug,
|
|
} from './shared.js'
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const dirname = path.dirname(filename)
|
|
|
|
/**
|
|
* TODO: Access Control
|
|
*
|
|
* FSK: 'should properly prevent / allow public users from reading a restricted field'
|
|
*
|
|
* Repeat all above for globals
|
|
*/
|
|
|
|
const { beforeAll, beforeEach, describe } = test
|
|
let payload: PayloadTestSDK<Config>
|
|
describe('Access Control', () => {
|
|
let page: Page
|
|
let url: AdminUrlUtil
|
|
let restrictedUrl: AdminUrlUtil
|
|
let unrestrictedURL: AdminUrlUtil
|
|
let readOnlyCollectionUrl: AdminUrlUtil
|
|
let richTextUrl: AdminUrlUtil
|
|
let readOnlyGlobalUrl: AdminUrlUtil
|
|
let restrictedVersionsUrl: AdminUrlUtil
|
|
let restrictedVersionsAdminPanelUrl: AdminUrlUtil
|
|
let userRestrictedCollectionURL: AdminUrlUtil
|
|
let userRestrictedGlobalURL: AdminUrlUtil
|
|
let disabledFields: AdminUrlUtil
|
|
let serverURL: string
|
|
let context: BrowserContext
|
|
let authFields: AdminUrlUtil
|
|
|
|
beforeAll(async ({ browser }, testInfo) => {
|
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
|
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
|
|
|
url = new AdminUrlUtil(serverURL, slug)
|
|
restrictedUrl = new AdminUrlUtil(serverURL, fullyRestrictedSlug)
|
|
richTextUrl = new AdminUrlUtil(serverURL, 'rich-text')
|
|
unrestrictedURL = new AdminUrlUtil(serverURL, unrestrictedSlug)
|
|
readOnlyCollectionUrl = new AdminUrlUtil(serverURL, readOnlySlug)
|
|
readOnlyGlobalUrl = new AdminUrlUtil(serverURL, readOnlySlug)
|
|
restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug)
|
|
restrictedVersionsAdminPanelUrl = new AdminUrlUtil(serverURL, restrictedVersionsAdminPanelSlug)
|
|
userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug)
|
|
userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug)
|
|
disabledFields = new AdminUrlUtil(serverURL, disabledSlug)
|
|
authFields = new AdminUrlUtil(serverURL, authSlug)
|
|
|
|
context = await browser.newContext()
|
|
page = await context.newPage()
|
|
initPageConsoleErrorCatch(page)
|
|
|
|
await ensureCompilationIsDone({ page, serverURL, noAutoLogin: true })
|
|
|
|
await login({ page, serverURL })
|
|
})
|
|
|
|
describe('fields', () => {
|
|
test('field without read access should not show', async () => {
|
|
const { id } = await createDoc({ restrictedField: 'restricted' })
|
|
|
|
await page.goto(url.edit(id))
|
|
|
|
await expect(page.locator('#field-restrictedField')).toHaveCount(0)
|
|
})
|
|
|
|
test('field without read access inside a group should not show', async () => {
|
|
const { id } = await createDoc({ restrictedField: 'restricted' })
|
|
|
|
await page.goto(url.edit(id))
|
|
|
|
await expect(page.locator('#field-group__restrictedGroupText')).toHaveCount(0)
|
|
})
|
|
|
|
test('field without read access inside a collapsible should not show', async () => {
|
|
const { id } = await createDoc({ restrictedField: 'restricted' })
|
|
|
|
await page.goto(url.edit(id))
|
|
|
|
await expect(page.locator('#field-restrictedRowText')).toHaveCount(0)
|
|
})
|
|
|
|
test('field without read access inside a row should not show', async () => {
|
|
const { id } = await createDoc({ restrictedField: 'restricted' })
|
|
|
|
await page.goto(url.edit(id))
|
|
|
|
await expect(page.locator('#field-restrictedCollapsibleText')).toHaveCount(0)
|
|
})
|
|
|
|
test('should not show field without permission', async () => {
|
|
await page.goto(url.account)
|
|
await expect(page.locator('#field-roles')).toBeHidden()
|
|
})
|
|
})
|
|
|
|
describe('rich text', () => {
|
|
test('rich text within block should render as editable', async () => {
|
|
await page.goto(richTextUrl.create)
|
|
|
|
await page.locator('.blocks-field__drawer-toggler').click()
|
|
await page.locator('.thumbnail-card').click()
|
|
const richTextField = page.locator('.rich-text-lexical')
|
|
const contentEditable = richTextField.locator('.ContentEditable__root').first()
|
|
await expect(contentEditable).toBeVisible()
|
|
await contentEditable.click()
|
|
|
|
const typedText = 'Hello, this field is editable!'
|
|
await page.keyboard.type(typedText)
|
|
|
|
await expect(
|
|
page.locator('[data-lexical-text="true"]', {
|
|
hasText: exactText(typedText),
|
|
}),
|
|
).toHaveCount(1)
|
|
})
|
|
|
|
const ensureRegression1FieldsHaveCorrectAccess = async () => {
|
|
await expect(
|
|
page.locator('#field-group1 .rich-text-lexical .ContentEditable__root'),
|
|
).toBeVisible()
|
|
// Wait until the contenteditable is editable
|
|
await expect(
|
|
page.locator('#field-group1 .rich-text-lexical .ContentEditable__root'),
|
|
).toBeEditable()
|
|
|
|
await expect(async () => {
|
|
const isAttached = page.locator('#field-group1 .rich-text-lexical--read-only')
|
|
await expect(isAttached).toBeHidden()
|
|
}).toPass({ timeout: 10000, intervals: [100] })
|
|
await expect(page.locator('#field-group1 #field-group1__text')).toBeEnabled()
|
|
|
|
// Click on button with text Tab1
|
|
await page.locator('.tabs-field__tab-button').getByText('Tab1').click()
|
|
|
|
await expect(
|
|
page.locator('.tabs-field__tab .rich-text-lexical .ContentEditable__root').first(),
|
|
).toBeVisible()
|
|
await expect(
|
|
page.locator('.tabs-field__tab .rich-text-lexical--read-only').first(),
|
|
).not.toBeAttached()
|
|
|
|
await expect(
|
|
page.locator(
|
|
'.tabs-field__tab #field-tab1__blocks2 .rich-text-lexical .ContentEditable__root',
|
|
),
|
|
).toBeVisible()
|
|
await expect(
|
|
page.locator('.tabs-field__tab #field-tab1__blocks2 .rich-text-lexical--read-only'),
|
|
).not.toBeAttached()
|
|
|
|
await expect(
|
|
page.locator('#field-array #array-row-0 .rich-text-lexical .ContentEditable__root'),
|
|
).toBeVisible()
|
|
await expect(
|
|
page.locator('#field-array #array-row-0 .rich-text-lexical--read-only'),
|
|
).not.toBeAttached()
|
|
|
|
await expect(
|
|
page.locator(
|
|
'#field-arrayWithAccessFalse #arrayWithAccessFalse-row-0 .rich-text-lexical .ContentEditable__root',
|
|
),
|
|
).toBeVisible()
|
|
await expect(
|
|
page.locator(
|
|
'#field-arrayWithAccessFalse #arrayWithAccessFalse-row-0 .rich-text-lexical--read-only',
|
|
),
|
|
).toBeVisible()
|
|
|
|
await expect(
|
|
page.locator('#field-blocks .rich-text-lexical .ContentEditable__root'),
|
|
).toBeVisible()
|
|
await expect(page.locator('#field-blocks.rich-text-lexical--read-only')).not.toBeAttached()
|
|
}
|
|
/**
|
|
* This reproduces a bug where certain fields were incorrectly marked as read-only
|
|
*/
|
|
|
|
test('ensure complex collection config fields show up in correct read-only state', async () => {
|
|
const regression1URL = new AdminUrlUtil(serverURL, 'regression1')
|
|
await page.goto(regression1URL.list)
|
|
|
|
await page.locator('.cell-id a').first().click()
|
|
await page.waitForURL(`**/collections/regression1/**`)
|
|
|
|
await ensureRegression1FieldsHaveCorrectAccess()
|
|
|
|
// Edit any field
|
|
await page.locator('#field-group1__text').fill('test!')
|
|
await saveDocAndAssert(page)
|
|
await wait(1000)
|
|
// Ensure fields still have the correct readOnly state. When saving the document, permissions are re-evaluated
|
|
await ensureRegression1FieldsHaveCorrectAccess()
|
|
})
|
|
|
|
const ensureRegression2FieldsHaveCorrectAccess = async () => {
|
|
await expect(
|
|
page.locator('#field-group .rich-text-lexical .ContentEditable__root'),
|
|
).toBeVisible()
|
|
// Wait until the contenteditable is editable
|
|
await expect(
|
|
page.locator('#field-group .rich-text-lexical .ContentEditable__root'),
|
|
).toBeEditable()
|
|
|
|
await expect(async () => {
|
|
const isAttached = page.locator('#field-group .rich-text-lexical--read-only')
|
|
await expect(isAttached).toBeHidden()
|
|
}).toPass({ timeout: 10000, intervals: [100] })
|
|
await expect(page.locator('#field-group #field-group__text')).toBeEnabled()
|
|
|
|
await expect(
|
|
page.locator('#field-array #array-row-0 .rich-text-lexical .ContentEditable__root'),
|
|
).toBeVisible()
|
|
await expect(
|
|
page.locator('#field-array #array-row-0 .rich-text-lexical--read-only'),
|
|
).toBeVisible() // => is read-only
|
|
}
|
|
|
|
/**
|
|
* This reproduces a bug where certain fields were incorrectly marked as read-only
|
|
*/
|
|
|
|
test('ensure complex collection config fields show up in correct read-only state 2', async () => {
|
|
const regression2URL = new AdminUrlUtil(serverURL, 'regression2')
|
|
await page.goto(regression2URL.list)
|
|
|
|
await page.locator('.cell-id a').first().click()
|
|
await page.waitForURL(`**/collections/regression2/**`)
|
|
|
|
await ensureRegression2FieldsHaveCorrectAccess()
|
|
|
|
// Edit any field
|
|
await page.locator('#field-group__text').fill('test!')
|
|
await saveDocAndAssert(page)
|
|
await wait(1000)
|
|
|
|
// Ensure fields still have the correct readOnly state. When saving the document, permissions are re-evaluated
|
|
await ensureRegression2FieldsHaveCorrectAccess()
|
|
})
|
|
})
|
|
|
|
describe('collection — fully restricted', () => {
|
|
let existingDoc: ReadOnlyCollection
|
|
|
|
beforeAll(async () => {
|
|
existingDoc = await payload.create({
|
|
collection: fullyRestrictedSlug,
|
|
data: {
|
|
name: 'name',
|
|
},
|
|
})
|
|
})
|
|
|
|
test('should not show in card list', async () => {
|
|
await page.goto(url.admin)
|
|
await expect(page.locator(`#card-${fullyRestrictedSlug}`)).toHaveCount(0)
|
|
})
|
|
|
|
test('should not show in nav', async () => {
|
|
await page.goto(url.admin)
|
|
await openNav(page)
|
|
|
|
await expect(
|
|
page.locator('.nav a', {
|
|
hasText: exactText('Restricteds'),
|
|
}),
|
|
).toHaveCount(0)
|
|
})
|
|
|
|
test('should not have list url', async () => {
|
|
const errors = []
|
|
|
|
page.on('console', (exception) => {
|
|
errors.push(exception)
|
|
})
|
|
|
|
await page.goto(restrictedUrl.list)
|
|
|
|
// eslint-disable-next-line payload/no-flaky-assertions
|
|
expect(errors).not.toHaveLength(0)
|
|
})
|
|
|
|
test('should not have create url', async () => {
|
|
await page.goto(restrictedUrl.create)
|
|
await expect(page.locator('.not-found')).toBeVisible()
|
|
})
|
|
|
|
test('should not have access to existing doc', async () => {
|
|
await page.goto(restrictedUrl.edit(existingDoc.id))
|
|
await expect(page.locator('.not-found')).toBeVisible()
|
|
})
|
|
})
|
|
|
|
describe('collection — read-only', () => {
|
|
let existingDoc: ReadOnlyCollection
|
|
|
|
beforeAll(async () => {
|
|
existingDoc = await payload.create({
|
|
collection: readOnlySlug,
|
|
data: {
|
|
name: 'name',
|
|
},
|
|
})
|
|
})
|
|
|
|
test('should show in card list', async () => {
|
|
await page.goto(url.admin)
|
|
await expect(page.locator(`#card-${readOnlySlug}`)).toHaveCount(1)
|
|
})
|
|
|
|
test('should show in nav', async () => {
|
|
await page.goto(url.admin)
|
|
await expect(page.locator(`.nav a[href="/admin/collections/${readOnlySlug}"]`)).toHaveCount(1)
|
|
})
|
|
|
|
test('should have collection url', async () => {
|
|
await page.goto(readOnlyCollectionUrl.list)
|
|
await expect(page).toHaveURL(new RegExp(`${readOnlyCollectionUrl.list}.*`)) // will redirect to ?limit=10 at the end, so we have to use a wildcard at the end
|
|
})
|
|
|
|
test('should not have "Create New" button', async () => {
|
|
await page.goto(readOnlyCollectionUrl.create)
|
|
await expect(page.locator('.collection-list__header a')).toHaveCount(0)
|
|
})
|
|
|
|
test('should not have quick create button', async () => {
|
|
await page.goto(url.admin)
|
|
await expect(page.locator(`#card-${readOnlySlug}`)).not.toHaveClass('card__actions')
|
|
})
|
|
|
|
test('should not display actions on edit view', async () => {
|
|
await page.goto(readOnlyCollectionUrl.edit(existingDoc.id))
|
|
await expect(page.locator('.collection-edit__collection-actions li')).toHaveCount(0)
|
|
})
|
|
|
|
test('fields should be read-only', async () => {
|
|
await page.goto(readOnlyCollectionUrl.edit(existingDoc.id))
|
|
await expect(page.locator('#field-name')).toBeDisabled()
|
|
|
|
await page.goto(readOnlyGlobalUrl.global(readOnlyGlobalSlug))
|
|
await expect(page.locator('#field-name')).toBeDisabled()
|
|
})
|
|
|
|
test('should not render dot menu popup when `create` and `delete` access control is set to false', async () => {
|
|
await page.goto(readOnlyCollectionUrl.edit(existingDoc.id))
|
|
await expect(page.locator('.collection-edit .doc-controls .doc-controls__popup')).toBeHidden()
|
|
})
|
|
})
|
|
|
|
describe('collection — create but not edit', () => {
|
|
test('should not show edit button', async () => {
|
|
const createNotUpdateURL = new AdminUrlUtil(serverURL, createNotUpdateCollectionSlug)
|
|
await page.goto(createNotUpdateURL.create)
|
|
await expect(page.locator('#field-name')).toBeVisible()
|
|
await page.locator('#field-name').fill('name')
|
|
await expect(page.locator('#field-name')).toHaveValue('name')
|
|
await expect(page.locator('#action-save')).toBeVisible()
|
|
await page.locator('#action-save').click()
|
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
|
await expect(page.locator('#action-save')).toBeHidden()
|
|
await expect(page.locator('#field-name')).toBeDisabled()
|
|
})
|
|
|
|
test('should maintain access control in document drawer', async () => {
|
|
const unrestrictedDoc = await payload.create({
|
|
collection: unrestrictedSlug,
|
|
data: {
|
|
name: 'unrestricted-123',
|
|
},
|
|
})
|
|
|
|
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id.toString()))
|
|
|
|
const addDocButton = page.locator(
|
|
'#createNotUpdateDocs-add-new button.relationship-add-new__add-button.doc-drawer__toggler',
|
|
)
|
|
|
|
await expect(addDocButton).toBeVisible()
|
|
await addDocButton.click()
|
|
const documentDrawer = page.locator(`[id^=doc-drawer_${createNotUpdateCollectionSlug}_1_]`)
|
|
await expect(documentDrawer).toBeVisible()
|
|
await expect(documentDrawer.locator('#action-save')).toBeVisible()
|
|
|
|
await documentDrawer.locator('#field-name').fill('name')
|
|
await expect(documentDrawer.locator('#field-name')).toHaveValue('name')
|
|
|
|
await saveDocAndAssert(
|
|
page,
|
|
`[id^=doc-drawer_${createNotUpdateCollectionSlug}_1_] #action-save`,
|
|
)
|
|
|
|
await expect(documentDrawer.locator('#action-save')).toBeHidden()
|
|
await expect(documentDrawer.locator('#field-name')).toBeDisabled()
|
|
})
|
|
})
|
|
|
|
describe('global — read but not update', () => {
|
|
test('should not show edit button', async () => {
|
|
const createNotUpdateURL = new AdminUrlUtil(serverURL, readNotUpdateGlobalSlug)
|
|
await page.goto(createNotUpdateURL.global(readNotUpdateGlobalSlug))
|
|
await expect(page.locator('#field-name')).toBeVisible()
|
|
await expect(page.locator('#field-name')).toBeDisabled()
|
|
await expect(page.locator('#action-save')).toBeHidden()
|
|
})
|
|
})
|
|
|
|
describe('dynamic update access', () => {
|
|
describe('collection', () => {
|
|
test('should restrict update access based on document field', async () => {
|
|
await page.goto(userRestrictedCollectionURL.create)
|
|
await expect(page.locator('#field-name')).toBeVisible()
|
|
await page.locator('#field-name').fill('anonymous@email.com')
|
|
await page.locator('#action-save').click()
|
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
|
await expect(page.locator('#field-name')).toBeDisabled()
|
|
await expect(page.locator('#action-save')).toBeHidden()
|
|
|
|
await page.goto(userRestrictedCollectionURL.create)
|
|
await expect(page.locator('#field-name')).toBeVisible()
|
|
await page.locator('#field-name').fill(devUser.email)
|
|
await page.locator('#action-save').click()
|
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
|
await expect(page.locator('#field-name')).toBeEnabled()
|
|
await expect(page.locator('#action-save')).toBeVisible()
|
|
})
|
|
|
|
test('maintain access control in document drawer', async () => {
|
|
const unrestrictedDoc = await payload.create({
|
|
collection: unrestrictedSlug,
|
|
data: {
|
|
name: 'unrestricted-123',
|
|
},
|
|
})
|
|
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id.toString()))
|
|
const field = page.locator('#field-userRestrictedDocs')
|
|
await expect(field.locator('input')).toBeEnabled()
|
|
const addDocButton = page.locator(
|
|
'#userRestrictedDocs-add-new button.relationship-add-new__add-button.doc-drawer__toggler',
|
|
)
|
|
await addDocButton.click()
|
|
const documentDrawer = page.locator('[id^=doc-drawer_user-restricted-collection_1_]')
|
|
await expect(documentDrawer).toBeVisible()
|
|
await documentDrawer.locator('#field-name').fill('anonymous@email.com')
|
|
await wait(500)
|
|
await documentDrawer.locator('#action-save').click()
|
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
|
await expect(documentDrawer.locator('#field-name')).toBeDisabled()
|
|
await documentDrawer.locator('button.doc-drawer__header-close').click()
|
|
await expect(documentDrawer).toBeHidden()
|
|
await addDocButton.click()
|
|
const documentDrawer2 = page.locator('[id^=doc-drawer_user-restricted-collection_1_]')
|
|
await expect(documentDrawer2).toBeVisible()
|
|
await documentDrawer2.locator('#field-name').fill('dev@payloadcms.com')
|
|
await documentDrawer2.locator('#action-save').click()
|
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
|
await expect(documentDrawer2.locator('#field-name')).toBeEnabled()
|
|
})
|
|
})
|
|
|
|
describe('global', () => {
|
|
test('should restrict update access based on document field', async () => {
|
|
await payload.updateGlobal({
|
|
slug: userRestrictedGlobalSlug,
|
|
data: {
|
|
name: 'dev@payloadcms.com',
|
|
},
|
|
})
|
|
|
|
await page.goto(userRestrictedGlobalURL.global(userRestrictedGlobalSlug))
|
|
await expect(page.locator('#field-name')).toBeVisible()
|
|
await expect(page.locator('#field-name')).toHaveValue(devUser.email)
|
|
await expect(page.locator('#field-name')).toBeEnabled()
|
|
await page.locator('#field-name').fill('anonymous@email.com')
|
|
await page.locator('#action-save').click()
|
|
await expect(page.locator('.payload-toast-container')).toContainText(
|
|
'You are not allowed to perform this action',
|
|
)
|
|
|
|
await payload.updateGlobal({
|
|
slug: userRestrictedGlobalSlug,
|
|
data: {
|
|
name: 'anonymous@payloadcms.com',
|
|
},
|
|
})
|
|
|
|
await page.goto(userRestrictedGlobalURL.global(userRestrictedGlobalSlug))
|
|
await expect(page.locator('#field-name')).toBeDisabled()
|
|
await expect(page.locator('#action-save')).toBeHidden()
|
|
})
|
|
|
|
test('should restrict access based on user settings', async () => {
|
|
const url = `${serverURL}/admin/globals/settings`
|
|
await page.goto(url)
|
|
await openNav(page)
|
|
await expect(page.locator('#nav-global-settings')).toBeVisible()
|
|
await expect(page.locator('#nav-global-test')).toBeHidden()
|
|
await closeNav(page)
|
|
await page.locator('.checkbox-input:has(#field-test) input').check()
|
|
await saveDocAndAssert(page)
|
|
await openNav(page)
|
|
const globalTest = page.locator('#nav-global-test')
|
|
await expect(async () => await globalTest.isVisible()).toPass({
|
|
timeout: POLL_TOPASS_TIMEOUT,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('collection — restricted versions', () => {
|
|
let existingDoc: RestrictedVersion
|
|
|
|
beforeAll(async () => {
|
|
existingDoc = await payload.create({
|
|
collection: restrictedVersionsAdminPanelSlug,
|
|
data: {
|
|
name: 'name',
|
|
},
|
|
})
|
|
|
|
await payload.update({
|
|
collection: restrictedVersionsAdminPanelSlug,
|
|
id: existingDoc.id,
|
|
data: {
|
|
hidden: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
test('versions tab should not show', async () => {
|
|
await page.goto(restrictedVersionsAdminPanelUrl.edit(existingDoc.id))
|
|
await page.locator('.doc-tabs__tabs').getByLabel('Versions').click()
|
|
const rows = page.locator('.versions table tbody tr')
|
|
await expect(rows).toHaveCount(1)
|
|
})
|
|
})
|
|
|
|
describe('doc level access', () => {
|
|
let existingDoc: ReadOnlyCollection
|
|
let docLevelAccessURL: AdminUrlUtil
|
|
|
|
beforeAll(async () => {
|
|
docLevelAccessURL = new AdminUrlUtil(serverURL, docLevelAccessSlug)
|
|
|
|
existingDoc = await payload.create({
|
|
collection: docLevelAccessSlug,
|
|
data: {
|
|
approvedForRemoval: false,
|
|
approvedTitle: 'Title',
|
|
lockTitle: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
test('should disable field based on document data', async () => {
|
|
await page.goto(docLevelAccessURL.edit(existingDoc.id))
|
|
const isDisabled = page.locator('#field-approvedTitle')
|
|
await expect(isDisabled).toBeDisabled()
|
|
})
|
|
|
|
test('should disable operation based on document data', async () => {
|
|
await page.goto(docLevelAccessURL.edit(existingDoc.id))
|
|
await openDocControls(page)
|
|
await expect(page.locator('#action-delete')).toBeHidden()
|
|
await page.locator('#field-approvedForRemoval').check()
|
|
await saveDocAndAssert(page)
|
|
await openDocControls(page)
|
|
await expect(page.locator('#action-delete')).toBeVisible()
|
|
})
|
|
})
|
|
|
|
describe('admin access', () => {
|
|
test('unauthenticated users should not have access to the admin panel', async () => {
|
|
await page.goto(url.logout)
|
|
|
|
await expect(page.locator('.payload-toast-container')).toContainText(
|
|
'You have been logged out successfully.',
|
|
)
|
|
|
|
await expect(page.locator('form.login__form')).toBeVisible()
|
|
|
|
await page.goto(url.admin)
|
|
|
|
// wait for redirect to login
|
|
await page.waitForURL(url.login)
|
|
|
|
expect(page.url()).toEqual(url.login)
|
|
})
|
|
|
|
test('non-admin users should not have access to the admin panel', async () => {
|
|
await page.goto(url.logout)
|
|
|
|
await login({
|
|
data: {
|
|
email: nonAdminEmail,
|
|
password: 'test',
|
|
},
|
|
page,
|
|
serverURL,
|
|
})
|
|
|
|
await expect(page.locator('.unauthorized .form-header h1')).toHaveText(
|
|
'Unauthorized, this user does not have access to the admin panel.',
|
|
)
|
|
|
|
await page.goto(url.logout)
|
|
|
|
await expect(page.locator('.payload-toast-container')).toContainText(
|
|
'You have been logged out successfully.',
|
|
)
|
|
|
|
await expect(page.locator('form.login__form')).toBeVisible()
|
|
})
|
|
|
|
test('public users should not have access to access admin', async () => {
|
|
await page.goto(url.logout)
|
|
|
|
const user = await payload.login({
|
|
collection: publicUsersSlug,
|
|
data: {
|
|
email: publicUserEmail,
|
|
password: devUser.password,
|
|
},
|
|
})
|
|
|
|
await context.addCookies([
|
|
{
|
|
name: 'payload-token',
|
|
value: user.token,
|
|
domain: 'localhost',
|
|
path: '/',
|
|
httpOnly: true,
|
|
secure: true,
|
|
},
|
|
])
|
|
|
|
await page.reload()
|
|
|
|
await page.goto(url.admin)
|
|
|
|
// await for redirect to unauthorized
|
|
await page.waitForURL(/unauthorized$/)
|
|
|
|
await expect(page.locator('.unauthorized .form-header h1')).toHaveText(
|
|
'Unauthorized, this user does not have access to the admin panel.',
|
|
)
|
|
|
|
await page.goto(url.logout)
|
|
|
|
await expect(page.locator('.payload-toast-container')).toContainText(
|
|
'You have been logged out successfully.',
|
|
)
|
|
|
|
await expect(page.locator('form.login__form')).toBeVisible()
|
|
})
|
|
})
|
|
|
|
describe('read-only from access control', () => {
|
|
beforeAll(async () => {
|
|
await login({
|
|
data: {
|
|
email: devUser.email,
|
|
password: devUser.password,
|
|
},
|
|
page,
|
|
serverURL,
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|
|
|
|
describe('restricting update access to auth fields', () => {
|
|
let existingDoc: ReadOnlyCollection
|
|
beforeAll(async () => {
|
|
existingDoc = await payload.create({
|
|
collection: authSlug,
|
|
data: {
|
|
email: 'test@payloadcms.com',
|
|
password: 'test',
|
|
},
|
|
})
|
|
})
|
|
test('should show email as readonly when user does not have update permission', async () => {
|
|
await page.goto(authFields.edit(existingDoc.id))
|
|
const emailField = page.locator('#field-email')
|
|
await expect(emailField).toBeVisible()
|
|
await expect(emailField).toBeDisabled()
|
|
})
|
|
|
|
test('should hide Change Password button when user does not have update permission', async () => {
|
|
await page.goto(authFields.edit(existingDoc.id))
|
|
const passwordField = page.locator('#field-password')
|
|
await expect(passwordField).toBeHidden()
|
|
const changePasswordButton = page.locator('#change-password')
|
|
await expect(changePasswordButton).toBeHidden()
|
|
})
|
|
})
|
|
})
|
|
|
|
async function createDoc(data: any): Promise<Record<string, unknown> & TypeWithID> {
|
|
return payload.create({
|
|
collection: slug,
|
|
data,
|
|
}) as any as Promise<Record<string, unknown> & TypeWithID>
|
|
}
|