UNPKG

@elsikora/nestjs-crud-automator

Version:

A library for automating the creation of CRUD operations in NestJS.

333 lines (330 loc) 15.6 kB
'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