Drei Aufgaben: Tests zuerst (RED 2/16) und Riegel (GREEN 16/16); Falsifizierung
durch Rueckbau, Kopfkommentar adminResetPassword, Gesamt-Gates (1028/62, tsc,
Biome relativ); Ledger #29 fixed plus zwei Nebenbefunde (Biome-Konfiguration im
Bestand nicht lauffaehig, Frontend verschluckt 403 still). Alle Zahlen zur
Planungszeit gemessen, Baseline 1020 Tests in 62 Dateien bei 37a2f73.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
Ein ADMIN kann einen SUPER_ADMIN seines eigenen Mandanten weder aendern (PATCH /users/:id — Kennwort, isActive, Rolle, Anmeldename, E-Mail) noch loeschen (DELETE /users/:id): beide Handler werfen `ForbiddenException`, und `UserService.update` bzw. `UserService.delete` werden NICHT aufgerufen (T-EBG-01, T-EBG-02, T-EBG-03).
Ein SUPER_ADMIN darf einen SUPER_ADMIN weiterhin aendern und loeschen; ein ADMIN darf einen USER und einen ADMIN seines Mandanten weiterhin aendern und loeschen (Regressionsschutz: kein bestehender Weg wird enger als noetig).
Reihenfolge der Pruefungen bleibt: Ziel aufloesen -> 404, Mandantengrenze -> 403 `Cannot modify/delete users from other tenants`, DANN Zielrolle -> 403, DANN (nur update) Rollenzuweisung -> 403. Ein ADMIN aus Mandant A erfaehrt ueber die Fehlermeldung nichts ueber die Rolle eines Benutzers in Mandant B (T-EBG-04) — im Spec durch je einen Ordnungstest gepinnt.
Der Riegel ist FALSIFIZIERT: Rueckbau der beiden neuen Pruefungen im Controller (bei unveraendertem Spec) laesst genau die zwei ADMIN-gegen-SUPER_ADMIN-Verbotstests rot werden (`Tests 2 failed | 14 passed (16)`), die anderen 14 bleiben gruen; danach byte-identisch wiederhergestellt. Der Nachweis steht im SUMMARY, nicht nur in der Commit-Nachricht.
Der Kopfkommentar von `AuthService.adminResetPassword` nennt den Schwesterweg nicht mehr als offen, sondern als durch 260914-ebg (WINDOWS #29) geschlossen; Verhalten von adminResetPassword unveraendert (Kommentar-only).
WINDOWS #29 steht auf `fixed`; die zwei zur Planungszeit gemessenen Nebenbefunde (Biome im Bestand nicht lauffaehig; Frontend verschluckt 403 still) sind als EIGENE Eintraege festgehalten, nicht in #29 mitgeschlossen und nicht verschwiegen.
Baseline am Ende: `Test Files 62 passed (62)` und `Tests 1028 passed (1028)` (Planungszeit-Baseline 1020/62 plus 8 neue Tests), `tsc --noEmit` Exit 0, Biome-Lint (Ersatzkonfiguration, siehe planning_measurements) je Datei 0 Fehler und nicht mehr Warnungen als die Baseline 22/25/20.
apps/api/src/user/user.controller.ts — je ein Zielrollen-Riegel in `update()` (nach der Mandantengrenze, vor der `dto.role`-Pruefung) und `remove()` (nach der Mandantengrenze, vor `userService.delete`), Meldungen `Cannot modify a SUPER_ADMIN user` / `Cannot delete a SUPER_ADMIN user`, Kommentar mit Verweis auf WINDOWS #29 und die Vorlage T-FH9-04
apps/api/src/user/user.controller.spec.ts — neuer describe-Block `update/remove — Zielrolle SUPER_ADMIN (WINDOWS #29)` mit acht Tests (Test 9 bis Test 16), Spec-Gesamtzahl 16
apps/api/src/auth/auth.service.ts — nur der Kopfkommentar ueber `adminResetPassword` (gemessen Zeilen 407-408) fortgeschrieben
.planning/WINDOWS.md — #29 `fixed`, zwei neue Eintraege `quick-260914-ebg` (kind `deviation`): `biome.json` und `apps/web/src/app/(portal)/admin/users/page.tsx`
`resolveTargetUser()` liefert `user.role` mit — der Riegel prueft `user.role === Role.SUPER_ADMIN && currentUser.role !== Role.SUPER_ADMIN`, dieselbe Form wie `AuthService.adminResetPassword` Zeile 426 (`user.role === Role.SUPER_ADMIN && callerRole !== Role.SUPER_ADMIN`)
Mandantengrenze VOR Zielrolle: der Ordnungstest mockt `findById` fuer einen ADMIN aus `t1` mit einem SUPER_ADMIN-Ziel aus `t2` und erwartet woertlich die Mandanten-Meldung — faellt die Reihenfolge, wird der Test rot
`git show HEAD -- apps/api/src/user/user.controller.ts > $SCR/riegel.patch && git apply -R $SCR/riegel.patch` ist der Rueckbau-Hebel fuer die Falsifizierung (Patchdatei im Scratchpad, pruefbar); `git checkout -- apps/api/src/user/user.controller.ts` stellt byte-identisch wieder her (Nachweis: `git status --porcelain apps/api/src/user/user.controller.ts` leer)
WINDOWS #29 schliessen: `UserController.update` (PATCH /users/:id) prueft bisher nur, ob die Rolle SUPER_ADMIN NEU zugewiesen wird (`dto.role === Role.SUPER_ADMIN`, gemessen Zeile 192), nicht ob das ZIEL diese Rolle bereits traegt; `UserController.remove` (DELETE /users/:id, gemessen Zeile 220) prueft nur Selbstloeschung und Mandantengrenze. Ein ADMIN kann damit heute den SUPER_ADMIN seines Mandanten uebernehmen (Kennwort setzen), aussperren (`isActive=false`), herabstufen (`role=USER`) oder loeschen. Dieser Plan zieht in beide Handler den Zielrollen-Riegel ein, dessen Vorlage seit 260911-fh9 in `AuthService.adminResetPassword` steht (T-FH9-04, gemessen Zeile 426), pinnt ihn mit acht Tests im bestehenden Spec, falsifiziert ihn durch Rueckbau, zieht den Kopfkommentar der Vorlage nach und schliesst den Ledger-Eintrag mit Nachweis.
Purpose: Rechteausweitung INNERHALB des Mandanten — der Ledger-Eintrag verlangt die Schliessung „vor dem ersten Mandanten mit einem zweiten Administrator". Kein Mandantenproblem, unabhaengig vom Umstellungsschalter (DATABASE_URL bleibt auf der BYPASSRLS-Rolle, Schema und Migrationen unangetastet, kein Compose, nichts in Active Directory).
Output: zwei Riegel im Controller, acht neue Tests (Spec 8 -> 16, Suite 1020 -> 1028), fortgeschriebener Kopfkommentar in auth.service.ts, WINDOWS #29 fixed plus zwei neue Eintraege fuer die Nebenbefunde, gepusht.
<planning_measurements>
Zur Planungszeit (2026-09-14, HEAD 37a2f73, Arbeitsbaum sauber) gemessen — die Ausfuehrung misst erneut, diese Zahlen sind der Bezugspunkt der Gates:
Gesamte API-Suite: pnpm -C apps/api exec vitest run -> Test Files 62 passed (62), Tests 1020 passed (1020).
Controller-Spec: pnpm -C apps/api exec vitest run src/user/user.controller.spec.ts -> Tests 8 passed (8) (Test 1 bis Test 8, 280 Zeilen).
Zeilen in auth.service.ts: Kopfkommentar 397-409, der fortzuschreibende Satz steht in 407-408 („Der Schwesterweg PATCH /users/:id hat dieselbe Luecke nicht geschlossen — offener Ledger-Eintrag T-FH9-05."), Riegel 426-428, Meldung Cannot reset password of a SUPER_ADMIN user. Die Vorlage-Tests stehen in auth.service.spec.ts 734-746 (Namen: „Aufrufer ADMIN, Ziel SUPER_ADMIN im SELBEN Mandanten: ForbiddenException (T-FH9-04), KEIN Schreibzugriff" und „Aufrufer SUPER_ADMIN, Ziel SUPER_ADMIN: gelingt").
UpdateUserDto = PartialType(CreateUserDto) plus isActive? — alle Felder optional, ein Objektliteral wie { password: 'x' } ist ohne as any zuweisbar. currentUser ist im Controller als any typisiert. Die neuen Tests brauchen deshalb KEIN as any.
Ledger: .planning/WINDOWS.md Frontmatter open_count: 15, waived_count: 1, fixed_count: 18, total_count: 34. #29 hat Status open. Signatur: node /home/vicolab/.claude/gsd-core/bin/gsd-tools.cjs windows fixed <id> bzw. windows append --kind K --phase N --file F --description D (gueltige kinds u. a. deviation, unmet-truth).
Biome ist im Bestand NICHT lauffaehig (Befund, nicht Annahme): pnpm exec biome check <datei> bricht mit „Found an unknown key organizeImports" in biome.json ab (Biome 2.5.0 kennt den Schluessel nur noch unter assist); ausserdem fehlt javascript.parser.unsafeParameterDecoratorsEnabled, ohne den JEDER NestJS-Parameter-Dekorator (@Param, @Body, @CurrentUser) ein Parse-Fehler ist (17 im Controller). Der CI-Schritt „Lint" ruft pnpm lint = turbo lint, und KEINE App hat ein lint-Skript — der Schritt ist ein Leerlauf, der gruen meldet. biome.json liegt ausserhalb der Erlaubnisliste und wird NICHT angefasst; das Gate „Biome sauber" wird deshalb RELATIV und mit einer Ersatzkonfiguration im Scratchpad gemessen. Baseline mit dieser Konfiguration, nur lint: user.controller.ts 0 Fehler / 22 Warnungen / 2 Infos, user.controller.spec.ts 0 / 25 / 0, auth.service.ts 0 / 20 / 1 (durchweg noExplicitAny-Familie). biome format ist im Bestand ebenfalls nicht sauber (Anfuehrungszeichen-Stil) und wird nicht als Gate gefuehrt.
Frontend apps/web/src/app/(portal)/admin/users/page.tsx (handleSubmit ~127-140, handleDelete ~143-156): if (res.ok) {...} ohne else, catch { /* silently fail */ } — ein 403 fuehrt zu KEINER sichtbaren Reaktion. Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung), ausserhalb der Erlaubnisliste, wird NICHT geaendert; der neue Riegel macht den Fall aber fuer einen ADMIN erstmals im Alltag erreichbar (SUPER_ADMIN-Zeile in der eigenen Liste). Deshalb Ledger-Eintrag, kein Frontend-Eingriff.
Detektoren der aktiven Capabilities: api-coverage ueber die Aufgabenbeschreibung -> {"detected":false} (kein externes API, kein SDK); assumption-delta scan -> {"skipped":true,"reason":"phase_unresolved"} (Quick-Task ohne ROADMAP-Abschnitt; inhaltlich keine Einzahl-zu-Mehrzahl-, Pflicht-zu-Optional- oder Ableitung-zu-Wahl-Aenderung); schema-gate -> keine Schemadatei in der Erlaubnisliste (prisma/schema.prisma, Migrationen unangetastet). Alle drei geprueft, keiner ausgeloest.
Konfiguration: workflow.tdd_mode=false (Task 1 traegt trotzdem tdd="true", weil die Tests VOR dem Riegel geschrieben werden koennen und der RED-Lauf zugleich die erste Haelfte der Falsifizierung ist), workflow.security_enforcement=true (ASVS Level 1, Blocking-Schwelle high), workflow.windows_enforce=false.
</planning_measurements>
Task 1: Zielrollen-Riegel in update() und remove() — Tests zuerst (RED), dann Riegel (GREEN)
apps/api/src/user/user.controller.spec.ts, apps/api/src/user/user.controller.ts
Neuer describe-Block `update/remove — Zielrolle SUPER_ADMIN (WINDOWS #29)` am Ende von `describe('UserController')`, Testnamen fortlaufend Test 9 bis Test 16 im Stil des Bestands (deutsch, ausfuehrlich, sagen was bewiesen wird). Aufrufer-Objekte wie im Bestand: `{ role: Role.ADMIN, tenantId: 't1', id: 'admin1' }` bzw. `{ role: Role.SUPER_ADMIN, tenantId: 't1', id: 'super1' }`. Zielbenutzer werden ueber `userService.findById.mockResolvedValue(...)` (ADMIN-Aufrufer) bzw. `userService.findByIdForPlatformAdmin.mockResolvedValue(...)` (SUPER_ADMIN-Aufrufer) gestellt und tragen IMMER `role`. Kein `as any` noetig (siehe planning_measurements).
- Test 9 (update, verboten): ADMIN `t1`, Ziel `{ id: 'boss', tenantId: 't1', role: Role.SUPER_ADMIN }`, `controller.update('boss', { password: 'fresh-password' }, admin)` -> `rejects.toThrow('Cannot modify a SUPER_ADMIN user')` UND `expect(userService.update).not.toHaveBeenCalled()`. Zweiter Aufruf im selben Test mit `{ isActive: false }` und dritter mit `{ role: Role.USER }` — jeweils dieselbe Ablehnung, `update` weiterhin nicht aufgerufen (die drei Angriffsformen aus #29: uebernehmen, aussperren, herabstufen).
- Test 10 (update, SUPER_ADMIN gegen SUPER_ADMIN erlaubt): SUPER_ADMIN-Aufrufer, Ziel `boss` SUPER_ADMIN in `t1` ueber `findByIdForPlatformAdmin`, `userService.update.mockResolvedValue({ id: 'boss', tenantId: 't1', role: Role.SUPER_ADMIN, passwordHash: 'h' })`; Aufruf mit `{ password: 'fresh-password' }` -> `userService.update` mit `('t1', 'boss', expect.objectContaining({ password: 'fresh-password' }))` aufgerufen, Ergebnis ohne `passwordHash`.
- Test 11 (update, ADMIN gegen USER erlaubt — Regressionsschutz): ADMIN `t1`, Ziel `{ id: 'u1', tenantId: 't1', role: Role.USER }`, `update.mockResolvedValue({ id: 'u1', tenantId: 't1', role: Role.USER })`; Aufruf mit `{ displayName: 'Neu' }` -> `userService.update` mit `('t1', 'u1', ...)` aufgerufen.
- Test 12 (update, Ordnung Mandantengrenze VOR Zielrolle): ADMIN `t1`, `findById` liefert (als zweite Schicht, die gebundene Aufloesung liefert im Normalfall null) `{ id: 'boss2', tenantId: 't2', role: Role.SUPER_ADMIN }`; Aufruf -> `rejects.toThrow('Cannot modify users from other tenants')` — woertlich die Mandanten-Meldung, nicht die SUPER_ADMIN-Meldung; `update` nicht aufgerufen.
- Test 13 (remove, verboten): ADMIN `t1`, Ziel `boss` SUPER_ADMIN `t1` -> `controller.remove('boss', admin)` `rejects.toThrow('Cannot delete a SUPER_ADMIN user')`, `expect(userService.delete).not.toHaveBeenCalled()`.
- Test 14 (remove, SUPER_ADMIN gegen SUPER_ADMIN erlaubt): SUPER_ADMIN `super1` loescht `boss` (SUPER_ADMIN, `t1`, ueber `findByIdForPlatformAdmin`) -> `userService.delete` mit `('t1', 'boss')` aufgerufen, Rueckgabe `{ message: 'User deleted' }`.
- Test 15 (remove, ADMIN gegen USER erlaubt — Regressionsschutz): ADMIN `t1` loescht `u1` (USER, `t1`) -> `userService.delete` mit `('t1', 'u1')` aufgerufen.
- Test 16 (remove, Ordnung Mandantengrenze VOR Zielrolle): ADMIN `t1`, `findById` liefert `{ id: 'boss2', tenantId: 't2', role: Role.SUPER_ADMIN }` -> `rejects.toThrow('Cannot delete users from other tenants')`, `delete` nicht aufgerufen.
Schritt A — RED: die acht Tests aus `` in `user.controller.spec.ts` anlegen (Datei einmal lesen, Stil der Tests 1-8 und der Vorlage in `auth.service.spec.ts` 734-746 uebernehmen; `Role` und `ForbiddenException` sind bereits importiert). Dann `pnpm -C apps/api exec vitest run src/user/user.controller.spec.ts` laufen lassen und die Ausgabezeile `Tests 2 failed | 14 passed (16)` im SUMMARY festhalten — genau Test 9 und Test 13 muessen rot sein (die sechs anderen sind Regressions- und Ordnungstests und sind gegen den Bestand bereits gruen). Ist die Zahl eine andere, erst die Tests korrigieren, nicht den Riegel vorziehen.
Schritt B — GREEN in `user.controller.ts`:
1. In `update()` NACH der Mandantengrenze (gemessen 184-189) und VOR der `dto.role`-Pruefung (191-194) einen Block einfuegen: Bedingung `user.role === Role.SUPER_ADMIN && currentUser.role !== Role.SUPER_ADMIN`, wirft `new ForbiddenException('Cannot modify a SUPER_ADMIN user')`. Kommentar davor (deutsch, ASCII-Umschrift, drei bis fuenf Zeilen): WINDOWS #29 / 260914-ebg; die bestehende Pruefung darunter sichert nur die NEUE Zuweisung der obersten Rolle, dieser Riegel sichert das ZIEL, das sie bereits traegt (Kennwort, isActive, Rolle, Anmeldename, E-Mail); Vorlage `AuthService.adminResetPassword` (T-FH9-04); die Mandantengrenze bleibt DAVOR, damit die Meldung nichts ueber die Rolle fremder Benutzer verraet (T-EBG-04).
2. In `remove()` NACH der Mandantengrenze (gemessen 240-245) und VOR `this.userService.delete(...)` denselben Riegel mit `new ForbiddenException('Cannot delete a SUPER_ADMIN user')` und einem kurzen Kommentar (Verweis auf den Block in `update()` und WINDOWS #29). Der Selbstloeschriegel (235-237) bleibt unveraendert an seiner Stelle.
3. Bestehende Kommentare, Reihenfolge und den Schreibzugriff mit `user.tenantId` (Bindung an den Mandanten des ZIELS, 260910-das) nicht anfassen. Keine neue Abhaengigkeit, kein neuer Import.
Dann Spec erneut: `Tests 16 passed (16)`. Commit: `fix(quick-260914-ebg): Zielrollen-Riegel in UserController.update/remove — ADMIN kann SUPER_ADMIN nicht mehr aendern oder loeschen (WINDOWS #29)` mit genau den zwei Dateien dieser Aufgabe.
cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/api exec vitest run src/user/user.controller.spec.ts 2>&1 | grep -E "^\s+Tests" ; grep -c "Cannot modify a SUPER_ADMIN user" apps/api/src/user/user.controller.ts ; grep -c "Cannot delete a SUPER_ADMIN user" apps/api/src/user/user.controller.ts ; grep -c "it('Test 1[0-6]\|it('Test 9" apps/api/src/user/user.controller.spec.ts
Vitest-Zeile lautet woertlich `Tests 16 passed (16)`; beide Meldungs-Greps liefern `1`; der Test-Grep liefert `8`. Der RED-Lauf aus Schritt A ist mit der woertlichen Zeile `Tests 2 failed | 14 passed (16)` und den Namen der zwei roten Tests (Test 9, Test 13) im SUMMARY festgehalten. Commit existiert und enthaelt genau `user.controller.ts` und `user.controller.spec.ts` (`git show --stat HEAD` zeigt 2 Dateien).
Task 2: Falsifizierung durch Rueckbau, Kopfkommentar der Vorlage nachziehen, Gesamt-Gates
apps/api/src/auth/auth.service.ts
Schritt A — Falsifizierung gegen den COMMITTETEN Stand (Rueckbau nur des Controllers, Spec bleibt): `git show HEAD -- apps/api/src/user/user.controller.ts > $SCR/riegel.patch && git apply -R $SCR/riegel.patch` mit `SCR=/tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad` (HEAD ist der Commit aus Task 1; ist inzwischen ein weiterer Commit davor, den Hash des Task-1-Commits statt HEAD einsetzen). Dann `pnpm -C apps/api exec vitest run src/user/user.controller.spec.ts`.
Erwartete Zeile woertlich: `Tests 2 failed | 14 passed (16)` — rot genau Test 9 und Test 13 (Namen aus der Ausgabe ins SUMMARY uebernehmen, Ueberschrift „Nachweis WINDOWS #29 — Rueckbau"). Danach `git checkout -- apps/api/src/user/user.controller.ts` und `git status --porcelain apps/api/src/user/user.controller.ts` muss LEER sein (byte-identisch wiederhergestellt), Spec erneut `Tests 16 passed (16)`. Weicht die Zahl der roten Tests von 2 ab, ist das ein Befund fuer das SUMMARY, kein Grund zum Nachjustieren der Tests.
Schritt B — Kopfkommentar in `auth.service.ts` (gemessen 407-408: der letzte Satz des Kommentars, der den Schwesterweg als noch offen benennt; Wortlaut steht in planning_measurements): durch einen Satz ersetzen, der sagt, dass die Schwesterwege `PATCH /users/:id` und `DELETE /users/:id` seit 260914-ebg (WINDOWS #29) denselben Riegel in `UserController.update()`/`remove()` tragen. Der neue Satz nennt WINDOWS #29 und 260914-ebg; die alte fh9-Ledger-Kennung T-FH9-05 darf danach in der Datei NICHT mehr vorkommen (das Gate greppt darauf, Erwartung 0). Nur Kommentar; kein Code, keine Signatur, keine Meldung in `adminResetPassword` aendern. `auth.service.spec.ts` bleibt unangetastet und muss unveraendert gruen sein.
<!-- planner-discipline-allow: T-FH9-05 -->
Schritt C — Gesamt-Gates (alle vier, Zahlen ins SUMMARY):
1. `pnpm -C apps/api exec vitest run` -> `Test Files 62 passed (62)` und `Tests 1028 passed (1028)`.
2. `pnpm -C apps/api exec tsc --noEmit; echo EXIT=$?` -> `EXIT=0`.
3. Biome relativ, Ersatzkonfiguration (siehe planning_measurements — `biome.json` im Repo bleibt unangetastet): Datei `$SCR/biome-ebg/biome.json` mit `SCR=/tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad` anlegen, falls nicht vorhanden, Inhalt genau: `{ "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", "javascript": { "parser": { "unsafeParameterDecoratorsEnabled": true } }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 }, "linter": { "enabled": true, "rules": { "recommended": true } } }`. Dann je Datei `pnpm exec biome lint --config-path=$SCR/biome-ebg <datei> 2>&1 | grep -E "^Found [0-9]+ (error|warning|info)"` -> keine `error`-Zeile in keiner der drei Dateien; Warnungen `user.controller.ts` <= 22, `user.controller.spec.ts` <= 25, `auth.service.ts` <= 20. Liegt eine Zahl darueber, den Befund beheben (kein neues `any`, kein neuer Import ohne `node:`-Praefix) — nicht die Schwelle anheben.
4. `git diff --stat 37a2f73 -- . ':!.planning'` zeigt genau drei Dateien: `user.controller.ts`, `user.controller.spec.ts`, `auth.service.ts` (Erlaubnisliste eingehalten).
Commit: `docs(quick-260914-ebg): Kopfkommentar adminResetPassword — Schwesterwege PATCH/DELETE /users/:id geschlossen (WINDOWS #29)`.
cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/api exec vitest run 2>&1 | grep -E "^\s+(Test Files|Tests)" ; pnpm -C apps/api exec tsc --noEmit; echo EXIT=$? ; grep -c "T-FH9-05" apps/api/src/auth/auth.service.ts ; grep -c "260914-ebg" apps/api/src/auth/auth.service.ts ; D=$(git diff --stat 37a2f73 -- . ':!.planning'); echo GIT_EXIT=$? ; tail -n1 <<< "$D"
Ausgabe enthaelt woertlich `Test Files 62 passed (62)` und `Tests 1028 passed (1028)`, `EXIT=0` und `GIT_EXIT=0`; `grep -c "T-FH9-05"` liefert `0` und `grep -c "260914-ebg"` liefert `1` in `auth.service.ts`; die `git diff --stat`-Summenzeile nennt `3 files changed`.
Das SUMMARY traegt unter „Nachweis WINDOWS #29 — Rueckbau" die Zeile `Tests 2 failed | 14 passed (16)` mit den Namen von Test 9 und Test 13 sowie die leere `git status --porcelain`-Ausgabe nach der Wiederherstellung; dazu die drei Biome-Zahlentripel je Datei.
Task 3: Ledger — #29 schliessen, zwei Nebenbefunde eintragen, pushen
.planning/WINDOWS.md
Alle Aufrufe ueber `node /home/vicolab/.claude/gsd-core/bin/gsd-tools.cjs windows ...` aus `/home/vicolab/projects/tessera-ctl`; WINDOWS.md NICHT von Hand editieren (das Werkzeug pflegt Tabelle, JSON-Block und Frontmatter-Zaehler gemeinsam).
1. `windows fixed 29`.
2. `windows append --kind deviation --phase quick-260914-ebg --file biome.json --description ""` — Text (ASCII, ein Absatz, keine Zeilenumbrueche): Biome ist im Bestand nicht lauffaehig: `biome.json` traegt den in Biome 2.5.0 unbekannten Schluessel `organizeImports` (gehoert unter `assist`), Biome bricht bei jedem Aufruf mit Konfigurationsfehler ab; zusaetzlich fehlt `javascript.parser.unsafeParameterDecoratorsEnabled`, ohne den jeder NestJS-Parameter-Dekorator ein Parse-Fehler ist (17 allein in user.controller.ts). Der CI-Schritt Lint ruft `pnpm lint` = `turbo lint`, keine App hat ein lint-Skript — der Schritt ist ein Leerlauf, der gruen meldet. CLAUDE.md und docs/anleitung-entwicklung.md beschreiben Biome als aktives Werkzeug. Gemessen 260914-ebg; das dortige Gate lief mit einer Ersatzkonfiguration im Scratchpad, relativ zur Baseline (0 Fehler, Warnungen je Datei 22/25/20, alle noExplicitAny-Familie; biome format ebenfalls unsauber, Anfuehrungszeichen-Stil). Zu entscheiden: biome.json reparieren (organizeImports nach assist, Parser-Schalter, quoteStyle single) und ein lint-Skript je App anlegen, dann die Warnungen in einem eigenen Durchlauf abbauen oder als Regelabschaltung begruenden.
3. `windows append --kind deviation --phase quick-260914-ebg --file "apps/web/src/app/(portal)/admin/users/page.tsx" --description ""` — Text: handleSubmit und handleDelete pruefen nur `res.ok` ohne else-Zweig und fangen mit leerem catch — ein 403 der API fuehrt zu keiner sichtbaren Reaktion (Formular bleibt offen, Loeschdialog bleibt stehen, keine Meldung). Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung); seit 260914-ebg (WINDOWS #29) ist der Fall fuer einen ADMIN im Alltag erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Familie der still verschluckten Antworten (#28, #32). Frontend von 260914-ebg NICHT geaendert (ausserhalb der Erlaubnisliste). Zu schliessen: Fehlermeldung aus dem Antwortrumpf anzeigen und die Aktionsknoepfe fuer SUPER_ADMIN-Zeilen einem ADMIN gar nicht erst anbieten.
4. Frontmatter pruefen: `open_count: 16`, `waived_count: 1`, `fixed_count: 19`, `total_count: 36`; Zeile `| 29 |` traegt `| fixed |` und ein `resolved_at`; die neuen Zeilen haben die IDs 35 und 36.
5. Commit: `docs(quick-260914-ebg): Ledger — WINDOWS #29 fixed, Nebenbefunde Biome-Konfiguration und stilles 403 im Frontend eingetragen` (nur `.planning/WINDOWS.md`). Danach `git push` (schlichter Aufruf, die Push-URL zeigt auf localhost:3002); `S=$(git status -sb); echo GIT_EXIT=$?; head -n1 <<< "$S"` muss `GIT_EXIT=0` liefern und darf kein `[ahead` mehr zeigen. Wird das SUMMARY erst nach diesem Schritt committet, den Push danach wiederholen.
cd /home/vicolab/projects/tessera-ctl && grep -E "^(open_count|waived_count|fixed_count|total_count):" .planning/WINDOWS.md ; grep -cE "^\| 29 \|.*\| fixed \|" .planning/WINDOWS.md ; grep -cE "^\| 3[56] \| quick-260914-ebg \| deviation \|" .planning/WINDOWS.md ; S=$(git status -sb); echo GIT_EXIT=$? ; head -n1 <<< "$S"
Frontmatter zeigt woertlich `open_count: 16`, `waived_count: 1`, `fixed_count: 19`, `total_count: 36`; der #29-Grep liefert `1`; der Grep auf die neuen Eintraege liefert `2`; `GIT_EXIT=0` und die Status-Zeile enthaelt kein `[ahead`.
<threat_model>
Trust Boundaries
Boundary
Description
Sitzungsnachweis (JWT) -> UserController
Rolle und Mandant des Aufrufers kommen aus JwtStrategy.validate() ({ id, username, role, tenantId }), der Rumpf (UpdateUserDto) und die Pfadkennung sind vom Aufrufer gewaehlt
UserController.update — dto.password gegen SUPER_ADMIN-Ziel (Kontouebernahme)
critical
mitigate
Zielrollen-Riegel vor dem Schreibzugriff (Task 1), gepinnt durch Test 9; Falsifizierung durch Rueckbau (Task 2)
T-EBG-02
Denial of Service / Elevation of Privilege
UserController.update — dto.isActive=false (Aussperren) und dto.role=USER (Herabstufen) gegen SUPER_ADMIN-Ziel
high
mitigate
Derselbe Riegel deckt alle DTO-Felder, Test 9 ruft die drei Formen einzeln ab
T-EBG-03
Elevation of Privilege
UserController.remove — ADMIN loescht SUPER_ADMIN des Mandanten
high
mitigate
Zielrollen-Riegel vor userService.delete (Task 1), gepinnt durch Test 13
T-EBG-04
Information Disclosure
Reihenfolge der Ablehnungen in update/remove — Meldung koennte die Rolle eines fremdmandantigen Benutzers verraten
medium
mitigate
Mandantengrenze bleibt VOR der Zielrollen-Pruefung; Ordnungstests 12 und 16 erwarten woertlich die Mandanten-Meldung
T-EBG-05
Repudiation
Abgewiesener Uebernahmeversuch wird nicht protokolliert (Controller hat keinen Logger, bestehende 403-Wege ebenso still)
low
accept
Gleichbehandlung mit den vorhandenen Ablehnungen; ein Audit-Protokoll fuer Verwaltungsaktionen waere ein eigener Durchlauf, nicht Teil dieser Erlaubnisliste
T-EBG-06
Tampering
Frontend verschluckt das neue 403 still — kein Sicherheitsverlust, aber der Verwalter sieht nicht, dass die Aktion verweigert wurde
low
accept
Als WINDOWS-Eintrag festgehalten (Task 3), Frontend ausserhalb der Erlaubnisliste
T-EBG-SC
Tampering
npm/pip/cargo installs
low
accept
Dieser Plan installiert KEIN Paket (kein Install-Task, kein neuer Import); Paketlegitimitaets-Gate nicht ausgeloest
</threat_model>
Nach Task 3, alles aus `/home/vicolab/projects/tessera-ctl`:
- `pnpm -C apps/api exec vitest run 2>&1 | grep -E "^\s+(Test Files|Tests)"` -> `Test Files 62 passed (62)` / `Tests 1028 passed (1028)`
- `pnpm -C apps/api exec tsc --noEmit; echo EXIT=$?` -> `EXIT=0`
- `D=$(git diff --stat 37a2f73 -- . ':!.planning'); echo GIT_EXIT=$?; tail -n1 <<< "$D"` -> `GIT_EXIT=0` und `3 files changed`
- `grep -cE "^\| 29 \|.*\| fixed \|" .planning/WINDOWS.md` -> `1`
- SUMMARY enthaelt den Abschnitt „Nachweis WINDOWS #29 — Rueckbau" mit `Tests 2 failed | 14 passed (16)` (RED-Lauf aus Task 1 UND Rueckbau-Lauf aus Task 2), die drei Biome-Zahlentripel und die vier Gate-Ausgaben.
- Nicht angefasst (Stichprobe): `git diff --stat 37a2f73 -- apps/api/prisma docker-compose*.yml biome.json apps/web` -> leer.
<success_criteria>
Ein ADMIN bekommt auf PATCH und DELETE gegen einen SUPER_ADMIN des eigenen Mandanten ForbiddenException, der Dienst wird nicht aufgerufen; SUPER_ADMIN gegen SUPER_ADMIN und ADMIN gegen USER/ADMIN bleiben erlaubt; Mandantengrenze bleibt vor der Zielrollen-Pruefung.
Spec 16 Tests, Suite 1028 Tests in 62 Dateien, Typpruefung Exit 0, Biome relativ ohne neue Fehler und ohne zusaetzliche Warnungen.
Rueckbau des Riegels macht genau zwei Tests rot — im SUMMARY belegt, byte-identisch wiederhergestellt.
auth.service.ts nennt T-FH9-05 nicht mehr als offen; WINDOWS #29 fixed, #35 und #36 als neue Befunde; drei Commits mit Scope quick-260914-ebg, gepusht.
</success_criteria>
Create `/home/vicolab/projects/tessera-ctl/.planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-SUMMARY.md` when done — auf Deutsch, mit den Ueberschriften „Nachweis WINDOWS #29 — Rueckbau" (beide Falsifizierungslaeufe woertlich), „Gates" (vier Ausgaben plus drei Biome-Tripel) und „Nebenbefunde" (#35 Biome, #36 Frontend, je ein Satz mit Verweis auf den Ledger).