UNPKG

@restorecommerce/acs-client

Version:

Access Control Service Client

446 lines 17.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.initAuthZ = exports.ACSAuthZ = exports.UnAuthZ = exports.createResourceTarget = exports.formatResourceType = exports.createSubjectTarget = exports.createActionTarget = exports.unauthZ = exports.authZ = void 0; const interfaces_1 = require("./interfaces"); const grpc_client_1 = require("@restorecommerce/grpc-client"); const config_1 = require("../config"); const logger_1 = __importDefault(require("../logger")); const cache_1 = require("./cache"); const kafka_client_1 = require("@restorecommerce/kafka-client"); const utils_1 = require("../utils"); const access_control_1 = require("@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/access_control"); const access_control_2 = require("@restorecommerce/rc-grpc-clients/dist/generated/io/restorecommerce/access_control"); const rule_1 = require("@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/rule"); const policy_1 = require("@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/policy"); const policy_set_1 = require("@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/policy_set"); (0, kafka_client_1.registerProtoMeta)(rule_1.protoMetadata, policy_1.protoMetadata, policy_set_1.protoMetadata); const urns = config_1.cfg.get('authorization:urns'); const createActionTarget = (action) => { if (Array.isArray(action)) { const actionList = []; for (let eachAction of action) { eachAction = eachAction.valueOf().toLowerCase(); actionList.push({ id: urns.actionID, value: urns.action + `:${eachAction}` }); } return actionList; } else { return [{ id: urns.actionID, value: urns.action + `:${action.valueOf().toLowerCase()}`, attributes: [] }]; } }; exports.createActionTarget = createActionTarget; const createSubjectTarget = (subject) => { if (subject.unauthenticated) { return [{ id: urns.unauthenticated_user, value: 'true', attributes: [] }]; } const flattened = [ { id: urns.subjectID, value: subject.id, attributes: [] } ]; if (subject.scope) { flattened.push({ id: urns.roleScopingInstance, value: subject.scope, attributes: [] }); } return flattened; }; exports.createSubjectTarget = createSubjectTarget; const formatResourceType = (type, namespacePrefix) => { // e.g: contact_point -> contact_point.ContactPoint const prefix = type; const suffixArray = type.split('_').map((word) => { return word.charAt(0).toUpperCase() + word.substring(1); }); const suffix = suffixArray.join(''); if (namespacePrefix) { return `${namespacePrefix}.${prefix}.${suffix}`; } else { return `${prefix}.${suffix}`; } }; exports.formatResourceType = formatResourceType; const createResourceTarget = (resource, action) => { const flattened = []; resource.forEach((resourceObj) => { if (action != interfaces_1.AuthZAction.EXECUTE) { const resourcenameNameSpace = resourceObj.resource; const resourceInstance = resourceObj.id; const resourceProperty = resourceObj.property; let resourceNameSpace, resourceName; const index = resourcenameNameSpace?.indexOf('.'); if (index > -1) { resourceNameSpace = resourcenameNameSpace.slice(0, index); // resource name from `.` till end, when no end index is specified for // slice api it returns till end of string resourceName = resourcenameNameSpace.slice(index + 1); } else { resourceName = resourcenameNameSpace; } // entity - urn:restorecommerce:acs:names:model:entity const entityName = urns[resourceName] ?? `${urns.model}:${(0, exports.formatResourceType)(resourceName, resourceNameSpace)}`; flattened.push({ id: urns.entity, value: entityName, attributes: [] }); // resource-id - urn:oasis:names:tc:xacml:1.0:resource:resource-id if (typeof resourceInstance === 'string') { flattened.push({ id: urns.resourceID, value: resourceInstance, attributes: [] }); } else if (resourceInstance && Array.isArray(resourceInstance) && resourceInstance.length > 0) { resourceInstance.forEach((instance) => { flattened.push({ id: urns.resourceID, value: instance, attributes: [] }); }); } // property - urn:restorecommerce:acs:names:model:property if (Array.isArray(resourceProperty) && resourceProperty.length > 0) { resourceProperty.forEach((property) => { flattened.push({ id: urns.property, value: `${entityName}#${property}`, attributes: [] }); }); } } else { flattened.push({ id: urns.operation, value: resourceObj.resource, attributes: [] }); } }); return flattened; }; exports.createResourceTarget = createResourceTarget; class UnAuthZ { acs; /** * * @param acs Access Control Service definition (gRPC) */ constructor(acs) { this.acs = acs; } encode(object) { if (object) { if (Array.isArray(object)) { return utils_1._.map(object, this.encode.bind(this)); } else { return { value: Buffer.from(JSON.stringify(object)) }; } } } async isAllowed(request, ctx, useCache) { const authZRequest = { target: { actions: (0, exports.createActionTarget)(request.target.actions), subjects: (0, exports.createSubjectTarget)(request.target.subjects), resources: (0, exports.createResourceTarget)(request.target.resources, request.target.actions) }, context: { subject: this.encode(request.target.subjects), resources: this.encode(ctx.resources) } }; let response; try { const isAllowed = await (0, cache_1.getOrFill)(authZRequest, async (_) => { return await this.acs.isAllowed(authZRequest); }, useCache, 'UnAuthZ:isAllowed'); response = { decision: isAllowed.decision, obligations: (0, utils_1.mapResourceURNObligationProperties)(isAllowed.obligations), operation_status: isAllowed.operation_status }; } catch (err) { logger_1.default.error('Error invoking access-control-srv isAllowed operation', { code: err.code, message: err.message, stack: err.stack }); if (!err.code) { err.code = 500; } response = { decision: access_control_2.Response_Decision.DENY, operation_status: { code: err.code, message: err.message } }; } if (utils_1._.isEmpty(response)) { logger_1.default.error('Unexpected empty response from ACS'); } return response; } async whatIsAllowed(request, ctx, useCache) { const authZRequest = { target: { actions: (0, exports.createActionTarget)(request.target.actions), subjects: (0, exports.createSubjectTarget)(request.target.subjects), resources: (0, exports.createResourceTarget)(request.target.resources, request.target.actions) }, context: { subject: this.encode(request.target.subjects), resources: this.encode(ctx.resources) } }; let response; try { const whatIsAllowed = await (0, cache_1.getOrFill)(authZRequest, async (req) => { return await this.acs.whatIsAllowed(authZRequest); }, useCache, 'UnAuthZ:whatIsAllowed'); response = { ...whatIsAllowed, obligations: (0, utils_1.mapResourceURNObligationProperties)(whatIsAllowed.obligations) }; // TODO Decision? } catch (err) { logger_1.default.error('Error invoking access-control-srv whatIsAllowed operation', { code: err.code, message: err.message, stack: err.stack }); if (!err.code) { err.code = 500; } response = { decision: access_control_2.Response_Decision.DENY, operation_status: { code: err.code, message: err.message } }; } if (utils_1._.isEmpty(response)) { logger_1.default.error('Unexpected empty response from ACS'); } return response; } } exports.UnAuthZ = UnAuthZ; /** * General authorizer. Marshalls data and requests access to the Access Control Service (ACS). */ class ACSAuthZ { acs; /** * * @param acs Access Control Service definition (gRPC) */ constructor(acs, ids) { this.acs = acs; } /** * Perform request to access-control-srv * @param request - authZRequest containing subject, resources and action * @param useCache * @returns {DecisionResponse} */ async isAllowed(request, ctx, useCache) { const authZRequest = this.prepareRequest(request); authZRequest.context = { subject: {}, resources: [], security: this.encode(request.context.security) }; const subject = { token: request.target.subjects.token }; let cachePrefix = 'ACSAuthZ'; if (request.target.subjects.id !== undefined) { cachePrefix = request.target.subjects.id + ':' + cachePrefix; } authZRequest.context.subject = this.encode(subject); authZRequest.context.resources = this.encode(ctx.resources); // for isAllowed we use the subject, action and resource fields .i.e. reqeust Target // since the context resources contains the values which would change for each // resource being created and should not be used in key when generating hash const cacheKey = { target: authZRequest.target }; let response; try { const isAllowed = await (0, cache_1.getOrFill)(cacheKey, async (req) => { return await this.acs.isAllowed(authZRequest); }, useCache, cachePrefix + ':isAllowed'); response = { decision: isAllowed?.decision, obligations: (0, utils_1.mapResourceURNObligationProperties)(isAllowed?.obligations), operation_status: isAllowed?.operation_status }; } catch (err) { logger_1.default.error('Error invoking access-control-srv isAllowed operation', { code: err.code, message: err.message, stack: err.stack }); if (!err.code) { err.code = 500; } response = { decision: access_control_2.Response_Decision.DENY, operation_status: { code: err.code, message: err.message } }; } if (utils_1._.isEmpty(response)) { logger_1.default.error('Unexpected empty response from ACS'); } return response; } /** * Perform request to access-control-srv * @param request - authZRequest containing subject, resource and action * @returns {PolicySetRQ} * @param resource */ async whatIsAllowed(request, ctx, useCache) { const authZRequest = this.prepareRequest(request); authZRequest.context = { subject: {}, resources: [], security: this.encode(request.context.security) }; const subject = { token: request.target.subjects.token }; let cachePrefix = 'ACSAuthZ'; if (request.target.subjects.id !== undefined) { cachePrefix = request.target.subjects.id + ':' + cachePrefix; } authZRequest.context.subject = this.encode(subject); authZRequest.context.resources = this.encode(ctx.resources); let response; try { const whatIsAllowed = await (0, cache_1.getOrFill)(authZRequest, async (req) => { return await this.acs.whatIsAllowed(authZRequest); }, useCache, cachePrefix + ':whatIsAllowed'); response = { ...whatIsAllowed, obligations: (0, utils_1.mapResourceURNObligationProperties)(whatIsAllowed.obligations) }; // TODO Decision? } catch (err) { logger_1.default.error('Error invoking access-control-srv whatIsAllowed operation', { code: err.code, message: err.message, stack: err.stack }); if (!err.code) { err.code = 500; } response = { decision: access_control_2.Response_Decision.DENY, operation_status: { code: err.code, message: err.message } }; } if (utils_1._.isEmpty(response)) { logger_1.default.error('Unexpected empty response from ACS'); } return response; } encode(object) { if (object) { if (Array.isArray(object)) { return utils_1._.map(object, this.encode.bind(this)); } else { return { value: Buffer.from(JSON.stringify(object)) }; } } } prepareRequest(request) { const { subjects, resources, actions } = request.target; const authZRequest = { target: { actions: (0, exports.createActionTarget)(actions), subjects: (0, exports.createSubjectTarget)(subjects), }, }; authZRequest.target.resources = (0, exports.createResourceTarget)(resources, actions); return authZRequest; } } exports.ACSAuthZ = ACSAuthZ; const acsEvents = [ 'policy_setCreated', 'policy_setModified', 'policy_setDeleted', 'policyCreated', 'policyModified', 'policyDeleted', 'ruleCreated', 'ruleModified', 'ruleDeleted', ]; const eventListener = async (msg, context, config, eventName) => { if (acsEvents.indexOf(eventName) > -1) { // no prefix provided, flush complete cache logger_1.default.info(`Received event ${eventName} and hence evicting ACS cache`); await (0, cache_1.flushCache)(); } }; const initAuthZ = async (config) => { if (!exports.authZ) { if (config) { (0, config_1.updateConfig)(config); } // gRPC interface for access-control-srv if (config_1.cfg.get('authorization:enabled')) { const kafkaCfg = config_1.cfg.get('authorization:events:kafka') ?? config_1.cfg.get('events:kafka'); const acsName = config_1.cfg.get('authorization:service') ?? 'acs-srv'; const grpcACSConfig = config_1.cfg.get('authorization:client')?.[acsName] ?? config_1.cfg.get('client')?.[acsName]; const acsClient = (0, grpc_client_1.createClient)({ ...grpcACSConfig, logger: logger_1.default }, access_control_1.AccessControlServiceDefinition, (0, grpc_client_1.createChannel)(grpcACSConfig.address)); exports.authZ = new ACSAuthZ(acsClient); exports.unauthZ = new UnAuthZ(acsClient); // listeners for rules / policies / policySets modified, so as to // delete the Cache as it would be invalid if ACS resources are modified if (kafkaCfg && kafkaCfg.evictACSCache) { const events = new kafka_client_1.Events(kafkaCfg, logger_1.default); await events.start(); for (const topicLabel in kafkaCfg.evictACSCache) { const topicCfg = kafkaCfg.evictACSCache[topicLabel]; const topic = await events.topic(topicCfg.topic); if (topicCfg.events) { for (const eachEvent of topicCfg.events) { await topic.on(eachEvent, eventListener); } } } } return exports.authZ; } } return exports.authZ; }; exports.initAuthZ = initAuthZ; //# sourceMappingURL=authz.js.map