UNPKG

@elsikora/nestjs-crud-automator

Version:

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

218 lines (215 loc) 15.4 kB
'use strict'; var tslib_es6 = require('../../../external/tslib/tslib.es6.js'); var cache_class = require('./hook/permission/cache.class.js'); require('./hook/permission/resolver.class.js'); var cache_class$1 = require('./iam/attachment/cache.class.js'); require('./iam/attachment/resolver.class.js'); var cache_class$2 = require('./iam/document/cache.class.js'); require('./iam/document/resolver.class.js'); require('./iam/document/validator.class.js'); require('./iam/engine.class.js'); require('./iam/query-planner.class.js'); var registry_class = require('./policy/registry.class.js'); var moduleOptionsToken_constant = require('../../../constant/class/authorization/module-options-token.constant.js'); var documentSourcesToken_constant = require('../../../constant/class/authorization/policy/document-sources-token.constant.js'); var controller_constant = require('../../../constant/decorator/api/controller.constant.js'); require('../../../constant/decorator/api/function.constant.js'); var method_constant = require('../../../constant/decorator/api/method.constant.js'); require('../../../enum/class/authorization/cache-mode.enum.js'); require('../../../enum/class/authorization/decision-type.enum.js'); 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'); require('../../../enum/decorator/api/route/subscriber-authorization-expectation.enum.js'); var type_enum = require('../../../enum/decorator/api/route/type.enum.js'); var common = require('@nestjs/common'); var core = require('@nestjs/core'); var exception_utility = require('../../../utility/error/exception.utility.js'); var logger_utility = require('../../../utility/logger.utility.js'); const authorizationBootstrapValidationLogger = logger_utility.LoggerUtility.getLogger("ApiAuthorizationBootstrapValidationService"); exports.ApiAuthorizationBootstrapValidationService = class ApiAuthorizationBootstrapValidationService { discoveryService; policyRegistry; hookPermissionCache; iamAttachmentCache; iamDocumentCache; documentSources; moduleOptions; constructor(discoveryService, policyRegistry, hookPermissionCache, iamAttachmentCache, iamDocumentCache, documentSources = [], moduleOptions = {}) { this.discoveryService = discoveryService; this.policyRegistry = policyRegistry; this.hookPermissionCache = hookPermissionCache; this.iamAttachmentCache = iamAttachmentCache; this.iamDocumentCache = iamDocumentCache; this.documentSources = documentSources; this.moduleOptions = moduleOptions; } onApplicationBootstrap() { this.hookPermissionCache.configure(this.moduleOptions.cache); this.iamAttachmentCache.configure(this.moduleOptions.cache); this.iamDocumentCache.configure(this.moduleOptions.cache); const controllerWrappers = this.discoveryService.getControllers(); authorizationBootstrapValidationLogger.verbose(`Starting authorization bootstrap validation for ${controllerWrappers.length} controllers.`); for (const controllerWrapper of controllerWrappers) { this.validateController(controllerWrapper); } authorizationBootstrapValidationLogger.verbose("Authorization bootstrap validation finished."); } assertValidMode(mode, controllerName, routeType) { if (mode === mode_enum.EApiAuthorizationMode.HOOKS || mode === mode_enum.EApiAuthorizationMode.IAM) { return; } if (routeType) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" route "${routeType}" uses unknown authorization mode "${String(mode)}"`); throw exception_utility.ErrorException(`Controller "${controllerName}" route "${routeType}" uses unknown authorization mode "${String(mode)}"`); } authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" uses unknown authorization mode "${String(mode)}"`); throw exception_utility.ErrorException(`Controller "${controllerName}" uses unknown authorization mode "${String(mode)}"`); } validateController(controllerWrapper) { const metatype = controllerWrapper.metatype; if (!metatype) { return; } authorizationBootstrapValidationLogger.verbose(`Validating authorization configuration for controller "${metatype.name}".`); const isSecurable = Boolean(Reflect.getMetadata(controller_constant.CONTROLLER_API_DECORATOR_CONSTANT.SECURABLE_METADATA_KEY, metatype)); const properties = Reflect.getMetadata(controller_constant.CONTROLLER_API_DECORATOR_CONSTANT.PROPERTIES_METADATA_KEY, metatype); const authorization = properties?.authorization; if (isSecurable && !authorization) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" is marked with @ApiControllerSecurable() but does not declare an authorization block`); throw exception_utility.ErrorException(`Controller "${metatype.name}" is marked with @ApiControllerSecurable() but does not declare an authorization block`); } if (!isSecurable && authorization) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" declares an authorization block but is missing @ApiControllerSecurable()`); throw exception_utility.ErrorException(`Controller "${metatype.name}" declares an authorization block but is missing @ApiControllerSecurable()`); } if (!authorization || !properties) { return; } this.assertValidMode(authorization.defaultMode, metatype.name); const enabledRouteTypes = Object.values(type_enum.EApiRouteType).filter((routeType) => properties.routes[routeType]?.generation?.isEnabled !== false); const enabledRouteModes = new Set(enabledRouteTypes.map((routeType) => properties.routes[routeType]?.security?.authorization?.mode ?? authorization.defaultMode)); const methodRouteModes = this.validateMethodLevelRouteMetadata(metatype, properties); for (const routeMode of methodRouteModes) { enabledRouteModes.add(routeMode); } const usesHooks = enabledRouteModes.has(mode_enum.EApiAuthorizationMode.HOOKS); const usesIam = enabledRouteModes.has(mode_enum.EApiAuthorizationMode.IAM); for (const routeType of enabledRouteTypes) { const routeMode = properties.routes[routeType]?.security?.authorization?.mode ?? authorization.defaultMode; this.assertValidMode(routeMode, metatype.name, routeType); } if (usesHooks && !this.policyRegistry.hasSubscriberForEntity(properties.entity)) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" uses hooks authorization but no @ApiAuthorizationPolicy(...) is registered for entity "${properties.entity.name ?? "UnknownEntity"}"`); throw exception_utility.ErrorException(`Controller "${metatype.name}" uses hooks authorization but no @ApiAuthorizationPolicy(...) is registered for entity "${properties.entity.name ?? "UnknownEntity"}"`); } if (usesIam) { if (!authorization.policyNamespace) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" uses IAM authorization but does not declare policyNamespace`); throw exception_utility.ErrorException(`Controller "${metatype.name}" uses IAM authorization but does not declare policyNamespace`); } if (!authorization.resourceDefinition) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" uses IAM authorization but does not declare resourceDefinition`); throw exception_utility.ErrorException(`Controller "${metatype.name}" uses IAM authorization but does not declare resourceDefinition`); } if (this.documentSources.length === 0) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" uses IAM authorization but no documentSources are registered`); throw exception_utility.ErrorException(`Controller "${metatype.name}" uses IAM authorization but no documentSources are registered`); } if (authorization.resourceDefinition.namespace !== authorization.policyNamespace) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" resourceDefinition namespace must match policyNamespace`); throw exception_utility.ErrorException(`Controller "${metatype.name}" resourceDefinition namespace must match policyNamespace`); } if (authorization.resourceDefinition.entity !== properties.entity) { authorizationBootstrapValidationLogger.error(`Controller "${metatype.name}" resourceDefinition entity must match controller entity`); throw exception_utility.ErrorException(`Controller "${metatype.name}" resourceDefinition entity must match controller entity`); } this.validateResourceDefinition(metatype.name, authorization.resourceDefinition); } } validateMethodLevelRouteMetadata(metatype, properties) { const modes = new Set(); for (const methodName of Object.getOwnPropertyNames(metatype.prototype)) { if (methodName === "constructor") { continue; } const handler = metatype.prototype[methodName]; const handlerMetadata = typeof handler === "function" ? Reflect.getMetadata(method_constant.METHOD_API_DECORATOR_CONSTANT.ROUTE_METADATA_KEY, handler) : undefined; const prototypeMetadata = Reflect.getMetadata(method_constant.METHOD_API_DECORATOR_CONSTANT.ROUTE_METADATA_KEY, metatype.prototype, methodName); const metadata = (handlerMetadata ?? prototypeMetadata); if (!metadata) { continue; } if (!metadata.resource?.action || typeof metadata.resource.action !== "string") { throw exception_utility.ErrorException(`Controller "${metatype.name}" method "${methodName}" must declare route resource.action`); } if (metadata.resource.entity !== properties.entity) { throw exception_utility.ErrorException(`Controller "${metatype.name}" method "${methodName}" route resource.entity must match controller entity`); } const mode = metadata.security?.authorization?.mode; if (!mode) { throw exception_utility.ErrorException(`Controller "${metatype.name}" method "${methodName}" must declare method-level authorization mode`); } this.assertValidMode(mode, metatype.name); modes.add(mode); } return modes; } validateResourceDefinition(controllerName, resourceDefinition) { if (!resourceDefinition) { return; } if (typeof resourceDefinition.namespace !== "string" || resourceDefinition.namespace.length === 0) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" resourceDefinition.namespace must be a non-empty string`); throw exception_utility.ErrorException(`Controller "${controllerName}" resourceDefinition.namespace must be a non-empty string`); } if (typeof resourceDefinition.resourceType !== "string" || resourceDefinition.resourceType.length === 0) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" resourceDefinition.resourceType must be a non-empty string`); throw exception_utility.ErrorException(`Controller "${controllerName}" resourceDefinition.resourceType must be a non-empty string`); } if (typeof resourceDefinition.resourcePath !== "string" || resourceDefinition.resourcePath.length === 0) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" resourceDefinition.resourcePath must be a non-empty string`); throw exception_utility.ErrorException(`Controller "${controllerName}" resourceDefinition.resourcePath must be a non-empty string`); } const fieldPaths = new Set(); const queryPaths = new Set(); for (const field of resourceDefinition.fields) { if (!field.path.startsWith("resource.")) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" resource field path "${field.path}" must start with "resource."`); throw exception_utility.ErrorException(`Controller "${controllerName}" resource field path "${field.path}" must start with "resource."`); } if (field.path.length === 0 || field.queryPath.length === 0) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" resource field mappings must use non-empty path and queryPath`); throw exception_utility.ErrorException(`Controller "${controllerName}" resource field mappings must use non-empty path and queryPath`); } if (fieldPaths.has(field.path)) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" resourceDefinition contains duplicate field path "${field.path}"`); throw exception_utility.ErrorException(`Controller "${controllerName}" resourceDefinition contains duplicate field path "${field.path}"`); } if (queryPaths.has(field.queryPath)) { authorizationBootstrapValidationLogger.error(`Controller "${controllerName}" resourceDefinition contains duplicate queryPath "${field.queryPath}"`); throw exception_utility.ErrorException(`Controller "${controllerName}" resourceDefinition contains duplicate queryPath "${field.queryPath}"`); } fieldPaths.add(field.path); queryPaths.add(field.queryPath); } } }; exports.ApiAuthorizationBootstrapValidationService = tslib_es6.__decorate([ common.Injectable(), tslib_es6.__param(5, common.Inject(documentSourcesToken_constant.AUTHORIZATION_POLICY_DOCUMENT_SOURCES_TOKEN)), tslib_es6.__param(5, common.Optional()), tslib_es6.__param(6, common.Inject(moduleOptionsToken_constant.AUTHORIZATION_MODULE_OPTIONS_TOKEN)), tslib_es6.__param(6, common.Optional()), tslib_es6.__metadata("design:paramtypes", [core.DiscoveryService, registry_class.ApiAuthorizationPolicyRegistry, cache_class.ApiAuthorizationHookPermissionCache, cache_class$1.ApiAuthorizationIamAttachmentCache, cache_class$2.ApiAuthorizationIamDocumentCache, Array, Object]) ], exports.ApiAuthorizationBootstrapValidationService); //# sourceMappingURL=bootstrap-validation.service.class.js.map