feat(260911-e2s): Benutzerzaehler im TenantController binden (Fan-out je Mandant)
findAll/findOne/remove zaehlen Benutzer je Mandant jetzt ueber drei
gebundene Aufrufstellen (tenantPrisma.user.count mit where: { tenantId
}, in remove zusaetzlich isActive: true) statt ueber den
Relationszaehler, der nach dem Scharfschalten unbemerkt unter der
Regel von User gelaufen waere (260911-e2s, Aufgabe 1, Pruefungen 5-7).
Fan-out-Muster aus UserService.findAllForPlatformAdmin uebernommen; die
vier tenant-Zugriffe bleiben ungebunden (Tenant ohne Regel). Antwortform,
Meldungen und Statuscodes unveraendert.
tenant.controller.spec.ts legt die Testlage aus dem Nichts an (20
Faelle): Zwei-Klienten-Nachweis ueber __makeBoundClient, Rollen-
Metadaten-Test (Klasse SUPER_ADMIN, kein Handler ueberschreibt), Wachhund
gegen mehrfache Klientenerzeugung. Falsifizierungsnachweis durchgefuehrt:
der probeweise ungebundene Zaehler in findOne macht 2 Faelle rot mit
"Cannot read properties of undefined (reading 'count')" — die dkv-Form
der Falsifizierung, nicht nur eine falsche Zahl —, danach zurueckgenommen.
Klassifikation und Entwicklungsanleitung nachgezogen: 64 Paare (ein
neues, tenant.controller.ts/user), Uebersichtszeile 8/3, Klassen-
Verteilung 32 muss-mandantengebunden, Erkennungsluecke fuer
Relationseinbindungen im Kopf der Bestandsaufnahme benannt, "Zwei
belegte Befunde" und "Was diese Etappe NICHT entscheidet" (erster
Punkt aufgeloest). Beide Dokument-Falsifizierungsnachweise durchgefuehrt
(falsche Klasse macht rls-access-inventory.spec.ts rot, falsche
Uebersichtszahl macht das herleitende Gate rot), zurueckgenommen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -145,13 +145,13 @@ Details dazu im Abschnitt [Das Modulsystem](#das-modulsystem).
|
||||
zeigt intern auf `http://api:3001`) auf.
|
||||
2. Die Anfrage trifft in `apps/api/src/main.ts` auf die globale `ValidationPipe` und läuft dann
|
||||
durch die drei global registrierten `APP_GUARD`s aus `app.module.ts`, in genau dieser
|
||||
Reihenfolge: `JwtAuthGuard` (Auth) → `TenantGuard` (setzt `req.tenantId`/`req.tenantPrisma`
|
||||
Reihenfolge: `JwtAuthGuard` (Auth) → `TenantGuard` (setzt `req.tenantId`
|
||||
aus dem JWT) → `RolesGuard` (prüft `@Roles()`).
|
||||
3. Trägt der Controller zusätzlich `@UseModule('slug')`, prüft anschließend `ModuleGuard`
|
||||
(`apps/api/src/module-registry/module.guard.ts`) Modulzugriff über `ModuleAccessService`.
|
||||
4. Der Controller ruft den zugehörigen Service auf, der über `PrismaService`
|
||||
(`apps/api/src/prisma/prisma.service.ts`) oder — für mandantensensible Tabellen — über den
|
||||
tenant-gescopten Client aus `req.tenantPrisma` auf Postgres zugreift.
|
||||
(`apps/api/src/prisma/prisma.service.ts`) oder — für mandantensensible Tabellen — über einen
|
||||
dienst-intern per `forTenant()` gebundenen Client auf Postgres zugreift.
|
||||
5. Die Antwort geht als JSON zurück; das Frontend rendert sie in der jeweiligen Server- oder
|
||||
Client-Komponente.
|
||||
|
||||
@@ -296,16 +296,17 @@ Unterverzeichnisse, die vom selben `layout.tsx` mitgedeckt werden.
|
||||
Der tatsächliche Mechanismus ist `TenantGuard` (`apps/api/src/tenant/tenant.guard.ts`), global als
|
||||
`APP_GUARD` in `app.module.ts` registriert — er läuft nach `JwtAuthGuard`, weil `req.user` erst
|
||||
dann gesetzt ist. `TenantGuard` liest `tenantId` aus dem JWT-Claim des Anfragenden, erlaubt
|
||||
SUPER_ADMIN einen Wechsel per `x-tenant-id`-Header, und setzt anschließend `req.tenantId` sowie
|
||||
`req.tenantPrisma` — einen über `forTenant()`
|
||||
(`apps/api/src/prisma/prisma-tenant.extension.ts`) erzeugten Prisma-Client, der vor **jeder** Query
|
||||
in einer Transaktion `SELECT set_config('app.current_tenant', $1, true)` ausführt.
|
||||
SUPER_ADMIN einen Wechsel per `x-tenant-id`-Header, und setzt anschließend AUSSCHLIESSLICH
|
||||
`req.tenantId` (260911-e2s). Die Bindung an den Mandanten geschieht dienst-intern, je
|
||||
Service-Methode neu, über das Bindungshilfsmittel `forTenant()`
|
||||
(`apps/api/src/prisma/prisma-tenant.extension.ts`), das vor **jeder** Query in einer Transaktion
|
||||
`SELECT set_config('app.current_tenant', $1, true)` ausführt — der Guard selbst erzeugt keinen
|
||||
Prisma-Client mehr und veröffentlicht keinen auf dem Anfrageobjekt.
|
||||
|
||||
> Im Code existiert daneben eine gleichnamige `TenantMiddleware`
|
||||
> (`apps/api/src/tenant/tenant.middleware.ts`) mit identischer Logik. Sie ist in `app.module.ts`
|
||||
> nirgends über `.apply(...).forRoutes(...)` eingebunden — der tatsächlich aktive Mechanismus ist
|
||||
> ausschließlich `TenantGuard`. Vereinzelte Code-Kommentare verweisen noch auf „TenantMiddleware“;
|
||||
> gemeint ist in jedem Fall der Guard.
|
||||
> Ein früherer Entwurf veröffentlichte zusätzlich einen gebundenen Prisma-Client auf dem
|
||||
> Anfrageobjekt, dupliziert in einer gleichnamigen, nie in `app.module.ts` registrierten
|
||||
> Express-Middleware mit identischer Logik — beides wurde mit 260911-e2s entfernt, nachdem eine
|
||||
> Volltextsuche keinen Leser dieser Eigenschaft außerhalb der beiden Dateien fand.
|
||||
|
||||
`app.current_tenant` wird von **Postgres Row-Level-Security** ausgewertet. RLS-Policies sind aber
|
||||
**nicht** auf allen Tabellen aktiv — aktuell nur auf `User`, `PasswordResetToken`, `LdapConfig`,
|
||||
@@ -318,12 +319,12 @@ RLS-Policy.
|
||||
**Was ein Entwickler nie vergessen darf:** Bei jeder Query gegen eine Tabelle ohne RLS-Policy muss
|
||||
`tenantId` **manuell** in die `where`-Klausel — die Datenbank filtert hier nichts von selbst. Das
|
||||
ist im Code auch der gelebte Stil: `DkvService.loadConfig()`
|
||||
(`apps/api/src/dkv/dkv.service.ts`) etwa nutzt den plain `PrismaService` (nicht
|
||||
`req.tenantPrisma`) und filtert explizit mit `where: { tenantId }`. Wer bei einer solchen Tabelle
|
||||
das `tenantId`-Filter vergisst, liest oder schreibt mandantenübergreifend — ohne dass RLS das
|
||||
auffängt. Bei den sieben RLS-geschützten Tabellen greift die DB-seitige Absicherung zusätzlich,
|
||||
vorausgesetzt die Query läuft tatsächlich über den `tenantPrisma`-Client aus `req.tenantPrisma`
|
||||
und nicht über den globalen `PrismaService`.
|
||||
(`apps/api/src/dkv/dkv.service.ts`) etwa nutzt den plain, UNGEBUNDENEN `PrismaService` und
|
||||
filtert explizit mit `where: { tenantId }`. Wer bei einer solchen Tabelle das `tenantId`-Filter
|
||||
vergisst, liest oder schreibt mandantenübergreifend — ohne dass RLS das auffängt. Bei den sieben
|
||||
RLS-geschützten Tabellen greift die DB-seitige Absicherung zusätzlich, vorausgesetzt die Query
|
||||
läuft tatsächlich über einen dienst-intern per `forTenant()` gebundenen Client und nicht über
|
||||
den globalen, ungebundenen `PrismaService`.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
|
||||
Reference in New Issue
Block a user