Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70d007bb47 | |||
| 63f9df0afb | |||
| 759ea3b2ca | |||
| e76c0f8a33 | |||
| 37a2f73ffb |
@@ -1,199 +0,0 @@
|
||||
---
|
||||
context: default
|
||||
phase: mandantentrennung-etappe-3
|
||||
task: null
|
||||
total_tasks: 4
|
||||
status: paused
|
||||
last_updated: 2026-09-14T08:02:29.277Z
|
||||
---
|
||||
|
||||
# Wiedereinstieg — Etappe 3b abgeschlossen, offen sind 3c, 3a, Etappe 4
|
||||
|
||||
## Critical Anti-Patterns
|
||||
|
||||
Alle stammen aus tatsaechlichen Fehlschlaegen der Etappen 1-3b, nicht aus Vorsicht.
|
||||
|
||||
| Muster | Beschreibung | Schwere | Vermeidung |
|
||||
|--------|--------------|---------|------------|
|
||||
| Auf ein ungeprueftes Fundament bauen | `forTenant()` setzte den Kontext per `set_config` auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client. Gemessen: `set_config` auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL. Die Trennung hat nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten. | blocking | Vor jedem Umbau, der auf einem Helfer aufsetzt, dessen Wirkung EMPIRISCH nachweisen — gegen eine Wegwerf-Datenbank, mit zwei Mandanten und einer echten Abfrage. Nicht den Code lesen und schliessen, dass er stimmt. |
|
||||
| Zaehlung ohne Ansehen der Treffer | Eine `grep`-Zaehlung ergab 36 mandantengebundene Stellen. Tatsaechlich waren die meisten Treffer Kommentare, die erklaeren, warum `forTenant()` dort FEHLT. Echte Aufrufstellen: 6. | blocking | Bei jeder Zahl, die eine Planung traegt, in die Treffer hineinsehen. Eine Zahl aus `grep -c` ist eine Behauptung, kein Befund. |
|
||||
| Falsche Datei auf dem Server bearbeitet | Die Volume-Zeile ging nach `/opt/tessera/docker-compose.yml` — der Server nutzt `docker-compose.prod.yml`, weil die `.env` `COMPOSE_FILE` setzt. | blocking | Nach JEDER Aenderung an einer Compose-Datei `docker compose config` rendern und pruefen, ob die Aenderung im Ergebnis auftaucht. Vorher `docker compose ls --format json` lesen. |
|
||||
| Bericht statt Arbeitsbaum geglaubt | Zwei Agenten brachen am Sitzungslimit NACH getaner Arbeit ab; die Arbeit lag vollstaendig auf der Platte, der Bericht fehlte. | blocking | `git status` ist die Wahrheit, nicht der Agentenbericht. |
|
||||
| Zeichensatz beim Veroeffentlichen angenommen | Die Handbuch-Webseite ging mit zerlegten Umlauten live ("Für" statt "Fuer"). | advisory | Seiten mit deutschem Text als reines ASCII ausliefern (Sonderzeichen als `\uXXXX`), mit `all(ord(c)<128 ...)` pruefen. |
|
||||
|
||||
<current_state>
|
||||
**Seit dem 2026-09-11 17:58 wurde nichts mehr angefasst.** Arbeitsbaum leer
|
||||
(`git status --porcelain` liefert nichts), `main == origin/main`, keine Datei im
|
||||
Repo ist juenger als der letzte Commit. Es gibt also KEINE angefangene Arbeit,
|
||||
die aufzunehmen waere — die Sitzung lief nach Etappe 3b bei ~70% Kontext aus,
|
||||
der Handoff war geschrieben. Diese Datei ist die Neuaufnahme desselben Standes
|
||||
am 2026-09-14, ergaenzt um zwei Punkte, die im alten Handoff fehlten
|
||||
(WINDOWS #29 und der nie gelaufene Ship von Phase 17).
|
||||
|
||||
**Stand der Mandantentrennung:** Etappe 1 (Fundament repariert, 227 Zugriffe
|
||||
klassifiziert), Etappe 2 (alle zwoelf Bereiche gebunden, jeder einzeln
|
||||
verifiziert), die drei vorgezogenen Datenbankregeln (260910-jab), WINDOWS #27
|
||||
(Relations-Blindstelle, 260911-mkj) und Etappe 3b (Benutzerdimension,
|
||||
260911-nke) sind fertig und gepusht. Endstand 3b: Migration
|
||||
`20260911120000_rls_user_dimension_personal_tables`, `current_user_id()`,
|
||||
`forTenant(prisma, tenantId, userId?)`, zehn persoenliche Tabellen, 34
|
||||
Aufrufstellen in acht Diensten, 1020 Tests in 62 Dateien, `rls-scratch-check.mjs`
|
||||
203/203, sechs Loch-Pruefungen umgedreht.
|
||||
|
||||
**Der Umstellungsschalter ist AUS.** `DATABASE_URL` zeigt weiter auf die Rolle
|
||||
`tessera` mit BYPASSRLS. Der User hat ausdruecklich verlangt, beim Scharfschalten
|
||||
angehalten und gefragt zu werden.
|
||||
</current_state>
|
||||
|
||||
<completed_work>
|
||||
|
||||
- Etappe 1 — `forTenant()` repariert, Anmeldeweg ueber SECURITY-DEFINER-Funktionen,
|
||||
227 Zugriffe klassifiziert (`5228f28`).
|
||||
- Etappe 2 — zwoelf Bereiche gebunden (ldap, groups, tenders, dkv, user,
|
||||
module-registry, dashboard, calendar, tenant, auth, favorites, settings),
|
||||
ueber zwoelf Quick-Tasks `260909-ipc` .. `260911-gwh` (`1240932`).
|
||||
- Drei zu kurz greifende Datenbankregeln geschlossen, auf Anweisung des Users
|
||||
vorgezogen (260910-jab, `03fb3bf`).
|
||||
- WINDOWS #27 geschlossen — vierte Erkennungsform der Bestandsaufnahme,
|
||||
72 Paare, Restmenge als #33 eingetragen (260911-mkj, `388690f`).
|
||||
- Etappe 3b — Benutzerdimension in den Regeln (260911-nke, `f0b531b`, `07fc653`,
|
||||
`b62a905`, `3b08d8e`), verifiziert 13/13.
|
||||
</completed_work>
|
||||
|
||||
<remaining_work>
|
||||
|
||||
1. **Etappe 3c — Systemkontext fuer die Hintergrunddienste.** Sechs Faelle:
|
||||
`dkv-scheduler` / `loadAnyActiveConfigForScheduler()` (WINDOWS #21, heute
|
||||
bereits falsch — willkuerlicher Mandant), `mail.module` /
|
||||
`loadAnySmtpConfigForStartupTransport()` (WINDOWS #30, dieselbe Form),
|
||||
`ldap-config.service` `getAllActiveConfigs()`, `tender-digest.scheduler`,
|
||||
`tender-matching.service`, `admin-seed.service`
|
||||
`ensureDefaultGroupsForAllTenants`. Bauform: benannter Systemkontext
|
||||
`forSystem(prisma)` mit dritter Sitzungsvariable `app.system_context`,
|
||||
lesend erlaubt, Schreiben innerhalb der Schleife je Mandant gebunden.
|
||||
**Braucht keine Entscheidung des Users — als naechstes empfohlen.**
|
||||
2. **Etappe 3a — Anmeldenamen pro Mandant.** `@@unique([tenantId, username])`
|
||||
und `@@unique([tenantId, email])` statt plattformweit, die drei
|
||||
SECURITY-DEFINER-Funktionen bekommen `p_tenant_id`. Enthaelt **die eine
|
||||
offene Produktfrage**: woran der Login den Mandanten erkennt — Subdomain je
|
||||
Mandant (Empfehlung, weil Tessera hinter Nginx Proxy Manager laeuft) oder
|
||||
Mandantenwahl im Anmeldeformular. Schliesst WINDOWS #22.
|
||||
3. **Etappe 4 — Scharfschalten.** `rls-preflight.mjs` davor, dokumentierter
|
||||
Rueckweg. NUR nach Rueckfrage beim User. WINDOWS #18.
|
||||
4. **WINDOWS #29 — offene Rechteausweitung, unabhaengig von der
|
||||
Mandantentrennung.** `adminResetPassword` verhindert ADMIN -> SUPER_ADMIN im
|
||||
eigenen Handler (T-FH9-04), der Schwesterweg `PATCH /users/:id`
|
||||
(`UserController.update`, T-02-08) prueft nur, ob die Rolle existiert. Das ist
|
||||
der einzige offene Punkt, der nicht am Scharfschalten haengt, und der einzige
|
||||
mit Sicherheitsbezug vor dem Live-Gehen.
|
||||
5. **Phase 17 ist VERIFIED, aber `/gsd-ship` lief nie** — v1.2 ist formal nicht
|
||||
geschlossen. Mit `windows_enforce` blockiert der Ship, solange
|
||||
`open_count > 0` (aktuell 15).
|
||||
|
||||
Von den 15 offenen WINDOWS-Eintraegen sind #25, #26, #28, #31, #32 Beschreibungen
|
||||
der umgedrehten Fehlerrichtung je Bereich — sie werden erst mit dem Scharfschalten
|
||||
(#18) akut und sind bewusst so abgelegt. #33 und #34 sind Luecken im Netz der
|
||||
Bestandsaufnahme, nicht im Produkt. #21, #22, #24, #30 fallen mit 3a bzw. 3c.
|
||||
</remaining_work>
|
||||
|
||||
<decisions_made>
|
||||
|
||||
- **Anmeldenamen pro Mandant eindeutig, nicht plattformweit** (User, 2026-09-10) —
|
||||
`m.schmidt` darf es bei Firma A und Firma B geben.
|
||||
- **Kollegen derselben Firma strikt getrennt** (User, 2026-09-10) — jeder sieht
|
||||
nur seine eigenen gespeicherten Suchen, Favoriten, Dashboard-Anordnung. Mit
|
||||
Etappe 3b in den Datenbankregeln verankert.
|
||||
- **Anmeldeweg ueber SECURITY-DEFINER-Funktionen**, nicht ueber eine Policy: eine
|
||||
Policy ist ein Zeilenpraedikat, jede Regel die eine Suche nach Benutzername
|
||||
erlaubt, erlaubt das Lesen der ganzen Tabelle. Die Funktion pinnt die Ausnahme
|
||||
auf feste Spaltenliste, Gleichheit und `LIMIT 1`.
|
||||
- **Helfer erweitern statt zweiten bauen** — `forTenant()` bekam den optionalen
|
||||
dritten Parameter statt eines `forTenantAndUser()`.
|
||||
- **Am Active Directory wird nichts veraendert** (User, 2026-09-09, mit Nachdruck).
|
||||
- **Datenverlust in der Datenbank ist derzeit hinnehmbar** (User, 2026-09-09):
|
||||
nichts laeuft produktiv. Gilt nur, solange das so bleibt.
|
||||
- **Beim Scharfschalten anhalten und fragen** (User, seit 2026-09-09) — die einzige
|
||||
Ausnahme von "nicht nachfragen".
|
||||
</decisions_made>
|
||||
|
||||
<blockers>
|
||||
|
||||
**Etappe 4 darf nicht vorgezogen werden.** Wird scharf geschaltet, bevor Etappe 3
|
||||
durch ist, liefern die noch nicht umgestellten Abfragen null Zeilen statt zu vieler.
|
||||
Der gefaehrlichste Fall ist der Loeschzweig in `ldap.service.ts` (~Zeile 1559), der
|
||||
Leere als "Gruppe im Verzeichnis verschwunden" deutet und samt Mitgliedschaften und
|
||||
Modulfreigaben loescht.
|
||||
|
||||
Keine offenen Handgriffe des Users. Der Server ist auf dem aktuellen Stand.
|
||||
Keine laufenden Hintergrundauftraege (`.planning/async-jobs/` existiert nicht).
|
||||
</blockers>
|
||||
|
||||
## Required Reading (in order)
|
||||
|
||||
1. `docs/mandantentrennung-etappe3-auftrag.md` — der gemessene Auftrag fuer 3a/3b/3c;
|
||||
3b ist dort als erledigt vermerkt, der historische Auftragstext steht daneben
|
||||
2. `.planning/STATE.md` — Position, Quick-Task-Tabelle mit allen Ergebnissen
|
||||
3. `docs/mandantentrennung-zugriffsklassifikation.md` — die Arbeitsgrundlage
|
||||
4. `docs/mandantentrennung-etappe2-fehlerrichtung.md` — Abschnitt "Regelschluss
|
||||
Benutzerdimension (Etappe 3b, 260911-nke)"
|
||||
5. `docs/mandantentrennung-datenbankrolle.md` — Befund, Sperrgrund, Handgriffe, Rueckweg
|
||||
6. `.planning/WINDOWS.md` — 15 offen, davon #29 als einziger ohne Bezug zum Schalter
|
||||
7. `apps/api/src/prisma/prisma-tenant.extension.ts` — der Helfer, dreistellig
|
||||
|
||||
## Infrastructure State
|
||||
|
||||
- **alpha** (192.168.13.12, https://alpha.tessera.ctl.de): Stand `ea003d4`,
|
||||
Container am 2026-09-09 neu erstellt, `user-files` als Volume `tessera_user-files`
|
||||
am api-Container nachgewiesen. **Live-Gehen am Dienstag, 2026-09-15** — braucht
|
||||
den Schalter NICHT (ein Mandant, BYPASSRLS-Stand laeuft heute dort).
|
||||
- **Die Serverdatei ist `docker-compose.prod.yml`**, nicht `docker-compose.yml` — die
|
||||
`.env` setzt `COMPOSE_FILE`. Sicherungen als `.bak.20260909-0818` daneben.
|
||||
- **git push** geht ausschliesslich ueber `localhost:3002`; die Push-URL des Remotes
|
||||
ist dauerhaft darauf gesetzt, ein schlichtes `git push` genuegt.
|
||||
- **Lokal**: `api`, `db` und `web` laufen; KEIN mailhog-Container, deshalb scheitert
|
||||
der Mailversand lokal mit `ENOTFOUND mailhog` — umgebungsbedingt, kein Defekt.
|
||||
- **Datenbank**: kein Host-Port. IP per `docker inspect` frisch ermitteln,
|
||||
`tessera:tessera_dev`. Prisma-Binary aus `apps/api/node_modules/.bin/prisma`,
|
||||
NICHT `npx prisma` (zieht Prisma 8).
|
||||
- **Worktree-Isolation ist abgeschaltet** (`workflow.use_worktrees=false`), weil
|
||||
`origin/HEAD` in diesem Repo nicht aufloesbar ist.
|
||||
|
||||
## Pre-Execution Critique Required
|
||||
|
||||
Vor jedem weiteren Bereich gilt die Frage aus Etappe 2 unveraendert:
|
||||
|
||||
**Woran wuerde ich merken, dass eine umgestellte Abfrage jetzt zu WENIG liefert statt
|
||||
zu viel?** Der Umbau dreht die Fehlerrichtung um. Der still gefaehrlichste Ort ist
|
||||
jeder Code, der Leere als Abwesenheit deutet und daraufhin loescht.
|
||||
|
||||
<context>
|
||||
Etappe 2 lief ueber zwoelf Quick-Tasks plus einen Regel-Durchlauf, jeder mit Planer,
|
||||
Plan-Pruefer, Executor, Verifizierer. ZEHN Lieferungen wurden vom jeweils NAECHSTEN
|
||||
Schritt gefangen, nie vom eigenen: vier geschrumpfte Zaehlungen, zwei nicht
|
||||
committete Messungen, zwei Zusammenfassungen mit N statt N-1, uebersprungene
|
||||
handgepflegte Dokumentstellen, Falsifizierungsnachweise nur in Commit-Nachrichten,
|
||||
eine Wegwerf-Tabelle ohne `createdAt`/`updatedAt`, ein Selbstwiderspruch, ein Pruefer
|
||||
der etwas als plausibel durchwinkte, ein still fehlgeschlagener `git add`, und zwei
|
||||
Agenten die am Sitzungslimit NACH getaner Arbeit abbrachen. Die Kette vollstaendig zu
|
||||
fahren ist deshalb keine Zeremonie, sondern das, was in dieser Arbeit tatsaechlich
|
||||
Fehler gefangen hat.
|
||||
|
||||
Verifizierer-Hinweis zu 3b (WINDOWS #34, angenommenes Risiko): die dreistelligen
|
||||
`forTenant()`-Zusicherungen sind je Spec-Datei, nicht je Methode; das Gate "keine
|
||||
zweistellige Form in den acht Dateien" ist ein Shell-Check, nicht CI. Wer das
|
||||
schliessen will, baut den Check in `rls-access-inventory.spec.ts` ein.
|
||||
|
||||
USER-ANWEISUNG 2026-09-11, weiter gueltig: "mach #27 und dann Etappe 3, nicht
|
||||
nachfragen. du machst alles, was ohne mich geht. am dienstag geht eine voll
|
||||
funktionsfaehige version live." Nach jedem Durchlauf pushen.
|
||||
</context>
|
||||
|
||||
<next_action>
|
||||
1. `docs/mandantentrennung-etappe3-auftrag.md` lesen — 3b ist dort als erledigt vermerkt.
|
||||
2. **Etappe 3c** (Systemkontext, sechs Hintergrunddienst-Faelle) als
|
||||
`/gsd-quick --validate` — braucht keine Entscheidung des Users.
|
||||
3. **WINDOWS #29** (`PATCH /users/:id` ohne Rollenausweitungs-Pruefung) —
|
||||
kleiner, unabhaengiger Durchlauf, der einzige Sicherheitspunkt vor dem Live-Gehen.
|
||||
4. **Etappe 3a** — enthaelt die eine offene Produktfrage (Subdomain vs. Login-Wahl).
|
||||
5. **Etappe 4** — Scharfschalten. NUR nach Rueckfrage beim User.
|
||||
</next_action>
|
||||
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"timestamp": "2026-09-14T08:02:29.277Z",
|
||||
"phase": null,
|
||||
"phase_name": "Mandantentrennung wirksam machen (Etappenarbeit ausserhalb der Phasen, ueber Quick-Tasks)",
|
||||
"phase_dir": null,
|
||||
"plan": null,
|
||||
"task": null,
|
||||
"total_tasks": 5,
|
||||
"status": "paused",
|
||||
"completed_tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Etappe 1 / forTenant() repariert, Anmeldeweg ueber SECURITY DEFINER, 227 Zugriffe klassifiziert",
|
||||
"status": "done",
|
||||
"commit": "5228f28"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Etappe 2 / alle zwoelf Bereiche gebunden, jeder einzeln verifiziert (260909-ipc .. 260911-gwh)",
|
||||
"status": "done",
|
||||
"commit": "1240932"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Auf Anweisung des Users vorgezogen: drei zu kurz greifende Datenbankregeln geschlossen (260910-jab)",
|
||||
"status": "done",
|
||||
"commit": "03fb3bf"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "WINDOWS #27 geschlossen: vierte Erkennungsform, 72 Paare, #33 fuer die Rest-Empfaenger (260911-mkj)",
|
||||
"status": "done",
|
||||
"commit": "388690f"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Etappe 3b: Benutzerdimension — current_user_id(), forTenant() dreistellig, zehn Tabellen, 34 Aufrufstellen, sechs Pruefungen umgedreht (260911-nke, verifiziert 13/13)",
|
||||
"status": "done",
|
||||
"commit": "3b08d8e"
|
||||
}
|
||||
],
|
||||
"remaining_tasks": [
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Etappe 3c: Systemkontext fuer die sechs Hintergrunddienst-Faelle (forSystem(), dritte Sitzungsvariable) — EMPFOHLEN ALS NAECHSTES, braucht keine Produktentscheidung; schliesst WINDOWS #21 und #30; siehe docs/mandantentrennung-etappe3-auftrag.md",
|
||||
"status": "not_started"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "WINDOWS #29: PATCH /users/:id (UserController.update) prueft die Rollenausweitung ADMIN -> SUPER_ADMIN nicht, der Schwesterweg adminResetPassword tut es. Unabhaengig vom Scharfschalten, einziger Sicherheitspunkt vor dem Live-Gehen",
|
||||
"status": "not_started"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Etappe 3a: Anmeldenamen pro Mandant (@@unique([tenantId, username/email]), p_tenant_id in den drei SECURITY-DEFINER-Funktionen) — enthaelt DIE EINE offene Produktfrage: Subdomain je Mandant vs. Mandantenwahl im Login; fuer Dienstag nicht noetig",
|
||||
"status": "not_started"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Etappe 4: Scharfschalten (DATABASE_URL auf eine Rolle ohne BYPASSRLS, rls-preflight.mjs davor) — USER WILL GEFRAGT WERDEN. Nicht noetig fuer das Live-Gehen am Dienstag 2026-09-15",
|
||||
"status": "not_started"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Phase 17 ist VERIFIED, aber /gsd-ship lief nie — v1.2 formal nicht geschlossen; windows_enforce blockiert den Ship bei open_count 15",
|
||||
"status": "not_started"
|
||||
}
|
||||
],
|
||||
"blockers": [
|
||||
{
|
||||
"description": "Etappe 4 darf erst nach Etappe 3 laufen. Vorher liefern die nicht umgestellten Abfragen null Zeilen statt zu vieler; der Loeschzweig in ldap.service.ts (~1559) deutet Leere als 'Gruppe im Verzeichnis verschwunden' und loescht. Der User hat ausdruecklich verlangt, beim Scharfschalten angehalten und gefragt zu werden.",
|
||||
"type": "process",
|
||||
"workaround": "Reihenfolge einhalten: 3c, dann 3a, dann fragen"
|
||||
}
|
||||
],
|
||||
"async_jobs": [],
|
||||
"human_actions_pending": [
|
||||
{
|
||||
"action": "Produktentscheidung fuer Etappe 3a: woran der Login den Mandanten erkennt — eigene Subdomain je Mandant (Empfehlung) oder Mandantenwahl im Anmeldeformular",
|
||||
"context": "Ohne diese Entscheidung laesst sich 3a nicht planen; mit einem Mandanten ist plattformweit = pro Mandant, deshalb fuer das Live-Gehen am 2026-09-15 nicht noetig",
|
||||
"blocking": false
|
||||
},
|
||||
{
|
||||
"action": "Freigabe fuer Etappe 4 (Scharfschalten)",
|
||||
"context": "Ausdrueckliche Anweisung des Users seit 2026-09-09",
|
||||
"blocking": true
|
||||
}
|
||||
],
|
||||
"decisions": [
|
||||
{
|
||||
"decision": "Anmeldenamen pro Mandant eindeutig, nicht plattformweit",
|
||||
"rationale": "Produktentscheidung des Users am 2026-09-10 — m.schmidt darf es bei Firma A und B geben",
|
||||
"phase": "Etappe 3"
|
||||
},
|
||||
{
|
||||
"decision": "Kollegen derselben Firma strikt getrennt — Benutzerdimension in die Datenbankregeln",
|
||||
"rationale": "Produktentscheidung des Users am 2026-09-10; mit Etappe 3b umgesetzt",
|
||||
"phase": "Etappe 3b"
|
||||
},
|
||||
{
|
||||
"decision": "Helfer erweitern statt zweiten bauen — forTenant(prisma, tenantId, userId?)",
|
||||
"rationale": "Praezedenz withTenantTransaction(); Hintergrunddienste und Verwaltungswege rufen weiter zweistellig, die Regel traegt IS-NULL",
|
||||
"phase": "260911-nke"
|
||||
},
|
||||
{
|
||||
"decision": "req.tenantPrisma entfernt, Middleware geloescht",
|
||||
"rationale": "Bei jeder Anfrage gebaut, nirgends gelesen; Middleware war nirgends registriert",
|
||||
"phase": "260911-e2s"
|
||||
},
|
||||
{
|
||||
"decision": "Datenbankregeln VOR Abschluss von Etappe 2 vorgezogen",
|
||||
"rationale": "Ausdrueckliche Anweisung des Users am 2026-09-10 — offene Loecher werden vergessen",
|
||||
"phase": "260910-jab"
|
||||
},
|
||||
{
|
||||
"decision": "Datenverlust in der Datenbank ist derzeit hinnehmbar",
|
||||
"rationale": "User 2026-09-09: nichts laeuft produktiv. Gilt nur solange das so bleibt.",
|
||||
"phase": "Etappe 4 (Vorgriff)"
|
||||
}
|
||||
],
|
||||
"uncommitted_files": [],
|
||||
"next_action": "docs/mandantentrennung-etappe3-auftrag.md lesen. Etappe 3c (Systemkontext) als /gsd-quick --validate starten — braucht keine Entscheidung des Users. Danach WINDOWS #29, dann 3a. NICHT nachfragen ausser beim Scharfschalten.",
|
||||
"context_notes": "Gemessen am 2026-09-14: git status --porcelain leer, main == origin/main, keine Datei juenger als der letzte Commit vom 2026-09-11 17:58 — es gibt KEINE angefangene Arbeit. Die Sitzung vom 11.09. lief nach Etappe 3b bei ~70% Kontext aus, der Handoff war geschrieben; dieser hier ist die Neuaufnahme desselben Standes plus zwei Punkte, die im alten fehlten (WINDOWS #29, nie gelaufener Ship von Phase 17). Verifizierer-Hinweis zu 3b: die dreistelligen forTenant()-Zusicherungen sind je Spec-Datei, nicht je Methode; das Gate 'keine zweistellige Form in den acht Dateien' ist ein Shell-Check, nicht CI (WINDOWS #34, angenommenes Risiko) — wer das schliessen will, baut den Check in rls-access-inventory.spec.ts ein. USER-ANWEISUNG 2026-09-11, weiter gueltig: 'mach #27 und dann Etappe 3, nicht nachfragen. du machst alles, was ohne mich geht. am dienstag geht eine voll funktionsfaehige version live.' Live-Gehen braucht den Schalter NICHT (ein Mandant, BYPASSRLS-Stand laeuft heute auf alpha); Etappe 3 darf es nicht blockieren. Nach jedem Durchlauf pushen. Etappe 2 lief ueber zwoelf Quick-Tasks plus einen Regel-Durchlauf, jeder mit Planer, Plan-Pruefer, Executor, Verifizierer: ZEHN Lieferungen wurden vom jeweils NAECHSTEN Schritt gefangen, nie vom eigenen — vier geschrumpfte Zaehlungen, zwei nicht committete Messungen, zwei Zusammenfassungen mit N statt N-1, uebersprungene handgepflegte Dokumentstellen, Falsifizierungsnachweise nur in Commit-Nachrichten, eine Wegwerf-Tabelle ohne createdAt/updatedAt, ein Selbstwiderspruch, ein Pruefer der etwas als plausibel durchwinkte, ein still fehlgeschlagener git add, und zwei Agenten die am Sitzungslimit NACH getaner Arbeit abbrachen. Die Placeholder-Suche ueber .planning/phases/ meldet 53 Treffer, alle geprueft: es sind Prosa-Stellen, die Debt-Marker BESCHREIBEN (z.B. 17-VERIFICATION.md Zeile 135), keine unfertigen Zusammenfassungen."
|
||||
}
|
||||
+3
-3
@@ -5,7 +5,7 @@ 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-11T15:49:27.138Z"
|
||||
last_updated: "2026-09-14T08:18:04.000Z"
|
||||
last_activity: 2026-09-10
|
||||
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
|
||||
@@ -435,8 +435,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-09-11T15:49:18.581Z
|
||||
Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus.
|
||||
Last session: 2026-09-14T08:18:04.000Z
|
||||
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).
|
||||
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
|
||||
|
||||
+35
-7
@@ -1,10 +1,10 @@
|
||||
---
|
||||
schema_version: 1
|
||||
open_count: 15
|
||||
open_count: 16
|
||||
waived_count: 1
|
||||
fixed_count: 18
|
||||
total_count: 34
|
||||
last_updated: 2026-09-11T15:46:08.295Z
|
||||
fixed_count: 19
|
||||
total_count: 36
|
||||
last_updated: 2026-09-14T08:38:12.619Z
|
||||
---
|
||||
|
||||
# Broken Windows Ledger
|
||||
@@ -43,12 +43,14 @@ last_updated: 2026-09-11T15:46:08.295Z
|
||||
| 26 | quick-260911-cwh | deviation | apps/web/src/components/dashboard/widgets/calendar-widget.tsx | | Bereich calendar: zu kleines Leseergebnis auf getSources/fetchAndCacheEvents sieht aus wie 'keine Quelle eingerichtet' bzw. 'keine Termine' (calendar-widget.tsx, calendar-settings-panel.tsx); das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand (calendar-widget.tsx catch->setEvents([]), calendar-settings-panel.tsx .catch(()=>{}) auf fetchSources); der Nutzer legt seine Quelle neu an und tippt Exchange-/CalDAV-Zugangsdaten ein zweites Mal in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt unsichtbar liegen und wird nach Behebung zur Dublette; Vorabpruefung fuer Etappe 4: physisch vorhandene CalendarSource-Zeilen je Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung vergleichen (docs/mandantentrennung-etappe2-fehlerrichtung.md (k4)(e)); an dieselbe Bedingung gebunden wie WINDOWS #18; Familie mit #23 (module-registry) und #25 (dashboard); das Frontend wird von 260911-cwh NICHT geaendert. | open | | 2026-09-11T07:57:36.769Z | |
|
||||
| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | fixed | | 2026-09-11T09:08:00.435Z | 2026-09-11T14:48:15.447Z |
|
||||
| 28 | quick-260911-fh9 | deviation | apps/web/src/components/layout/header.tsx | | Bereich auth: getMe liefert nach dem Scharfschalten null, der Controller antwortet 200 mit leerem Rumpf, fetchCurrentUser (auth-actions.ts) macht daraus null, header.tsx und account-settings-form.tsx tun bei null nichts — die Portalhuelle rendert ohne angemeldeten Benutzer; changePassword liest sich als networkError (nicht als falsches Kennwort); adminResetPassword als 'User not found' ohne UI-Aufrufer. 'nicht angemeldet' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert — an dieselbe Bedingung gebunden wie WINDOWS #18; Familie #23/#25/#26; Etappe-4-Vorabpruefung: bekannten Benutzer ueber die Wartungsrolle lesen und den gebundenen findUnique unter seinem Claim-Mandanten daneben halten. Das Frontend wird von 260911-fh9 NICHT geaendert. | open | | 2026-09-11T10:00:29.558Z | |
|
||||
| 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | open | | 2026-09-11T10:00:38.418Z | |
|
||||
| 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | fixed | | 2026-09-11T10:00:38.418Z | 2026-09-14T08:37:53.307Z |
|
||||
| 30 | quick-260911-gwh | deviation | apps/api/src/mail/mail.module.ts | | Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a). | open | | 2026-09-11T11:57:36.656Z | |
|
||||
| 31 | quick-260911-gwh | deviation | apps/web/src/components/dashboard/widgets/favorites-widget.tsx | | Bereich favorites: ein nach dem Scharfschalten (#18) zu klein gebliebenes Leseergebnis auf list() sieht aus wie 'Noch keine Favoriten.' (favorites-widget.tsx Zeile um 212, de.json favorites.empty) -- fetchFavorites (favorites-api.ts) reicht die leere Liste durch. 'Nie einen gespeichert' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert []. Etappe-4-Vorabpruefung (f4)(d): fuer einen bekannten Nutzer/Widget die Favoritenzahl ueber die Wartungsrolle und ueber den gebundenen findMany daneben halten. Familie #23/#25/#26/#28. Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich favorites' (f3)/(f4). | open | | 2026-09-11T11:57:50.276Z | |
|
||||
| 32 | quick-260911-gwh | deviation | apps/web/src/components/settings/smtp-settings-form.tsx | | Bereich settings: getSmtpConfig liefert nach dem Scharfschalten (#18) null, der Controller antwortet 200 mit leerem Rumpf, fetchSmtp (settings-api.ts) laeuft mit res.json() auf den leeren Rumpf und wirft, smtp-settings-form.tsx verschluckt das in .catch(() => {}) -- leeres Formular 'nicht eingerichtet', waehrend die Zugangsdaten physisch da sind. Ein erneutes Speichern unter der ungebundenen Form scheitert am Eindeutigkeitsindex SmtpConfig_tenantId_key (PrismaClientUnknownRequestError, gemessen in Aufgabe 1 Pruefung 8) -- nach diesem Lauf ist saveSmtpConfig gebunden und trifft die eigene Zeile, dieser Rest bestand nur unter der ungebundenen Form vor dieser Aenderung. Dieselbe 200-leerer-Rumpf-Kette wie #28. Etappe-4-Vorabpruefung (s4)(e). Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s3)/(s4). | open | | 2026-09-11T11:57:50.484Z | |
|
||||
| 33 | quick-260911-mkj | unmet-truth | apps/api/src/tenders/tenders.seed.ts | | Modellaufrufe auf Empfaengern, die weder this.prisma noch eine const X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: tenders/tenders.seed.ts (Funktionsparameter prisma: PrismaService, tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) und tenders/backfill-tender-source.ts (eigenstaendiges Skript mit new PrismaClient(), tender.findMany/update, durch RELATION_SPEC_EXCEPTIONS laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER <Kennung>.<Modell>.<Operation>(-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut. | open | | 2026-09-11T14:48:09.723Z | |
|
||||
| 34 | quick-260911-nke | deviation | apps/api/src/prisma/prisma-tenant.extension.ts | | Etappe 3b: ein Nutzer-CRUD-Aufrufer, der den Benutzer an forTenant() vergisst, sieht den ganzen Mandanten (IS-NULL-Form) — gleicher Stand wie vor 20260911120000, keine Verschlechterung, aber kein Netz. Die Bestandsaufnahme unterscheidet nur mandanten-gebunden/ungebunden, nicht benutzer-gebunden; ein Waechter, der jede Methode mit userId-Parameter auf das dritte Argument prueft, ist NICHT gebaut. Bis dahin sind die dreistelligen Spec-Zusicherungen je Dienst das einzige Netz. Vor dem Scharfschalten (Etappe 4, rls-preflight.mjs) zu entscheiden: Waechter bauen oder Rest benennen. | open | | 2026-09-11T15:46:08.295Z | |
|
||||
| 35 | quick-260914-ebg | deviation | biome.json | | 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. | open | | 2026-09-14T08:38:04.079Z | |
|
||||
| 36 | quick-260914-ebg | deviation | apps/web/src/app/(portal)/admin/users/page.tsx | | 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. | open | | 2026-09-14T08:38:12.619Z | |
|
||||
|
||||
````json
|
||||
[
|
||||
@@ -395,10 +397,10 @@ last_updated: 2026-09-11T15:46:08.295Z
|
||||
"file": "apps/api/src/user/user.controller.ts",
|
||||
"line": null,
|
||||
"description": "Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen.",
|
||||
"status": "open",
|
||||
"status": "fixed",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-11T10:00:38.418Z",
|
||||
"resolved_at": null
|
||||
"resolved_at": "2026-09-14T08:37:53.307Z"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
@@ -459,6 +461,32 @@ last_updated: 2026-09-11T15:46:08.295Z
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-11T15:46:08.295Z",
|
||||
"resolved_at": null
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"kind": "deviation",
|
||||
"phase": "quick-260914-ebg",
|
||||
"file": "biome.json",
|
||||
"line": null,
|
||||
"description": "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.",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-14T08:38:04.079Z",
|
||||
"resolved_at": null,
|
||||
"milestone": "v1.2"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"kind": "deviation",
|
||||
"phase": "quick-260914-ebg",
|
||||
"file": "apps/web/src/app/(portal)/admin/users/page.tsx",
|
||||
"line": null,
|
||||
"description": "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.",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-14T08:38:12.619Z",
|
||||
"resolved_at": null,
|
||||
"milestone": "v1.2"
|
||||
}
|
||||
]
|
||||
````
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
---
|
||||
phase: quick-260914-ebg
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
autonomous: true
|
||||
requirements: [WINDOWS-29, T-FH9-05]
|
||||
|
||||
files_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
|
||||
|
||||
estimate:
|
||||
tokens: 45000
|
||||
raw_tokens: 45000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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."
|
||||
artifacts:
|
||||
- "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`"
|
||||
key_links:
|
||||
- "`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)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@.planning/WINDOWS.md
|
||||
@apps/api/src/user/user.controller.ts
|
||||
@apps/api/src/user/user.controller.spec.ts
|
||||
@apps/api/src/auth/auth.service.ts
|
||||
@apps/api/src/auth/auth.service.spec.ts
|
||||
|
||||
<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).
|
||||
- Typpruefung: `pnpm -C apps/api exec tsc --noEmit` -> Exit 0.
|
||||
- Zeilen im Controller: `resolveTargetUser` 68-73; `update()` 173-212, Mandantengrenze 184-189, `dto.role`-Pruefung 191-194 (Meldung `Cannot assign SUPER_ADMIN role`), Schreibzugriff 201; `remove()` 220-251, Selbstloeschriegel 235-237, Mandantengrenze 240-245 (Meldung `Cannot delete users from other tenants`), Loeschzugriff 249.
|
||||
- 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>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Zielrollen-Riegel in update() und remove() — Tests zuerst (RED), dann Riegel (GREEN)</name>
|
||||
<files>apps/api/src/user/user.controller.spec.ts, apps/api/src/user/user.controller.ts</files>
|
||||
<behavior>
|
||||
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.
|
||||
</behavior>
|
||||
<action>
|
||||
Schritt A — RED: die acht Tests aus `<behavior>` 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<done>
|
||||
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).
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Falsifizierung durch Rueckbau, Kopfkommentar der Vorlage nachziehen, Gesamt-Gates</name>
|
||||
<files>apps/api/src/auth/auth.service.ts</files>
|
||||
<action>
|
||||
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)`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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"</automated>
|
||||
</verify>
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Ledger — #29 schliessen, zwei Nebenbefunde eintragen, pushen</name>
|
||||
<files>.planning/WINDOWS.md</files>
|
||||
<action>
|
||||
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>"` — 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>"` — 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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"</automated>
|
||||
</verify>
|
||||
<done>
|
||||
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`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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 |
|
||||
| ADMIN (Mandanten-Verwalter) -> SUPER_ADMIN (oberste Rolle) | Rollengrenze INNERHALB eines Mandanten; `RolesGuard` laesst beide Rollen auf die Handler, die Feinpruefung liegt im Handler |
|
||||
|
||||
## STRIDE Threat Register (ASVS Level 1, Blocking-Schwelle `high`)
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-EBG-01 | Elevation of Privilege | `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>
|
||||
|
||||
<verification>
|
||||
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.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
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).
|
||||
</output>
|
||||
@@ -404,8 +404,9 @@ export class AuthService {
|
||||
* `BadRequestException` nennt weder Halter noch Mandanten. Der Riegel
|
||||
* unten schliesst zusaetzlich die Rechteausweitung INNERHALB des
|
||||
* Mandanten (T-FH9-04): ein Nicht-SUPER_ADMIN darf das Kennwort eines
|
||||
* SUPER_ADMIN nicht setzen. Der Schwesterweg `PATCH /users/:id` hat
|
||||
* dieselbe Luecke nicht geschlossen — offener Ledger-Eintrag T-FH9-05.
|
||||
* SUPER_ADMIN nicht setzen. Die Schwesterwege `PATCH /users/:id` und
|
||||
* `DELETE /users/:id` tragen seit 260914-ebg (WINDOWS #29) denselben
|
||||
* Riegel in `UserController.update()`/`remove()`.
|
||||
*/
|
||||
async adminResetPassword(
|
||||
tenantId: string,
|
||||
|
||||
@@ -277,4 +277,119 @@ describe('UserController', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update/remove — Zielrolle SUPER_ADMIN (WINDOWS #29)', () => {
|
||||
it('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', async () => {
|
||||
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
userService.findById.mockResolvedValue({ id: 'boss', tenantId: 't1', role: Role.SUPER_ADMIN });
|
||||
|
||||
await expect(
|
||||
controller.update('boss', { password: 'fresh-password' }, admin),
|
||||
).rejects.toThrow('Cannot modify a SUPER_ADMIN user');
|
||||
expect(userService.update).not.toHaveBeenCalled();
|
||||
|
||||
await expect(
|
||||
controller.update('boss', { isActive: false }, admin),
|
||||
).rejects.toThrow('Cannot modify a SUPER_ADMIN user');
|
||||
expect(userService.update).not.toHaveBeenCalled();
|
||||
|
||||
await expect(
|
||||
controller.update('boss', { role: Role.USER }, admin),
|
||||
).rejects.toThrow('Cannot modify a SUPER_ADMIN user');
|
||||
expect(userService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Test 10: ein SUPER_ADMIN kann einen anderen SUPER_ADMIN weiterhin ändern — der Zielrollen-Riegel gilt nur für Nicht-SUPER_ADMIN-Aufrufer', async () => {
|
||||
const superAdmin = { role: Role.SUPER_ADMIN, tenantId: 't1', id: 'super1' };
|
||||
userService.findByIdForPlatformAdmin.mockResolvedValue({
|
||||
id: 'boss',
|
||||
tenantId: 't1',
|
||||
role: Role.SUPER_ADMIN,
|
||||
});
|
||||
userService.update.mockResolvedValue({
|
||||
id: 'boss',
|
||||
tenantId: 't1',
|
||||
role: Role.SUPER_ADMIN,
|
||||
passwordHash: 'h',
|
||||
});
|
||||
|
||||
const result = await controller.update('boss', { password: 'fresh-password' }, superAdmin);
|
||||
|
||||
expect(userService.update).toHaveBeenCalledWith(
|
||||
't1',
|
||||
'boss',
|
||||
expect.objectContaining({ password: 'fresh-password' }),
|
||||
);
|
||||
expect(result).not.toHaveProperty('passwordHash');
|
||||
});
|
||||
|
||||
it('Test 11: ein Mandanten-Administrator kann einen USER seines Mandanten weiterhin ändern — Regressionsschutz, der Zielrollen-Riegel engt bestehende Wege nicht zusätzlich ein', async () => {
|
||||
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
userService.findById.mockResolvedValue({ id: 'u1', tenantId: 't1', role: Role.USER });
|
||||
userService.update.mockResolvedValue({ id: 'u1', tenantId: 't1', role: Role.USER });
|
||||
|
||||
await controller.update('u1', { displayName: 'Neu' }, admin);
|
||||
|
||||
expect(userService.update).toHaveBeenCalledWith(
|
||||
't1',
|
||||
'u1',
|
||||
expect.objectContaining({ displayName: 'Neu' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('Test 12: die Mandantengrenze wird VOR der Zielrollen-Prüfung geprüft — ein Administrator, der (bei einer fehlerhaften Auflösung) ein Ziel eines fremden Mandanten mit der obersten Rolle erhält, bekommt die Mandanten-Meldung, nicht die Zielrollen-Meldung, und erfährt so nichts über die Rolle des fremden Benutzers', async () => {
|
||||
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
userService.findById.mockResolvedValue({ id: 'boss2', tenantId: 't2', role: Role.SUPER_ADMIN });
|
||||
|
||||
await expect(
|
||||
controller.update('boss2', { displayName: 'Neu' }, admin),
|
||||
).rejects.toThrow('Cannot modify users from other tenants');
|
||||
expect(userService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('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', async () => {
|
||||
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
userService.findById.mockResolvedValue({ id: 'boss', tenantId: 't1', role: Role.SUPER_ADMIN });
|
||||
|
||||
await expect(controller.remove('boss', admin)).rejects.toThrow(
|
||||
'Cannot delete a SUPER_ADMIN user',
|
||||
);
|
||||
expect(userService.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Test 14: ein SUPER_ADMIN kann einen anderen SUPER_ADMIN weiterhin löschen — der Zielrollen-Riegel gilt nur für Nicht-SUPER_ADMIN-Aufrufer', async () => {
|
||||
const superAdmin = { role: Role.SUPER_ADMIN, tenantId: 't1', id: 'super1' };
|
||||
userService.findByIdForPlatformAdmin.mockResolvedValue({
|
||||
id: 'boss',
|
||||
tenantId: 't1',
|
||||
role: Role.SUPER_ADMIN,
|
||||
});
|
||||
userService.delete.mockResolvedValue({ message: 'User deleted' });
|
||||
|
||||
const result = await controller.remove('boss', superAdmin);
|
||||
|
||||
expect(userService.delete).toHaveBeenCalledWith('t1', 'boss');
|
||||
expect(result).toEqual({ message: 'User deleted' });
|
||||
});
|
||||
|
||||
it('Test 15: ein Mandanten-Administrator kann einen USER seines Mandanten weiterhin löschen — Regressionsschutz', async () => {
|
||||
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
userService.findById.mockResolvedValue({ id: 'u1', tenantId: 't1', role: Role.USER });
|
||||
userService.delete.mockResolvedValue({ message: 'User deleted' });
|
||||
|
||||
await controller.remove('u1', admin);
|
||||
|
||||
expect(userService.delete).toHaveBeenCalledWith('t1', 'u1');
|
||||
});
|
||||
|
||||
it('Test 16: die Mandantengrenze wird VOR der Zielrollen-Prüfung geprüft — beim Löschen bekommt ein Administrator mit einem fremdmandantigen Ziel der obersten Rolle die Mandanten-Meldung, nicht die Zielrollen-Meldung', async () => {
|
||||
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
|
||||
userService.findById.mockResolvedValue({ id: 'boss2', tenantId: 't2', role: Role.SUPER_ADMIN });
|
||||
|
||||
await expect(controller.remove('boss2', admin)).rejects.toThrow(
|
||||
'Cannot delete users from other tenants',
|
||||
);
|
||||
expect(userService.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,6 +188,17 @@ export class UserController {
|
||||
throw new ForbiddenException('Cannot modify users from other tenants');
|
||||
}
|
||||
|
||||
// Zielrollen-Riegel (WINDOWS #29, 260914-ebg): die Pruefung unten sichert
|
||||
// nur die NEUE Zuweisung der obersten Rolle (dto.role) — dieser Riegel
|
||||
// sichert das ZIEL, das die oberste Rolle bereits traegt, gegen JEDES
|
||||
// Feld dieses DTO (Kennwort, isActive, Rolle, Anmeldename, E-Mail).
|
||||
// Vorlage: `AuthService.adminResetPassword` (T-FH9-04). Die
|
||||
// Mandantengrenze bleibt DAVOR, damit die Meldung nichts ueber die
|
||||
// Rolle eines fremdmandantigen Benutzers verraet (T-EBG-04).
|
||||
if (user.role === Role.SUPER_ADMIN && currentUser.role !== Role.SUPER_ADMIN) {
|
||||
throw new ForbiddenException('Cannot modify a SUPER_ADMIN user');
|
||||
}
|
||||
|
||||
// T-02-08: ADMIN cannot set role to SUPER_ADMIN
|
||||
if (currentUser.role !== Role.SUPER_ADMIN && dto.role === Role.SUPER_ADMIN) {
|
||||
throw new ForbiddenException('Cannot assign SUPER_ADMIN role');
|
||||
@@ -244,6 +255,13 @@ export class UserController {
|
||||
throw new ForbiddenException('Cannot delete users from other tenants');
|
||||
}
|
||||
|
||||
// Zielrollen-Riegel (WINDOWS #29, 260914-ebg): derselbe Riegel wie in
|
||||
// update() oben — ein Nicht-SUPER_ADMIN darf den SUPER_ADMIN seines
|
||||
// Mandanten nicht loeschen.
|
||||
if (user.role === Role.SUPER_ADMIN && currentUser.role !== Role.SUPER_ADMIN) {
|
||||
throw new ForbiddenException('Cannot delete a SUPER_ADMIN user');
|
||||
}
|
||||
|
||||
// Gebunden an den Mandanten des ZIELBENUTZERS, derselbe Grund wie bei
|
||||
// update() oben.
|
||||
await this.userService.delete(user.tenantId, id);
|
||||
|
||||
Reference in New Issue
Block a user