1
import type { Accountability, Filter, Permission, SchemaOverview } from '@directus/types';
2
import { assign, set, uniq } from 'lodash-es';
3
import { schemaPermissions } from '@directus/system-data';
4
import { mergePermissions } from './merge-permissions.js';
5
import { reduceSchema } from './reduce-schema.js';
7
export function mergePermissionsForShare(
8
currentPermissions: Permission[],
9
accountability: Accountability,
10
schema: SchemaOverview,
12
const defaults: Permission = {
14
role: accountability.role,
22
const { collection, item } = accountability.share_scope!;
24
const parentPrimaryKeyField = schema.collections[collection]!.primary;
26
const reducedSchema = reduceSchema(schema, currentPermissions, ['read']);
28
const relationalPermissions = traverse(reducedSchema, parentPrimaryKeyField, item, collection);
30
const parentCollectionPermission: Permission = assign({}, defaults, {
33
[parentPrimaryKeyField]: {
39
// All permissions that will be merged into the original permissions set
40
const allGeneratedPermissions = [
41
parentCollectionPermission,
42
...relationalPermissions.map((generated) => assign({}, defaults, generated)),
46
// All the collections that are touched through the relational tree from the current root collection, and the schema collections
47
const allowedCollections = uniq(allGeneratedPermissions.map(({ collection }) => collection));
49
const generatedPermissions: Permission[] = [];
51
// Merge all the permissions that relate to the same collection with an _or (this allows you to properly retrieve)
52
// the items of a collection if you entered that collection from multiple angles
53
for (const collection of allowedCollections) {
54
const permissionsForCollection = allGeneratedPermissions.filter(
55
(permission) => permission.collection === collection,
58
if (permissionsForCollection.length > 0) {
59
generatedPermissions.push(...mergePermissions('or', permissionsForCollection));
61
generatedPermissions.push(...permissionsForCollection);
65
// Explicitly filter out permissions to collections unrelated to the root parent item.
66
const limitedPermissions = currentPermissions.filter(
67
({ action, collection }) => allowedCollections.includes(collection) && action === 'read',
70
return mergePermissions('and', limitedPermissions, generatedPermissions);
73
export function traverse(
74
schema: SchemaOverview,
75
rootItemPrimaryKeyField: string,
76
rootItemPrimaryKey: string,
77
currentCollection: string,
78
parentCollections: string[] = [],
80
): Partial<Permission>[] {
81
const permissions: Partial<Permission>[] = [];
83
// If there's already a permissions rule for the collection we're currently checking, we'll shortcircuit.
84
// This prevents infinite loop in recursive relationships, like articles->related_articles->articles, or
85
// articles.author->users.avatar->files.created_by->users.avatar->files.created_by->🔁
86
if (parentCollections.includes(currentCollection)) {
90
const relationsInCollection = schema.relations.filter((relation) => {
91
return relation.collection === currentCollection || relation.related_collection === currentCollection;
94
for (const relation of relationsInCollection) {
97
if (relation.related_collection === currentCollection) {
99
} else if (!relation.related_collection) {
105
if (type === 'o2m') {
107
collection: relation.collection,
108
permissions: getFilterForPath(type, [...path, relation.field], rootItemPrimaryKeyField, rootItemPrimaryKey),
114
rootItemPrimaryKeyField,
117
[...parentCollections, currentCollection],
118
[...path, relation.field],
123
if (type === 'a2o' && relation.meta?.one_allowed_collections) {
124
for (const collection of relation.meta.one_allowed_collections) {
127
permissions: getFilterForPath(
129
[...path, `$FOLLOW(${relation.collection},${relation.field},${relation.meta.one_collection_field})`],
130
rootItemPrimaryKeyField,
137
if (type === 'm2o') {
139
collection: relation.related_collection!,
140
permissions: getFilterForPath(
142
[...path, `$FOLLOW(${relation.collection},${relation.field})`],
143
rootItemPrimaryKeyField,
148
if (relation.meta?.one_field) {
152
rootItemPrimaryKeyField,
154
relation.related_collection!,
155
[...parentCollections, currentCollection],
156
[...path, relation.meta?.one_field],
166
export function getFilterForPath(
167
type: 'o2m' | 'm2o' | 'a2o',
169
rootPrimaryKeyField: string,
170
rootPrimaryKey: string,
172
const filter: Filter = {};
174
if (type === 'm2o' || type === 'a2o') {
175
set(filter, path.reverse(), { [rootPrimaryKeyField]: { _eq: rootPrimaryKey } });
177
set(filter, path.reverse(), { _eq: rootPrimaryKey });