15 KiB
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 |
|
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:
- Startup blocker genuinely defused, and only it.
admin-seed.service.tsbinds admin creation to the just-created tenant (forTenant(this.prisma, tenant.id)) and catcheserr?.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 refusedstill rejectsonApplicationBootstrap()) individually; both pass. No over-broad catch exists. - Bind/don't-bind line drawn per method, with reason at each site. Read
every method of
user.service.ts,admin-seed.service.ts, anduser.controller.ts. All administration paths (findById,create,update,deactivate,delete, both platform-admin methods, all seven controller accesses) run throughtenantPrisma.findByUsernameand the seed-check lookup are the only deliberately unbound paths, each carrying an in-code comment naming the reason (platform-wide uniqueness ofusername) and theresolveEmailForWriteprecedent. findByUsernamecaller count.grep -rn "findByUsername" apps/api/src packagesreturns 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.- Self-delete guard. Confirmed the code now compares
user.id === currentUser.id(not.sub). Independently reverted the comparison back tocurrentUser.suband re-ran the single named test (user.controller.spec.ts, "Test 6: der Riegel gegen das Löschen des eigenen Kontos greift") — it failed withpromise 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 diffclean). The falsification claim holds. - SUPER_ADMIN view.
UserService.findAllForPlatformAdmin/findByIdForPlatformAdminloop overthis.prisma.tenant.findMany()(unbound,Tenantcarries no RLS — confirmed via the scratch check'spg_class.relrowsecuritymeasurement) and issue one boundtenantPrisma.user.*call per tenant inside the loop, matching theensureDefaultGroupsForAllTenants()precedent.UserController.findAllandresolveTargetUseronly route to these methods whencurrentUser.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. - Mid-task deviation (Rule 3).
git show 888f660 -- docs/... klassifikation.mdshows the task-2 correction updated only theStandcolumn (togemischt) 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. - 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, total63— matches the document's "Klassen-Verteilung" table exactly. The "Übersicht je Bereich" row foruser(8 ungebunden / 14 gebunden) matches a freshgrep -ro "this\.prisma\.[a-zA-Z]*"/tenantPrisma\.[a-zA-Z]*\.count. "Der Hintergrunddienst als Falle" section lists five cases (was four), with theadmin-seed.service.tscase correctly described as the first already-correct-on-both-halves case. - Measurements committed, not merely described. Ran
apps/api/scripts/rls-scratch-check.mjsmyself against a freshly resolvedtessera-ctl-db-1IP (172.19.0.2, resolved fresh viadocker inspect, not copied from any document). Output: "Alle 53 Pruefungen bestanden." — 41 prior + 12 new, all twelve nameduser-*checks present and passed, includinguser-eindeutigkeit-greift-trotz-unsichtbarkeit, which explicitly distinguishes SQLSTATE 23505 (uniqueness violation) from 42501 (row-security rejection) in its own message text. - 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. - Constraints held.
git diff --name-only 7e7a697..HEADtouches exactly the 10 files listed infiles_modified— none underapps/api/prisma, no compose file, no env file, nothing underauth/orldap/(confirmed viagit diff --statagainst those directories: empty).docker-compose.ymlstill defaultsDATABASE_URLto thetesserarole (BYPASSRLS, switch off). WINDOWS #22 records the platform-wide uniqueness question asopen, 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 |
Key Link Verification
| 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)