Files
tessera-ctl/.planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-VERIFICATION.md
T
schalli 6236b302f4
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 56s
Tessera CI/CD / Build & Publish Images (push) Successful in 29s
docs(quick-260911-e2s): Etappe 2 Bereich tenant abgeschlossen, WINDOWS #27 Relations-Blindstelle
2026-09-11 11:08:33 +02:00

17 KiB
Raw Blame History

task, verified, status, score, commits_reviewed, base, covered_files, advisory
task verified status score commits_reviewed base covered_files advisory
quick-260911-e2s 2026-09-11T09:06:02Z passed 10/10 must-have truths verified
652e762
11f5731
17dca0d
c8de72e
6426b18
apps/api/scripts/rls-scratch-check.mjs
apps/api/src/app.module.ts
apps/api/src/dkv/dkv.controller.ts
apps/api/src/module-registry/module.guard.ts
apps/api/src/prisma/rls-access-inventory.spec.ts
apps/api/src/tenant/tenant.controller.spec.ts
apps/api/src/tenant/tenant.controller.ts
apps/api/src/tenant/tenant.guard.spec.ts
apps/api/src/tenant/tenant.guard.ts
apps/api/src/tenant/tenant.middleware.ts
docs/anleitung-entwicklung.md
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-zugriffsklassifikation.md
.planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-PLAN.md
.planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-SUMMARY.md
finding category reason evidence_status
Kein WINDOWS-Ledger-Eintrag fuer die strukturelle Erkennungsluecke von rls-access-inventory.spec.ts (Relationseinbindungen/`include`/`_count` in eine zweite Tabelle bleiben fuer das Werkzeug unsichtbar, unabhaengig davon, dass die heute einzige gefaehrliche Auspraegung in diesem Plan behoben wurde). architectural Die Luecke ist ein dauerhaftes Werkzeug-Merkmal, kein historischer Einzelfall — ein KUENFTIGER `include: { _count }`-Zugriff auf eine geschuetzte Tabelle waere von der Bestandsaufnahme strukturell genauso unsichtbar wie der hier gefundene. Der Plan begruendet den Verzicht auf einen Ledger-Eintrag ausdruecklich mit 'nur diese eine Auspraegung existierte, hier behoben' — das schliesst aber nur die heutigen Instanzen, nicht den Mechanismus. Gemessen und in (n4)(b) sowie im Kopf der Bestandsaufnahme benannt; im Bedrohungsregister als T-E2S-09 (medium, accept) gefuehrt. Kein WINDOWS-Eintrag angelegt.

Quick 260911-e2s: Mandantentrennung Etappe 2, Bereich tenant — Verification Report

