---
context: default
phase: mandantentrennung-etappe-3
task: null
total_tasks: 5
status: paused
last_updated: 2026-09-11T13:00:00.000Z
---
# Wiedereinstieg — Mandantentrennung, ETAPPE 2 ABGESCHLOSSEN, vor WINDOWS #27 und Etappe 3
## Critical Anti-Patterns
Alle vier stammen aus tatsaechlichen Fehlschlaegen dieser Sitzung, nicht aus Vorsicht.
| Muster | Beschreibung | Schwere | Vermeidung |
|--------|--------------|---------|------------|
| Auf ein ungeprueftes Fundament bauen | `forTenant()` — der Helfer, auf dem die ganze Mandantentrennung ruht — 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. Waere vor dem Scharfschalten nicht geprueft worden, haetten die Abfragen danach NULL Zeilen geliefert und der LDAP-Loeschzweig haette das als "Gruppe im Verzeichnis verschwunden" gedeutet und geloescht. | 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 wurde in `/opt/tessera/docker-compose.yml` eingetragen — der Server nutzt aber `docker-compose.prod.yml`, weil die `.env` `COMPOSE_FILE=docker-compose.prod.yml` setzt. Die Aenderung waere wirkungslos geblieben und haette wie erledigt ausgesehen. | 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, um zu sehen, welche Datei ueberhaupt gilt. |
| Zeichensatz beim Veroeffentlichen angenommen | Die Handbuch-Webseite ging mit zerlegten Umlauten live ("Für" statt "Fuer"), weil im lokalen Test der Zeichensatz fehlte und ich annahm, das Veroeffentlichen setze ihn schon richtig. | advisory | Seiten mit deutschem Text als reines ASCII ausliefern (Sonderzeichen als `\uXXXX` in den Daten). Dann kann kein Zeichensatz sie falsch auslegen. Die fertige Datei mit `all(ord(c)<128 ...)` pruefen. |
**Etappe 2 ist am 2026-09-11 abgeschlossen.** Alle zwoelf Bereiche sind gebunden und
einzeln verifiziert (ldap, groups, tenders, dkv, user, module-registry, dashboard,
calendar, tenant, auth, favorites+settings); die drei Datenbankregeln wurden auf
Anweisung des Users vorgezogen (260910-jab). Endstand: 994 Tests in 62 Dateien
(Ausgang 701/53), 137 Live-Pruefungen (Ausgang 8), 65 Paare / 68 ungebunden /
178 gebunden — jeder ungebundene Zugriff liegt auf einer plattformglobalen Tabelle
oder einem benannten Startpfad. Alles gepusht, Arbeitsbaum sauber.
**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.
Die maschinelle Bestandsaufnahme hat eine bekannte Blindstelle (WINDOWS #27):
`include:`/`_count:` in fremd geschuetzte Tabellen sieht sie nicht. Alle heutigen
Instanzen sind einzeln geprueft; der Mechanismus muss VOR Etappe 4 geschlossen werden.
Diese Sitzung, in Reihenfolge:
1. **Aufraeumen** (`98a8c93`) — zwei veraltete Checkpoint-Dateien entfernt, ihr noch
gueltiges Wissen (5 Anti-Patterns, Infrastruktur-Stand) nach STATE.md gerettet.
2. **Live-Test AD** (`a1a8b4f`, `b6b964b`) — WINDOWS #4 und #6 belegt und geschlossen,
ohne jede Aenderung am Verzeichnis: Umbenennung und Verschwinden wurden ueber den in
Tessera gespeicherten Stand nachgestellt.
3. **Verbindungstest Postfach** (`c4db3b2`, abgenommen) — WINDOWS #16.
4. **Zwei Defekte behoben** (`e8c2411`, `1222951`, `2167046`) — WINDOWS #14 (Matrix-Suche)
und #15 (Sync-Meldungen); dabei den Sicherheitsfund T-Q3-01 mitgeschlossen.
5. **Anleitungen** (`3501eb4`) — vier Handbuecher plus Einstieg unter `docs/`, gegen den
Quelltext geschrieben und unabhaengig gegengeprueft. Zusaetzlich als Webseite
veroeffentlicht: https://claude.ai/code/artifact/67372b7f-4d7c-49c7-9642-1c5ef576f245
6. **Dateisicherung** (`dab72eb`) — `user-files` als benanntes Volume; auf dem Server
nachgetragen und am laufenden System belegt, WINDOWS #17 geschlossen.
7. **Versionsangaben** (`c807049`) — CLAUDE.md auf den installierten Stand; sechs nie
eingebaute Empfehlungen benannt (u.a. Keycloak, Redis, shadcn/ui).
8. **Mandantentrennung Etappe 1** (`bbf1795`, `de50297`, `5f3a39c`, `da0ac04`).
**Etappe 2 — die eigentliche Umstellung.** 31 Einheiten vollstaendig, 9 teilweise.
Grundlage: `docs/mandantentrennung-zugriffsklassifikation.md`, maschinell gegen
Abdriften abgesichert durch `apps/api/src/prisma/rls-access-inventory.spec.ts`.
Geschaetzt 5-8 Durchlaeufe, nach Bereichen gebuendelt.
Groessen je Bereich: tenders 62, groups 37, ldap 21, dkv 21, user 17,
module-registry 17, dashboard 13, calendar 12, tenant 8, favorites 7, settings 4.
**Etappe 3** — Systemkontext fuer Hintergrundlaeufe (ein Cron-Job liest bewusst ueber
alle Mandanten, muss aber INNERHALB der Schleife je Mandant binden) plus WINDOWS #19.
**Etappe 4** — Scharfschalten mit `rls-preflight.mjs` davor und dokumentiertem Rueckweg.
- **Anmeldeweg ueber SECURITY-DEFINER-Funktionen**, nicht ueber eine Policy und nicht
ueber eine zweite Rolle. Eine Policy ist ein Zeilenpraedikat: jede Regel, die eine
Suche nach Benutzername erlaubt, erlaubt zwangslaeufig das Lesen der ganzen Tabelle.
Die Funktion pinnt die Ausnahme auf feste Spaltenliste, Gleichheit und `LIMIT 1`.
- **Benanntes Volume statt Bind-Mount** fuer `user-files` — Eigentuemerschaft, nicht
Sicherungskomfort, gab den Ausschlag.
- **Am Active Directory wird nichts veraendert** (User, 2026-09-09, mit Nachdruck).
Pruefungen, die nach einer Verzeichnis-Aenderung aussehen, werden ueber den in Tessera
gespeicherten Stand nachgestellt — so wurden #4 und #6 geschlossen.
- **Datenverlust in der Datenbank ist derzeit hinnehmbar** (User, 2026-09-09): nichts
laeuft produktiv. Erlaubt beim Scharfschalten den direkten Weg statt aufwendiger
Absicherung. Gilt nur, solange das so bleibt — vor einem Produktivbetrieb neu bewerten.
**Etappe 4 darf nicht vorgezogen werden.** Wird scharf geschaltet, bevor Etappe 2 und 3
durch sind, 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.
## Required Reading (in order)
1. `.planning/STATE.md` — Position, Quick-Task-Tabelle mit allen Ergebnissen dieser Sitzung
2. `docs/mandantentrennung-zugriffsklassifikation.md` — die Arbeitsgrundlage fuer Etappe 2
3. `docs/mandantentrennung-datenbankrolle.md` — Befund, Sperrgrund, Handgriffe, Rueckweg
4. `.planning/WINDOWS.md` — offen sind #18, #19, #20
5. `apps/api/src/prisma/prisma-tenant.extension.ts` — der reparierte Helfer
## Infrastructure State
- **alpha** (192.168.13.12, https://alpha.tessera.ctl.de): auf dem Stand von `ea003d4`,
Container am 2026-09-09 neu erstellt. `user-files` haengt als Volume
`tessera_user-files` am api-Container, nachgewiesen.
- **Die Serverdatei ist `docker-compose.prod.yml`**, nicht `docker-compose.yml` — die
`.env` setzt `COMPOSE_FILE`. Sicherungen liegen als `.bak.20260909-0818` daneben.
- **git push** geht ausschliesslich ueber `localhost:3002`; die Push-URL des Remotes ist
seit dieser Sitzung dauerhaft darauf gesetzt, ein schlichtes `git push` genuegt.
- **Lokal**: `api`, `db` und `web` laufen; es gibt KEINEN mailhog-Container, deshalb
scheitert der Mailversand lokal mit `ENOTFOUND mailhog` — das ist umgebungsbedingt und
kein Defekt.
- **Worktree-Isolation ist abgeschaltet** (`workflow.use_worktrees=false`), weil
`origin/HEAD` in diesem Repo nicht aufloesbar ist und ein isolierter Baum von einem
veralteten Stand abzweigen wuerde.
## Pre-Execution Critique Required
Bevor Etappe 2 beginnt, ist die Antwort auf diese Frage schriftlich festzuhalten:
**Woran wuerde ich merken, dass eine umgestellte Abfrage jetzt zu WENIG liefert statt zu
viel?** Der Umbau dreht die Fehlerrichtung um. Bis heute war der Fehlerfall "sieht zu
viel"; nach der Umstellung ist er "sieht nichts" — und der still gefaehrlichste Ort
dafuer ist jeder Code, der Leere als Abwesenheit deutet und daraufhin loescht. Vor der
Umstellung eines Bereichs ist zu pruefen, ob er solchen Code enthaelt.
1. WINDOWS #27 schliessen — Detektor in `rls-access-inventory.spec.ts` um
`include:`/`select:`/`_count:` auf Modellnamen erweitern, Zieltabelle als eigene
Fundstelle fuehren. Zwingend vor Etappe 4.
2. Etappe 3 planen (drei Teile): (a) Anmeldenamen pro Mandant — Schema-Aenderung,
Anmeldeweg muss den Mandanten VOR der Suche kennen, SECURITY-DEFINER-Funktionen
mit zwei Gleichheitsbedingungen; (b) Benutzerdimension — `app.current_user`,
`current_user_id()`, forTenant() erweitern, Regeln der zehn nutzerbezogenen
Tabellen; (c) Systemkontext fuer die sechs Hintergrunddienst-Faelle.
3. Etappe 4 — Scharfschalten. NUR nach Rueckfrage beim User.
Frische Sitzung, dann `/gsd-resume-work`.