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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -25,6 +25,7 @@ type ADD = {
|
||||
hasMultipleRelations: boolean
|
||||
collection: SanitizedCollectionConfig
|
||||
sort?: boolean
|
||||
ids?: unknown[]
|
||||
}
|
||||
|
||||
export type Action = CLEAR | ADD
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user