Files
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

177 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
task: quick-260911-e2s
verified: 2026-09-11T09:06:02Z
status: passed
score: 10/10 must-have truths verified
commits_reviewed: [652e762, 11f5731, 17dca0d, c8de72e]
base: 6426b18
covered_files:
- 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
advisory:
- finding: "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)."
category: architectural
reason: "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."
evidence_status: "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). |
## 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)*