test(access-control): restricted and read-only (#754)
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
|
import { devUser } from '../../credentials';
|
||||||
import { buildConfig } from '../buildConfig';
|
import { buildConfig } from '../buildConfig';
|
||||||
|
import type { ReadOnlyCollection } from './payload-types';
|
||||||
|
|
||||||
export const slug = 'access-controls';
|
export const slug = 'access-controls';
|
||||||
|
export const readOnlySlug = 'read-only-collection';
|
||||||
|
export const restrictedSlug = 'restricted';
|
||||||
|
|
||||||
export default buildConfig({
|
export default buildConfig({
|
||||||
collections: [
|
collections: [
|
||||||
@@ -17,20 +21,52 @@ export default buildConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'restricted',
|
slug: restrictedSlug,
|
||||||
fields: [],
|
fields: [],
|
||||||
access: {
|
access: {
|
||||||
|
create: () => false,
|
||||||
read: () => false,
|
read: () => false,
|
||||||
|
update: () => false,
|
||||||
|
delete: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: readOnlySlug,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
access: {
|
||||||
|
create: () => false,
|
||||||
|
read: () => true,
|
||||||
|
update: () => false,
|
||||||
|
delete: () => false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: slug,
|
collection: slug,
|
||||||
data: {
|
data: {
|
||||||
restrictedField: 'restricted',
|
restrictedField: 'restricted',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
|
await payload.create<ReadOnlyCollection>({
|
||||||
|
collection: readOnlySlug,
|
||||||
|
data: {
|
||||||
|
name: 'read-only',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,21 +3,13 @@ import { expect, test } from '@playwright/test';
|
|||||||
import payload from '../../../src';
|
import payload from '../../../src';
|
||||||
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
import { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||||
import { initPayloadE2E } from '../../helpers/configHelpers';
|
import { initPayloadE2E } from '../../helpers/configHelpers';
|
||||||
import { firstRegister } from '../helpers';
|
import { login } from '../helpers';
|
||||||
import { slug } from './config';
|
import { readOnlySlug, restrictedSlug, slug } from './config';
|
||||||
|
import type { ReadOnlyCollection } from './payload-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Access Control
|
* TODO: Access Control
|
||||||
* - [x] restricted collections not shown
|
|
||||||
* - no sidebar link
|
|
||||||
* - no route
|
|
||||||
* - no card
|
|
||||||
* [x] field without read access should not show
|
|
||||||
* prevent user from logging in (canAccessAdmin)
|
* prevent user from logging in (canAccessAdmin)
|
||||||
* no create controls if no access
|
|
||||||
* no update control if no update
|
|
||||||
* - check fields are rendered as readonly
|
|
||||||
* no delete control if no access
|
|
||||||
* no version controls is no access
|
* no version controls is no access
|
||||||
*
|
*
|
||||||
* FSK: 'should properly prevent / allow public users from reading a restricted field'
|
* FSK: 'should properly prevent / allow public users from reading a restricted field'
|
||||||
@@ -26,26 +18,26 @@ import { slug } from './config';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { beforeAll, describe } = test;
|
const { beforeAll, describe } = test;
|
||||||
let url: AdminUrlUtil;
|
|
||||||
|
|
||||||
describe('access control', () => {
|
describe('access control', () => {
|
||||||
let page: Page;
|
let page: Page;
|
||||||
|
let url: AdminUrlUtil;
|
||||||
|
let restrictedUrl: AdminUrlUtil;
|
||||||
|
let readoOnlyUrl: AdminUrlUtil;
|
||||||
|
|
||||||
beforeAll(async ({ browser }) => {
|
beforeAll(async ({ browser }) => {
|
||||||
const { serverURL } = await initPayloadE2E(__dirname);
|
const { serverURL } = await initPayloadE2E(__dirname);
|
||||||
// await clearDocs(); // Clear any seeded data from onInit
|
|
||||||
|
|
||||||
url = new AdminUrlUtil(serverURL, slug);
|
url = new AdminUrlUtil(serverURL, slug);
|
||||||
|
restrictedUrl = new AdminUrlUtil(serverURL, restrictedSlug);
|
||||||
|
readoOnlyUrl = new AdminUrlUtil(serverURL, readOnlySlug);
|
||||||
|
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
page = await context.newPage();
|
page = await context.newPage();
|
||||||
|
|
||||||
await firstRegister({ page, serverURL });
|
await login({ page, serverURL });
|
||||||
});
|
});
|
||||||
|
|
||||||
// afterEach(async () => {
|
|
||||||
// });
|
|
||||||
|
|
||||||
test('field without read access should not show', async () => {
|
test('field without read access should not show', async () => {
|
||||||
const { id } = await createDoc({ restrictedField: 'restricted' });
|
const { id } = await createDoc({ restrictedField: 'restricted' });
|
||||||
|
|
||||||
@@ -55,9 +47,20 @@ describe('access control', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('restricted collection', () => {
|
describe('restricted collection', () => {
|
||||||
|
let existingDoc: ReadOnlyCollection;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
existingDoc = await payload.create<ReadOnlyCollection>({
|
||||||
|
collection: readOnlySlug,
|
||||||
|
data: {
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should not show in card list', async () => {
|
test('should not show in card list', async () => {
|
||||||
await page.goto(url.admin);
|
await page.goto(url.admin);
|
||||||
await expect(page.locator('.dashboard__card-list >> text=Restricteds')).toHaveCount(0);
|
await expect(page.locator(`#card-${restrictedSlug}`)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not show in nav', async () => {
|
test('should not show in nav', async () => {
|
||||||
@@ -65,11 +68,67 @@ describe('access control', () => {
|
|||||||
await expect(page.locator('.nav >> a:has-text("Restricteds")')).toHaveCount(0);
|
await expect(page.locator('.nav >> a:has-text("Restricteds")')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not have collection url', async () => {
|
test('should not have list url', async () => {
|
||||||
await page.goto(url.list);
|
await page.goto(restrictedUrl.list);
|
||||||
await page.locator('text=Nothing found').click();
|
await expect(page.locator('.unauthorized')).toBeVisible();
|
||||||
await page.locator('a:has-text("Back to Dashboard")').click();
|
});
|
||||||
await expect(page).toHaveURL(url.admin);
|
|
||||||
|
test('should not have create url', async () => {
|
||||||
|
await page.goto(restrictedUrl.create);
|
||||||
|
await expect(page.locator('.unauthorized')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have access to existing doc', async () => {
|
||||||
|
await page.goto(restrictedUrl.edit(existingDoc.id));
|
||||||
|
await expect(page.locator('.unauthorized')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('read-only collection', () => {
|
||||||
|
let existingDoc: ReadOnlyCollection;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
existingDoc = await payload.create<ReadOnlyCollection>({
|
||||||
|
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(readoOnlyUrl.list);
|
||||||
|
await expect(page).toHaveURL(readoOnlyUrl.list); // no redirect
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have "Create New" button', async () => {
|
||||||
|
await page.goto(readoOnlyUrl.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('edit view should not have buttons', async () => {
|
||||||
|
await page.goto(readoOnlyUrl.edit(existingDoc.id));
|
||||||
|
await expect(page.locator('.collection-edit__collection-actions li')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fields should be read-only', async () => {
|
||||||
|
await page.goto(readoOnlyUrl.edit(existingDoc.id));
|
||||||
|
await expect(page.locator('#field-name')).toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
51
test/e2e/access-control/payload-types.ts
Normal file
51
test/e2e/access-control/payload-types.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload CMS.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Config {}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "access-controls".
|
||||||
|
*/
|
||||||
|
export interface AccessControl {
|
||||||
|
id: string;
|
||||||
|
restrictedField?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "restricted".
|
||||||
|
*/
|
||||||
|
export interface Restricted {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "read-only-collection".
|
||||||
|
*/
|
||||||
|
export interface ReadOnlyCollection {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users".
|
||||||
|
*/
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email?: string;
|
||||||
|
resetPasswordToken?: string;
|
||||||
|
resetPasswordExpiration?: string;
|
||||||
|
loginAttempts?: number;
|
||||||
|
lockUntil?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
@@ -208,7 +208,6 @@ describe('fields - relationship', () => {
|
|||||||
|
|
||||||
// Check existing relationship for correct title
|
// Check existing relationship for correct title
|
||||||
await expect(field).toHaveText(relationWithTitle.name);
|
await expect(field).toHaveText(relationWithTitle.name);
|
||||||
await page.screenshot({ path: './bad.png' });
|
|
||||||
|
|
||||||
await field.click({ delay: 100 });
|
await field.click({ delay: 100 });
|
||||||
const options = page.locator('.rs__option');
|
const options = page.locator('.rs__option');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import express from 'express';
|
|||||||
import type { CollectionConfig } from '../../src/collections/config/types';
|
import type { CollectionConfig } from '../../src/collections/config/types';
|
||||||
import type { InitOptions } from '../../src/config/types';
|
import type { InitOptions } from '../../src/config/types';
|
||||||
import payload from '../../src';
|
import payload from '../../src';
|
||||||
|
import { devUser } from '../credentials';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
__dirname: string
|
__dirname: string
|
||||||
|
|||||||
Reference in New Issue
Block a user