@elsikora/nestjs-crud-automator
Version:
A library for automating the creation of CRUD operations in NestJS.
333 lines (330 loc) • 15.6 kB
JavaScript
'use strict';
var tslib_es6 = require('../../../../external/tslib/tslib.es6.js');
var queryPlanner_class = require('./query-planner.class.js');
require('../../../../enum/class/authorization/cache-mode.enum.js');
var decisionType_enum = require('../../../../enum/class/authorization/decision-type.enum.js');
var effect_enum = require('../../../../enum/class/authorization/effect.enum.js');
var mode_enum = require('../../../../enum/class/authorization/mode.enum.js');
require('../../../../enum/class/authorization/permission-match.enum.js');
require('../../../../enum/class/authorization/policy/attachment-type.enum.js');
require('../../../../enum/class/authorization/policy/on-type.enum.js');
require('../../../../enum/class/authorization/policy/source-type.enum.js');
require('../../../../enum/class/authorization/policy/status.enum.js');
require('../../../../enum/class/authorization/principal-type.enum.js');
var common = require('@nestjs/common');
var scopeMergeWhere_utility = require('../../../../utility/authorization/scope-merge-where.utility.js');
var exception_utility = require('../../../../utility/error/exception.utility.js');
var logger_utility = require('../../../../utility/logger.utility.js');
const iamEngineLogger = logger_utility.LoggerUtility.getLogger("ApiAuthorizationIamEngine");
exports.ApiAuthorizationIamEngine = class ApiAuthorizationIamEngine {
queryPlanner;
constructor(queryPlanner) {
this.queryPlanner = queryPlanner;
}
evaluate(options) {
iamEngineLogger.verbose(`Evaluating IAM authorization for principal "${options.principal.id}" action "${options.action}" with ${options.documents.length} documents.`);
const resourceString = this.resolveResourceString(options.resourceDefinition, options.requestMetadata, options.resource);
const documentMap = new Map(options.documents.map((document) => [document.id, document]));
const identityDocuments = options.attachments.attachments.map((attachment) => this.resolveDocumentRecord(attachment.policyId, documentMap, options.policyNamespace));
const boundaryDocuments = options.attachments.boundaries.map((attachment) => this.resolveDocumentRecord(attachment.policyId, documentMap, options.policyNamespace));
const identityEvaluation = this.evaluateDocumentSet({
action: options.action,
documents: identityDocuments,
principal: options.principal,
requestMetadata: options.requestMetadata,
resource: options.resource,
resourceDefinition: options.resourceDefinition,
resourceString,
});
const boundaryEvaluation = this.evaluateDocumentSet({
action: options.action,
documents: boundaryDocuments,
principal: options.principal,
requestMetadata: options.requestMetadata,
resource: options.resource,
resourceDefinition: options.resourceDefinition,
resourceString,
});
const identityScope = this.queryPlanner.plan({
principal: options.principal,
requestMetadata: options.requestMetadata,
resource: options.resource,
resourceDefinition: options.resourceDefinition,
statements: identityEvaluation.allowStatements,
});
const boundaryScope = boundaryDocuments.length === 0
? { isFullyCompilable: true, scope: undefined }
: this.queryPlanner.plan({
principal: options.principal,
requestMetadata: options.requestMetadata,
resource: options.resource,
resourceDefinition: options.resourceDefinition,
statements: boundaryEvaluation.allowStatements,
});
const statementTraces = [...identityEvaluation.statementTraces, ...boundaryEvaluation.statementTraces];
if (identityEvaluation.hasExplicitDeny || boundaryEvaluation.hasExplicitDeny) {
iamEngineLogger.verbose(`IAM authorization resolved explicit deny for principal "${options.principal.id}" action "${options.action}".`);
return Promise.resolve(this.buildDecision(options, {
decisionType: decisionType_enum.EApiAuthorizationDecisionType.EXPLICIT_DENY,
effect: effect_enum.EApiPolicyEffect.DENY,
scope: undefined,
statementTraces,
}));
}
if (identityEvaluation.allowStatements.length === 0 || (boundaryDocuments.length > 0 && boundaryEvaluation.allowStatements.length === 0) || !identityScope.isFullyCompilable || !boundaryScope.isFullyCompilable) {
iamEngineLogger.verbose(`IAM authorization resolved implicit deny for principal "${options.principal.id}" action "${options.action}".`);
return Promise.resolve(this.buildDecision(options, {
decisionType: decisionType_enum.EApiAuthorizationDecisionType.IMPLICIT_DENY,
effect: effect_enum.EApiPolicyEffect.DENY,
scope: undefined,
statementTraces,
}));
}
iamEngineLogger.verbose(`IAM authorization resolved explicit allow for principal "${options.principal.id}" action "${options.action}".`);
return Promise.resolve(this.buildDecision(options, {
decisionType: decisionType_enum.EApiAuthorizationDecisionType.EXPLICIT_ALLOW,
effect: effect_enum.EApiPolicyEffect.ALLOW,
scope: this.mergeScopes(identityScope.scope, boundaryScope.scope),
statementTraces,
}));
}
buildDecision(options, payload) {
return {
action: options.action,
appliedRules: [],
effect: payload.effect,
mode: mode_enum.EApiAuthorizationMode.IAM,
permissions: [],
policyId: options.policyNamespace,
policyIds: options.documents.map((document) => document.id),
principal: options.principal,
resource: options.resource,
resourceType: options.resourceDefinition.resourceType,
scope: payload.scope,
trace: {
attachments: options.attachments.attachments,
boundaries: options.attachments.boundaries,
decisionType: payload.decisionType,
documents: options.documents,
mode: mode_enum.EApiAuthorizationMode.IAM,
permissions: [],
statements: payload.statementTraces,
},
transforms: [],
};
}
evaluateDocumentSet(options) {
const allowStatements = [];
const statementTraces = [];
for (const document of options.documents) {
for (const statement of document.document.Statement) {
const isStatementMatched = this.isStatementMatched({
action: options.action,
principal: options.principal,
requestMetadata: options.requestMetadata,
resource: options.resource,
resourceDefinition: options.resourceDefinition,
resourceString: options.resourceString,
statement,
});
statementTraces.push({
effect: statement.Effect,
isMatched: isStatementMatched,
policyId: document.id,
sid: statement.Sid,
sourceType: document.sourceType,
});
if (!isStatementMatched) {
continue;
}
if (statement.Effect === effect_enum.EApiPolicyEffect.DENY) {
return {
allowStatements,
hasExplicitDeny: true,
statementTraces,
};
}
allowStatements.push(statement);
}
}
return {
allowStatements,
hasExplicitDeny: false,
statementTraces,
};
}
evaluateOperator(operator, actualValue, expectedValue) {
switch (operator) {
case "StringEquals": {
return this.normalizeComparable(actualValue) === this.normalizeComparable(expectedValue);
}
case "StringEqualsIfExists": {
return actualValue === undefined ? true : this.normalizeComparable(actualValue) === this.normalizeComparable(expectedValue);
}
case "StringNotEquals": {
return this.normalizeComparable(actualValue) !== this.normalizeComparable(expectedValue);
}
case "StringNotEqualsIfExists": {
return actualValue === undefined ? true : this.normalizeComparable(actualValue) !== this.normalizeComparable(expectedValue);
}
default: {
return false;
}
}
}
isRecord(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
isStatementMatched(options) {
const isActionMatched = options.statement.Action.some((pattern) => this.matchesWildcardPattern(pattern, options.action));
const isResourceMatched = options.statement.Resource.some((pattern) => this.matchesWildcardPattern(pattern, options.resourceString));
if (!isActionMatched || !isResourceMatched) {
return false;
}
if (!options.statement.Condition) {
return true;
}
const condition = options.statement.Condition;
for (const [operator, operands] of Object.entries(condition)) {
for (const [path, rawExpectedValue] of Object.entries(operands)) {
if (path.startsWith("resource.") && options.resource === undefined) {
const queryPlan = this.queryPlanner.plan({
principal: options.principal,
requestMetadata: options.requestMetadata,
resource: options.resource,
resourceDefinition: options.resourceDefinition,
statements: [options.statement],
});
if (!queryPlan.isFullyCompilable) {
return false;
}
continue;
}
const actualValue = this.resolvePathValue({
principal: options.principal,
request: {
body: options.requestMetadata.body,
headers: options.requestMetadata.headers,
ip: options.requestMetadata.ip,
parameters: options.requestMetadata.parameters,
query: options.requestMetadata.query,
},
resource: options.resource,
}, path);
const expectedValue = this.resolveConditionValue(rawExpectedValue, {
principal: options.principal,
request: {
body: options.requestMetadata.body,
headers: options.requestMetadata.headers,
ip: options.requestMetadata.ip,
parameters: options.requestMetadata.parameters,
query: options.requestMetadata.query,
},
resource: options.resource,
});
if (!this.evaluateOperator(operator, actualValue, expectedValue)) {
return false;
}
}
}
return true;
}
matchesWildcardPattern(pattern, value) {
if (pattern === "*") {
return true;
}
if (!pattern.includes("*")) {
return pattern === value;
}
if (!pattern.endsWith("*")) {
return false;
}
return value.startsWith(pattern.slice(0, -1));
}
mergeScopes(baseScope, additionalScope) {
if (!baseScope) {
return additionalScope;
}
if (!additionalScope) {
return baseScope;
}
return {
...baseScope,
...additionalScope,
where: scopeMergeWhere_utility.AuthorizationScopeMergeWhere(baseScope.where, additionalScope.where),
};
}
normalizeComparable(value) {
if (value === undefined || value === null) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
return value.toString();
}
return "";
}
resolveConditionValue(value, context) {
if (typeof value !== "string") {
return value;
}
const matcher = /^\$\{(.+)\}$/.exec(value);
if (!matcher) {
return value;
}
const matchedPath = matcher[1];
if (!matchedPath) {
return undefined;
}
return this.resolvePathValue({
principal: context.principal,
request: {
body: context.request.body,
headers: context.request.headers,
ip: context.request.ip,
parameters: context.request.parameters,
query: context.request.query,
},
resource: context.resource,
}, matchedPath);
}
resolveDocumentRecord(policyId, documentMap, policyNamespace) {
const document = documentMap.get(policyId);
if (!document) {
iamEngineLogger.error(`Policy document "${policyId}" is not available for IAM evaluation`);
throw exception_utility.ErrorException(`Policy document "${policyId}" is not available for IAM evaluation`);
}
if (document.namespace !== policyNamespace) {
iamEngineLogger.error(`Policy document "${policyId}" namespace "${document.namespace}" does not match required namespace "${policyNamespace}"`);
throw exception_utility.ErrorException(`Policy document "${policyId}" namespace "${document.namespace}" does not match required namespace "${policyNamespace}"`);
}
return document;
}
resolvePathValue(source, path) {
let currentValue = source;
for (const segment of path.split(".").filter(Boolean)) {
if (!this.isRecord(currentValue)) {
return undefined;
}
currentValue = currentValue[segment];
}
return currentValue;
}
resolveResourceString(resourceDefinition, requestMetadata, resource) {
const resourceSource = {
...(this.isRecord(resource) ? resource : {}),
...(this.isRecord(requestMetadata.parameters) ? requestMetadata.parameters : {}),
};
return resourceDefinition.resourcePath.replaceAll(/\{([\w.-]+)\}/g, (_match, key) => {
const resolvedValue = resourceSource[key];
return typeof resolvedValue === "string" && resolvedValue.length > 0 ? resolvedValue : "*";
});
}
};
exports.ApiAuthorizationIamEngine = tslib_es6.__decorate([
common.Injectable(),
tslib_es6.__metadata("design:paramtypes", [queryPlanner_class.ApiAuthorizationIamQueryPlanner])
], exports.ApiAuthorizationIamEngine);
//# sourceMappingURL=engine.class.js.map