- 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
13 KiB
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 pushgenuegt, 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-mkjin Arbeit oder abgeschlossen —git logund.planning/quick/260911-mkj-*/pruefen. Nach dessen Abschluss: 72 Paare. - Der Schalter ist AUS.
DATABASE_URLzeigt auf Rolletessera.
Die zwei Produktentscheidungen (User, 2026-09-10)
- Anmeldenamen pro Mandant eindeutig, nicht plattformweit.
m.schmidtdarf es bei Firma A und Firma B geben. - 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)inapps/api/src/prisma/prisma-tenant.extension.ts:111setzt genau EINE Sitzungsvariable:set_config('app.current_tenant', ..., true).current_tenant_id()ist in20260618112133_rls_policiesdefiniert.- Es gibt KEIN
app.current_userund KEINcurrent_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, Funktioncurrent_user_id()nach dem Muster voncurrent_tenant_id(). forTenant()bekommt einen optionalen dritten ParameteruserId(oder ein SchwesterhelferforTenantAndUser()— Entscheidung im Plan, mit Begruendung; Praezedenz fuer "Helfer erweitern statt zweiten bauen" istwithTenantTransaction()). 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.mjsbekommt 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
tendersunddashboard("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 @uniqueundUser.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 mitLIMIT 1. Der Mandant kommt erst AUS der gefundenen Zeile. - Aufrufer:
auth.service.ts:95,110,218unduser.service.ts:33. - Bewusst ungebundene Stellen, die genau an dieser plattformweiten
Eindeutigkeit haengen und mit 3a fallen:
ldap.service.tsresolveEmailForWrite(260909-ipc, Befund A), die P2002-Kollisionskette intenders/user(unsichtbare Zeile -> falsches "frei" -> harter Fehler),user.service.tsfindByUsername(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 indocs/mandantentrennung-datenbankrolle.mdnachziehen. - 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.servicegetAllActiveConfigs()/onApplicationBootstrap— heute korrekt, verstummt spaeter.tender-digest.scheduler,tender-matching.service— uebergreifende Haelften an Etappe 3 uebergeben (260909-laa).admin-seed.serviceensureDefaultGroupsForAllTenants— 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 statusist die Wahrheit, nicht der Agentenbericht. Zwei Agenten brachen am Sitzungslimit NACH getaner Arbeit ab.- Ein
grep -cist 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 addmit mehreren Pfaden, einer davon geloescht, scheitert still.- Der Datenbank-Container hat keinen Host-Port: IP per
docker inspectfrisch ermitteln,tessera:tessera_dev. Prisma-Binary ausapps/api/node_modules/.bin/prisma, NICHTnpx prisma(zieht Prisma 8). - Kein
mailhoglokal —ENOTFOUND mailhogist 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
forSystemimportiert, braucht den Export im Mock (260914-eym: sechs Spec-Dateien). - Ein Gate mit
grep -rh ... | grep -v specfiltert KEINE Spec-Dateien (-hlaesst den Dateinamen weg,specsteht 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.