docs(quick-260910-jab): Aktenstand kohaerent machen — Ledger, Klassifikation, Kritikschrift, Betriebsanleitung
- WINDOWS.md: #19 auf fixed gesetzt (Tabelle + JSON-Block), mit Beleg (Migrationsname + benannte Pruefungen) und ausdruecklicher Feststellung, dass die SearchProvider-Haelfte als widerlegte Praemisse schliesst, nicht als geloestes Problem. #18/#20/#21/#22/#23 bleiben unveraendert offen. Ein neuer Eintrag #24 haelt den fehlenden Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle offen (verschwindet nicht mit #19). Die vier Kopfzahlen sind aus dem JSON-Block abgeleitet (6 offen, 17 behoben, 1 zurueckgestellt, 24 gesamt). - Klassifikation: #19-Block von offener Frage zu beantwortet, die Uebersichtszeile tenders (35/27) und Summenzeile (107/135) aus dem Quelltext neu abgeleitet, vier Bestandsaufnahme-Zeilen nachgezogen (searchProvider, groups.service.ts/user, module-grants.service.ts/ moduleGrant, tender-rss-feed.service.ts/tenderRssFeedSource), der Punkt in "Was diese Etappe NICHT entscheidet" aufgeloest. - Kritikschrift: neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit tatsaechlich beobachteter Ausgabe, Regelliste der lebenden Datenbank, Signaltabelle mit beiden Fehlerrichtungen je Regel, der neuen Stelle aus Befund F, der selbst ausgefuehrten Messung zur fehlenden Benutzerdimension (keine zweite Sitzungsvariable gefunden) und dem, was dieser Durchlauf nicht loest/nicht anfasst. Fuenf ueberholte Bestandsstellen mit Nachtraegen versehen (g4, t4, die Signaltabellenzeile zu den RSS-Pfaden, drei aufgezeichnete Werkzeugausgaben, m4), die alten Messprotokolle bleiben woertlich stehen. - Betriebsanleitung: die eine Stelle, die #19 als offen fuehrte, nennt jetzt den Aufloesungsstand und WINDOWS #24. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
+20
-7
@@ -2,9 +2,9 @@
|
|||||||
schema_version: 1
|
schema_version: 1
|
||||||
open_count: 6
|
open_count: 6
|
||||||
waived_count: 1
|
waived_count: 1
|
||||||
fixed_count: 16
|
fixed_count: 17
|
||||||
total_count: 23
|
total_count: 24
|
||||||
last_updated: 2026-09-10T09:28:45.310Z
|
last_updated: 2026-09-10T12:35:40.000Z
|
||||||
---
|
---
|
||||||
|
|
||||||
# Broken Windows Ledger
|
# Broken Windows Ledger
|
||||||
@@ -33,11 +33,12 @@ last_updated: 2026-09-10T09:28:45.310Z
|
|||||||
| 16 | 14 | unmet-truth | apps/api/src/tenders/tenders.controller.ts | | Das Postfach im Ausschreibungs-Radar hat keinen Verbindungstest, obwohl die Faehigkeit fertig vorliegt. Beide Inbox-Provider bringen testConnection() mit (imap.provider.ts:343, exchange-inbox.provider.ts:294), und das DKV-Modul nutzt sie ueber POST /dkv/test-connection samt Knopf 'Verbindung testen' im InboxConfigForm. Beim Ausschreibungs-Radar fehlt beides: der Controller kennt zu email-config nur GET (:343) und PUT (:357), kein Test-Endpunkt, und das Formular unter Meine Quellen hat keinen Knopf. Historie geprueft: der Knopf war nie vorhanden (git log -S testConnection im tender-radar-Frontend ist leer), es ist also eine Luecke, keine Regression. Folge fuer den Betrieb: ein Tippfehler in der EWS-Endpunkt-URL oder falsche Zugangsdaten fallen erst auf, wenn dauerhaft nichts ankommt — und dann ist nicht unterscheidbar, ob die Verbindung scheitert oder schlicht keine Alarm-Mail da war. Das trifft besonders WINDOWS #12, dessen ganzer Zweck der Beleg des handgeschriebenen NTLM/SOAP-Wegs ist. Aufgefallen am 2026-09-07 beim Einrichten des Postfachs. | fixed | | 2026-09-07T13:22:33.916Z | 2026-09-09T05:20:09.851Z |
|
| 16 | 14 | unmet-truth | apps/api/src/tenders/tenders.controller.ts | | Das Postfach im Ausschreibungs-Radar hat keinen Verbindungstest, obwohl die Faehigkeit fertig vorliegt. Beide Inbox-Provider bringen testConnection() mit (imap.provider.ts:343, exchange-inbox.provider.ts:294), und das DKV-Modul nutzt sie ueber POST /dkv/test-connection samt Knopf 'Verbindung testen' im InboxConfigForm. Beim Ausschreibungs-Radar fehlt beides: der Controller kennt zu email-config nur GET (:343) und PUT (:357), kein Test-Endpunkt, und das Formular unter Meine Quellen hat keinen Knopf. Historie geprueft: der Knopf war nie vorhanden (git log -S testConnection im tender-radar-Frontend ist leer), es ist also eine Luecke, keine Regression. Folge fuer den Betrieb: ein Tippfehler in der EWS-Endpunkt-URL oder falsche Zugangsdaten fallen erst auf, wenn dauerhaft nichts ankommt — und dann ist nicht unterscheidbar, ob die Verbindung scheitert oder schlicht keine Alarm-Mail da war. Das trifft besonders WINDOWS #12, dessen ganzer Zweck der Beleg des handgeschriebenen NTLM/SOAP-Wegs ist. Aufgefallen am 2026-09-07 beim Einrichten des Postfachs. | fixed | | 2026-09-07T13:22:33.916Z | 2026-09-09T05:20:09.851Z |
|
||||||
| 17 | 6 | unmet-truth | docker-compose.yml | | Hochgeladene Dateien ueberleben kein Neuerstellen der Container. Der Code legt sie unter user-files/ ab (user.controller.ts:40 und :266 fuer Profilbilder, dazu die DKV-Exporte), aber KEINE der Compose-Dateien mountet dieses Verzeichnis — weder im Repository (docker-compose.yml, .prod.yml, .dev.yml haben nur das Volume pgdata) noch in der abweichenden Datei auf dem Server /opt/tessera/docker-compose.yml. Am 2026-09-09 gemessen: 'docker inspect' auf tessera-api-1 meldet ueberhaupt keinen Mount, /app/user-files liegt damit nur in der beschreibbaren Container-Schicht und ist bei jedem 'up -d --force-recreate' weg. Aufgefallen beim Schreiben des Betriebshandbuchs. KEIN Schaden entstanden: aktuell hat kein Nutzer ein Profilbild hinterlegt (avatarPath ueberall NULL), und die bisherigen Neuerstellungen trafen einen leeren Ordner. Die Luecke schlaegt zu, sobald der erste Nutzer ein Bild hochlaedt oder ein DKV-Export aufgehoben werden soll. Behebung: ein benanntes Volume oder Bind-Mount fuer user-files in beiden Compose-Dateien; die Server-Datei muss zusaetzlich von Hand ergaenzt werden, weil sie vom Repository abweicht. | fixed | | 2026-09-09T06:42:22.801Z | 2026-09-09T08:22:51.235Z |
|
| 17 | 6 | unmet-truth | docker-compose.yml | | Hochgeladene Dateien ueberleben kein Neuerstellen der Container. Der Code legt sie unter user-files/ ab (user.controller.ts:40 und :266 fuer Profilbilder, dazu die DKV-Exporte), aber KEINE der Compose-Dateien mountet dieses Verzeichnis — weder im Repository (docker-compose.yml, .prod.yml, .dev.yml haben nur das Volume pgdata) noch in der abweichenden Datei auf dem Server /opt/tessera/docker-compose.yml. Am 2026-09-09 gemessen: 'docker inspect' auf tessera-api-1 meldet ueberhaupt keinen Mount, /app/user-files liegt damit nur in der beschreibbaren Container-Schicht und ist bei jedem 'up -d --force-recreate' weg. Aufgefallen beim Schreiben des Betriebshandbuchs. KEIN Schaden entstanden: aktuell hat kein Nutzer ein Profilbild hinterlegt (avatarPath ueberall NULL), und die bisherigen Neuerstellungen trafen einen leeren Ordner. Die Luecke schlaegt zu, sobald der erste Nutzer ein Bild hochlaedt oder ein DKV-Export aufgehoben werden soll. Behebung: ein benanntes Volume oder Bind-Mount fuer user-files in beiden Compose-Dateien; die Server-Datei muss zusaetzlich von Hand ergaenzt werden, weil sie vom Repository abweicht. | fixed | | 2026-09-09T06:42:22.801Z | 2026-09-09T08:22:51.235Z |
|
||||||
| 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 | |
|
| 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). | open | | 2026-09-09T08:08:19.293Z | |
|
| 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 | |
|
| 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. | open | | 2026-09-09T14:41:07.256Z | |
|
||||||
| 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 | |
|
| 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 | |
|
| 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 | |
|
||||||
|
|
||||||
````json
|
````json
|
||||||
[
|
[
|
||||||
@@ -263,11 +264,11 @@ last_updated: 2026-09-10T09:28:45.310Z
|
|||||||
"phase": "2",
|
"phase": "2",
|
||||||
"file": "apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql",
|
"file": "apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql",
|
||||||
"line": null,
|
"line": null,
|
||||||
"description": "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).",
|
"description": "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.",
|
||||||
"status": "open",
|
"status": "fixed",
|
||||||
"reason": "",
|
"reason": "",
|
||||||
"recorded_at": "2026-09-09T08:08:19.293Z",
|
"recorded_at": "2026-09-09T08:08:19.293Z",
|
||||||
"resolved_at": null
|
"resolved_at": "2026-09-10T12:35:40.000Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 20,
|
"id": 20,
|
||||||
@@ -316,6 +317,18 @@ last_updated: 2026-09-10T09:28:45.310Z
|
|||||||
"reason": "",
|
"reason": "",
|
||||||
"recorded_at": "2026-09-10T09:28:45.310Z",
|
"recorded_at": "2026-09-10T09:28:45.310Z",
|
||||||
"resolved_at": null
|
"resolved_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"kind": "deviation",
|
||||||
|
"phase": "quick-260910-jab",
|
||||||
|
"file": "apps/api/src/tenders/tender-rss-feed.service.ts",
|
||||||
|
"line": null,
|
||||||
|
"description": "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.",
|
||||||
|
"status": "open",
|
||||||
|
"reason": "",
|
||||||
|
"recorded_at": "2026-09-10T12:35:40.000Z",
|
||||||
|
"resolved_at": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
````
|
````
|
||||||
|
|||||||
@@ -87,8 +87,13 @@ Arbeitsvorrat fuer die Umstellung. **16** Paare betreffen keine
|
|||||||
mandantengebundene Tabelle (plattformweite Daten wie der Ausschreibungs- und
|
mandantengebundene Tabelle (plattformweite Daten wie der Ausschreibungs- und
|
||||||
Modulkatalog, D-03) und **3** sind bewusst uebergreifend mit ausgeschriebenem
|
Modulkatalog, D-03) und **3** sind bewusst uebergreifend mit ausgeschriebenem
|
||||||
Grund. Zusaetzlich zu beachten: das entdeckte, aber in dieser Etappe nicht
|
Grund. Zusaetzlich zu beachten: das entdeckte, aber in dieser Etappe nicht
|
||||||
behobene `forTenant()`-Verbindungsproblem (WINDOWS #20, siehe unten) und
|
behobene `forTenant()`-Verbindungsproblem (WINDOWS #20, siehe unten). WINDOWS
|
||||||
WINDOWS #19 (nullbares `tenantId` bei `SearchProvider`/`TenderRssFeedSource`).
|
#19 (nullbares `tenantId` bei `SearchProvider`/`TenderRssFeedSource`) ist
|
||||||
|
GESCHLOSSEN (260910-jab, Migration
|
||||||
|
`20260910120000_rls_widen_membership_grant_and_platform_read`) — siehe
|
||||||
|
`docs/mandantentrennung-zugriffsklassifikation.md`, Abschnitt "Zwei belegte
|
||||||
|
Befunde", und `.planning/WINDOWS.md`. Offen geblieben ist der Verwaltungsweg
|
||||||
|
fuer plattformweite Zeilen unter der Anwendungsrolle (WINDOWS #24).
|
||||||
|
|
||||||
Darunter war bis Aufgabe 2 dieser Etappe auch der Anmeldeweg selbst, der
|
Darunter war bis Aufgabe 2 dieser Etappe auch der Anmeldeweg selbst, der
|
||||||
strukturell nicht anders funktionieren konnte: `apps/api/src/auth/auth.service.ts`
|
strukturell nicht anders funktionieren konnte: `apps/api/src/auth/auth.service.ts`
|
||||||
|
|||||||
@@ -198,6 +198,18 @@ modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt: bestanden —
|
|||||||
tenantmoduleactivation-gebunden-nur-eigene-zeile: bestanden — forTenant(TENANT-A) liefert 1 Zeile(n): ["TENANT-A"]
|
tenantmoduleactivation-gebunden-nur-eigene-zeile: bestanden — forTenant(TENANT-A) liefert 1 Zeile(n): ["TENANT-A"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**NACHTRAG (260910-jab):** die beiden fett hervorgehobenen Zeilen oben
|
||||||
|
(`groupmembership-schreiben-fremder-benutzer-nicht-verhindert` und
|
||||||
|
`modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt`) sind ein
|
||||||
|
Messprotokoll vom 2026-09-09 und bleiben UNVERÄNDERT stehen — sie belegen,
|
||||||
|
dass die beiden Löcher existierten. Seit Migration
|
||||||
|
`20260910120000_rls_widen_membership_grant_and_platform_read` sind beide
|
||||||
|
Aussagen ÜBERHOLT: die Prüfungen sind umgekehrt (`groupmembership-schreiben-
|
||||||
|
fremder-benutzer-abgelehnt`, `modulegrant-fremde-gruppe-abgelehnt`), das
|
||||||
|
INSERT wird jetzt jeweils ABGEWIESEN statt zu gelingen. Siehe den neuen
|
||||||
|
Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" unten für die
|
||||||
|
aktuell beobachtete Ausgabe.
|
||||||
|
|
||||||
Die Belegzeile, die diesen Abschnitt trägt, ist `group-ungebunden-null-zeilen`:
|
Die Belegzeile, die diesen Abschnitt trägt, ist `group-ungebunden-null-zeilen`:
|
||||||
der IDENTISCHE `SELECT "tenantId" FROM "Group"` ohne vorheriges `set_config`
|
der IDENTISCHE `SELECT "tenantId" FROM "Group"` ohne vorheriges `set_config`
|
||||||
liefert **0 Zeilen**, nicht etwa die 2 tatsächlich vorhandenen — an der
|
liefert **0 Zeilen**, nicht etwa die 2 tatsächlich vorhandenen — an der
|
||||||
@@ -366,6 +378,14 @@ zum `deleteMany`-mit-`notIn` des ldap-Bereichs.
|
|||||||
`addUserToDefaultGroup`, `assertTargetBelongsToTenant`) bleiben deshalb
|
`addUserToDefaultGroup`, `assertTargetBelongsToTenant`) bleiben deshalb
|
||||||
der primäre Schutz gegen diese beiden Formen der Rechteausweitung und
|
der primäre Schutz gegen diese beiden Formen der Rechteausweitung und
|
||||||
werden durch diesen Durchlauf NICHT durch die Datenbank ersetzt.
|
werden durch diesen Durchlauf NICHT durch die Datenbank ersetzt.
|
||||||
|
**NACHTRAG (260910-jab):** ÜBERHOLT — seit Migration
|
||||||
|
`20260910120000_rls_widen_membership_grant_and_platform_read` prüft
|
||||||
|
`GroupMembership` BEIDE Seiten (T-JTS-02 geschlossen) und `ModuleGrant`
|
||||||
|
zusätzlich beide möglichen Ziele (T-JTS-03 geschlossen, beide Zweige des
|
||||||
|
Entweder-oder D-04). Die Anwendungsprüfungen bleiben trotzdem bestehen —
|
||||||
|
sie sind bis zum Scharfschalten (#18) der einzige tatsächlich wirksame
|
||||||
|
Schutz, siehe den neuen Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und
|
||||||
|
WINDOWS #19" unten.
|
||||||
|
|
||||||
### (g5) Fortschreibung des ldap-Abschnitts
|
### (g5) Fortschreibung des ldap-Abschnitts
|
||||||
|
|
||||||
@@ -411,6 +431,17 @@ tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit: bestanden
|
|||||||
Alle 32 Pruefungen bestanden.
|
Alle 32 Pruefungen bestanden.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**NACHTRAG (260910-jab):** die fett hervorgehobene Zeile oben
|
||||||
|
(`tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar`) ist ein
|
||||||
|
Messprotokoll vom 2026-09-09 und bleibt UNVERÄNDERT stehen — sie belegt,
|
||||||
|
dass WINDOWS #19 existierte. Seit Migration
|
||||||
|
`20260910120000_rls_widen_membership_grant_and_platform_read` ist diese
|
||||||
|
Aussage ÜBERHOLT: die Prüfung ist umgekehrt
|
||||||
|
(`tenderrssfeed-plattformzeile-gebunden-sichtbar`), die plattformweite Zeile
|
||||||
|
ist jetzt unter BEIDEN Mandantenkontexten SICHTBAR. Siehe den neuen
|
||||||
|
Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" unten für die
|
||||||
|
aktuell beobachtete Ausgabe.
|
||||||
|
|
||||||
Die Belegzeile, die diesen Abschnitt trägt, ist
|
Die Belegzeile, die diesen Abschnitt trägt, ist
|
||||||
`tendersavedsearch-ungebunden-null-zeilen`: der IDENTISCHE
|
`tendersavedsearch-ungebunden-null-zeilen`: der IDENTISCHE
|
||||||
`SELECT "tenantId" FROM "TenderSavedSearch"` ohne vorheriges `set_config`
|
`SELECT "tenantId" FROM "TenderSavedSearch"` ohne vorheriges `set_config`
|
||||||
@@ -441,6 +472,12 @@ Bereich brauchte:
|
|||||||
Kehrseite: ein gebundenes `INSERT` mit `tenantId = NULL` wird von der
|
Kehrseite: ein gebundenes `INSERT` mit `tenantId = NULL` wird von der
|
||||||
ausgelieferten Policy abgewiesen — `createPlatform` würde in genau diese
|
ausgelieferten Policy abgewiesen — `createPlatform` würde in genau diese
|
||||||
Abweisung laufen, würde man es binden.
|
Abweisung laufen, würde man es binden.
|
||||||
|
**NACHTRAG (260910-jab):** ÜBERHOLT für `listForUser` — seit Migration
|
||||||
|
`20260910120000_rls_widen_membership_grant_and_platform_read` bindet
|
||||||
|
dieser Pfad (die neue Leseregel schließt Zeilen ohne Mandant ausdrücklich
|
||||||
|
ein). `createPlatform`/`remove` bleiben unverändert ungebunden, aus dem
|
||||||
|
jetzt ausdrücklichen Grund einer eigenen Schreibregel je Befehl (WINDOWS
|
||||||
|
#24).
|
||||||
|
|
||||||
Eine dritte Messung trägt die Fehlerbehandlung von Aufgabe 2:
|
Eine dritte Messung trägt die Fehlerbehandlung von Aufgabe 2:
|
||||||
`tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit` zeigt,
|
`tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit` zeigt,
|
||||||
@@ -474,7 +511,7 @@ nicht eingeführt.
|
|||||||
| `TenderNotificationPrefService.getForUser` | **Sonderfall**: kein Treffer bedeutet hier nicht "leer", sondern der Vorgabewert `daily` (D-01) | Ein Nutzer, der `off` gewählt hat, sieht in der Oberfläche wieder `daily` — ein zu kleines Leseergebnis setzt die Einstellung stillschweigend auf täglich zurück, statt sie leer zu lassen |
|
| `TenderNotificationPrefService.getForUser` | **Sonderfall**: kein Treffer bedeutet hier nicht "leer", sondern der Vorgabewert `daily` (D-01) | Ein Nutzer, der `off` gewählt hat, sieht in der Oberfläche wieder `daily` — ein zu kleines Leseergebnis setzt die Einstellung stillschweigend auf täglich zurück, statt sie leer zu lassen |
|
||||||
| `TenderEmailConfigService.getConfigForApi` | Der gebundene `findUnique` liefert 0 Zeilen statt der eigenen Konfiguration | `GET /email-config` liefert `null`; die Oberfläche zeigt "kein Postfach verbunden" für einen Nutzer, der tatsächlich eines hat |
|
| `TenderEmailConfigService.getConfigForApi` | Der gebundene `findUnique` liefert 0 Zeilen statt der eigenen Konfiguration | `GET /email-config` liefert `null`; die Oberfläche zeigt "kein Postfach verbunden" für einen Nutzer, der tatsächlich eines hat |
|
||||||
| `TenderEmailConfigService.testConnection` | Der gebundene Rückgriff auf gespeicherte Zugangsdaten liefert 0 Zeilen statt der eigenen | Ein Verbindungstest mit leer gelassenem Formular (Rückgriff auf gespeicherte Zugangsdaten) schlägt fehl, obwohl gespeicherte Zugangsdaten existieren |
|
| `TenderEmailConfigService.testConnection` | Der gebundene Rückgriff auf gespeicherte Zugangsdaten liefert 0 Zeilen statt der eigenen | Ein Verbindungstest mit leer gelassenem Formular (Rückgriff auf gespeicherte Zugangsdaten) schlägt fehl, obwohl gespeicherte Zugangsdaten existieren |
|
||||||
| `TenderRssFeedSourceService.listForUser`/`createPlatform`/`remove` (bewusst UNGEBUNDEN, WINDOWS #19) | Betrifft nicht diese drei Pfade selbst — sie binden nicht und liefern deshalb weiterhin die plattformweite Zeile korrekt. Das Risiko liegt in einer KÜNFTIGEN Bindung (Etappe 3), nicht in dieser Umstellung | Würde man sie binden: leere Feed-Liste bzw. 404 beim Entfernen einer plattformweiten Quelle — hier ausdrücklich als Grenze festgehalten, nicht als heute beobachtbares Verhalten |
|
| `TenderRssFeedSourceService.listForUser`/`createPlatform`/`remove` (bewusst UNGEBUNDEN, WINDOWS #19) | Betrifft nicht diese drei Pfade selbst — sie binden nicht und liefern deshalb weiterhin die plattformweite Zeile korrekt. Das Risiko liegt in einer KÜNFTIGEN Bindung (Etappe 3), nicht in dieser Umstellung | Würde man sie binden: leere Feed-Liste bzw. 404 beim Entfernen einer plattformweiten Quelle — hier ausdrücklich als Grenze festgehalten, nicht als heute beobachtbares Verhalten. **NACHTRAG (260910-jab):** ÜBERHOLT für `listForUser` — dieser Pfad ist seit Migration `20260910120000_rls_widen_membership_grant_and_platform_read` GEBUNDEN (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur ihn sonst still auf nur die plattformweiten Zeilen reduziert). `createPlatform`/`remove` bleiben unverändert ungebunden (WINDOWS #24). |
|
||||||
| `TenderRssFeedSourceService.createForUser` (gebunden) | Der Zähler (`count`, gebunden) liefert 0 statt der tatsächlichen Anzahl eigener Feeds | Ein Nutzer, der bereits am Limit von 20 eigenen Feeds ist, könnte scheinbar unbegrenzt neue anlegen (harmlose Richtung: die Kappung aus T-17-10 wirkt nicht mehr, kein Datenverlust) |
|
| `TenderRssFeedSourceService.createForUser` (gebunden) | Der Zähler (`count`, gebunden) liefert 0 statt der tatsächlichen Anzahl eigener Feeds | Ein Nutzer, der bereits am Limit von 20 eigenen Feeds ist, könnte scheinbar unbegrenzt neue anlegen (harmlose Richtung: die Kappung aus T-17-10 wirkt nicht mehr, kein Datenverlust) |
|
||||||
|
|
||||||
### (t3) Welcher Code Leere als Abwesenheit deutet
|
### (t3) Welcher Code Leere als Abwesenheit deutet
|
||||||
@@ -541,6 +578,18 @@ Warnung wäre Dauerlärm und verlöre ihr Signal.
|
|||||||
Policy-Semantik selbst (eine `tenantId IS NULL OR tenantId =
|
Policy-Semantik selbst (eine `tenantId IS NULL OR tenantId =
|
||||||
current_tenant_id()`-Lesevariante) gehört zu Etappe 3 und wird hier nicht
|
current_tenant_id()`-Lesevariante) gehört zu Etappe 3 und wird hier nicht
|
||||||
angefasst.
|
angefasst.
|
||||||
|
**NACHTRAG (260910-jab): ÜBERHOLT — WINDOWS #19 ist geschlossen.** Migration
|
||||||
|
`20260910120000_rls_widen_membership_grant_and_platform_read` ersetzt die
|
||||||
|
einfache Regel durch vier nach Befehl getrennte Regeln
|
||||||
|
(`tenant_platform_read_policy` schließt Zeilen ohne Mandant beim Lesen
|
||||||
|
ausdrücklich ein, die drei Schreibregeln verlangen weiterhin einen
|
||||||
|
Mandanten). `listForUser` ist seither GEBUNDEN (Befund F: ungebunden hätte
|
||||||
|
die Reparatur ihn sonst still auf nur die plattformweiten Zeilen
|
||||||
|
reduziert); `createPlatform`/`remove` bleiben bewusst ungebunden — beide
|
||||||
|
Pfade lassen sich unter der Anwendungsrolle grundsätzlich nicht
|
||||||
|
anlegen/entfernen (WINDOWS #24, eigener offener Punkt, verschwindet nicht
|
||||||
|
mit #19). Siehe den neuen Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und
|
||||||
|
WINDOWS #19" unten.
|
||||||
- **Die übergreifenden Hälften der beiden Hintergrunddienste.** Aufgabe 3
|
- **Die übergreifenden Hälften der beiden Hintergrunddienste.** Aufgabe 3
|
||||||
bindet nur die Je-Treffer-Hälften von `tender-digest.scheduler.ts` und
|
bindet nur die Je-Treffer-Hälften von `tender-digest.scheduler.ts` und
|
||||||
`tender-matching.service.ts`; die Kandidatenabfrage
|
`tender-matching.service.ts`; die Kandidatenabfrage
|
||||||
@@ -1293,6 +1342,18 @@ freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung: bestanden — a
|
|||||||
Alle 66 Pruefungen bestanden.
|
Alle 66 Pruefungen bestanden.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**NACHTRAG (260910-jab):** die fett hervorgehobene Zeile oben
|
||||||
|
(`gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus`) ist ein
|
||||||
|
Messprotokoll vom 2026-09-10 und bleibt UNVERÄNDERT stehen. Ihre Behauptung
|
||||||
|
"die Regel auf ModuleGrant lässt diese Zeile durch (T-JTS-03)" ist seit
|
||||||
|
Migration `20260910120000_rls_widen_membership_grant_and_platform_read`
|
||||||
|
ÜBERHOLT: die Regel weist ein gebundenes Einfügen von `grant-foreign-group`
|
||||||
|
jetzt nachweislich ab (`modulegrant-fremde-gruppe-abgelehnt`); die Zeile
|
||||||
|
existiert in aktuellen Läufen nur noch, weil
|
||||||
|
`modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich` sie
|
||||||
|
über die Wartungsrolle (BYPASSRLS) anlegt. Der Meldetext dieser Prüfung ist
|
||||||
|
im Quelltext des Werkzeugs entsprechend richtiggestellt.
|
||||||
|
|
||||||
Die Belegzeile, die diesen Abschnitt trägt, ist
|
Die Belegzeile, die diesen Abschnitt trägt, ist
|
||||||
`modulegrant-ungebunden-null-zeilen`: der IDENTISCHE `SELECT "tenantId" FROM
|
`modulegrant-ungebunden-null-zeilen`: der IDENTISCHE `SELECT "tenantId" FROM
|
||||||
"ModuleGrant"` ohne vorheriges `set_config` liefert **0 Zeilen**, nicht etwa
|
"ModuleGrant"` ohne vorheriges `set_config` liefert **0 Zeilen**, nicht etwa
|
||||||
@@ -1426,7 +1487,7 @@ und verlöre ihr Signal.
|
|||||||
|
|
||||||
### (m4) Was dieser Durchlauf bewusst nicht löst
|
### (m4) Was dieser Durchlauf bewusst nicht löst
|
||||||
|
|
||||||
- **T-JTS-02/T-JTS-03 bleiben aufgezeichnet und ungefixt.** Gemessen in
|
- ~~**T-JTS-02/T-JTS-03 bleiben aufgezeichnet und ungefixt.** Gemessen in
|
||||||
Aufgabe 1 (Prüfungen 6/7): die Regel auf `ModuleGrant` prüft nur die
|
Aufgabe 1 (Prüfungen 6/7): die Regel auf `ModuleGrant` prüft nur die
|
||||||
Mandantenkennung der Zeile selbst, nicht die referenzierte Gruppe; die
|
Mandantenkennung der Zeile selbst, nicht die referenzierte Gruppe; die
|
||||||
Regel auf `GroupMembership` prüft nur die Gruppenseite. Die Bindung fügt
|
Regel auf `GroupMembership` prüft nur die Gruppenseite. Die Bindung fügt
|
||||||
@@ -1434,7 +1495,17 @@ und verlöre ihr Signal.
|
|||||||
schließt die fremde Gruppe gebunden aus), aber erst nach Etappe 4 — die
|
schließt die fremde Gruppe gebunden aus), aber erst nach Etappe 4 — die
|
||||||
Schreibseiten-Gegenprüfung in `module-grants.service.ts`
|
Schreibseiten-Gegenprüfung in `module-grants.service.ts`
|
||||||
(`assertTargetBelongsToTenant`) bleibt deshalb der heutige Schutz und wird
|
(`assertTargetBelongsToTenant`) bleibt deshalb der heutige Schutz und wird
|
||||||
durch diesen Durchlauf NICHT ersetzt.
|
durch diesen Durchlauf NICHT ersetzt.~~
|
||||||
|
**NACHTRAG (260910-jab): ÜBERHOLT — BEIDE Befunde sind geschlossen.**
|
||||||
|
Migration `20260910120000_rls_widen_membership_grant_and_platform_read`
|
||||||
|
lässt die Regel auf `GroupMembership` jetzt beide Seiten prüfen
|
||||||
|
(T-JTS-02) und die Regel auf `ModuleGrant` zusätzlich beide möglichen
|
||||||
|
Ziele (T-JTS-03, beide Zweige des Entweder-oder D-04). Die
|
||||||
|
Schreibseiten-Gegenprüfung in `module-grants.service.ts`
|
||||||
|
(`assertTargetBelongsToTenant`) bleibt TROTZDEM bestehen — sie ist bis
|
||||||
|
zum Scharfschalten (#18, Schalter weiterhin aus) der einzige tatsächlich
|
||||||
|
wirksame Schutz und wird NICHT ersetzt. Siehe den neuen Abschnitt
|
||||||
|
"Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" unten.
|
||||||
- **Die Regel für den Modulkatalog gehört zu Etappe 3.** Gemessen in
|
- **Die Regel für den Modulkatalog gehört zu Etappe 3.** Gemessen in
|
||||||
Aufgabe 1 (Prüfung 8/9, Befund E): `"Module"` trägt heute keinen
|
Aufgabe 1 (Prüfung 8/9, Befund E): `"Module"` trägt heute keinen
|
||||||
Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal.
|
Zeilenschutz, eine Bindung wäre heute wirkungslos, nicht katastrophal.
|
||||||
@@ -1540,6 +1611,122 @@ nichts gefunden" unterscheidet, mit der Vorabprüfung für Etappe 4 und der
|
|||||||
begründeten Verwerfung einer Laufzeitwarnung — siehe (m3) oben und
|
begründeten Verwerfung einer Laufzeitwarnung — siehe (m3) oben und
|
||||||
`.planning/WINDOWS.md`.
|
`.planning/WINDOWS.md`.
|
||||||
|
|
||||||
|
## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19
|
||||||
|
|
||||||
|
Dieser Abschnitt weicht bewusst von der geplanten Reihenfolge ab (260910-jab,
|
||||||
|
auf ausdrückliche Anweisung des Nutzers): die drei Regelfixe, ursprünglich
|
||||||
|
nach Etappe 2 vorgesehen, wurden vorgezogen. Folge: die fünf noch offenen
|
||||||
|
Bereiche der Etappe 2 messen ab jetzt gegen die NEUEN Regeln, mehrere
|
||||||
|
bereits abgeschlossene Bereiche (`groups`, `tenders`, `module-registry` oben)
|
||||||
|
haben gegen die ALTEN gemessen — diese Messungen stehen aufgezeichnet und
|
||||||
|
sind mit Nachträgen an den betroffenen Stellen versehen, nicht umgeschrieben.
|
||||||
|
|
||||||
|
### (r1) Tatsächlich beobachtete Ausgabe und Regelliste der lebenden Datenbank
|
||||||
|
|
||||||
|
Lauf vom 2026-09-10 gegen `tessera-ctl-db-1`, Adresse `172.19.0.2` (nur die
|
||||||
|
Zeilen der drei betroffenen Bereiche sowie der beiden aufsetzenden
|
||||||
|
`module-registry`-Prüfungen; die vollständige Ausgabe umfasst 74
|
||||||
|
Prüfungen):
|
||||||
|
|
||||||
|
```
|
||||||
|
groupmembership-folgt-join-auf-group: bestanden — forTenant(TENANT-A) liefert 1 Mitgliedschaft(en): ["group-a"]
|
||||||
|
groupmembership-schreiben-fremde-gruppe-abgelehnt: bestanden — INSERT mit fremder groupId abgewiesen: ERROR: new row violates row-level security policy for table "GroupMembership"
|
||||||
|
groupmembership-schreiben-fremder-benutzer-abgelehnt: bestanden — INSERT mit A-eigener Gruppe, aber fremder Benutzerkennung (user-b, TENANT-B) abgewiesen: ERROR: new row violates row-level security policy for table "GroupMembership"
|
||||||
|
groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich: bestanden — INSERT mit A-eigener Gruppe, aber fremder Benutzerkennung (user-b, TENANT-B) ist ueber die Wartungsrolle (BYPASSRLS) weiterhin GELUNGEN — der Ausschluss aus der vorigen Pruefung kommt damit nachweislich von der Regel, nicht vom Aufbau
|
||||||
|
modulegrant-gebunden-nur-eigene-zeile: bestanden — forTenant(TENANT-A) liefert 1 Zeile(n): ["TENANT-A"]
|
||||||
|
modulegrant-fremde-gruppe-abgelehnt: bestanden — INSERT mit korrekter eigener tenantId, aber fremder groupId (group-b, TENANT-B) abgewiesen: ERROR: new row violates row-level security policy for table "ModuleGrant"
|
||||||
|
modulegrant-fremder-benutzer-abgelehnt: bestanden — INSERT mit korrekter eigener tenantId, aber fremder userId (user-b, TENANT-B) abgewiesen: ERROR: new row violates row-level security policy for table "ModuleGrant"
|
||||||
|
modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich: bestanden — INSERT mit korrekter eigener tenantId, aber fremder groupId ist ueber die Wartungsrolle (BYPASSRLS) weiterhin GELUNGEN — legt zugleich die Zeile 'grant-foreign-group' an, auf der zwei Pruefungen des Bereichs module-registry aufsetzen (Befund C)
|
||||||
|
tenderrssfeed-plattformzeile-gebunden-sichtbar: bestanden — forTenant(TENANT-A) sieht die Platform-Zeile: true, forTenant(TENANT-B) sieht sie: true
|
||||||
|
tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar: bestanden — forTenant(TENANT-A) liefert ["rss-a","rss-platform"] — eigene Zeile (rss-a) sichtbar: true, fremde Zeile (rss-b, TENANT-B) sichtbar: false
|
||||||
|
tenderrssfeed-ungebunden-nur-die-plattformzeile: bestanden — ungebundener SELECT auf "TenderRssFeedSource" liefert 1 Zeile(n): ["rss-platform"]
|
||||||
|
tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt: bestanden — gebundenes INSERT mit tenantId=NULL abgewiesen (tenant_insert_policy)
|
||||||
|
tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt: bestanden — gebundenes UPDATE auf die plattformweite Zeile traf keine Zeile (tenant_update_policy filtert sie heraus) — url unveraendert
|
||||||
|
tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt: bestanden — gebundenes DELETE auf die plattformweite Zeile traf 0 Zeilen (tenant_delete_policy filtert sie heraus)
|
||||||
|
searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar: bestanden — forTenant(TENANT-A) sieht die mandantenlose Zeile: false, forTenant(TENANT-B) sieht sie: false — bewusst UNVERAENDERT (widerlegte Praemisse)
|
||||||
|
gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus: bestanden — forTenant(TENANT-A) liefert 'grant-foreign-group' ueber denselben Drei-Tabellen-Weg NICHT — die Zeile wurde ueber die Wartungsrolle angelegt
|
||||||
|
gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit: bestanden — dieselbe Abfrage ueber die Verwaltungsrolle (BYPASSRLS) liefert ["grant-a","grant-foreign-group"]
|
||||||
|
Alle 74 Pruefungen bestanden.
|
||||||
|
```
|
||||||
|
|
||||||
|
Regelliste aus `pg_policies` der lebenden Datenbank (Systemkatalog, nicht die
|
||||||
|
Migrationsdatei):
|
||||||
|
|
||||||
|
```
|
||||||
|
GroupMembership#tenant_isolation_policy#ALL#(("groupId" IN (SELECT "Group".id FROM "Group" WHERE ("Group"."tenantId" = current_tenant_id()))) AND ("userId" IN (SELECT "User".id FROM "User" WHERE ("User"."tenantId" = current_tenant_id()))))
|
||||||
|
ModuleGrant#tenant_isolation_policy#ALL#(("tenantId" = current_tenant_id()) AND (("groupId" IS NULL) OR ("groupId" IN (SELECT "Group".id FROM "Group" WHERE ("Group"."tenantId" = current_tenant_id())))) AND (("userId" IS NULL) OR ("userId" IN (SELECT "User".id FROM "User" WHERE ("User"."tenantId" = current_tenant_id())))))
|
||||||
|
SearchProvider#tenant_isolation_policy#ALL#("tenantId" = current_tenant_id())
|
||||||
|
TenderRssFeedSource#tenant_delete_policy#DELETE#("tenantId" = current_tenant_id())
|
||||||
|
TenderRssFeedSource#tenant_insert_policy#INSERT##WITH CHECK ("tenantId" = current_tenant_id())
|
||||||
|
TenderRssFeedSource#tenant_platform_read_policy#SELECT#(("tenantId" = current_tenant_id()) OR ("tenantId" IS NULL))
|
||||||
|
TenderRssFeedSource#tenant_update_policy#UPDATE#USING ("tenantId" = current_tenant_id())#WITH CHECK ("tenantId" = current_tenant_id())
|
||||||
|
```
|
||||||
|
|
||||||
|
### (r2) Signaltabelle — beide Fehlerrichtungen je Regel
|
||||||
|
|
||||||
|
| Regel | Zu streng (Falsch-Negativ, DoS) | Zu locker (Falsch-Positiv, Rechteausweitung) |
|
||||||
|
|---|---|---|
|
||||||
|
| `GroupMembership` | Wäre die neue Regel zu streng, würde eine tatsächlich mandanteneigene Mitgliedschaft unsichtbar — Signal: `groupmembership-folgt-join-auf-group` würde fehlschlagen (liefert heute korrekt 1 Zeile) | `groupmembership-schreiben-fremder-benutzer-abgelehnt` — Signal wäre ein GELUNGENES Einfügen einer fremden Benutzerkennung; heute abgewiesen |
|
||||||
|
| `ModuleGrant` | Eine eigene, korrekt referenzierende Freigabe würde unsichtbar — Signal: `modulegrant-gebunden-nur-eigene-zeile` würde fehlschlagen (heute 1 Zeile) | `modulegrant-fremde-gruppe-abgelehnt`/`modulegrant-fremder-benutzer-abgelehnt` — Signal wäre ein GELUNGENES Einfügen mit fremder Gruppen- bzw. Benutzerkennung; heute beide abgewiesen |
|
||||||
|
| `TenderRssFeedSource` (Lese-/Schreibsplit) | Zu streng bedeutet: die plattformweite Zeile bleibt trotz Bindung unsichtbar — Signal: `tenderrssfeed-plattformzeile-gebunden-sichtbar` würde fehlschlagen (heute sichtbar unter beiden Mandanten) ODER die eigene Zeile verschwindet — Signal: `tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar` würde fehlschlagen | Zu locker bedeutet: ein Mandant kann die plattformweite Zeile ändern/entfernen — Signal: `tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt`/`tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt` würden fehlschlagen (heute beide: 0 betroffene Zeilen) |
|
||||||
|
|
||||||
|
### (r3) Stellen, an denen Leere weiterhin als Abwesenheit gedeutet wird
|
||||||
|
|
||||||
|
Namentliche Liste, wie in den Abschnitten (d)/(g3)/(t3)/(u3)/(m3) oben je
|
||||||
|
Bereich geführt — hier bereichsübergreifend für die drei betroffenen
|
||||||
|
Tabellen, inklusive der NEUEN Stelle aus Befund F:
|
||||||
|
|
||||||
|
- `TenderRssFeedSourceService.listForUser` (**NEU seit 260910-jab, Aufgabe
|
||||||
|
2**) — vor dieser Reparatur lieferte der ungebundene Pfad nach dem
|
||||||
|
Scharfschalten NICHTS (eine schreiende Leere, die auffällt). Nach der
|
||||||
|
Reparatur würde er UNGEBUNDEN nur die plattformweiten Zeilen liefern — eine
|
||||||
|
kurze, glaubhafte Teilantwort, die NICHT auffällt (der Nutzer sieht
|
||||||
|
plattformweite Feeds und hat keinen Anlass zu melden, dass seine eigenen
|
||||||
|
fehlen). Deshalb gebunden — die einzige Stelle, die diese Aufgabe still
|
||||||
|
falsch gemacht hätte, wenn sie nicht mitgebunden worden wäre.
|
||||||
|
- Alle bereits in (t3) genannten fünf lautlosen Stellen in den beiden
|
||||||
|
Tender-Hintergrunddiensten bleiben unverändert lautlos — von dieser
|
||||||
|
Regeländerung nicht betroffen.
|
||||||
|
- `GroupMembership`/`ModuleGrant` selbst tragen keine "Leere als Abwesenheit
|
||||||
|
gedeutet"-Stelle im hier verstandenen Sinn (eine ABGEWIESENE Schreibung ist
|
||||||
|
ein harter Fehler, keine stille Leere) — die relevante Fehlerrichtung ist
|
||||||
|
hier die Rechteausweitung (r2), nicht die stille Leere.
|
||||||
|
|
||||||
|
### (r4) Was dieser Durchlauf bewusst nicht löst
|
||||||
|
|
||||||
|
- **Der Verwaltungsweg für plattformweite Zeilen fehlt.** Unter der
|
||||||
|
Anwendungsrolle lässt sich eine plattformweite `TenderRssFeedSource`-Zeile
|
||||||
|
weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil
|
||||||
|
jede Schreibregel einen Mandanten verlangt. `createPlatform`/`remove`
|
||||||
|
bleiben deshalb bewusst ungebunden. Als eigener offener Ledger-Eintrag
|
||||||
|
festgehalten (WINDOWS #24), damit dieser Rest nicht mit #19 verschwindet —
|
||||||
|
ein Verwaltungsweg (z. B. eine eigene Systemrolle oder ein expliziter
|
||||||
|
Admin-Bypass-Pfad) ist Gegenstand von Etappe 4.
|
||||||
|
- **Die fehlende Benutzerdimension der Ausschreibungsregeln.** Selbst
|
||||||
|
gemessen statt übernommen (`grep -rn "current_setting\|set_config"
|
||||||
|
apps/api/src apps/api/prisma/migrations`, 260910-jab): es existiert
|
||||||
|
GENAU EINE Sitzungsvariable, `app.current_tenant`
|
||||||
|
(`prisma-tenant.extension.ts`, `20260618112133_rls_policies`). Es gibt
|
||||||
|
KEINE zweite Sitzungsvariable für den Benutzer — Tabellen wie
|
||||||
|
`TenderSavedSearch` (siehe `tendersavedsearch-fremder-nutzer-desselben-
|
||||||
|
mandanten-sichtbar` oben) haben deshalb strukturell keine Möglichkeit,
|
||||||
|
eine Benutzerdimension auf Datenbankebene durchzusetzen, ohne eine solche
|
||||||
|
Variable erst einzuführen. Nicht gebaut in diesem Durchlauf — die
|
||||||
|
anwendungsseitige `userId`-Filterung bleibt der einzige Schutz.
|
||||||
|
- **Die plattformweite Eindeutigkeit von Anmeldename und Adresse (WINDOWS
|
||||||
|
#22)** — unverändert, nicht Gegenstand dieses Plans.
|
||||||
|
|
||||||
|
### (r5) Was dieser Durchlauf bewusst nicht anfasst
|
||||||
|
|
||||||
|
- `SearchProvider` — die Regel bleibt wörtlich unverändert (`"tenantId" =
|
||||||
|
current_tenant_id()`), mit gemessener Begründung (widerlegte Prämisse,
|
||||||
|
siehe `docs/mandantentrennung-zugriffsklassifikation.md`).
|
||||||
|
- Die Regeln auf `Group` und `TenantModuleActivation` — beide unverändert,
|
||||||
|
nicht Teil der drei benannten Löcher.
|
||||||
|
- Der Schalter (`DATABASE_URL`, Rolle `tessera`) — bleibt aus. Die drei
|
||||||
|
Regeln sind heute wirkungslos; das Wegwerf-Werkzeug und die Regelliste der
|
||||||
|
lebenden Datenbank sind die einzigen Zeugen dafür, dass sie greifen.
|
||||||
|
|
||||||
## Verweis
|
## Verweis
|
||||||
|
|
||||||
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
||||||
|
|||||||
@@ -48,20 +48,40 @@ zweiten `forTenant()`-Aufruf je Service-Methode) oder ob der Weg ersatzlos
|
|||||||
entfällt. Dieser Plan entscheidet das nicht, hält den Befund nur fest.
|
entfällt. Dieser Plan entscheidet das nicht, hält den Befund nur fest.
|
||||||
|
|
||||||
**WINDOWS #19 — nullbares `tenantId` bei `SearchProvider` und
|
**WINDOWS #19 — nullbares `tenantId` bei `SearchProvider` und
|
||||||
`TenderRssFeedSource`.** Beide Modelle tragen ein nullbares `tenantId`
|
`TenderRssFeedSource` — GESCHLOSSEN (260910-jab, Aufgabe 1/2).** Beide Modelle
|
||||||
(`SearchProvider` für admin-gepflegte Vorgabe-Suchmaschinen, wobei laut
|
tragen ein nullbares `tenantId` (`SearchProvider` für admin-gepflegte
|
||||||
05-02-Entscheidung die tatsächlichen Vorgaben als Konstanten und nicht als
|
Vorgabe-Suchmaschinen; `TenderRssFeedSource` für plattformweite RSS-Quellen
|
||||||
DB-Zeilen mit `tenantId = NULL` geführt werden — die Spalte ist nullbar,
|
wie den geseedeten `service.bund.de`-Feed, D-06). Die ausgelieferte Policy
|
||||||
ob es heute tatsächlich `NULL`-Zeilen gibt, ist damit eine offene Frage für
|
`"tenantId" = current_tenant_id()` verglich `NULL` nie gleich — nach dem
|
||||||
Etappe 3, nicht eine hier beantwortete; `TenderRssFeedSource` für
|
Scharfschalten wären plattformweite Zeilen für JEDEN Mandanten unsichtbar
|
||||||
plattformweite RSS-Quellen wie den geseedeten `service.bund.de`-Feed, D-06).
|
gewesen, nicht nur für fremde.
|
||||||
Die aktuelle Policy `"tenantId" = current_tenant_id()` vergleicht `NULL`
|
|
||||||
nie gleich — nach dem Scharfschalten wären plattformweite Zeilen für JEDEN
|
Geschlossen durch Migration
|
||||||
Mandanten unsichtbar, nicht nur für fremde. Das ist heute ohne Wirkung
|
`20260910120000_rls_widen_membership_grant_and_platform_read`, lokal
|
||||||
(Schalter aus, #18), muss aber in Etappe 3 zusammen mit den restlichen
|
angewandt und gegen den Systemkatalog der lebenden Datenbank gemessen:
|
||||||
Fundstellen gelöst werden: die Policy braucht für den Lesezugriff
|
`TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln —
|
||||||
`tenantId IS NULL OR tenantId = current_tenant_id()`, während Schreibzugriffe
|
`tenant_platform_read_policy` (SELECT) schließt Zeilen ohne Mandant
|
||||||
weiterhin einen Mandanten verlangen.
|
ausdrücklich ein, `tenant_insert_policy`/`tenant_update_policy`/
|
||||||
|
`tenant_delete_policy` verlangen weiterhin ausnahmslos einen Mandanten. Die
|
||||||
|
Trennung nach Befehl ist notwendig, weil ein einzelner `USING`-Ausdruck auch
|
||||||
|
bestimmt, welche Zeilen `UPDATE`/`DELETE` erreichen — eine Leseregel, die
|
||||||
|
plattformweite Zeilen einschließt, hätte ohne Trennung jedem Mandanten auch
|
||||||
|
das Ändern/Entfernen dieser Zeilen erlaubt.
|
||||||
|
|
||||||
|
Die Hälfte zur Suchanbietertabelle (`SearchProvider`) schließt NICHT als
|
||||||
|
gelöstes Problem, sondern als **widerlegte Prämisse**: lokal gemessen gibt es
|
||||||
|
keinen Codeweg, der eine mandantenlose `SearchProvider`-Zeile erzeugt — der
|
||||||
|
einzige Schreibweg (`dashboard.service.ts`) verlangt die Mandantenkennung als
|
||||||
|
Pflichtparameter, und die Vorgabe-Suchmaschinen kommen laut 05-02-Entscheidung
|
||||||
|
aus Konstanten, nicht aus der Datenbank. Die Regel bleibt deshalb bewusst
|
||||||
|
unverändert streng; eine Lockerung wäre hier die falsche Richtung, weil sie
|
||||||
|
eine künftige mandantenlose Zeile jedem Mandanten zeigen würde.
|
||||||
|
|
||||||
|
Was diese Reparatur NICHT löst: unter der Anwendungsrolle lässt sich eine
|
||||||
|
plattformweite `TenderRssFeedSource`-Zeile weder anlegen noch entfernen, in
|
||||||
|
der alten wie in der neuen Regel, weil jede Schreibregel einen Mandanten
|
||||||
|
verlangt. Als eigener offener Ledger-Eintrag festgehalten (WINDOWS #24),
|
||||||
|
damit dieser Rest nicht mit #19 verschwindet.
|
||||||
|
|
||||||
## Übersicht je Bereich (Zeilentreffer je Bereich, ungebunden vs. gebunden)
|
## Übersicht je Bereich (Zeilentreffer je Bereich, ungebunden vs. gebunden)
|
||||||
|
|
||||||
@@ -96,7 +116,7 @@ autoritative Quelle.
|
|||||||
|
|
||||||
| Bereich | Ungebunden | Gebunden | Hinweis |
|
| Bereich | Ungebunden | Gebunden | Hinweis |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| tenders | 36 | 26 | **war 62/0** — Aufgabe 2/3 (260909-laa) haben die fünf Nutzer-CRUD-Dienste (vier vollständig, `tender-rss-feed.service.ts` teilweise mit im Code begründeter WINDOWS-#19-Grenze) und die Je-Treffer-Hälften der beiden Hintergrunddienste (`tender-digest.scheduler.ts`, `tender-matching.service.ts`) auf `forTenant()` umgestellt. Die 36 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die drei bewusst ungebundenen RSS-Pfade plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
|
| 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) |
|
| 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) |
|
| 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 |
|
| 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 |
|
||||||
@@ -108,7 +128,7 @@ autoritative Quelle.
|
|||||||
| tenant | 8 | 0 | unverändert |
|
| tenant | 8 | 0 | unverändert |
|
||||||
| favorites | 7 | 0 | unverändert |
|
| favorites | 7 | 0 | unverändert |
|
||||||
| settings | 4 | 0 | unverändert |
|
| settings | 4 | 0 | unverändert |
|
||||||
| **Summe** | **108** | **134** | Ungebunden: war 118 nach 260910-das, Delta = die 10 in Aufgabe 2/3 (260910-exd) gesunkenen `module-registry`-Rohtreffer (17→7). Gebunden: war 124, jetzt zusätzlich 10 in `module-registry`. 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 |
|
| **Summe** | **107** | **135** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), jetzt 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), jetzt 135 nach 260910-jab (zusätzlich 1 in `tenders`). 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, 63 Paare)
|
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 63 Paare)
|
||||||
|
|
||||||
@@ -276,7 +296,7 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
|||||||
| apps/api/src/calendar/calendar.service.ts | calendarSource | muss-mandantengebunden | ungebunden | Kalenderquellen eines Nutzers, `tenantId`-Spalte vorhanden. |
|
| apps/api/src/calendar/calendar.service.ts | calendarSource | muss-mandantengebunden | ungebunden | Kalenderquellen eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||||||
| apps/api/src/dashboard/dashboard.service.ts | dashboardLayout | muss-mandantengebunden | ungebunden | Widget-Anordnung eines Nutzers, `tenantId`-Spalte vorhanden. |
|
| apps/api/src/dashboard/dashboard.service.ts | dashboardLayout | muss-mandantengebunden | ungebunden | Widget-Anordnung eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||||||
| apps/api/src/dashboard/dashboard.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId` (Migration 20260909140000, Gruppe b). |
|
| apps/api/src/dashboard/dashboard.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId` (Migration 20260909140000, Gruppe b). |
|
||||||
| apps/api/src/dashboard/dashboard.service.ts | searchProvider | muss-mandantengebunden | ungebunden | `tenantId` nullbar (WINDOWS #19) — heutige, tatsächlich gespeicherte Zeilen sind nutzerangelegt und tragen einen Mandanten; Vorgabe-Anbieter kommen laut 05-02 aus Konstanten, nicht aus der DB. |
|
| apps/api/src/dashboard/dashboard.service.ts | searchProvider | muss-mandantengebunden | ungebunden | `tenantId` nullbar. WINDOWS #19 geschlossen (260910-jab) als **widerlegte Prämisse** für dieses Modell: lokal gemessen null Zeilen mit `tenantId = NULL` insgesamt, dieser Schreibweg verlangt die Mandantenkennung als Pflichtparameter; Vorgabe-Anbieter kommen laut 05-02 aus Konstanten, nicht aus der DB. Die Regel auf `SearchProvider` bleibt deshalb unverändert streng. |
|
||||||
| apps/api/src/dashboard/dashboard.service.ts | widgetInstance | muss-mandantengebunden | ungebunden | Platzierte Dashboard-Widgets eines Nutzers, `tenantId`-Spalte vorhanden. |
|
| apps/api/src/dashboard/dashboard.service.ts | widgetInstance | muss-mandantengebunden | ungebunden | Platzierte Dashboard-Widgets eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||||||
| apps/api/src/dkv/dkv.service.ts | dkvInvoiceHistory | muss-mandantengebunden | gebunden | DKV-Rechnungshistorie je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-mir (Aufgabe 3) laufen beide Historien-Schreibzugriffe der Verarbeitungsstrecke, beide parallelen Lesezugriffe von `getHistory` und der neue Riegel vor dem Ausfuhrdatei-Download vollstaendig ueber `forTenant()`. |
|
| apps/api/src/dkv/dkv.service.ts | dkvInvoiceHistory | muss-mandantengebunden | gebunden | DKV-Rechnungshistorie je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-mir (Aufgabe 3) laufen beide Historien-Schreibzugriffe der Verarbeitungsstrecke, beide parallelen Lesezugriffe von `getHistory` und der neue Riegel vor dem Ausfuhrdatei-Download vollstaendig ueber `forTenant()`. |
|
||||||
| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | gemischt | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 2) laufen `loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection` und der Konfigurations-Lesezugriff der Verarbeitungsstrecke ueber `forTenant()`. Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad `loadAnyActiveConfigForScheduler()` (WINDOWS #21) — keine uebersehene Fundstelle. |
|
| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | gemischt | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. Seit 260909-mir (Aufgabe 2) laufen `loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection` und der Konfigurations-Lesezugriff der Verarbeitungsstrecke ueber `forTenant()`. Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad `loadAnyActiveConfigForScheduler()` (WINDOWS #21) — keine uebersehene Fundstelle. |
|
||||||
@@ -286,10 +306,10 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
|||||||
| apps/api/src/groups/groups.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group` (Migration 20260618112133-Nachfolger) — braucht trotzdem `forTenant()`, damit der Join-Kontext gesetzt ist. Seit 260909-jts gebunden, einschliesslich der drei Zugriffe innerhalb des Standardgruppen-Aufbaus (`ensureDefaultGroup`), die zuvor ueber den Transaktionsparameter liefen und fuer keine Pruefung dieses Projekts sichtbar waren (Befund B). |
|
| apps/api/src/groups/groups.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group` (Migration 20260618112133-Nachfolger) — braucht trotzdem `forTenant()`, damit der Join-Kontext gesetzt ist. Seit 260909-jts gebunden, einschliesslich der drei Zugriffe innerhalb des Standardgruppen-Aufbaus (`ensureDefaultGroup`), die zuvor ueber den Transaktionsparameter liefen und fuer keine Pruefung dieses Projekts sichtbar waren (Befund B). |
|
||||||
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B). |
|
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B). |
|
||||||
| apps/api/src/groups/groups.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von `ensureDefaultGroup` ueber den Rueckgabeparameter (`tx.tenantModuleActivation.findMany`) — weder `this.prisma.<Modell>` noch `<gebundener Client>.<Modell>` sahen das, weil `tx` weder `this.prisma` noch aus einer `forTenant(`-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber `withTenantTransaction()`). |
|
| apps/api/src/groups/groups.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von `ensureDefaultGroup` ueber den Rueckgabeparameter (`tx.tenantModuleActivation.findMany`) — weder `this.prisma.<Modell>` noch `<gebundener Client>.<Modell>` sahen das, weil `tx` weder `this.prisma` noch aus einer `forTenant(`-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber `withTenantTransaction()`). |
|
||||||
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; `addUserToDefaultGroup` prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02) — die Regel auf `GroupMembership` prueft nachweislich nur die Gruppenseite. |
|
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; `addUserToDefaultGroup` prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02). Bis 20260910120000_rls_widen_membership_grant_and_platform_read pruefte die Regel auf `GroupMembership` nachweislich nur die Gruppenseite — seit 260910-jab prueft sie beide Seiten, die Anwendungspruefung bleibt trotzdem bestehen (Schalter weiterhin aus, #18). |
|
||||||
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | gebunden | Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — `assertTargetBelongsToTenant` erzeugt seinen eigenen Kontext, `getMatrix` teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen. |
|
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | gebunden | Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — `assertTargetBelongsToTenant` erzeugt seinen eigenen Kontext, `getMatrix` teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen. |
|
||||||
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. Seit 260909-jts gebunden; der `where`-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1). |
|
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. Seit 260909-jts gebunden; der `where`-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1). |
|
||||||
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (`assertTargetBelongsToTenant`) bleibt zusätzlich bestehen, weil die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile prüft, nicht die referenzierte Gruppe (Befund F, T-JTS-03, Aufgabe 1). |
|
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (`assertTargetBelongsToTenant`) bleibt zusätzlich bestehen. Bis 20260910120000_rls_widen_membership_grant_and_platform_read prüfte die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe/den referenzierten Benutzer (Befund F, T-JTS-03, Aufgabe 1) — seit 260910-jab prüft sie beide Ziele zusätzlich, die Anwendungsprüfung bleibt trotzdem der erste Schutz (Schalter weiterhin aus, #18). |
|
||||||
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden. |
|
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden. |
|
||||||
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | gebunden | Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen. |
|
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | gebunden | Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen. |
|
||||||
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
||||||
@@ -322,7 +342,7 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
|||||||
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert). |
|
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert). |
|
||||||
| apps/api/src/tenders/tender-matching.service.ts | user | beides | gebunden | E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten des jeweiligen Profils. |
|
| apps/api/src/tenders/tender-matching.service.ts | user | beides | gebunden | E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten des jeweiligen Profils. |
|
||||||
| apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getForUser`/`setForUser` vollständig über `forTenant()`; `setForUser` übersetzt eine P2002-Verletzung (Befund F) in eine verständliche deutsche Meldung. |
|
| apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getForUser`/`setForUser` vollständig über `forTenant()`; `setForUser` übersetzt eine P2002-Verletzung (Befund F) in eine verständliche deutsche Meldung. |
|
||||||
| apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | beides | gemischt | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in `adapters/rss.adapter.ts`). Seit 260909-laa (Aufgabe 2) bindet `createForUser` (Zähler + Anlage, beide ausschließlich auf persönlichen Zeilen mit gesetztem Mandanten) über `forTenant()`; `listForUser`/`createPlatform`/`remove` bleiben bewusst ungebunden, weil sie (auch) die nullbare, plattformweite Zeile berühren (WINDOWS #19, Aufgabe 1 gemessen: eine gebundene Zeile wäre unter jedem Mandanten unsichtbar bzw. ein gebundenes Einfügen ohne Mandant würde abgewiesen). Korrektur der Klasse von `muss-mandantengebunden` auf `beides`, keine Verhaltensänderung — derselbe Präzedenzfall wie `ldapConfig` in 260909-ipc. |
|
| apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | beides | gemischt | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in `adapters/rss.adapter.ts`). Seit 260909-laa (Aufgabe 2) bindet `createForUser` (Zähler + Anlage, beide ausschließlich auf persönlichen Zeilen mit gesetztem Mandanten) über `forTenant()`. Seit 260910-jab (Aufgabe 2, WINDOWS #19 geschlossen) bindet zusätzlich `listForUser` — die neue Leseregel (`tenant_platform_read_policy`, 20260910120000_rls_widen_membership_grant_and_platform_read) schließt die plattformweite Zeile ausdrücklich ein, ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert (Befund F). `createPlatform`/`remove` bleiben bewusst ungebunden — beide Pfade lassen sich unter der Anwendungsrolle grundsätzlich nicht anlegen/entfernen, weil jede Schreibregel einen Mandanten verlangt (WINDOWS #24, eigener offener Punkt). Klasse `beides` bleibt korrekt: zwei gebundene, zwei bewusst ungebundene Zugriffe in derselben Datei. |
|
||||||
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `list`/`create`/`update`/`remove` vollständig über `forTenant()`. |
|
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `list`/`create`/`update`/`remove` vollständig über `forTenant()`. |
|
||||||
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
|
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
|
||||||
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen `setTriage`/`listForUser`/`favoriteIds` vollständig über `forTenant()`. |
|
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen `setTriage`/`listForUser`/`favoriteIds` vollständig über `forTenant()`. |
|
||||||
@@ -348,9 +368,13 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
|||||||
`forTenant()`- bzw. `withTenantTransaction()`-Aufruf, gebundene Clients
|
`forTenant()`- bzw. `withTenantTransaction()`-Aufruf, gebundene Clients
|
||||||
werden nicht zwischen Methoden weitergereicht. Die Frage bleibt für alle
|
werden nicht zwischen Methoden weitergereicht. Die Frage bleibt für alle
|
||||||
übrigen Bereiche der Etappe 2 offen.
|
übrigen Bereiche der Etappe 2 offen.
|
||||||
- Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
|
- ~~Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
|
||||||
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
|
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
|
||||||
muss.
|
muss.~~ Aufgelöst (260910-jab): `TenderRssFeedSource` bekommt vier nach
|
||||||
|
Befehl getrennte Regeln, `SearchProvider` bleibt unverändert streng
|
||||||
|
(widerlegte Prämisse). Siehe Abschnitt "Zwei belegte Befunde" oben. Was
|
||||||
|
weiterhin offen ist: der Verwaltungsweg für plattformweite Zeilen unter
|
||||||
|
der Anwendungsrolle (WINDOWS #24).
|
||||||
- Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die
|
- Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die
|
||||||
Klassen-Verteilung oben der Arbeitsvorrat, siehe `<next_stages>` im
|
Klassen-Verteilung oben der Arbeitsvorrat, siehe `<next_stages>` im
|
||||||
Plan `260909-eor-PLAN.md`.
|
Plan `260909-eor-PLAN.md`.
|
||||||
|
|||||||
Reference in New Issue
Block a user