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:
2026-09-10 14:44:34 +02:00
parent 6b237351e9
commit 03fb3bf9c7
4 changed files with 263 additions and 34 deletions
@@ -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"]
```
**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`:
der IDENTISCHE `SELECT "tenantId" FROM "Group"` ohne vorheriges `set_config`
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
der primäre Schutz gegen diese beiden Formen der Rechteausweitung und
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
@@ -411,6 +431,17 @@ tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit: 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
`tendersavedsearch-ungebunden-null-zeilen`: der IDENTISCHE
`SELECT "tenantId" FROM "TenderSavedSearch"` ohne vorheriges `set_config`
@@ -441,6 +472,12 @@ Bereich brauchte:
Kehrseite: ein gebundenes `INSERT` mit `tenantId = NULL` wird von der
ausgelieferten Policy abgewiesen — `createPlatform` würde in genau diese
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:
`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 |
| `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 |
| `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) |
### (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 =
current_tenant_id()`-Lesevariante) gehört zu Etappe 3 und wird hier nicht
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
bindet nur die Je-Treffer-Hälften von `tender-digest.scheduler.ts` und
`tender-matching.service.ts`; die Kandidatenabfrage
@@ -1293,6 +1342,18 @@ freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung: bestanden — a
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
`modulegrant-ungebunden-null-zeilen`: der IDENTISCHE `SELECT "tenantId" FROM
"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
- **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
Mandantenkennung der Zeile selbst, nicht die referenzierte Gruppe; die
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
Schreibseiten-Gegenprüfung in `module-grants.service.ts`
(`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
Aufgabe 1 (Prüfung 8/9, Befund E): `"Module"` trägt heute keinen
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
`.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
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang