Files
tessera-ctl/docs/mandantentrennung-etappe3-auftrag.md
T
schalli 926359b067
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
docs: Auftrag fuer Etappe 3 der Mandantentrennung, gemessen statt erinnert
2026-09-11 16:35:05 +02:00

185 lines
9.8 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
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
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.
## 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.