@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
101 lines (100 loc) • 5.01 kB
JavaScript
import { toBoolean } from '@directus/utils';
import { fetchPermittedAstRootFields } from '../../../../database/run-ast/modules/fetch-permitted-ast-root-fields.js';
import { fetchPermissions } from '../../../lib/fetch-permissions.js';
import { fetchPolicies } from '../../../lib/fetch-policies.js';
import { fetchAllowedFields } from '../../fetch-allowed-fields/fetch-allowed-fields.js';
import { injectCases } from '../../process-ast/lib/inject-cases.js';
import { processAst } from '../../process-ast/process-ast.js';
export async function validateItemAccess(options, context) {
const collectionInfo = context.schema.collections[options.collection];
const primaryKeyField = collectionInfo?.primary;
if (!primaryKeyField) {
throw new Error(`Cannot find primary key for collection "${options.collection}"`);
}
const isSingleton = collectionInfo?.singleton === true;
const hasPrimaryKeys = options.primaryKeys && options.primaryKeys.length > 0;
// For non-singletons, we must have PKs to validate against
if (!isSingleton && !hasPrimaryKeys) {
throw new Error(`Primary keys are required for non-singleton collection "${options.collection}"`);
}
// When we're looking up access to specific items, we have to read them from the database to
// make sure you are allowed to access them.
const ast = {
type: 'root',
name: options.collection,
query: { limit: isSingleton && !hasPrimaryKeys ? 1 : options.primaryKeys.length },
// Act as if every field was a "normal" field
children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [], alias: false })) ??
[],
cases: [],
};
await processAst({ ast, ...options }, context);
// Inject the filter after the permissions have been processed, as to not require access to the primary key
// Skip adding filter for singletons without explicit PKs
if (hasPrimaryKeys) {
ast.query.filter = {
[primaryKeyField]: {
_in: options.primaryKeys,
},
};
}
let hasItemRules;
let permissionedFields;
// Inject the root fields after the permissions have been processed, as to not require access to all collection fields
if (options.returnAllowedRootFields) {
const allowedFields = await fetchAllowedFields({ accountability: options.accountability, action: options.action, collection: options.collection }, context);
const schemaFields = Object.keys(context.schema.collections[options.collection].fields);
const hasWildcard = allowedFields.includes('*');
permissionedFields = hasWildcard ? schemaFields : allowedFields;
const policies = await fetchPolicies(options.accountability, context);
const permissions = await fetchPermissions({ action: options.action, policies, collections: [options.collection], accountability: options.accountability }, context);
// Only inject cases if there are item-level permission rules
hasItemRules = permissions.some((p) => p.permissions && Object.keys(p.permissions).length > 0);
if (hasItemRules) {
// Create children only for fields that exist in schema and are allowed by permissions
ast.children = permissionedFields.map((field) => ({
type: 'field',
name: field,
fieldKey: field,
whenCase: [],
alias: false,
}));
injectCases(ast, permissions);
}
}
const items = await fetchPermittedAstRootFields(ast, {
schema: context.schema,
accountability: options.accountability,
knex: context.knex,
action: options.action,
});
const expectedCount = isSingleton && !hasPrimaryKeys ? 1 : options.primaryKeys.length;
const hasAccess = items && items.length === expectedCount;
if (!hasAccess) {
if (options.returnAllowedRootFields) {
return { accessAllowed: false, allowedRootFields: [] };
}
return { accessAllowed: false };
}
let accessAllowed = true;
// If specific fields were requested, verify they are all accessible
if (options.fields) {
accessAllowed = items.every((item) => options.fields.every((field) => toBoolean(item[field])));
}
// If returnAllowedRootFields, return intersection of allowed fields across all items
if (options.returnAllowedRootFields) {
// If there are no item-level rules, return the permissioned fields directly
if (!hasItemRules) {
return {
accessAllowed,
allowedRootFields: permissionedFields,
};
}
const allowedRootFields = items.length > 0 ? Object.keys(items[0]).filter((field) => items.every((item) => item[field] === 1)) : [];
return {
accessAllowed,
allowedRootFields,
};
}
return { accessAllowed };
}