Files
tessera-ctl/docs/mandantentrennung-etappe3-auftrag.md
schalli 939c8121a1
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
docs(quick-260914-eym): Etappe 3c abgeschlossen — Kritikschrift, Klassifikation, Auftrag, Datenbankrolle, WINDOWS #21/#30 geschlossen, Single-Flight-Riegel als Eintrag
- Kritikschrift: neuer Abschnitt "## Systemkontext (Etappe 3c, 260914-eym)"
  mit (y1) woertlicher Werkzeugausgabe und pg_policies der lebenden DB,
  (y2) Signaltabelle beider Fehlerrichtungen samt Rueckbau-Belegen (a)-(d),
  (y3) Leere-als-Abwesenheit je Pfad (kein Pfad loescht), (y4) bewusst
  nicht geloest, (y5) bewusst nicht angefasst; Nachtraege in (d4), (s4),
  (b4) und im Abschluss
- Klassifikation: Uebersichtstabelle mit dritter Spalte System, Werte
  nachgerechnet (61/179/5), Stand-Absatz 260914-eym (72 Paare, Klassen
  unveraendert, sieben Staende geaendert), sechs Regelschluesse im
  Hintergrunddienst-Abschnitt, admin-seed-Zeile mit 3c-Befund, 3c-Punkt
  unter "NICHT entscheidet" erledigt
- Auftrag: 3c als Erledigt vermerkt (3d64567/6e2a641), zwei neue Fallen
  unter "Werkzeuge und Fallen"
- Datenbankrolle: dritte Sitzungsvariable, Nachtrag zum Systemkontext und
  zur weiterhin gueltigen Vorher-Pruefung ohne-kontext-leer
- Ledger (ueber gsd-tools windows): #21 fixed, #30 fixed, #37 neu
  (prozessweiter Single-Flight-Riegel processInbox) — open 15 / waived 1 /
  fixed 21 / total 37, aus den Zeilen gezaehlt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-14 11:52:47 +02:00

230 lines
13 KiB
Markdown

