diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 8d3b0f0..9b845ab 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -54,7 +54,7 @@ import { UserModule } from './user/user.module'; provide: APP_GUARD, useClass: JwtAuthGuard, }, - // Runs after JwtAuthGuard — sets req.tenantId and req.tenantPrisma from req.user + // Runs after JwtAuthGuard — sets req.tenantId from req.user (260911-e2s: no longer creates a Prisma client) { provide: APP_GUARD, useClass: TenantGuard, diff --git a/apps/api/src/dkv/dkv.controller.ts b/apps/api/src/dkv/dkv.controller.ts index 6ecc90d..fce940c 100644 --- a/apps/api/src/dkv/dkv.controller.ts +++ b/apps/api/src/dkv/dkv.controller.ts @@ -30,7 +30,7 @@ import { CreateVehicleDto, UpdateVehicleDto } from './dto/dkv-vehicle.dto'; * Global JwtAuthGuard enforces JWT authentication; RolesGuard enforces the * @Roles decorator. No route is publicly accessible. * - * Tenant extraction: `req.tenantId` set by TenantMiddleware (runs after auth guards). + * Tenant extraction: `req.tenantId` set by TenantGuard (runs after auth guards). * All operations are scoped to the authenticated tenant's data. * * Routes: diff --git a/apps/api/src/module-registry/module.guard.ts b/apps/api/src/module-registry/module.guard.ts index 947dc1a..933bc7b 100644 --- a/apps/api/src/module-registry/module.guard.ts +++ b/apps/api/src/module-registry/module.guard.ts @@ -22,7 +22,7 @@ export const MODULE_SLUG_KEY = 'moduleSlug'; * Gruppen-Grant), D-01. * * Per T-03-04/T-15-10: tenantId, userId und role stammen ausschließlich - * aus dem validierten JWT (via TenantMiddleware/JwtAuthGuard), nie aus + * 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 diff --git a/apps/api/src/prisma/rls-access-inventory.spec.ts b/apps/api/src/prisma/rls-access-inventory.spec.ts index 86792ca..5045d33 100644 --- a/apps/api/src/prisma/rls-access-inventory.spec.ts +++ b/apps/api/src/prisma/rls-access-inventory.spec.ts @@ -1,4 +1,4 @@ -import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative } from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -43,17 +43,27 @@ const DOC_PATH = join(REPO_ROOT, 'docs/mandantentrennung-zugriffsklassifikation. /** * Dateien, in denen ein `forTenant(`-Aufruf bewusst NICHT der erkannten - * `const = forTenant(`-Zuweisungsform folgt. Beide veroeffentlichen - * den gebundenen Client auf dem Anfrageobjekt (`req.tenantPrisma = ...`) - * statt ihn einer lokalen Konstante zuzuweisen — genau dieser Weg ist die - * offene Architekturfrage aus docs/mandantentrennung-zugriffsklassifikation.md - * ("Was diese Etappe NICHT entscheidet"), hier bewusst offen gehalten statt - * stillschweigend als Erkennungsluecke durchzurutschen. + * `const = forTenant(`-Zuweisungsform folgt. + * + * Bis 260911-e2s standen hier zwei Dateien (`tenant.middleware.ts`, + * `tenant.guard.ts`): beide veroeffentlichten einen gebundenen Client auf + * dem Anfrageobjekt statt ihn einer lokalen Konstante zuzuweisen — der Weg + * war die offene Architekturfrage aus + * docs/mandantentrennung-zugriffsklassifikation.md ("Was diese Etappe NICHT + * entscheidet"). + * + * Die Frage ist mit 260911-e2s (Aufgabe 2) ENTSCHIEDEN: die + * dienst-interne Bindung (ein Klient je Methode, wie es alle neun vor + * diesem Bereich umgestellten Bereiche bereits vormachen) ist die + * Konvention; der Guard erzeugt ueberhaupt keinen Client mehr, die + * gleichlautende, nie verdrahtete Middleware ist geloescht. Diese Liste + * startet deshalb leer und bleibt es, bis ein begruendeter neuer + * Ausnahmefall auftritt — dieselbe Form wie + * `INTERACTIVE_TRANSACTION_EXCEPTIONS` unten. Der Test + * "keine veraltete Ausnahmeliste" unter dieser Datei stellt sicher, dass ein + * kuenftiger Eintrag nicht unbemerkt veraltet. */ -const FORTENANT_ASSIGNMENT_EXCEPTIONS = new Set([ - 'apps/api/src/tenant/tenant.middleware.ts', - 'apps/api/src/tenant/tenant.guard.ts', -]); +const FORTENANT_ASSIGNMENT_EXCEPTIONS = new Set([]); /** * Dateien, in denen eine interaktive Transaktion (`empfaenger.$transaction( @@ -347,6 +357,28 @@ describe('mandantentrennung-zugriffsklassifikation.md deckt den Quelltext vollst expect(violations, violations.join('\n')).toEqual([]); }); + it('keine veraltete Ausnahmeliste: jede Datei in [...FORTENANT_ASSIGNMENT_EXCEPTIONS] existiert und traegt tatsaechlich mindestens einen forTenant(-Aufruf ausserhalb der Zuweisungsform (260911-e2s, Aufgabe 2)', () => { + const staleEntries: string[] = []; + const analysesByFile = new Map(analyses.map((a) => [a.file, a])); + for (const file of [...FORTENANT_ASSIGNMENT_EXCEPTIONS]) { + if (!existsSync(join(REPO_ROOT, file))) { + staleEntries.push(`${file}: Datei existiert nicht mehr`); + continue; + } + const analysis = analysesByFile.get(file); + const unmatched = analysis ? analysis.totalForTenantCalls - analysis.assignmentFormCalls : 0; + if (unmatched <= 0) { + staleEntries.push( + `${file}: enthaelt keinen forTenant(-Aufruf ausserhalb der erkannten Zuweisungsform mehr — die Ausnahme ist ueberholt und gehoert entfernt`, + ); + } + } + expect( + staleEntries, + `Eine Ausnahmeliste, die Dateien nennt, die es nicht gibt oder die keinen Ausnahmefall mehr enthalten, ist dieselbe tote Verdrahtung, die 260911-e2s im Guard entfernt hat:\n${staleEntries.join('\n')}`, + ).toEqual([]); + }); + it('jede interaktive Transaktion (empfaenger.$transaction(async ...)) entspricht einer der erkannten Empfaengerformen oder steht in der begruendeten Ausnahmeliste (260909-jts, Befund B)', () => { const violations: string[] = []; for (const a of analyses) { diff --git a/apps/api/src/tenant/tenant.guard.ts b/apps/api/src/tenant/tenant.guard.ts index ef36bc1..5b57d2c 100644 --- a/apps/api/src/tenant/tenant.guard.ts +++ b/apps/api/src/tenant/tenant.guard.ts @@ -4,20 +4,32 @@ import { ForbiddenException, Injectable, } from '@nestjs/common'; -import { forTenant } from '../prisma/prisma-tenant.extension'; -import { PrismaService } from '../prisma/prisma.service'; /** * Runs AFTER JwtAuthGuard (guard execution order follows APP_GUARD registration order). - * At this point req.user is populated — middleware ran too early to access it. + * At this point req.user is populated — the order is load-bearing. * - * Sets req.tenantId and req.tenantPrisma for downstream controllers. - * Super-Admin can override tenant via x-tenant-id header (D-10). + * Sets ONLY req.tenantId for downstream code. Super-Admin can override the + * tenant via the x-tenant-id header (D-10). + * + * DECISION (260911-e2s, Aufgabe 2): an earlier design also published a + * tenant-scoped Prisma client on the request object, under a property + * named `tenantPrisma` (assigned via `forTenant(...)`), duplicated + * identically in a never-registered Express middleware class with the same + * logic (deleted with 260911-e2s). A full-text search across + * `apps/api/src` found no reader of that property outside those two + * files — every one of the nine areas converted before this one binds + * service-internally instead, one client per method call via the + * tenant-binding helper in + * `prisma-tenant.extension.ts`. That convention, settled by nine-fold + * practice, is why this guard no longer creates a client at all: dead + * wiring that LOOKS like a protection mechanism is worse than none — it + * suggests a safeguard to a later reader that never actually ran. See + * docs/mandantentrennung-etappe2-fehlerrichtung.md, section "## Bereich + * tenant", (n4)(a) for the measurement and the reasoning. */ @Injectable() export class TenantGuard implements CanActivate { - constructor(private readonly prisma: PrismaService) {} - canActivate(context: ExecutionContext): boolean { const req = context.switchToHttp().getRequest(); const user = req.user; @@ -38,10 +50,8 @@ export class TenantGuard implements CanActivate { } if (tenantId) { - req.tenantPrisma = forTenant(this.prisma, tenantId); req.tenantId = tenantId; } else { - req.tenantPrisma = this.prisma; req.tenantId = null; }