UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

185 lines (184 loc) 8.52 kB
import { schemaPermissions } from '@directus/system-data'; import { set, uniq } from 'lodash-es'; import { reduceSchema } from '../../utils/reduce-schema.js'; import { fetchPermissions } from '../lib/fetch-permissions.js'; import { fetchPolicies } from '../lib/fetch-policies.js'; import { fetchRolesTree } from '../lib/fetch-roles-tree.js'; import { fetchAllowedFieldMap } from '../modules/fetch-allowed-field-map/fetch-allowed-field-map.js'; import { fetchGlobalAccess } from '../modules/fetch-global-access/fetch-global-access.js'; import { fetchShareInfo } from './fetch-share-info.js'; import { mergePermissions } from './merge-permissions.js'; export async function getPermissionsForShare(accountability, collections, context) { const defaults = { action: 'read', collection: '', permissions: {}, policy: null, validation: null, presets: null, fields: null, }; const { collection, item, role, user_created } = await fetchShareInfo(accountability.share, context); const userAccountability = { user: user_created.id, role: user_created.role, roles: await fetchRolesTree(user_created.role, { knex: context.knex }), admin: false, app: false, ip: accountability.ip, }; // Fallback to public accountability so merging later on has no issues const shareAccountability = { user: null, role: role, roles: await fetchRolesTree(role, { knex: context.knex }), admin: false, app: false, ip: accountability.ip, }; const [{ admin: shareIsAdmin }, { admin: userIsAdmin }, userPermissions, sharePermissions, shareFieldMap, userFieldMap,] = await Promise.all([ fetchGlobalAccess(shareAccountability, { knex: context.knex }), fetchGlobalAccess(userAccountability, { knex: context.knex }), getPermissionsForAccountability(userAccountability, context), getPermissionsForAccountability(shareAccountability, context), fetchAllowedFieldMap({ accountability: shareAccountability, action: 'read', }, context), fetchAllowedFieldMap({ accountability: userAccountability, action: 'read', }, context), ]); const isAdmin = userIsAdmin && shareIsAdmin; let permissions = []; let reducedSchema; if (isAdmin) { defaults.fields = ['*']; reducedSchema = context.schema; } else if (userIsAdmin && !shareIsAdmin) { permissions = sharePermissions; reducedSchema = reduceSchema(context.schema, shareFieldMap); } else if (shareIsAdmin && !userIsAdmin) { permissions = userPermissions; reducedSchema = reduceSchema(context.schema, userFieldMap); } else { permissions = mergePermissions('intersection', sharePermissions, userPermissions); reducedSchema = reduceSchema(context.schema, shareFieldMap); reducedSchema = reduceSchema(reducedSchema, userFieldMap); } if (!isAdmin) defaults.fields = permissions.find((perm) => perm.collection === collection)?.fields ?? []; const parentPrimaryKeyField = context.schema.collections[collection].primary; const relationalPermissions = traverse(reducedSchema, parentPrimaryKeyField, item, collection); const parentCollectionPermission = { ...defaults, collection, permissions: { [parentPrimaryKeyField]: { _eq: item, }, }, }; // All permissions that will be merged into the original permissions set const allGeneratedPermissions = [ parentCollectionPermission, ...relationalPermissions.map((generated) => ({ ...defaults, ...generated })), ...schemaPermissions, ]; // All the collections that are touched through the relational tree from the current root collection, and the schema collections const allowedCollections = uniq(allGeneratedPermissions.map(({ collection }) => collection)); const generatedPermissions = []; // Merge all the permissions that relate to the same collection with an _or (this allows you to properly retrieve) // the items of a collection if you entered that collection from multiple angles for (const collection of allowedCollections) { const permissionsForCollection = allGeneratedPermissions.filter((permission) => permission.collection === collection); if (permissionsForCollection.length > 0) { generatedPermissions.push(...mergePermissions('or', permissionsForCollection)); } else { generatedPermissions.push(...permissionsForCollection); } } if (isAdmin) { return filterCollections(collections, generatedPermissions); } // Explicitly filter out permissions to collections unrelated to the root parent item. const limitedPermissions = permissions.filter(({ action, collection }) => allowedCollections.includes(collection) && action === 'read'); return filterCollections(collections, mergePermissions('and', limitedPermissions, generatedPermissions)); } function filterCollections(collections, permissions) { if (!collections) { return permissions; } return permissions.filter(({ collection }) => collections.includes(collection)); } async function getPermissionsForAccountability(accountability, context) { const policies = await fetchPolicies(accountability, context); return fetchPermissions({ policies, accountability, }, context); } export function traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, currentCollection, parentCollections = [], path = []) { const permissions = []; // If there's already a permissions rule for the collection we're currently checking, we'll shortcircuit. // This prevents infinite loop in recursive relationships, like articles->related_articles->articles, or // articles.author->users.avatar->files.created_by->users.avatar->files.created_by->🔁 if (parentCollections.includes(currentCollection)) { return permissions; } const relationsInCollection = schema.relations.filter((relation) => { return relation.collection === currentCollection || relation.related_collection === currentCollection; }); for (const relation of relationsInCollection) { let type; if (relation.related_collection === currentCollection) { type = 'o2m'; } else if (!relation.related_collection) { type = 'a2o'; } else { type = 'm2o'; } if (type === 'o2m') { permissions.push({ collection: relation.collection, permissions: getFilterForPath(type, [...path, relation.field], rootItemPrimaryKeyField, rootItemPrimaryKey), }); permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.collection, [...parentCollections, currentCollection], [...path, relation.field])); } if (type === 'a2o' && relation.meta?.one_allowed_collections) { for (const collection of relation.meta.one_allowed_collections) { permissions.push({ collection, permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field},${relation.meta.one_collection_field})`], rootItemPrimaryKeyField, rootItemPrimaryKey), }); } } if (type === 'm2o') { permissions.push({ collection: relation.related_collection, permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field})`], rootItemPrimaryKeyField, rootItemPrimaryKey), }); if (relation.meta?.one_field) { permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.related_collection, [...parentCollections, currentCollection], [...path, relation.meta?.one_field])); } } } return permissions; } function getFilterForPath(type, path, rootPrimaryKeyField, rootPrimaryKey) { const filter = {}; if (type === 'm2o' || type === 'a2o') { set(filter, path.reverse(), { [rootPrimaryKeyField]: { _eq: rootPrimaryKey } }); } else { set(filter, path.reverse(), { _eq: rootPrimaryKey }); } return filter; }