Compare commits

...

1 Commits

Author SHA1 Message Date
Dan Ribbens
d5e84c1534 fix: restrict access control on unnamed tabs 2023-05-23 10:04:35 -04:00
7 changed files with 145 additions and 13 deletions

View File

@@ -215,7 +215,9 @@ export const collapsible = baseField.keys({
});
const tab = baseField.keys({
name: joi.string().when('localized', { is: joi.exist(), then: joi.required() }),
name: joi.string()
.when('localized', { is: joi.exist(), then: joi.required() })
.when('access', { is: joi.exist(), then: joi.required() }),
localized: joi.boolean(),
label: joi.alternatives().try(
joi.string(),

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-use-before-define */
import { CSSProperties } from 'react';
import { Editor } from 'slate';
import type { TFunction, i18n as Ii18n } from 'i18next';
import type { i18n as Ii18n, TFunction } from 'i18next';
import type { EditorProps } from '@monaco-editor/react';
import { Operation, Where } from '../../types';
import { SanitizedConfig } from '../../config/types';
@@ -214,6 +214,7 @@ export type NamedTab = TabBase
export type UnnamedTab = Omit<TabBase, 'name'> & {
label: Record<string, string> | string
localized?: never
access?: never
}
export type Tab = NamedTab | UnnamedTab

View File

@@ -155,6 +155,15 @@ export function getEntityPolicies<T extends Args>(args: T): ReturnType<T> {
field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
if (!mutablePolicies[tab.name]) mutablePolicies[tab.name] = { fields: {} };
if (tab.access && typeof tab.access[operation] === 'function') {
promises.push(createAccessPromise({
policiesObj: mutablePolicies[tab.name],
access: tab.access[operation],
operation,
disableWhere: true,
accessLevel: 'field',
}));
}
executeFieldPolicies({
policiesObj: mutablePolicies[tab.name],
fields: tab.fields,

View File

@@ -112,6 +112,44 @@ export default buildConfig({
},
],
},
{
type: 'tabs',
tabs: [
// this tab should not be visible in the admin ui
{
name: 'restrictedTab',
label: 'Restricted Tab',
// access should work the same as a group field
access: {
create: () => false,
read: () => false,
update: () => false,
},
fields: [
// the field will be visible even though the parent tab should restrict it
{
name: 'test',
type: 'text',
},
],
},
{
// this tab is visible and can be clicked, even though there isn't a field available
label: 'Restricted Fields',
fields: [
{
name: 'tabText',
type: 'text',
access: {
create: () => false,
read: () => false,
update: () => false,
},
},
],
},
],
},
],
},
{

View File

@@ -4,7 +4,14 @@ import payload from '../../src';
import { AdminUrlUtil } from '../helpers/adminUrlUtil';
import { initPayloadE2E } from '../helpers/configHelpers';
import { login } from '../helpers';
import { restrictedVersionsSlug, readOnlySlug, restrictedSlug, slug, docLevelAccessSlug, unrestrictedSlug } from './config';
import {
docLevelAccessSlug,
readOnlySlug,
restrictedSlug,
restrictedVersionsSlug,
slug,
unrestrictedSlug,
} from './config';
import type { ReadOnlyCollection, RestrictedVersion } from './payload-types';
import wait from '../../src/utilities/wait';
@@ -73,6 +80,24 @@ describe('access control', () => {
await expect(page.locator('#field-restrictedCollapsibleText')).toHaveCount(0);
});
test('field without read access inside a tab should not show', async () => {
const { id } = await createDoc({});
await page.goto(url.edit(id));
// TODO: this shouldn't be passing right now, the tab exists
await expect(page.getByText('Restricted Tab')).toBeHidden();
});
test('tab without read access should not show', async () => {
const { id } = await createDoc({ });
await page.goto(url.edit(id));
// TODO: this shouldn't be passing right now, the tab exists
await expect(page.getByText('Restricted Fields')).toBeHidden();
});
describe('restricted collection', () => {
let existingDoc: ReadOnlyCollection;

View File

@@ -157,6 +157,34 @@ describe('Access Control', () => {
expect(retrievedDoc.restrictedField).toBeUndefined();
});
it('field within restricted tab should not show', async () => {
const { id } = await payload.create({
collection: slug,
overrideAccess: true,
data: {
restrictedTab: { test: 'restricted' },
},
});
const retrievedDoc = await payload.findByID({ collection: slug, id, overrideAccess: false });
expect(retrievedDoc.restrictedTab).toBeUndefined();
});
it('field within tab should not show', async () => {
const { id } = await payload.create({
collection: slug,
overrideAccess: true,
data: {
tabText: 'restricted',
},
});
const retrievedDoc = await payload.findByID({ collection: slug, id, overrideAccess: false });
expect(retrievedDoc.tabText).toBeUndefined();
});
it('field without read access should not show when overrideAccess: true', async () => {
const { id, restrictedField } = await createDoc<Post>({ restrictedField: 'restricted' });

View File

@@ -9,25 +9,28 @@ export interface Config {
collections: {
users: User;
posts: Post;
unrestricted: Unrestricted;
restricted: Restricted;
'read-only-collection': ReadOnlyCollection;
'user-restricted': UserRestricted;
'restricted-versions': RestrictedVersion;
'sibling-data': SiblingDatum;
'rely-on-request-headers': RelyOnRequestHeader;
'doc-level-access': DocLevelAccess;
'hidden-fields': HiddenField;
'hidden-access': HiddenAccess;
};
globals: {};
}
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email?: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
loginAttempts?: number;
lockUntil?: string;
createdAt: string;
updatedAt: string;
password?: string;
}
export interface Post {
@@ -38,26 +41,44 @@ export interface Post {
};
restrictedRowText?: string;
restrictedCollapsibleText?: string;
createdAt: string;
restrictedTab: {
test?: string;
};
test?: string;
updatedAt: string;
createdAt: string;
}
export interface Unrestricted {
id: string;
name?: string;
userRestrictedDocs?: string[] | UserRestricted[];
updatedAt: string;
createdAt: string;
}
export interface UserRestricted {
id: string;
name?: string;
updatedAt: string;
createdAt: string;
}
export interface Restricted {
id: string;
name?: string;
createdAt: string;
updatedAt: string;
createdAt: string;
}
export interface ReadOnlyCollection {
id: string;
name?: string;
createdAt: string;
updatedAt: string;
createdAt: string;
}
export interface RestrictedVersion {
id: string;
name?: string;
createdAt: string;
hidden?: boolean;
updatedAt: string;
createdAt: string;
}
export interface SiblingDatum {
id: string;
@@ -66,22 +87,22 @@ export interface SiblingDatum {
text?: string;
id?: string;
}[];
createdAt: string;
updatedAt: string;
createdAt: string;
}
export interface RelyOnRequestHeader {
id: string;
name?: string;
createdAt: string;
updatedAt: string;
createdAt: string;
}
export interface DocLevelAccess {
id: string;
approvedForRemoval?: boolean;
approvedTitle?: string;
lockTitle?: boolean;
createdAt: string;
updatedAt: string;
createdAt: string;
}
export interface HiddenField {
id: string;
@@ -95,6 +116,14 @@ export interface HiddenField {
value?: string;
id?: string;
}[];
createdAt: string;
hidden?: boolean;
updatedAt: string;
createdAt: string;
}
export interface HiddenAccess {
id: string;
title: string;
hidden?: boolean;
updatedAt: string;
createdAt: string;
}