Files

88 KiB

phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, files_deleted, estimate, must_haves
phase plan type wave depends_on autonomous requirements files_modified files_deleted estimate must_haves
quick-260911-e2s 01 execute 1
true
WINDOWS-18
ETAPPE-2-TENANT
apps/api/scripts/rls-scratch-check.mjs
docs/mandantentrennung-etappe2-fehlerrichtung.md
apps/api/src/tenant/tenant.guard.ts
apps/api/src/tenant/tenant.guard.spec.ts
apps/api/src/tenant/tenant.middleware.ts
apps/api/src/prisma/rls-access-inventory.spec.ts
apps/api/src/app.module.ts
apps/api/src/module-registry/module.guard.ts
apps/api/src/dkv/dkv.controller.ts
apps/api/src/tenant/tenant.controller.ts
apps/api/src/tenant/tenant.controller.spec.ts
docs/mandantentrennung-zugriffsklassifikation.md
docs/anleitung-entwicklung.md
apps/api/src/tenant/tenant.middleware.ts
tokens raw_tokens tasks confidence
180000 180000 3 low
truths artifacts key_links
Die seit Etappe 1 offene Frage nach dem gebundenen Klienten auf dem Anfrageobjekt ist ENTSCHIEDEN, nicht ein zehntes Mal vertagt: die Verdrahtung ist entfernt (Guard setzt nur noch die Mandantenkennung; die nie verdrahtete Middleware ist geloescht), die Entscheidung steht mit Grund in Code, Kritikschrift, Klassifikation (Abschnitt `Zwei belegte Befunde` UND Abschnitt `Was diese Etappe NICHT entscheidet`) und Entwicklungsanleitung, und ein Test macht ein Wiederauftauchen der Eigenschaft rot.
`req.tenantId` — die tragende Leistung des Guards, gelesen an 22 Stellen in 9 Dateien ausserhalb des Bereichs (gemessen) — bleibt in ALLEN fuenf Zweigen gesetzt; der SUPER_ADMIN-Wechsel per `x-tenant-id` (vom Marktplatz-Frontend benutzt) und die `ForbiddenException` fuer mandantenlose Nicht-SUPER_ADMIN bleiben. Jeder Zweig ist als Testfall festgenagelt, nicht behauptet.
Die Klassifikation der Tabelle `Tenant` ist gegen den Code UND gegen alle 34 ausgelieferten Migrationen geprueft, zur Laufzeit im Werkzeug (Pruefung 1): kein `CREATE POLICY`, kein `ENABLE ROW LEVEL SECURITY`, keine `tenantId`-Spalte, auch nicht in 20260910120000. Ein gebundener und ein ungebundener Lesezugriff auf `Tenant` liefern DIESELBEN Zeilen — Roh-SQL UND generierter Client (Pruefungen 2 und 4). Auf `Tenant` selbst ist nichts zu binden, und das ist gemessen.
Der Befund, den die Erwartung 'null Umstellungsarbeit' uebersehen haette, ist gemessen und behoben: drei der acht Zugriffsstellen (`findAll`, `findOne`, `remove` im Controller) zaehlen ueber den Relationszaehler in die geschuetzte Tabelle `User` hinein — Prisma erzeugt EINE Anweisung mit LEFT JOIN auf `User`. Nach dem Scharfschalten zeigte die Mandantenliste des Plattform-Administrators fuer JEDEN Mandanten null Benutzer, und der Loeschriegel (T-02-09) wuerde vakuum. Gemessen im Werkzeug (Pruefungen 5-7), behoben nach dem Fan-out-Muster des Bereichs `user` (260910-das): Mandanten ungebunden lesen, je Mandant EINEN gebundenen Zaehler.
Die umgekehrte Fehlerrichtung dieses Bereichs ist in ihrer Auspraegung benannt: nicht 'keine Mandanten', sondern 'jeder Mandant hat 0 Benutzer' (eine falsche Zahl, keine leere Liste) und ein Loeschen, das statt der verstaendlichen 400-Meldung an den Fremdschluessel `ON DELETE RESTRICT` laeuft (laut, 500, falsche Botschaft; die referentielle Pruefung umgeht den Zeilenschutz — gemessen, Pruefung 7). Das Frontend (`admin/tenants/page.tsx`) zeigt die falsche Zahl an und verschluckt jede nicht-OK-Antwort beim Loeschen. Festgehalten in (n2)/(n3).
Die Erkennungsluecke der maschinellen Bestandsaufnahme ist benannt und vermessen: sie sieht (Datei, Modell)-Paare ueber `this.prisma.<Modell>` bzw. `<gebundener Klient>.<Modell>`, aber KEINE Relationseinbindung (`include`, Relationszaehler) in eine zweite Tabelle. Alle 19 `include:`-Stellen und alle `_count`-Stellen des API-Quelltexts sind einzeln nachgesehen; der Bereich `tenant` war die einzige gefaehrliche Auspraegung (ungebundener aeusserer Aufruf auf ungeschuetzter Tabelle, Einbindung in geschuetzte Tabelle). Steht in (n4) und im Kopf der Bestandsaufnahme.
Die SUPER_ADMIN-Beschraenkung der Mandantenverwaltung ist GELESEN und festgenagelt: klassenweites `@Roles(Role.SUPER_ADMIN)`, `RolesGuard` mit `getAllAndOverride([handler, class])`, kein Handler ueberschreibt — ein Metadaten-Test schlaegt fehl, sobald ein Handler eine schwaechere Rolle bekommt.
Die Testlage fuer Guard und Controller entsteht aus dem Nichts (heute: nur `tenant.service.spec.ts` mit zwei Faellen zu `create`): `tenant.guard.spec.ts` und `tenant.controller.spec.ts` mit dem Zwei-Klienten-Nachweis, jeweils durch einen zurueckgenommenen Falsifizierungsnachweis mit woertlicher Meldung belegt.
Alle fuenf handgepflegten Dokumentstellen der Klassifikation sind nachgezogen und maschinell gegatet, die Gates LEITEN ihre Werte aus den im Dokument genannten Messanweisungen ab; der Umfang ist als ERLAUBNISLISTE gegen `f1017fa` gegatet; drei Fremddateien werden ausschliesslich in Kommentarzeilen angefasst und genau das ist gegatet.
Baseline gehalten am Ende JEDER Aufgabe: mindestens 883 Tests gruen (nach Aufgabe 2 und 3 mehr), Typpruefung sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden (mindestens 110). Der Schalter bleibt AUS, Schema und Migrationen unveraendert, keine Compose- oder Umgebungsdatei angefasst, nichts in Active Directory, KEINE Regel auf `Tenant`.
apps/api/scripts/rls-scratch-check.mjs — ein zwoelfter Abschnitt `runTenantAreaChecks` mit mindestens neun namentlich benannten Pruefungen, davon fuenf ueber den generierten Client; Migrationsscan zur Laufzeit; Wegwerf-Tabelle `Tenant` auf den ausgelieferten Spaltensatz gebracht und gegen `CREATE TABLE` aus 20260618112124 geprueft; Fremdschluessel `User_tenantId_fkey` wortgleich aus der Migration
docs/mandantentrennung-etappe2-fehlerrichtung.md — Abschnitt `## Bereich tenant` mit (n1) Messung, (n2) Signaltabelle, (n3) Leere/falsche Zahl als Abwesenheit im Backend UND Frontend, (n4) bewusst nicht geloest (darunter die Entscheidung zum Anfrageobjekt mit Grund, die Erkennungsluecke mit Liste, der Fremdschluessel als Rueckhalt), (n5) bewusst nicht angefasst
apps/api/src/tenant/tenant.guard.ts — setzt nur noch `req.tenantId`; keine Prisma-Abhaengigkeit mehr; Kopfkommentar nennt die Entscheidung und den Grund
apps/api/src/tenant/tenant.middleware.ts — GELOESCHT (nie verdrahtet, gemessen)
apps/api/src/tenant/tenant.guard.spec.ts — NEU, alle fuenf Zweige, Abwesenheit der Anfrageobjekt-Eigenschaft, Header nur fuer SUPER_ADMIN
apps/api/src/prisma/rls-access-inventory.spec.ts — Ausnahmeliste `FORTENANT_ASSIGNMENT_EXCEPTIONS` geleert mit fortgeschriebenem Kommentar; neuer Testfall gegen veraltete Ausnahmeeintraege
apps/api/src/app.module.ts, apps/api/src/module-registry/module.guard.ts, apps/api/src/dkv/dkv.controller.ts — je EINE Kommentarzeile berichtigt (nennen heute die Anfrageobjekt-Eigenschaft bzw. die nie verdrahtete Middleware), keine Codezeile
apps/api/src/tenant/tenant.controller.ts — `findAll`/`findOne`/`remove` zaehlen Benutzer je Mandant ueber `forTenant()` (drei Aufrufstellen, `tenantPrisma.user.count`), die vier `tenant`-Zugriffe bleiben ungebunden; Kopfkommentar nennt Fan-out-Muster und Grund
apps/api/src/tenant/tenant.controller.spec.ts — NEU, Zwei-Klienten-Nachweis ueber `__makeBoundClient`, Faelle fuer alle drei zaehlenden Handler, Rollen-Metadaten-Test, Wachhund
docs/mandantentrennung-zugriffsklassifikation.md — Abschnitt `Zwei belegte Befunde` (Befund 1 entschieden), Kopf der Bestandsaufnahme (Erkennungsluecke), drei Bestandsaufnahme-Zeilen (zwei fortgeschrieben, eine NEU: `tenant.controller.ts`/`user`), Uebersichtszeile, Summenzeile, Klassen-Verteilung (64 Paare, mit Stand-Vermerk), Hintergrunddienst-Abschnitt (kein sechster Fall, gemessen), Abschnitt `Was diese Etappe NICHT entscheidet` (erster Punkt aufgeloest)
docs/anleitung-entwicklung.md — Abschnitt `## Mandantentrennung`: Guard-Beschreibung ohne Anfrageobjekt-Klient, Middleware-Hinweiskasten ersetzt durch einen Satz zur Entfernung
`TenantGuard` ist als zweiter `APP_GUARD` in `app.module.ts` registriert (nach `JwtAuthGuard`, vor `RolesGuard`); die Reihenfolge ist tragend, weil `req.user` erst nach dem JWT-Guard existiert. Die Middleware war NIE registriert (kein `MiddlewareConsumer`, kein `configure(` im gesamten `apps/api`) — der Hinweis im Auftrag, `app.module.ts` verdrahte die Middleware, ist falsch: Zeile 57 verdrahtet den Guard.
Prisma 6.19 uebersetzt `include: { _count: { select: { users: true } } }` in EINE SQL-Anweisung: `SELECT ... FROM "Tenant" LEFT JOIN (SELECT "tenantId", COUNT(*) FROM "User" ... GROUP BY "tenantId") ...` (gemessen mit Abfrageprotokoll, Befund F). Der Zaehler laeuft damit unter der Regel von `User` — ungebunden nach dem Scharfschalten null fuer jeden Mandanten.
Die referentielle Pruefung des Fremdschluessels `User_tenantId_fkey` (`ON DELETE RESTRICT`, 20260618112124:105) laeuft mit den Rechten des Tabelleneigentuemers und umgeht den Zeilenschutz — deshalb faengt sie ein Loeschen, das der vakuum gewordene Riegel durchliesse, laut ab (Pruefung 7 misst das, statt es der Dokumentation zu glauben).
Das Fan-out-Muster: `this.prisma.tenant.findMany` ungebunden (Tenant ohne Regel, gemessen) als Schleifentreiber, dann je Mandant `const tenantPrisma = forTenant(this.prisma, tenant.id) as any; tenantPrisma.user.count({ where: { tenantId: tenant.id } })` — wortgleich die Form von `UserService.findAllForPlatformAdmin`. Das ausdrueckliche `where: { tenantId }` ist HEUTE (BYPASSRLS) der einzige Filter und bleibt.
Der SUPER_ADMIN-Kopfwechsel `x-tenant-id` ist NICHT tot: `apps/web/src/app/(portal)/marketplace/page.tsx` und `marketplace/[slug]/page.tsx` senden ihn (vier Stellen). Er bleibt im Guard und wird durch Tests festgenagelt — auch der Fall, dass ein Nicht-SUPER_ADMIN den Header schickt und ignoriert wird (T-04-03).
Etappe 2 der Mandantentrennung, zehnter Bereich: `tenant`. Acht Zugriffe in zwei Dateien, beide auf die Mandantentabelle selbst — die einzige Tabelle, die per Definition keine Mandantenkennung tragen kann. Auf ihr ist nichts zu binden, und dieser Plan misst das, statt es der Klassifikation zu glauben.

