UNPKG

@directus/api

Version:

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

101 lines (100 loc) 5.55 kB
import { useLogger } from '../../logger/index.js'; import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js'; import { fetchPolicies } from '../../permissions/lib/fetch-policies.js'; import { fetchAllowedFields } from '../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js'; import { validateItemAccess } from '../../permissions/modules/validate-access/lib/validate-item-access.js'; import { extractRequiredDynamicVariableContextForPermissions } from '../../permissions/utils/extract-required-dynamic-variable-context.js'; import { fetchDynamicVariableData } from '../../permissions/utils/fetch-dynamic-variable-data.js'; import { processPermissions } from '../../permissions/utils/process-permissions.js'; import { getService } from '../../utils/get-service.js'; import { calculateCacheMetadata } from './calculate-cache-metadata.js'; import { filterToFields } from './filter-to-fields.js'; import { permissionCache } from './permissions-cache.js'; /** * Verify if a client has permissions to perform an action on the item. * - `string[]`: List of fields the client has access to, empty if item exists but access is restricted. * - `null`: Indicates the item doesn't exist. */ export async function verifyPermissions(accountability, collection, item, action = 'read', options) { if (!accountability) return []; const { schema, knex } = options; if (!schema.collections[collection]) return []; if (accountability.admin) return ['*']; const cached = permissionCache.get(accountability, collection, String(item), action); if (cached !== undefined) return cached; // Prevent caching stale permissions if invalidation occurs during async steps const startInvalidationCount = permissionCache.getInvalidationCount(); let itemData = null; try { const adminService = getService(collection, { schema, knex }); const policies = await fetchPolicies(accountability, { knex, schema }); const rawPermissions = await fetchPermissions({ action, collections: [collection], policies, accountability, bypassDynamicVariableProcessing: true }, { knex, schema }); // Check for item-level rules to skip DB fetch const hasItemRules = rawPermissions.some((p) => p.permissions && Object.keys(p.permissions).length > 0); if (hasItemRules) { // Resolve dynamic variables used in the permission filters const dynamicVariableContext = extractRequiredDynamicVariableContextForPermissions(rawPermissions); const permissionsContext = await fetchDynamicVariableData({ accountability, policies, dynamicVariableContext }, { knex, schema }); const processedPermissions = processPermissions({ permissions: rawPermissions, accountability, permissionsContext, }); const fieldsToFetch = processedPermissions .map((perm) => (perm.permissions ? filterToFields(perm.permissions, collection, schema) : [])) .flat(); // Fetch current item data to evaluate any conditional permission filters based on record state if (item && action !== 'create') { try { itemData = await adminService.readOne(item, { fields: fieldsToFetch, }); } catch { // Item doesn't exist permissionCache.set(accountability, collection, String(item), action, null, []); return null; } } else if (schema.collections[collection]?.singleton && action !== 'create') { const pkField = schema.collections[collection].primary; if (pkField) { if (Array.from(fieldsToFetch).some((field) => field === '*' || field === pkField) === false) { fieldsToFetch.push(pkField); } } itemData = await adminService.readSingleton({ fields: fieldsToFetch }); } } let allowedFields = []; if ((item || schema.collections[collection]?.singleton) && hasItemRules) { const primaryKeys = item ? [item] : []; const validationContext = { collection, accountability, action, primaryKeys, returnAllowedRootFields: true, }; allowedFields = (await validateItemAccess(validationContext, { knex, schema })).allowedRootFields || []; } else { allowedFields = await fetchAllowedFields({ accountability, action, collection }, { knex, schema }); } // Only cache if the state hasn't been invalidated by another operation in the meantime if (permissionCache.getInvalidationCount() === startInvalidationCount) { // Determine TTL and relational dependencies for cache invalidation const { ttlMs, dependencies } = calculateCacheMetadata(collection, itemData, rawPermissions, schema, accountability); permissionCache.set(accountability, collection, String(item), action, allowedFields, dependencies, ttlMs); } return allowedFields; } catch (err) { useLogger().error(err, `[Collab] verifyPermissions failed for user "${accountability.user}", collection "${collection}", and item "${item}"`); return []; } }