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

9.8 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 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.