Zweck: dieser Bereich traegt zwei Dinge, die jeder der neun Bereiche davor vertagt oder uebersehen hat. Erstens die seit Etappe 1 offene Frage, ob der gebundene Klient auf dem Anfrageobjekt (gesetzt von Guard und Middleware, von niemandem gelesen) bleibt oder geht — nach neun Bereichen, die alle dienst-intern binden, ist die Konvention durch Praxis entschieden, und tote Verdrahtung, die wie ein Sicherheitsmechanismus AUSSIEHT, ist schlimmer als keine. Zweitens einen Befund, den die Erwartung "null Umstellungsarbeit" verdeckt haette: drei der acht Zugriffe zaehlen ueber den Relationszaehler in die geschuetzte Tabelle User hinein. Nach dem Scharfschalten saehe der Plattform-Administrator eine Mandantenliste, in der JEDER Mandant null Benutzer hat, und der Loeschriegel "keine aktiven Benutzer" wuerde vakuum. Die maschinelle Bestandsaufnahme kann das strukturell nicht sehen — auch das wird hier benannt und vermessen.

Ergebnis: eine Entscheidung mit Grund an vier Stellen, eine geloeschte Middleware, ein Guard ohne Prisma, zwei aus dem Nichts angelegte Testdateien, drei gebundene Zaehler im Controller nach dem Fan-out-Muster des Bereichs user, eine gemessene Kritikschrift, und Dokumente, die am Ende nachweislich mit dem Quelltext uebereinstimmen.

Der Schalter bleibt AUS. DATABASE_URL zeigt weiterhin auf die Rolle tessera mit BYPASSRLS. Schema und Migrationen werden NICHT angefasst, Tenant bekommt KEINE Regel. Nichts wird in Active Directory geaendert.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/STATE.md @docs/mandantentrennung-zugriffsklassifikation.md @docs/mandantentrennung-etappe2-fehlerrichtung.md @docs/anleitung-entwicklung.md @apps/api/prisma/migrations/20260618112124_auth_multi_tenancy/migration.sql @apps/api/prisma/migrations/20260618112133_rls_policies/migration.sql @apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql @apps/api/src/prisma/prisma-tenant.extension.ts @apps/api/src/prisma/rls-access-inventory.spec.ts @apps/api/scripts/rls-scratch-check.mjs @apps/api/src/tenant/tenant.guard.ts @apps/api/src/tenant/tenant.middleware.ts @apps/api/src/tenant/tenant.controller.ts @apps/api/src/tenant/tenant.service.ts @apps/api/src/tenant/tenant.service.spec.ts @apps/api/src/tenant/tenant.module.ts @apps/api/src/app.module.ts @apps/api/src/auth/guards/roles.guard.ts @apps/api/src/auth/decorators/roles.decorator.ts @apps/api/src/user/user.service.ts @apps/api/src/dkv/dkv.service.spec.ts @apps/api/src/dashboard/dashboard.service.spec.ts @apps/api/src/module-registry/module.guard.spec.ts @.planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-PLAN.md

<planning_time_findings>

Alle Zahlen unten sind zur Planungszeit am 2026-09-11 gegen HEAD f1017fa GEMESSEN, mit der jeweils angegebenen Anweisung. Sie leiten die Untersuchung, sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder Aenderung erneut gelesen, jede Zahl zur Ausfuehrungszeit erneut gemessen, und weicht eine Messung ab, gilt die Messung und nicht dieser Plan. Baseline zur Planungszeit selbst nachgemessen: 883 Tests in 57 Dateien gruen, Werkzeug 101/101 in etwa neun Sekunden.

Befund A — die Zahl haelt, ein Modell, zwei Dateien; die Klassifikation der Tabelle stimmt. Gemessen mit grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/tenant | grep -v spec: acht Treffer, alle auf tenant — vier in tenant.controller.ts (findAll: findMany, findOne: findUnique, remove: findUnique und delete), vier in tenant.service.ts (findAll, findById, create, update). model Tenant in schema.prisma hat keine tenantId-Spalte. Gemessen ueber alle 34 Migrationsverzeichnisse mit grep -rhoiE 'ALTER TABLE "[A-Za-z]+" (ENABLE|FORCE) ROW LEVEL SECURITY' apps/api/prisma/migrations --include=*.sql | sort | uniq -c: 23 Tabellen mit Zeilenschutz, Tenant ist NICHT darunter; mit grep -rniE 'POLICY|ROW LEVEL|GRANT|REVOKE|FORCE' apps/api/prisma/migrations --include=*.sql | grep -iE '"Tenant"': genau ein Treffer, und das ist die Fremdschluessel-Definition von ModuleGrant, keine Regel. grep -n '"Tenant"' apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql: null Treffer. "Tenant" kommt ueberhaupt nur in zwei Migrationen vor (20260618112124 mit CREATE TABLE, 20260804130130 als Fremdschluesselziel); es gibt KEIN ALTER TABLE "Tenant" — der Spaltensatz aus CREATE TABLE ist der ausgelieferte Stand (sechs Spalten: id, name, slug, isActive, createdAt, updatedAt). Die beiden Bestandsaufnahme-Zeilen (keine-mandantengebundene-tabelle, ungebunden) stimmen mit dem Quelltext ueberein. Aufgabe 1 prueft das zur Laufzeit im Werkzeug (Pruefung 1), damit die Aussage nicht an diesem Absatz haengt.