Task goal: Remove the never-read req.tenantPrisma wiring (delete the unwired middleware, strip the guard's Prisma dependency) while preserving req.tenantId and the x-tenant-id switch; replace the three relation-count reads into the protected User table with a bound fan-out; create the missing guard and controller tests; leave the classification document in sync.

Verified: 2026-09-11T09:06:02Z Status: passed Re-verification: No — initial verification

This is an adversarial, independent re-verification. Every claim below was checked against the live codebase and, where feasible, against the running database — SUMMARY.md text was never accepted as evidence on its own.

Goal Achievement — Observable Truths (from PLAN.md must_haves.truths)

# Truth Status Evidence
1 Architecture decision on the bound-client-on-request-object question is made and recorded in code, kritikschrift, classification, and dev guide; a test makes a reappearance fail red ✓ VERIFIED tenant.guard.ts header comment cites 260911-e2s decision + reasoning; docs/mandantentrennung-etappe2-fehlerrichtung.md (n4)(a); docs/mandantentrennung-zugriffsklassifikation.md "Zwei belegte Befunde" ("Entschieden (260911-e2s, Aufgabe 2)"); docs/anleitung-entwicklung.md "## Mandantentrennung" rewritten. Independently broke the property back into the guard style described by SUMMARY's own falsification proof — not re-tested directly (guard no longer accepts it structurally, no constructor param); instead independently falsified the closely-related header/role gates below with the same red-then-restore method. 'tenantPrisma' in req assertions present in all 7 tenant.guard.spec.ts cases.
2 req.tenantId stays set in all 5 branches; SUPER_ADMIN x-tenant-id switch and ForbiddenException for tenant-less non-SUPER_ADMIN survive; every branch pinned by a test ✓ VERIFIED Read tenant.guard.ts: 2 req.tenantId = assignments, no Prisma import, header check gated on user.role === 'SUPER_ADMIN'. Independently broke the header gate twice (forced false && ..., then removed the role check entirely) and re-ran tenant.guard.spec.ts each time — exactly 1 named test failed each time, with the expected assertion message; restored and confirmed 7/7 green and git diff clean afterwards.
3 Tenant classification checked against code and all 34 migrations; bound vs. unbound reads return identical rows (raw SQL + generated client) ✓ VERIFIED Ran rls-scratch-check.mjs live against tessera-ctl-db-1 (address resolved fresh: 172.19.0.2). All 110 checks passed, exit 0, including all 9 named tenant-* checks from the plan (tenant-keine-regel-in-allen-ausgelieferten-migrationen through tenant-fan-out-je-mandant-gebundene-zaehlung-stimmt). Migration scan output explicitly names 20260910120000_rls_widen_membership_grant_and_platform_read and confirms "Tenant" is absent from it.
4 Relation-count finding measured and fixed: 3 of 8 accesses count through the User relation; fixed via bound fan-out ✓ VERIFIED Live probe checks 5–7 show the unbound relation counter returning 0 for every tenant while the maintenance role counts >0, and the FK (User_tenantId_fkey) loudly catching the vacuum delete-gate with P2003. tenant.controller.ts now uses exactly 3 tenantPrisma.user.count( calls (verified by grep) and 0 include/_count occurrences outside comments.
5 Reverse error direction is named: "every tenant has 0 users" (not empty list) and a 500 instead of the 400 message; frontend shown to pass both through ✓ VERIFIED docs/mandantentrennung-etappe2-fehlerrichtung.md "## Bereich tenant" (n2)/(n3) name admin/tenants/page.tsx and TenantContextSelector.tsx explicitly, with line references and the exact backend behavior (0 userCount / loud 500 vs. 400).
6 Detection gap of the automated inventory (relation includes into a second table) is named and measured ✓ VERIFIED (n4)(b) and the "## Bestandsaufnahme" head both name the gap; Befund G's two lists (_count sites, include: sites) are reproduced in the doc with per-site judgment. See Advisory note below re: no WINDOWS ledger entry.
7 SUPER_ADMIN restriction read and pinned as a metadata test ✓ VERIFIED tenant.controller.ts carries class-wide @Roles(Role.SUPER_ADMIN); tenant.controller.spec.ts asserts Reflect.getMetadata(ROLES_KEY, TenantController) equals [Role.SUPER_ADMIN] and, for each of the 5 handlers, that no handler-level override exists.
8 Test landscape for guard and controller created from nothing (previously only 2 cases in tenant.service.spec.ts) ✓ VERIFIED tenant.guard.spec.ts (7 cases) and tenant.controller.spec.ts (20 cases) both newly created; both files exist, both pass (confirmed live: 7/7 and 20/20).
9 All five hand-maintained classification doc sections updated and machine-gated ✓ VERIFIED Independently recomputed: 64 (file,model) pairs in the Bestandsaufnahme table; class distribution 32/17/13/2 = 64 matches the doc's own "Klassen-Verteilung" table; overview row | tenant | 8 | 3 | matches independently-measured grep counts (DU=8, DB=3); "Zwei belegte Befunde" carries the 260911-e2s resolution; "Was diese Etappe NICHT entscheidet" first item marked Aufgelöst (260911-e2s).
10 Baseline held: ≥883 tests green, type-check clean, tool ≥110 checks; switch stays OFF, schema/migrations untouched, no compose/env files touched, nothing in AD, NO policy on Tenant ✓ VERIFIED Orchestrator independently measured 911/911 tests (59 files) and clean type-check (both re-confirmed structurally: find apps/api/src -name '*.spec.ts' | wc -l = 59). git diff --name-only 6426b18..HEAD -- apps/api/prisma empty; -- docker-compose.yml docker-compose.prod.yml '*.env*' empty; -- apps/web empty. Live probe: pg_class.relrowsecurity for "Tenant" = false, no CREATE POLICY on Tenant in any of 34 migrations.

Score: 10/10 truths verified, 0 present-but-behavior-unverified.

Independent Falsification (adversarial, not from SUMMARY)

All four falsifications below were run by the verifier directly against the working tree, each backed up first and restored immediately after, with git status --short / diff confirming a byte-identical restore:

  1. Header switch removed for SUPER_ADMIN (false && req.headers[...]) → tenant.guard.spec.ts failed exactly 1/7: SUPER_ADMIN mit tenantId und x-tenant-id-Kopfzeile..., expected 't1' to be 't2'. Restored, 7/7 green.
  2. Role gate removed (header now honoured for ANY role) → failed exactly 1/7: Nutzer der Rolle ADMIN mit tenantId UND x-tenant-id-Kopfzeile... (T-04-03), expected 't2' to be 't1'. Restored, 7/7 green.
  3. findOne bound counter replaced with unbound (this.prisma as any).user.count → tenant.controller.spec.ts failed exactly 2/20 with TypeError: Cannot read properties of undefined (reading 'count') — matches SUMMARY's claimed falsification exactly. Restored, 20/20 green.
  4. Stale entry injected into FORTENANT_ASSIGNMENT_EXCEPTIONS ('apps/api/src/does-not-exist.ts') → the new watchdog test failed exactly as designed: "apps/api/src/does-not-exist.ts: Datei existiert nicht mehr". Restored, 11/11 green.

All four confirm the tests genuinely exercise the invariants they claim to pin, not just that the invariants happen to hold today.

Required Artifacts

Artifact Expected Status Details
apps/api/scripts/rls-scratch-check.mjs 12th section runTenantAreaChecks, ≥9 named checks, 5+ over generated client ✓ VERIFIED Confirmed at line 3163, called between runCalendarAreaChecks and runTransactionShapeMeasurement (line order verified). Live run: all 9 named checks pass, 110/110 total.
docs/mandantentrennung-etappe2-fehlerrichtung.md ## Bereich tenant with (n1)-(n5) ✓ VERIFIED All five ### (nX) subsections present at line 2262+.
apps/api/src/tenant/tenant.guard.ts sets only req.tenantId, no Prisma dependency ✓ VERIFIED Confirmed by direct read; no constructor, no forTenant/PrismaService import.
apps/api/src/tenant/tenant.middleware.ts DELETED ✓ VERIFIED test -e confirms absence.
apps/api/src/tenant/tenant.guard.spec.ts NEW, all 5 branches + property-absence + header-only-SUPER_ADMIN ✓ VERIFIED 7 cases, all read and confirmed present.
apps/api/src/prisma/rls-access-inventory.spec.ts FORTENANT_ASSIGNMENT_EXCEPTIONS emptied + watchdog ✓ VERIFIED new Set<string>([]); watchdog test confirmed to fire (see falsification #4).
apps/api/src/app.module.ts, module.guard.ts, dkv.controller.ts comment-only fixes ✓ VERIFIED Reviewed diffs manually; no TenantMiddleware/.tenantPrisma references remain anywhere in apps/api/src.
apps/api/src/tenant/tenant.controller.ts 3 bound fan-out counters, 4 unbound tenant accesses ✓ VERIFIED Grep confirms exactly 3 tenantPrisma.user.count( and 4 this.prisma.tenant. occurrences; 0 include/_count.
apps/api/src/tenant/tenant.controller.spec.ts NEW, two-client proof, all behaviors, role metadata, watchdog ✓ VERIFIED 20 cases, all read and confirmed to match plan's <behavior> spec.
docs/mandantentrennung-zugriffsklassifikation.md 5 hand-maintained sections updated ✓ VERIFIED 64 pairs independently recomputed and cross-checked against the class-distribution table.
docs/anleitung-entwicklung.md Guard description without request-object client; middleware hint box replaced ✓ VERIFIED 0 occurrences of req.tenantPrisma/TenantMiddleware in the file; obsolete table list intentionally left unchanged per (n5).
From To Via Status Details
TenantGuard app.module.ts APP_GUARD registration order JwtAuthGuard → TenantGuard → RolesGuard ✓ WIRED Confirmed by direct read of app.module.ts lines 50-65.
Prisma include: {_count} single SQL statement, LEFT JOIN into User Prisma 6.19 query rendering ✓ VERIFIED (live) Reproduced live via probe checks 5-7 against the running dev DB, not merely asserted.
User_tenantId_fkey (ON DELETE RESTRICT) referential check bypasses RLS live delete against scratch DB ✓ VERIFIED (live) Check 7 output shows P2003 thrown, row still visible via maintenance role.
Fan-out pattern UserService.findAllForPlatformAdmin identical form (this.prisma.tenant.findMany unbound driver + forTenant() bound counter per tenant) ✓ VERIFIED Confirmed by direct code comparison — same structure.
SUPER_ADMIN x-tenant-id header marketplace frontend 4 send sites ✓ VERIFIED grep -rn "x-tenant-id" apps/web/src returns exactly 4 hits in marketplace/page.tsx and marketplace/[slug]/page.tsx, matching the plan's claim.

Behavioral Spot-Checks / Probe Execution

Probe Command Result Status
apps/api/scripts/rls-scratch-check.mjs (live, adversary-resolved DB address) TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs Alle 110 Pruefungen bestanden., exit 0 ✓ PASS
tenant.guard.spec.ts (single file) npm --prefix apps/api run test -- src/tenant/tenant.guard.spec.ts 7/7 ✓ PASS
tenant.controller.spec.ts (single file) npm --prefix apps/api run test -- src/tenant/tenant.controller.spec.ts 20/20 ✓ PASS
rls-access-inventory.spec.ts (single file) npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts 11/11 ✓ PASS

Full-suite result (911/911, 59 files) and type-check (clean) were not re-run in full by this verifier — already independently measured by the orchestrator per the task brief; spec-file count (59) was independently confirmed by filesystem enumeration.

Scope / Allow-list Verification

git diff --name-only 6426b18..HEAD returns exactly the 13 files declared in the PLAN's files_modified frontmatter — no more, no less. No changes under apps/api/prisma, apps/web, docker-compose*.yml, or any .env* file. Working tree is clean except the untracked SUMMARY.md (expected — committed by the orchestrator, not the task commits).

Requirements Coverage

Requirement Description Status Evidence
WINDOWS-18 Switch stays OFF; measurement tool covers the tenant area ✓ SATISFIED 110/110 checks pass live; switch confirmed unchanged (role tessera, BYPASSRLS, not touched by this diff).
ETAPPE-2-TENANT Guard/controller/tests for the tenant area ✓ SATISFIED All artifacts and truths above.

Anti-Patterns Found

None of TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER found in any of the 13 changed files. No stub returns, no empty handlers, no hardcoded-empty props found in the reviewed source files.

Judgment Call Requested (Advisory, non-blocking)

Item 8 of the verification brief: should the absence of a WINDOWS ledger entry for the structural blind spot in rls-access-inventory.spec.ts (relation includes/_count into a second table are invisible to the (file, model)-pair scanner) be acceptable?

My judgment: it should be recorded regardless, even though it does not block this phase. The plan's own reasoning for skipping a ledger entry — "the only dangerous instance found across the whole API source was this one, and it's fixed here" — closes out today's instances, not the underlying mechanism. The scanner will remain structurally blind to any future include: { _count } (or similar relation-count) access into a row-level-secured table; nothing added by this task changes that. This is exactly the category of finding the project's own WINDOWS ledger exists to track (compare entries #24, #19, #25, #26 in .planning/WINDOWS.md, all of which record a persisting structural gap rather than a fixed one-off). The plan did document the gap thoroughly (measured lists of all 19 include: and all _count sites, judged individually, in (n4)(b) and the Bestandsaufnahme head) and carried it in the threat register as T-E2S-09 (medium, accept) — so this is not a hidden risk, just an un-ledgered one. This does not affect the phase's must-have truths (truth #6 only requires the gap to be named and measured, which it is) and is therefore not a gap for this task, but is flagged here for a human decision on whether to open a WINDOWS entry going forward.

Gaps Summary

None. All 10 must-have truths verified with adversarial, independently reproduced evidence (including 4 successful red-then-restore falsifications and a live 110/110 probe run against the actual database). Scope is exactly the declared 13-file allow-list. One advisory judgment call is flagged above (WINDOWS ledger entry) — it does not block phase completion.


Verified: 2026-09-11T09:06:02Z Verifier: Claude (gsd-verifier)