UNPKG

@elsikora/nestjs-crud-automator

Version:

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

202 lines (199 loc) 8.83 kB
import { ApiAuthorizationPolicyExecutor } from './executor.class.js'; import { PolicySubscriberWrapper } from './subscriber-wrapper.class.js'; import { AUTHORIZATION_POLICY_DECORATOR_CONSTANT } from '../../../../constant/class/authorization/policy/decorator.constant.js'; import { AuthorizationResolveDefaultPrincipal } from '../../../../utility/authorization/resolve-default-principal.utility.js'; import { GenerateEntityInformation } from '../../../../utility/generate-entity-information.utility.js'; import { LoggerUtility } from '../../../../utility/logger.utility.js'; const policyRegistryLogger = LoggerUtility.getLogger("ApiAuthorizationPolicyRegistry"); class ApiAuthorizationPolicyRegistry { cacheOptions; POLICY_REGISTRY; POLICY_RULE_CACHE; constructor() { this.POLICY_RULE_CACHE = new Map(); this.POLICY_REGISTRY = new Map(); this.cacheOptions = { isEnabled: false }; } async buildAggregatedPolicy(entity, action, options = {}) { const entityName = this.getEntityName(entity); policyRegistryLogger.debug(`Building aggregated policy for entity "${entityName}" action "${action}"`); const registrationWrapper = this.POLICY_REGISTRY.get(entityName); const registrations = registrationWrapper?.registrations ?? []; policyRegistryLogger.debug(`Found ${registrations.length} registration(s) for entity "${entityName}"`); policyRegistryLogger.debug(`All registered entities: [${[...this.POLICY_REGISTRY.values()].map((wrapper) => wrapper.getName()).join(", ")}]`); if (registrations.length === 0) { return undefined; } const entityMetadata = GenerateEntityInformation(entity); const { authenticationRequest, permissions = [], principal: principalOverride, principalResolver, requestMetadata, routeType: routeTypeOverride } = options; const principal = await this.resolvePrincipal(authenticationRequest, principalOverride, principalResolver); const contextData = { action, authenticationRequest, ...requestMetadata, entity, entityMetadata, permissions, principal, routeType: routeTypeOverride, }; const aggregatedRules = []; const policyIds = new Set(); for (const registration of registrations) { const context = { ...contextData, DATA: contextData, }; const rules = await this.resolvePolicyRules(registration, action, context, entityName); if (rules.length === 0) { continue; } policyIds.add(registration.policyId); const normalizedRules = rules.map((rule) => this.normalizeRule(registration.policyId, registration.priority ?? 0, rule, action)); aggregatedRules.push(...normalizedRules); } if (aggregatedRules.length === 0) { return undefined; } aggregatedRules.sort((a, b) => b.priority - a.priority); const policyDescription = registrations.find((registration) => Boolean(registration.description))?.description; const policyIdList = [...policyIds]; const policy = { action, description: policyDescription, entity, policyId: this.resolvePolicyId(entity), policyIds: policyIdList, rules: aggregatedRules, }; return policy; } clear() { this.POLICY_RULE_CACHE.clear(); this.POLICY_REGISTRY.clear(); } configureCache(options = {}) { this.cacheOptions = { isEnabled: Boolean(options.isEnabled), ttlMs: options.ttlMs, }; } hasSubscriberForEntity(entity) { return (this.POLICY_REGISTRY.get(this.getEntityName(entity))?.registrations.length ?? 0) > 0; } invalidateCache(entity) { if (!entity) { this.POLICY_RULE_CACHE.clear(); return; } this.invalidateCacheForEntity(this.getEntityName(entity)); } registerSubscriber(registration) { const normalizedRegistration = { cache: registration.cache, description: registration.description, entity: registration.entity, policyId: registration.policyId, priority: registration.priority ?? 0, subscriber: registration.subscriber, }; const entityName = this.getEntityName(normalizedRegistration.entity); policyRegistryLogger.verbose(`Registering policy subscriber for entity "${entityName}" with policyId "${normalizedRegistration.policyId}" and priority ${normalizedRegistration.priority}`); let wrapper = this.POLICY_REGISTRY.get(entityName); if (!wrapper) { wrapper = new PolicySubscriberWrapper(entityName); this.POLICY_REGISTRY.set(entityName, wrapper); } wrapper.addRegistration(normalizedRegistration); policyRegistryLogger.debug(`Total registrations for entity "${entityName}": ${wrapper.getRegistrationCount()}`); this.invalidateCache(normalizedRegistration.entity); } cacheRules(cacheKey, rules, cacheOptions) { if (!cacheOptions.isEnabled) { return; } this.POLICY_RULE_CACHE.set(cacheKey, { cachedAt: Date.now(), rules: rules }); } createPolicyCacheKey(entityName, registration, action, routeType) { const subscriberName = this.getSubscriberName(registration.subscriber); return `${entityName}::${registration.policyId}::${subscriberName}::${(routeType ?? "custom").toLowerCase()}::${action.toLowerCase()}`; } getCachedRules(cacheKey, cacheOptions) { if (!cacheOptions.isEnabled) { return undefined; } const cachedEntry = this.POLICY_RULE_CACHE.get(cacheKey); if (!cachedEntry) { return undefined; } if (this.isCacheExpired(cachedEntry.cachedAt, cacheOptions.ttlMs)) { this.POLICY_RULE_CACHE.delete(cacheKey); return undefined; } return cachedEntry.rules; } getEntityName(entity) { return (entity.name ?? "UnknownResource").toLowerCase(); } getSubscriberName(subscriber) { return subscriber.constructor?.name ?? "UnknownPolicySubscriber"; } invalidateCacheForEntity(entityName) { for (const cacheKey of this.POLICY_RULE_CACHE.keys()) { if (cacheKey.startsWith(`${entityName}::`)) { this.POLICY_RULE_CACHE.delete(cacheKey); } } } isCacheExpired(cachedAt, ttlMs) { if (ttlMs === undefined) { return false; } return Date.now() - cachedAt > ttlMs; } normalizeRule(policyId, subscriberPriority, rule, action) { const rulePriority = rule.priority ?? 0; return { action, condition: rule.condition, description: rule.description, effect: rule.effect, policyId, priority: subscriberPriority + rulePriority, resultTransform: rule.resultTransform, scope: rule.scope, }; } resolveCacheOptions(options) { return { isEnabled: options?.isEnabled ?? this.cacheOptions.isEnabled, ttlMs: options?.ttlMs ?? this.cacheOptions.ttlMs, }; } resolvePolicyId(entity) { return `${this.getEntityName(entity)}${AUTHORIZATION_POLICY_DECORATOR_CONSTANT.DEFAULT_POLICY_ID_SUFFIX}`; } async resolvePolicyRules(registration, action, context, entityName) { const cacheOptions = this.resolveCacheOptions(registration.cache); const cacheKey = this.createPolicyCacheKey(entityName, registration, action, context.routeType); const cachedRules = this.getCachedRules(cacheKey, cacheOptions); if (cachedRules) { return cachedRules; } const rules = await ApiAuthorizationPolicyExecutor.execute(registration.subscriber, action, context); this.cacheRules(cacheKey, rules, cacheOptions); return rules; } async resolvePrincipal(authenticationRequest, principalOverride, principalResolver) { if (principalOverride) { return principalOverride; } if (principalResolver) { return await principalResolver.resolve(authenticationRequest?.user, authenticationRequest); } return AuthorizationResolveDefaultPrincipal(authenticationRequest?.user); } } const apiAuthorizationPolicyRegistry = new ApiAuthorizationPolicyRegistry(); export { ApiAuthorizationPolicyRegistry, apiAuthorizationPolicyRegistry }; //# sourceMappingURL=registry.class.js.map