fix: relationship field disabled from access control in related collections (#644)

* fix: disabled relationship field from access control in related collections

* fix: ids can be read from relationships regardless of access of related document
This commit is contained in:
Dan Ribbens
2022-07-03 07:01:45 -04:00
committed by GitHub
parent 601e69ab0d
commit 91e33d1c1c
10 changed files with 102 additions and 57 deletions

View File

@@ -86,7 +86,8 @@ The `filterOptions` property can either be a `Where` query directly, or a functi
You can learn more about writing queries [here](/docs/queries/overview).
<Banner type="warning">
When a relationship field has both <strong>filterOptions</strong> and a custom <strong>validate</strong> function, the server-side validation will not enforce <strong>filterOptions</strong>unless you call the default relationship field validation function imported from <strong>payload/fields/validations</strong> within your custom function.
<strong>Note:</strong><br/>
When a relationship field has both <strong>filterOptions</strong> and a custom <strong>validate</strong> function, the api will not validate <strong>filterOptions</strong> unless you call the default relationship field validation function imported from <strong>payload/fields/validations</strong> in your validate function.
</Banner>
### How the data is saved

View File

@@ -153,3 +153,8 @@ To populate `user.author.department` in it's entirety you could specify `?depth=
}
}
```
<Banner type="warning">
<strong>Note:</strong><br/>
When access control on collections prevents relationship fields from populating, the API response will contain the relationship id instead of the full document.
</Banner>

View File

@@ -1,4 +1,3 @@
import { Value } from '../../../elements/ReactSelect/types';
import { ValueWithRelation } from './types';
type RelationMap = {
@@ -17,11 +16,17 @@ export const createRelationMap: CreateRelationMap = ({
value,
}) => {
const hasMultipleRelations = Array.isArray(relationTo);
const relationMap: RelationMap = {};
let relationMap: RelationMap;
if (Array.isArray(relationTo)) {
relationMap = relationTo.reduce((map, current) => {
return { ...map, [current]: [] };
}, {});
} else {
relationMap = { [relationTo]: [] };
}
const add = (relation: string, id: unknown) => {
if (((typeof id === 'string' && id !== 'null') || typeof id === 'number') && typeof relation === 'string') {
if (typeof relationMap[relation] === 'undefined') relationMap[relation] = [];
if (((typeof id === 'string') || typeof id === 'number') && typeof relation === 'string') {
relationMap[relation].push(id);
}
};

View File

@@ -58,7 +58,7 @@ const Relationship: React.FC<Props> = (props) => {
} = useConfig();
const { id } = useDocumentInfo();
const { user } = useAuth();
const { user, permissions } = useAuth();
const { getData, getSiblingData } = useWatchForm();
const formProcessing = useFormProcessing();
const hasMultipleRelations = Array.isArray(relationTo);
@@ -93,6 +93,9 @@ const Relationship: React.FC<Props> = (props) => {
value: valueArg,
sort,
}) => {
if (!permissions) {
return;
}
let lastLoadedPageToUse = typeof lastLoadedPageArg !== 'undefined' ? lastLoadedPageArg : 1;
const lastFullyLoadedRelationToUse = typeof lastFullyLoadedRelationArg !== 'undefined' ? lastFullyLoadedRelationArg : -1;
@@ -160,13 +163,27 @@ const Relationship: React.FC<Props> = (props) => {
}
}
}
} else if (response.status === 403) {
setLastFullyLoadedRelation(relations.indexOf(relation));
lastLoadedPageToUse = 1;
dispatchOptions({ type: 'ADD', data: { docs: [] } as PaginatedDocs<unknown>, relation, hasMultipleRelations, collection, sort, ids: relationMap[relation] });
} else {
setErrorLoading('An error has occurred.');
}
}
}, Promise.resolve());
}
}, [relationTo, hasMany, errorLoading, collections, optionFilters, serverURL, api, hasMultipleRelations]);
}, [
permissions,
relationTo,
hasMany,
errorLoading,
collections,
optionFilters,
serverURL,
api,
hasMultipleRelations,
]);
const findOptionsByValue = useCallback((): Option | Option[] => {
if (value) {
@@ -264,12 +281,12 @@ const Relationship: React.FC<Props> = (props) => {
if (!errorLoading) {
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`);
const collection = collections.find((coll) => coll.slug === relation);
if (response.ok) {
const data = await response.json();
const collection = collections.find((coll) => coll.slug === relation);
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, sort: true });
} else {
console.error(`There was a problem loading relationships to related collection ${relation}.`);
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, sort: true, ids });
} else if (response.status === 403) {
dispatchOptions({ type: 'ADD', data: { docs: [] } as PaginatedDocs, relation, hasMultipleRelations, collection, sort: true, ids });
}
}
}

View File

