Files
tessera-ctl/.planning/quick/260910-das-mandantentrennung-etappe-2-bereich-user-/260910-das-VERIFICATION.md
T

15 KiB
Raw Blame History

phase, verified, status, score, covered_files, covered_digest, behavior_unverified, overrides_applied
phase verified status score covered_files covered_digest behavior_unverified overrides_applied
quick-260910-das 2026-09-10T08:43:00Z passed 10/10 must-haves verified
.planning/WINDOWS.md
.planning/quick/260910-das-mandantentrennung-etappe-2-bereich-user-/260910-das-PLAN.md
.planning/quick/260910-das-mandantentrennung-etappe-2-bereich-user-/260910-das-SUMMARY.md
apps/api/scripts/rls-scratch-check.mjs
apps/api/src/user/admin-seed.service.spec.ts
apps/api/src/user/admin-seed.service.ts
apps/api/src/user/user.controller.spec.ts
apps/api/src/user/user.controller.ts
apps/api/src/user/user.service.spec.ts
apps/api/src/user/user.service.ts
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-zugriffsklassifikation.md
v1:sha256:57beb919b493cdad90d7e21464d014838272020b4459348b970c7c9f0fec2bed 0 0

Phase quick-260910-das: Mandantentrennung Etappe 2, Bereich user — Verification Report

Phase Goal: Bind the tenant-bound administration paths in apps/api/src/user/ while deliberately NOT binding the platform-wide uniqueness/lookup paths, defuse the post-cutover startup blocker, and leave the classification document's four hand-maintained sections in sync.

Verified: 2026-09-10T08:43:00Z Status: passed Re-verification: No — initial verification

Independent Re-Measurement Summary

