test(access-control): restricted and read-only (#754)

This commit is contained in:
Elliot DeNolf
2022-07-14 14:33:54 -07:00
committed by GitHub
parent baf4664073
commit f7a81d70ac
5 changed files with 172 additions and 26 deletions

View File

@@ -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',
},
});
},
}); });

View File

@@ -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();
}); });
}); });
}); });

View 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;
}

View File

@@ -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');

View File

@@ -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