Files
tessera-ctl/apps/api/src/module-registry/module.guard.ts
T
schalli 17dca0dfad fix(260911-e2s): Guard-Umbau und Kommentarkorrekturen nachtragen (Aufgabe 2 vollstaendig)
Die vorige Aufgabe-2-Teilcommit (11f5731) hatte nur die Loeschung von
tenant.middleware.ts und die neue tenant.guard.spec.ts erfasst — ein
`git add` mit mehreren Pfaden schlug wegen eines bereits entfernten
Pfads fataler fehl und liess die restlichen fuenf Dateien unstaged,
ohne dass das beim Commit auffiel (Rule 1 — Prozessfehler, hier
korrigiert). Dieser Commit traegt den eigentlichen Umbau nach:
tenant.guard.ts ohne Prisma-Abhaengigkeit, die geleerte
FORTENANT_ASSIGNMENT_EXCEPTIONS samt Wachhund-Test in
rls-access-inventory.spec.ts, und die drei berichtigten
Kommentarzeilen (app.module.ts, module.guard.ts, dkv.controller.ts).
Inhaltlich identisch mit dem, was bereits verifiziert wurde (891 Tests
gruen, Typpruefung sauber) — nur die Staging-Reihenfolge war fehlerhaft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-11 10:50:26 +02:00

110 lines
3.2 KiB
TypeScript

import {
applyDecorators,
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
SetMetadata,
UseGuards,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
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';
/**
* 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).
*/
@Injectable()
export class ModuleGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly moduleRegistryService: ModuleRegistryService,
private readonly moduleAccessService: ModuleAccessService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Get moduleSlug from metadata (set by @UseModule decorator)
const moduleSlug = this.reflector.getAllAndOverride<string>(
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 accessibleModuleIds = await this.moduleAccessService.getAccessibleModuleIds(
tenantId,
userId,
role,
);
if (!accessibleModuleIds.has(module.id)) {
throw new ForbiddenException(
`Module '${moduleSlug}' is not accessible for this user`,
);
}
// Per-Request-Memoisierung (D-09): ein nachfolgender Handler im
// selben Request bezahlt die Auflösung nicht ein zweites Mal. Über
// Request-Grenzen hinweg wird nichts zwischengespeichert.
request.moduleAccessIds = accessibleModuleIds;
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),
);
}