UNPKG

@restorecommerce/acs-client

Version:

Access Control Service Client

208 lines 9.51 kB
import { ServiceConfig } from '@restorecommerce/service-config'; import { Logger } from '@restorecommerce/logger'; import { createClient, createChannel, } from '@restorecommerce/grpc-client'; import { UserServiceDefinition } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/user.js'; import { Response_Decision } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/access_control.js'; import { initAuthZ, } from './authz.js'; import { initializeCache, } from './cache.js'; import { AuthZAction, } from './interfaces.js'; import { accessRequest, } from './resolver.js'; import { cfg, entities, urns } from '../config.js'; import { randomUUID } from 'crypto'; import { Filter_Operation, Filter_ValueType } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/filter.js'; export const DefaultACSClientContextFactory = async (self, request, context) => { const ids = request.ids ?? request.items?.map((item) => item.id); const resources = await self.get(ids, request.subject, context, true); return { ...context, subject: request.subject, resources: [ ...resources.items ?? [], ...request.items ?? [], ], }; }; export const DefaultResourceFactory = (...resourceNames) => async (self, request, context) => (resourceNames?.length ? resourceNames : [self.name ?? self.constructor?.name])?.map(resourceName => ({ resource: resourceName, id: request.items?.map((item) => item.id) })); export const DefaultResourceFactoryInstance = DefaultResourceFactory(); export const DefaultSubjectResolver = async (self, request, ...args) => { const subject = request?.subject; if (subject?.id) { // we don't trust incoming subject id! delete subject.id; } if (subject?.token) { const user = await self.__userService.findByToken({ token: subject.token }); if (user?.payload?.id) { subject.id = user.payload.id; } } return request; }; export const DefaultMetaDataInjector = async (self, request, ...args) => { const ids = Array.from(new Set(request.items?.map((item) => item.id).filter(id => id) ?? []).values()); const meta_map = ids.length ? await self.read({ filters: [{ filters: [{ field: '_key', operation: Filter_Operation.in, value: JSON.stringify(ids), type: Filter_ValueType.ARRAY, }] }], limit: ids.length, subject: request.subject }).then((response) => new Map(response.items?.filter(item => item.payload).map(item => [item.payload.id, item.payload.meta]))) : undefined; request.items?.forEach((item) => { item.meta ??= meta_map?.get(item.id) ?? {}; item.meta.modified ??= new Date(); item.meta.modified_by ??= request.subject?.id; item.meta.owners ??= [ request.subject?.scope ? { id: urns.ownerIndicatoryEntity, value: entities.organization, attributes: [{ id: urns.ownerInstance, value: request.subject.scope }], } : undefined, request.subject?.id ? { id: urns.ownerIndicatoryEntity, value: entities.user, attributes: [{ id: urns.ownerInstance, value: request.subject.id }], } : undefined, ].filter(Boolean); item.id ??= randomUUID().replaceAll('-', ''); }); return request; }; export function access_controlled_service(baseService) { return class extends baseService { __userService; __acsDatabaseProvider; constructor(...args) { super(...args); const cfg = args.find((arg) => (arg instanceof ServiceConfig)); const logger = args.find((arg) => (arg instanceof Logger)); this.__acsDatabaseProvider = cfg.get('authorization:database') ?? 'arangoDB'; this.__userService = createClient({ ...cfg.get('client:user'), logger }, UserServiceDefinition, createChannel(cfg.get('client:user:address'))); initAuthZ(cfg, logger); initializeCache(); } }; } export function access_controlled_function(kwargs) { return function (target, context, fallback) { const reflection = async function (request, ...args) { try { if (!this.__userService) { throw new Error('An @access_controlled_function must be member of an @access_controlled_service class'); } request = kwargs.subject === undefined ? await DefaultSubjectResolver(this, request, ...args) : await kwargs.subject?.(this, request, ...args) ?? request; // Read actions should not require Meta from request // use null to disable MetaDataInjector // however, kwargs.meta can still be undefined! if (kwargs.action !== AuthZAction.READ && kwargs.meta !== null) { if (kwargs.meta) { await kwargs.meta(this, request, ...args); } else { await DefaultMetaDataInjector(this, request, ...args); } } const acsContext = typeof (kwargs.context) === 'function' ? await kwargs.context(this, request, ...args) : kwargs.context ?? await DefaultACSClientContextFactory(this, request, ...args); const resource = typeof (kwargs.resource) === 'function' ? await kwargs.resource(this, request, ...args) : kwargs.resource ?? await DefaultResourceFactoryInstance(this, request, ...args); const database = typeof (kwargs.database) === 'function' ? await kwargs.database(this, request, ...args) : kwargs.database ?? this.__acsDatabaseProvider ?? 'arangoDB'; const acsResponse = await accessRequest(acsContext.subject, resource ?? [], kwargs.action, acsContext, { operation: kwargs.operation, database: database ?? this.__acsDatabaseProvider ?? 'arangoDB', useCache: kwargs.useCache ?? cfg.get('authorization:cache:enabled') ?? false }); if (acsResponse?.decision !== Response_Decision.PERMIT) { return acsResponse; } if (request) { const arg = acsResponse?.custom_query_args?.find(arg => resource?.some(r => r.resource === arg.resource)); request.custom_queries = arg?.custom_queries; request.custom_arguments = arg?.custom_arguments; } return await target.call(this, request, ...args); } catch (err) { const { code, message, details, stack } = err; this.logger?.error('Operation Status Error:', { code, message, details, stack }); return { operation_status: { code: Number.isInteger(code) ? code : 500, message: details ?? message ?? err, } }; } }; if (fallback) { // A 3rd param? // fallback to decorator stage 1 or 2. // is it a pure function or a stage 2 descriptor? let's guess! target = fallback.value ?? fallback; fallback.value = reflection; } else { // lucky we are on stage 3 - simple: return reflection; } }; } export function resolves_subject(subjectResolver = (DefaultSubjectResolver)) { return function (target, context, fallback) { const reflection = async function (request, ...args) { request = await subjectResolver(this, request, ...args); return await target.call(this, request, ...args); }; if (fallback) { // A 3rd param? // fallback to decorator stage 1 or 2. // is it a pure function or a stage 2 descriptor? let's guess! target = fallback.value ?? fallback; fallback.value = reflection; } else { // lucky we are on stage 3 - simple: return reflection; } }; } export function injects_meta_data(metaDataInjector = (DefaultMetaDataInjector)) { return function (target, context, fallback) { const reflection = async function (request, ...args) { request = await metaDataInjector(this, request, ...args); return await target.call(this, request, ...args); }; if (fallback) { // A 3rd param? // fallback to decorator stage 1 or 2. // is it a pure function or a stage 2 descriptor? let's guess! target = fallback.value ?? fallback; fallback.value = reflection; } else { // lucky we are on stage 3 - simple: return reflection; } }; } //# sourceMappingURL=decorators.js.map