Files
directus/api/src/utils/get-permissions.ts
José Varela cc343fdf91 Improve cache performance by compressing records (#14833)
* Utils to compress/decompress data
Gzip was chosen because we want smaller data but quick algorithm since this will be ran for every request

* Compress system cache

* Decompress system cache

* Set/Get compressed cache for individual requests

* Switch from gzip to snappy, use json compression too

* Fix cache exp set/get

* Remove unused import

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
2022-08-04 17:35:27 -04:00

201 lines
6.4 KiB
TypeScript

import { Accountability, Permission, SchemaOverview } from '@directus/shared/types';
import { deepMap, parseFilter, parseJSON, parsePreset } from '@directus/shared/utils';
import { cloneDeep } from 'lodash';
import hash from 'object-hash';
import { getCache, getSystemCache, setSystemCache } from '../cache';
import getDatabase from '../database';
import { appAccessMinimalPermissions } from '../database/system-data/app-access-permissions';
import env from '../env';
import { RolesService } from '../services/roles';
import { UsersService } from '../services/users';
import { mergePermissions } from '../utils/merge-permissions';
import { mergePermissionsForShare } from './merge-permissions-for-share';
export async function getPermissions(accountability: Accountability, schema: SchemaOverview) {
const database = getDatabase();
const { cache } = getCache();
let permissions: Permission[] = [];
const { user, role, app, admin, share_scope } = accountability;
const cacheKey = `permissions-${hash({ user, role, app, admin, share_scope })}`;
if (env.CACHE_PERMISSIONS !== false) {
const cachedPermissions = await getSystemCache(cacheKey);
if (cachedPermissions) {
if (!cachedPermissions.containDynamicData) {
return processPermissions(accountability, cachedPermissions.permissions, {});
}
const cachedFilterContext = await cache?.get(
`filterContext-${hash({ user, role, permissions: cachedPermissions.permissions })}`
);
if (cachedFilterContext) {
return processPermissions(accountability, cachedPermissions.permissions, cachedFilterContext);
} else {
const {
permissions: parsedPermissions,
requiredPermissionData,
containDynamicData,
} = parsePermissions(cachedPermissions.permissions);
permissions = parsedPermissions;
const filterContext = containDynamicData
? await getFilterContext(schema, accountability, requiredPermissionData)
: {};
if (containDynamicData && env.CACHE_ENABLED !== false) {
await cache?.set(`filterContext-${hash({ user, role, permissions })}`, filterContext);
}
return processPermissions(accountability, permissions, filterContext);
}
}
}
if (accountability.admin !== true) {
const query = database.select('*').from('directus_permissions');
if (accountability.role) {
query.where({ role: accountability.role });
} else {
query.whereNull('role');
}
const permissionsForRole = await query;
const {
permissions: parsedPermissions,
requiredPermissionData,
containDynamicData,
} = parsePermissions(permissionsForRole);
permissions = parsedPermissions;
if (accountability.app === true) {
permissions = mergePermissions(
'or',
permissions,
appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability.role }))
);
}
if (accountability.share_scope) {
permissions = mergePermissionsForShare(permissions, accountability, schema);
}
const filterContext = containDynamicData
? await getFilterContext(schema, accountability, requiredPermissionData)
: {};
if (env.CACHE_PERMISSIONS !== false) {
await setSystemCache(cacheKey, { permissions, containDynamicData });
if (containDynamicData && env.CACHE_ENABLED !== false) {
await cache?.set(`filterContext-${hash({ user, role, permissions })}`, filterContext);
}
}
return processPermissions(accountability, permissions, filterContext);
}
return permissions;
}
function parsePermissions(permissions: any[]) {
const requiredPermissionData = {
$CURRENT_USER: [] as string[],
$CURRENT_ROLE: [] as string[],
};
let containDynamicData = false;
permissions = permissions.map((permissionRaw) => {
const permission = cloneDeep(permissionRaw);
if (permission.permissions && typeof permission.permissions === 'string') {
permission.permissions = parseJSON(permission.permissions);
} else if (permission.permissions === null) {
permission.permissions = {};
}
if (permission.validation && typeof permission.validation === 'string') {
permission.validation = parseJSON(permission.validation);
} else if (permission.validation === null) {
permission.validation = {};
}
if (permission.presets && typeof permission.presets === 'string') {
permission.presets = parseJSON(permission.presets);
} else if (permission.presets === null) {
permission.presets = {};
}
if (permission.fields && typeof permission.fields === 'string') {
permission.fields = permission.fields.split(',');
} else if (permission.fields === null) {
permission.fields = [];
}
const extractPermissionData = (val: any) => {
if (typeof val === 'string' && val.startsWith('$CURRENT_USER.')) {
requiredPermissionData.$CURRENT_USER.push(val.replace('$CURRENT_USER.', ''));
containDynamicData = true;
}
if (typeof val === 'string' && val.startsWith('$CURRENT_ROLE.')) {
requiredPermissionData.$CURRENT_ROLE.push(val.replace('$CURRENT_ROLE.', ''));
containDynamicData = true;
}
return val;
};
deepMap(permission.permissions, extractPermissionData);
deepMap(permission.validation, extractPermissionData);
deepMap(permission.presets, extractPermissionData);
return permission;
});
return { permissions, requiredPermissionData, containDynamicData };
}
async function getFilterContext(schema: SchemaOverview, accountability: Accountability, requiredPermissionData: any) {
const usersService = new UsersService({ schema });
const rolesService = new RolesService({ schema });
const filterContext: Record<string, any> = {};
if (accountability.user && requiredPermissionData.$CURRENT_USER.length > 0) {
filterContext.$CURRENT_USER = await usersService.readOne(accountability.user, {
fields: requiredPermissionData.$CURRENT_USER,
});
}
if (accountability.role && requiredPermissionData.$CURRENT_ROLE.length > 0) {
filterContext.$CURRENT_ROLE = await rolesService.readOne(accountability.role, {
fields: requiredPermissionData.$CURRENT_ROLE,
});
}
return filterContext;
}
function processPermissions(
accountability: Accountability,
permissions: Permission[],
filterContext: Record<string, any>
) {
return permissions.map((permission) => {
permission.permissions = parseFilter(permission.permissions, accountability!, filterContext);
permission.validation = parseFilter(permission.validation, accountability!, filterContext);
permission.presets = parsePreset(permission.presets, accountability!, filterContext);
return permission;
});
}