import { applyDecorators, CanActivate, ExecutionContext, ForbiddenException, Injectable, SetMetadata, UseGuards, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ModuleGrantLevel } from '@prisma/client'; import { ModuleAccessService } from './module-access.service'; import { ModuleRegistryService } from './module-registry.service'; /** * Metadata key for the module slug attached by @UseModule(). */ export const MODULE_SLUG_KEY = 'moduleSlug'; /** * Metadata key set by @ModuleManage(): the route needs the Freigabestufe * Verwalten (MANAGE) for the module, not just access (261002-icv). */ export const MODULE_MANAGE_KEY = 'moduleManage'; /** * Guard that checks whether the requesting user has access to the module * identified by its slug — Aktivierung UND (Rolle ODER Direkt-Grant ODER * Gruppen-Grant), D-01. * * Per T-03-04/T-15-10: tenantId, userId und role stammen ausschließlich * aus dem validierten JWT (via TenantGuard/JwtAuthGuard), nie aus * Body oder Params — verhindert Elevation of Privilege. * * T-15-03: Ohne `@UseModule(slug)`-Metadaten gibt der Guard bewusst * `true` zurück (Durchsetzung hängt am Dekorator) — jeder neue * Modul-Controller MUSS `@UseModule` tragen (Projektregel seit Phase 3). * * 261002-icv: Trägt die Route zusätzlich `@ModuleManage(slug)`, genügt * Zugriff allein nicht — die wirksame Freigabestufe muss Verwalten sein * (Administratoren erfüllen das über den Kurzschluss in * `getModuleAccessLevels`). Die Stufe wird serverseitig aus den ModuleGrant- * Zeilen aufgelöst, nie aus Body/Query (T-icv-02), und nur für das Modul * der Route (T-icv-04). */ @Injectable() export class ModuleGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly moduleRegistryService: ModuleRegistryService, private readonly moduleAccessService: ModuleAccessService, ) {} async canActivate(context: ExecutionContext): Promise { // Get moduleSlug from metadata (set by @UseModule decorator) const moduleSlug = this.reflector.getAllAndOverride( MODULE_SLUG_KEY, [context.getHandler(), context.getClass()], ); // If no module slug is set, allow (guard is not applicable) if (!moduleSlug) { return true; } const request = context.switchToHttp().getRequest(); const tenantId = request.tenantId ?? request.user?.tenantId; if (!tenantId) { throw new ForbiddenException('No tenant context'); } const userId = request.user?.id; const role = request.user?.role; if (!userId || !role) { throw new ForbiddenException('No user context'); } const module = await this.moduleRegistryService.findBySlug(moduleSlug); if (!module) { throw new ForbiddenException( `Module '${moduleSlug}' is not activated for this tenant`, ); } const requireManage = this.reflector.getAllAndOverride(MODULE_MANAGE_KEY, [ context.getHandler(), context.getClass(), ]) === true; // Per-Request-Memoisierung (D-09): ein Klassen-@UseModule plus ein // Handler-@ModuleManage lassen diesen Guard zweimal pro Request laufen; // die Aufloesung bezahlt nur der erste Lauf. Ueber Request-Grenzen // hinweg wird nichts zwischengespeichert. const levels: Map = request.moduleAccessLevels instanceof Map ? request.moduleAccessLevels : await this.moduleAccessService.getModuleAccessLevels(tenantId, userId, role); const level = levels.get(module.id); if (!level) { throw new ForbiddenException( `Module '${moduleSlug}' is not accessible for this user`, ); } if (requireManage && level !== ModuleGrantLevel.MANAGE) { throw new ForbiddenException( `Module '${moduleSlug}' requires manage permission`, ); } request.moduleAccessLevels = levels; request.moduleAccessIds = new Set(levels.keys()); return true; } } /** * Decorator that protects a controller or route handler with the ModuleGuard. * Ensures the specified module is accessible for the requesting user. * * Usage: * @UseModule('domaincheck') * @Controller('domaincheck') * export class DomaincheckController { ... } */ export function UseModule(slug: string) { return applyDecorators( SetMetadata(MODULE_SLUG_KEY, slug), UseGuards(ModuleGuard), ); } /** * Decorator fuer modul-eigene Konfiguration: verlangt Zugriff auf das Modul * UND die Freigabestufe Verwalten (261002-icv). Ersetzt * `@Roles(ADMIN, SUPER_ADMIN)` fuer Handler, die nur dieses eine Modul * konfigurieren. * * Verwendbar auf einem Handler innerhalb eines `@UseModule`-Controllers oder * auf einem ganzen Controller. Administratoren bestehen ueber den * D-03-Kurzschluss (sie loesen auf allen aktiven Modulen zu MANAGE auf). * * Niemals zusammen mit `@Roles` am selben Handler: der globale RolesGuard * wuerde Verwalter trotzdem sperren. Mandant, Benutzer und Rolle stammen * ausschliesslich aus dem JWT (T-15-10). * * Usage: * @ModuleManage('kantine-datev') * @Put('settings') * saveSettings(...) { ... } */ export function ModuleManage(slug: string) { return applyDecorators( SetMetadata(MODULE_SLUG_KEY, slug), SetMetadata(MODULE_MANAGE_KEY, true), UseGuards(ModuleGuard), ); }