17 KiB
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 |
|
6426b18 |
|
|
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:
- Header switch removed for SUPER_ADMIN (
false && req.headers[...]) →tenant.guard.spec.tsfailed exactly 1/7:SUPER_ADMIN mit tenantId und x-tenant-id-Kopfzeile...,expected 't1' to be 't2'. Restored, 7/7 green. - 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. findOnebound counter replaced with unbound(this.prisma as any).user.count→tenant.controller.spec.tsfailed exactly 2/20 withTypeError: Cannot read properties of undefined (reading 'count')— matches SUMMARY's claimed falsification exactly. Restored, 20/20 green.- 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). |
Key Link Verification
| 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)