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