@restorecommerce/acs-client
Version:
Access Control Service Client
208 lines • 9.51 kB
JavaScript
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