939c8121a1
- 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
230 lines
13 KiB
Markdown
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.
|