@@ -29,7 +29,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
}
case 'ADD': {
const { hasMultipleRelations, collection, relation, data, sort } = action;
const { hasMultipleRelations, collection, relation, data, sort, ids = [] } = action;
const labelKey = collection.admin.useAsTitle || 'id';
@@ -50,7 +50,11 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
];
}
return docs;
}, []),
},
ids.map((id) => ({
label: labelKey === 'id' ? id : `Untitled - ID: ${id}`,
value: id,
}))),
];
return sort ? sortOptions(options) : options;
@@ -74,7 +78,14 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
}
return docs;
}, []);
},
[
...ids.map((id) => ({
label: labelKey === 'id' ? id : `Untitled - ID: ${id}`,
value: id,
relationTo: relation,
})),
]);
if (optionsToAddTo) {
const subOptions = [

View File

@@ -25,6 +25,7 @@ type ADD = {
hasMultipleRelations: boolean
collection: SanitizedCollectionConfig
sort?: boolean
ids?: unknown[]
}
export type Action = CLEAR | ADD

View File

@@ -20,8 +20,14 @@ const RelationshipCell = (props) => {
if (collection) {
const useAsTitle = collection.admin.useAsTitle ? collection.admin.useAsTitle : 'id';
let title: string;
if (typeof doc === 'string') {
title = doc;
} else {
title = doc?.[useAsTitle] ? doc[useAsTitle] : doc;
}
return newData ? `${newData}, ${doc?.[useAsTitle]}` : doc?.[useAsTitle];
return newData ? `${newData}, ${title}` : title;
}
return newData;
@@ -34,7 +40,7 @@ const RelationshipCell = (props) => {
if (collection && doc) {
const useAsTitle = collection.admin.useAsTitle ? collection.admin.useAsTitle : 'id';
setData(doc[useAsTitle]);
setData(doc[useAsTitle] ? doc[useAsTitle] : doc);
}
}
}

View File

@@ -58,18 +58,6 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
};
}
if (field.type === 'relationship') {
const relatedCollections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo];
relatedCollections.forEach((slug) => {
const collection = config.collections.find((coll) => coll.slug === slug);
if (collection && collection.access && collection.access[operation]) {
promises.push(createAccessPromise(updatedObj[field.name], collection.access[operation], operation, true));
}
});
}
if (field.fields) {
if (!updatedObj[field.name].fields) updatedObj[field.name].fields = {};
executeFieldPolicies(updatedObj[field.name].fields, field.fields, operation);

View File

@@ -1,5 +1,6 @@
import getConfig from '../../config/load';
import { email, password } from '../../mongoose/testCredentials';
import { PaginatedDocs } from '../../mongoose/types';
require('isomorphic-fetch');
@@ -8,6 +9,15 @@ const { serverURL: url } = getConfig();
let token = null;
let headers = null;
type RelationshipA = {
id: string
post?: string | RelationshipB
}
type RelationshipB = {
id: string
post?: (string | RelationshipA)[]
}
describe('Collections - REST', () => {
beforeAll(async (done) => {
const response = await fetch(`${url}/api/admins/login`, {
@@ -93,7 +103,7 @@ describe('Collections - REST', () => {
const response = await fetch(`${url}/api/relationship-b/${documentB.id}`);
const data = await response.json();
expect(data.strictAccess).toBeNull();
expect(typeof data.strictAccess).not.toBe('object');
});
it('should populate strict access when authorized', async () => {
@@ -110,18 +120,18 @@ describe('Collections - REST', () => {
headers,
method: 'get',
});
const data = await response.json();
const [doc] = data.docs;
expect(doc.id).toBe(documentA.id);
let nested = doc.post;
expect(nested.id).toBe(documentB.id);
[nested] = nested.post;
expect(nested.id).toBe(documentA.id);
nested = nested.post;
expect(nested.id).toBe(documentB.id);
[nested] = nested.post;
expect(nested).not.toHaveProperty('post');
expect(nested).toBe(documentA.id);
const data: PaginatedDocs<RelationshipA> = await response.json();
const [depth0] = data.docs;
expect(depth0.id).toBe(documentA.id);
const depth1 = depth0.post as RelationshipB;
expect(depth1.id).toBe(documentB.id);
const [depth2] = depth1.post as RelationshipA[];
expect(depth2.id).toBe(documentA.id);
const depth3 = depth2.post as RelationshipB;
expect(depth3.id).toBe(documentB.id);
const [depth4] = depth3.post as RelationshipA[];
expect(depth4).not.toHaveProperty('post');
expect(depth4).toBe(documentA.id);
});
it('should respect max depth at the field level', async () => {

View File

@@ -25,21 +25,20 @@ const populate = async ({
showHiddenFields,
}: PopulateArgs) => {
const dataToUpdate = dataReference;
const relation = Array.isArray(field.relationTo) ? (data.relationTo as string) : field.relationTo;
const relatedCollection = req.payload.collections[relation];
if (relatedCollection) {
let idString = Array.isArray(field.relationTo) ? data.value : data;
let relationshipValue;
const shouldPopulate = depth && currentDepth <= depth;
if (typeof idString !== 'string' && typeof idString?.toString === 'function') {
idString = idString.toString();
}
let populatedRelationship;
if (depth && currentDepth <= depth) {
populatedRelationship = await req.payload.findByID({
if (shouldPopulate) {
relationshipValue = await req.payload.findByID({
req,
collection: relatedCollection.config.slug,
id: idString as string,
@@ -51,19 +50,21 @@ const populate = async ({
});
}
// If populatedRelationship comes back, update value
if (populatedRelationship || populatedRelationship === null) {
if (typeof index === 'number') {
if (Array.isArray(field.relationTo)) {
dataToUpdate[field.name][index].value = populatedRelationship;
} else {
dataToUpdate[field.name][index] = populatedRelationship;
}
} else if (Array.isArray(field.relationTo)) {
dataToUpdate[field.name].value = populatedRelationship;
if (!relationshipValue) {
// ids are visible regardless of access controls
relationshipValue = idString;
}
if (typeof index === 'number') {
if (Array.isArray(field.relationTo)) {
dataToUpdate[field.name][index].value = relationshipValue;
} else {
dataToUpdate[field.name] = populatedRelationship;
dataToUpdate[field.name][index] = relationshipValue;
}
} else if (Array.isArray(field.relationTo)) {
dataToUpdate[field.name].value = relationshipValue;
} else {
dataToUpdate[field.name] = relationshipValue;
}
}
};