All ten investigation items from the verification brief were independently re-measured against the live codebase and a live throwaway-database run — not read off the SUMMARY. Findings:

  1. Startup blocker genuinely defused, and only it. admin-seed.service.ts binds admin creation to the just-created tenant (forTenant(this.prisma, tenant.id)) and catches err?.code === 'P2002' specifically, logging and returning instead of throwing. Every other error still throws — confirmed by running Test 10 (P2002 absorbed, no throw) and Test 11 (connection refused still rejects onApplicationBootstrap()) individually; both pass. No over-broad catch exists.
  2. Bind/don't-bind line drawn per method, with reason at each site. Read every method of user.service.ts, admin-seed.service.ts, and user.controller.ts. All administration paths (findById, create, update, deactivate, delete, both platform-admin methods, all seven controller accesses) run through tenantPrisma. findByUsername and the seed-check lookup are the only deliberately unbound paths, each carrying an in-code comment naming the reason (platform-wide uniqueness of username) and the resolveEmailForWrite precedent.
  3. findByUsername caller count. grep -rn "findByUsername" apps/api/src packages returns exactly one hit — the definition itself (user.service.ts:51). No production caller. The comment at the definition now states this measured fact and correctly attributes the login path to the three SECURITY DEFINER functions (Etappe 1, 260909-eor) instead of claiming cross-tenant login still depends on this method.
  4. Self-delete guard. Confirmed the code now compares user.id === currentUser.id (not .sub). Independently reverted the comparison back to currentUser.sub and re-ran the single named test (user.controller.spec.ts, "Test 6: der Riegel gegen das Löschen des eigenen Kontos greift") — it failed with promise resolved "{ message: 'User deleted' }" instead of rejecting, exactly the failure mode described in the SUMMARY. Reverted the temporary change back (file now matches the committed state, git diff clean). The falsification claim holds.
  5. SUPER_ADMIN view. UserService.findAllForPlatformAdmin / findByIdForPlatformAdmin loop over this.prisma.tenant.findMany() (unbound, Tenant carries no RLS — confirmed via the scratch check's pg_class.relrowsecurity measurement) and issue one bound tenantPrisma.user.* call per tenant inside the loop, matching the ensureDefaultGroupsForAllTenants() precedent. UserController.findAll and resolveTargetUser only route to these methods when currentUser.role === Role.SUPER_ADMIN; a non-SUPER_ADMIN caller always goes through the tenant-bound branch. No cross-tenant leak to a non-SUPER_ADMIN caller.
  6. Mid-task deviation (Rule 3). git show 888f660 -- docs/... klassifikation.md shows the task-2 correction updated only the Stand column (to gemischt) and added the new (user.service.ts, tenant) row — an honest, accurate description of the intermediate state, not a loosened check. The full class corrections followed in task 3 as planned. Legitimate.
  7. Four hand-maintained sections. Recomputed the class distribution directly from the 63 Bestandsaufnahme rows via awk (independent of the document's own summary table): muss-mandantengebunden=31, keine-mandantengebundene-tabelle=17, beides=13, bewusst-uebergreifend=2, total 63 — matches the document's "Klassen-Verteilung" table exactly. The "Übersicht je Bereich" row for user (8 ungebunden / 14 gebunden) matches a fresh grep -ro "this\.prisma\.[a-zA-Z]*" / tenantPrisma\.[a-zA-Z]*\. count. "Der Hintergrunddienst als Falle" section lists five cases (was four), with the admin-seed.service.ts case correctly described as the first already-correct-on-both-halves case.
  8. Measurements committed, not merely described. Ran apps/api/scripts/rls-scratch-check.mjs myself against a freshly resolved tessera-ctl-db-1 IP (172.19.0.2, resolved fresh via docker inspect, not copied from any document). Output: "Alle 53 Pruefungen bestanden." — 41 prior + 12 new, all twelve named user-* checks present and passed, including user-eindeutigkeit-greift-trotz-unsichtbarkeit, which explicitly distinguishes SQLSTATE 23505 (uniqueness violation) from 42501 (row-security rejection) in its own message text.
  9. Falsification proofs in SUMMARY. Present for four sites, each naming the exact broken binding and the exact named test that went red (UserService.findById, AdminSeedService.seedAdmin, UserController.uploadAvatar, and the self-delete-guard red-before-fix). Independently reproduced the self-delete-guard proof (item 4 above); the other three read as specific and plausible given the test code inspected.
  10. Constraints held. git diff --name-only 7e7a697..HEAD touches exactly the 10 files listed in files_modified — none under apps/api/prisma, no compose file, no env file, nothing under auth/ or ldap/ (confirmed via git diff --stat against those directories: empty). docker-compose.yml still defaults DATABASE_URL to the tessera role (BYPASSRLS, switch off). WINDOWS #22 records the platform-wide uniqueness question as open, not decided, with no schema/migration change.

Goal Achievement

Observable Truths

# Truth Status Evidence
1 Bind/don't-bind line drawn per method, justified in code, no direction silently decided ✓ VERIFIED user.service.ts, admin-seed.service.ts, user.controller.ts — every method inspected; unbound paths carry written reasons
2 Chain (invisible row → false "free" → hard uniqueness error) measured at the real shipped policy, distinguishing 23505 from 42501 ✓ VERIFIED rls-scratch-check.mjs run live: user-eindeutigkeit-greift-trotz-unsichtbarkeit passes with SQLSTATE 23505, explicitly not 42501
3 Worst inverse-error-direction case (startup blocker) found, measured, and defused in application code only ✓ VERIFIED admin-seed.service.ts catches P2002 specifically; Test 10/11 individually run and pass; no schema change
4 Classification line for admin first-creation corrected (tenant is known, not structurally absent) ✓ VERIFIED docs/mandantentrennung-zugriffsklassifikation.md row for (admin-seed.service.ts, user), class beides, with Befund-J correction text
5 Etappe-1 login path untouched; findByUsername's stale comment corrected with measured caller count ✓ VERIFIED git diff --stat empty for auth//ldap/; findByUsername comment states "genau EINEN Treffer... kein Aufrufer", confirmed via fresh grep
6 Platform-admin overview preserved as a bound loop over all tenants, not silently degraded or broken ✓ VERIFIED findAllForPlatformAdmin/findByIdForPlatformAdmin; Test 6/7 in user.service.spec.ts; scratch check user-fan-out-je-mandant-gebunden-liefert-alle-zeilen passes
7 Existing self-delete gap closed ✓ VERIFIED Code compares currentUser.id; independently reverted and confirmed Test 6 in user.controller.spec.ts goes red, then restored
8 Test coverage repaired across all three files with two-client proof ✓ VERIFIED user.service.spec.ts (13 tests), admin-seed.service.spec.ts (10 tests), user.controller.spec.ts (8 tests, new file) — all inspected and run
9 Classification doc and rls-access-inventory.spec.ts in sync, including all four hand-maintained sections plus the fifth background-service case ✓ VERIFIED Recomputed 63/31/17/13/2 from raw Bestandsaufnahme rows; matches document; rls-access-inventory.spec.ts green (part of 810/810)
10 789+ tests and type-check green, tool reports all checks passed, schema/migrations/compose/env unchanged, switch stays off ✓ VERIFIED 810/810 tests green (independently re-run), type-check exit 0, scratch tool "Alle 53 Pruefungen bestanden.", git diff --name-only = exactly the 10 declared files

Score: 10/10 truths verified (0 present-but-behavior-unverified)

Required Artifacts

Artifact Expected Status Details
apps/api/scripts/rls-scratch-check.mjs Seventh section runUserAreaChecks, 12 new named checks ✓ VERIFIED Present, run live, all 12 pass alongside the prior 41
docs/mandantentrennung-etappe2-fehlerrichtung.md ## Bereich user section (u1–u5) ✓ VERIFIED All five subsections present; (u1) contains the actual pasted measurement output, not a narration
apps/api/src/user/user.service.ts Bound admin methods, unbound findByUsername with corrected comment ✓ VERIFIED Inspected in full
apps/api/src/user/user.service.spec.ts Two-client proof, 13 tests ✓ VERIFIED Present, run, passes
apps/api/src/user/admin-seed.service.ts Bound first-admin creation, P2002 absorption ✓ VERIFIED Inspected in full
apps/api/src/user/admin-seed.service.spec.ts Two-client proof, 10 tests ✓ VERIFIED Present, run, passes
apps/api/src/user/user.controller.ts All 7 accesses bound, self-delete guard fixed ✓ VERIFIED Inspected in full
apps/api/src/user/user.controller.spec.ts New file, two-client proof, 8 tests ✓ VERIFIED Present, run, passes
docs/mandantentrennung-zugriffsklassifikation.md All four hand-maintained sections updated ✓ VERIFIED Recomputed arithmetic matches
.planning/WINDOWS.md Open entry for platform-wide uniqueness ✓ VERIFIED Entry #22, status open, recorded not decided
From To Via Status
bound client tenant_isolation_policy on User (from migration 20260618112133_rls_policies) user-policy-aus-migration-wortgleich ✓ WIRED — check passes, policies wordidentical
platform-wide unique username/email bound collision check user-gebundene-suche-nach-fremdem-benutzernamen-liefert-keine-zeile + user-eindeutigkeit-greift-trotz-unsichtbarkeit ✓ WIRED — chain measured end to end
Erstanlage-check platform-wide uniqueness uncapsulated seedAdmin() ✓ WIRED — Test 10/11 individually confirm both halves
freshly created tenant first-admin insert tenant.id passed into forTenant() ✓ WIRED — code + Test 9
Tenant table without RLS platform-admin view + default-group repair loop drivers this.prisma.tenant.findMany() ✓ WIRED — tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar passes
Etappe-1 SECURITY DEFINER functions findByUsername boundary corrected comment + caller-count measurement ✓ WIRED — grep confirms zero callers
rls-access-inventory.spec.ts classification doc's Stand/Klassen-Verteilung/Summenzeilen machine check ✓ WIRED — green in full suite run, arithmetic independently recomputed

Behavioral Spot-Checks

Behavior Command Result Status
Self-delete guard actually guards revert to currentUser.sub, run named test Test failed with described error, then restored ✓ PASS
P2002 absorbed, other errors abort run Test 10 and Test 11 individually Both pass independently ✓ PASS
Scratch DB tool reports the full check set TESSERA_SCRATCH_ADMIN_URL=... node rls-scratch-check.mjs against freshly resolved container IP "Alle 53 Pruefungen bestanden." ✓ PASS
Full test suite npm run test (apps/api) 810/810 passed ✓ PASS
Type check npm run type-check (apps/api) exit 0 ✓ PASS

Anti-Patterns Found

None. Scanned all 9 modified/created code and doc files for TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER — zero hits.

Requirements Coverage

Requirement Description Status Evidence
WINDOWS-18 Chain measured at real deployed policy, SQLSTATE distinction ✓ SATISFIED runUserAreaChecks, live run, user-eindeutigkeit-greift-trotz-unsichtbarkeit
ETAPPE-2-USER User area bound per the bind/don't-bind rule, startup blocker defused, docs in sync ✓ SATISFIED All 10 truths above

No orphaned requirements found for this phase in .planning/WINDOWS.md/REQUIREMENTS.md cross-reference.

Human Verification Required

None. All must-haves are verifiable via code inspection, a live database run, and test execution — no UI, visual, or external-service behavior is in scope for this phase.

Gaps Summary

No gaps found. All ten must-have truths, all ten required artifacts, and all seven key links independently re-verified against the live codebase and a live throwaway-database run — not accepted from the SUMMARY. The self-delete-guard falsification claim was independently reproduced (revert → red → restore → clean diff). The class-distribution arithmetic (63 pairs: 31/17/13/2) was independently recomputed from raw table rows, not copied from the document's own summary line. The scratch-check tool was re-run against a freshly resolved container address and reports 53/53 passing, matching the claimed 41+12. Constraints (no schema/migration/compose/env change, login path untouched, switch off) all hold under independent git diff inspection.


Verified: 2026-09-10T08:43:00Z Verifier: Claude (gsd-verifier)