--- phase: quick-260914-ebg plan: 01 subsystem: auth tags: [nestjs, prisma, rbac, vitest, biome] requires: - phase: quick-260911-fh9 provides: "Zielrollen-Riegel-Vorlage in AuthService.adminResetPassword (T-FH9-04) — dieselbe Bedingung (user.role === SUPER_ADMIN && callerRole !== SUPER_ADMIN) wird hier in UserController.update()/remove() uebernommen" provides: - "Zielrollen-Riegel in UserController.update() und UserController.remove(): ein ADMIN kann den SUPER_ADMIN seines eigenen Mandanten weder aendern noch loeschen" - "acht neue Tests (Test 9-16) in user.controller.spec.ts, Spec-Gesamtzahl 8 -> 16" - "WINDOWS #29 geschlossen (fixed); zwei neue Ledger-Eintraege #35 (Biome-Konfiguration defekt) und #36 (Frontend verschluckt 403 still)" affects: [user-management, auth, windows-ledger] actuals: tokens: 6669 tasks: 3 commits: 3 plan_head_before: e76c0f8a3371d6ac210bce5e1d2ce0fe1d372e8c tech-stack: added: [] patterns: - "Zielrollen-Riegel-Muster: Mandantengrenze IMMER vor Zielrollen-Pruefung, damit die Fehlermeldung nichts ueber die Rolle eines fremdmandantigen Benutzers verraet (jetzt an zwei Stellen: AuthService.adminResetPassword, UserController.update/remove)" key-files: created: [] modified: - apps/api/src/user/user.controller.ts - apps/api/src/user/user.controller.spec.ts - apps/api/src/auth/auth.service.ts - .planning/WINDOWS.md key-decisions: - "Sechs ueberfluessige `as any`-Umschreibungen in den neuen Tests entfernt (Rule 1) — UpdateUserDto ist vollstaendig optional, die Objektliteral-Zuweisung ist ohne Umschreibung typkorrekt, und die Umschreibungen trieben die Biome-Warnungen der Spec-Datei ueber die Baseline (25)" requirements-completed: [WINDOWS-29, T-FH9-05] coverage: - id: D1 description: "ADMIN kann SUPER_ADMIN des eigenen Mandanten weder per PATCH aendern (Kennwort, isActive, Rolle) noch per DELETE loeschen — ForbiddenException, Dienst nicht aufgerufen" requirement: "WINDOWS-29" verification: - kind: unit ref: "apps/api/src/user/user.controller.spec.ts#Test 9: ein Mandanten-Administrator kann den SUPER_ADMIN des eigenen Mandanten weder übernehmen..." status: pass - kind: unit ref: "apps/api/src/user/user.controller.spec.ts#Test 13: ein Mandanten-Administrator kann den SUPER_ADMIN des eigenen Mandanten nicht löschen..." status: pass human_judgment: false - id: D2 description: "Regressionsschutz: SUPER_ADMIN gegen SUPER_ADMIN und ADMIN gegen USER bleiben auf beiden Wegen erlaubt" verification: - kind: unit ref: "apps/api/src/user/user.controller.spec.ts#Test 10, Test 11, Test 14, Test 15" status: pass human_judgment: false - id: D3 description: "Reihenfolge: Mandantengrenze VOR Zielrollen-Pruefung in beiden Handlern — die Mandanten-Meldung verraet nichts ueber die Rolle eines fremdmandantigen Benutzers" requirement: "T-EBG-04" verification: - kind: unit ref: "apps/api/src/user/user.controller.spec.ts#Test 12, Test 16" status: pass human_judgment: false - id: D4 description: "Falsifizierung: Rueckbau des Riegels macht genau Test 9 und Test 13 rot, byte-identisch wiederhergestellt" verification: - kind: other ref: "git apply -R Rueckbau-Lauf, siehe Abschnitt Nachweis WINDOWS #29 — Rueckbau unten" status: pass human_judgment: false - id: D5 description: "Kopfkommentar AuthService.adminResetPassword nennt T-FH9-05 nicht mehr als offen" requirement: "T-FH9-05" verification: - kind: other ref: "grep -c T-FH9-05 apps/api/src/auth/auth.service.ts -> 0" status: pass human_judgment: false - id: D6 description: "WINDOWS-Ledger: #29 fixed, #35 und #36 als eigene Nebenbefunde eingetragen, nicht mitgeschlossen" verification: - kind: other ref: "node gsd-tools.cjs windows fixed 29; windows append (2x); Frontmatter-Gegenprobe" status: pass human_judgment: false duration: 6min completed: 2026-09-14 status: complete --- # Quick 260914-ebg: Zielrollen-Riegel in UserController.update/remove Summary **Zielrollen-Riegel (Vorlage aus AuthService.adminResetPassword, T-FH9-04) in UserController.update() und remove() eingezogen — ein ADMIN kann den SUPER_ADMIN seines Mandanten nicht mehr uebernehmen, aussperren, herabstufen oder loeschen; acht neue Tests, Falsifizierung durch Rueckbau bestanden, WINDOWS #29 geschlossen.** ## Performance - **Duration:** 6 min - **Started:** 2026-09-14T10:33:00+02:00 (Baseline-Lauf vor Task 1) - **Completed:** 2026-09-14T10:38:29+02:00 (Push) - **Tasks:** 3 - **Files modified:** 4 ## Accomplishments - Zielrollen-Riegel in `UserController.update()` (nach der Mandantengrenze, vor der `dto.role`-Pruefung) und `UserController.remove()` (nach der Mandantengrenze, vor `userService.delete`), jeweils mit `ForbiddenException` und Kommentar, der auf WINDOWS #29 und die Vorlage T-FH9-04 verweist - Acht neue Tests (Test 9-16) in `user.controller.spec.ts`: drei Angriffsformen gegen den SUPER_ADMIN (Kennwort, isActive, Rolle), zwei Regressionstests (SUPER_ADMIN gegen SUPER_ADMIN, ADMIN gegen USER) je Handler, zwei Ordnungstests (Mandantengrenze vor Zielrolle) - Falsifizierung: Rueckbau des Task-1-Commits macht genau Test 9 und Test 13 rot, danach byte-identisch wiederhergestellt - Kopfkommentar `AuthService.adminResetPassword` fortgeschrieben — die alte Ledger-Kennung T-FH9-05 kommt in der Datei nicht mehr vor - WINDOWS #29 auf `fixed` gesetzt; zwei neue, eigenstaendige Nebenbefunde #35 (Biome-Konfiguration defekt) und #36 (Frontend verschluckt 403 still) eingetragen, nicht mit #29 mitgeschlossen ## Task Commits Alle drei Aufgaben wurden einzeln committet: 1. **Task 1: Zielrollen-Riegel in update() und remove() — Tests zuerst (RED), dann Riegel (GREEN)** - `759ea3b` (fix) 2. **Task 2: Falsifizierung durch Rueckbau, Kopfkommentar der Vorlage nachziehen, Gesamt-Gates** - `63f9df0` (docs) 3. **Task 3: Ledger — #29 schliessen, zwei Nebenbefunde eintragen, pushen** - `70d007b` (docs) _Task 1 ist TDD: Tests wurden vor dem Riegel geschrieben (RED), dann der Riegel eingezogen (GREEN) — beides im selben Commit, da RED und GREEN Teil derselben Aufgabe und desselben Nachweises sind._ ## Nachweis WINDOWS #29 — Rueckbau **RED-Lauf (Task 1, Schritt A — vor dem Riegel, Tests bereits vorhanden):** ``` Test Files 1 failed (1) Tests 2 failed | 14 passed (16) ``` Rot: `Test 9: ein Mandanten-Administrator kann den SUPER_ADMIN des eigenen Mandanten weder übernehmen (Kennwort setzen), noch aussperren (isActive=false), noch herabstufen (role=USER) — alle drei Angriffsformen werden mit der Zielrollen-Ausnahme abgelehnt, und der Dienst wird in keinem der drei Fälle aufgerufen` (Fehler: `Cannot destructure property 'passwordHash' of 'updated' as it is undefined` statt der erwarteten `Cannot modify a SUPER_ADMIN user`) und `Test 13: ein Mandanten-Administrator kann den SUPER_ADMIN des eigenen Mandanten nicht löschen — die Zielrollen-Ausnahme greift, und der Dienst wird nicht aufgerufen` (Promise loeste mit `{ message: 'User deleted' }` auf statt abzulehnen). Alle anderen 14 Tests gruen. Nach dem Riegel (GREEN): `Tests 16 passed (16)`. **Falsifizierungs-Rueckbau (Task 2, Schritt A — gegen den committeten Stand):** ```bash git show HEAD -- apps/api/src/user/user.controller.ts > $SCR/riegel.patch && git apply -R $SCR/riegel.patch pnpm -C apps/api exec vitest run src/user/user.controller.spec.ts ``` Ergebnis, woertlich: ``` Test Files 1 failed (1) Tests 2 failed | 14 passed (16) ``` Rot exakt dieselben zwei: `Test 9` (`AssertionError: expected [Function] to throw error including 'Cannot modify a SUPER_ADMIN user' but got 'Cannot destructure property \'passwor…'`) und `Test 13` (`AssertionError: promise resolved "{ message: 'User deleted' }" instead of rejecting`). Die anderen 14 Tests blieben gruen — der Riegel ist damit als notwendig fuer genau diese zwei Verhaltensnachweise belegt. **Wiederherstellung:** ```bash git checkout -- apps/api/src/user/user.controller.ts git status --porcelain apps/api/src/user/user.controller.ts ``` Ausgabe der zweiten Zeile: leer (byte-identisch wiederhergestellt). Spec danach erneut `Tests 16 passed (16)`. ## Gates 1. `pnpm -C apps/api exec vitest run` -> `Test Files 62 passed (62)` / `Tests 1028 passed (1028)` (Baseline 1020/62 plus 8 neue Tests) 2. `pnpm -C apps/api exec tsc --noEmit; echo EXIT=$?` -> `EXIT=0` 3. `D=$(git diff --stat 37a2f73 -- . ':!.planning'); echo GIT_EXIT=$?; tail -n1 <<< "$D"` -> `GIT_EXIT=0`, `3 files changed, 136 insertions(+), 2 deletions(-)` 4. `grep -cE "^\| 29 \|.*\| fixed \|" .planning/WINDOWS.md` -> `1` **Biome-Zahlentripel (Ersatzkonfiguration im Scratchpad, relativ zur Baseline — `biome.json` im Repo unangetastet):** | Datei | Fehler | Warnungen | Infos | Baseline-Warnungen | |-------|--------|-----------|-------|---------------------| | `apps/api/src/user/user.controller.ts` | 0 | 22 | 2 | 22 | | `apps/api/src/user/user.controller.spec.ts` | 0 | 25 | 0 | 25 | | `apps/api/src/auth/auth.service.ts` | 0 | 20 | 1 | 20 | Alle drei Dateien treffen die Baseline exakt (nach dem Rule-1-Nebenfund unten). Kein neues `any`, kein neuer Import. **git status --porcelain nach Task 3 (Arbeitsbaum sauber):** ``` (leer) ``` **git log e76c0f8..HEAD:** ``` 70d007b docs(quick-260914-ebg): Ledger — WINDOWS #29 fixed, Nebenbefunde Biome-Konfiguration und stilles 403 im Frontend eingetragen 63f9df0 docs(quick-260914-ebg): Kopfkommentar adminResetPassword — Schwesterwege PATCH/DELETE /users/:id geschlossen (WINDOWS #29) 759ea3b fix(quick-260914-ebg): Zielrollen-Riegel in UserController.update/remove — ADMIN kann SUPER_ADMIN nicht mehr aendern oder loeschen (WINDOWS #29) ``` ## Files Created/Modified - `apps/api/src/user/user.controller.ts` - Zielrollen-Riegel in `update()` und `remove()`, je ein Kommentarblock mit Verweis auf WINDOWS #29 und T-FH9-04 - `apps/api/src/user/user.controller.spec.ts` - neuer describe-Block mit acht Tests (Test 9-16), Spec-Gesamtzahl 16 - `apps/api/src/auth/auth.service.ts` - Kopfkommentar `adminResetPassword` fortgeschrieben (T-FH9-05 nicht mehr offen) - `.planning/WINDOWS.md` - #29 `fixed`, #35 und #36 neu (`quick-260914-ebg`, `deviation`) ## Decisions Made - Zielrollen-Riegel als eigenstaendige `if`-Pruefung nach der Mandantengrenze eingezogen, nicht als Erweiterung der bestehenden `dto.role`-Pruefung — die bestehende Pruefung sichert die NEUE Rollenzuweisung, der neue Riegel sichert das bereits vorhandene ZIEL; beide bleiben unabhaengig lesbar - Mandantengrenze bewusst VOR der Zielrollen-Pruefung belassen (nicht umgestellt), damit ein Ordnungsfehler durch die Ordnungstests (Test 12, Test 16) sofort rot wird ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Sechs ueberfluessige `as any`-Umschreibungen in den neuen Tests entfernt** - **Found during:** Task 2, Schritt C.3 (Biome-Gate) - **Issue:** Die acht neuen Tests in `user.controller.spec.ts` trugen sechs `as any`-Umschreibungen bei den `UpdateUserDto`-Objektliteralen (`{ password: '...' } as any` usw.). Der Plan hatte in `planning_measurements` bereits festgehalten, dass `UpdateUserDto` vollstaendig optional ist und die Objektliterale ohne Umschreibung zuweisbar sind — die Umschreibungen waren unnoetig und trieben die Biome-Warnungen von `user.controller.spec.ts` von der Baseline 25 auf 31 (relative Ersatzkonfiguration, `noExplicitAny`-Familie). - **Fix:** Alle sechs `as any` an den betroffenen Aufrufstellen entfernt (Test 9, Test 10, Test 11, Test 12). - **Files modified:** `apps/api/src/user/user.controller.spec.ts` - **Verification:** `pnpm -C apps/api exec vitest run src/user/user.controller.spec.ts` weiterhin `Tests 16 passed (16)`; Biome-Gate danach `Found 25 warnings.` (Baseline exakt getroffen) - **Committed in:** `63f9df0` (Task 2 Commit, zusammen mit dem Kopfkommentar in `auth.service.ts`, da beide Aenderungen aus demselben Gate-Durchlauf stammen) --- **Total deviations:** 1 auto-fixed (Rule 1) **Impact on plan:** Reine Aufraeumarbeit an eigenem, in Task 1 neu geschriebenem Testcode — kein Scope-Creep, keine Verhaltensaenderung, Biome-Schwelle nicht angehoben, sondern die Baseline exakt wiederhergestellt. ## Nebenbefunde **#35 (Biome-Konfiguration defekt, `biome.json`):** Biome ist im Bestand nicht lauffaehig — `biome.json` traegt den in Biome 2.5.0 unbekannten Schluessel `organizeImports` (gehoert unter `assist`), und es fehlt `javascript.parser.unsafeParameterDecoratorsEnabled`, ohne den jeder NestJS-Parameter-Dekorator ein Parse-Fehler ist. Der CI-Schritt „Lint" ruft `pnpm lint` = `turbo lint`, doch keine App hat ein `lint`-Skript — der Schritt ist ein Leerlauf, der gruen meldet. Als eigener Ledger-Eintrag festgehalten (nicht in #29 mitgeschlossen), `biome.json` liegt ausserhalb der Erlaubnisliste dieses Plans. **#36 (Frontend verschluckt 403 still, `apps/web/.../admin/users/page.tsx`):** `handleSubmit` und `handleDelete` pruefen nur `res.ok` ohne `else`-Zweig und fangen mit leerem `catch` — ein 403 fuehrt zu keiner sichtbaren Reaktion. Bestehendes Verhalten fuer alle 403-Wege, aber seit diesem Plan (WINDOWS #29) fuer einen ADMIN im Alltag erstmals erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Als eigener Ledger-Eintrag festgehalten, Frontend von diesem Plan nicht geaendert (ausserhalb der Erlaubnisliste). ## Issues Encountered None - die Umsetzung folgte dem Plan, bis auf den in „Deviations from Plan" dokumentierten Rule-1-Nebenfund. ## User Setup Required None - keine externe Konfiguration erforderlich. ## Next Phase Readiness - WINDOWS #29 geschlossen, Rechteausweitung ADMIN gegen SUPER_ADMIN in beiden Schwesterwegen (`adminResetPassword`, `UserController.update/remove`) geschlossen - Zwei Nebenbefunde (#35 Biome, #36 Frontend-403) offen und im Ledger sichtbar — kein Blocker fuer diesen Plan, aber vor dem naechsten Milestone-Abschluss zu pruefen - Kein laufender Milestone begonnen; v1.2 bleibt abgeschlossen (siehe STATE.md) --- *Phase: quick-260914-ebg* *Completed: 2026-09-14* ## Self-Check: PASSED Alle vier geaenderten Dateien und die SUMMARY-Datei selbst gefunden; alle drei Task-Commits (`759ea3b`, `63f9df0`, `70d007b`) in der Historie gefunden.