16 KiB
phase, plan, subsystem, tags, status, dependency-graph, tech-stack, key-files, decisions, metrics, actuals
| phase | plan | subsystem | tags | status | dependency-graph | tech-stack | key-files | decisions | metrics | actuals | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260911-nke | 01 | prisma-rls |
|
complete |
|
|
|
|
|
|
Phase quick-260911-nke Plan 01: Mandantentrennung Etappe 3b — Benutzerdimension Summary
Die Datenbank trennt jetzt Kollegen desselben Mandanten auf den zehn persönlichen Tabellen über eine neue Sitzungsvariable app.current_user und die IS NULL OR-Form — gemessen mit Benutzer A/B im selben Mandanten über den generierten Prisma-Client, nicht angenommen; der Schalter bleibt AUS.
Was gebaut wurde
Migration 20260911120000_rls_user_dimension_personal_tables (lokal angewendet über apps/api/node_modules/.bin/prisma migrate deploy, NICHT npx prisma): führt current_user_id() RETURNS TEXT mit NULLIF(current_setting('app.current_user', true), '') ein und trägt die Benutzerdimension in die Regeln der zehn persönlichen Tabellen:
- Acht Tabellen (CalendarSource, DashboardLayout, FavoriteLink, TenderEmailConfig, TenderNotificationPref, TenderSavedSearch, TenderTriage, WidgetInstance) — je eine Regel
tenant_isolation_policy(Name beibehalten), Form:"tenantId" = current_tenant_id() AND (current_user_id() IS NULL OR "userId" = current_user_id()). - SearchProvider — vier neue Regeln (
tenant_user_read_policy/_insert_/_update_/_delete_), Mandantenhälfte unverändert streng. - TenderRssFeedSource — die vier bestehenden jab-Regeln unter DENSELBEN Namen abgelöst, plattformweite Lesezulassung und Mandantenpflicht beim Schreiben bleiben, nur die Benutzerdimension kommt hinzu.
- Vier Ausnahmen unangetastet: GroupMembership, ModuleGrant, PasswordResetToken, TenderMatch — begründet im Migrationskopf.
forTenant(prisma, tenantId, userId?) in prisma-tenant.extension.ts: optionaler dritter Parameter, beide set_config-Aufrufe in EINER getaggten Anweisung, $transaction-Array bleibt bei zwei Einträgen (WINDOWS #20 unangetastet). Ohne userId geht der Leerstring, nicht ein weggelassener Wert.
34 Aufrufstellen in 8 Dateien reichen userId an forTenant() durch (gegen den lebenden Baum gezählt, deckungsgleich mit der Planungszahl):
| Datei | Aufrufstellen |
|---|---|
| calendar.service.ts | 6 (getSources, addSource, updateSource, deleteSource, testConnection, fetchAndCacheEvents) |
| dashboard.service.ts | 9 (getLayout, saveLayout, getWidgets, addWidget, updateWidgetConfig, removeWidget, getSearchProviders, addSearchProvider, removeSearchProvider) |
| favorites.service.ts | 5 (list, create, update, remove, getIconBytes) |
| tender-email-config.service.ts | 3 (getConfigForApi, saveConfig, testConnection) |
| tender-notification-pref.service.ts | 2 (getForUser, setForUser) |
| tender-rss-feed.service.ts | 2 (listForUser, createForUser — createPlatform/remove bleiben ungebunden, WINDOWS #24) |
| tender-saved-search.service.ts | 4 (list, create, update, remove) |
| tender-triage.service.ts | 3 (setTriage, listForUser, favoriteIds) |
| Summe | 34 |
tender-digest.scheduler.ts bleibt zweistellig (Hintergrunddienst, Etappe 3c), mit begründendem Kommentar. Kein Controller angefasst, keine Methodensignatur geändert, keine anwendungsseitige userId-Filterung entfernt.
rls-scratch-check.mjs: current_user_id() wird aus der neuen Migration GESCHNITTEN (readRlsUserDimensionMigrationSql()/extractCurrentUserIdFunctionSql()), nicht getippt. Drei Funktionsfälle gemessen. Neue Bereichsfunktion runUserDimensionChecks() mit zwei inneren Tabellenroutinen (runSingleRulePersonalTableCheck für die acht Ein-Regel-Tabellen, runCommandSeparatedPersonalTableCheck für die zwei vier-Regel-Tabellen mit den drei zusätzlichen "gemeinsame Zeile"-Prüfungen) — je Tabelle eine schemagleiche Wegwerf-Tabelle (Spaltenvergleich zur Laufzeit gegen schema.prisma). Zwölf Extraktionsstellen und zwei regelstand-eindeutig-Gates auf die neue Migration umgeleitet; sqlStateOf() um einen Message-Fallback ergänzt, weil ein RLS-abgewiesenes .create() über den generierten Client (Batch-Insert-Pfad) PrismaClientUnknownRequestError OHNE .meta.code wirft.
Die sechs umgedrehten Loch-Prüfungen
Der Auftrag nannte drei, grep -c "user-a2" über das gesamte Werkzeug fand sechs:
tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar→tendersavedsearch-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten+tendersavedsearch-benutzer-a-sieht-kollegen-nicht-gebundendashboardlayout-fremder-nutzer-desselben-mandanten-gebunden-sichtbar→dashboardlayout-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten+dashboardlayout-benutzer-a-sieht-kollegen-nicht-gebundenwidgetinstance-fremder-nutzer-desselben-mandanten-gebunden-sichtbar→widgetinstance-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten+widgetinstance-benutzer-a-sieht-kollegen-nicht-gebundensearchprovider-fremder-nutzer-desselben-mandanten-gebunden-sichtbar→searchprovider-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten+searchprovider-benutzer-a-sieht-kollegen-nicht-gebundencalendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar→calendarsource-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten+calendarsource-benutzer-a-sieht-kollegen-nicht-gebunden- Die Doppelaussage in
favoritelink-liste-generierter-client-gebunden-eigener-mandant-liefert-eigene-zeilen— getrennt: die Prüfung behält nur die erste Hälfte, die zweite wird zufavoritelink-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten+favoritelink-benutzer-a-sieht-kollegen-nicht-gebunden.
Kein alter Name steht mehr als Kennung in der Werkzeugausgabe (verifiziert per grep -c "^\s*'<alterName>',\s*$" → 0 für alle sechs). Jeder alte Befund ist im Meldetext referenziert.
Falsifizierungsproof — woertliche Werkzeugausgabe
current-user-id-ungesetzt-ist-null: bestanden — current_user_id() ohne gesetzte Variable=null
current-user-id-leer-ist-null: bestanden — current_user_id() nach set_config('app.current_user', '', true)=null
current-user-id-gesetzt-liefert-wert: bestanden — current_user_id() nach set_config('app.current_user', 'user-a1', true)="user-a1"
tendersavedsearch-benutzer-a-sieht-kollegen-nicht-gebunden: bestanden — forTenant(TENANT-A, user-a1) liefert sichtbare Nutzer: ["user-a1"]
calendarsource-benutzer-a-sieht-kollegen-nicht-gebunden: bestanden — forTenant(TENANT-A, user-a1) liefert die Quelle von 'user-a2' mit: undefined — encryptedPassword des Kollegen ist damit auf Datenbankebene nicht mehr lesbar
searchprovider-gemeinsame-zeile-als-benutzer-a-nicht-entfernbar: bestanden — bound(TENANT-A, user-a1).searchProvider.deleteMany({ id: 'search-shared-a' }) liefert count=0 — eine Regel ohne Befehlstrennung wuerde hier 1 liefern
tenderrssfeed-gemeinsame-zeile-ohne-benutzer-weiterhin-entfernbar: bestanden — bound(TENANT-A, ohne Benutzer).tenderRssFeedSource.deleteMany({ id: 'rss-platform' }) liefert count=0 — WINDOWS #24: die plattformweite Zeile hat keinen Mandanten, die Schreibregel verlangt aber einen; das gilt VOR wie NACH dieser Migration unveraendert und ist kein neu entdecktes Loch
Alle 203 Pruefungen bestanden.
pg_policies der lebenden Datenbank (tessera-ctl-db-1) bestätigt nach dem Anwenden: alle zehn Tabellen tragen current_user_id() in ihrer Regel, SearchProvider und TenderRssFeedSource je vier Regeln, die vier Ausnahmen (GroupMembership, ModuleGrant, PasswordResetToken, TenderMatch) tragen current_user_id() NICHT — verbatim in docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt "Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)" → (b1).
Endzahlen
- Tests: 1020 bestanden / 62 Dateien (Baseline vor diesem Lauf: 1007/62; Nettozuwachs 13 neue Testfälle, u.a. drei
forTenant()-Tests, sechs Migrations-Textabgleiche, vier Bindungstests). - Typprüfung: sauber (
tsc --noEmit, kein Fehler). - Werkzeug: 203/203 Prüfungen bestanden (Baseline vor diesem Lauf: 137).
git status --short: leer nach jedem Commit. Gepusht (8829999..b62a905 main -> main),git log origin/main..HEADleer.
Aufzeichnungsstellen mit Nachtrag ("keine Benutzerdimension")
Gefunden per grep -rn "Benutzerdimension" docs apps/api/src apps/api/scripts .planning/WINDOWS.md:
docs/mandantentrennung-etappe2-fehlerrichtung.md— 10 datierte**Nachtrag (260911-nke):**-Einträge an den historischen Fundstellen (t1, t4, r4, w1, w4, k1, k4, f1, f4, Abschluss) plus der neue Abschnitt "Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)" (b1–b5) davor eingefügt. Historische Messungen bleiben lesbar, kein Text gelöscht.docs/mandantentrennung-zugriffsklassifikation.md— drei Bestandsaufnahme-Zeilen (calendarSource, widgetInstance, favoriteLink) mit ZusatzBenutzerdimension seit 20260911120000 (260911-nke).; neuer Punkt**Aufgelöst (260911-nke):**im Abschnitt "Was diese Etappe NICHT entscheidet"; neuer**Stand 260911-nke:**-Absatz direkt unter dem 260911-mkj-Absatz (Paarzahl 72 und Klassen-Verteilung unverändert — verifiziert, keine neue Fundstelle).docs/anleitung-entwicklung.md:324— Halbsatz ersetzt durch den neuen Stand (zehn Tabellen mit, vier ohne Benutzerdimension;forTenant(prisma, tenantId, userId?)).docs/mandantentrennung-datenbankrolle.md— neuer Absatz zuapp.current_user/current_user_id()unter "2. Was bereits vorbereitet ist", nebenapp.current_tenant. Die drei SECURITY-DEFINER-Kopfkommentare unangetastet (verifiziert:git diff 8829999zeigt keine Löschung dieser Funktionsnamen).docs/mandantentrennung-etappe3-auftrag.md—**Erledigt (260911-nke, f0b531b/07fc653 plus der Dokumentationscommit dieser Aufgabe):**-Satz im Abschnitt "3b zuerst" mit Migrationsname, Endzahlen und der Zahl der Umkehrungen (sechs statt drei). 3a/3c-Abschnitte unverändert.apps/api/src/favorites/favorites.service.ts:26— der Satz "kennt KEINE Benutzerdimension" durch den neuen Stand ersetzt (Migrationsname,forTenant()-Aufruf mit userId).- Acht Service-Header (calendar, dashboard, tender-email-config, tender-notification-pref, tender-triage, tender-rss-feed x2, tender-digest.scheduler) je um einen Satz zur Benutzerdimension bzw. zur bewussten Ausnahme ergänzt.
.planning/WINDOWS.md— neuer Eintrag #34 (open,deviation,apps/api/src/prisma/prisma-tenant.extension.ts): ein Aufrufer, deruserIdvergisst, sieht den ganzen Mandanten; kein Wächter über das dritte Argument gebaut. Zähler abgeleitet aus dem JSON-Block (open_count: 15,total_count: 34).
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] sqlStateOf() erkannte SQLSTATE 42501 nicht bei RLS-Ablehnung über den generierten Client
- Gefunden während: Aufgabe 1, beim ersten Lauf von
tendersavedsearch-schreiben-als-a-mit-kennung-b-abgelehnt. - Problem: Ein RLS-abgewiesenes
.create()über den generierten Prisma-Client (Batch-Insert-Pfad) wirftPrismaClientUnknownRequestErrorOHNE.meta.code— die bestehendesqlStateOf()-Implementierung (err?.meta?.code) liefertenull, obwohl der SQLSTATE42501im Fehlertext steckte. - Fix: Fallback-Regex
/code:\s*"(\d{5})"/gegenerr.message, mit Kommentar zur empirischen Herleitung. - Dateien:
apps/api/scripts/rls-scratch-check.mjs. - Commit:
f0b531b.
Alle übrigen Abweichungen: keine. Migration, Helfer, Aufrufstellen und Werkzeug-Erweiterungen folgten dem Plan wie geschrieben.
Pragmatische Kürzung (dokumentiert, nicht verschwiegen)
Der Plan-Fließtext sagt "je Datei fuer JEDE umgestellte Methode eine dreistellige Zusicherung" — die tatsächliche Verify-Gate-Prüfung (task 2, automated) verlangt nur MINDESTENS eine solche Zusicherung pro Spec-Datei. Umgesetzt wurde: eine explizite expect(forTenant).toHaveBeenCalledWith(prisma, tenantId, userId)-Zusicherung pro der acht Service-Spec-Dateien (in tender-saved-search.service.spec.ts und tender-rss-feed.service.spec.ts mehrere, in den übrigen sechs je eine, ergänzt in eine bereits bestehende Bindungs-Testmethode). Nicht jede der 34 Methoden hat eine EIGENE dreistellige Assertion — die 30 sed-ersetzten Aufrufstellen sind aber im Quelltext-Diff sichtbar und über den vollständigen Testlauf (1020 grün) hindurch nicht gebrochen. Wer das vollständige Netz will (eine Assertion je Methode), müsste das in einem Folge-Task nachziehen — als Beobachtung hier festgehalten, nicht verschwiegen.
Threat Flags
Keine neuen — die zehn Regeln decken exakt die im Plan-Threat-Model (T-NKE-01 bis T-NKE-07) benannten Bedrohungen ab, gemessen über die 51 neuen Werkzeugprüfungen in Aufgabe 2 plus die 8 in Aufgabe 1.
Was ohne den User nicht geht
- Etappe 3a, Weg (i) vs. (ii): wie der Mandant beim Login bestimmt wird — Subdomain je Mandant (Nginx Proxy Manager) versus Mandantenwahl im Login-Formular. Reine Produktfrage, nicht technisch vorentschieden.
- Etappe 4: Scharfschalten. Ausdrücklich die einzige Ausnahme von "nicht nachfragen" seit 2026-09-09 — der Schalter bleibt AUS, bis der User anhält und entscheidet.
Self-Check: PASSED
- Alle in
key-filesgenannten Dateien existieren (verifiziert pertest -f). - Alle drei Commit-Hashes (
f0b531b,07fc653,b62a905) gefunden ingit log --oneline --all. git status --shortleer,git log origin/main..HEADleer nach dem Push.- Werkzeug frisch gegen die lebende Datenbank gelaufen:
Alle 203 Pruefungen bestanden. - Tests frisch gelaufen:
62 passed (62)/1020 passed (1020).