202 lines
15 KiB
Markdown
202 lines
15 KiB
Markdown
---
|
||
phase: quick-260910-das
|
||
verified: 2026-09-10T08:43:00Z
|
||
status: passed
|
||
score: 10/10 must-haves verified
|
||
covered_files:
|
||
- .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
|
||
covered_digest: "v1:sha256:57beb919b493cdad90d7e21464d014838272020b4459348b970c7c9f0fec2bed"
|
||
behavior_unverified: 0
|
||
overrides_applied: 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 |
|
||
|
||
### 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)*
|