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