Skip to content

Commit

Permalink
feat: provides field access control with document data
Browse files Browse the repository at this point in the history
  • Loading branch information
jmikrut committed Mar 14, 2021
1 parent 36aae5c commit 339f750
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 16 deletions.
26 changes: 26 additions & 0 deletions demo/collections/LocalizedArray.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { PayloadCollectionConfig } from '../../src/collections/config/types';
import { FieldAccess } from '../../src/fields/config/types';
import checkRole from '../access/checkRole';

const PublicReadabilityAccess: FieldAccess = ({ req: { user }, siblingData }) => {
if (checkRole(['admin'], user)) {
return true;
}

if (siblingData.allowPublicReadability) return true;

return false;
};

const LocalizedArrays: PayloadCollectionConfig = {
slug: 'localized-arrays',
Expand All @@ -22,17 +34,31 @@ const LocalizedArrays: PayloadCollectionConfig = {
{
type: 'row',
fields: [
{
name: 'allowPublicReadability',
label: 'Allow Public Readability',
type: 'checkbox',
},
{
name: 'arrayText1',
label: 'Array Text 1',
type: 'text',
required: true,
admin: {
width: '50%',
},
access: {
read: PublicReadabilityAccess,
},
},
{
name: 'arrayText2',
label: 'Array Text 2',
type: 'text',
required: true,
admin: {
width: '50%',
},
},
],
},
Expand Down
28 changes: 17 additions & 11 deletions docs/access-control/fields.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,34 @@ Returns a boolean which allows or denies the ability to set a field's value when

**Available argument properties:**

| Option | Description |
| --------- | ----------- |
| **`req`** | The Express `request` object containing the currently authenticated `user` |
| Option | Description |
| ----------------- | ----------- |
| **`req`** | The Express `request` object containing the currently authenticated `user` |
| **`data`** | The full data passed to create the document. |
| **`siblingData`** | Immediately adjacent field data passed to create the document. |

### Read

Returns a boolean which allows or denies the ability to read a field's value. If `false`, the entire property is omitted from the resulting document.

**Available argument properties:**

| Option | Description |
| --------- | ----------- |
| **`req`** | The Express `request` object containing the currently authenticated `user` |
| **`id`** | `id` of the document being read |
| Option | Description |
| ----------------- | ----------- |
| **`req`** | The Express `request` object containing the currently authenticated `user` |
| **`id`** | `id` of the document being read |
| **`data`** | The full data of the document being read. |
| **`siblingData`** | Immediately adjacent field data of the document being read. |

### Update

Returns a boolean which allows or denies the ability to update a field's value. If `false` is returned, any passed values will be discarded.

**Available argument properties:**

| Option | Description |
| --------- | ----------- |
| **`req`** | The Express `request` object containing the currently authenticated `user` |
| **`id`** | `id` of the document being updated |
| Option | Description |
| ----------------- | ----------- |
| **`req`** | The Express `request` object containing the currently authenticated `user` |
| **`id`** | `id` of the document being updated |
| **`data`** | The full data passed to update the document. |
| **`siblingData`** | Immediately adjacent field data passed to update the document with. |
56 changes: 56 additions & 0 deletions src/collections/tests/collections.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,4 +464,60 @@ describe('Collections - REST', () => {
expect(sortedData.docs[1].id).toStrictEqual(id1);
});
});

describe('Field Access', () => {
it('should properly prevent / allow public users from reading a restricted field', async () => {
const firstArrayText1 = 'test 1';
const firstArrayText2 = 'test 2';

const response = await fetch(`${url}/api/localized-arrays`, {
body: JSON.stringify({
array: [
{
arrayText1: firstArrayText1,
arrayText2: 'test 2',
arrayText3: 'test 3',
allowPublicReadability: true,
},
{
arrayText1: firstArrayText2,
arrayText2: 'test 2',
arrayText3: 'test 3',
allowPublicReadability: false,
},
],
}),
headers,
method: 'post',
});

const data = await response.json();
const docId = data.doc.id;
expect(response.status).toBe(201);
expect(data.doc.array[1].arrayText1).toStrictEqual(firstArrayText2);

const unauthenticatedResponse = await fetch(`${url}/api/localized-arrays/${docId}`, {
headers: {
'Content-Type': 'application/json',
},
});
expect(unauthenticatedResponse.status).toBe(200);
const unauthenticatedData = await unauthenticatedResponse.json();

// This string should be allowed to come back
expect(unauthenticatedData.array[0].arrayText1).toBe(firstArrayText1);

// This string should be prevented from coming back
expect(unauthenticatedData.array[1].arrayText1).toBeUndefined();

const authenticatedResponse = await fetch(`${url}/api/localized-arrays/${docId}`, {
headers,
});

const authenticatedData = await authenticatedResponse.json();

// If logged in, we should get this field back
expect(authenticatedData.array[1].arrayText1).toStrictEqual(firstArrayText2);
});
});
});
4 changes: 3 additions & 1 deletion src/fields/accessPromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PayloadRequest } from '../express/types';

type Arguments = {
data: Record<string, unknown>
fullData: Record<string, unknown>
originalDoc: Record<string, unknown>
field: Field
operation: Operation
Expand All @@ -21,6 +22,7 @@ type Arguments = {

const accessPromise = async ({
data,
fullData,
originalDoc,
field,
operation,
Expand All @@ -45,7 +47,7 @@ const accessPromise = async ({
}

if (field.access && field.access[accessOperation]) {
const result = overrideAccess ? true : await field.access[accessOperation]({ req, id });
const result = overrideAccess ? true : await field.access[accessOperation]({ req, id, siblingData: data, data: fullData });

if (!result && accessOperation === 'update' && originalDoc[field.name] !== undefined) {
resultingData[field.name] = originalDoc[field.name];
Expand Down
14 changes: 10 additions & 4 deletions src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { CSSProperties } from 'react';
import { Editor } from 'slate';
import { PayloadRequest } from '../../express/types';
import { Access } from '../../config/types';
import { Document } from '../../types';
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';

Expand All @@ -16,6 +15,13 @@ export type FieldHook = (args: {
req: PayloadRequest
}) => Promise<unknown> | unknown;

export type FieldAccess = (args: {
req: PayloadRequest
id?: string
data: Record<string, unknown>
siblingData: Record<string, unknown>
}) => Promise<boolean> | boolean;

type Admin = {
position?: string;
width?: string;
Expand Down Expand Up @@ -60,9 +66,9 @@ export interface FieldBase {
}
admin?: Admin;
access?: {
create?: Access;
read?: Access;
update?: Access;
create?: FieldAccess;
read?: FieldAccess;
update?: FieldAccess;
};
}

Expand Down
1 change: 1 addition & 0 deletions src/fields/traverseFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const traverseFields = (args: Arguments): void => {

accessPromises.push(accessPromise({
data,
fullData,
originalDoc,
field,
operation,
Expand Down

0 comments on commit 339f750

Please sign in to comment.