Befund B — die Anfrageobjekt-Eigenschaft hat KEINEN Leser, und die Reichweite der Suche steht hier. Suchumfang: grep -rn "tenantPrisma" apps/api --include=*.ts --include=*.mjs --include=*.js (ohne node_modules, dist/ ist gitignored und Build-Ausgabe): 30 Dateien unter apps/api/src. JEDER Treffer ausserhalb von tenant/ ist eine lokale Zuweisung const tenantPrisma = forTenant( mit anschliessendem tenantPrisma.<Modell> — die dienst-interne Konvention. Property-Zugriffe der Form <etwas>.tenantPrisma (gemessen mit grep -rn '\.tenantPrisma' apps/api/src --include=*.ts) gibt es genau sieben: die vier Zuweisungen in tenant.middleware.ts:44,48 und tenant.guard.ts:41,44, und drei KOMMENTARE (tenant.guard.ts:14, app.module.ts:57, rls-access-inventory.spec.ts:47). Kein Lesezugriff. apps/web/src und packages: null Treffer. Zusatzbefund zu Fehler 6 des Vorhabens: der Etappe-1-Befund spricht von "diesen beiden Dateien und ihren Tests" — es GIBT keine Tests fuer Guard oder Middleware (ls apps/api/src/tenant/: einzige Testdatei ist tenant.service.spec.ts, zwei Faelle, beide zu create; grep -rln "TenantGuard\|TenantMiddleware" apps/api/src | grep spec: nur rls-access-inventory.spec.ts, das die beiden Dateien in einer Ausnahmeliste fuehrt). Die Auflage "Tests anpassen, nicht loeschen" trifft deshalb auf nichts — die Tests muessen ANGELEGT werden (Befund I).

Befund C — die Middleware ist NICHT verdrahtet, und der Auftrag irrt an dieser Stelle. grep -rn "TenantMiddleware\|MiddlewareConsumer\|configure(" apps/api --include=*.ts --include=*.js --include=*.mjs --include=*.json -l | grep -v node_modules | grep -v /dist/: drei Dateien — die Klasse selbst und zwei Kommentare (module.guard.ts:25 "via TenantMiddleware/JwtAuthGuard", dkv.controller.ts:33 "set by TenantMiddleware"). app.module.ts implementiert kein NestModule, hat kein configure, importiert die Middleware nicht; Zeile 57 ist ein Kommentar ueber den GUARD, Zeile 58-61 registriert TenantGuard als APP_GUARD. Der Kopfkommentar des Guards erklaert es selbst: "middleware ran too early to access it" — die Middleware ist der ueberholte erste Entwurf aus Phase 02, durch den Guard ersetzt und liegen geblieben. docs/anleitung-entwicklung.md:304-307 haelt das bereits als Hinweiskasten fest ("nirgends ueber .apply(...).forRoutes(...) eingebunden"). ENTSCHEIDUNG dieses Plans: die Middleware wird GELOESCHT (eine nie aufgerufene Kopie des Guards mit identischer Logik ist tote Verdrahtung in Reinform), der Guard verliert die Anfrageobjekt-Eigenschaft und die Prisma-Abhaengigkeit. Grund: neun Bereiche binden dienst-intern, ein Klient je Methode; ein je Anfrage erzeugter Klient ohne Leser ist Kosten ohne Nutzen (eine $extends-Instanz je authentifizierter Anfrage — kein Verbindungsaufbau, aber eine Zuweisung, die niemand liest) und suggeriert einem spaeteren Leser einen Schutz, der nicht wirkt. Zur Ausfuehrungszeit ist die Nichtverdrahtung erneut zu messen (Anweisung oben); faende die Messung eine Verdrahtung, bleibt die Datei und verliert nur die Eigenschaft — dann steht das im SUMMARY als Abweichung.

Befund D — was der Guard SONST tut, ist tragend und bleibt. Er setzt req.tenantId — gelesen an 22 Stellen in 9 Dateien ausserhalb von src/tenant/ (gemessen mit grep -rc "req\.tenantId\|request\.tenantId" apps/api/src --include=*.ts | grep -v ":0" | grep -v spec | grep -v src/tenant/: ldap.controller.ts 11, settings.controller.ts 3, dkv.controller.ts 2, dkv.service.ts, groups.controller.ts, module-grants.controller.ts, module.guard.ts, tender-triage.dto.ts, app.module.ts je 1). Er erlaubt SUPER_ADMIN den Mandantenwechsel per x-tenant-id — und das Frontend BENUTZT ihn: grep -rn "x-tenant-id" apps/web/src findet vier Stellen in marketplace/page.tsx und marketplace/[slug]/page.tsx. Er wirft ForbiddenException('No tenant context') fuer mandantenlose Nicht-SUPER_ADMIN. Fuenf Zweige: kein Nutzer (Durchlass, nichts gesetzt); Nutzer mit Mandant ohne Header; Nicht-SUPER_ADMIN MIT Header (Header wird ignoriert, T-04-03); SUPER_ADMIN mit Header; SUPER_ADMIN ohne Mandant und ohne Header (null); mandantenloser Nicht-SUPER_ADMIN (Ausnahme). Der null-Zweig ist mit heutigem Sitzungsnachweis unerreichbar (User.tenantId ist String, nicht nullbar; auth.service.ts:146 und jwt.strategy.ts:32 tragen tenantId immer) — er bleibt unveraendert und wird als heutiges Verhalten getestet, nicht umgebaut (Befund L).

Befund E — die SUPER_ADMIN-Beschraenkung ist adaequat, aber nirgends festgenagelt. tenant.controller.ts:26-27: klassenweit @UseGuards(RolesGuard) und @Roles(Role.SUPER_ADMIN); kein Handler traegt ein eigenes @Roles. roles.guard.ts:11-14: getAllAndOverride mit [context.getHandler(), context.getClass()] — Handler-Metadaten wuerden die Klassen-Metadaten UEBERSCHREIBEN. Heute korrekt; ein spaeteres @Roles(Role.ADMIN) an einem einzelnen Handler oeffnete die Mandantenverwaltung fuer Mandanten-Admins, ohne dass irgendein Test rot wuerde. RolesGuard ist zusaetzlich global als APP_GUARD registriert — die klassenweite @UseGuards-Registrierung ist redundant, harmlos, bleibt (n5). Aufgabe 3 nagelt die Metadaten fest.

Befund F — der eigentliche Befund dieses Bereichs: drei Zugriffe zaehlen in die geschuetzte Tabelle User hinein. findAll (Zeile 40-47) und findOne (65-72) laden mit include: { _count: { select: { users: true } } }, remove (126-135) mit users: { where: { isActive: true } } als Zaehler. Gemessen mit Prisma-Abfrageprotokoll gegen die lokale Entwicklungsdatenbank (nur lesend): Prisma 6.19 erzeugt EINE Anweisung — SELECT "Tenant".* , COALESCE("aggr_selection_0_User"."_aggr_count_users", 0) FROM "Tenant" LEFT JOIN (SELECT "tenantId", COUNT(*) AS "_aggr_count_users" FROM "User" WHERE 1=1 GROUP BY "tenantId") AS "aggr_selection_0_User" ON ("Tenant"."id" = "aggr_selection_0_User"."tenantId") (fuer remove mit WHERE "User"."isActive" = $1 im Unterselect). User traegt die Regel "tenantId" = current_tenant_id() (20260618112133:10-11). Folge nach dem Scharfschalten: ungebunden (der heutige Weg — SUPER_ADMIN ohne Header, Controller auf this.prisma) liefert die Mandantenzeilen vollstaendig (Tenant ohne Regel), aber userCount = 0 fuer JEDEN Mandanten; gebunden unter Mandant X liefert die Zeilen vollstaendig, den Zaehler nur fuer X. remove sieht null aktive Benutzer, der Riegel T-02-09 passiert, tenant.delete laeuft — und trifft den Fremdschluessel User_tenantId_fkey ON DELETE RESTRICT (20260618112124:105): laut, SQLSTATE 23503, Prisma P2003, HTTP 500 mit generischer Meldung statt der verstaendlichen 400. Die referentielle Pruefung laeuft laut PostgreSQL-Dokumentation mit Eigentuemerrechten und umgeht den Zeilenschutz — Aufgabe 1 MISST das (Pruefung 7) statt es zu zitieren. Zur Ausfuehrungszeit an allen drei Stellen erneut nachzulesen.

Befund G — die Bestandsaufnahme ist an dieser Stelle strukturell blind, und die Blindheit ist vermessen. rls-access-inventory.spec.ts sammelt (Datei, Modell)-Paare ueber this.prisma.<Modell> und <gebundener Klient>.<Modell>; eine Relationseinbindung in eine ZWEITE Tabelle (include, Relationszaehler) erzeugt kein Paar. Gemessen: grep -rn "_count" apps/api/src --include=*.ts | grep -v spec ausserhalb von tenant/: groups.service.ts:66 (auf gebundenem Klient — harmlos) und tenders.controller.ts:405 (groupBy auf der plattformweiten Tender, kein Relationszugriff — harmlos). grep -rn "include:" apps/api/src --include=*.ts | grep -v spec: 19 Stellen in 8 Dateien; alle auf gebundenen Klienten oder auf plattformweiten Tabellen mit plattformweiten Relationen (tenders.controller.ts:612 bindet sources = TenderSource, D-03), mit EINER Ausnahme neben tenant: ldap-config.service.ts:309 (getAllActiveConfigs, include: { tenant, fieldMappings }) — dort ist die AEUSSERE Tabelle LdapConfig bereits geschuetzt, die Einbindung fuegt dem bekannten Etappe-3-Fall nichts hinzu. Die gefaehrliche Auspraegung (ungebundener aeusserer Aufruf auf UNGESCHUETZTER Tabelle, Einbindung in GESCHUETZTE Tabelle) existiert im gesamten API-Quelltext genau einmal: in diesem Bereich. Aufgabe 1 wiederholt beide Messungen und listet sie in (n4); Aufgabe 3 haelt die Luecke im Kopf der Bestandsaufnahme fest. KEIN neuer Ledger-Eintrag dafuer (Entscheidung, siehe (n4)): die einzige Auspraegung wird in diesem Plan behoben; faende die Wiederholung zur Ausfuehrungszeit eine ZWEITE, ist ein Ledger-Eintrag ueber gsd-tools windows append anzulegen, und das steht dann im SUMMARY.

Befund H — die Reparaturform existiert bereits im Codebestand. user.service.ts:231-252 findAllForPlatformAdmin (260910-das): Mandanten ungebunden lesen (this.prisma.tenant.findMany, "DARF DAS: Tenant traegt keinen Zeilenschutz"), dann je Mandant const tenantPrisma = forTenant(this.prisma, tenant.id) as any; und ein gebundener Lesezugriff mit ausdruecklichem where: { tenantId: tenant.id }. Genau diese Form bekommen findAll, findOne und remove: der Relationszaehler faellt weg, an seine Stelle tritt je Mandant EIN gebundener user.count. Das ausdrueckliche where: { tenantId } ist HEUTE (Rolle mit BYPASSRLS) der einzige wirksame Filter und bleibt. Der Controller greift weiterhin direkt auf Prisma zu — das wird NICHT in den Dienst verschoben (Scope-Disziplin laut Auftrag; user.controller.ts hat denselben Bau mit fuenf forTenant()-Stellen). Mehrkosten: eine gebundene Zaehlabfrage je Mandant statt eines Joins — bei einstelliger Mandantenzahl belanglos, in (n4) festgehalten.

Befund I — es gibt KEINE Testdatei fuer Guard, Middleware oder Controller. Vorlagen: module.guard.spec.ts (makeContext-Attrappe fuer ExecutionContext), dkv.service.spec.ts (vi.mock des Bindungshilfsmittels, __makeBoundClient, Bindungsprotokoll), dashboard.service.spec.ts ab Zeile 545 (Wachhund ueber vi.mocked(forTenant).mock.calls.length). Der ungebundene Nachbau in tenant.controller.spec.ts bekommt KEIN user-Modell — ein versehentlich ungebundener Zaehler scheitert dann mit "Cannot read properties of undefined" (die dkv-Form der Falsifizierung), nicht mit einer falschen Zahl.

Befund J — kein Hintergrunddienst, kein Roh-SQL, keine Transaktion. grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout\|Scheduler" apps/api/src/tenant --include=*.ts und grep -rn '\$transaction(\|\$queryRaw\|\$executeRaw' apps/api/src/tenant --include=*.ts: je null Treffer. TenantService.create ruft groupsService.ensureDefaultGroup(tenant.id) auf — der laeuft seit 260909-jts ueber withTenantTransaction(), gebunden an den soeben angelegten Mandanten; nach dem Scharfschalten funktioniert die Mandantenanlage samt Standardgruppe. Kein sechster Fall der Hintergrunddienst-Falle.

Befund K — die Buchfuehrung nach diesem Plan. Uebersichtszeile tenant: heute 8/0; danach 8/3 (die vier tenant-Zugriffe des Controllers und die vier des Dienstes bleiben ungebunden; drei tenantPrisma.user.count( erfuellen das Muster tenantPrisma\.[a-zA-Z]*\.). Summenzeile: ungebunden 83 unveraendert, gebunden 159 + 3 = 162. Bestandsaufnahme: ein NEUES Paar tenant.controller.ts/user (muss-mandantengebunden, gebunden) — 64 Paare; Klassen-Verteilung muss-mandantengebunden 31 → 32, Ueberschrift 63 → 64. Das Loeschen der Middleware aendert KEIN Paar (sie enthaelt weder this.prisma.<Modell> noch eine const X = forTenant(-Zuweisung). Alle Zahlen sind zur Ausfuehrungszeit aus den Messanweisungen des Dokuments abzuleiten, nicht von hier abzuschreiben.

Befund L — zwei Nebenbefunde, festgehalten statt repariert. TenantService.findAll hat null Aufrufer (grep -rn "tenantService\.findAll" apps/api/src --include=*.ts | grep -v spec: nichts) — bleibt, wird in (n4) genannt (Scope). Der null-Zweig des Guards (Befund D) — bleibt, getestet als heutiges Verhalten.

Befund M — was das Werkzeug bereits hat. runUserAreaChecks legt seit 260910-das eine Wegwerf-Tabelle "Tenant" an (nur id, slug) und misst tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar; runAuthLookupChecks legt "User" an (mit eingeschaltetem und erzwungenem Zeilenschutz und der wortgleichen Regel; OHNE Fremdschluessel auf "Tenant"). Die beiden Abschnitte nach runCalendarAreaChecks (runTransactionShapeMeasurement, runConcurrencyProbe) fassen weder "User" noch "Tenant" an (gemessen: kein Treffer nach Zeile 3189). Der neue Abschnitt darf deshalb "Tenant" um die vier fehlenden Spalten erweitern und den Fremdschluessel nachruesten — alle vorhandenen User-Zeilen referenzieren TENANT-A/TENANT-B, die es gibt. readSchemaModelFieldNames() zaehlt Relationsfelder mit (users, ldapConfig, groups, moduleGrants sind KEINE Spalten) — fuer Tenant ist die Spaltenliste deshalb aus CREATE TABLE "Tenant" in 20260618112124 zu schneiden, nicht aus dem Schema.

</planning_time_findings>

Aufgabe 1: Die Fehlerrichtung fuer diesen Bereich MESSEN und aufschreiben — nichts auf `Tenant` zu binden, aber drei Zaehler laufen in `User` hinein Der lokale Datenbank-Container `tessera-ctl-db-1` laeuft; `docker inspect tessera-ctl-db-1` liefert eine Adresse. Ohne ihn kann das Wegwerf-Werkzeug nichts messen und die Aufgabe ist zu stoppen, nicht zu schaetzen. apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md apps/api/scripts/rls-scratch-check.mjs (Kopf, `forTenantQuery`, `extractPolicySql`, `readRlsWidenMigrationSql`, `runAuthLookupChecks` bis zur Anlage von "User", `runUserAreaChecks` VOLLSTAENDIG (Anlage von "Tenant", Pruefung `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`, Fan-out-Pruefung), `readSchemaModelFieldNames`, `runCalendarAreaChecks` (Pruefung 8 und die Client-Pruefungen), `buildInlineExtendedClient`, `main`), apps/api/prisma/migrations/20260618112124_auth_multi_tenancy/migration.sql (CREATE TABLE "Tenant", Zeile 105 Fremdschluessel), apps/api/prisma/migrations/20260618112133_rls_policies/migration.sql (Regel auf "User"), apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql, apps/api/prisma/schema.prisma (model Tenant, model User), apps/api/src/tenant/tenant.controller.ts, apps/api/src/tenant/tenant.service.ts, apps/api/src/tenant/tenant.guard.ts, apps/api/src/tenant/tenant.middleware.ts, apps/api/src/app.module.ts, apps/api/src/auth/guards/roles.guard.ts, apps/web/src/app/(portal)/admin/tenants/page.tsx, apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx, docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitte `## Bereich user` (u1)-(u4) und `## Bereich calendar` vollstaendig, dazu den Absatz zur offenen Architekturfrage in (e)) TEIL 1 — die Messung. Erweitere `apps/api/scripts/rls-scratch-check.mjs` um einen zwoelften Abschnitt `runTenantAreaChecks(adminUrl, scratchRoleUrl, results)` und rufe ihn in `main()` NACH `runCalendarAreaChecks` und VOR `runTransactionShapeMeasurement` auf. Der Abschnitt setzt auf den bereits vorhandenen Wegwerf-Tabellen `"Tenant"` (aus `runUserAreaChecks`) und `"User"` (aus `runAuthLookupChecks`, mit Zeilenschutz und wortgleicher Regel) auf; keine spaetere Pruefung setzt auf seinen Aenderungen auf (Befund M). Halte diese Reihenfolgebedingung im Kopfkommentar fest, wie `runUserAreaChecks` es vormacht, und nenne dort in EINEM Satz, warum dieser Abschnitt nicht "null Pruefungen" hat, obwohl auf `Tenant` nichts zu binden ist: drei der acht Zugriffsstellen zaehlen ueber eine Relationseinbindung in `User`.

Vorbereitung ueber die Wartungsrolle: (a) ALTER TABLE "Tenant" um die vier fehlenden Spalten name, isActive, createdAt, updatedAt mit Typen und Vorgaben aus dem ausgelieferten CREATE TABLE (fuer name und updatedAt brauchen die vorhandenen Zeilen einen DEFAULT, den die Migration nicht hat — vermerke das im Kommentar als Abweichung, die nur das Nachruesten betrifft); (b) den Fremdschluessel User_tenantId_fkey WORTGLEICH aus Zeile 105 der Migration 20260618112124 schneiden (zur Laufzeit lesen, nicht tippen; bricht mit FEHLGESCHLAGEN ab, wenn die Zeile nicht gefunden wird) und auf die Wegwerf-Tabelle "User" anwenden; (c) eine dritte Mandantenzeile TENANT-C ohne Benutzer einfuegen. Alle Pruefungen laufen unter der Rolle ohne BYPASSRLS; Vergleichswerte kommen ueber die Wartungsrolle.

Mindestens neun namentlich benannte Pruefungen, jede mit einer Belegausgabe, die die beobachteten Werte nennt:

  1. tenant-keine-regel-in-allen-ausgelieferten-migrationen — liest zur Laufzeit JEDE migration.sql unter MIGRATIONS_DIR und prueft, dass keine ein CREATE POLICY auf "Tenant", kein ENABLE/FORCE ROW LEVEL SECURITY auf "Tenant" und kein ALTER TABLE "Tenant" enthaelt. Die Belegausgabe nennt die Zahl der gelesenen Dateien und ausdruecklich, dass 20260910120000_rls_widen_membership_grant_and_platform_read darunter ist und "Tenant" nicht nennt.
  2. tenant-gebunden-und-ungebunden-liefern-dieselben-zeilen — Roh-SQL: forTenantQuery(prisma, 'TENANT-A', SELECT id FROM "Tenant" ORDER BY id), derselbe Lesezugriff ungebunden, und derselbe ueber die Wartungsrolle — drei identische Kennungsmengen (A, B, C). Das ist der Beleg "nichts zu binden".
  3. tenant-wegwerftabelle-deckt-alle-spalten-des-generierten-clients — Spaltenmenge der Wegwerf-Tabelle aus information_schema.columns ist identisch mit den Spaltennamen, die zur Laufzeit aus dem CREATE TABLE "Tenant"-Block der Migration 20260618112124 geschnitten werden (NICHT aus readSchemaModelFieldNames, Befund M). Steht VOR den Client-Pruefungen; faellt sie durch, bricht der Abschnitt ab.
  4. tenant-generierter-client-zeilen-gebunden-und-ungebunden-identisch — buildInlineExtendedClient(prisma, 'TENANT-A').tenant.findMany({ orderBy: { id: 'asc' } }) und prisma.tenant.findMany(...) ungebunden liefern dieselben Kennungen wie Pruefung 2.
  5. tenant-generierter-client-benutzerzaehler-ungebunden-null-fuer-jeden-mandanten — die tragende Belegzeile: die Abfrage, die findAll heute stellt (findMany mit Relationszaehler ueber users), UNGEBUNDEN auf dem generierten Client: jeder Zaehler ist 0, waehrend die Wartungsrolle je Mandant (SELECT "tenantId", count(*) FROM "User" GROUP BY 1) fuer TENANT-A und TENANT-B mehr als 0 zaehlt. Die Belegausgabe nennt beide Zahlen je Mandant und sagt beim Namen: das ist die Zahl, die admin/tenants/page.tsx als Benutzeranzahl anzeigen wuerde.
  6. tenant-generierter-client-benutzerzaehler-gebunden-nur-eigener-mandant — dieselbe Abfrage gebunden unter TENANT-A: Zaehler fuer A gleich der Wartungszahl, fuer B gleich 0 obwohl die Wartungszahl groesser 0 ist, fuer C gleich 0.
  7. tenant-loeschriegel-ungebunden-vakuum-fremdschluessel-faengt-laut — die Abfrage, die remove heute stellt (findUnique fuer TENANT-A mit Zaehler ueber aktive Benutzer), ungebunden: Zaehler 0, obwohl die Wartungsrolle aktive Benutzer zaehlt — der Riegel T-02-09 liesse das Loeschen durch. Danach prisma.tenant.delete({ where: { id: 'TENANT-A' } }) ungebunden ueber den generierten Client: bestanden genau dann, wenn ein Fehler geworfen wird UND die Zeile ueber die Wartungsrolle danach noch existiert; die Belegausgabe nennt KONSTRUKTORNAME und code woertlich (rate das Ergebnis nicht vorweg) und sagt, dass die referentielle Pruefung den Zeilenschutz umgeht — sonst haette sie die unsichtbaren User-Zeilen nicht sehen koennen.
  8. tenant-loeschen-ohne-benutzer-gelingt-wie-heute — dasselbe Loeschen fuer TENANT-C gelingt, die Zeile ist danach ueber die Wartungsrolle weg. Das falsifiziert die Deutung "Loeschen scheitert immer".
  9. tenant-fan-out-je-mandant-gebundene-zaehlung-stimmt — die Form, die Aufgabe 3 einbaut: prisma.tenant.findMany ungebunden als Treiber, dann je verbliebenem Mandanten buildInlineExtendedClient(prisma, t.id).user.count({ where: { tenantId: t.id } }) — jede Zahl gleich der Wartungszahl des Mandanten.

Ergaenzt wird nur, gestrichen wird nicht; nenne im SUMMARY die tatsaechlich gezaehlte Zahl, nicht diese.

TEIL 2 — die Codeaussagen, jede mit ihrer Reichweite. Fuehre die Nachpruefungen aus den Befunden B, C, D, E, F, G und L tatsaechlich aus und notiere jeweils die Anweisung oder Datei-und-Zeile, damit jede Aussage widerlegbar bleibt: (B) alle Property-Zugriffe auf die Anfrageobjekt- Eigenschaft, mit Suchumfang; (C) die Nichtverdrahtung der Middleware — die drei Anweisungen aus Befund C, plus apps/api/src/main.ts lesen; (D) die Leser von req.tenantId je Datei und die vier Header-Sender im Frontend; (E) Klassen- und Handler-Metadaten des Controllers, die Reihenfolge in getAllAndOverride; (F) die drei Zaehlerstellen, die Regel auf User, den Fremdschluessel; (G) BEIDE Listen — alle _count-Stellen und alle include:-Stellen — je Stelle: aeusserer Aufruf gebunden oder nicht, aeussere Tabelle geschuetzt oder nicht, eingebundene Tabelle geschuetzt oder nicht; (L) Aufrufer von TenantService.findAll. Faellt eine Nachpruefung ANDERS aus als in den Planungsbefunden, gilt die Messung; schreibe sie auf und benenne die Abweichung ausdruecklich. Findet (G) eine zweite gefaehrliche Auspraegung, ist das im SUMMARY als eigener Befund zu fuehren und ein Ledger-Eintrag anzulegen (Befund G).

TEIL 3 — die Kritikschrift. Erweitere docs/mandantentrennung-etappe2-fehlerrichtung.md um einen Abschnitt ## Bereich tenant unmittelbar VOR ## Verweis, in der Form der vorhandenen Bereichsabschnitte, mit fuenf Unterabschnitten unter dem Buchstaben n (von Ma-n-dant; t ist durch tenders belegt):

  • ### (n1) Die Messung — die tatsaechlich beobachtete Werkzeugausgabe woertlich eingerueckt, die tragende Belegzeile (Pruefung 5) benannt, der Migrationsscan mit ausdruecklicher Nennung von 20260910120000, und der Satz, warum dieser Bereich trotz "nichts zu binden" Messungen hat. Nenne, welche Pruefungen ueber den generierten Client laufen und warum (Relationszaehler ist eine Client-Form, Roh-SQL sieht ihn nicht — Fehler 7 des Vorhabens).
  • ### (n2) Signaltabelle je Pfad — je Handler des Controllers eine Zeile (findAll, findOne, create, update, remove), je Dienstmethode eine (findAll ohne Aufrufer, findById, create samt gebundener Standardgruppe, update), und eine fuer TenantGuard (kein Datenbankzugriff): Verhalten HEUTE nach dem Scharfschalten ohne diesen Plan, das konkrete Signal, und in eigener Spalte, ob das Frontend es durchlaesst. Die remove-Zeile nennt die gemessene Fehlerklasse aus Pruefung 7.
  • ### (n3) Welcher Code eine falsche Zahl als Wahrheit deutet — hier nicht Leere, sondern eine falsche Zahl: Backend findAll/findOne liefern userCount: 0 ohne jedes Signal; remove laesst den Riegel passieren und der Fremdschluessel antwortet laut mit der falschen Botschaft. Frontend, namentlich mit Stelle: admin/tenants/page.tsx zeigt tenant.userCount an (um Zeile 209), verschluckt nicht-OK-Antworten in fetchTenants (um Zeile 44-55) und in handleDelete (um Zeile 118-133: bei nicht-OK passiert NICHTS, der Bestaetigungsdialog bleibt) — ein 500 aus dem Fremdschluessel sieht fuer den Administrator aus wie ein Knopf, der nicht reagiert; TenantContextSelector.tsx faengt Fehler in eine leere Liste. Zur Ausfuehrungszeit an den Dateien erneut zu pruefen.
  • ### (n4) Was dieser Durchlauf bewusst nicht löst — (a) die ENTSCHEIDUNG zur Anfrageobjekt-Eigenschaft in einem Absatz: was gemessen wurde (kein Leser, Suchumfang; Middleware nie verdrahtet, Anweisung), was entschieden ist (Guard setzt nur die Mandantenkennung; Middleware geloescht; die dienst-interne Bindung ist die Konvention), und der Grund (neun Bereiche Praxis; Kosten ohne Nutzen; tote Verdrahtung, die wie Schutz aussieht) — dieser Absatz ist die Stelle, auf die Aufgabe 2 und 3 verweisen; (b) die Erkennungsluecke der Bestandsaufnahme (Befund G) mit BEIDEN Listen aus TEIL 2 und dem Urteil je Stelle, plus die Entscheidung gegen einen Ledger-Eintrag mit Grund; (c) der Fremdschluessel als Rueckhalt: er faengt auch INAKTIVE Benutzer, waehrend der Riegel nur aktive zaehlt — ein Mandant mit ausschliesslich inaktiven Benutzern ist heute wie nach dem Plan nicht loeschbar (500 statt 400), bestehendes Verhalten, nicht dieser Auftrag; (d) TenantService.findAll ohne Aufrufer; (e) der unerreichbare null-Zweig des Guards; (f) der Header-Wert wird nicht gegen vorhandene Mandanten geprueft (D-10 wie entworfen; ein erfundener Wert bindet an einen leeren Kontext — null Zeilen, kein Leck); (g) die Mehrkosten des Fan-outs; (h) die Etappe-4-Vorabpruefung: die Benutzerzahl je Mandant ueber die Wartungsrolle gegen die gebundene Fan-out-Zaehlung — dieselbe Pruefung wie im Bereich user, deshalb kein eigener Eintrag.
  • ### (n5) Was dieser Durchlauf bewusst nicht anfasst — der direkte Prisma-Zugriff im Controller (Muster wie user.controller.ts, bleibt, Wartbarkeitsvermerk, keine Verschiebung in den Dienst); die redundante @UseGuards(RolesGuard)-Registrierung; das Frontend (nur beschrieben); tenant.service.ts (unveraendert); die veraltete Tabellenliste im Abschnitt ## Mandantentrennung von docs/anleitung-entwicklung.md ("aktuell nur auf User, PasswordResetToken, ..." — seit 20260909140000 sind es 23 Tabellen; Aufgabe 3 aendert in jener Datei NUR die Absaetze zu Guard und Middleware, die Liste bleibt und wird hier als Ungenauigkeit festgehalten); die historische Nennung der Middleware in docs/mandantentrennung-datenbankrolle.md:124 (beschreibt den Stand vor Etappe 1 korrekt); Schema und Migrationen.

Aendere in dieser Aufgabe KEINE Datei unter apps/api/src, KEINE unter apps/api/prisma und KEINE unter apps/web. DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in tenant-keine-regel-in-allen-ausgelieferten-migrationen tenant-gebunden-und-ungebunden-liefern-dieselben-zeilen tenant-wegwerftabelle-deckt-alle-spalten-des-generierten-clients tenant-generierter-client-zeilen-gebunden-und-ungebunden-identisch tenant-generierter-client-benutzerzaehler-ungebunden-null-fuer-jeden-mandanten tenant-generierter-client-benutzerzaehler-gebunden-nur-eigener-mandant tenant-loeschriegel-ungebunden-vakuum-fremdschluessel-faengt-laut tenant-loeschen-ohne-benutzer-gelingt-wie-heute tenant-fan-out-je-mandant-gebundene-zaehlung-stimmt; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && N=$(echo "$OUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden.$/\1/p') && { test "$N" -ge 110 || { echo "PRUEFUNGSZAHL: $N, erwartet mindestens 110 (101 bisherige plus mindestens 9 neue)"; exit 1; }; } && grep -q 'runTenantAreaChecks' apps/api/scripts/rls-scratch-check.mjs && awk '/await runCalendarAreaChecks(/{c=NR} /await runTenantAreaChecks(/{n=NR} /await runTransactionShapeMeasurement(/{t=NR} END{ if(!(c&&n&&t&&c<n&&n<t)){print "REIHENFOLGE in main(): runTenantAreaChecks muss nach runCalendarAreaChecks und vor runTransactionShapeMeasurement stehen"; exit 1} }' apps/api/scripts/rls-scratch-check.mjs && grep -q 'User_tenantId_fkey' apps/api/scripts/rls-scratch-check.mjs && grep -q '^## Bereich tenant$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in n1 n2 n3 n4 n5; do grep -qE "^### ($S) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && awk '/^## Bereich tenant$/{f=1; next} /^## /{f=0} f && /20260910120000/{r=1} f && /admin/tenants/page.tsx/{p=1} f && /TenantContextSelector/{s=1} f && /_count/{c=1} f && /User_tenantId_fkey/{k=1} f && /include:/{i=1} f && /findAllForPlatformAdmin/{h=1} f && /MiddlewareConsumer|configure(/{m=1} END{ if(!r){print "(n1): der Migrationsstand einschliesslich 20260910120000 ist nicht benannt"; exit 1} if(!p||!s){print "(n3): die beiden Frontend-Dateien sind nicht namentlich genannt"; exit 1} if(!c){print "(n1)/(n2): der Relationszaehler ist nicht benannt"; exit 1} if(!k){print "(n2)/(n4): der Fremdschluessel als Rueckhalt ist nicht benannt"; exit 1} if(!i){print "(n4): die Erkennungsluecke fuer Relationseinbindungen ist nicht mit ihrer Liste benannt"; exit 1} if(!h){print "(n4): das Fan-out-Muster aus dem Bereich user ist nicht benannt"; exit 1} if(!m){print "(n4)(a): die Anweisung, mit der die Nichtverdrahtung der Middleware gemessen wurde, fehlt"; exit 1} }' docs/mandantentrennung-etappe2-fehlerrichtung.md && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ Tests +([0-9]+) passed./\1/p' | head -1) && { test -n "$T" && test "$T" -ge 883 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mindestens 883"; exit 1; }; } && npm --prefix apps/api run type-check && git rev-parse --verify f1017fa >/dev/null && PRISMA_CHANGED=$(git diff --name-only f1017fa -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only f1017fa) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check.mjs|docs/mandantentrennung-etappe2-fehlerrichtung.md|.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; } apps/api/scripts/rls-scratch-check.mjs hat einen zwoelften Abschnitt runTenantAreaChecks in der richtigen Reihenfolge mit mindestens neun neuen, namentlich benannten Pruefungen, davon fuenf ueber den generierten Client an einer Wegwerf-Tabelle, deren Spaltenmenge zur Laufzeit gegen das ausgelieferte CREATE TABLE geprueft wird, mit dem wortgleich aus der Migration geschnittenen Fremdschluessel; der Migrationsscan laeuft zur Laufzeit; alle Pruefungen des Werkzeugs bestehen (mindestens 110). docs/mandantentrennung-etappe2-fehlerrichtung.md hat einen Abschnitt ## Bereich tenant mit (n1) bis (n5), die tatsaechlich beobachtete Werkzeugausgabe woertlich, die Entscheidung zur Anfrageobjekt-Eigenschaft mit Messung und Grund, beide Listen der Erkennungsluecke mit Urteil je Stelle, die Frontend-Stellen namentlich. Baseline gehalten: mindestens 883 Tests gruen, Typpruefung sauber. Unter apps/api/src, apps/api/prisma, apps/web und den Compose-/Umgebungsdateien ist nichts geaendert.

Aufgabe 2: Die Entscheidung umsetzen — Guard setzt nur noch die Mandantenkennung, die nie verdrahtete Middleware geht, die Testlage fuer den Guard entsteht, drei Kommentare hoeren auf zu luegen apps/api/src/tenant/tenant.guard.ts, apps/api/src/tenant/tenant.guard.spec.ts, apps/api/src/tenant/tenant.middleware.ts, apps/api/src/prisma/rls-access-inventory.spec.ts, apps/api/src/app.module.ts, apps/api/src/module-registry/module.guard.ts, apps/api/src/dkv/dkv.controller.ts apps/api/src/tenant/tenant.guard.ts, apps/api/src/tenant/tenant.middleware.ts, apps/api/src/app.module.ts, apps/api/src/module-registry/module.guard.spec.ts (`makeContext`), apps/api/src/module-registry/module.guard.ts (Zeile 25), apps/api/src/dkv/dkv.controller.ts (Zeile 33), apps/api/src/prisma/rls-access-inventory.spec.ts (Kopf, `FORTENANT_ASSIGNMENT_EXCEPTIONS`, die Pruefung "jedes forTenant(-Vorkommen entspricht der erkannten Zuweisungsform"), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitt (n4)(a) aus Aufgabe 1), apps/api/src/auth/strategies/jwt.strategy.ts (`validate`) ZUERST die Testlage, DANN der Umbau. Lege `apps/api/src/tenant/tenant.guard.spec.ts` NEU an, in der Form von `module.guard.spec.ts` (`makeContext(request)` mit `switchToHttp().getRequest()`); der Guard wird OHNE Argumente konstruiert — das ist zugleich die Typpruefungs-Aussage, dass er keine Prisma-Abhaengigkeit mehr hat. Jeder Fall eigenstaendig, jeder mit sprechendem Namen:
  • Kein req.user (oeffentliche Route): canActivate liefert true, und weder tenantId noch die alte Anfrageobjekt-Eigenschaft sind als Schluessel auf dem Anfrageobjekt vorhanden ('tenantId' in req ist falsch).
  • Nutzer der Rolle USER mit tenantId = 't1', keine Kopfzeile: true, req.tenantId === 't1'.
  • Nutzer der Rolle ADMIN mit tenantId = 't1' UND Kopfzeile x-tenant-id: 't2': die Kopfzeile wird IGNORIERT, req.tenantId === 't1' — die T-04-03-Zusicherung, erstmals festgenagelt.
  • SUPER_ADMIN mit tenantId = 't1' und Kopfzeile 't2': req.tenantId === 't2' (D-10, vom Marktplatz-Frontend benutzt).
  • SUPER_ADMIN mit tenantId = 't1' ohne Kopfzeile: req.tenantId === 't1'.
  • SUPER_ADMIN ohne tenantId und ohne Kopfzeile: true, req.tenantId === null (heutiges Verhalten, Befund D/L — mit dem heutigen Sitzungsnachweis unerreichbar, trotzdem festgenagelt, damit eine Aenderung sichtbar wird).
  • Nutzer der Rolle USER ohne tenantId: wirft ForbiddenException('No tenant context').
  • In JEDEM durchlassenden Fall: der Schluessel der alten Anfrageobjekt-Eigenschaft ist auf dem Anfrageobjekt NICHT vorhanden (pruefe ueber den in-Operator mit dem Namen als Zeichenkette, nicht ueber einen Property-Zugriff mit Punkt — das Gate dieser Aufgabe zaehlt den Punkt-Zugriff im gesamten apps/api/src auf null, Kommentare eingeschlossen).

Die Zahl der Faelle wird am Ende ABGEZAEHLT und im SUMMARY mit der gezaehlten Zahl genannt. Bevor du aenderst: wiederhole die Messung aus Befund C (drei Anweisungen, dazu apps/api/src/main.ts lesen). Ist die Middleware nirgends verdrahtet, LOESCHE apps/api/src/tenant/tenant.middleware.ts (git rm). Waere sie verdrahtet — dann steht das im SUMMARY als Abweichung, die Datei bleibt und verliert wie der Guard nur die Anfrageobjekt-Eigenschaft und die Prisma-Abhaengigkeit; der Rest dieser Aufgabe gilt dann sinngemaess fuer beide Dateien.

Baue apps/api/src/tenant/tenant.guard.ts um: entferne die beiden Zuweisungen der Anfrageobjekt-Eigenschaft (Zeile 41 und 44), den Import des Bindungshilfsmittels, den Import und die Konstruktor-Injektion von PrismaService (der Guard hat danach KEINEN Konstruktor mit Parametern). Die Zuweisungen von req.tenantId in BEIDEN Zweigen, der SUPER_ADMIN-Wechsel per x-tenant-id, die ForbiddenException('No tenant context') und der Durchlass ohne Nutzer bleiben WORTGLEICH. Schreibe den Kopfkommentar neu: er nennt, dass der Guard ausschliesslich req.tenantId setzt, dass die Bindung an den Mandanten dienst-intern je Methode ueber das Bindungshilfsmittel aus prisma-tenant.extension.ts geschieht (Konvention aller umgestellten Bereiche der Etappe 2), dass ein frueherer Entwurf einen gebundenen Klienten auf dem Anfrageobjekt veroeffentlichte, den nie jemand las (entfernt mit 260911-e2s, Messung und Grund in docs/mandantentrennung-etappe2-fehlerrichtung.md, (n4)(a)), und dass die Reihenfolge nach JwtAuthGuard tragend bleibt. Der Kommentar darf die alte Eigenschaft NICHT mit Punkt-Zugriff benennen (siehe Behavior, letzter Punkt) und den Namen des Bindungshilfsmittels nicht in Codezeilen fuehren — im Kommentar ist er erlaubt, das Gate filtert Kommentarzeilen.

Leere in apps/api/src/prisma/rls-access-inventory.spec.ts die Ausnahmeliste FORTENANT_ASSIGNMENT_EXCEPTIONS (new Set<string>([])) und schreibe ihren Kommentar fort: was die Ausnahme war (zwei Dateien veroeffentlichten den gebundenen Klienten auf dem Anfrageobjekt statt ihn einer lokalen Konstante zuzuweisen), dass die Architekturfrage mit 260911-e2s ENTSCHIEDEN ist (dienst-interne Bindung, Eigenschaft entfernt, Middleware geloescht) und dass die Liste deshalb leer startet und leer bleibt, bis ein begruendeter neuer Ausnahmefall auftritt — die Form, die INTERACTIVE_TRANSACTION_EXCEPTIONS bereits hat. Ohne Punkt-Zugriff auf den alten Namen. Ergaenze EINEN neuen Testfall: jede in FORTENANT_ASSIGNMENT_EXCEPTIONS gefuehrte Datei muss existieren UND tatsaechlich mindestens einen Aufruf ausserhalb der Zuweisungsform haben — eine Ausnahmeliste, die Dateien nennt, die es nicht gibt oder die keinen Ausnahmefall mehr enthalten, ist dieselbe tote Verdrahtung, die diese Aufgabe im Guard entfernt.

Berichtige in drei Fremddateien je EINE Kommentarzeile, sonst nichts: apps/api/src/app.module.ts:57 (nennt heute beide Eigenschaften — nur noch req.tenantId), apps/api/src/module-registry/module.guard.ts:25 und apps/api/src/dkv/dkv.controller.ts:33 (nennen die nie verdrahtete Middleware — es ist der Guard). Keine Codezeile in diesen drei Dateien darf sich aendern; das Gate prueft die Diffs auf Kommentarzeilen.

Falsifizierungsnachweis am Ende dieser Aufgabe: setze im Guard probeweise die alte Anfrageobjekt-Eigenschaft in EINEM Zweig wieder (irgendein Wert genuegt), ueberzeuge dich, dass tenant.guard.spec.ts rot wird, notiere Testname und Fehlermeldung woertlich, und stelle den Zustand wieder her.

Aendere keine Datei ausserhalb der sieben genannten. test ! -e apps/api/src/tenant/tenant.middleware.ts && test -f apps/api/src/tenant/tenant.guard.spec.ts && npm --prefix apps/api run test -- src/tenant/tenant.guard.spec.ts && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ Tests +([0-9]+) passed./\1/p' | head -1) && { test -n "$T" && test "$T" -gt 883 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 883 (neue Guard-Faelle)"; exit 1; }; } && npm --prefix apps/api run type-check && test 0 -eq "$(grep -rn '.tenantPrisma' apps/api/src --include=.ts | wc -l | tr -d ' ')" && test 0 -eq "$(grep -rn 'TenantMiddleware' apps/api/src --include=.ts | wc -l | tr -d ' ')" && test 2 -eq "$(grep -c 'req.tenantId = ' apps/api/src/tenant/tenant.guard.ts)" && test 0 -eq "$(grep -vE '^\s*(//|*|/*)' apps/api/src/tenant/tenant.guard.ts | grep -c 'forTenant')" && test 0 -eq "$(grep -vE '^\s*(//|*|/*)' apps/api/src/tenant/tenant.guard.ts | grep -c 'PrismaService')" && grep -q 'x-tenant-id' apps/api/src/tenant/tenant.guard.ts && grep -q 'No tenant context' apps/api/src/tenant/tenant.guard.ts && grep -q '260911-e2s' apps/api/src/tenant/tenant.guard.ts && test 0 -eq "$(awk '/const FORTENANT_ASSIGNMENT_EXCEPTIONS/,/])/' apps/api/src/prisma/rls-access-inventory.spec.ts | grep -c 'apps/api/src')" && grep -q '260911-e2s' apps/api/src/prisma/rls-access-inventory.spec.ts && grep -q "FORTENANT_ASSIGNMENT_EXCEPTIONS]" apps/api/src/prisma/rls-access-inventory.spec.ts && grep -q "tenantPrisma' in" apps/api/src/tenant/tenant.guard.spec.ts && grep -q 'No tenant context' apps/api/src/tenant/tenant.guard.spec.ts && grep -q 'x-tenant-id' apps/api/src/tenant/tenant.guard.spec.ts && grep -q 'new TenantGuard()' apps/api/src/tenant/tenant.guard.spec.ts && awk '/useClass: JwtAuthGuard/{j=NR} /useClass: TenantGuard/{t=NR} /useClass: RolesGuard/{r=NR} END{ if(!(j&&t&&r&&j<t&&t<r)){print "app.module.ts: Reihenfolge JwtAuthGuard -> TenantGuard -> RolesGuard verletzt"; exit 1} }' apps/api/src/app.module.ts && git rev-parse --verify f1017fa >/dev/null && for F in apps/api/src/app.module.ts apps/api/src/module-registry/module.guard.ts apps/api/src/dkv/dkv.controller.ts; do FDIFF=$(git diff f1017fa -- "$F") || { echo "git diff fuer $F fehlgeschlagen"; exit 1; }; NONCOMMENT=$(printf '%s\n' "$FDIFF" | grep -E '^[+-]' | grep -vE '^(+++|---)' | grep -vE '^[+-]\s*(//|*|/*)' || true); test -z "$NONCOMMENT" || { printf 'NICHT-KOMMENTAR-AENDERUNG in %s (nur Kommentarzeilen erlaubt):\n%s\n' "$F" "$NONCOMMENT"; exit 1; }; done && PRISMA_CHANGED=$(git diff --name-only f1017fa -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only f1017fa) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check.mjs|docs/mandantentrennung-etappe2-fehlerrichtung.md|apps/api/src/tenant/tenant.guard.ts|apps/api/src/tenant/tenant.guard.spec.ts|apps/api/src/tenant/tenant.middleware.ts|apps/api/src/prisma/rls-access-inventory.spec.ts|apps/api/src/app.module.ts|apps/api/src/module-registry/module.guard.ts|apps/api/src/dkv/dkv.controller.ts|.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; } tenant.middleware.ts existiert nicht mehr (Nichtverdrahtung zur Ausfuehrungszeit erneut gemessen). tenant.guard.ts setzt in beiden Zweigen nur noch req.tenantId, hat keine Prisma-Abhaengigkeit und keinen Aufruf des Bindungshilfsmittels mehr, der Kopfkommentar nennt Entscheidung und Grund; x-tenant-id-Wechsel und ForbiddenException bleiben wortgleich. tenant.guard.spec.ts existiert neu und deckt jeden in <behavior> genannten Fall ab, einschliesslich der Abwesenheit der alten Eigenschaft in jedem Durchlass-Fall. Im gesamten apps/api/src gibt es keinen Punkt-Zugriff auf die alte Eigenschaft und keine Nennung der Middleware mehr, auch nicht in Kommentaren. Die Ausnahmeliste in rls-access-inventory.spec.ts ist leer, ihr Kommentar nennt die Entscheidung, ein neuer Testfall verhindert veraltete Eintraege. Die drei Fremddateien sind ausschliesslich in Kommentarzeilen geaendert. Der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und mit Testname und Fehlermeldung notiert. Baseline gehalten, Testzahl gestiegen.

Aufgabe 3: Die drei Zaehler binden (Fan-out je Mandant), die Testlage fuer den Controller anlegen, die SUPER_ADMIN-Beschraenkung festnageln und alle Dokumentstellen nachziehen apps/api/src/tenant/tenant.controller.ts, apps/api/src/tenant/tenant.controller.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md, docs/anleitung-entwicklung.md apps/api/src/tenant/tenant.controller.ts, apps/api/src/user/user.service.ts (`findAllForPlatformAdmin`, `findByIdForPlatformAdmin` samt Kopfkommentaren), apps/api/src/dkv/dkv.service.spec.ts (Kopf, `vi.mock`, `makeFakePrisma` mit `__makeBoundClient`, `expectBoundCall`), apps/api/src/dashboard/dashboard.service.spec.ts (Zeilen 545-570, Wachhund), apps/api/src/auth/decorators/roles.decorator.ts, apps/api/src/auth/guards/roles.guard.ts, apps/api/src/prisma/rls-access-inventory.spec.ts (`analyzeFile`, `parseDocEntries`), docs/mandantentrennung-zugriffsklassifikation.md (VOLLSTAENDIG: `Zwei belegte Befunde`, Uebersichtstabelle samt Messanweisung, Klassen-Verteilung, Hintergrunddienst-Abschnitt, Kopf und beide `tenant`-Zeilen der Bestandsaufnahme, `Was diese Etappe NICHT entscheidet`), docs/anleitung-entwicklung.md (Abschnitt `## Mandantentrennung`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitt `## Bereich tenant` aus Aufgabe 1) ZUERST die Testlage, DANN der Umbau. Lege `apps/api/src/tenant/tenant.controller.spec.ts` NEU an, in der Form von `dkv.service.spec.ts`: `vi.mock` auf das Bindungshilfsmittel, umgeleitet auf `prisma.__makeBoundClient(tenantId)`; ein handgerollter Prisma-Nachbau mit In-Memory-Zeilen fuer `tenant` (`findMany`, `findUnique`, `delete`) und `user` (Zeilen mit `tenantId` und `isActive`). Der UNGEBUNDENE Nachbau hat KEIN `user`-Modell — ein ungebundener Zaehler scheitert mit "Cannot read properties of undefined"; der gebundene Klient (`__makeBoundClient(tenantId)`) bietet `user.count({ where })` an, zaehlt nur Zeilen SEINES Mandanten (zusaetzlich gefiltert nach `where.tenantId` und `where.isActive`, falls gesetzt) und schreibt je Aufruf Mandantenkennung, Modell, Methode und `where` in ein Bindungsprotokoll. `TenantService` ist eine Attrappe (`create`, `findById`, `update` als `vi.fn`).

Jeder Fall eigenstaendig, jeder mit sprechendem Namen:

  • findAll mit drei Mandanten (A: zwei Benutzer, B: ein Benutzer, C: keiner): liefert drei Eintraege mit userCount 2/1/0 in der Reihenfolge des Nachbaus; das Protokoll enthaelt genau DREI gebundene Zaehlaufrufe, je einen unter der Kennung des jeweiligen Mandanten, jeder mit where.tenantId gleich dieser Kennung; die Mandantenzeilen selbst kamen vom ungebundenen Nachbau (tenant.findMany aufgerufen).
  • findAll ohne Mandanten: leere Liste, KEIN gebundener Klient erzeugt.
  • findAll: die Antwort traegt genau die Felder id, name, slug, isActive, createdAt, userCount (die heutige Form bleibt).
  • findOne mit unbekannter Kennung: NotFoundException('Tenant not found'), KEIN gebundener Klient.
  • findOne mit bekannter Kennung: userCount aus dem gebundenen Klienten UNTER DIESER Kennung, where.tenantId gleich der Kennung.
  • remove mit unbekannter Kennung: NotFoundException, kein gebundener Klient, tenant.delete nicht aufgerufen.
  • remove mit aktiven Benutzern: BadRequestException mit der heutigen Meldung ("Cannot delete tenant with active users..."), tenant.delete NICHT aufgerufen; der gebundene Zaehlaufruf traegt where.isActive === true und where.tenantId gleich der Kennung.
  • remove mit ausschliesslich INAKTIVEN Benutzern: der Riegel laesst durch (Zaehler 0), tenant.delete wird ungebunden mit { where: { id } } aufgerufen, Antwort { message: 'Tenant deleted' } — festgenagelt als heutiges Verhalten; der Fremdschluessel, der das in der echten Datenbank abfaengt, existiert im Nachbau nicht und wird hier nicht simuliert (das misst Pruefung 7 in Aufgabe 1).
  • remove ohne Benutzer: geloescht, Antwort wie oben.
  • create und update: delegieren an die Dienst-Attrappe; weder ein gebundener Klient noch ein Aufruf des ungebundenen Nachbaus.
  • Rollen-Metadaten (Befund E): Reflect.getMetadata(ROLES_KEY, TenantController) ist genau [Role.SUPER_ADMIN], und fuer JEDEN der fuenf Handler (findAll, findOne, create, update, remove) ist Reflect.getMetadata(ROLES_KEY, TenantController.prototype.<handler>) undefiniert — ein Handler mit eigener, schwaecherer Rolle wuerde die Klassenrolle ueberschreiben (getAllAndOverride), dieser Fall wird dann rot. Importiere reflect-metadata am Kopf der Testdatei.
  • Wachhund: findOne und remove erzeugen je genau EINEN gebundenen Klienten je Aufruf; findAll genau so viele wie Mandanten (Muster dashboard.service.spec.ts Zeile 545, hier mit der erwarteten Zahl je Handler).

Die Zahl der Faelle wird am Ende ABGEZAEHLT und im SUMMARY mit der gezaehlten Zahl genannt. TEIL 1 — der Controller. Stelle in apps/api/src/tenant/tenant.controller.ts die drei Handler findAll, findOne und remove auf das Fan-out-Muster von UserService.findAllForPlatformAdmin um: der Relationszaehler faellt weg (kein include mehr auf den tenant-Abfragen); die tenant-Abfragen selbst bleiben UNGEBUNDEN und wortgleich in ihrer Form (findMany mit orderBy: { name: 'asc' }, findUnique ueber die Kennung, delete ueber die Kennung) — Tenant traegt keine Regel, gemessen in Aufgabe 1. Je Handler tritt an die Stelle des Zaehlers EIN gebundener Aufruf in der Zuweisungsform, die rls-access-inventory.spec.ts erkennt und die user.service.ts vormacht: Konstante tenantPrisma aus dem Bindungshilfsmittel mit this.prisma und der Mandantenkennung, as any, dann user.count mit ausdruecklichem where: { tenantId } — in remove zusaetzlich isActive: true. In findAll steht die Zuweisung INNERHALB der Schleife ueber die Mandanten (eine Aufrufstelle, ein Klient je Mandant, sequentiell wie die Vorlage); findOne und remove binden nach dem 404-Riegel an die Kennung aus dem Pfad — die Kennung ist hier KEINE neue Vertrauensquelle, denn sie bezeichnet den zu verwaltenden Mandanten, nicht den des Anfragenden, und der Anfragende ist per Klassenrolle SUPER_ADMIN. Antwortform, Meldungen und Statuscodes bleiben unveraendert. Importiere das Bindungshilfsmittel aus '../prisma/prisma-tenant.extension'.

Schreibe den Kopfkommentar der Klasse fort: warum die Zaehler gebunden werden (Relationszaehler lief unter der Regel von User; nach dem Scharfschalten null fuer jeden Mandanten, Riegel vakuum — 260911-e2s, Aufgabe 1, Pruefungen 5-7), warum die tenant-Zugriffe ungebunden bleiben (keine Regel, Pruefung 1/2), warum das ausdrueckliche where: { tenantId } heute der einzige Filter ist (Rolle mit BYPASSRLS, WINDOWS #18), und dass der direkte Prisma-Zugriff im Controller bewusst beibehalten wurde (Muster user.controller.ts, Vermerk in (n5)). Kommentare in dieser Datei duerfen die Zeichenfolge, mit der die Uebersichtstabelle ungebundene Zugriffe zaehlt (siehe deren Messanweisung), NICHT ausserhalb der vier echten tenant-Zugriffe enthalten — das Gate vergleicht die Rohzaehlung mit und ohne Kommentare; und sie duerfen einen ungebundenen Zugriff auf user nirgends woertlich nennen.

Falsifizierungsnachweis nach dem Umbau: ersetze in findOne den gebundenen Zaehler probeweise durch einen Zaehler auf dem ungebundenen Basisclient, ueberzeuge dich, dass tenant.controller.spec.ts rot wird (erwartete Form: der Nachbau hat kein ungebundenes user-Modell), notiere Testname und Fehlermeldung woertlich, und stelle den Zustand wieder her.

TEIL 2 — die Klassifikation. Ziehe docs/mandantentrennung-zugriffsklassifikation.md an ALLEN handgepflegten Stellen nach, jede einzeln nachgesehen, keine ueberflogen (Fehler 4 des Vorhabens):

  1. Abschnitt ## Zwei belegte Befunde, erster Befund: fortschreiben, nicht loeschen — der Befund aus Etappe 1 bleibt lesbar, darunter ein Absatz **Entschieden (260911-e2s, Aufgabe 2):** mit Messung (kein Leser, Suchumfang; keine Tests fuer beide Dateien — der Satz "und ihrer Tests" im Befund oben war unbelegt; Middleware nie verdrahtet, Anweisung), Entscheidung (Eigenschaft entfernt, Middleware geloescht, Guard ohne Prisma, dienst-interne Bindung ist die Konvention) und Grund, mit Verweis auf (n4)(a). Der Abschnittstitel bleibt.
  2. Kopf des Abschnitts ## Bestandsaufnahme (der Absatz vor der Tabelle): ein Satz zur Erkennungsluecke — die Bestandsaufnahme sieht (Datei, Modell)-Paare, aber keine Relationseinbindung (include, _count) in eine zweite Tabelle; vermessen in 260911-e2s (Aufgabe 1, (n4)(b)), einzige gefaehrliche Auspraegung war tenant.controller.ts, in Aufgabe 3 behoben.
  3. Bestandsaufnahme-Zeilen: die beiden tenant-Zeilen behalten Klasse und Stand (keine-mandantengebundene-tabelle, ungebunden), ihre Begruendung wird fortgeschrieben (Messung 260911-e2s: keine Regel in allen Migrationen, gebunden gleich ungebunden; beim Controller zusaetzlich: die frueheren Relationszaehler sind durch gebundene Zaehler ersetzt, siehe neue Zeile). EINE NEUE Zeile | apps/api/src/tenant/tenant.controller.ts | user | muss-mandantengebunden | gebunden | ... | in alphabetischer Ordnung der Tabelle, Begruendung: Benutzerzaehler je Mandant fuer die Plattform-Administratorsicht, Fan-out-Muster aus user.service.ts, where: { tenantId } bleibt, drei Aufrufstellen. Ohne diese Zeile ist rls-access-inventory.spec.ts am Ende dieser Aufgabe rot (Lehre aus 260910-exd).
  4. Uebersichtszeile tenant mit den NEU GEMESSENEN Zahlen aus der im Dokument genannten Messanweisung (beide Spalten), im etablierten Stil mit Vermerk des vorherigen Standes (**war 8/0**), der drei gebundenen Zaehler und der Begruendung, warum acht Treffer ungebunden BLEIBEN (Mandantentabelle ohne Regel — hier ist Ungebundenheit richtig, nicht geduldet).
  5. Summenzeile derselben Tabelle mit fortgeschriebener Herkunftsspur.
  6. Klassen-Verteilung samt Zahl in der Ueberschrift: ein neues Paar, muss-mandantengebunden steigt um eins; ein **Stand 260911-e2s**-Absatz sagt, welches Paar hinzukam und dass keine Klasse sich verschob.
  7. Hintergrunddienst-Abschnitt: ein PLAIN-Absatz (kein Aufzaehlungspunkt, keine Zeile der Form "Der ... Fall, anderer Bauart"), der festhaelt, dass dieser Bereich keinen sechsten Fall hinzufuegt, mit der Anweisung aus Befund J, und dass TenantService.create die Standardgruppe ueber den bereits gebundenen Weg des Bereichs groups anlegt.
  8. Abschnitt Was diese Etappe NICHT entscheidet, erster Punkt: in der Form des aufgeloesten #19-Punktes durchstreichen und Aufgelöst (260911-e2s) anfuegen — die Frage ist fuer ALLE Bereiche entschieden, nicht nur fuer diesen: dienst-intern, ein Klient je Methode, die Anfrageobjekt-Eigenschaft existiert nicht mehr.

TEIL 3 — die Entwicklungsanleitung. Aendere in docs/anleitung-entwicklung.md im Abschnitt ## Mandantentrennung NUR den ersten Absatz (Beschreibung des Guards: setzt req.tenantId; die Bindung geschieht je Dienstmethode ueber das Bindungshilfsmittel; kein Klient auf dem Anfrageobjekt) und ersetze den Hinweiskasten ueber die Middleware durch EINEN Satz: der nie verdrahtete Zwilling des Guards wurde mit 260911-e2s entfernt. Der Klassenname der Middleware darf in der Datei danach nicht mehr vorkommen, die alte Eigenschaft nicht mehr mit Punkt-Zugriff. Die veraltete Tabellenliste im Absatz danach bleibt UNVERAENDERT (steht in (n5) als Ungenauigkeit).

TEIL 4 — zwei Falsifizierungsnachweise fuer die Dokument-Gates, jeder zurueckgenommen und mit Meldung woertlich notiert: (a) setze die neue Bestandsaufnahme-Zeile probeweise auf ungebunden — die maschinelle Bestandspruefung muss rot werden; (b) setze die Uebersichtszeile probeweise auf eine falsche Zahl — das herleitende Gate dieser Aufgabe muss fehlschlagen.

Aendere keine Datei ausserhalb der vier genannten. DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && test -f apps/api/src/tenant/tenant.controller.spec.ts && npm --prefix apps/api run test -- src/tenant/tenant.controller.spec.ts && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ Tests +([0-9]+) passed./\1/p' | head -1) && { test -n "$T" && test "$T" -gt 883 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 883"; exit 1; }; } && npm --prefix apps/api run type-check && grep -q '__makeBoundClient' apps/api/src/tenant/tenant.controller.spec.ts && grep -q "vi.mock('../prisma/prisma-tenant.extension'" apps/api/src/tenant/tenant.controller.spec.ts && grep -q 'ROLES_KEY' apps/api/src/tenant/tenant.controller.spec.ts && grep -q 'reflect-metadata' apps/api/src/tenant/tenant.controller.spec.ts && grep -q 'isActive' apps/api/src/tenant/tenant.controller.spec.ts && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/tenant/tenant.controller.ts && SRC=$(grep -vE '^\s*(//|*|/*)' apps/api/src/tenant/tenant.controller.ts) && test 0 -eq "$(printf '%s\n' "$SRC" | grep -c '_count')" && test 0 -eq "$(printf '%s\n' "$SRC" | grep -c 'include')" && test 0 -eq "$(grep -c 'this.prisma.user' apps/api/src/tenant/tenant.controller.ts)" && B=$(printf '%s\n' "$SRC" | grep -o 'tenantPrisma.user.count(' | wc -l | tr -d ' ') && { test "$B" -eq 3 || { echo "BINDUNG: $B gebundene Zaehler in tenant.controller.ts, erwartet genau 3 (findAll, findOne, remove)"; exit 1; }; } && C=$(printf '%s\n' "$SRC" | grep -o 'forTenant(this.prisma' | wc -l | tr -d ' ') && { test "$C" -eq 3 || { echo "KLIENTEN: $C Aufrufstellen des Bindungshilfsmittels in tenant.controller.ts, erwartet genau 3"; exit 1; }; } && U=$(printf '%s\n' "$SRC" | grep -o 'this.prisma.tenant.' | wc -l | tr -d ' ') && { test "$U" -eq 4 || { echo "TENANT-ZUGRIFFE: $U ungebundene tenant-Zugriffe in tenant.controller.ts, erwartet genau 4 (bleiben ungebunden, Tenant ohne Regel)"; exit 1; }; } && URAW=$(grep -o 'this.prisma.[a-zA-Z]' apps/api/src/tenant/tenant.controller.ts | wc -l | tr -d ' ') && { test "$URAW" -eq "$U" || { echo "KOMMENTARE in tenant.controller.ts nennen die ungebundene Zugriffsform woertlich ($URAW mit, $U ohne Kommentare)"; exit 1; }; } && grep -q 'isActive: true' apps/api/src/tenant/tenant.controller.ts && grep -q '260911-e2s' apps/api/src/tenant/tenant.controller.ts && grep -q 'Role.SUPER_ADMIN' apps/api/src/tenant/tenant.controller.ts && test 0 -eq "$(grep -rn '.tenantPrisma' apps/api/src --include=.ts | wc -l | tr -d ' ')" && DU=$(grep -ro "this.prisma.[a-zA-Z]" apps/api/src/tenant | grep -v spec | wc -l | tr -d ' ') && DB=$(grep -ro "tenantPrisma.[a-zA-Z]." apps/api/src/tenant | grep -v spec | wc -l | tr -d ' ') && { test "$DU" -eq 8 || { echo "UEBERSICHTSZEILE: ungebundene Rohtreffer in apps/api/src/tenant sind $DU, erwartet 8"; exit 1; }; } && { test "$DB" -eq 3 || { echo "UEBERSICHTSZEILE: gebundene Rohtreffer in apps/api/src/tenant sind $DB, erwartet 3"; exit 1; }; } && { grep -qE "^| tenant | ${DU} | ${DB} | **war 8/0**" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE tenant nennt nicht die neu gemessenen Zahlen ${DU}/${DB} im etablierten Stil"; exit 1; }; } && awk -F'|' '$2 ~ /^ [a-z][a-z-] *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ ***Summe** *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk -F'|' '$2 ~ /^ *apps/api/src// { k=$4; gsub(/^ +| +$/,"",k); cls[k]++; pairs++ } $2 ~ /^ *(muss-mandantengebunden|keine-mandantengebundene-tabelle|beides|bewusst-uebergreifend) *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *$/ { k=$2; gsub(/^ +| +$/,"",k); v=$3; gsub(/[^0-9]/,"",v); tab[k]=v+0; tn++ } $2 ~ /^ ***Summe** *$/ && $4 ~ /^ *$/ { v=$3; gsub(/[^0-9]/,"",v); tsum=v+0; tseen=1 } /^## Klassen-Verteilung/ { h=$0; gsub(/[^0-9]/,"",h); hp=h+0; hseen=1 } END { if (tn+0 != 4 || !tseen || !hseen) { print "KLASSEN-VERTEILUNG nicht erkannt: Klassenzeilen " tn ", Summenzeile " tseen ", Ueberschrift " hseen; exit 1 } if (tsum != pairs+0) { print "KLASSEN-SUMME stimmt nicht: Bestandsaufnahme hat " pairs " Paare, Tabellensumme nennt " tsum; exit 1 } if (hp != pairs+0) { print "UEBERSCHRIFT der Klassen-Verteilung nennt " hp " Paare, Bestandsaufnahme hat " pairs; exit 1 } s=0; for (k in tab) { if (tab[k] != cls[k]+0) { print "KLASSE " k ": Tabelle nennt " tab[k] ", Bestandsaufnahme zaehlt " cls[k]+0; exit 1 } s+=tab[k] } if (s != pairs+0) { print "KLASSENZEILEN ergeben " s ", Bestandsaufnahme hat " pairs; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/tenant/tenant.controller.ts | user | muss-mandantengebunden | gebunden |' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden |.*260911-e2s' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden |.*260911-e2s' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Klassen-Verteilung/{f=1; next} /^## /{f=0} f && /Stand 260911-e2s/{m=1} END{ if(!m){print "KLASSEN-VERTEILUNG: kein Stand-Vermerk fuer 260911-e2s"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk 'BEGIN{split("ein zwei drei vier x sechs sieben acht neun",w," "); w[5]="fünf"} /^## Der Hintergrunddienst als Falle/{seen=1; head=$0; f=1; next} /^## /{f=0} f && /^- **/{n++} f && /^\*\*Der .* Fall, anderer Bauart/{n++} f && /260911-e2s/{m=1} END{ if(!seen){print "ABSCHNITT Hintergrunddienst nicht gefunden"; exit 1} want="## Der Hintergrunddienst als Falle — " w[n] " Fälle"; if(head != want){printf "HINTERGRUNDDIENST-UEBERSCHRIFT nennt \"%s\", gezaehlt wurden %d Faelle, erwartet \"%s\"\n", head, n, want; exit 1} if(!m){print "ABSCHNITT Hintergrunddienst nennt 260911-e2s nicht — die Abwesenheit eines sechsten Falls ist nicht belegt"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Zwei belegte Befunde/{f=1; next} /^## /{f=0} f && /260911-e2s/{m=1} f && /[Ee]ntschieden/{e=1} END{ if(!m||!e){print "ABSCHNITT \"Zwei belegte Befunde\": die Entscheidung zu 260911-e2s fehlt"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Bestandsaufnahme/{f=1; next} /^\| Datei \|/{f=0} f && /_count|include/{m=1} END{ if(!m){print "KOPF der Bestandsaufnahme nennt die Erkennungsluecke fuer Relationseinbindungen nicht"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Was diese Etappe NICHT entscheidet/{f=1; next} f && /Aufgelöst \(260911-e2s\)/{m=1} END{ if(!m){print "ABSCHNITT \"Was diese Etappe NICHT entscheidet\": der erste Punkt ist nicht als aufgeloest (260911-e2s) markiert"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && test 0 -eq "$(grep -c 'req.tenantPrisma' docs/anleitung-entwicklung.md)" && test 0 -eq "$(grep -c 'TenantMiddleware' docs/anleitung-entwicklung.md)" && awk '/^## Mandantentrennung/{f=1; next} /^## /{f=0} f && /260911-e2s/{m=1} f && /req\.tenantId/{t=1} END{ if(!m||!t){print "ANLEITUNG: Abschnitt Mandantentrennung nennt 260911-e2s oder req.tenantId nicht"; exit 1} }' docs/anleitung-entwicklung.md && test 0 -eq "$(grep -rn '\$transaction(' apps/api/src/tenant --include=*.ts | grep -v spec | wc -l | tr -d ' ')" && git rev-parse --verify f1017fa >/dev/null && PRISMA_CHANGED=$(git diff --name-only f1017fa -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && WEB_CHANGED=$(git diff --name-only f1017fa -- apps/web docker-compose.yml docker-compose.prod.yml) && { test -z "$WEB_CHANGED" || { printf 'FRONTEND/COMPOSE GEAENDERT — in diesem Plan verboten (Umgebungsdateien faengt die Erlaubnisliste unten):\n%s\n' "$WEB_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only f1017fa) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|apps/api/src/tenant/tenant\.guard\.ts|apps/api/src/tenant/tenant\.guard\.spec\.ts|apps/api/src/tenant/tenant\.middleware\.ts|apps/api/src/prisma/rls-access-inventory\.spec\.ts|apps/api/src/app\.module\.ts|apps/api/src/module-registry/module\.guard\.ts|apps/api/src/dkv/dkv\.controller\.ts|apps/api/src/tenant/tenant\.controller\.ts|apps/api/src/tenant/tenant\.controller\.spec\.ts|docs/mandantentrennung-zugriffsklassifikation\.md|docs/anleitung-entwicklung\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }</automated> </verify> <done>tenant.controller.ts: findAll, findOneundremove zaehlen Benutzer je Mandant ueber genau drei gebundene Aufrufstellen (tenantPrisma.user.countmitwhere: { tenantId }, in removemitisActive: true), kein Relationszaehler und kein includemehr, die viertenant-Zugriffe bleiben ungebunden, Antwortform und Meldungen unveraendert, Kopfkommentar nennt Grund und Entscheidung. tenant.controller.spec.tsexistiert neu mit dem Zwei-Klienten-Nachweis, deckt jeden ingenannten Fall ab — einschliesslich Rollen-Metadaten und Wachhund. Alle handgepflegten Stellen vondocs/mandantentrennung-zugriffsklassifikation.mdsind nachgezogen und maschinell gegatet:Zwei belegte Befunde(entschieden), Kopf der Bestandsaufnahme (Erkennungsluecke), drei Bestandsaufnahme-Zeilen (eine neu), Uebersichtszeile mit neu gemessenen Zahlen, Summenzeile, Klassen-Verteilung mit 64 Paaren und Stand-Vermerk, Hintergrunddienst-Abschnitt,Was diese Etappe NICHT entscheidet(erster Punkt aufgeloest).docs/anleitung-entwicklung.mdbeschreibt den Guard richtig und nennt die Middleware nicht mehr. Alle drei Falsifizierungsnachweise dieser Aufgabe sind durchgefuehrt, zurueckgenommen und woertlich notiert. Werkzeug alle Pruefungen bestanden, Baseline gehalten, Testzahl gestiegen, Schalter unveraendert aus, keine Regel aufTenant`.

<threat_model>

Konfiguriert: ASVS-Stufe 1, blockierend ab high.

Trust Boundaries

Boundary Description
Browser/Plattform-Administrator → Mandanten-API Die Rolle stammt ausschliesslich aus dem validierten Sitzungsnachweis; die Mandantenkennung im Pfad ist frei waehlbare Eingabe, bezeichnet aber den zu VERWALTENDEN Mandanten, nicht den des Anfragenden.
Jeder authentifizierte Nutzer → TenantGuard Die Grenze, an der req.tenantId entsteht: aus dem Sitzungsnachweis, fuer SUPER_ADMIN wahlweise aus x-tenant-id. 22 Lesestellen in 9 Dateien haengen daran.
SUPER_ADMIN → x-tenant-id Ein frei waehlbarer Header, der NUR fuer diese Rolle wirkt (T-04-03). Wird vom Marktplatz-Frontend benutzt.
API → PostgreSQL, Tabelle Tenant KEINE Zeilenschutz-Grenze — gemessen. Gebunden gleich ungebunden.
API → PostgreSQL, Tabelle User (ueber den Zaehler) Die Grenze, die drei Zugriffe dieses Bereichs unbemerkt ueberschritten: der Relationszaehler lief unter der Regel von User.
Loeschen eines Mandanten → Fremdschluessel User_tenantId_fkey Der Rueckhalt: ON DELETE RESTRICT, referentielle Pruefung umgeht den Zeilenschutz (gemessen, Pruefung 7).

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-E2S-01 Elevation of Privilege tenant.controller.ts, alle fuenf Handler high mitigate Mandantenverwaltung durch einen Nicht-SUPER_ADMIN. Heute: klassenweit @Roles(Role.SUPER_ADMIN), RolesGuard mit getAllAndOverride([handler, class]), kein Handler ueberschreibt (Befund E, gelesen). Ein spaeteres Handler-@Roles mit schwaecherer Rolle wuerde die Klassenrolle UEBERSCHREIBEN, ohne dass etwas rot wird — Aufgabe 3 nagelt Klassen- UND Handler-Metadaten als Test fest.
T-E2S-02 Spoofing tenant.guard.ts, tenant.middleware.ts, Anfrageobjekt-Eigenschaft high mitigate Tote Verdrahtung, die wie ein Schutz aussieht: ein je Anfrage erzeugter gebundener Klient ohne Leser, in zwei Dateien, eine davon nie verdrahtet. Ein spaeterer Leser koennte annehmen, Controller seien "automatisch gebunden". Entfernt (Aufgabe 2); Guard ohne Prisma; Middleware geloescht; Test asserts Abwesenheit; Gate zaehlt Punkt-Zugriffe in apps/api/src auf null, Kommentare eingeschlossen; Entscheidung an vier Dokumentstellen.
T-E2S-03 Denial of Service / Elevation of Privilege tenant.guard.ts, req.tenantId high mitigate Der Umbau koennte versehentlich die tragende Zuweisung req.tenantId mitreissen — 22 Lesestellen in 9 Dateien (ldap, settings, dkv, groups, module-guard, tenders) wuerden ohne Mandant laufen oder abbrechen. Alle fuenf Zweige als Testfaelle festgenagelt; Gate zaehlt genau zwei Zuweisungen; Guard-Reihenfolge in app.module.ts gegatet; Typpruefung ueber den gesamten API-Quelltext.
T-E2S-04 Information Disclosure / Tampering tenant.controller.ts findAll/findOne/remove, Relationszaehler high mitigate Nach dem Scharfschalten: Benutzerzahl 0 fuer jeden Mandanten in der Plattform-Administratorsicht (falsche Zahl ohne Signal), Loeschriegel T-02-09 vakuum. Gemessen ueber den generierten Client (Pruefungen 5-7). Behoben durch Fan-out je Mandant mit gebundenem Zaehler und ausdruecklichem where: { tenantId } (heute der einzige Filter, WINDOWS #18); als Testfaelle festgenagelt; Falsifizierung durch Rueckbau eines Zaehlers.
T-E2S-05 Spoofing tenant.guard.ts, x-tenant-id medium mitigate Ein Nicht-SUPER_ADMIN schickt den Header und wechselt den Mandanten. Heute korrekt abgewiesen (user.role === 'SUPER_ADMIN', T-04-03), aber nie getestet — Aufgabe 2 nagelt den Fall fest (Header wird ignoriert, req.tenantId bleibt der eigene).
T-E2S-06 Information Disclosure tenant.guard.ts, Header-Wert ohne Existenzpruefung low accept SUPER_ADMIN kann eine erfundene Mandantenkennung schicken; sie bindet an einen leeren Kontext — null Zeilen, kein Leck (D-10 wie entworfen). Nicht dieser Auftrag; in (n4)(f) festgehalten.
T-E2S-07 Denial of Service tenant.controller.ts findAll, Fan-out low accept Eine gebundene Zaehlabfrage je Mandant statt eines Joins. Mandantenzahl ist einstellig; dieselbe Form wie UserService.findAllForPlatformAdmin; in (n4)(g) festgehalten.
T-E2S-08 Tampering Fremdschluessel User_tenantId_fkey, remove low accept Der Fremdschluessel faengt auch INAKTIVE Benutzer, der Riegel zaehlt nur aktive — ein Mandant mit ausschliesslich inaktiven Benutzern liefert heute wie nach dem Plan 500 statt 400. Bestehendes Verhalten, gemessen (Pruefung 7/8), in (n4)(c) festgehalten, nicht geaendert.
T-E2S-09 Repudiation / Information Disclosure rls-access-inventory.spec.ts, Erkennungsluecke fuer Relationseinbindungen medium accept Die maschinelle Vollstaendigkeitsaussage der Klassifikation gilt nur fuer (Datei, Modell)-Paare, nicht fuer include/Relationszaehler in eine zweite Tabelle. Vermessen (Befund G, alle 19 include:- und alle _count-Stellen einzeln beurteilt): einzige gefaehrliche Auspraegung war dieser Bereich, hier behoben. Luecke im Kopf der Bestandsaufnahme und in (n4)(b) benannt; kein Ledger-Eintrag, solange die Wiederholung zur Ausfuehrungszeit keine zweite Auspraegung findet.
T-E2S-10 — (Wartbarkeit, keine Bedrohung) tenant.controller.ts, direkter Prisma-Zugriff low accept Controller mit direktem Datenbankzugriff (wie user.controller.ts). Bewusst NICHT in den Dienst verschoben (Scope); als Muster in (n5) festgehalten.
T-E2S-11 Spoofing Sitzungsnachweis low accept Mandanten- oder Rollenangabe aus Rumpf oder Pfad. Bereits in Phase 02/05 behandelt; dieser Plan aendert daran nichts.
T-E2S-SC Tampering Paketinstallation low accept Dieser Plan installiert kein Paket (npm/pip/cargo) und fuegt keine Abhaengigkeit hinzu. Das Legitimitaets-Gate faellt nicht an; ausdruecklich festgehalten statt schweigend ausgelassen.

</threat_model>

Nach Abschluss aller drei Aufgaben:

  1. node apps/api/scripts/rls-scratch-check.mjs meldet alle Pruefungen bestanden (101 bisherige plus die neuen, mindestens 110), Rueckgabewert 0.
  2. npm --prefix apps/api run test meldet mehr als 883 Tests gruen in mindestens 59 Dateien (57 bisherige plus zwei neue Testdateien; die geloeschte Middleware hatte keine).
  3. npm --prefix apps/api run type-check ist sauber.
  4. npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts ist gruen — 64 Paare, die Bestandsaufnahme stimmt mit dem Quelltext ueberein, die Ausnahmeliste ist leer und selbstpruefend.
  5. grep -rn '\.tenantPrisma' apps/api/src --include=*.ts und grep -rn 'TenantMiddleware' apps/api/src --include=*.ts liefern nichts; apps/api/src/tenant/tenant.middleware.ts existiert nicht.
  6. Alle handgepflegten Stellen der Klassifikation sind maschinell gegatet und gruen, die Zaehlgates LEITEN ihre Werte aus den im Dokument genannten Messanweisungen ab.
  7. Der Umfang ist als ERLAUBNISLISTE gegatet: jede Datei, die sich gegenueber f1017fa geaendert hat, ist eine der dreizehn in files_modified genannten (oder liegt unter .planning/); die drei Fremddateien nur in Kommentarzeilen. Unter apps/api/prisma, apps/web und den Compose-/Umgebungsdateien hat sich nichts geaendert.
  8. DATABASE_URL zeigt unveraendert auf die Rolle tessera; Tenant traegt keine Regel; nichts in Active Directory.

<success_criteria>

  • Die Architekturfrage aus Etappe 1 ist entschieden und umgesetzt: kein gebundener Klient auf dem Anfrageobjekt, keine Middleware, Guard ohne Prisma — und die Entscheidung steht mit Messung und Grund in Code, Kritikschrift, Klassifikation und Entwicklungsanleitung.
  • req.tenantId, der SUPER_ADMIN-Wechsel und die Ausnahme fuer mandantenlose Nutzer sind unveraendert und erstmals als Tests festgenagelt.
  • Auf Tenant ist nichts zu binden — gemessen an allen Migrationen und an der Datenbank, Roh-SQL und generierter Client; die beiden tenant-Paare bleiben ungebunden, mit Grund.
  • Der Relationszaehler-Befund ist gemessen, behoben (drei gebundene Zaehler, Fan-out je Mandant) und als Tests festgenagelt; die Erkennungsluecke der Bestandsaufnahme ist vermessen und benannt.
  • Die SUPER_ADMIN-Beschraenkung ist gelesen, bestaetigt und als Metadaten-Test festgenagelt.
  • Alle Falsifizierungsnachweise (Guard, Controller, zwei Dokument-Gates) sind durchgefuehrt, zurueckgenommen und im SUMMARY mit Testname beziehungsweise Meldung festgehalten.
  • Jede im SUMMARY genannte Zahl (neue Werkzeugpruefungen, neue Testfaelle je Datei, gebundene Zaehler, Paare) ist ABGEZAEHLT, nicht aus diesem Plan abgeschrieben; das SUMMARY widerspricht sich nicht (Fehler 8).
  • Baseline gehalten am Ende jeder Aufgabe. Der Schalter ist weiterhin AUS.

</success_criteria>

Create `.planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-SUMMARY.md` when done