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 type { ReadOnlyCollection } from './payload-types';
|
||||
|
||||
export const slug = 'access-controls';
|
||||
export const readOnlySlug = 'read-only-collection';
|
||||
export const restrictedSlug = 'restricted';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
@@ -17,20 +21,52 @@ export default buildConfig({
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'restricted',
|
||||
slug: restrictedSlug,
|
||||
fields: [],
|
||||
access: {
|
||||
create: () => 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) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
});
|
||||
|
||||
await payload.create({
|
||||
collection: slug,
|
||||
data: {
|
||||
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 { AdminUrlUtil } from '../../helpers/adminUrlUtil';
|
||||
import { initPayloadE2E } from '../../helpers/configHelpers';
|
||||
import { firstRegister } from '../helpers';
|
||||
import { slug } from './config';
|
||||
import { login } from '../helpers';
|
||||
import { readOnlySlug, restrictedSlug, slug } from './config';
|
||||
import type { ReadOnlyCollection } from './payload-types';
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* 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
|
||||
*
|
||||
* FSK: 'should properly prevent / allow public users from reading a restricted field'
|
||||
@@ -26,26 +18,26 @@ import { slug } from './config';
|
||||
*/
|
||||
|
||||
const { beforeAll, describe } = test;
|
||||
let url: AdminUrlUtil;
|
||||
|
||||
describe('access control', () => {
|
||||
let page: Page;
|
||||
let url: AdminUrlUtil;
|
||||
let restrictedUrl: AdminUrlUtil;
|
||||
let readoOnlyUrl: AdminUrlUtil;
|
||||
|
||||
beforeAll(async ({ browser }) => {
|
||||
const { serverURL } = await initPayloadE2E(__dirname);
|
||||
// await clearDocs(); // Clear any seeded data from onInit
|
||||
|
||||
url = new AdminUrlUtil(serverURL, slug);
|
||||
restrictedUrl = new AdminUrlUtil(serverURL, restrictedSlug);
|
||||
readoOnlyUrl = new AdminUrlUtil(serverURL, readOnlySlug);
|
||||
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
|
||||
await firstRegister({ page, serverURL });
|
||||
await login({ page, serverURL });
|
||||
});
|
||||
|
||||
// afterEach(async () => {
|
||||
// });
|
||||
|
||||
test('field without read access should not show', async () => {
|
||||
const { id } = await createDoc({ restrictedField: 'restricted' });
|
||||
|
||||
@@ -55,9 +47,20 @@ describe('access control', () => {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
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 () => {
|
||||
@@ -65,11 +68,67 @@ describe('access control', () => {
|
||||
await expect(page.locator('.nav >> a:has-text("Restricteds")')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should not have collection url', async () => {
|
||||
await page.goto(url.list);
|
||||
await page.locator('text=Nothing found').click();
|
||||
await page.locator('a:has-text("Back to Dashboard")').click();
|
||||
await expect(page).toHaveURL(url.admin);
|
||||
test('should not have list url', async () => {
|
||||
await page.goto(restrictedUrl.list);
|
||||
await expect(page.locator('.unauthorized')).toBeVisible();
|
||||
});
|
||||
|
||||
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
|
||||
await expect(field).toHaveText(relationWithTitle.name);
|
||||
await page.screenshot({ path: './bad.png' });
|
||||
|
||||
await field.click({ delay: 100 });
|
||||
const options = page.locator('.rs__option');
|
||||
|
||||
@@ -5,6 +5,7 @@ import express from 'express';
|
||||
import type { CollectionConfig } from '../../src/collections/config/types';
|
||||
import type { InitOptions } from '../../src/config/types';
|
||||
import payload from '../../src';
|
||||
import { devUser } from '../credentials';
|
||||
|
||||
type Options = {
|
||||
__dirname: string
|
||||
|
||||
Reference in New Issue
Block a user