# Mandantentrennung — Etappe 3: Auftrag fuer die naechste Sitzung
Geschrieben am 2026-09-11 am Ende der Sitzung, die Etappe 2 abgeschlossen hat.
Zweck: Eine frische Sitzung soll Etappe 3 ohne Rueckfrage und ohne Neuermittlung
der Grundlagen beginnen koennen. Alles hier ist gemessen, nicht erinnert.
## Randbedingungen vom User (2026-09-11)
- **Dienstag, 2026-09-15, geht die erste voll funktionsfaehige Version live.**
Live-Gehen braucht den Schalter NICHT — heute laeuft alpha mit dem
BYPASSRLS-Stand und einem Mandanten, und das ist fuer den Betrieb
unerheblich. Etappe 3 darf das Live-Gehen nicht blockieren.
- **Nicht nachfragen.** Alles machen, was ohne den User geht. Was nicht ohne
ihn geht, am Ende benennen.
- **Beim Scharfschalten (Etappe 4) anhalten und fragen.** Das ist die einzige
Ausnahme, und sie steht seit dem 2026-09-09.
- **Nach jedem abgeschlossenen Durchlauf pushen** (`git push` genuegt, die
Push-URL zeigt auf localhost:3002).
## Stand beim Einstieg
- Etappe 2 abgeschlossen, alle zwoelf Bereiche gebunden, jeder einzeln
verifiziert. Endstand 994 Tests / 62 Dateien, 137 Live-Pruefungen,
Klassifikation 65 Paare / 68 ungebunden / 178 gebunden.
- WINDOWS #27 (Relations-Blindstelle der Bestandsaufnahme) ist als Quick-Task
`260911-mkj` in Arbeit oder abgeschlossen — `git log` und
`.planning/quick/260911-mkj-*/` pruefen. Nach dessen Abschluss: 72 Paare.
- Der Schalter ist AUS. `DATABASE_URL` zeigt auf Rolle `tessera`.
## Die zwei Produktentscheidungen (User, 2026-09-10)
1. **Anmeldenamen pro Mandant eindeutig**, nicht plattformweit. `m.schmidt`
darf es bei Firma A und Firma B geben.
2. **Kollegen derselben Firma strikt getrennt.** Jeder sieht nur seine eigenen
gespeicherten Suchen, Favoriten, Dashboard-Anordnung.
## Etappe 3 in drei Teilen — empfohlene Reihenfolge
### 3b zuerst: Benutzerdimension in den Regeln
**Erledigt (260911-nke, f0b531b/07fc653 plus der Dokumentationscommit dieser
Aufgabe):** Migration
`20260911120000_rls_user_dimension_personal_tables` bringt `current_user_id()`
und die Benutzerdimension in die Regeln der zehn persoenlichen Tabellen;
`forTenant(prisma, tenantId, userId?)` bekommt den optionalen dritten
Parameter, 34 Nutzer-CRUD-Aufrufstellen in acht Diensten reichen ihn durch.
Gemessene Zahl der umgedrehten Loch-Pruefungen: SECHS, nicht drei wie unten
noch angenommen. Endzahlen: Tests 1020/62 Dateien, Werkzeug
`rls-scratch-check.mjs` 203/203 bestanden (Baseline vor diesem Lauf: 137).
Siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
"Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)". Der ursprüngliche
Auftragstext unten bleibt unveraendert stehen (historische Planungsgrundlage).
Warum zuerst: reiner Datenbank- und Helfer-Umbau, beruehrt den Anmeldeweg
NICHT, und schliesst die Klasse von Befunden, die in sieben Bereichen als
"Policy hat keine Benutzerdimension" festgehalten wurde.
Gemessene Grundlagen:
- `forTenant(prisma, tenantId)` in `apps/api/src/prisma/prisma-tenant.extension.ts:111`
setzt genau EINE Sitzungsvariable: `set_config('app.current_tenant', ..., true)`.
`current_tenant_id()` ist in `20260618112133_rls_policies` definiert.
- Es gibt KEIN `app.current_user` und KEIN `current_user_id()` — nirgends in
Migrationen oder Quelltext.
- **14 Modelle tragen eine `userId`-Spalte:** CalendarSource, DashboardLayout,
FavoriteLink, GroupMembership, ModuleGrant, PasswordResetToken, SearchProvider,
TenderEmailConfig, TenderMatch, TenderNotificationPref, TenderRssFeedSource,
TenderSavedSearch, TenderTriage, WidgetInstance. NICHT alle davon sind
"persoenliche Daten": GroupMembership und ModuleGrant sind
Verwaltungsobjekte (ein Admin darf sie fuer andere sehen),
PasswordResetToken ist ein Anmelde-Artefakt, TenderMatch wird vom
Hintergrunddienst je Treffer geschrieben. Die Benutzerdimension gehoert
auf die ZEHN echten Nutzerobjekte; welche das sind, ist je Modell zu
entscheiden und am Ort zu begruenden — Vorgabe aus den Bereichs-Kritiken:
CalendarSource, DashboardLayout, FavoriteLink, SearchProvider (persoenliche
Zeilen), TenderEmailConfig, TenderNotificationPref, TenderRssFeedSource
(persoenliche Zeilen), TenderSavedSearch, TenderTriage, WidgetInstance.
- Der Anwendungscode trennt heute bereits korrekt nach Benutzer (in jedem
Bereich stichprobenartig belegt, Besitzpruefungen in ldap/dkv/dashboard/
calendar/favorites/auth als Tests festgenagelt). Die Datenbank tut es
nicht. Das zweite Netz fehlt.
Bauform, an der es sich zu orientieren gilt:
- Zweite Sitzungsvariable `app.current_user`, Funktion `current_user_id()`
nach dem Muster von `current_tenant_id()`.
- `forTenant()` bekommt einen optionalen dritten Parameter `userId` (oder
ein Schwesterhelfer `forTenantAndUser()` — Entscheidung im Plan, mit
Begruendung; Praezedenz fuer "Helfer erweitern statt zweiten bauen" ist
`withTenantTransaction()`). Hintergrunddienste und Verwaltungswege rufen
weiter ohne userId; nur die Nutzer-CRUD-Wege setzen ihn.
- Regeln der zehn Tabellen: Lesen `tenantId = current_tenant_id() AND
(current_user_id() IS NULL OR userId = current_user_id())` — damit ein
Aufruf OHNE gesetzten Benutzer (Admin, Hintergrunddienst) weiter alles
des Mandanten sieht. Schreiben analog. Das IS-NULL-Muster ist die Form aus
der #19-Loesung (260910-jab), dort fuer plattformweite Zeilen.
- Messen, nicht annehmen: `rls-scratch-check.mjs` bekommt einen Abschnitt
je umgestellter Tabelle, ueber den GENERIERTEN Client, mit
schemagleicher Wegwerf-Tabelle (Spaltenvergleich zur Laufzeit —
`createdAt`/`updatedAt`-Falle aus 260910-krx).
- Die drei loch-behauptenden Pruefungen aus `tenders` und `dashboard`
("Policies haben keine Benutzerdimension", z. B.
`tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar`)
UMDREHEN, nicht loeschen — Muster aus 260910-jab.
### 3a danach: Anmeldenamen pro Mandant
Warum danach: aendert das Schema UND den Anmeldeweg, ist der riskanteste
Teil, und braucht 3b nicht.
Gemessene Grundlagen:
- `User.username String @unique` und `User.email String? @unique`
(`schema.prisma:31-32`) — plattformweit.
- Die drei SECURITY-DEFINER-Funktionen in
`20260909160000_auth_lookup_functions`: `auth_lookup_user_by_username(p_username)`,
`auth_lookup_user_by_email(p_email)`, `auth_lookup_reset_token(p_token)`.
Jede sucht ueber EINE Gleichheitsbedingung mit `LIMIT 1`. Der Mandant
kommt erst AUS der gefundenen Zeile.
- Aufrufer: `auth.service.ts:95,110,218` und `user.service.ts:33`.
- Bewusst ungebundene Stellen, die genau an dieser plattformweiten
Eindeutigkeit haengen und mit 3a fallen: `ldap.service.ts`
`resolveEmailForWrite` (260909-ipc, Befund A), die P2002-Kollisionskette in
`tenders`/`user` (unsichtbare Zeile -> falsches "frei" -> harter Fehler),
`user.service.ts` `findByUsername` (null Aufrufer, 260910-das).
Bauform:
- Schema: `@@unique([tenantId, username])`, `@@unique([tenantId, email])`
statt `@unique`. Migration mit Datenpruefung davor (heute ein Mandant,
also keine Kollision moeglich — trotzdem messen).
- Der Anmeldeweg muss den Mandanten kennen, BEVOR er die Zeile sucht.
Zwei uebliche Wege: (i) eigene Adresse je Mandant (Subdomain
`firma-a.tessera.ctl.de` -> Mandant aus dem Host), (ii) Mandantenwahl
beim Login. **Das ist eine Produktfrage, die der User NICHT beantwortet
hat.** Empfehlung fuer die Planung: (i), weil Tessera hinter Nginx Proxy
Manager laeuft und Subdomains dort trivial sind, und weil (ii) den
Anmeldenamen als Geheimnis schwaecht. Wenn die Planung das anders sieht,
ist es einer der Punkte, die am Ende dem User genannt werden.
- Die drei Funktionen bekommen eine zweite Gleichheitsbedingung
(`p_tenant_id`) — ENGER, nicht weiter. Die Kopfkommentare in
`docs/mandantentrennung-datenbankrolle.md` nachziehen.
- Bis der Mandant vor der Suche bekannt ist, laesst sich 3a NICHT
scharfschalten. Deshalb ist 3a fuer Dienstag NICHT noetig: mit einem
Mandanten ist plattformweit = pro Mandant.
### 3c zuletzt: Systemkontext fuer die Hintergrunddienste
**Erledigt (260914-eym, 3d64567/6e2a641 plus der Dokumentationscommit dieser
Aufgabe):** Migration `20260914120000_rls_system_context_read` bringt
`is_system_context()` (COALESCE, STABLE) und je eine zusaetzliche, NUR
lesende Regel `system_read_policy ... FOR SELECT` auf FUENF Tabellen —
DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch
(nicht sechs: SmtpConfig traegt keine, weil der Mail-Startpfad ENTFERNT und
nicht umgestellt wurde). Helfer `forSystem(prisma)` als Schwesterhelfer von
`forTenant()` (setzt `app.system_context = 'true'` und die beiden anderen
Variablen ausdruecklich leer; `forTenant()`/`withTenantTransaction()` setzen
umgekehrt `app.system_context = ''`). Die sechs Faelle: DKV-Planer je
Mandant (Auftrag `dkv-inbox-poll:<tenantId>`, WINDOWS #21 geschlossen);
Mail-Transport je Versand nach Mandant des Empfaengers, Startpfad und
Mailer-Fabrik geloescht (WINDOWS #30 geschlossen); ldap mit ZWEI
Systemkontext-Lesern (`getAllActiveConfigs`, Nachverschluesselung — die
Schreibzeile je Altzeile gebunden); digest und matching ueber den
Systemkontext, Schleifen gebunden; admin-seed nur dokumentiert (liest
ausserhalb der Schleife nur `Tenant`, keine Regel, Datei unveraendert).
Detektor mit fuenfter Erkennungsform und Erlaubnisliste (4 Dateien, 5
Aufrufe, exakt). Endzahlen: Tests 1054/64 Dateien, Typpruefung sauber,
Werkzeug `rls-scratch-check.mjs` 253/253 bestanden (Baseline vor diesem
Lauf: 203). Siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`,
Abschnitt "## Systemkontext (Etappe 3c, 260914-eym)" mit (y1)-(y5). Der
urspruengliche Auftragstext unten bleibt unveraendert stehen (historische
Planungsgrundlage).
Sechs Faelle, alle im Abschnitt "Der Hintergrunddienst als Falle" der
Klassifikation und in den Bereichs-Kritiken:
- `dkv-scheduler` / `loadAnyActiveConfigForScheduler()` (WINDOWS #21) —
heute bereits falsch (willkuerlicher Mandant).
- `mail.module` / `loadAnySmtpConfigForStartupTransport()` (WINDOWS #30) —
dieselbe Form.
- `ldap-config.service` `getAllActiveConfigs()` / `onApplicationBootstrap`
— heute korrekt, verstummt spaeter.
- `tender-digest.scheduler`, `tender-matching.service` — uebergreifende
Haelften an Etappe 3 uebergeben (260909-laa).
- `admin-seed.service` `ensureDefaultGroupsForAllTenants` — Schleife ueber
alle Mandanten, Rumpf bereits gebunden.
Bauform: Ein benannter Systemkontext (z. B. `forSystem(prisma)`), der eine
DRITTE Sitzungsvariable `app.system_context = 'true'` setzt, und Regeln,
die diesen Kontext fuer LESENDE Zugriffe zulassen (`OR current_setting(
'app.system_context', true) = 'true'`). Schreibzugriffe innerhalb der
Schleife bleiben je Mandant gebunden. Der DKV- und der SMTP-Startpfad
werden dann zu "einmal-abfragen-viele-bedienen" — das ist die in 07-04
zurueckgestellte Mehrmandanten-Planung und der einzige echte
Funktionsausbau in Etappe 3.
## Was NICHT ohne den User geht
- **3a, Weg (i) vs. (ii):** wie der Mandant beim Login bestimmt wird.
- **Etappe 4:** Scharfschalten. Ausdruecklich.
## Werkzeuge und Fallen (aus Etappe 2, jede mindestens einmal erlebt)
- `git status` ist die Wahrheit, nicht der Agentenbericht. Zwei Agenten
brachen am Sitzungslimit NACH getaner Arbeit ab.
- Ein `grep -c` ist eine Behauptung. Vier Kopfzahlen schrumpften beim
Hineinsehen.
- Eine nicht committete Messung ist kein Beleg (zweimal passiert).
- Zusammenfassungen behaupten N, wo N-1 geliefert ist (zweimal passiert).
- Handgepflegte Dokumentstellen werden uebersprungen — Gates ABLEITEN.
- Roh-SQL ist nicht der generierte Client (Wegwerf-Tabelle ohne
`createdAt`/`updatedAt`).
- Ein Plan-Pruefer, der "plausibel" sagt, hat nicht geprueft.
- `git add` mit mehreren Pfaden, einer davon geloescht, scheitert still.
- Der Datenbank-Container hat keinen 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).
- Kein `mailhog` lokal — `ENOTFOUND mailhog` ist Umgebung, kein Defekt.
- Backticks in Heredoc-Python werden von der Shell ausgewertet — Skripte
in eine Datei schreiben, dann ausfuehren.
- Eine Mock-Fabrik ohne den neuen Export wirft erst beim ZUGRIFF
(vitest-Proxy) — jede Spec, deren Pruefling `forSystem` importiert,
braucht den Export im Mock (260914-eym: sechs Spec-Dateien).
- Ein Gate mit `grep -rh ... | grep -v spec` filtert KEINE Spec-Dateien
(`-h` laesst den Dateinamen weg, `spec` steht nicht im Zeilentext) —
Proben in einer Spec zaehlen mit; Empfaengernamen in Proben deshalb
anders waehlen als im Produktivcode (260914-eym, `sysPrisma`).
## Einstieg
`/gsd-resume-work`, dann `/gsd-quick` fuer 3b mit `--validate`. Planer,
Plan-Pruefer, Executor, Verifizierer — die Kette vollstaendig, jede
Lieferung wurde in Etappe 2 vom jeweils naechsten Schritt gefangen, nie
vom eigenen.