Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
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 |
|
|
|
|
|
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_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:
tenant-keine-regel-in-allen-ausgelieferten-migrationen— liest zur Laufzeit JEDEmigration.sqlunterMIGRATIONS_DIRund prueft, dass keine einCREATE POLICYauf"Tenant", keinENABLE/FORCE ROW LEVEL SECURITYauf"Tenant"und keinALTER TABLE "Tenant"enthaelt. Die Belegausgabe nennt die Zahl der gelesenen Dateien und ausdruecklich, dass20260910120000_rls_widen_membership_grant_and_platform_readdarunter ist und"Tenant"nicht nennt.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".tenant-wegwerftabelle-deckt-alle-spalten-des-generierten-clients— Spaltenmenge der Wegwerf-Tabelle ausinformation_schema.columnsist identisch mit den Spaltennamen, die zur Laufzeit aus demCREATE TABLE "Tenant"-Block der Migration 20260618112124 geschnitten werden (NICHT ausreadSchemaModelFieldNames, Befund M). Steht VOR den Client-Pruefungen; faellt sie durch, bricht der Abschnitt ab.tenant-generierter-client-zeilen-gebunden-und-ungebunden-identisch—buildInlineExtendedClient(prisma, 'TENANT-A').tenant.findMany({ orderBy: { id: 'asc' } })undprisma.tenant.findMany(...)ungebunden liefern dieselben Kennungen wie Pruefung 2.tenant-generierter-client-benutzerzaehler-ungebunden-null-fuer-jeden-mandanten— die tragende Belegzeile: die Abfrage, diefindAllheute stellt (findManymit Relationszaehler ueberusers), 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, dieadmin/tenants/page.tsxals Benutzeranzahl anzeigen wuerde.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.tenant-loeschriegel-ungebunden-vakuum-fremdschluessel-faengt-laut— die Abfrage, dieremoveheute stellt (findUniquefuer 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. Danachprisma.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 undcodewoertlich (rate das Ergebnis nicht vorweg) und sagt, dass die referentielle Pruefung den Zeilenschutz umgeht — sonst haette sie die unsichtbarenUser-Zeilen nicht sehen koennen.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".tenant-fan-out-je-mandant-gebundene-zaehlung-stimmt— die Form, die Aufgabe 3 einbaut:prisma.tenant.findManyungebunden als Treiber, dann je verbliebenem MandantenbuildInlineExtendedClient(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 (findAllohne Aufrufer,findById,createsamt gebundener Standardgruppe,update), und eine fuerTenantGuard(kein Datenbankzugriff): Verhalten HEUTE nach dem Scharfschalten ohne diesen Plan, das konkrete Signal, und in eigener Spalte, ob das Frontend es durchlaesst. Dieremove-Zeile nennt die gemessene Fehlerklasse aus Pruefung 7.### (n3) Welcher Code eine falsche Zahl als Wahrheit deutet— hier nicht Leere, sondern eine falsche Zahl: BackendfindAll/findOneliefernuserCount: 0ohne jedes Signal;removelaesst den Riegel passieren und der Fremdschluessel antwortet laut mit der falschen Botschaft. Frontend, namentlich mit Stelle:admin/tenants/page.tsxzeigttenant.userCountan (um Zeile 209), verschluckt nicht-OK-Antworten infetchTenants(um Zeile 44-55) und inhandleDelete(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.tsxfaengt 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.findAllohne Aufrufer; (e) der unerreichbarenull-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 Bereichuser, deshalb kein eigener Eintrag.### (n5) Was dieser Durchlauf bewusst nicht anfasst— der direkte Prisma-Zugriff im Controller (Muster wieuser.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## Mandantentrennungvondocs/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 indocs/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.
- Kein
req.user(oeffentliche Route):canActivatelieferttrue, und wedertenantIdnoch die alte Anfrageobjekt-Eigenschaft sind als Schluessel auf dem Anfrageobjekt vorhanden ('tenantId' in reqist falsch). - Nutzer der Rolle USER mit
tenantId = 't1', keine Kopfzeile:true,req.tenantId === 't1'. - Nutzer der Rolle ADMIN mit
tenantId = 't1'UND Kopfzeilex-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
tenantIdund 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: wirftForbiddenException('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 gesamtenapps/api/srcauf 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.
Jeder Fall eigenstaendig, jeder mit sprechendem Namen:
findAllmit drei Mandanten (A: zwei Benutzer, B: ein Benutzer, C: keiner): liefert drei Eintraege mituserCount2/1/0 in der Reihenfolge des Nachbaus; das Protokoll enthaelt genau DREI gebundene Zaehlaufrufe, je einen unter der Kennung des jeweiligen Mandanten, jeder mitwhere.tenantIdgleich dieser Kennung; die Mandantenzeilen selbst kamen vom ungebundenen Nachbau (tenant.findManyaufgerufen).findAllohne Mandanten: leere Liste, KEIN gebundener Klient erzeugt.findAll: die Antwort traegt genau die Felderid,name,slug,isActive,createdAt,userCount(die heutige Form bleibt).findOnemit unbekannter Kennung:NotFoundException('Tenant not found'), KEIN gebundener Klient.findOnemit bekannter Kennung:userCountaus dem gebundenen Klienten UNTER DIESER Kennung,where.tenantIdgleich der Kennung.removemit unbekannter Kennung:NotFoundException, kein gebundener Klient,tenant.deletenicht aufgerufen.removemit aktiven Benutzern:BadRequestExceptionmit der heutigen Meldung ("Cannot delete tenant with active users..."),tenant.deleteNICHT aufgerufen; der gebundene Zaehlaufruf traegtwhere.isActive === trueundwhere.tenantIdgleich der Kennung.removemit ausschliesslich INAKTIVEN Benutzern: der Riegel laesst durch (Zaehler 0),tenant.deletewird 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).removeohne Benutzer: geloescht, Antwort wie oben.createundupdate: 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) istReflect.getMetadata(ROLES_KEY, TenantController.prototype.<handler>)undefiniert — ein Handler mit eigener, schwaecherer Rolle wuerde die Klassenrolle ueberschreiben (getAllAndOverride), dieser Fall wird dann rot. Importierereflect-metadataam Kopf der Testdatei. - Wachhund:
findOneundremoveerzeugen je genau EINEN gebundenen Klienten je Aufruf;findAllgenau so viele wie Mandanten (Musterdashboard.service.spec.tsZeile 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):
- 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. - 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 wartenant.controller.ts, in Aufgabe 3 behoben. - 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 aususer.service.ts,where: { tenantId }bleibt, drei Aufrufstellen. Ohne diese Zeile istrls-access-inventory.spec.tsam Ende dieser Aufgabe rot (Lehre aus 260910-exd). - Uebersichtszeile
tenantmit 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). - Summenzeile derselben Tabelle mit fortgeschriebener Herkunftsspur.
- Klassen-Verteilung samt Zahl in der Ueberschrift: ein neues Paar,
muss-mandantengebundensteigt um eins; ein**Stand 260911-e2s**-Absatz sagt, welches Paar hinzukam und dass keine Klasse sich verschob. - 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.createdie Standardgruppe ueber den bereits gebundenen Weg des Bereichsgroupsanlegt. - Abschnitt
Was diese Etappe NICHT entscheidet, erster Punkt: in der Form des aufgeloesten #19-Punktes durchstreichen undAufgelö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:
node apps/api/scripts/rls-scratch-check.mjsmeldet alle Pruefungen bestanden (101 bisherige plus die neuen, mindestens 110), Rueckgabewert 0.npm --prefix apps/api run testmeldet mehr als 883 Tests gruen in mindestens 59 Dateien (57 bisherige plus zwei neue Testdateien; die geloeschte Middleware hatte keine).npm --prefix apps/api run type-checkist sauber.npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.tsist gruen — 64 Paare, die Bestandsaufnahme stimmt mit dem Quelltext ueberein, die Ausnahmeliste ist leer und selbstpruefend.grep -rn '\.tenantPrisma' apps/api/src --include=*.tsundgrep -rn 'TenantMiddleware' apps/api/src --include=*.tsliefern nichts;apps/api/src/tenant/tenant.middleware.tsexistiert nicht.- Alle handgepflegten Stellen der Klassifikation sind maschinell gegatet und gruen, die Zaehlgates LEITEN ihre Werte aus den im Dokument genannten Messanweisungen ab.
- Der Umfang ist als ERLAUBNISLISTE gegatet: jede Datei, die sich gegenueber
f1017fageaendert hat, ist eine der dreizehn infiles_modifiedgenannten (oder liegt unter.planning/); die drei Fremddateien nur in Kommentarzeilen. Unterapps/api/prisma,apps/webund den Compose-/Umgebungsdateien hat sich nichts geaendert. DATABASE_URLzeigt unveraendert auf die Rolletessera;Tenanttraegt 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
Tenantist nichts zu binden — gemessen an allen Migrationen und an der Datenbank, Roh-SQL und generierter Client; die beidentenant-Paare bleibenungebunden, 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