docs(quick-260914-eym): Etappe 3c abgeschlossen — Kritikschrift, Klassifikation, Auftrag, Datenbankrolle, WINDOWS #21/#30 geschlossen, Single-Flight-Riegel als Eintrag
- 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
This commit is contained in:
+24
-10
@@ -1,10 +1,10 @@
|
||||
---
|
||||
schema_version: 1
|
||||
open_count: 16
|
||||
open_count: 15
|
||||
waived_count: 1
|
||||
fixed_count: 19
|
||||
total_count: 36
|
||||
last_updated: 2026-09-14T08:38:12.619Z
|
||||
fixed_count: 21
|
||||
total_count: 37
|
||||
last_updated: 2026-09-14T09:51:24.295Z
|
||||
---
|
||||
|
||||
# Broken Windows Ledger
|
||||
@@ -35,7 +35,7 @@ last_updated: 2026-09-14T08:38:12.619Z
|
||||
| 18 | 2 | unmet-truth | docker-compose.yml | | Die Mandantentrennung auf Datenbankebene ist wirkungslos, weil die Anwendungsrolle sie umgeht. Die API verbindet laut docker-compose.yml:33 als Rolle 'tessera'; diese Rolle hat auf alpha rolsuper=t UND rolbypassrls=t. PostgreSQL wendet Row-Level-Security auf solche Rollen grundsaetzlich nicht an — auch FORCE ROW LEVEL SECURITY aendert daran nichts, das erzwingt nur die Anwendung auf den Tabelleneigentuemer, nicht auf BYPASSRLS-Rollen. Am 2026-09-09 praktisch gemessen: ohne gesetztes app.current_tenant liefert 'SELECT count(*) FROM "Group"' zwei Zeilen, waehrend die Policy USING ("tenantId" = current_tenant_id()) bei NULL-Kontext null Zeilen liefern muesste. Damit sind alle sieben bisher mit RLS ausgestatteten Tabellen (User, Group, GroupMembership, LdapConfig, LdapFieldMapping, ModuleGrant, PasswordResetToken) faktisch ungeschuetzt; die Trennung haengt allein am manuellen 'where tenantId' im Anwendungscode. Die Migration 20260804130918 nennt RLS ausdruecklich 'ein zweites Sicherheitsnetz' — dieses Netz existiert derzeit nicht. Reihenfolge der Behebung: ZUERST eine eigene Anwendungsrolle ohne Superuser- und BYPASSRLS-Recht einrichten und die Anwendung darauf umstellen, DANN greifen die vorhandenen Policies, und ERST DANN lohnt es, fehlende Tabellen zu ergaenzen. Vorher gebaute Policies waeren wirkungslos und wuerden eine Sicherheit vortaeuschen. Kein akutes Risiko, solange Tessera nur intern und einmandantig laeuft (ein einziger Mandant 'default'), aber vor jedem Kundeneinsatz zwingend. NACHTRAG (260909-eor, Aufgabe 3): dieser Plan hat den fuer die Umstellung noetigen Anwendungscode klassifiziert (docs/mandantentrennung-zugriffsklassifikation.md, 227 Fundstellen / 59 Datei-Modell-Paare) und zusaetzlich einen weiteren, beim Anlegen dieses Eintrags noch nicht bekannten Defekt gefunden und behoben: forTenant() setzte den Mandantenkontext auf einer anderen Datenbankverbindung als die eigentliche Abfrage lief (siehe #20). #20 bleibt trotz nachgewiesener Reparatur bewusst OPEN, an dieselbe Bedingung gebunden wie dieser Eintrag — die Wirkung unter der echten Rolle ist erst nach dem Scharfschalten beobachtbar. | open | | 2026-09-09T07:42:13.878Z | |
|
||||
| 19 | 2 | unmet-truth | apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql | | Zwei der neuen Policies wuerden plattformweite Zeilen unsichtbar machen, sobald die Mandantentrennung scharf geschaltet wird. SearchProvider und TenderRssFeedSource haben ein nullable tenantId: Zeilen mit tenantId = NULL gelten fuer alle Mandanten (die von der Administration gepflegten Feeds und Suchanbieter). Die einfache Policy 'tenantId = current_tenant_id()' vergleicht NULL niemals gleich, diese Zeilen waeren nach der Aktivierung fuer JEDEN Mandanten weg — nicht nur fuer fremde. Heute ohne Wirkung, weil die Anwendung weiter als BYPASSRLS-Rolle verbindet (#18, Schalter bewusst aus). Beim Scharfschalten zwingend mitzuloesen, zusammen mit den 182 unskalierten Zugriffen: die Policy muss die plattformweiten Zeilen ausdruecklich einschliessen, etwa ueber 'tenantId IS NULL OR tenantId = current_tenant_id()' fuer den Lesezugriff, waehrend Schreibzugriffe weiterhin einen Mandanten verlangen. Beim Schreiben der Migration am 2026-09-09 aufgefallen und bewusst nicht eigenmaechtig anders geloest, weil die richtige Semantik eine Produktentscheidung ist. NACHTRAG (260909-eor, Aufgabe 3): docs/mandantentrennung-zugriffsklassifikation.md haelt diesen Befund im Abschnitt 'Zwei belegte Befunde' fest und benennt ihn als Blocker fuer Etappe 3. Die '182 unskalierten Zugriffe' sind ueberholt — die aktuelle, maschinell geprüfte Zahl ist 227 Fundstellen (59 Datei-Modell-Paare, siehe Klassifikationsdokument). NACHTRAG (260910-jab, Aufgabe 1/2): geschlossen durch Migration 20260910120000_rls_widen_membership_grant_and_platform_read — TenderRssFeedSource bekommt vier nach Befehl getrennte Regeln (tenant_platform_read_policy schliesst Zeilen ohne Mandant ausdruecklich ein, tenant_insert_policy/tenant_update_policy/tenant_delete_policy verlangen weiterhin einen Mandanten), lokal angewandt und am Systemkatalog der lebenden Datenbank gemessen. Belegt durch rls-scratch-check.mjs: tenderrssfeed-plattformzeile-gebunden-sichtbar, tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar, tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt, tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt, tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt (alle bestanden). Die Haelfte zur Suchanbietertabelle (SearchProvider) schliesst NICHT als geloestes Problem, sondern als WIDERLEGTE PRAEMISSE: es gibt lokal gemessen keinen Codeweg, der eine mandantenlose SearchProvider-Zeile erzeugt (dashboard.service.ts verlangt die Mandantenkennung als Pflichtparameter, die Vorgabe-Suchmaschinen sind Konstanten, 05-02) — die Regel bleibt deshalb bewusst unveraendert streng, belegt durch searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar (bestanden). Anwendungsseitig ist genau ein Pfad mitgebunden worden: TenderRssFeedSourceService.listForUser (Befund F) — ungebunden haette die Reparatur ihn sonst still auf nur die plattformweiten Zeilen reduziert. Was diese Reparatur NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite Zeile weder anlegen noch entfernen, in der alten wie in der neuen Regel — siehe WINDOWS #24, das diesen Rest als eigenen offenen Punkt fuehrt und nicht mit diesem Eintrag verschwindet. | fixed | | 2026-09-09T08:08:19.293Z | 2026-09-10T12:35:40.000Z |
|
||||
| 20 | 2 | unmet-truth | apps/api/src/prisma/prisma-tenant.extension.ts | | forTenant() setzt den Mandantenkontext auf einer anderen Verbindung als die Abfrage laeuft — die Mandantentrennung hat damit nie funktioniert, auch nicht dort, wo sie scheinbar benutzt wird. Die Erweiterung oeffnet prisma.$transaction, setzt app.current_tenant per set_config(..., true) auf tx, ruft dann aber query(args) auf, das ueber den AEUSSEREN Client dispatcht. set_config mit local=true gilt nur in der Transaktion und nur auf deren Verbindung. Am 2026-09-09 gegen die lokale Datenbank reproduziert: set_config landete auf Backend-PID 254999, die eigentliche Abfrage auf 255000, und dort war current_setting('app.current_tenant') NULL. Heute ohne sichtbare Folge, weil die Anwendungsrolle BYPASSRLS hat (#18) und deshalb ohnehin alles sieht. NACH dem Scharfschalten kehrt sich das um: die betroffenen Abfragen liefern dann NULL ZEILEN statt zu vieler. Besonders gefaehrlich in ldap.service.ts (Loeschzweig um Zeile 1559): der Sync deutet die Leere als 'Gruppe im Verzeichnis verschwunden' und loescht sie samt Mitgliedschaften und Modulfreigaben — aus einem stillen Trennungsfehler wuerde stiller Datenverlust. Zusatzbefund: von den 36 vermeintlichen forTenant-Vorkommen sind die meisten Kommentare, die erklaeren, warum forTenant FEHLT; echte Aufrufstellen sind 6, echte mandantengebundene Abfragen 9, alle in ldap.service.ts. Ausserdem setzen tenant.middleware.ts:44 und tenant.guard.ts:41 ein req.tenantPrisma, das in apps/api/src von NIEMANDEM gelesen wird. Muss vor jedem weiteren Umbau repariert werden, sonst baut alles Weitere auf einem Helfer auf, der nicht traegt. NACHTRAG (260909-eor, Aufgabe 1/4): der beschriebene Verbindungsfehler ist behoben (Array-Form von $transaction, prisma-tenant.extension.ts) und gegen eine Wegwerf-Datenbank mit einer Rolle ohne BYPASSRLS live nachgewiesen (rls-scratch-check.mjs, 8/8 Pruefungen bestanden). Bleibt dennoch bewusst OPEN, nicht fixed: die Wirkung unter der echten Anwendungsrolle tessera_app ist erst nach dem Scharfschalten (#18) beobachtbar — bis dahin bleibt #20 an dieselbe Bedingung gebunden wie #18 und #19. | open | | 2026-09-09T08:44:18.496Z | |
|
||||
| 21 | 2 | deviation | apps/api/src/dkv/dkv-scheduler.service.ts | | DKV-Planer-Startpfad (DkvSchedulerService.onModuleInit -> DkvService.loadAnyActiveConfigForScheduler, vormals loadConfig() ohne Mandant) bleibt bewusst UNGEBUNDEN, als benannte Altlast aus 07-04 (260909-mir, Befund D). Zwei Zustaende, beide gehoeren genannt: HEUTE bereits falsch -- findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die uebrigen NIE (isActive-Pruefung kann den Planer sogar ganz leer laufen lassen, wenn ausgerechnet die gezogene Zeile inaktiv ist, obwohl ein zweiter Mandant aktiv waere). NACH DEM SCHARFSCHALTEN (#18) verstummt sie zusaetzlich -- dieselbe Abfrage liefert dann null, der Planer protokolliert 'no active config found' und richtet fuer JEDEN Mandanten nichts ein, ohne Alarm. Drei erwogene Formen prufen: (a) an einen konkret aufgeloesten Mandanten binden -- nicht moeglich, onModuleInit() hat beim Boot strukturell keinen Mandantenkontext. (b) Umbau auf einmal-abfragen-viele-bedienen -- abgelehnt, das ist die in 07-04 zurueckgestellte Mehrmandanten-Planung (neue Auftragsverwaltung je Mandant statt des heutigen setInterval() mit GENAU EINEM Auftrag) und damit eine Funktionsaenderung, kein Bindungsumbau. (c) Als benannte Altlast weiterfuehren, mit Markierung -- GEWAEHLT, Praezedenzfall LdapConfigService.getAllActiveConfigs() (260909-ipc, Befund B). Die Unsymmetrie zu diesem Praezedenzfall: getAllActiveConfigs ist HEUTE korrekt und verstummt erst spaeter: der DKV-Planer ist HEUTE bereits falsch UND verstummt zusaetzlich spaeter. Markierung dreifach: eigene benannte Methode loadAnyActiveConfigForScheduler() mit Kopfkommentar (dkv.service.ts), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Abschnitt (d4) in docs/mandantentrennung-etappe2-fehlerrichtung.md. Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (rls-preflight.mjs), NICHT in diesen Durchlauf. | open | | 2026-09-09T14:41:07.256Z | |
|
||||
| 21 | 2 | deviation | apps/api/src/dkv/dkv-scheduler.service.ts | | DKV-Planer-Startpfad (DkvSchedulerService.onModuleInit -> DkvService.loadAnyActiveConfigForScheduler, vormals loadConfig() ohne Mandant) bleibt bewusst UNGEBUNDEN, als benannte Altlast aus 07-04 (260909-mir, Befund D). Zwei Zustaende, beide gehoeren genannt: HEUTE bereits falsch -- findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die uebrigen NIE (isActive-Pruefung kann den Planer sogar ganz leer laufen lassen, wenn ausgerechnet die gezogene Zeile inaktiv ist, obwohl ein zweiter Mandant aktiv waere). NACH DEM SCHARFSCHALTEN (#18) verstummt sie zusaetzlich -- dieselbe Abfrage liefert dann null, der Planer protokolliert 'no active config found' und richtet fuer JEDEN Mandanten nichts ein, ohne Alarm. Drei erwogene Formen prufen: (a) an einen konkret aufgeloesten Mandanten binden -- nicht moeglich, onModuleInit() hat beim Boot strukturell keinen Mandantenkontext. (b) Umbau auf einmal-abfragen-viele-bedienen -- abgelehnt, das ist die in 07-04 zurueckgestellte Mehrmandanten-Planung (neue Auftragsverwaltung je Mandant statt des heutigen setInterval() mit GENAU EINEM Auftrag) und damit eine Funktionsaenderung, kein Bindungsumbau. (c) Als benannte Altlast weiterfuehren, mit Markierung -- GEWAEHLT, Praezedenzfall LdapConfigService.getAllActiveConfigs() (260909-ipc, Befund B). Die Unsymmetrie zu diesem Praezedenzfall: getAllActiveConfigs ist HEUTE korrekt und verstummt erst spaeter: der DKV-Planer ist HEUTE bereits falsch UND verstummt zusaetzlich spaeter. Markierung dreifach: eigene benannte Methode loadAnyActiveConfigForScheduler() mit Kopfkommentar (dkv.service.ts), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Abschnitt (d4) in docs/mandantentrennung-etappe2-fehlerrichtung.md. Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (rls-preflight.mjs), NICHT in diesen Durchlauf. | fixed | | 2026-09-09T14:41:07.256Z | 2026-09-14T09:51:23.849Z |
|
||||
| 22 | quick-260910-das | deviation | apps/api/src/user/user.service.ts | | Plattformweite Eindeutigkeit von username/email (kein tenantId-Anteil im Unique-Index): die gemessene Kette unsichtbare Zeile -> falsches 'frei' -> harter Eindeutigkeitsfehler (SQLSTATE 23505) ist in dieser Etappe im Anwendungscode entschaerft (Konfliktmeldung bei create/update, Startsperre in admin-seed.service.ts abgefangen), nicht an der Ursache geloest. Die ehrliche Reparatur waere eine Schemaaenderung (Eindeutigkeit mit Mandantendimension) und ist als Produktentscheidung fuer Etappe 3 vorgemerkt. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich user' (u1/u4). | open | | 2026-09-10T08:17:20.009Z | |
|
||||
| 23 | quick-260910-exd | deviation | apps/api/src/module-registry/module-access.service.ts | | Es gibt heute KEIN Signal, das 'wirklich keine Freigabe' (USER hat tatsaechlich keinen Grant) von 'die Abfrage hat nichts gefunden' (z. B. eine nach dem Scharfschalten ungebunden gebliebene Abfrage) unterscheidet: dieselbe ForbiddenException-Meldung im Waechter, dieselbe leere Modulliste mit Status 200, kein Protokolleintrag. Nach dem Scharfschalten ist ein zu kleines Ergebnis in diesem Bereich TOTAL und lautlos -- JEDER Benutzer JEDES Mandanten verliert gleichzeitig jedes Modul, waehrend die Aktivierungs- und Freigabetabellen weiterhin Zeilen halten -- und sieht fuer den Betroffenen wie ein absichtlicher Rechteentzug aus, nicht wie ein Fehler (der Betroffene hat eine fertige, falsche Erklaerung zur Hand und meldet deshalb keinen Fehler). Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): aktive Aktivierungszeilen vorhanden, aber die Aufloesung liefert fuer einen bekannten Administrator eine leere Menge. Eine Laufzeitwarnung an den betroffenen Stellen wurde erwogen und begruendet verworfen (Dauerlaerm auf einer frischen Installation, dieselbe Begruendung wie bei getAllActiveConfigs im Bereich ldap und den fuenf Stellen im Bereich tenders). In module.guard.spec.ts als Testfall festgenagelt, damit die Aufzeichnung rot wird, sobald jemand ein unterscheidendes Signal einbaut. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich module-registry' (m3). | open | | 2026-09-10T09:28:45.310Z | |
|
||||
| 24 | quick-260910-jab | deviation | apps/api/src/tenders/tender-rss-feed.service.ts | | Was das Schliessen von WINDOWS #19 NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle (TenderRssFeedSource, tenantId NULL) weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil jede Schreibregel (Einfuegen/Aendern/Entfernen) ausdruecklich einen Mandanten verlangt (tenant_insert_policy/tenant_update_policy/tenant_delete_policy, 20260910120000_rls_widen_membership_grant_and_platform_read). Betroffen sind zwei Pfade in TenderRssFeedSourceService: createPlatform() (setzt tenantId=NULL, ein gebundenes INSERT liefe in die WITH-CHECK-Klausel und wuerde abgewiesen) und remove() (deckt fuer Administratoren auch das Entfernen einer plattformweiten Zeile ab; ein gebundenes DELETE traefe sie nie). Beide bleiben deshalb bewusst ungebunden — das ist KEINE Folge dieser Reparatur, sondern bestand bereits vor 260910-jab identisch, weil die vom Ledger vorgegebene #19-Semantik Schreibzugriffe ausdruecklich an einen Mandanten bindet. Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): ein Verwaltungsweg fuer plattformweite Zeilen (Anlegen/Entfernen unter der Anwendungsrolle) muss gebaut werden, BEVOR die Rolle umgeschaltet wird — sonst kann kein Administrator nach dem Scharfschalten mehr eine plattformweite Quelle pflegen. Eigener Eintrag, damit dieser Rest nicht mit #19 verschwindet. | open | | 2026-09-10T12:35:40.000Z | |
|
||||
@@ -44,13 +44,14 @@ last_updated: 2026-09-14T08:38:12.619Z
|
||||
| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | fixed | | 2026-09-11T09:08:00.435Z | 2026-09-11T14:48:15.447Z |
|
||||
| 28 | quick-260911-fh9 | deviation | apps/web/src/components/layout/header.tsx | | Bereich auth: getMe liefert nach dem Scharfschalten null, der Controller antwortet 200 mit leerem Rumpf, fetchCurrentUser (auth-actions.ts) macht daraus null, header.tsx und account-settings-form.tsx tun bei null nichts — die Portalhuelle rendert ohne angemeldeten Benutzer; changePassword liest sich als networkError (nicht als falsches Kennwort); adminResetPassword als 'User not found' ohne UI-Aufrufer. 'nicht angemeldet' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert — an dieselbe Bedingung gebunden wie WINDOWS #18; Familie #23/#25/#26; Etappe-4-Vorabpruefung: bekannten Benutzer ueber die Wartungsrolle lesen und den gebundenen findUnique unter seinem Claim-Mandanten daneben halten. Das Frontend wird von 260911-fh9 NICHT geaendert. | open | | 2026-09-11T10:00:29.558Z | |
|
||||
| 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | fixed | | 2026-09-11T10:00:38.418Z | 2026-09-14T08:37:53.307Z |
|
||||
| 30 | quick-260911-gwh | deviation | apps/api/src/mail/mail.module.ts | | Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a). | open | | 2026-09-11T11:57:36.656Z | |
|
||||
| 30 | quick-260911-gwh | deviation | apps/api/src/mail/mail.module.ts | | Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a). | fixed | | 2026-09-11T11:57:36.656Z | 2026-09-14T09:51:24.073Z |
|
||||
| 31 | quick-260911-gwh | deviation | apps/web/src/components/dashboard/widgets/favorites-widget.tsx | | Bereich favorites: ein nach dem Scharfschalten (#18) zu klein gebliebenes Leseergebnis auf list() sieht aus wie 'Noch keine Favoriten.' (favorites-widget.tsx Zeile um 212, de.json favorites.empty) -- fetchFavorites (favorites-api.ts) reicht die leere Liste durch. 'Nie einen gespeichert' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert []. Etappe-4-Vorabpruefung (f4)(d): fuer einen bekannten Nutzer/Widget die Favoritenzahl ueber die Wartungsrolle und ueber den gebundenen findMany daneben halten. Familie #23/#25/#26/#28. Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich favorites' (f3)/(f4). | open | | 2026-09-11T11:57:50.276Z | |
|
||||
| 32 | quick-260911-gwh | deviation | apps/web/src/components/settings/smtp-settings-form.tsx | | Bereich settings: getSmtpConfig liefert nach dem Scharfschalten (#18) null, der Controller antwortet 200 mit leerem Rumpf, fetchSmtp (settings-api.ts) laeuft mit res.json() auf den leeren Rumpf und wirft, smtp-settings-form.tsx verschluckt das in .catch(() => {}) -- leeres Formular 'nicht eingerichtet', waehrend die Zugangsdaten physisch da sind. Ein erneutes Speichern unter der ungebundenen Form scheitert am Eindeutigkeitsindex SmtpConfig_tenantId_key (PrismaClientUnknownRequestError, gemessen in Aufgabe 1 Pruefung 8) -- nach diesem Lauf ist saveSmtpConfig gebunden und trifft die eigene Zeile, dieser Rest bestand nur unter der ungebundenen Form vor dieser Aenderung. Dieselbe 200-leerer-Rumpf-Kette wie #28. Etappe-4-Vorabpruefung (s4)(e). Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s3)/(s4). | open | | 2026-09-11T11:57:50.484Z | |
|
||||
| 33 | quick-260911-mkj | unmet-truth | apps/api/src/tenders/tenders.seed.ts | | Modellaufrufe auf Empfaengern, die weder this.prisma noch eine const X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: tenders/tenders.seed.ts (Funktionsparameter prisma: PrismaService, tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) und tenders/backfill-tender-source.ts (eigenstaendiges Skript mit new PrismaClient(), tender.findMany/update, durch RELATION_SPEC_EXCEPTIONS laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER <Kennung>.<Modell>.<Operation>(-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut. | open | | 2026-09-11T14:48:09.723Z | |
|
||||
| 34 | quick-260911-nke | deviation | apps/api/src/prisma/prisma-tenant.extension.ts | | Etappe 3b: ein Nutzer-CRUD-Aufrufer, der den Benutzer an forTenant() vergisst, sieht den ganzen Mandanten (IS-NULL-Form) — gleicher Stand wie vor 20260911120000, keine Verschlechterung, aber kein Netz. Die Bestandsaufnahme unterscheidet nur mandanten-gebunden/ungebunden, nicht benutzer-gebunden; ein Waechter, der jede Methode mit userId-Parameter auf das dritte Argument prueft, ist NICHT gebaut. Bis dahin sind die dreistelligen Spec-Zusicherungen je Dienst das einzige Netz. Vor dem Scharfschalten (Etappe 4, rls-preflight.mjs) zu entscheiden: Waechter bauen oder Rest benennen. | open | | 2026-09-11T15:46:08.295Z | |
|
||||
| 35 | quick-260914-ebg | deviation | biome.json | | Biome ist im Bestand nicht lauffaehig: biome.json traegt den in Biome 2.5.0 unbekannten Schluessel organizeImports (gehoert unter assist), Biome bricht bei jedem Aufruf mit Konfigurationsfehler ab; zusaetzlich fehlt javascript.parser.unsafeParameterDecoratorsEnabled, ohne den jeder NestJS-Parameter-Dekorator ein Parse-Fehler ist (17 allein in user.controller.ts). Der CI-Schritt Lint ruft pnpm lint = turbo lint, keine App hat ein lint-Skript - der Schritt ist ein Leerlauf, der gruen meldet. CLAUDE.md und docs/anleitung-entwicklung.md beschreiben Biome als aktives Werkzeug. Gemessen 260914-ebg; das dortige Gate lief mit einer Ersatzkonfiguration im Scratchpad, relativ zur Baseline (0 Fehler, Warnungen je Datei 22/25/20, alle noExplicitAny-Familie; biome format ebenfalls unsauber, Anfuehrungszeichen-Stil). Zu entscheiden: biome.json reparieren (organizeImports nach assist, Parser-Schalter, quoteStyle single) und ein lint-Skript je App anlegen, dann die Warnungen in einem eigenen Durchlauf abbauen oder als Regelabschaltung begruenden. | open | | 2026-09-14T08:38:04.079Z | |
|
||||
| 36 | quick-260914-ebg | deviation | apps/web/src/app/(portal)/admin/users/page.tsx | | handleSubmit und handleDelete pruefen nur res.ok ohne else-Zweig und fangen mit leerem catch - ein 403 der API fuehrt zu keiner sichtbaren Reaktion (Formular bleibt offen, Loeschdialog bleibt stehen, keine Meldung). Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung); seit 260914-ebg (WINDOWS #29) ist der Fall fuer einen ADMIN im Alltag erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Familie der still verschluckten Antworten (#28, #32). Frontend von 260914-ebg NICHT geaendert (ausserhalb der Erlaubnisliste). Zu schliessen: Fehlermeldung aus dem Antwortrumpf anzeigen und die Aktionsknoepfe fuer SUPER_ADMIN-Zeilen einem ADMIN gar nicht erst anbieten. | open | | 2026-09-14T08:38:12.619Z | |
|
||||
| 37 | quick-260914-eym | deviation | apps/api/src/dkv/dkv.service.ts | | Der Single-Flight-Riegel processing in DkvService.processInbox ist EIN prozessweites Boolean, nicht je Mandant. Seit 260914-eym laeuft je aktivem Mandanten ein eigener Cron-Auftrag (dkv-inbox-poll:<tenantId>); ueberschneiden sich zwei Ticks verschiedener Mandanten, bricht der zweite still ab (Warnzeile 'already processing') und der Mandant wartet bis zum naechsten Intervall - kein Datenverlust, Verzoegerung; mit EINEM Mandanten unveraendert. Der Tick blieb in 3c laut Auftrag unangetastet (T-EYM-09, accept mit Aufzeichnung). Zu schliessen: Riegel je Mandant (Set<tenantId>) mit Test 'zwei Mandanten gleichzeitig, beide werden bedient'. | open | | 2026-09-14T09:51:24.295Z | |
|
||||
|
||||
````json
|
||||
[
|
||||
@@ -301,10 +302,10 @@ last_updated: 2026-09-14T08:38:12.619Z
|
||||
"file": "apps/api/src/dkv/dkv-scheduler.service.ts",
|
||||
"line": null,
|
||||
"description": "DKV-Planer-Startpfad (DkvSchedulerService.onModuleInit -> DkvService.loadAnyActiveConfigForScheduler, vormals loadConfig() ohne Mandant) bleibt bewusst UNGEBUNDEN, als benannte Altlast aus 07-04 (260909-mir, Befund D). Zwei Zustaende, beide gehoeren genannt: HEUTE bereits falsch -- findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die uebrigen NIE (isActive-Pruefung kann den Planer sogar ganz leer laufen lassen, wenn ausgerechnet die gezogene Zeile inaktiv ist, obwohl ein zweiter Mandant aktiv waere). NACH DEM SCHARFSCHALTEN (#18) verstummt sie zusaetzlich -- dieselbe Abfrage liefert dann null, der Planer protokolliert 'no active config found' und richtet fuer JEDEN Mandanten nichts ein, ohne Alarm. Drei erwogene Formen prufen: (a) an einen konkret aufgeloesten Mandanten binden -- nicht moeglich, onModuleInit() hat beim Boot strukturell keinen Mandantenkontext. (b) Umbau auf einmal-abfragen-viele-bedienen -- abgelehnt, das ist die in 07-04 zurueckgestellte Mehrmandanten-Planung (neue Auftragsverwaltung je Mandant statt des heutigen setInterval() mit GENAU EINEM Auftrag) und damit eine Funktionsaenderung, kein Bindungsumbau. (c) Als benannte Altlast weiterfuehren, mit Markierung -- GEWAEHLT, Praezedenzfall LdapConfigService.getAllActiveConfigs() (260909-ipc, Befund B). Die Unsymmetrie zu diesem Praezedenzfall: getAllActiveConfigs ist HEUTE korrekt und verstummt erst spaeter: der DKV-Planer ist HEUTE bereits falsch UND verstummt zusaetzlich spaeter. Markierung dreifach: eigene benannte Methode loadAnyActiveConfigForScheduler() mit Kopfkommentar (dkv.service.ts), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Abschnitt (d4) in docs/mandantentrennung-etappe2-fehlerrichtung.md. Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (rls-preflight.mjs), NICHT in diesen Durchlauf.",
|
||||
"status": "open",
|
||||
"status": "fixed",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-09T14:41:07.256Z",
|
||||
"resolved_at": null
|
||||
"resolved_at": "2026-09-14T09:51:23.849Z"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
@@ -409,10 +410,10 @@ last_updated: 2026-09-14T08:38:12.619Z
|
||||
"file": "apps/api/src/mail/mail.module.ts",
|
||||
"line": null,
|
||||
"description": "Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a).",
|
||||
"status": "open",
|
||||
"status": "fixed",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-11T11:57:36.656Z",
|
||||
"resolved_at": null
|
||||
"resolved_at": "2026-09-14T09:51:24.073Z"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
@@ -487,6 +488,19 @@ last_updated: 2026-09-14T08:38:12.619Z
|
||||
"recorded_at": "2026-09-14T08:38:12.619Z",
|
||||
"resolved_at": null,
|
||||
"milestone": "v1.2"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"kind": "deviation",
|
||||
"phase": "quick-260914-eym",
|
||||
"file": "apps/api/src/dkv/dkv.service.ts",
|
||||
"line": null,
|
||||
"description": "Der Single-Flight-Riegel processing in DkvService.processInbox ist EIN prozessweites Boolean, nicht je Mandant. Seit 260914-eym laeuft je aktivem Mandanten ein eigener Cron-Auftrag (dkv-inbox-poll:<tenantId>); ueberschneiden sich zwei Ticks verschiedener Mandanten, bricht der zweite still ab (Warnzeile 'already processing') und der Mandant wartet bis zum naechsten Intervall - kein Datenverlust, Verzoegerung; mit EINEM Mandanten unveraendert. Der Tick blieb in 3c laut Auftrag unangetastet (T-EYM-09, accept mit Aufzeichnung). Zu schliessen: Riegel je Mandant (Set<tenantId>) mit Test 'zwei Mandanten gleichzeitig, beide werden bedient'.",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-14T09:51:24.295Z",
|
||||
"resolved_at": null,
|
||||
"milestone": "v1.2"
|
||||
}
|
||||
]
|
||||
````
|
||||
|
||||
@@ -85,6 +85,25 @@ noetig sind:
|
||||
ein Aufruf OHNE gesetzten Benutzer (Admin, Hintergrunddienst) sieht
|
||||
weiterhin den ganzen Mandanten, das macht die Aenderung fuer heutige
|
||||
Aufrufer wirkungslos.
|
||||
- **Eine dritte Sitzungsvariable fuer den Systemkontext.** Migration
|
||||
`20260914120000_rls_system_context_read` (Etappe 3c, 260914-eym) bringt
|
||||
`app.system_context` und die Funktion `is_system_context()` —
|
||||
`COALESCE(current_setting('app.system_context', true) = 'true', false)`,
|
||||
damit die Regel ohne gesetzte Variable FALSE sieht, nicht NULL — sowie je
|
||||
eine zusaetzliche PERMISSIVE Regel `system_read_policy ... FOR SELECT
|
||||
USING (is_system_context())` auf genau den fuenf Tabellen, die die
|
||||
Hintergrunddienste ueber alle Mandanten LESEN (DkvModuleConfig, LdapConfig,
|
||||
LdapFieldMapping, TenderMatch, TenderSavedSearch). Permissive Regeln werden
|
||||
ODER-verknuepft: fuer SELECT gilt (Mandantenregel ODER Systemregel), fuer
|
||||
INSERT/UPDATE/DELETE weiter NUR die Mandantenregel — unter Systemkontext
|
||||
ist `current_tenant_id()` der Leerstring, jedes Schreiben faellt durch
|
||||
(gemessen: 42501 / count 0 / P2025). Der Helfer `forSystem()` setzt
|
||||
`app.system_context = 'true'` und die beiden anderen Variablen
|
||||
AUSDRUECKLICH leer; `forTenant()` und `withTenantTransaction()` setzen
|
||||
umgekehrt `app.system_context = ''` — kein Kontext erbt vom anderen
|
||||
(`local=true` als erstes Netz, der Reset als zweites, beides im Werkzeug
|
||||
gemessen und durch Rueckbau belegt). Kein `GRANT EXECUTE` noetig, wie bei
|
||||
den beiden anderen Funktionen.
|
||||
|
||||
## 3. Der Sperrgrund — warum die Umstellung noch nicht erfolgt ist
|
||||
|
||||
@@ -135,6 +154,21 @@ werden darf. Das ist **eigene Arbeit und nicht Teil dieser Aenderung**
|
||||
Abschnitt 5 belegt ausschliesslich, dass die Datenbankseite stimmt — er sagt
|
||||
nichts ueber diese Zugriffe aus.
|
||||
|
||||
**Nachtrag (260914-eym, Etappe 3c):** der Systemkontext ist gebaut — siehe
|
||||
den Punkt "Eine dritte Sitzungsvariable" in Abschnitt 2 und
|
||||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
|
||||
"## Systemkontext (Etappe 3c, 260914-eym)". Vier Hintergrunddienst-Dateien
|
||||
lesen ueber `forSystem()`, der Mail-Startpfad ist entfernt, die Erstanlage
|
||||
des Administrators liest nur `Tenant` (keine Regel). Die Vorher-Pruefung
|
||||
`ohne-kontext-leer` in `rls-preflight.mjs` (Abschnitt 5) bleibt GUELTIG und
|
||||
wird durch die neue Regel NICHT gelockert: ohne gesetzte Variable ist
|
||||
`is_system_context()` false — Werkzeugbeleg
|
||||
`is-system-context-ungesetzt-false` (`rls-scratch-check.mjs`, Rohwert
|
||||
`null` -> `false`). Etappe 4 ergaenzt die Vorher-Pruefung um
|
||||
`mit-systemkontext-sichtbar` (mit `app.system_context = 'true'` sind die
|
||||
fuenf Tabellen lesbar); `rls-preflight.mjs` ist in 3c bewusst nicht
|
||||
angefasst.
|
||||
|
||||
**Zusaetzlicher Sperrgrund, ebenfalls am 2026-09-09 gemessen (WINDOWS #20):**
|
||||
`forTenant()` selbst war bis Aufgabe 1 dieser Etappe defekt — `set_config()`
|
||||
lief auf einer anderen Datenbankverbindung als die eigentliche Abfrage, sodass
|
||||
|
||||
@@ -985,6 +985,13 @@ abgeschrieben.
|
||||
entscheidet sie nicht — er bindet dienst-intern, wie `ldap`, `groups` und
|
||||
`tenders` es vormachen.
|
||||
|
||||
**Nachtrag (260914-eym):** WINDOWS #21 ist GESCHLOSSEN — `loadActiveConfigsForScheduler()`
|
||||
liest über `forSystem()` (Systemleseregel auf DkvModuleConfig) ALLE aktiven
|
||||
Konfigurationen, der Planer registriert je Mandant einen eigenen Auftrag
|
||||
`dkv-inbox-poll:<tenantId>`; die Erwähnung des einen Auftragsnamens
|
||||
`dkv-inbox-poll` oben bleibt als historischer Stand stehen. Siehe
|
||||
`## Systemkontext (Etappe 3c, 260914-eym)`.
|
||||
|
||||
### (d5) Was dieser Durchlauf bewusst nicht anfasst
|
||||
|
||||
- **Die beiden mehrschrittigen Stellen bleiben unatomar (Befund C, TEIL
|
||||
@@ -3096,6 +3103,15 @@ unverändert und steht nicht in der Erlaubnisliste.
|
||||
`SmtpConfig`-Zeile über die Wartungsrolle lesen und den gebundenen
|
||||
`findUnique` daneben halten — dieselbe Form wie bei `dashboard`/`calendar`.
|
||||
|
||||
**Nachtrag (260914-eym):** WINDOWS #30 ist GESCHLOSSEN — nicht durch einen
|
||||
Systemkontext, sondern durch ENTFERNEN des Startpfads: die Mailer-Fabrik in
|
||||
`mail.module.ts` und die Startpfad-Methode in `settings.service.ts` sind
|
||||
gelöscht, `MailService` baut je Versand einen Transport aus
|
||||
`getDecryptedSmtpConfig(tenantId)` des Empfänger-Mandanten
|
||||
(`requestPasswordReset` reicht `user.tenantId` durch). Deshalb trägt
|
||||
SmtpConfig keine `system_read_policy`. Siehe
|
||||
`## Systemkontext (Etappe 3c, 260914-eym)`.
|
||||
|
||||
### (s5) Was dieser Durchlauf bewusst nicht anfasst
|
||||
|
||||
- `settings.controller.ts` — nur gelesen (siehe (s4)(d)).
|
||||
@@ -3214,6 +3230,11 @@ Forbidden zu NotFound.
|
||||
nichts, `tenderrssfeed-gemeinsame-zeile-ohne-benutzer-weiterhin-entfernbar`
|
||||
bestätigt das lediglich erneut.
|
||||
|
||||
**Nachtrag (260914-eym):** der erste Punkt ist eingelöst — der Systemkontext
|
||||
für Hintergrunddienste ist gebaut (`## Systemkontext (Etappe 3c, 260914-eym)`),
|
||||
`tender-digest.scheduler.ts` liest seine Kandidaten über `forSystem()` und
|
||||
bleibt in der Schleife gebunden.
|
||||
|
||||
### (b5) Was dieser Durchlauf bewusst nicht anfasst
|
||||
|
||||
- Die vier Tabellen mit `userId`-Spalte, die KEINE persönlichen Daten tragen
|
||||
@@ -3226,6 +3247,221 @@ Forbidden zu NotFound.
|
||||
- Der Schalter (`DATABASE_URL` → Rolle `tessera`, BYPASSRLS) — bleibt AUS.
|
||||
- Compose-/Umgebungsdateien — unangetastet.
|
||||
|
||||
## Systemkontext (Etappe 3c, 260914-eym)
|
||||
|
||||
Helfer-, Datenbank- und Dienstumbau: Migration `20260914120000_rls_system_context_read`
|
||||
bringt die Funktion `is_system_context()` und je betroffener Tabelle eine
|
||||
zusätzliche, NUR lesende Regel `system_read_policy … FOR SELECT` auf
|
||||
DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch und
|
||||
TenderSavedSearch. `forSystem(prisma)` ist der Schwesterhelfer von
|
||||
`forTenant()` (gleiche Array-Form-Bauart, setzt `app.system_context = 'true'`
|
||||
und die beiden anderen Sitzungsvariablen ausdrücklich leer; `forTenant()`
|
||||
und `withTenantTransaction()` setzen umgekehrt `app.system_context = ''`).
|
||||
Vier Dateien rufen ihn an fünf Stellen (Erlaubnisliste
|
||||
`FORSYSTEM_ALLOWED_CALL_SITES` im Detektor, exakte Zahl je Datei): der
|
||||
DKV-Planer-Startpfad (jetzt ein Cron-Auftrag je aktivem Mandanten,
|
||||
WINDOWS #21), beide Leser in `ldap-config.service.ts`, die Kandidatenabfrage
|
||||
des Digest, die Profilabfrage des Abgleichs. Der Mail-Startpfad ist nicht
|
||||
umgestellt, sondern ENTFERNT (Transport je Versand nach Mandant des
|
||||
Empfängers, WINDOWS #30); `admin-seed.service.ts` liest außerhalb seiner
|
||||
Schleife nur `Tenant` (keine Regel) und ist unverändert. Der Schalter bleibt
|
||||
AUS — nichts hiervon wirkt, bis Etappe 4 scharfschaltet.
|
||||
|
||||
### (y1) Die Messung
|
||||
|
||||
Wörtliche Werkzeugausgabe der vier Funktionsfälle (`rls-scratch-check.mjs`,
|
||||
Wegwerf-Rolle ohne BYPASSRLS, Funktion aus der Migration geschnitten):
|
||||
|
||||
```
|
||||
is-system-context-ungesetzt-false: bestanden — ohne gesetzte Variable: is_system_context() = false (Rohwert null) — die Vorher-Pruefung ohne-kontext-leer in rls-preflight.mjs bleibt gueltig
|
||||
is-system-context-leer-false: bestanden — nach set_config('app.system_context', '', true): false
|
||||
is-system-context-true-true: bestanden — nach set_config('app.system_context', 'true', true): true
|
||||
is-system-context-fremdwert-false: bestanden — nach set_config('app.system_context', 'yes', true): false
|
||||
```
|
||||
|
||||
Je Tabelle die drei Kern-Kennungen (zu wenig / zu viel / Erben) über den
|
||||
GENERIERTEN Client, Wegwerf-Tabellen mit allen skalaren Spalten, Regeln
|
||||
wortgleich aus ihren Migrationen geschnitten:
|
||||
|
||||
```
|
||||
dkvmoduleconfig-systemkontext-sieht-beide-mandanten: bestanden — system.dkvModuleConfig.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
|
||||
dkvmoduleconfig-systemkontext-insert-abgewiesen-42501: bestanden — system.dkvModuleConfig.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(Post […]
|
||||
dkvmoduleconfig-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).dkvModuleConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
|
||||
ldapconfig-systemkontext-sieht-beide-mandanten: bestanden — system.ldapConfig.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
|
||||
ldapconfig-systemkontext-insert-abgewiesen-42501: bestanden — system.ldapConfig.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(PostgresError […]
|
||||
ldapconfig-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).ldapConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
|
||||
ldapfieldmapping-systemkontext-sieht-beide-mandanten: bestanden — system.ldapFieldMapping.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
|
||||
ldapfieldmapping-systemkontext-insert-abgewiesen-42501: bestanden — system.ldapFieldMapping.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(Po […]
|
||||
ldapfieldmapping-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).ldapFieldMapping.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
|
||||
tendermatch-systemkontext-sieht-beide-mandanten: bestanden — system.tenderMatch.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
|
||||
tendermatch-systemkontext-insert-abgewiesen-42501: bestanden — system.tenderMatch.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(PostgresErro […]
|
||||
tendermatch-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).tenderMatch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
|
||||
tendersavedsearch-systemkontext-sieht-beide-mandanten: bestanden — system.tenderSavedSearch.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
|
||||
tendersavedsearch-systemkontext-insert-abgewiesen-42501: bestanden — system.tenderSavedSearch.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError( […]
|
||||
tendersavedsearch-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).tenderSavedSearch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
|
||||
```
|
||||
|
||||
Die #27-Form unter Systemkontext (der Pfad von `getAllActiveConfigs()`):
|
||||
|
||||
```
|
||||
ldapconfig-systemkontext-include-fieldmappings-beider-mandanten: bestanden — system.ldapConfig.findMany({ where: { isActive: true }, include: { fieldMappings: true } }) liefert 2 Zeile(n): ["TENANT-A:1","TENANT-B:1"] (Mandant:Anzahl Zuordnungen)
|
||||
```
|
||||
|
||||
Schlusszeile: `Alle 253 Pruefungen bestanden.` (Baseline vor diesem Lauf: 203; nach Aufgabe 1: 216).
|
||||
|
||||
`pg_policies` der LEBENDEN Datenbank nach `prisma migrate deploy` (36
|
||||
Migrationen, `pg_proc` kennt `is_system_context`, 34 Regeln gesamt, alle
|
||||
PERMISSIVE; Form Tabelle#Regelname#Befehl#USING#WITH CHECK, beide Regeln je
|
||||
Tabelle):
|
||||
|
||||
```
|
||||
DkvModuleConfig#system_read_policy#SELECT#is_system_context()#
|
||||
DkvModuleConfig#tenant_isolation_policy#ALL#("tenantId" = current_tenant_id())#
|
||||
LdapConfig#system_read_policy#SELECT#is_system_context()#
|
||||
LdapConfig#tenant_isolation_policy#ALL#("tenantId" = current_tenant_id())#
|
||||
LdapFieldMapping#system_read_policy#SELECT#is_system_context()#
|
||||
LdapFieldMapping#tenant_isolation_policy#ALL#("ldapConfigId" IN ( SELECT "LdapConfig".id FROM "LdapConfig" WHERE ("LdapConfig"."tenantId" = current_tenant_id())))#
|
||||
TenderMatch#system_read_policy#SELECT#is_system_context()#
|
||||
TenderMatch#tenant_isolation_policy#ALL#("tenantId" = current_tenant_id())#
|
||||
TenderSavedSearch#system_read_policy#SELECT#is_system_context()#
|
||||
TenderSavedSearch#tenant_isolation_policy#ALL#(("tenantId" = current_tenant_id()) AND ((current_user_id() IS NULL) OR ("userId" = current_user_id())))#
|
||||
```
|
||||
|
||||
Endstand nach Aufgabe 2: Tests 1054 bestanden / 64 Dateien (Baseline
|
||||
1028 / 62), Typprüfung sauber, Werkzeug 253.
|
||||
|
||||
### (y2) Signaltabelle — beide Fehlerrichtungen je Regel
|
||||
|
||||
| Fehlerrichtung | Erwartung | Gemessen | Befund |
|
||||
|---|---|---|---|
|
||||
| Zu streng: der Systemkontext sähe nichts (ein Systemleser OHNE Regel liefert nach dem Scharfschalten 0 Zeilen und schweigt — T-EYM-04) | Systemkontext liefert beide Mandanten | `<tabelle>-ungebunden-null-zeilen` UND `<tabelle>-systemkontext-sieht-beide-mandanten` als Paar (fünf Tabellen), dazu `ldapconfig-systemkontext-include-fieldmappings-beider-mandanten` | NICHT der Fall — jede der fünf Tabellen ist geöffnet; Rückbau (b) unten zeigt, dass das Werkzeug der Datei folgt |
|
||||
| Zu locker: der Systemkontext könnte schreiben (T-EYM-02) | INSERT/UPDATE/DELETE scheitern an der Mandantenregel | `<tabelle>-systemkontext-insert-abgewiesen-42501`, `…-updatemany-count-0`, `…-deletemany-count-0` (fünf Tabellen) | NICHT der Fall — die Regel ist `FOR SELECT`; Rückbau (a): `FOR SELECT` entfernt → der Insert GELINGT, `cmd` wird `ALL` |
|
||||
| Zu locker: ein Anfrageweg ruft `forSystem` (T-EYM-01) | Spec rot | `FORSYSTEM_ALLOWED_CALL_SITES` mit exakter Zahl je Datei; Rückbau (d): Zahl 0 → zwei Zusicherungen rot, Fremddatei → drei rot | Wachhund greift; keine Ausnahmeliste für die Zuweisungsform |
|
||||
| Erben: eine Verbindung trägt `app.system_context` in eine spätere `forTenant`-Abfrage (T-EYM-03) | `forTenant(A)` nach `forSystem` sieht nur A; `is_system_context()` unter `forTenant` ist false | `<tabelle>-fortenant-a-nach-systemkontext-nur-a`, `<tabelle>-is-system-context-unter-fortenant-false` (fünf Tabellen) | NICHT der Fall — `local=true` (erstes Netz) UND ausdrücklicher Reset (zweites Netz); Rückbau (c) unten belegt, dass der Reset allein trägt |
|
||||
|
||||
**Rückbau (a)** — in der Migrationsdatei bei `"TenderMatch"` das `FOR SELECT`
|
||||
entfernt (Regel wird `ALL`), Werkzeug:
|
||||
|
||||
```
|
||||
tendermatch-systemkontext-insert-abgewiesen-42501: FEHLGESCHLAGEN — system.tenderMatch.create({"id":"tm-system-schreibversuch","tenderId":"tender-2","savedSearchId":"ss-a","userId":"user-a","tenantId":"TENANT-A"}) ist NICHT fehlgeschlagen — angelegt: "tm-system-schreibversuch"
|
||||
tendermatch-systemkontext-updatemany-count-0: FEHLGESCHLAGEN — system.tenderMatch.updateMany({ where: {}, data: {"notifiedChannel":"SYSTEM-SCHREIBVERSUCH"} }) liefert count=3
|
||||
tendermatch-systemkontext-deletemany-count-0: FEHLGESCHLAGEN — system.tenderMatch.deleteMany({}) liefert count=3; Zeilen danach (Wartungsrolle): 0
|
||||
tendermatch-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).tenderMatch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 0 Zeile(n) aus []
|
||||
tendermatch-pg-policies-genau-eine-system-read-policy-select: FEHLGESCHLAGEN — pg_policies fuer "TenderMatch" (system_read_policy): [{"policyname":"system_read_policy","cmd":"ALL","permissive":"PERMISSIVE","qual":"is_system_context()"}]
|
||||
5 von 253 Pruefungen fehlgeschlagen.
|
||||
```
|
||||
|
||||
**Rückbau (b)** — die `system_read_policy` für `"TenderSavedSearch"` aus der
|
||||
Migrationsdatei entfernt. Die innere Routine bricht für diese Tabelle mit
|
||||
einer eigenen roten Kennung ab (Muster `runSingleRulePersonalTableCheck`:
|
||||
nicht raten, wenn die Regel fehlt) — deshalb 245 statt 253 Prüfungen, nicht
|
||||
die im Plan erwarteten zwei roten Kennungen `…-sieht-beide-mandanten`/`…-pg-policies-…`;
|
||||
die lebende Datenbank blieb währenddessen bei 34 Regeln und einer
|
||||
`system_read_policy` auf TenderSavedSearch (per `pg_policies` gelesen):
|
||||
|
||||
```
|
||||
tendersavedsearch-system-read-policy-aus-migration-gefunden: FEHLGESCHLAGEN — CREATE POLICY system_read_policy ON "TenderSavedSearch" nicht in der Systemkontext-Migration (20260914120000) gefunden
|
||||
1 von 245 Pruefungen fehlgeschlagen.
|
||||
```
|
||||
|
||||
**Rückbau (c)** — im Werkzeug `forSystemQuery`/`buildInlineSystemClient` auf
|
||||
`set_config(…, false)` gestellt: `Alle 253 Pruefungen bestanden.` — alle
|
||||
`…-fortenant-a-nach-systemkontext-nur-a` BLEIBEN grün, weil der Reset in
|
||||
`buildInlineExtendedClient` trägt. Zusätzlich den Reset dort entfernt:
|
||||
|
||||
```
|
||||
dkvmoduleconfig-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).dkvModuleConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
|
||||
ldapconfig-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).ldapConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
|
||||
ldapfieldmapping-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).ldapFieldMapping.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
|
||||
tendermatch-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).tenderMatch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
|
||||
tendersavedsearch-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).tenderSavedSearch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
|
||||
5 von 253 Pruefungen fehlgeschlagen.
|
||||
```
|
||||
|
||||
**Rückbau (d)** — Detektor, Zahl für `tender-matching.service.ts` auf 0:
|
||||
|
||||
```
|
||||
AssertionError: apps/api/src/tenders/tender-matching.service.ts: gemessen 1 forSystem(-Aufruf(e), erlaubt sind genau 0: expected [ Array(1) ] to deeply equal []
|
||||
AssertionError: apps/api/src/tenders/tender-matching.service.ts: Erlaubnisliste nennt 0, gemessen 1 — der Eintrag ist ueberholt: expected [ Array(1) ] to deeply equal []
|
||||
```
|
||||
|
||||
Fremddatei `admin-seed.service.ts` vorübergehend mit `forSystem(` versehen:
|
||||
|
||||
```
|
||||
AssertionError: apps/api/src/user/admin-seed.service.ts: 2 forSystem(-Aufruf(e), Datei steht NICHT in FORSYSTEM_ALLOWED_CALL_SITES — ein Anfrageweg darf den Systemkontext nie rufen: expected [ Array(1) ] to deeply equal []
|
||||
AssertionError: apps/api/src/user/admin-seed.service.ts: 1 forSystem(-Aufruf(e) ausserhalb der Zuweisungsform: expected [ Array(1) ] to deeply equal []
|
||||
AssertionError: apps/api/src/user/admin-seed.service.ts: 1 include:/select:/_count:-Angabe(n) ausserhalb eines erkannten Modellaufrufs: expected [ Array(1) ] to deeply equal []
|
||||
```
|
||||
|
||||
Alle Rückbauten zurückgenommen (`git checkout` bzw. Kopie mit gleichem
|
||||
Hash), Werkzeug danach erneut 253, Detektor 30/30.
|
||||
|
||||
### (y3) Welcher Code Leere als Abwesenheit deutet
|
||||
|
||||
Die Frage aus dem Auftrag (`docs/mandantentrennung-etappe3-auftrag.md`, 3c):
|
||||
deutet einer der sechs Pfade eine LEERE Systemkontext-Antwort als "es gibt
|
||||
nichts" und löscht oder deaktiviert daraufhin? Je Pfad mit Datei und Stelle:
|
||||
|
||||
- **dkv-scheduler** (`apps/api/src/dkv/dkv-scheduler.service.ts`,
|
||||
`onModuleInit`): leere Liste → Protokollzeile `no active config found`,
|
||||
`return` — kein Auftrag, nichts gelöscht, nichts deaktiviert.
|
||||
- **ldap-sync.scheduler** (`apps/api/src/ldap/ldap-sync.scheduler.ts`, ruft
|
||||
`getAllActiveConfigs()`): leere Liste → kein Sync-Lauf. Der gefährliche
|
||||
Löschzweig in `apps/api/src/ldap/ldap.service.ts` (Deaktivieren/Entfernen
|
||||
nicht mehr im Verzeichnis gefundener Nutzer) liegt INNERHALB eines je
|
||||
Mandant gebundenen Sync-Laufs (`forTenant(this.prisma, tenantId)`), den
|
||||
eine leere Konfigurationsliste gar nicht erst startet — Leere auf der
|
||||
Systemkontext-Ebene erreicht diesen Zweig strukturell nicht.
|
||||
- **ldap onApplicationBootstrap** (`ldap-config.service.ts`): leere Liste →
|
||||
`legacy.length === 0` → `return` — die Nachverschlüsselung ist Nichtstun.
|
||||
- **tender-digest** (`tender-digest.scheduler.ts`, `runDigest`): leere
|
||||
Kandidatenliste → `if (!candidates.length) return;` — kein Versand,
|
||||
`notifiedAt` bleibt NULL (wiederholbar).
|
||||
- **tender-matching** (`tender-matching.service.ts`, `matchDelta`): leere
|
||||
Profilliste → die Schleife läuft nicht, keine Treffer, keine Sofortmeldung.
|
||||
- **admin-seed** (`apps/api/src/user/admin-seed.service.ts`): leere
|
||||
Mandantenliste → keine Reparatur; `Tenant` trägt keine Regel, ein
|
||||
Systemkontext ist dort gar nicht nötig (gemessen: einziger Lesezugriff
|
||||
außerhalb der Schleife ist `tenant.findMany`).
|
||||
|
||||
Fazit: KEIN Pfad löscht oder deaktiviert auf Leere. Die verbleibende Gefahr
|
||||
war das STUMME Nichtstun nach dem Scharfschalten — genau die schließt die
|
||||
Systemleseregel (Paar `…-ungebunden-null-zeilen` / `…-sieht-beide-mandanten`).
|
||||
|
||||
### (y4) Was dieser Durchlauf bewusst nicht löst
|
||||
|
||||
- Der Single-Flight-Riegel `processing` in `DkvService.processInbox` ist EIN
|
||||
prozessweites Boolean, nicht je Mandant. Seit je aktivem Mandanten ein
|
||||
eigener Cron-Auftrag läuft, können sich zwei Ticks verschiedener Mandanten
|
||||
überschneiden — der zweite bricht still ab und wartet bis zum nächsten
|
||||
Intervall (Verzögerung, kein Datenverlust; mit einem Mandanten
|
||||
unverändert). Neuer WINDOWS-Eintrag (#37) mit Lösungsweg (Riegel je
|
||||
Mandant, `Set<tenantId>`, Test "zwei Mandanten gleichzeitig, beide werden
|
||||
bedient"). Der Tick bleibt in diesem Durchlauf unangetastet (Auftrag).
|
||||
- `sendWelcomeEmail` hat weiterhin null Aufrufer; `@nestjs-modules/mailer`
|
||||
bleibt in `package.json`/Lockfile installiert, ist aber unbenutzt —
|
||||
Aufräumen, kein Defekt, kein Lockfile-Eingriff in diesem Durchlauf.
|
||||
- Der Sonderfall "Nutzer mit Treffern unter zwei Mandanten" im Digest
|
||||
(`distinct: ['userId']` liefert nur eine tenantId je Nutzer) bleibt wie in
|
||||
(t4) beschrieben ungelöst.
|
||||
- `rls-preflight.mjs` bekommt in Etappe 4 eine Prüfung
|
||||
`mit-systemkontext-sichtbar` — hier nicht gebaut, weil das Werkzeug unter
|
||||
der Wegwerf-Rolle dasselbe bereits misst (`…-sieht-beide-mandanten`).
|
||||
|
||||
### (y5) Was dieser Durchlauf bewusst nicht anfasst
|
||||
|
||||
- Der Schalter (`DATABASE_URL` → Rolle `tessera`, BYPASSRLS) — bleibt AUS;
|
||||
Compose-/Umgebungsdateien unangetastet (Gate gegen `5e0e408` in jeder
|
||||
Aufgabe).
|
||||
- `schema.prisma`, bestehende Migrationen (Prüfsumme), `package.json`,
|
||||
Lockfile.
|
||||
- Die drei SECURITY-DEFINER-Anmeldefunktionen — unangetastet.
|
||||
- `admin-seed.service.ts` — unverändert (nur dokumentiert, siehe (y3)).
|
||||
- `rls-preflight.mjs` — `ohne-kontext-leer` bleibt gültig, weil
|
||||
`is_system_context()` ohne Variable false ist (`is-system-context-ungesetzt-false`).
|
||||
- Der Tick `DkvService.processInbox` (je Mandant gebunden seit 260909-mir)
|
||||
und die 3b-Regeln der zehn persönlichen Tabellen.
|
||||
|
||||
## Etappe 2 — Abschluss
|
||||
|
||||
Etappe 2 der Mandantentrennung ist mit diesem Lauf (260911-gwh) vollständig:
|
||||
@@ -3353,8 +3589,17 @@ jetzt erfüllte Befund-K-Bedingung, die entfällt):
|
||||
- Das Verstummen des Mail-Startpfads (dieser Lauf, (s4)(a)) — EIGENES
|
||||
Signal, NICHT an #21 angeschlossen.
|
||||
|
||||
**Nachtrag (260914-eym):** Etappe 3c ist abgeschlossen — Systemkontext für
|
||||
die Hintergrunddienste (`## Systemkontext (Etappe 3c, 260914-eym)`); die
|
||||
beiden Signale #21 (DKV-Planer) und #30 (Mail-Startpfad) aus dieser Liste
|
||||
sind geschlossen, das eine durch Auftrag je Mandant über den Systemkontext,
|
||||
das andere durch Entfernen des Startpfads. Die übrigen Vorher-Prüfungen für
|
||||
Etappe 4 bleiben wie oben; hinzu kommt `mit-systemkontext-sichtbar` (siehe
|
||||
(y4)).
|
||||
|
||||
## Verweis
|
||||
|
||||
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
||||
bereits vollzogen hat (Stand-Spalte `gebunden`/`ungebunden`/`gemischt`),
|
||||
steht in `docs/mandantentrennung-zugriffsklassifikation.md`.
|
||||
steht in `docs/mandantentrennung-zugriffsklassifikation.md` (seit 260914-eym
|
||||
mit dem vierten Stand-Wert `system-gebunden`).
|
||||
|
||||
@@ -142,6 +142,31 @@ Bauform:
|
||||
|
||||
### 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) —
|
||||
@@ -188,6 +213,13 @@ Funktionsausbau in Etappe 3.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -150,21 +150,31 @@ dieser Übersicht auf, zum Beispiel
|
||||
Relationsfilter `group: { memberships: { some: { userId } } }` sichtbar,
|
||||
niemals als `tenantPrisma.group` im Quelltext).
|
||||
|
||||
| Bereich | Ungebunden | Gebunden | Hinweis |
|
||||
|---|---|---|---|
|
||||
| tenders | 35 | 27 | **war 62/0**, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat `tender-rss-feed.service.ts`/`listForUser` zusätzlich auf `forTenant()` umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (`createPlatform`/`remove`, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
|
||||
| groups | 0 | 31 | **war 37/0** — Aufgabe 2/3 (260909-jts) haben `groups.service.ts` (12 Methoden) und `module-grants.service.ts` (5 Methoden) vollständig auf `forTenant()`/`withTenantTransaction()` umgestellt. Die neun zusätzlichen, über `tx` gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben) |
|
||||
| ldap | 4 | 26 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04) |
|
||||
| dkv | 1 | 22 | **war 21/0** — Aufgabe 2/3 (260909-mir) haben `dkv.service.ts` vollständig auf `forTenant()` umgestellt: Konfigurationspfade (`loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection`), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf `dkvInvoiceHistory` einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung |
|
||||
| user | 8 | 14 | **war 17/0** — Aufgabe 2/3 (260910-das) haben `user.service.ts` (`findById`/`create`/`update`/`deactivate`/`delete` sowie die zwei neuen Plattform-Administratorsicht-Methoden), `admin-seed.service.ts` (Erstanlage des Administrators) und `user.controller.ts` (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf `forTenant()` umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: `findByUsername` in `user.service.ts` (plattformweit eindeutiger Schluessel, derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`), die Erstanlage-Pruefung und beide Zugriffe auf `tenant` in `admin-seed.service.ts`, sowie der neue Schleifentreiber `this.prisma.tenant.findMany` der beiden Plattform-Administratorsicht-Methoden in `user.service.ts` (`Tenant` traegt keinen Zeilenschutz) |
|
||||
| module-registry | 7 | 10 | **war 17/0** — Aufgabe 2/3 (260910-exd) haben `module-access.service.ts` (`getAccessibleModuleIds`: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; `getCatalogFlags`: eigener Aktivierungs-Lesezugriff) und `module-registry.service.ts` (`findActiveForTenant`, `activateForTenant`, `deactivateForTenant`, `isModuleActive`) auf `forTenant()` umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in `module-access.service.ts` (`findAccessibleModules`) und die sechs Katalogzugriffe in `module-registry.service.ts` (`findAll`, `findBySlug`, die beiden Katalog-Existenzpruefungen in `activateForTenant`/`deactivateForTenant`, die Katalogsuche in `isModuleActive`, `seedModule`) — der Modulkatalog (`Module`) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E) |
|
||||
| dashboard | 1 | 12 | **war 13/0** — Aufgabe 2/3 (260910-krx) haben `dashboard.service.ts` vollständig umgestellt: `getLayout`/`saveLayout` (gemeinsam gebunden), `getWidgets`/`addWidget`/`updateWidgetConfig`/`removeWidget` sowie `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` laufen über `forTenant()`, je Methode ein Klient. Der eine verbleibende ungebundene Rohtreffer ist bewusst: der Modulkatalog (`Module`) trägt heute keinen Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal — katastrophal würde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E aus `module-registry`, hier übernommen) |
|
||||
| auth | 3 | 10 | **war 8/5** — 260911-fh9 (Aufgabe 2) hat `getMe`, `changePassword`, `adminResetPassword` (fünf Rohtreffer auf `user`, drei Methoden) auf `forTenant()` umgestellt. Die 3 verbleibenden ungebundenen Rohtreffer sind die `$queryRaw`-Aufrufe der drei Anmeldefunktionen (`validateUser`, `requestPasswordReset`, `resetPassword`) — KEINE Modellzugriffe (`$` liegt nicht in `[a-zA-Z]`, die Bestandsaufnahme führt sie deshalb nicht als (Datei, Modell)-Paar), bewusst und dauerhaft ungebunden, siehe `20260909160000_auth_lookup_functions` und `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt "## Bereich auth", (h1) |
|
||||
| calendar | 0 | 12 | **war 12/0** — Aufgabe 2 (260911-cwh) hat `calendar.service.ts` vollständig auf `forTenant()` umgestellt: `getSources`, `addSource`, beide Abfragen von `updateSource`/`deleteSource`, alle drei Abfragen von `testConnection`, Laden plus beide Synchronstatus-Rückschreibungen von `fetchAndCacheEvents` — je Methode ein Klient. Anders als bei den sieben Bereichen davor bleibt KEIN ungebundener Rest übrig: `CalendarSource` trägt eine Pflicht-Mandantenkennung, und kein Pfad dieses Bereichs liest über Mandanten hinweg |
|
||||
| tenant | 8 | 3 | **war 8/0** — 260911-e2s (Aufgabe 3) hat drei gebundene Benutzerzähler in `tenant.controller.ts` eingeführt (Fan-out je Mandant nach dem Muster von `UserService.findAllForPlatformAdmin`, ersetzt die drei vorherigen Relationszähler); die acht `tenant`-Zugriffe selbst BLEIBEN ungebunden — `Tenant` trägt keine Regel in irgendeiner ausgelieferten Migration (260911-e2s Aufgabe 1, Prüfung 1/2), hier ist Ungebundenheit richtig, nicht geduldet |
|
||||
| favorites | 0 | 8 | **war 7/0** — 260911-gwh (Aufgabe 2) hat `favorites.service.ts` vollständig auf `forTenant()` umgestellt: `list`, `create`, `update`, `remove`, `getIconBytes` laufen je über EINEN Klienten `tenantPrisma` (7 gebundene `favoriteLink`-Rohtreffer); `create` prüft zusätzlich über einen gebundenen `widgetInstance.findUnique`, dass das Ziel-Widget dem Aufrufer gehört (T-GWH-05, Befund F aus Aufgabe 1: der Fremdschlüssel prüft am Zeilenschutz vorbei) — der achte gebundene Rohtreffer dieser Zeile |
|
||||
| settings | 1 | 3 | **war 4/0** — 260911-gwh (Aufgabe 2) hat `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` auf `forTenant()` umgestellt (3 gebundene `smtpConfig`-Rohtreffer). Der eine verbleibende ungebundene Rohtreffer ist der umbenannte Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (Befund D, WINDOWS #30) — bewusst, mit dreifacher Markierung; Befund K (`tenders`/`dkv` hängen an `getDecryptedSmtpConfig`) ist damit erfüllt |
|
||||
| **Summe** | **68** | **178** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), dann 83 nach 260911-cwh (`calendar` 12→0), unverändert nach 260911-e2s (`tenant` bleibt bei 8 ungebundenen Rohtreffern), dann 78 nach 260911-fh9 (`auth` 8→3), jetzt 68 nach 260911-gwh (`favorites` 7→0, `settings` 4→1). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), dann 159 nach 260911-cwh (zusätzlich 12 in `calendar`), dann 162 nach 260911-e2s (zusätzlich 3 in `tenant`), dann 167 nach 260911-fh9 (zusätzlich 5 in `auth`), jetzt 178 nach 260911-gwh (zusätzlich 8 in `favorites`, 3 in `settings`). Dies ist der ENDSTAND der Etappe 2: jeder verbleibende ungebundene Rohtreffer ist einer der in diesem Dokument benannten, bewusst ungebundenen Fälle. Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
|
||||
**Dritte Spalte `System` (260914-eym, Etappe 3c):** Rohtreffer
|
||||
`systemPrisma\.[a-zA-Z]*\.` je Bereich, gleiche Grep-Form wie die beiden
|
||||
anderen Spalten (nur `.ts` ohne `.spec.ts`) — die direkten Modellaufrufe
|
||||
über den Systemkontext-Klienten `forSystem()`. Dieselbe Grenze wie die
|
||||
anderen beiden Spalten: Relationsziele (`include: { fieldMappings }` in
|
||||
`ldap-config.service.ts`) zählt auch sie NICHT; autoritativ bleibt die
|
||||
Bestandsaufnahme unten (Stand `system-gebunden`). Die Werte aller drei
|
||||
Spalten sind mit der Schleife aus dem Gate von 260914-eym nachgerechnet
|
||||
(`for d in apps/api/src/*/`), nicht abgeschrieben.
|
||||
|
||||
| Bereich | Ungebunden | Gebunden | System | Hinweis |
|
||||
|---|---|---|---|---|
|
||||
| tenders | 33 | 27 | 2 | **war 62/0**, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat `tender-rss-feed.service.ts`/`listForUser` zusätzlich auf `forTenant()` umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (`createPlatform`/`remove`, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe). **260914-eym:** diese beiden Hälften (Kandidatenabfrage des Digest, Profilabfrage des Abgleichs) lesen jetzt über `forSystem()` — 35→33 ungebunden, 2 System |
|
||||
| groups | 0 | 31 | 0 | **war 37/0** — Aufgabe 2/3 (260909-jts) haben `groups.service.ts` (12 Methoden) und `module-grants.service.ts` (5 Methoden) vollständig auf `forTenant()`/`withTenantTransaction()` umgestellt. Die neun zusätzlichen, über `tx` gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben) |
|
||||
| ldap | 1 | 27 | 2 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer waren bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04). **260914-eym:** die beiden Leser in `ldap-config.service.ts` laufen über `forSystem()` (4→1 ungebunden, 2 System), die Schreibzeile der Nachverschlüsselung über `forTenant()` (26→27 gebunden); der eine verbleibende ungebundene Rohtreffer ist `resolveEmailForWrite` |
|
||||
| dkv | 0 | 22 | 1 | **war 21/0** — Aufgabe 2/3 (260909-mir) haben `dkv.service.ts` vollständig auf `forTenant()` umgestellt: Konfigurationspfade (`loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection`), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf `dkvInvoiceHistory` einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer war der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21). **260914-eym:** ersetzt durch `loadActiveConfigsForScheduler()` über `forSystem()` (1→0 ungebunden, 1 System) — WINDOWS #21 geschlossen |
|
||||
| user | 8 | 14 | 0 | **war 17/0** — Aufgabe 2/3 (260910-das) haben `user.service.ts` (`findById`/`create`/`update`/`deactivate`/`delete` sowie die zwei neuen Plattform-Administratorsicht-Methoden), `admin-seed.service.ts` (Erstanlage des Administrators) und `user.controller.ts` (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf `forTenant()` umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: `findByUsername` in `user.service.ts` (plattformweit eindeutiger Schluessel, derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`), die Erstanlage-Pruefung und beide Zugriffe auf `tenant` in `admin-seed.service.ts`, sowie der neue Schleifentreiber `this.prisma.tenant.findMany` der beiden Plattform-Administratorsicht-Methoden in `user.service.ts` (`Tenant` traegt keinen Zeilenschutz) |
|
||||
| module-registry | 7 | 10 | 0 | **war 17/0** — Aufgabe 2/3 (260910-exd) haben `module-access.service.ts` (`getAccessibleModuleIds`: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; `getCatalogFlags`: eigener Aktivierungs-Lesezugriff) und `module-registry.service.ts` (`findActiveForTenant`, `activateForTenant`, `deactivateForTenant`, `isModuleActive`) auf `forTenant()` umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in `module-access.service.ts` (`findAccessibleModules`) und die sechs Katalogzugriffe in `module-registry.service.ts` (`findAll`, `findBySlug`, die beiden Katalog-Existenzpruefungen in `activateForTenant`/`deactivateForTenant`, die Katalogsuche in `isModuleActive`, `seedModule`) — der Modulkatalog (`Module`) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E) |
|
||||
| dashboard | 1 | 12 | 0 | **war 13/0** — Aufgabe 2/3 (260910-krx) haben `dashboard.service.ts` vollständig umgestellt: `getLayout`/`saveLayout` (gemeinsam gebunden), `getWidgets`/`addWidget`/`updateWidgetConfig`/`removeWidget` sowie `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` laufen über `forTenant()`, je Methode ein Klient. Der eine verbleibende ungebundene Rohtreffer ist bewusst: der Modulkatalog (`Module`) trägt heute keinen Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal — katastrophal würde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E aus `module-registry`, hier übernommen) |
|
||||
| auth | 3 | 10 | 0 | **war 8/5** — 260911-fh9 (Aufgabe 2) hat `getMe`, `changePassword`, `adminResetPassword` (fünf Rohtreffer auf `user`, drei Methoden) auf `forTenant()` umgestellt. Die 3 verbleibenden ungebundenen Rohtreffer sind die `$queryRaw`-Aufrufe der drei Anmeldefunktionen (`validateUser`, `requestPasswordReset`, `resetPassword`) — KEINE Modellzugriffe (`$` liegt nicht in `[a-zA-Z]`, die Bestandsaufnahme führt sie deshalb nicht als (Datei, Modell)-Paar), bewusst und dauerhaft ungebunden, siehe `20260909160000_auth_lookup_functions` und `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt "## Bereich auth", (h1) |
|
||||
| calendar | 0 | 12 | 0 | **war 12/0** — Aufgabe 2 (260911-cwh) hat `calendar.service.ts` vollständig auf `forTenant()` umgestellt: `getSources`, `addSource`, beide Abfragen von `updateSource`/`deleteSource`, alle drei Abfragen von `testConnection`, Laden plus beide Synchronstatus-Rückschreibungen von `fetchAndCacheEvents` — je Methode ein Klient. Anders als bei den sieben Bereichen davor bleibt KEIN ungebundener Rest übrig: `CalendarSource` trägt eine Pflicht-Mandantenkennung, und kein Pfad dieses Bereichs liest über Mandanten hinweg |
|
||||
| tenant | 8 | 3 | 0 | **war 8/0** — 260911-e2s (Aufgabe 3) hat drei gebundene Benutzerzähler in `tenant.controller.ts` eingeführt (Fan-out je Mandant nach dem Muster von `UserService.findAllForPlatformAdmin`, ersetzt die drei vorherigen Relationszähler); die acht `tenant`-Zugriffe selbst BLEIBEN ungebunden — `Tenant` trägt keine Regel in irgendeiner ausgelieferten Migration (260911-e2s Aufgabe 1, Prüfung 1/2), hier ist Ungebundenheit richtig, nicht geduldet |
|
||||
| favorites | 0 | 8 | 0 | **war 7/0** — 260911-gwh (Aufgabe 2) hat `favorites.service.ts` vollständig auf `forTenant()` umgestellt: `list`, `create`, `update`, `remove`, `getIconBytes` laufen je über EINEN Klienten `tenantPrisma` (7 gebundene `favoriteLink`-Rohtreffer); `create` prüft zusätzlich über einen gebundenen `widgetInstance.findUnique`, dass das Ziel-Widget dem Aufrufer gehört (T-GWH-05, Befund F aus Aufgabe 1: der Fremdschlüssel prüft am Zeilenschutz vorbei) — der achte gebundene Rohtreffer dieser Zeile |
|
||||
| settings | 0 | 3 | 0 | **war 4/0** — 260911-gwh (Aufgabe 2) hat `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` auf `forTenant()` umgestellt (3 gebundene `smtpConfig`-Rohtreffer). Der eine verbleibende ungebundene Rohtreffer war der umbenannte Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (Befund D, WINDOWS #30). **260914-eym:** GELÖSCHT — `MailService` baut je Versand einen Transport über `getDecryptedSmtpConfig(tenantId)` (1→0 ungebunden, 0 System, kein Systemkontext nötig); Befund K (`tenders`/`dkv`/`mail` hängen an `getDecryptedSmtpConfig`) ist damit erfüllt — WINDOWS #30 geschlossen |
|
||||
| **Summe** | **61** | **179** | **5** | **260914-eym:** Ungebunden 68→61 (`tenders` −2, `ldap` −3, `dkv` −1, `settings` −1), Gebunden 178→179 (`ldap` +1), System 5 (`dkv` 1, `ldap` 2, `tenders` 2) — nachgerechnet mit der Gate-Schleife, nicht abgeschrieben. Vorgeschichte: Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), dann 83 nach 260911-cwh (`calendar` 12→0), unverändert nach 260911-e2s (`tenant` bleibt bei 8 ungebundenen Rohtreffern), dann 78 nach 260911-fh9 (`auth` 8→3), jetzt 68 nach 260911-gwh (`favorites` 7→0, `settings` 4→1). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), dann 159 nach 260911-cwh (zusätzlich 12 in `calendar`), dann 162 nach 260911-e2s (zusätzlich 3 in `tenant`), dann 167 nach 260911-fh9 (zusätzlich 5 in `auth`), jetzt 178 nach 260911-gwh (zusätzlich 8 in `favorites`, 3 in `settings`). Dies ist der ENDSTAND der Etappe 2: jeder verbleibende ungebundene Rohtreffer ist einer der in diesem Dokument benannten, bewusst ungebundenen Fälle. Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
|
||||
|
||||
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 72 Paare)
|
||||
|
||||
@@ -287,6 +297,25 @@ Benutzerdimension steht stattdessen in der Begründungsspalte der drei
|
||||
betroffenen Bestandsaufnahme-Zeilen (`calendarSource`, `widgetInstance`,
|
||||
`favoriteLink`) oben und im Abschnitt "Was diese Etappe NICHT entscheidet".
|
||||
|
||||
|
||||
**Stand 260914-eym:** die Paarzahl (72) und die Klassen-Verteilung sind
|
||||
UNVERÄNDERT — die fünfte Erkennungsform (`const X = forSystem(`) bringt
|
||||
keine neue Fundstelle und lässt keine verschwinden. Sieben Paare ändern nur
|
||||
ihren Stand: SECHS auf den neuen Wert `system-gebunden` —
|
||||
`dkv/dkv.service.ts`/`dkvModuleConfig`,
|
||||
`ldap/ldap-config.service.ts`/`ldapConfig`, `/ldapFieldMapping`, `/tenant`,
|
||||
`tenders/tender-digest.scheduler.ts`/`tenderMatch`,
|
||||
`tenders/tender-matching.service.ts`/`tenderSavedSearch` — und EINES auf
|
||||
`gebunden` (`settings/settings.service.ts`/`smtpConfig`, Startpfad
|
||||
gelöscht). Der neue Stand-Wert bedeutet: mindestens ein Zugriff dieses
|
||||
Paars läuft über den Systemkontext-Klienten und KEIN Zugriff ist ungebunden;
|
||||
Vorrang: ungebunden vorhanden UND anderes → `gemischt`, nur ungebunden →
|
||||
`ungebunden`, System ohne ungebunden → `system-gebunden` (auch neben
|
||||
mandantengebundenen Zugriffen — die Begründungsspalte nennt sie), sonst
|
||||
`gebunden`. Die Zahlen sind der Ausgabe von `rls-access-inventory.spec.ts`
|
||||
entnommen (30 Zusicherungen, darunter der Wachhund
|
||||
`FORSYSTEM_ALLOWED_CALL_SITES`).
|
||||
|
||||
| Klasse | Anzahl Paare |
|
||||
|---|---|
|
||||
| muss-mandantengebunden | 35 |
|
||||
@@ -529,6 +558,41 @@ Anmeldeweg (`validateUser`, `requestPasswordReset`, `resetPassword`) —
|
||||
geloest durch die drei SECURITY-DEFINER-Funktionen, nicht durch die Bauform
|
||||
"übergreifend lesen, dann je Mandant binden".
|
||||
|
||||
**Regelschluss (260914-eym) — Etappe 3c hat den Systemkontext gebaut; je Fall:**
|
||||
|
||||
- **Regelschluss (260914-eym), Fall ldap** (`getAllActiveConfigs()` und die
|
||||
Nachverschlüsselung in `onApplicationBootstrap()`): beide Methoden lesen
|
||||
über `forSystem()` (`system_read_policy … FOR SELECT` auf LdapConfig und —
|
||||
für `include: { fieldMappings }` — auf LdapFieldMapping); die
|
||||
Nachverschlüsselung schreibt je Altzeile GEBUNDEN über
|
||||
`forTenant(this.prisma, config.tenantId)`, weil Schreiben unter
|
||||
Systemkontext abgewiesen wird (gemessen P2025/42501). `Tenant` braucht
|
||||
keine Regel. Der Detektor zählt zwei `forSystem(`-Aufrufe in dieser Datei.
|
||||
- **Regelschluss (260914-eym), Fall tender-digest** (`runDigest`): die
|
||||
Kandidatenabfrage liest über `forSystem()` (Regel auf TenderMatch), die
|
||||
Schleife bleibt je Kandidatenzeile gebunden, bewusst ohne Benutzer.
|
||||
- **Regelschluss (260914-eym), Fall tender-matching** (`matchDelta`): die
|
||||
Profilabfrage liest über `forSystem()` (Regel auf TenderSavedSearch);
|
||||
Treffer-Anlage und Sofortmeldung bleiben je Profil gebunden, der
|
||||
Katalog-Lesezugriff (`tender`, D-03) bleibt ungebunden.
|
||||
- **Regelschluss (260914-eym), Fall admin-seed**: GEMESSEN — der einzige
|
||||
Lesezugriff außerhalb der Schleife ist `tenant.findMany` auf `Tenant`, das
|
||||
in keiner Migration `ENABLE ROW LEVEL SECURITY` trägt. Nichts umgebaut,
|
||||
Datei unverändert (Gate gegen `5e0e408`), nur dokumentiert; Stand bleibt
|
||||
`ungebunden`.
|
||||
- **Regelschluss (260914-eym), fünfter Fall (DKV-Planer, WINDOWS #21
|
||||
GESCHLOSSEN)**: `loadActiveConfigsForScheduler()` liest über `forSystem()`
|
||||
ALLE aktiven Konfigurationen (Regel auf DkvModuleConfig), der Planer
|
||||
registriert je Mandant einen eigenen Auftrag `dkv-inbox-poll:<tenantId>`
|
||||
(einmal-abfragen-viele-bedienen); mit einem Mandanten beobachtbar
|
||||
identisch (Cron-Expression, Tick, Protokollzeile — je ein Test).
|
||||
- **Regelschluss (260914-eym), sechster Fall (Mail-Startpfad, WINDOWS #30
|
||||
GESCHLOSSEN)**: der sechste Fall EXISTIERT NICHT MEHR — der Startpfad
|
||||
(`findFirst()` beim Boot) ist entfernt, nicht umgestellt; `MailService`
|
||||
baut je Versand einen Transport aus `getDecryptedSmtpConfig(tenantId)`
|
||||
des Empfänger-Mandanten. Deshalb trägt SmtpConfig keine
|
||||
`system_read_policy`.
|
||||
|
||||
## Bestandsaufnahme
|
||||
|
||||
Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit
|
||||
@@ -661,7 +725,7 @@ werden.
|
||||
| apps/api/src/tenders/tenders.controller.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `getTender` (Zeile 612) `include: { sources: { select: ... } }` auf `this.prisma.tender.findUnique`; `TenderSource` plattformweit ohne Zeilenschutz (dieselbe Einordnung wie `tender-dedup.service.ts`/`tenderSource`). |
|
||||
| apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. |
|
||||
| apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. |
|
||||
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an und liest beim Start alle Mandanten fuer die Standardgruppen-Reparatur — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). Fuenfter und bislang einziger bereits vollstaendig richtiger Fall der Hintergrunddienst-Falle (Befund K, siehe Abschnitt unten). |
|
||||
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an und liest beim Start alle Mandanten fuer die Standardgruppen-Reparatur — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). Fuenfter und bislang einziger bereits vollstaendig richtiger Fall der Hintergrunddienst-Falle (Befund K, siehe Abschnitt unten). 3c-Befund (260914-eym): einziger Lesezugriff außerhalb der Schleife, `Tenant` ohne Regel — kein Systemkontext nötig, Datei unverändert, Stand bleibt `ungebunden`. |
|
||||
| apps/api/src/user/admin-seed.service.ts | user | beides | gemischt | Klassenkorrektur (260910-das, Aufgabe 3): wechselt von `bewusst-uebergreifend` auf `beides`, weil die bisherige Begruendung ("es gibt strukturell keinen Mandanten zum Binden") nachweislich FALSCH war (Befund J) — der Mandant wird eine Anweisung vorher angelegt und ist bekannt. Die Erstanlage-Pruefung bleibt bewusst ungebunden (kein Mandant existiert zu diesem Zeitpunkt, `username` ist plattformweit eindeutig); die Erstanlage des Administrators selbst laeuft seit Aufgabe 2 ueber `forTenant()`, gebunden an den unmittelbar zuvor angelegten Mandanten. Eine P2002-Kollision beim Anlegen wird wie "Administrator existiert bereits" behandelt statt den Start abzubrechen (Befund I). |
|
||||
| apps/api/src/user/user.controller.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins (260910-das, Aufgabe 3): die Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege (rollenabhaengig ueber `UserService.findById`/`findByIdForPlatformAdmin`) und alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/ausliefern, Akzentfarbe) laufen ueber `forTenant()`; die Rollenverzweigung zwischen mandantengebundener ADMIN-Sicht und der uebergreifenden `SUPER_ADMIN`-Sicht (ueber `UserService.findAllForPlatformAdmin`) bleibt bestehen. Der wirkungslose Selbstloesch-Riegel (Befund H, verglich gegen `currentUser.sub`, ein im Sitzungsnachweis nicht existierendes Feld) ist auf `currentUser.id` korrigiert. |
|
||||
| apps/api/src/user/user.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Schleifentreiber der neuen Plattform-Administratorsicht (`findAllForPlatformAdmin`/`findByIdForPlatformAdmin`, 260910-das, Aufgabe 2, Befund F/N) — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). |
|
||||
@@ -728,15 +792,23 @@ werden.
|
||||
"Regelschluss Benutzerdimension (Etappe 3b, 260911-nke)". Was weiterhin
|
||||
offen ist: ein Aufrufer, der `userId` vergisst, sieht den ganzen
|
||||
Mandanten (kein Wächter gebaut, siehe `.planning/WINDOWS.md`);
|
||||
Systemkontext (Etappe 3c) und Anmeldenamen pro Mandant (Etappe 3a) bleiben
|
||||
offen.
|
||||
- **Wie das Mailmodul künftig je Mandant versendet (260911-gwh).** Der
|
||||
Startpfad `loadAnySmtpConfigForStartupTransport()` bleibt bewusst
|
||||
ungebunden (sechster Fall der Hintergrunddienst-Falle, WINDOWS #30, siehe
|
||||
oben) — ein Umbau auf Transport je Versand aus
|
||||
`getDecryptedSmtpConfig(tenantId)`, die Form, die `DkvMailService`/
|
||||
`TenderMailService` bereits haben, ist eine Funktionsänderung
|
||||
(Umbau des Mailmoduls), kein Bindungsumbau, und deshalb NICHT Gegenstand
|
||||
dieser Etappe. Siehe
|
||||
Anmeldenamen pro Mandant (Etappe 3a) bleibt offen. **Systemkontext
|
||||
(Etappe 3c) — erledigt (260914-eym):** Migration
|
||||
`20260914120000_rls_system_context_read` (`is_system_context()`,
|
||||
`system_read_policy … FOR SELECT` auf fünf Tabellen), Schwesterhelfer
|
||||
`forSystem()`, fünfte Erkennungsform des Detektors mit Erlaubnisliste;
|
||||
siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
|
||||
"## Systemkontext (Etappe 3c, 260914-eym)" und den Regelschluss je Fall
|
||||
im Hintergrunddienst-Abschnitt oben.
|
||||
- **Wie das Mailmodul künftig je Mandant versendet (260911-gwh).** ~~Der
|
||||
Startpfad bleibt bewusst ungebunden (sechster Fall der
|
||||
Hintergrunddienst-Falle, WINDOWS #30, siehe oben) — ein Umbau auf
|
||||
Transport je Versand aus `getDecryptedSmtpConfig(tenantId)`, die Form,
|
||||
die `DkvMailService`/`TenderMailService` bereits haben, ist eine
|
||||
Funktionsänderung (Umbau des Mailmoduls), kein Bindungsumbau, und deshalb
|
||||
NICHT Gegenstand dieser Etappe.~~ **Aufgelöst (260914-eym):** genau dieser
|
||||
Umbau ist gebaut — Startpfad und Mailer-Fabrik entfernt, `MailService`
|
||||
baut je Versand einen Transport nach Mandant des Empfängers, WINDOWS #30
|
||||
geschlossen. Siehe
|
||||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, "## Bereich settings",
|
||||
(s4)(a).
|
||||
(s4)(a) mit Nachtrag.
|
||||
|
||||
Reference in New Issue
Block a user