UNPKG

@restorecommerce/acs-client

Version:

Access Control Service Client

298 lines 14.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.whatIsAllowed = exports.isAllowed = exports.accessRequest = exports.isAllowedRequest = void 0; const utils_1 = require("../utils"); const access_control_1 = require("@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/access_control"); const authz_1 = require("./authz"); const interfaces_1 = require("./interfaces"); const logger_1 = __importDefault(require("../logger")); const config_1 = require("../config"); const subjectIsUnauthenticated = (subject) => { return subject?.unauthenticated === true; }; const whatIsAllowedRequest = async (subject, resources, actions, ctx, useCache) => { if (subjectIsUnauthenticated(subject)) { return await authz_1.unauthZ.whatIsAllowed({ target: { subjects: subject, resources, actions }, context: { security: {} } }, ctx, useCache); } else { return await authz_1.authZ.whatIsAllowed({ context: { security: {} }, target: { subjects: subject, resources, actions } }, ctx, useCache); } }; const isAllowedRequest = async (subject, resources, actions, ctx, useCache) => { if (subjectIsUnauthenticated(subject)) { return await authz_1.unauthZ.isAllowed({ target: { subjects: subject, resources, actions }, context: { security: {} } }, ctx, useCache); } else { return await authz_1.authZ.isAllowed({ context: { security: {} }, target: { subjects: subject, resources, actions } }, ctx, useCache); } }; exports.isAllowedRequest = isAllowedRequest; /** * It turns an API request as can be found in typical Web frameworks like express, koa etc. * into a proper ACS request. For `whatIsAllowed` operation it returns the filters * to enforce the applicapble poilicies. The response is `Decision` * or policy set reverse query `PolicySetRQ` depending on the requeste operation `isAllowed()` or * `whatIsAllowed()` respectively. * @param {Subject} subject Contains subject information * @param {ACSResource[]} resource Contains resource name, resource instance and optional resource properties * @param {AuthZAction} action Action to be performed on resource * @param {ACSClientContext} ctx Context containing Subject and Context Resources for ACS * @param {Operation} operation Operation to perform `isAllowed` or `whatIsAllowed`, * if this param is missing defaults to `isAllowed` operation * @param {Database} database database used either `arangoDB` or `postgres`, * if this param is missing defaults to `arangoDB` * @param {boolean} useCache by default ACS caching is used, if set to false then ACS cache * is not used and ACS request is made to `access-control-srv` * @returns {DecisionResponse | PolicySetRQResponse} */ const accessRequest = async (subject, resource, action, ctx, options) => { if (utils_1._.isEmpty(subject) || !subject.token) { // check if unauthenticated user is configured in config.json subject = config_1.cfg.get('authorization:users:unauthenticated_user') // fallback to old configs ?? config_1.cfg.get('authorization:unauthenticated_user') // when subject is not passed (if auth header is not set) ?? { unauthenticated: true }; } const subClone = utils_1._.cloneDeep(subject); // by default if the config for authorization enabling and enforcement is missing // enable it by default (true) const authzEnabled = config_1.cfg.get('authorization:enabled') ?? true; const authzEnforced = config_1.cfg.get('authorization:enforce') ?? true; // if authorization is disabled if (!authzEnabled) { return { decision: access_control_1.Response_Decision.PERMIT, operation_status: (0, utils_1.generateOperationStatus)(200, 'success') }; } if (utils_1._.isEmpty(subject)) { return { decision: access_control_1.Response_Decision.DENY, operation_status: (0, utils_1.generateOperationStatus)(config_1.errors.USER_NOT_LOGGED_IN.code, config_1.errors.USER_NOT_LOGGED_IN.message) }; } // resolve userID by token const subjectID = subject?.id; const targetScope = subject?.scope; const targetScopeMessage = targetScope ? `, target_scope:${targetScope};` : ';'; if (resource && !utils_1._.isArray(resource)) { resource = [resource]; } const resourceName = resource?.map(r => r.resource).join(','); if (utils_1._.isEmpty(resource)) { const msg = [ `Access not allowed for request with`, `subject:${subjectID}, resource:${resourceName}, action:${action}${targetScopeMessage}`, `the response was ${access_control_1.Response_Decision.INDETERMINATE}`, ].join(' '); const details = 'Entity missing'; logger_1.default.verbose(msg); logger_1.default.verbose('Details:', { details }); return { decision: access_control_1.Response_Decision.DENY, operation_status: (0, utils_1.generateOperationStatus)(Number(config_1.errors.ACTION_NOT_ALLOWED.code), msg) }; } // default ACS operation is isAllowed const operation = options?.operation ? options.operation : interfaces_1.Operation.isAllowed; // default database is arangoDB const database = options?.database ? options.database : 'arangoDB'; const useCache = options?.useCache ? options.useCache : true; // ctx.resources if (ctx.resources && !utils_1._.isArray(ctx.resources)) { ctx.resources = [ctx.resources]; } // whatIsAllowed Operation if (operation === interfaces_1.Operation.whatIsAllowed) { let policySetResponse; try { // retrieving set of applicable policies/rules from ACS // Note: it is assumed that there is only one policy set policySetResponse = await whatIsAllowedRequest(subClone, resource, action, ctx, useCache); } catch (err) { logger_1.default.error('Error calling whatIsAllowed operation', { code: err.code, message: err.message, stack: err.stack, }); return { decision: access_control_1.Response_Decision.DENY, operation_status: (0, utils_1.generateOperationStatus)(err.code, err.message) }; } // handle case if policySet is empty if (authzEnforced && (!policySetResponse || utils_1._.isEmpty(policySetResponse.policy_sets))) { const msg = [ `Access not allowed for request with subject:${subjectID},`, `resource:${resourceName}, action:${action}${targetScopeMessage}`, 'the response was INDETERMINATE' ].join(' '); const details = 'no matching policy/rule could be found'; logger_1.default.verbose(msg); logger_1.default.verbose('Details:', { details }); return { decision: access_control_1.Response_Decision.DENY, operation_status: (0, utils_1.generateOperationStatus)(Number(config_1.errors.ACTION_NOT_ALLOWED.code), msg) }; } if (!authzEnforced && (!policySetResponse || utils_1._.isEmpty(policySetResponse.policy_sets))) { logger_1.default.verbose([ `The Access response was INDETERMIATE for a request with subject:${subjectID},`, `resource:${resourceName}, action:${action}${targetScopeMessage}`, `as no matching policy/rule could be found, but since ACS enforcement`, `config is disabled overriding the ACS result`, ].join(' ')); } // create filters to enforce applicable policies and custom query / args if applicable // TODO check and modify this const resourceFilters = await (0, utils_1.createResourceFilterMap)(resource, policySetResponse, ctx.resources, action, subClone, subjectID, authzEnforced, targetScope, database); if (resourceFilters.decision) { return resourceFilters; } policySetResponse.filters = resourceFilters.resourceFilterMap; policySetResponse.custom_query_args = resourceFilters.customQueryArgs; policySetResponse.decision = access_control_1.Response_Decision.PERMIT; // Adding Permit to read response (since we no longer throw errors) policySetResponse.operation_status = (0, utils_1.generateOperationStatus)(200, 'success'); return policySetResponse; } // default deny let decisionResponse = { decision: access_control_1.Response_Decision.DENY, operation_status: { code: 0, message: '' } }; // isAllowed operation if (operation === interfaces_1.Operation.isAllowed) { // authorization try { decisionResponse = await (0, exports.isAllowedRequest)(subClone, resource, action, ctx, useCache); } catch (err) { logger_1.default.error('Error calling isAllowed operation', { code: err.code, message: err.message, stack: err.stack }); return { decision: access_control_1.Response_Decision.DENY, operation_status: (0, utils_1.generateOperationStatus)(err.code, err.message) }; } if (authzEnforced && decisionResponse && decisionResponse.decision != access_control_1.Response_Decision.PERMIT) { let details = ''; if (decisionResponse.decision === access_control_1.Response_Decision.INDETERMINATE) { details = 'No matching policy / rule was found'; } else if (decisionResponse.decision === access_control_1.Response_Decision.DENY) { details = `Subject:${subjectID} does not have access to requested target scope ${targetScope}`; } const msg = [ `Access not allowed for request with subject:${subjectID},`, `resource:${resourceName}, action:${action}${targetScopeMessage}`, `the response was ${access_control_1.Response_Decision[decisionResponse.decision]}`, ].join(' '); logger_1.default.verbose(msg); logger_1.default.verbose('Details:', { details }); return { decision: access_control_1.Response_Decision.DENY, operation_status: (0, utils_1.generateOperationStatus)(Number(config_1.errors.ACTION_NOT_ALLOWED.code), msg) }; } } if (!authzEnforced && decisionResponse && decisionResponse.decision != access_control_1.Response_Decision.PERMIT) { let details = ''; if (decisionResponse.decision === access_control_1.Response_Decision.INDETERMINATE) { details = 'No matching policy / rule was found'; } else if (decisionResponse.decision === access_control_1.Response_Decision.DENY) { details = `Subject:${subjectID} does not have access to requested target scope ${targetScope}`; } logger_1.default.verbose([ `Access not allowed for request with subject:${subjectID},`, `resource:${resourceName}, action:${action}${targetScopeMessage}`, `the response was ${access_control_1.Response_Decision[decisionResponse.decision]}`, ].join(' ')); logger_1.default.verbose(`${details}, Overriding the ACS result as ACS enforce config is disabled`); decisionResponse.decision = access_control_1.Response_Decision.PERMIT; } return decisionResponse; }; exports.accessRequest = accessRequest; /** * Exposes the isAllowed() api of `access-control-srv` and retruns the response * as `Decision`. * @param {ACSRequest} request input authorization request * @param {ACSContext} ctx Context Object containing requester's subject information * @return {Decision} PERMIT or DENY or INDETERMINATE */ const isAllowed = async (request, authZ) => { let response; try { const isAllowedResponse = await authZ.acs.isAllowed(request); response = { decision: isAllowedResponse.decision, obligations: (0, utils_1.mapResourceURNObligationProperties)(isAllowedResponse.obligations), operation_status: isAllowedResponse.operation_status }; } catch (err) { logger_1.default.error('Error invoking acs-srv isAllowed method', { code: err.code, message: err.message, stack: err.stack }); return { decision: access_control_1.Response_Decision.DENY, operation_status: (0, utils_1.generateOperationStatus)(err.code, err.message) }; } return response; }; exports.isAllowed = isAllowed; /** * Exposes the whatIsAllowed() api of `access-control-srv` and retruns the response * a policy set reverse query `PolicySetRQ` * @param {ACSRequest} authZRequest input authorization request * @param {ACSContext} ctx Context Object containing requester's subject information * @return {PolicySetRQ} set of applicable policies and rules for the input request */ const whatIsAllowed = async (request, authZ) => { let response; try { const whatIsAllowedResponse = await authZ.acs.whatIsAllowed(request); response = { ...whatIsAllowedResponse }; // TODO Decision? response.obligations = (0, utils_1.mapResourceURNObligationProperties)(whatIsAllowedResponse.obligations); } catch (err) { logger_1.default.error('Error invoking acs-srv whatIsAllowed method', { code: err.code, message: err.message, stack: err.stack }); return { decision: access_control_1.Response_Decision.DENY, policy_sets: [], operation_status: (0, utils_1.generateOperationStatus)(err.code, err.message) }; } return response; }; exports.whatIsAllowed = whatIsAllowed; //# sourceMappingURL=resolver.js.map