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