diff --git a/.planning/STATE.md b/.planning/STATE.md index 0927379..efaae29 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,14 +4,14 @@ milestone: v1.2 current_phase: 17 current_phase_name: eigene-ausschreibungs-quellen-je-nutzer status: verified -stopped_at: "Quick 260911-nke abgeschlossen: Etappe 3b Benutzerdimension — Migration 20260911120000, forTenant() mit userId, 34 Aufrufstellen, 203/203 Werkzeugpruefungen, sechs Loch-Pruefungen umgedreht, gepusht" -last_updated: "2026-09-14T08:18:04.000Z" -last_activity: 2026-09-10 +stopped_at: "Quick 260914-ebg abgeschlossen: WINDOWS #29 geschlossen — Zielrollen-Riegel in UserController.update/remove, 16 Spec-Tests, Falsifizierung bestanden, gepusht" +last_updated: "2026-09-14T08:41:17.656Z" +last_activity: 2026-09-11 last_activity_desc: Quick 260910-jab — drei zu kurz greifende RLS-Regeln geschlossen (GroupMembership beide Seiten, ModuleGrant beide Ziele, TenderRssFeedSource Lese-/Schreibsplit), listForUser gebunden, Aktenstand kohaerent -state_head: b62a905adb19f8c68eba45e66f2290ed978c2239 +state_head: 70d007bb47a2e39db54da47297dda162044dc79e progress: total_phases: 17 - completed_phases: 3 + completed_phases: 15 total_plans: 83 completed_plans: 82 milestone_name: Plattform-Berechtigungen @@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17) Phase: 17 (eigene-ausschreibungs-quellen-je-nutzer) — VERIFIED / passed Plan: 3 of 3 Status: Phase abgeschlossen und im Browser gegengeprueft — bereit fuer /gsd-ship -Last activity: 2026-09-11 - Etappe 3b Benutzerdimension abgeschlossen (260911-nke, verifiziert 13/13); 3a und 3c offen, Auftrag in docs/mandantentrennung-etappe3-auftrag.md +Last activity: 2026-09-14 - WINDOWS #29 geschlossen (260914-ebg, verifiziert 6/6); als naechstes Etappe 3c (Systemkontext), dann 3a, Etappe 4 nur nach Rueckfrage Progress: [██████████] 100% @@ -121,6 +121,7 @@ Progress: [██████████] 100% | Phase quick-260910-krx P01 | 26min | 3 tasks | 7 files | | Phase quick-260911-cwh P01 | 21min | 3 tasks | 7 files | | Phase quick-260911-nke P01 | 1 Sitzung | 3 tasks | 27 files | +| Phase quick-260914-ebg P01 | 6min | 3 tasks | 4 files | ## Accumulated Context @@ -301,6 +302,7 @@ Recent decisions affecting current work: - [Phase 17]: 260910-krx: Bereich dashboard vollstaendig umgestellt — 12 gebunden, 1 begruendet ungebunden (Modulkatalog); getLayout/saveLayout gemeinsam gebunden; saveLayout uebersetzt PrismaClientUnknownRequestError (nicht P2002) in deutsche Konfliktmeldung; WINDOWS #25 fuer die beweisvernichtende Schleife offen angelegt - [Phase 17]: [quick-260911-cwh]: Bereich calendar Etappe 2 gebunden — Cache-Schluessel bleibt ohne Mandantenanteil (User.id ist plattformweit eindeutige UUID, Etappe-3-Entscheidung (1) betrifft nur username/email); keine neue Fehleruebersetzung fuer Besitzpruefungen noetig (Wettlauf-Fall wirft P2025, strukturell unerreichbar); refreshCacheInBackground zaehlt nicht als sechster Hintergrunddienst-Fall - [Phase 17]: 260911-nke: forTenant(prisma, tenantId, userId?) — optionaler dritter Parameter statt Schwesterhelfer, IS-NULL-OR-Form in den Regeln der zehn persoenlichen Tabellen, sechs Loch-Pruefungen umgedreht +- [Phase 17]: [quick-260914-ebg]: Zielrollen-Riegel als eigenstaendige Pruefung nach der Mandantengrenze in UserController.update()/remove() eingezogen (Vorlage AuthService.adminResetPassword, T-FH9-04) — WINDOWS #29 geschlossen ### Pitfalls & Anti-Patterns @@ -396,6 +398,7 @@ None yet. | 260911-nke | **Etappe 3b — Benutzerdimension in den Datenbankregeln.** Migration `20260911120000_rls_user_dimension_personal_tables`: `current_user_id()` (liest `app.current_user`, `NULLIF` fuer den Leerstring), `forTenant(prisma, tenantId, userId?)` mit optionalem drittem Parameter (kein Schwesterhelfer — der Inventar-Detektor haette ihn nicht gesehen), beide `set_config` in EINER Anweisung, `$transaction` behaelt zwei Eintraege. Regeln der ZEHN persoenlichen Tabellen in der Form `tenantId = current_tenant_id() AND (current_user_id() IS NULL OR userId = current_user_id())` — ein Aufruf ohne Benutzer (Admin, Hintergrunddienst) sieht weiter den ganzen Mandanten. `SearchProvider`/`TenderRssFeedSource` mit vier befehlsgetrennten Regeln (jab-Praezedenz), Mandantenhaelften unveraendert; GroupMembership/ModuleGrant/PasswordResetToken/TenderMatch bewusst ohne Benutzerdimension (Verwaltungs-/Anmelde-/Hintergrundobjekte). 34 Nutzer-CRUD-Aufrufstellen in 8 Diensten reichen den Benutzer durch, Scheduler und Verwaltungswege bleiben zweistellig. **SECHS loch-behauptende Pruefungen statt drei** — und die Umkehrung war nicht trivial: die alten massen OHNE Benutzer, eine naive Umkehrung waere nach der Migration rot geworden, weil der Aufruf ohne Benutzer per Absicht beide sieht; jede wurde zu ZWEI (alte Messung unter neuem Namen als gewollte Eigenschaft, Umkehrung MIT Benutzer). 13 Extraktionsstellen im Werkzeug auf die neue Migration umgeleitet. **Wirkungslos mit ausgeschaltetem Schalter** (Rolle `tessera` hat BYPASSRLS, live bestaetigt) — blockiert das Live-Gehen am Dienstag nicht. Angenommene offene Flanke, festgehalten statt verschwiegen: ein Aufrufer, der den Benutzer vergisst, sieht den ganzen Mandanten (heutiger Stand, keine Verschlechterung) — WINDOWS #34; die dreistelligen Spec-Zusicherungen sind je Datei, nicht je Methode, das Gate 'keine zweistellige Form' ist ein Shell-Check, nicht CI — vom Verifizierer als Bewusstseinspunkt vermerkt. **Verifiziert 13/13** (1020/1020 Tests, Typpruefung sauber, 203/203 Live-Pruefungen; `NULLIF` durch Rueckbau falsifiziert, 33 Pruefungen rot; alle zehn Regeln live in `pg_policies` gelesen) | 2026-09-11 | f0b531b,07fc653,b62a905 | [260911-nke-mandantentrennung-etappe-3b-benutzerdime](./quick/260911-nke-mandantentrennung-etappe-3b-benutzerdime/) | | 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) | | 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) | +| 260914-ebg | **WINDOWS #29 geschlossen — Zielrollen-Riegel in `UserController.update()`/`remove()`.** Ein ADMIN kann den SUPER_ADMIN seines Mandanten nicht mehr aendern (Kennwort, isActive, Rolle, Anmeldename, E-Mail) oder loeschen; Riegel nach der Mandantengrenze, vor der Rollenzuweisungs-Pruefung (Vorlage `AuthService.adminResetPassword`, T-FH9-04). Acht neue Spec-Tests (8 -> 16), Baseline 1020 -> 1028 Tests / 62 Dateien, Falsifizierung durch Rueckbau `Tests 2 failed | 14 passed (16)` (Test 9/13), unabhaengig vom Verifizierer wiederholt. Kopfkommentar `adminResetPassword` nachgezogen (T-FH9-05 nicht mehr offen). Ledger 16 offen / 1 zurueckgestellt / 19 geschlossen / 36 gesamt: #29 fixed, NEU #35 (Biome-Konfiguration im Bestand nicht lauffaehig, `pnpm lint` Leerlauf) und #36 (Admin-Frontend verschluckt 403 still). Verifiziert 6/6, gepusht. | 2026-09-14 | 759ea3b,63f9df0,70d007b | [260914-ebg-windows-29-schliessen-rechteausweitung-a](./quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/) | ## Deferred Items @@ -435,8 +438,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen. ## Session Continuity -Last session: 2026-09-14T08:18:04.000Z +Last session: 2026-09-14T08:41:09.727Z Resumed: 2026-09-14 — Sitzung ueber /gsd-resume-work fortgesetzt; HANDOFF.json und .continue-here.md verbraucht und entfernt. Entscheidung des Users: WINDOWS #29 VOR Etappe 3c (Live-Gehen am 2026-09-15), danach 3c, dann 3a, Etappe 4 nur nach Rueckfrage. -Stopped at: **ETAPPE 2 KOMPLETT, WINDOWS #27 GESCHLOSSEN, ETAPPE 3b (BENUTZERDIMENSION) KOMPLETT — 2026-09-11.** Endstand 1020 Tests / 62 Dateien, 203 Live-Pruefungen, 72 Paare in der Klassifikation, Ledger 15 offen von 34. Alles gepusht, Arbeitsbaum sauber. DER SCHALTER IST AUS. **User-Anweisung 2026-09-11: 'mach #27 und dann Etappe 3, nicht nachfragen. am dienstag [2026-09-15] geht eine voll funktionsfaehige version live.'** Live-Gehen braucht den Schalter NICHT (ein Mandant, BYPASSRLS-Stand laeuft heute auf alpha). OFFEN: **Etappe 3a** (Anmeldenamen pro Mandant — Schema `@@unique([tenantId, username/email])`, Anmeldeweg muss den Mandanten VOR der Suche kennen, SECURITY-DEFINER-Funktionen mit zweiter Gleichheitsbedingung; enthaelt EINE Produktfrage an den User: Mandant per Subdomain (Empfehlung) oder Login-Wahl; fuer Dienstag NICHT noetig, da ein Mandant) und **Etappe 3c** (Systemkontext `app.system_context` fuer die sechs Hintergrunddienst-Faelle #21/#30 und die vier beides-Uebergaben; macht DKV- und SMTP-Startpfad zu einmal-abfragen-viele-bedienen). VOLLSTAENDIGER AUFTRAG: docs/mandantentrennung-etappe3-auftrag.md (3b dort als 'Erledigt' vermerkt). Danach Etappe 4 Scharfschalten mit rls-preflight.mjs — DER USER WILL DORT GEFRAGT WERDEN. Die Sitzung vom 2026-09-11 endete bei ~70% Kontext nach 3b; Einstieg `/gsd-resume-work`, dann 3c oder 3a als `/gsd-quick --validate` mit vollstaendiger Kette (Planer, Pruefer, Executor, Verifizierer). +Stopped at: Quick 260914-ebg abgeschlossen: WINDOWS #29 geschlossen — Zielrollen-Riegel in UserController.update/remove, 16 Spec-Tests, Falsifizierung bestanden, gepusht Resume file: None -Last activity: 2026-09-10 - Completed quick task 260910-jab: Die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) geschlossen +Last activity: 2026-09-14 - Completed quick task 260914-ebg: WINDOWS #29 geschlossen, Zielrollen-Riegel in UserController.update/remove diff --git a/.planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-SUMMARY.md b/.planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-SUMMARY.md new file mode 100644 index 0000000..712d92b --- /dev/null +++ b/.planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-SUMMARY.md @@ -0,0 +1,243 @@ +--- +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. diff --git a/.planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-VERIFICATION.md b/.planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-VERIFICATION.md new file mode 100644 index 0000000..ed52eeb --- /dev/null +++ b/.planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-VERIFICATION.md @@ -0,0 +1,194 @@ +--- +phase: quick-260914-ebg +verified: 2026-09-14T10:44:00Z +status: passed +score: 6/6 must-haves verified +covered_files: + - .planning/WINDOWS.md + - .planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-PLAN.md + - .planning/quick/260914-ebg-windows-29-schliessen-rechteausweitung-a/260914-ebg-SUMMARY.md + - apps/api/src/auth/auth.service.ts + - apps/api/src/user/user.controller.spec.ts + - apps/api/src/user/user.controller.ts +covered_digest: "v1:sha256:6bdca3a5ea5b9c6eccd3f2c1118f8de02e124d12b1e4e6099abacf2c0286cd51" +behavior_unverified: 0 +overrides_applied: 0 +--- + +# Quick-Task 260914-ebg: WINDOWS #29 schliessen — Verifikationsbericht + +**Ziel:** Rechteausweitung ADMIN -> SUPER_ADMIN in `UserController.update` (PATCH /users/:id) und `UserController.remove` (DELETE /users/:id) verhindern, Zielrollen-Riegel nach der Mandantengrenze, acht Tests, Falsifizierung durch Rueckbau, Ledger-Eintrag #29 auf `fixed`, gepusht. +**Verifiziert:** 2026-09-14, unabhaengig vom SUMMARY nachgemessen (nicht dessen Angaben uebernommen). +**Status:** passed + +## Beweisfuehrung (jeder Schritt unabhaengig ausgefuehrt) + +### 1. Commit- und Dateiumfang + +Befehl: `git log --oneline 37a2f73..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) +e76c0f8 docs(quick-260914-ebg): Plan fuer WINDOWS #29, Zielrollen-Riegel in UserController.update/remove +``` + +Befehl: `git diff --stat 37a2f73..HEAD -- . ':!.planning'` + +``` + apps/api/src/auth/auth.service.ts | 5 +- + apps/api/src/user/user.controller.spec.ts | 115 ++++++++++++++++++++++++++++++ + apps/api/src/user/user.controller.ts | 18 +++++ + 3 files changed, 136 insertions(+), 2 deletions(-) +``` + +Ergebnis: **VERIFIZIERT** — genau die drei vom Plan vorgesehenen Nicht-Planning-Dateien geaendert (`.planning/WINDOWS.md` liegt ausserhalb dieses Filters und wurde separat geprueft, siehe Punkt 6). + +### 2. Reihenfolge der Pruefungen in `user.controller.ts` + +Beide Handler gelesen (`sed -n '160,270p' apps/api/src/user/user.controller.ts`): + +- `update()`: `resolveTargetUser` -> 404 (`NotFoundException`) -> Mandantengrenze -> 403 `Cannot modify users from other tenants` -> Zielrollen-Riegel -> 403 `Cannot modify a SUPER_ADMIN user` -> `dto.role`-Zuweisungspruefung -> 403 `Cannot assign SUPER_ADMIN role` -> `userService.update(...)`. +- `remove()`: `resolveTargetUser` -> 404 -> Selbstloeschriegel -> 403 `Cannot delete your own account` -> Mandantengrenze -> 403 `Cannot delete users from other tenants` -> Zielrollen-Riegel -> 403 `Cannot delete a SUPER_ADMIN user` -> `userService.delete(...)`. + +Ergebnis: **VERIFIZIERT** — Reihenfolge entspricht dem must-have (Ziel aufloesen -> 404, Mandantengrenze -> 403, Zielrolle -> 403, [nur update] Rollenzuweisung -> 403, dann Dienstaufruf). Beide neuen Riegel tragen einen Kommentarblock mit Verweis auf WINDOWS #29 und die Vorlage T-FH9-04. + +### 3. Controller-Spec — 16 Tests, Inhalt gelesen + +Befehl: `cd apps/api && npx vitest run src/user/user.controller.spec.ts` + +``` +✓ src/user/user.controller.spec.ts (16 tests) 24ms +Test Files 1 passed (1) + Tests 16 passed (16) +``` + +Alle acht neuen Tests (Test 9-16) vollstaendig gelesen (`sed -n '281,396p'`): + +- Test 9 (update, verboten, 3 Formen: Kennwort/isActive/Rolle) — jeweils `rejects.toThrow('Cannot modify a SUPER_ADMIN user')` UND `expect(userService.update).not.toHaveBeenCalled()`. +- Test 10 (update, SUPER_ADMIN gegen SUPER_ADMIN) — `userService.update` mit `toHaveBeenCalledWith('t1', 'boss', ...)`. +- Test 11 (update, ADMIN gegen USER) — `toHaveBeenCalledWith('t1', 'u1', ...)`. +- Test 12 (update, Ordnungstest) — `rejects.toThrow('Cannot modify users from other tenants')` (woertlich die Mandanten-Meldung, nicht die Zielrollen-Meldung) UND `not.toHaveBeenCalled()`. +- Test 13 (remove, verboten) — `rejects.toThrow('Cannot delete a SUPER_ADMIN user')` UND `expect(userService.delete).not.toHaveBeenCalled()`. +- Test 14 (remove, SUPER_ADMIN gegen SUPER_ADMIN) — `toHaveBeenCalledWith('t1', 'boss')`. +- Test 15 (remove, ADMIN gegen USER) — `toHaveBeenCalledWith('t1', 'u1')`. +- Test 16 (remove, Ordnungstest) — `rejects.toThrow('Cannot delete users from other tenants')` UND `not.toHaveBeenCalled()`. + +Ergebnis: **VERIFIZIERT** — die Tests behaupten nicht nur einen geworfenen Fehler, sondern pruefen explizit den Dienstaufruf (nicht/aufgerufen). Kein Test prueft nur den Wurf ohne Mock-Kontrolle. + +### 4. Gesamte API-Suite und Typpruefung + +Befehl: `cd apps/api && npx vitest run` + +``` +Test Files 62 passed (62) + Tests 1028 passed (1028) +``` + +Befehl: `cd apps/api && npx tsc --noEmit; echo EXIT=$?` -> `EXIT=0` + +Ergebnis: **VERIFIZIERT** — exakt die im Plan geforderten Zahlen. + +### 5. Falsifizierung (unabhaengig wiederholt) + +Reverse-Patch aus dem Task-1-Commit (`759ea3b` = `HEAD~2`) erzeugt und angewendet: + +```bash +git show HEAD~2 -- apps/api/src/user/user.controller.ts > $SCR/riegel.patch && git apply -R $SCR/riegel.patch +``` + +`APPLY_EXIT=0`. Spec danach erneut ausgefuehrt: + +``` +FAIL src/user/user.controller.spec.ts > ... > Test 13: ... AssertionError: promise resolved "{ message: 'User deleted' }" instead of rejecting +Test Files 1 failed (1) + Tests 2 failed | 14 passed (16) +``` + +Rot ausschliesslich Test 9 (Fehlermeldung `Cannot destructure property 'passwordHash' ...` statt `Cannot modify a SUPER_ADMIN user`, weil der Riegel fehlt und der Update-Mock nicht konfiguriert war) und Test 13 (Promise loeste mit `{ message: 'User deleted' }` auf statt abzulehnen) — exakt die zwei im Plan/SUMMARY behaupteten Tests, die anderen 14 blieben gruen. + +Wiederherstellung: + +```bash +git checkout -- apps/api/src/user/user.controller.ts +git status --porcelain -- apps/api/src/user/user.controller.ts # leer +git status --porcelain -- apps/api # leer +``` + +Spec danach erneut: `Tests 16 passed (16)`. + +Ergebnis: **VERIFIZIERT** — Falsifizierung unabhaengig reproduziert, byte-identische Wiederherstellung bestaetigt, Arbeitsbaum nach Wiederherstellung sauber. + +### 6. Ledger + +Befehl: `node gsd-tools.cjs windows status` und Grep gegen `.planning/WINDOWS.md`: + +- Frontmatter: `open_count: 16`, `waived_count: 1`, `fixed_count: 19`, `total_count: 36` — stimmt exakt mit dem Plan-Gate ueberein. +- Zeile `| 29 | quick-260911-fh9 | unmet-truth | ... | fixed | | 2026-09-11T10:00:38.418Z | 2026-09-14T08:37:53.307Z |` — Status `fixed`, `resolved_at` gesetzt. +- Zeile `| 35 | quick-260914-ebg | deviation | biome.json | ... | open | ...` und `| 36 | quick-260914-ebg | deviation | apps/web/.../page.tsx | ... | open | ...` — beide als eigenstaendige, offene Nebenbefunde eingetragen, nicht in #29 mitgeschlossen. + +Ergebnis: **VERIFIZIERT**. + +### 7. Kopfkommentar `auth.service.ts` + +Befehl: `grep -n "T-FH9-05" apps/api/src/auth/auth.service.ts` -> kein Treffer (Exit 1, Anzahl 0). +Befehl: `grep -c "260914-ebg" apps/api/src/auth/auth.service.ts` -> `1`. + +Kommentar gelesen (Zeilen um 395-410): „Die Schwesterwege `PATCH /users/:id` und `DELETE /users/:id` tragen seit 260914-ebg (WINDOWS #29) denselben Riegel in `UserController.update()`/`remove()`." — ersetzt den alten Satz, der T-FH9-05 als offen benannte. `adminResetPassword`-Verhalten unveraendert (nur Kommentar). + +Ergebnis: **VERIFIZIERT**. + +### 8. Push-Status + +Befehl: `git fetch -q && git status -sb | head -1` -> `## main...origin/main` (kein `[ahead`). + +Ergebnis: **VERIFIZIERT** — alle drei Task-Commits sind im Remote. + +## Beobachtete Arbeitsbaum-Reste + +Nach allen Pruefungen ist der Arbeitsbaum exakt im Ausgangszustand: nur `.planning/STATE.md` (modifiziert) und die neue `260914-ebg-SUMMARY.md` (untracked) sind vorhanden — beides Artefakte, die dem Orchestrator gehoeren und laut Auftrag nicht angefasst werden durften. Kein von dieser Verifikation verursachter Rest. + +## Beobachtete Truths + +| # | Truth | Status | Beweis | +|---|-------|--------|--------| +| 1 | ADMIN kann SUPER_ADMIN des eigenen Mandanten weder aendern noch loeschen (Dienst nicht aufgerufen) | VERIFIZIERT | Test 9, Test 13 gelesen + unabhaengig ausgefuehrt (16/16 gruen); Falsifizierung macht genau diese zwei rot | +| 2 | SUPER_ADMIN gegen SUPER_ADMIN und ADMIN gegen USER/ADMIN bleiben erlaubt | VERIFIZIERT | Test 10, 11, 14, 15 gelesen + gruen | +| 3 | Mandantengrenze vor Zielrolle, Meldung verraet keine fremde Rolle | VERIFIZIERT | Test 12, 16 gelesen + gruen, Reihenfolge im Quellcode bestaetigt | +| 4 | Falsifizierung: Rueckbau macht genau 2 Tests rot, danach wiederhergestellt | VERIFIZIERT | unabhaengig wiederholt, identisches Ergebnis, `git status --porcelain` leer | +| 5 | Kopfkommentar `adminResetPassword` nennt T-FH9-05 nicht mehr als offen | VERIFIZIERT | grep 0 Treffer, Kommentartext gelesen | +| 6 | WINDOWS #29 `fixed`, Nebenbefunde als eigene Eintraege, Frontmatter-Zaehler korrekt, gepusht | VERIFIZIERT | Ledger-Grep, `git status -sb` gegen origin/main | + +**Score:** 6/6 truths verifiziert. + +### Required Artifacts + +| Artefakt | Erwartung | Status | Details | +|----------|-----------|--------|---------| +| `apps/api/src/user/user.controller.ts` | Zielrollen-Riegel in update()/remove() | VERIFIZIERT | Code gelesen, Reihenfolge und Meldungen bestaetigt | +| `apps/api/src/user/user.controller.spec.ts` | 8 neue Tests, Spec 16 | VERIFIZIERT | Vollstaendig gelesen, alle 16 Tests gruen | +| `apps/api/src/auth/auth.service.ts` | Kopfkommentar aktualisiert, kein Verhaltensaenderung | VERIFIZIERT | Diff nur im Kommentarblock (5 Zeilen), `auth.service.spec.ts` unangetastet und Teil der gruenen Gesamt-Suite | +| `.planning/WINDOWS.md` | #29 fixed, #35/#36 neu | VERIFIZIERT | Frontmatter + Zeilen gepruef | + +### Anti-Pattern-Scan + +Keine TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER-Marker in den drei geaenderten Code-Dateien gefunden. Keine leeren Stub-Implementierungen. Keine Blocker. + +### Requirements Coverage + +- `WINDOWS-29`: SATISFIED (Riegel + Tests + Ledger `fixed`). +- `T-FH9-05`: SATISFIED (Kopfkommentar aktualisiert, Kennung entfernt). + +### Human Verification Required + +Keine. Alle must-haves sind unit-testbar und wurden unabhaengig ausgefuehrt/reproduziert; keine UI-/Laufzeit-/Browser-Pruefung im Scope dieser Aufgabe. + +### Gaps Summary + +Keine Luecken gefunden. Alle im Plan formulierten must-haves sind im Code, in den Tests, im Ledger und im Git-Verlauf nachweisbar — unabhaengig von den SUMMARY-Behauptungen nachgemessen mit identischem Ergebnis. + +--- + +_Verifiziert: 2026-09-14_ +_Verifier: Claude (gsd-verifier)_