Schliesst T-JTS-02, T-JTS-03 und WINDOWS #19 in einer handgeschriebenen Migration. Drei zur Planungszeit gemessene Funde praegen den Zuschnitt: eine DRITTE loch-behauptende Pruefung im Wegwerf-Werkzeug (nicht zwei), eine Zeile, auf der zwei spaetere Pruefungen aufsetzen und die nach der Reparatur nicht mehr entsteht, und genau ein Anwendungspfad, den die Reparatur still falsch machen wuerde. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
63 KiB
phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
| phase | plan | type | wave | depends_on | autonomous | requirements | files_modified | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260910-jab | 01 | execute | 1 | true |
|
|
|
|
Zweck: alle drei sind heute wirkungslos, weil die Anwendung als Rolle mit Umgehungsrecht verbindet (#18). Genau das macht sie jetzt gefahrlos aenderbar — und bedeutet zugleich, dass die laufende Anwendung die Reparatur nicht bestaetigen kann. Das Wegwerf-Werkzeug und die Regelliste der lebenden Datenbank sind die einzigen Zeugen, die es gibt.
Dieser Durchlauf weicht bewusst von der geplanten Reihenfolge ab, auf ausdrueckliche Anweisung des Nutzers. Die Folge ist einzukalkulieren, nicht zu verschweigen: die fuenf noch offenen Bereiche der Etappe 2 messen ab jetzt gegen die NEUEN Regeln, und mehrere bereits abgeschlossene Bereiche haben gegen die alten gemessen. Diese Messungen stehen aufgezeichnet. Sie muessen am Ende stimmen.
Ergebnis: eine handgeschriebene, lokal angewandte Migration; ein Messwerkzeug, dessen drei loch-behauptende Pruefungen umgekehrt statt entfernt sind; genau ein mitgebundener Anwendungspfad, weil die Reparatur ihn sonst still falsch machen wuerde; und ein Aktenstand, der nirgends mehr ein Loch beschreibt, das es nicht mehr gibt.
Der Schalter bleibt AUS. DATABASE_URL zeigt weiterhin auf die Rolle
tessera. Das Scharfschalten ist Etappe 4 und findet hier NICHT statt.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
<planning_time_findings>
Alle Zahlen und Fundstellen unten sind zur Planungszeit am 2026-09-10 gegen HEAD
4843058 gemessen, mit der jeweils genannten Anweisung. Sie leiten die
Untersuchung, sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder
Aenderung erneut gelesen, und jede Zeilenangabe erneut aufgesucht statt
abgeschrieben.
Befund A — die drei Regeln, im ausgelieferten Text nachgelesen.
20260804130918_groups_rls_policies/migration.sql fuehrt drei Regeln unter dem
Namen tenant_isolation_policy: Group vergleicht direkt gegen
current_tenant_id(); GroupMembership prueft ausschliesslich, ob groupId in
den Gruppen des Mandanten liegt; ModuleGrant vergleicht ausschliesslich die
eigene tenantId. 20260909140000_rls_remaining_tenant_tables/migration.sql
legt sechzehn weitere Regeln an, darunter SearchProvider und
TenderRssFeedSource, beide in der einfachen Gleichheitsform. Ihr Kopf sagt
ausdruecklich, dass keine getrennte Schreibbedingung angelegt wurde, weil
PostgreSQL dann denselben Ausdruck auch fuer neu geschriebene Zeilen verwendet —
genau diese Vereinfachung ist es, die fuer die nullbaren Spalten nicht traegt.
Befund B — es sind DREI loch-behauptende Pruefungen im Werkzeug, nicht zwei.
Die Aufgabenstellung nennt zwei und sagt ausdruecklich, dass der Fund einer
Planungszeit-Suche keine Vollstaendigkeitsgarantie ist. Die Suche ist gemacht
worden. Die dritte ist tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar
in runTendersAreaChecks (rls-scratch-check.mjs, im Bereich der Zeilen
1041-1061 zu suchen, nicht abzuschreiben): sie besteht, WEIL die plattformweite
Zeile unter beiden Mandantenkontexten fehlt, und haelt in ihrem Meldetext
zusaetzlich die Handlungsanweisung fest, drei RSS-Pfade deshalb nicht zu binden.
Sie faellt mit der Reparatur und ist wie die beiden anderen umzukehren.
Gemessen mit grep -n "GELUNGEN\|nicht-verhindert\|erlaubt\|unsichtbar" ueber
die Werkzeugdatei; ausserdem wurde die Datei nach weiteren Pruefungen
durchgesehen, deren bestandenes Ergebnis das ALTE Verhalten ist — es gibt keine
vierte.
Befund C — eine der beiden benannten Pruefungen LIEFERT eine Zeile, auf der
zwei spaetere Pruefungen aufsetzen. Das ist die gefaehrlichste Einzelheit
dieses Durchlaufs. Die Pruefung, die T-JTS-03 festhaelt, fuegt dabei die Zeile
grant-foreign-group ein (Mandant TENANT-A, Gruppe group-b). Zwei Pruefungen
des Bereichs module-registry bauen darauf auf: eine weist nach, dass der
gebundene Drei-Tabellen-Weg diese Zeile ausschliesst, die andere weist ueber die
Wartungsrolle nach, dass der Ausschluss von der Bindung kommt und nicht vom
Aufbau. Nach der Reparatur wird das Einfuegen abgewiesen, die Zeile entsteht
nicht mehr — die erste Pruefung bestuende dann aus dem falschen Grund (es gibt
nichts auszuschliessen) und die zweite SCHEITERT. Gemessen mit
grep -n "grant-foreign-group" apps/api/scripts/rls-scratch-check.mjs: fuenf
Fundstellen, eine Einfuegestelle und vier Verwendungen. Die Gegenprobe fuer die
zweite loch-behauptende Zeile faellt anders aus: membership-foreign-user hat
ausser ihrer Einfuegestelle keine Verwendung.
Befund D — die Extraktion liest die Regel aus der Migrationsdatei, und sie
kennt nur einen Namen. extractPolicySql() sucht woertlich nach
CREATE POLICY tenant_isolation_policy ON "<Tabelle>" und liefert GENAU EINEN
Treffer. Daraus folgen zwei Dinge, die sonst still schiefgehen: erstens muss die
Extraktion fuer die abgeloesten Regeln auf die NEUE Migrationsdatei zeigen,
sonst misst das Werkzeug weiter die abgeloeste Regel und die umgekehrten
Pruefungen scheitern aus einem verwirrenden Grund; zweitens braucht die Tabelle
mit vier befehlsgetrennten Regeln eine Extraktion, die mehr als einen Treffer
liefern kann.
Befund E — die Praemisse von #19 stimmt nur zur Haelfte. Fuer
TenderRssFeedSource gibt es plattformweite Zeilen, sie sind gewollt (D-06) und
sie sind da: in der lokalen Datenbank zwei Zeilen insgesamt, davon eine mit
leerem Benutzer UND leerem Mandanten. Fuer SearchProvider gilt das nicht.
Gemessen: dashboard.service.ts ist der einzige Ort, der die Tabelle beruehrt
(vier Zugriffe), und der einzige Schreibweg dorthin nimmt die Mandantenkennung
als Pflichtparameter entgegen und setzt sie. Die Vorgabe-Suchmaschinen kommen
aus der Konstanten DEFAULT_SEARCH_PROVIDERS und werden erst nach dem Lesen
davorgehaengt — sie sind keine Datenbankzeilen (Entscheidung 05-02). Lokal
gemessen: null Zeilen insgesamt, null mit leerer Mandantenkennung. Sowohl der
Kopfkommentar der ausgelieferten Migration als auch die Klassifikation sagen das
bereits; allein der Ledger-Eintrag #19 behauptet "von der Administration
gepflegte Suchanbieter". Folge fuer diesen Plan: SearchProvider behaelt seine
strenge Regel, und die Schliessung von #19 schliesst diese Haelfte als
WIDERLEGTE PRAEMISSE, nicht als geloestes Problem. Eine Lockerung waere hier die
falsche Richtung: sie wuerde eine kuenftige mandantenlose Zeile jedem Mandanten
zeigen.
Befund F — die Behauptung "kein Anwendungscode muss sich aendern" trifft fuer
GENAU EINEN Pfad nicht zu. TenderRssFeedSourceService.listForUser ist heute
bewusst ungebunden, mit einem Kopfkommentar, der als Begruendung die alte Regel
nennt. Nach der Reparatur kehrt sich die Lage um. Heute liefert dieser Pfad
ungebunden nach dem Scharfschalten NICHTS (die Gleichheitsbedingung vergleicht
den leeren Kontext mit nichts). Danach liefert er die plattformweiten Zeilen —
und nur die. Aus einer leeren Liste, die schreit, wird eine kurze Liste, die
luegt: der Nutzer saehe die plattformweiten Feeds und haette keinen Anlass zu
melden, dass seine eigenen fehlen. Gebunden liefert derselbe Pfad genau das
Richtige, eigene plus plattformweite. Die Reparatur ERZEUGT hier also die stille
Falschantwort, gegen die dieses Vorhaben laeuft, wenn dieser Pfad nicht
mitgebunden wird. Der aufrufende Endpunkt hat den Mandanten bereits zur Hand (er
reicht ihn eine Methode weiter beim Anlegen eines eigenen Feeds durch), die
Aenderung ist deshalb klein und ohne neue Aufloesung.
Befund G — zwei Pfade bleiben unter der Anwendungsrolle unmoeglich, in der alten wie in der neuen Regel. Das Anlegen einer plattformweiten Zeile setzt die Mandantenkennung leer; die Schreibbedingung verlangt einen Mandanten — vor wie nach der Reparatur abgewiesen. Das Entfernen einer plattformweiten Zeile laeuft ungebunden und trifft nach dem Scharfschalten nichts. Beides ist keine Folge dieses Plans und wird von ihm auch nicht geloest: die vom Ledger vorgegebene Semantik lautet ausdruecklich, dass Schreibzugriffe weiterhin einen Mandanten verlangen. Es ist damit ein Verwaltungsweg, der in Etappe 4 gebaut werden muss, und es darf nicht mit #19 zusammen verschwinden.
Befund H — der Bestand ist sauber, lokal gemessen. Ueber die lokale Datenbank gezaehlt: keine Mitgliedschaft, deren Gruppe und Benutzer zu verschiedenen Mandanten gehoeren; keine Freigabe, deren Gruppe oder Benutzer zu einem anderen Mandanten gehoert als die Zeile selbst; drei Mitgliedschaften, vier Freigaben, ein Mandant. Die schaerferen Regeln machen also lokal keine vorhandene Zeile unsichtbar. Fuer das Testsystem ist das NICHT gemessen und darf nicht angenommen werden — dieselbe Zaehlung gehoert als Vorabpruefung in Etappe 4, denn eine Zeile, die die neue Regel nicht mehr sieht, waere danach weder sichtbar noch loeschbar.
Befund I — zwei Buchhaltungszeilen bewegen sich. Wird listForUser
gebunden, sinkt die ungebundene Rohtrefferzahl des Bereichs tenders um eins
und die gebundene steigt um eins; die Uebersichtszeile und die Summenzeile der
Klassifikation nennen beide Zahlen. Gemessen mit den beiden Anweisungen, die die
Klassifikation selbst als maszgeblich fuehrt: tenders steht heute auf 36
ungebunden und 26 gebunden, die Summenzeile auf 108 und 134. Diese Zahlen stehen
hier als ERWARTUNG, nicht als Vorgabewert — die Pruefung leitet sie zur Laufzeit
erneut aus dem Quelltext ab.
Befund J — welche Aufzeichnungen die Reparatur unwahr macht. Vollstaendig
gesucht mit grep -rn "T-JTS-02\|T-JTS-03" und grep -rn "#19" ueber Quelltext
und Dokumente. Im Quelltext vier Stellen: der Kopfkommentar zur
Zugriffsaufloesung in module-access.service.ts, der Kommentar an der
Benutzerpruefung in groups.service.ts, der Kommentar an der
Mandanten-Gegenpruefung in module-grants.service.ts und die
Beschreibungszeile fuer GroupMembership in der Ausnahmeliste von
rls-coverage.spec.ts. Dazu die drei Kopfkommentare in
tender-rss-feed.service.ts. In der Klassifikation: der #19-Block, die
Bestandsaufnahme-Zeilen fuer searchProvider, groups.service.ts/user,
module-grants.service.ts/moduleGrant und
tender-rss-feed.service.ts/tenderRssFeedSource, die Uebersichtszeile
tenders, die Summenzeile und der Punkt in "Was diese Etappe NICHT
entscheidet", der die #19-Regel ausdruecklich als offen fuehrt. In der
Kritikschrift: der Punkt in (g4), der beide Regeln als einseitig beschreibt;
der Punkt in (t4), der #19 als nicht geloest fuehrt; die Zeile der
Signaltabelle des Bereichs tenders zu den drei RSS-Pfaden; die beiden
aufgezeichneten Werkzeugausgaben, die die alten Meldetexte woertlich zitieren;
und der Punkt im Abschnitt module-registry, der beide Befunde als offen
fuehrt. Dazu eine Stelle in der Betriebsanleitung zur Datenbankrolle.
Befund K — bei getrennter Lese- und Schreibbedingung entscheidet der Befehl, nicht der Ausdruck. Ein einziger permissiver Ausdruck, der die plattformweiten Zeilen einschliesst, gilt in PostgreSQL auch fuer UPDATE und DELETE — ein Mandant duerfte eine plattformweite Zeile dann aendern und entfernen. Deshalb wird die Regel dieser Tabelle nach BEFEHL getrennt: eine Leseregel, die die plattformweiten Zeilen einschliesst, und je eine Regel fuer Einfuegen, Aendern und Entfernen, die einen Mandanten verlangen. Vier ausdrueckliche Regeln statt einer mit stillschweigender Wirkung; beide Fehlerrichtungen werden gemessen, nicht erschlossen.
Gemessene Ausgangslage (2026-09-10, HEAD 4843058): Arbeitsbaum sauber;
Datenbankcontainer tessera-ctl-db-1 unter 172.19.0.2 erreichbar (Adresse bei
JEDEM Lauf neu ermitteln, nie abschreiben), Zugang tessera:tessera_dev,
Datenbank tessera. Die Aufgabenstellung nennt als Baseline 833 Tests in 56
Dateien, eine saubere Typpruefung und 66 bestandene Pruefungen des
Wegwerf-Werkzeugs; diese drei Zahlen sind zu Beginn von Aufgabe 1 SELBST zu
messen und im Bericht mit dem gemessenen Wert zu nennen, nicht zu uebernehmen —
eine Zahl aus zweiter Hand ist in diesem Vorhaben schon viermal geschrumpft.
</planning_time_findings>
Aufgabe 1: Die drei Regeln schreiben, lokal anwenden und an der echten Datenbank messen Der Container `tessera-ctl-db-1` laeuft und ist ueber seine zur Laufzeit ermittelte Container-Adresse mit `tessera:tessera_dev` erreichbar; der Arbeitsbaum ist sauber. apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql, apps/api/scripts/rls-scratch-check.mjs, apps/api/src/groups/migration-sql.spec.ts Diese Aufgabe ist der duenne, durchgehende Faden: sie beruehrt die Migrationsdatei, die lebende Datenbank, das Messwerkzeug und den Textabgleich und beweist damit von Ende zu Ende, dass die Regelaenderung traegt, bevor ein einziger Anwendungspfad angefasst wird. Sie aendert keinen Anwendungscode.Die neuen und geaenderten Pruefungen des Wegwerf-Werkzeugs, jede mit dieser Kennung und jede mit dem tatsaechlich beobachteten Wert im Meldetext:
groupmembership-schreiben-fremder-benutzer-abgelehnt— die Umkehr der ersten loch-behauptenden Pruefung. Gemessen mit einem Benutzer, den es im anderen Mandanten TATSAECHLICH gibt (user-b), nicht mit einer erfundenen Kennung: nur so misst sie den Fall, den T-JTS-02 benannt hat, statt eines Fremdschluessel-Nichts. Der Meldetext nennt den alten Pruefungsnamen und den Befund, damit der Nachweis, dass das Loch existierte, erhalten bleibt.groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich— die Gegenmessung: dasselbe Einfuegen ueber die Verwaltungsrolle mit Umgehungsrecht GELINGT. Damit steht fest, dass die Abweisung von der Regel kommt und nicht vom Aufbau. Praezedenzfall fuer diese Messform:gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit.modulegrant-fremde-gruppe-abgelehnt— die Umkehr der zweiten loch-behauptenden Pruefung, mit demselben Verweis-Erfordernis.modulegrant-fremder-benutzer-abgelehnt— NEU, der zweite Zweig des Entweder-oder (D-04): eine Freigabe mit eigener Mandantenkennung und fremder Benutzerkennung. T-JTS-03 hat nur den Gruppenzweig gemessen; die Regel muss beide decken, sonst bleibt die Haelfte des Lochs offen.modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich— die Gegenmessung dazu. Diese Pruefung legt ZUGLEICH die Zeilegrant-foreign-groupan, die bisher als Nebenwirkung der loch-behauptenden Pruefung entstand und auf der die beiden Pruefungen des Bereichsmodule-registryaufsetzen (Befund C). Der Aufbau muss so sein, dass diese Zeile weiterhin vorhanden ist, wenn der Bereichmodule-registrygemessen wird.tenderrssfeed-plattformzeile-gebunden-sichtbar— die Umkehr der dritten loch-behauptenden Pruefung: unter BEIDEN Mandantenkontexten ist die plattformweite Zeile jetzt sichtbar. Der Meldetext nennt den alten Pruefungsnamen, WINDOWS #19 und die beobachteten Kennungen beider Ergebnisse.tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar— die andere Fehlerrichtung derselben Aenderung: der gebundene Lesezugriff liefert ZUSAETZLICH weiterhin die eigene Zeile des Mandanten und NICHT die des anderen. Ohne diese Pruefung waere eine zu weit gefasste Leseregel unbemerkt.tenderrssfeed-ungebunden-nur-die-plattformzeile— die Belegzeile fuer Befund F: ohne gesetzten Mandantenkontext liefert der Lesezugriff jetzt genau die plattformweiten Zeilen und keine persoenliche. Das ist die neue Fehlerrichtung, die Aufgabe 2 traegt; sie gehoert gemessen, nicht behauptet.tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt— BESTEHT BEREITS und muss bestanden bleiben. Ihr Meldetext ist an die neue, jetzt ausdrueckliche Schreibbedingung anzupassen.tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt— NEU: ein gebundenes UPDATE auf die plattformweite Zeile wird abgewiesen.tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt— NEU: ein gebundenes DELETE auf die plattformweite Zeile wird abgewiesen. Diese beiden sind die Messung der Richtung "zu locker" und damit der eigentliche Grund fuer die Trennung nach Befehl (Befund K).searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar— NEU, mit einer eigenen Wegwerf-Tabelle und der UNVERAENDERT ausgelieferten Regel: eine mandantenlose Zeile bleibt hier bewusst unsichtbar. Der Meldetext haelt die Begruendung aus Befund E fest — es gibt keinen Codeweg, der eine solche Zeile erzeugt, die Vorgaben sind Konstanten — und benennt die Bedingung, unter der diese Entscheidung neu zu bewerten waere.
Die Extraktion der abgeloesten Regeln muss auf die NEUE Migrationsdatei zeigen
(Befund D). Findet sie eine benoetigte Regel nicht, meldet der Abschnitt eine
FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Regel
weiterzumessen — die Form, die die bestehenden Abschnitte bereits vormachen.
Der Meldetext der Pruefung gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus
im Bereich module-registry behauptet heute, die Regel lasse die fremde Zeile
durch; das ist nach dieser Aufgabe unwahr und im selben Zug richtigzustellen.
Zuerst die Ausgangslage SELBST messen und die drei Zahlen notieren (Testlauf,
Typpruefung, Wegwerf-Werkzeug). Erst danach anfangen.
Dann die drei ausgelieferten Migrationen und die vier Modelle im Schema erneut
lesen — die Angaben aus <planning_time_findings> leiten nur die Suche.
Eine EINZIGE neue Migration anlegen, Verzeichnisname mit dem Zeitstempel
20260910120000 und dem Namensteil rls_widen_membership_grant_and_platform_read;
der Zeitstempel muss hinter dem juengsten vorhandenen liegen. Die bestehenden
Migrationsdateien bleiben UNVERAENDERT — Prisma fuehrt ihre Pruefsummen, eine
Aenderung braechte den Migrationslauf zum Abbruch; der Praezedenzfall dafuer und
die Form des erklaerenden Kopfes stehen im Kopf von 20260909140000. Der Kopf der
neuen Datei nennt: welche Regeln sie abloest und warum, dass die alten Dateien
deshalb stehen bleiben, dass der Schalter weiterhin aus ist und die Regeln damit
heute wirkungslos sind, sowie die Begruendung aus Befund E dafuer, dass
SearchProvider ausdruecklich NICHT angefasst wird.
Inhalt der Migration, in dieser Reihenfolge:
(1) GroupMembership: die bestehende Regel entfernen und unter demselben Namen
neu anlegen, mit einer zweiten Bedingung fuer die Benutzerseite nach dem
Join-Muster, das PasswordResetToken in 20260618112133 vormacht — die
Benutzerkennung muss unter den Benutzern des laufenden Mandanten liegen, ebenso
wie die Gruppenkennung unter dessen Gruppen. Beide Bedingungen mit UND
verknuepft.
(2) ModuleGrant: die bestehende Regel entfernen und unter demselben Namen neu
anlegen. Die eigene Mandantenkennung wird wie bisher verglichen; zusaetzlich
muss die Gruppenkennung entweder leer sein oder unter den Gruppen des laufenden
Mandanten liegen, und die Benutzerkennung entweder leer sein oder unter dessen
Benutzern. Die Leer-Zulassung ist zwingend, weil das Modell Gruppe und Benutzer
als Entweder-oder fuehrt (D-04).
(3) TenderRssFeedSource: die bestehende Regel entfernen und durch VIER nach
Befehl getrennte Regeln ersetzen (Befund K), mit den Namen
tenant_platform_read_policy, tenant_insert_policy, tenant_update_policy
und tenant_delete_policy. Die Leseregel gilt fuer SELECT und laesst zusaetzlich
zur Gleichheit mit dem laufenden Mandanten die Zeilen ohne Mandantenkennung zu.
Die drei Schreibregeln verlangen ausnahmslos Gleichheit mit dem laufenden
Mandanten — die Einfuegeregel als Schreibbedingung, die Aenderungsregel in beiden
Klauseln, die Loeschregel als Lesebedingung ihres Befehls.
(4) SearchProvider: keine Anweisung, nur der erklaerende Absatz im Kopf.
Danach die Migration LOKAL ANWENDEN — ohne diesen Schritt ist nichts gemessen, und Bau wie Typpruefung liefen auch ohne ihn gruen. Die Container-Adresse frisch ermitteln, nicht abschreiben. Anwenden ausschliesslich mit dem Befehl, der ausstehende Migrationen anwendet, NIEMALS mit der Entwicklungsvariante und niemals mit dem Ruecksetzbefehl — diese Datenbank traegt den lokalen Bestand. Danach den Status abfragen und die Regelliste der lebenden Datenbank aus dem Systemkatalog auslesen; diese Liste ist der Beleg, dass die Regel wirklich angekommen ist.
Zusaetzlich die Bestandszaehlung aus Befund H erneut ausfuehren (Mitgliedschaften und Freigaben, die ueber Mandantengrenzen zeigen) und das Ergebnis fuer den Bericht festhalten. Ist es nicht null, HALTEN und melden statt weitermachen — solche Zeilen waeren nach dem Scharfschalten weder sichtbar noch loeschbar.
Dann das Wegwerf-Werkzeug wie unter <behavior> beschrieben umbauen. Die
Extraktion auf die neue Datei umleiten, eine Extraktionsform ergaenzen, die
mehrere Regeln je Tabelle liefern kann, die drei loch-behauptenden Pruefungen
UMKEHREN statt zu entfernen oder zu lockern, die Gegenmessungen und die vier
Befehlsrichtungen ergaenzen, und die Bereitstellung von grant-foreign-group
so umbauen, dass die beiden aufsetzenden Pruefungen weiterhin messen, was sie
behaupten.
Zuletzt einen neuen Beschreibungsblock in apps/api/src/groups/migration-sql.spec.ts
fuer die neue Migration, im Stil der beiden bestehenden Bloecke: reiner
Textabgleich ohne Datenbank. Er prueft, dass die neue Datei die abgeloesten
Regeln entfernt und neu anlegt, dass die Benutzerseite in beiden neuen
Bedingungen vorkommt, dass die Tabelle mit nullbarer Mandantenkennung genau vier
nach Befehl getrennte Regeln bekommt, und dass ausschliesslich die Leseregel die
Zeilen ohne Mandant zulaesst.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && DATABASE_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/tessera" npx prisma migrate status --schema apps/api/prisma/schema.prisma | tee /tmp/jab-migrate-status.txt && grep -qiE 'up to date|schema is up to date|keine ausstehenden' /tmp/jab-migrate-status.txt && POL=$(docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c "SELECT tablename||'#'||policyname||'#'||coalesce(cmd,'')||'#'||coalesce(qual,'')||'#'||coalesce(with_check,'') FROM pg_policies WHERE tablename IN ('GroupMembership','ModuleGrant','TenderRssFeedSource','SearchProvider') ORDER BY tablename, policyname") && echo "$POL" && echo "$POL" | grep '^GroupMembership#' | grep -q '"User"' && echo "$POL" | grep '^ModuleGrant#' | grep -q '"User"' && echo "$POL" | grep '^ModuleGrant#' | grep -q '"Group"' && test 4 -eq "$(echo "$POL" | grep -c '^TenderRssFeedSource#')" && test 1 -eq "$(echo "$POL" | grep -c '^SearchProvider#')" && echo "$POL" | grep '^TenderRssFeedSource#' | grep '#SELECT#' | grep -q 'IS NULL' && test 0 -eq "$(echo "$POL" | grep '^TenderRssFeedSource#' | grep -vE '#SELECT#' | grep -c 'IS NULL')" && test 0 -eq "$(echo "$POL" | grep '^SearchProvider#' | grep -c 'IS NULL')" && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in groupmembership-schreiben-fremder-benutzer-abgelehnt groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich modulegrant-fremde-gruppe-abgelehnt modulegrant-fremder-benutzer-abgelehnt modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich tenderrssfeed-plattformzeile-gebunden-sichtbar tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar tenderrssfeed-ungebunden-nur-die-plattformzeile tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && for A in T-JTS-02 T-JTS-03; do echo "$OUT" | grep -q "$A" || { echo "VERWEIS AUF DEN ALTEN BEFUND FEHLT IM MELDETEXT: $A"; exit 1; }; done && ls -d apps/api/prisma/migrations/*_rls_widen_membership_grant_and_platform_read >/dev/null && npm --prefix apps/api run test -- src/groups/migration-sql.spec.ts src/prisma/rls-coverage.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma/schema.prisma apps/api/src/tenders apps/api/src/module-registry apps/api/src/dashboard apps/api/src/user apps/api/src/ldap apps/api/src/auth docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
Eine neue Migration mit dem Namensteil rls_widen_membership_grant_and_platform_read existiert, ist LOKAL angewandt und der Migrationsstatus meldet keinen Rueckstand; die Regelliste der lebenden Datenbank zeigt fuer die Mitgliedschaftstabelle und die Freigabetabelle eine Bedingung, die die Benutzertabelle nennt, fuer die Freigabetabelle zusaetzlich die Gruppentabelle, fuer die RSS-Quellentabelle genau vier nach Befehl getrennte Regeln, von denen ausschliesslich die Leseregel die Zeilen ohne Mandantenkennung zulaesst, und fuer die Suchanbietertabelle unveraendert genau eine strenge Regel; die Bestandszaehlung ueber Mandantengrenzen zeigende Zeilen ist ausgefuehrt und ihr Ergebnis im Bericht genannt; das Wegwerf-Werkzeug meldet alle Pruefungen bestanden mit Rueckgabewert 0, die zwoelf neuen bzw. umgekehrten Kennungen stehen einzeln als bestanden in der Ausgabe, und die Meldetexte nennen beide alten Befundkennungen, damit der Nachweis ihrer Existenz erhalten bleibt; die beiden aufsetzenden Pruefungen des Bereichs module-registry stehen weiterhin auf bestanden, ihre Grundlage wird ueber die Wartungsrolle bereitgestellt und die Meldetexte behaupten nicht mehr, die Regel lasse die fremde Zeile durch; der neue Beschreibungsblock in der Migrations-Textpruefung ist vorhanden und gruen; der Gesamttestlauf ist gruen und die Typpruefung sauber; Schema, Anwendungscode und die Compose- und Beispiel-Umgebungsdateien sind unveraendert.
- Die Feed-Auflistung erzeugt genau EINEN gebundenen Klienten mit der Mandantenkennung aus dem Aufrufzusammenhang und fuehrt die Abfrage ueber ihn aus — nachgewiesen mit dem im Vorhaben etablierten Zwei-Klienten-Nachweis (zwei unterscheidbare Klienten, der ungebundene darf die Abfrage nicht sehen), nicht mit einer Identitaets-Attrappe. Eine Attrappe, die den Helfer als Identitaet abbildet, wuerde die Umstellung in KEINER Richtung bemerken; dieser Fehler ist in diesem Vorhaben bereits dreimal aufgetreten.
- Der aufrufende Endpunkt reicht die Mandantenkennung aus dem Sitzungsnachweis durch — nachgewiesen an der Aufrufform, nicht an der Antwort.
- Die Abbildung der Antwort bleibt unveraendert: plattformweite Zeilen werden weiterhin als solche gekennzeichnet und die Besitzerkennung weiterhin entfernt. Der bestehende Fall dazu bleibt inhaltlich erhalten.
- Die beiden Pfade, die eine plattformweite Zeile anlegen bzw. entfernen, bleiben UNGEBUNDEN. Ein Fall haelt das fest, damit ein spaeterer Leser sie nicht "der Vollstaendigkeit halber" mitbindet — gebunden koennte niemand mehr eine plattformweite Quelle anlegen oder entfernen.
- Die Mandanten-Gegenpruefung vor jedem Erteilen einer Modulfreigabe bleibt
bestehen. Ein Fall haelt fest, dass ein Erteilen auf ein fremdes Ziel weiterhin
im Anwendungscode scheitert — nicht erst in der Datenbank, die heute ohnehin
nichts durchsetzt. Existiert ein solcher Fall bereits, wird er um einen
Kommentar ergaenzt, der sagt, warum er nach dieser Regelaenderung NICHT
entbehrlich geworden ist.
Zuerst die Ausgangslage erneut messen (Testzahl, Typpruefung, Wegwerf-Werkzeug),
dann die Testerwartungen aus
<behavior>schreiben und rot sehen, dann umstellen.
Die Auflistung der Feeds nimmt statt der blossen Benutzerkennung einen Aufrufzusammenhang mit Benutzer- UND Mandantenkennung entgegen und fuehrt ihre Abfrage ueber einen gebundenen Klienten aus, benannt wie in den bereits umgestellten Bereichen. Der bestehende Filter auf eigene und plattformweite Zeilen bleibt stehen — er ist nach der Bindung nicht ueberfluessig, sondern das zweite Netz. Der aufrufende Endpunkt reicht die Mandantenkennung aus dem Sitzungsnachweis durch; er hat sie bereits zur Hand.
Die Begruendung fuer diese Bindung gehoert in den Kopfkommentar der Methode, und zwar als das, was sie ist: die Regelaenderung dreht die Fehlerrichtung dieses Pfades um. Ungebunden lieferte er nach dem Scharfschalten frueher gar nichts und liefert jetzt die plattformweiten Zeilen — eine kurze, glaubhafte Liste statt einer leeren. Der Kommentar nennt die neue Migration und die Pruefung, die das belegt.
Die beiden Kopfkommentare der Pfade, die eine plattformweite Zeile anlegen bzw. entfernen, werden an der neuen Regel richtiggestellt: sie bleiben ungebunden, aber aus dem jetzt ausdruecklichen Grund (die Schreibregeln verlangen einen Mandanten), und sie halten fest, dass beide Faelle unter der Anwendungsrolle ueberhaupt nicht mehr durchgehen — vor wie nach dieser Aenderung — und deshalb in Etappe 4 einen Verwaltungsweg brauchen. Ein Verweis auf den dafuer in Aufgabe 3 angelegten Ledger-Eintrag gehoert dazu.
Die vier Aufzeichnungen im Quelltext, die die alten Regeln beschreiben, werden an der Messung aus Aufgabe 1 richtiggestellt: der Kopfkommentar zur Zugriffsaufloesung im Modulbereich, der Kommentar an der Benutzerpruefung in der Gruppenverwaltung, der Kommentar an der Mandanten-Gegenpruefung bei den Modulfreigaben und die Beschreibungszeile fuer die Mitgliedschaftstabelle in der Ausnahmeliste des Abdeckungstests, die die Absicherung heute nur ueber die Gruppenseite beschreibt und jetzt beide Seiten nennen muss. Jede dieser vier Stellen nennt die neue Migration.
Die Mandanten-Gegenpruefung selbst wird NICHT entfernt und nicht abgeschwaecht. Ihr Kommentar sagt kuenftig, dass die Datenbank die Grenze inzwischen ebenfalls zieht, dass diese zweite Ziehung aber erst nach dem Scharfschalten wirkt und die Pruefung im Anwendungscode bis dahin der einzige und danach der erste Schutz bleibt.
Zum Abschluss den Falsifizierungsnachweis: den Bindungsaufruf der Feed-Auflistung
versuchsweise zuruecknehmen, den roten Testnamen samt Fehlermeldung notieren,
die Ruecknahme rueckgaengig machen. Ohne diesen Nachweis ist nicht belegt, dass
der neue Test die Umstellung ueberhaupt bemerken wuerde. Er gehoert in den
Bericht.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/tenders/tender-rss-feed.service.ts && B=$(grep -c 'tenantPrisma.tenderRssFeedSource.' apps/api/src/tenders/tender-rss-feed.service.ts) && { test "$B" -ge 3 || { echo "BINDUNG: nur $B gebundene Zugriffe in tender-rss-feed.service.ts, erwartet mindestens 3 (zwei bestehende plus die Auflistung)"; exit 1; }; } && U=$(grep -c 'this.prisma.tenderRssFeedSource.' apps/api/src/tenders/tender-rss-feed.service.ts) && { test "$U" -eq 2 || { echo "UNGEBUNDEN: $U ungebundene Zugriffe in tender-rss-feed.service.ts, erwartet genau 2 (Anlegen und Entfernen plattformweiter Zeilen)"; exit 1; }; } && A=$(grep -c 'assertTargetBelongsToTenant' apps/api/src/groups/module-grants.service.ts) && { test "$A" -ge 3 || { echo "GEGENPRUEFUNG: nur $A Vorkommen von assertTargetBelongsToTenant, die Pruefung wurde entfernt oder ihre Aufrufe reduziert"; exit 1; }; } && MIG=$(basename $(ls -d apps/api/prisma/migrations/*_rls_widen_membership_grant_and_platform_read)) && for F in apps/api/src/tenders/tender-rss-feed.service.ts apps/api/src/groups/groups.service.ts apps/api/src/groups/module-grants.service.ts apps/api/src/module-registry/module-access.service.ts apps/api/src/prisma/rls-coverage.spec.ts; do grep -q "$MIG" "$F" || { echo "AUFZEICHNUNG NENNT DIE NEUE MIGRATION NICHT: $F"; exit 1; }; done && grep -q 'tenantId' apps/api/src/tenders/tenders.controller.ts && npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts src/tenders/tenders.controller.spec.ts src/groups/module-grants.service.spec.ts src/prisma/rls-coverage.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/dashboard apps/api/src/user apps/api/src/ldap apps/api/src/auth apps/api/src/dkv docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
Die Feed-Auflistung laeuft ueber einen gebundenen Klienten, nimmt die Mandantenkennung aus dem Aufrufzusammenhang entgegen und wird vom aufrufenden Endpunkt entsprechend bedient; die beiden Pfade fuer plattformweite Zeilen bleiben ungebunden und tragen die richtiggestellte Begruendung samt Verweis auf den in Aufgabe 3 angelegten offenen Eintrag; der Zwei-Klienten-Nachweis liegt in der Testdatei des Dienstes vor, keine Identitaets-Attrappe; die Mandanten-Gegenpruefung bei den Modulfreigaben steht unveraendert und ist durch einen Fall gehalten, der beim Rueckbau rot wird; alle fuenf Aufzeichnungen im Quelltext nennen die neue Migration und beschreiben die neue Regel statt der alten; der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und im Bericht mit Testnamen und Fehlermeldung festgehalten; der Gesamttestlauf ist gruen mit mindestens der zu Beginn dieser Aufgabe gemessenen Testzahl, die Typpruefung sauber, das Wegwerf-Werkzeug weiterhin vollstaendig bestanden; Schema, Migrationen und die Bereiche dashboard, user, ldap, auth, dkv sowie die Compose- und Beispiel-Umgebungsdateien sind unveraendert.
Ein NEUER offener Eintrag kommt hinzu, ebenfalls in Tabelle und JSON-Block: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil Schreibzugriffe ausdruecklich einen Mandanten verlangen. Der Eintrag benennt die beiden betroffenen Pfade, sagt, dass dies KEINE Folge dieser Reparatur ist, und benennt die konkrete Vorabpruefung fuer Etappe 4. Er ist der Grund, warum die Schliessung von #19 nichts verschwinden laesst.
Die Zaehler im Kopf der Datei werden aus dem JSON-Block ABGELEITET, nicht weitergezaehlt — vier von vier Kopfzahlen dieses Vorhabens sind bei genauerem Hinsehen schon einmal geschrumpft.
Die Klassifikation. Der Block zu #19 wird von einer offenen Frage zu einer beantworteten: er nennt die neue Migration, die getroffene Semantik (Lesen schliesst die plattformweiten Zeilen ein, Schreiben verlangt einen Mandanten, getrennt nach Befehl weil ein Lesebedingung allein auch Aendern und Entfernen regelt) und die Widerlegung fuer die Suchanbietertabelle. Der Punkt in "Was diese Etappe NICHT entscheidet", der genau diese Regelform als offen fuehrt, wird entsprechend aufgeloest statt stehengelassen.
In der Bestandsaufnahme werden die vier betroffenen Zeilen nachgezogen: die Zeile zur Suchanbietertabelle (die Begruendung bleibt richtig, bekommt aber die Messung und den Verweis), die beiden Zeilen, die die einseitigen Regeln als Grund fuer eine zusaetzliche Anwendungspruefung nennen, und die Zeile zu den RSS-Quellen, deren Stand-Begruendung jetzt einen gebundenen Pfad mehr und zwei ungebundene mit neuer Begruendung nennt. Uebersichtszeile und Summenzeile werden aus dem Quelltext neu abgeleitet, nicht aus diesem Plan uebernommen.
Die Kritikschrift bekommt einen neuen Abschnitt ## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19, gesetzt nach dem Abschnitt zum Bereich
module-registry und vor den Verweis am Ende, mit den fuenf im Dokument
ueblichen Unterabschnitten unter den Kennungen (r1) bis (r5): die
TATSAECHLICH beobachtete Ausgabe des Werkzeuglaufs und der Regelliste aus der
lebenden Datenbank; eine Signaltabelle, die fuer jede der drei Regeln BEIDE
Fehlerrichtungen fuehrt (zu streng und zu locker) samt dem Signal, an dem man sie
erkennen wuerde; die namentliche Liste der Stellen, an denen Leere weiterhin als
Abwesenheit gedeutet wird, einschliesslich der neuen Stelle aus Befund F und der
Begruendung, warum sie gebunden wurde; was dieser Durchlauf bewusst nicht loest
(der Verwaltungsweg fuer plattformweite Zeilen, die fehlende Benutzerdimension
der Ausschreibungsregeln, die plattformweite Eindeutigkeit von Anmeldename und
Adresse); und was er bewusst nicht anfasst.
Zur fehlenden Benutzerdimension gehoert eine eigene Messung statt einer Uebernahme: es ist selbst nachzusehen, ob es irgendwo eine zweite Sitzungsvariable fuer den Benutzer gibt, und das Ergebnis mit der ausgefuehrten Anweisung festzuhalten. Gebaut wird sie hier nicht.
Ausserdem werden die vier ueberholten Bestandsstellen der Kritikschrift mit
einem Nachtrag versehen — die Form dafuer macht der Abschnitt zur Fortschreibung
des ldap-Abschnitts bereits vor: der Punkt, der beide Regeln als einseitig
beschreibt; der Punkt, der #19 als nicht geloest fuehrt; die Zeile der
Signaltabelle des Bereichs tenders zu den drei RSS-Pfaden; und die beiden
aufgezeichneten Werkzeugausgaben, die die alten Meldetexte woertlich zitieren.
Die zitierten alten Ausgaben werden NICHT umgeschrieben — sie sind ein
Messprotokoll und bleiben, was sie waren; der Nachtrag steht daneben und sagt,
seit wann und wodurch die Aussage ueberholt ist. Ebenso die eine Stelle in der
Betriebsanleitung zur Datenbankrolle, die #19 als offen fuehrt.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && MIG=$(basename $(ls -d apps/api/prisma/migrations/_rls_widen_membership_grant_and_platform_read)) && python3 - "$MIG" <<'PY'
import json, re, sys
mig = sys.argv[1]
t = open('.planning/WINDOWS.md', encoding='utf-8').read()
m = re.search(r'{3,}json\s*\n(\[.*?\])\s*\n{3,}', t, re.S)
if not m:
sys.exit('JSON-Block in WINDOWS.md nicht gefunden')
rows = json.loads(m.group(1))
by_id = {r['id']: r for r in rows}
if by_id[19]['status'] != 'fixed':
sys.exit('WINDOWS #19 steht nicht auf fixed')
if not by_id[19].get('resolved_at'):
sys.exit('WINDOWS #19 hat keinen Aufloesungszeitpunkt')
for i in (18, 20, 21, 23):
if by_id[i]['status'] != 'open':
sys.exit(f'WINDOWS #{i} ist nicht mehr offen')
new = [r for r in rows if r['id'] > 23]
if len(new) != 1:
sys.exit(f'genau ein neuer Eintrag erwartet, gefunden: {len(new)}')
if new[0]['status'] != 'open':
sys.exit('der neue Eintrag ist nicht offen')
open_n = sum(1 for r in rows if r['status'] == 'open')
fixed_n = sum(1 for r in rows if r['status'] == 'fixed')
waived_n = sum(1 for r in rows if r['status'] == 'waived')
head = t.split('---')[1]
for key, want in (('open_count', open_n), ('fixed_count', fixed_n),
('waived_count', waived_n), ('total_count', len(rows))):
got = re.search(rf'^{key}:\s(\d+)\s*$', head, re.M)
if not got or int(got.group(1)) != want:
sys.exit(f'{key} im Kopf stimmt nicht mit dem JSON-Block: {got and got.group(1)} statt {want}')
for r in rows:
line = re.search(rf'^|\s*{r["id"]}\s*|.$', t, re.M)
if not line:
sys.exit(f'Eintrag {r["id"]} fehlt in der Markdown-Tabelle')
if f'| {r["status"]} |' not in line.group(0):
sys.exit(f'Status von Eintrag {r["id"]} weicht zwischen Tabelle und JSON-Block ab')
if mig not in (by_id[19].get('reason') or '') + (by_id[19].get('description') or ''):
sys.exit('der Beleg zu #19 nennt die neue Migration nicht')
print(f'WINDOWS.md kohaerent: {open_n} offen, {fixed_n} behoben, {waived_n} zurueckgestellt, {len(rows)} gesamt')
PY
&& grep -q '^## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in r1 r2 r3 r4 r5; do grep -qE "^### ($S) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && for F in docs/mandantentrennung-etappe2-fehlerrichtung.md docs/mandantentrennung-zugriffsklassifikation.md docs/mandantentrennung-datenbankrolle.md .planning/WINDOWS.md; do grep -q "$MIG" "$F" || { echo "DOKUMENT NENNT DIE NEUE MIGRATION NICHT: $F"; exit 1; }; done && U=$(grep -ro "this.prisma.[a-zA-Z]" apps/api/src/tenders | grep -v spec | wc -l | tr -d ' ') && B=$(grep -ro "tenantPrisma.[a-zA-Z]*." apps/api/src/tenders | grep -v spec | wc -l | tr -d ' ') && { grep -qE "^| tenders | ${U} | ${B} |" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE tenders nennt nicht die neu gemessenen Zahlen ${U}/${B}"; exit 1; }; } && awk -F'|' '$2 ~ /^ [a-z][a-z-] *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ ***Summe** *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma/schema.prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
WINDOWS #19 steht in Tabelle UND JSON-Block auf behoben, mit Zeitpunkt, mit dem Namen der neuen Migration im Beleg und mit der ausdruecklichen Feststellung, dass die Haelfte zur Suchanbietertabelle als widerlegte Praemisse und nicht als geloestes Problem schliesst; #18, #20, #21 und #23 sind unveraendert offen; genau ein neuer offener Eintrag beschreibt den fehlenden Verwaltungsweg fuer plattformweite Zeilen samt Vorabpruefung fuer Etappe 4; die vier Kopfzahlen stimmen mit dem JSON-Block ueberein und sind daraus abgeleitet; die Klassifikation fuehrt den #19-Block als beantwortet, hat den zugehoerigen Punkt in "Was diese Etappe NICHT entscheidet" aufgeloest, die vier Bestandsaufnahme-Zeilen nachgezogen und Uebersichts- wie Summenzeile aus dem Quelltext neu abgeleitet; die Kritikschrift traegt den neuen Abschnitt mit allen fuenf Unterabschnitten, der tatsaechlich beobachteten Ausgabe, einer Signaltabelle mit BEIDEN Fehlerrichtungen je Regel, der selbst ausgefuehrten Messung zur fehlenden Benutzerdimension und Nachtraegen an den vier ueberholten Bestandsstellen, ohne die alten Messprotokolle umzuschreiben; die Betriebsanleitung nennt #19 nicht mehr als offen; die Inventarpruefung, der Gesamttestlauf und die Typpruefung sind gruen; Schema und die Compose- und Beispiel-Umgebungsdateien sind unveraendert.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Mandant A → Datenzeilen des Mandanten B | Die Grenze, die dieser Plan an zwei Stellen von einseitig auf beidseitig zieht |
| Mandant → plattformweite Zeile (leere Mandantenkennung) | Neue, ausdrueckliche Grenze: lesen ja, schreiben nein — und sie muss nach BEIDEN Seiten stimmen |
| Anwendungscode → Datenbankregel | Die Regel ist ein ZWEITES Netz. Wird der Anwendungscode im Vertrauen darauf abgebaut, faellt der einzige heute wirksame Schutz weg (Schalter ist aus) |
| Aufzeichnung → spaeterer Leser | Eine Aufzeichnung, die ein geschlossenes Loch als offen fuehrt oder eine ueberholte Handlungsanweisung gibt, ist ein Angriffsweg auf den naechsten Durchlauf |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-JAB-01 | Elevation of Privilege | Regel auf GroupMembership |
high | mitigate | Die Regel prueft nach Aufgabe 1 beide Seiten der Beziehung; gemessen mit einem Benutzer, den es im fremden Mandanten TATSAECHLICH gibt, samt Wartungsrollen-Gegenmessung, die belegt, dass die Abweisung von der Regel kommt |
| T-JAB-02 | Elevation of Privilege | Regel auf ModuleGrant, referenzierte Gruppe |
high | mitigate | Die Regel prueft zusaetzlich die referenzierte Gruppe; die anwendungsseitige Gegenpruefung bleibt bestehen und ist durch einen Fall gehalten, der beim Rueckbau rot wird |
| T-JAB-03 | Elevation of Privilege | Regel auf ModuleGrant, referenzierter Benutzer |
high | mitigate | Der zweite Zweig des Entweder-oder wird ausdruecklich mitgeprueft — T-JTS-03 hatte nur den Gruppenzweig gemessen, eine Reparatur nur dieses Zweigs liesse die Haelfte des Lochs offen |
| T-JAB-04 | Elevation of Privilege | Lese-/Schreibsplit zu locker gefasst | high | mitigate | Vier nach Befehl getrennte Regeln statt einer permissiven; drei Pruefungen messen, dass Einfuegen, Aendern und Entfernen einer plattformweiten Zeile unter gebundenem Kontext abgewiesen werden, und die Regelliste der lebenden Datenbank belegt, dass ausschliesslich die Leseregel die Zeilen ohne Mandant zulaesst |
| T-JAB-05 | Denial of Service | Lese-/Schreibsplit zu streng gefasst | high | mitigate | Zwei Pruefungen messen die Gegenrichtung: die plattformweite Zeile ist unter beiden Mandantenkontexten sichtbar UND die eigene Zeile des Mandanten bleibt es ebenfalls. Ohne die zweite waere eine Leseregel, die nur noch die plattformweiten Zeilen liefert, unbemerkt |
| T-JAB-06 | Denial of Service | Bestandszeilen, die die schaerfere Regel nicht mehr sieht | medium | mitigate | Aufgabe 1 zaehlt vor der Anwendung, ob es Mitgliedschaften oder Freigaben ueber Mandantengrenzen gibt, und HAELT bei einem Ergebnis ungleich null; dieselbe Zaehlung wird als Vorabpruefung an Etappe 4 uebergeben, weil das Testsystem hier nicht gemessen ist |
| T-JAB-07 | Information Disclosure | Suchanbietertabelle faelschlich gelockert | medium | mitigate | Die Tabelle behaelt ihre strenge Regel; die Praemisse von #19 ist fuer sie gemessen widerlegt. Eine Lockerung wuerde eine kuenftige mandantenlose Zeile jedem Mandanten zeigen — genau die falsche Richtung |
| T-JAB-08 | Tampering | Abbau der anwendungsseitigen Gegenpruefung im Vertrauen auf die neue Regel | high | mitigate | Die Gegenpruefung bleibt unveraendert, ihr Kommentar sagt ausdruecklich, dass die zweite Ziehung erst nach dem Scharfschalten wirkt, und eine Zaehlung ihrer Vorkommen ist Teil der Abnahme von Aufgabe 2 |
| T-JAB-09 | Repudiation | Der Nachweis, dass die Loecher existierten, geht verloren | medium | mitigate | Die drei Pruefungen werden UMGEKEHRT statt geloescht; jede traegt im Meldetext den alten Pruefungsnamen und die Befundkennung, und die Abnahme prueft, dass beide Kennungen in der Ausgabe des Werkzeugs vorkommen |
| T-JAB-10 | Spoofing | Eine Pruefung besteht aus dem falschen Grund, weil ihre Grundlage weggefallen ist | high | mitigate | Die Zeile, auf der zwei spaetere Pruefungen aufsetzen, wird ueber die Wartungsrolle bereitgestellt; die Abnahme fordert beide aufsetzenden Pruefungen namentlich als bestanden, und die Gegenmessung ueber die Wartungsrolle wuerde scheitern, wenn die Zeile fehlte |
| T-JAB-11 | Tampering | Die Regelaenderung wird geschrieben, aber nie angewandt | high | mitigate | Blockierender Schritt in Aufgabe 1: Migrationsstatus ohne Rueckstand UND Auslesen der Regelliste aus dem Systemkatalog der laufenden Datenbank. Bau und Typpruefung liefen auch ohne die Anwendung gruen |
| T-JAB-12 | Information Disclosure | Zugangsdaten im versionierten Text | low | accept | Die lokalen Entwicklungszugangsdaten stehen bereits als Vorgabewert in der Compose-Datei; es entsteht kein neues Geheimnis. Die Migration selbst vergibt kein Kennwort — derselbe Vorsatz wie in 20260909130000 |
| T-JAB-13 | Denial of Service | Der Verwaltungsweg fuer plattformweite Zeilen fehlt nach dem Scharfschalten | medium | transfer | Nicht durch diesen Plan geloest, weil die vorgegebene Semantik Schreibzugriffe an einen Mandanten bindet. Als eigener offener Ledger-Eintrag mit Vorabpruefung an Etappe 4 uebergeben, damit er nicht mit #19 verschwindet |
| T-JAB-14 | Repudiation | Handgepflegte Dokumentstellen werden still uebersprungen | high | mitigate | Vollstaendige Fundstellenliste in Befund J, abgeleitete Pruefungen statt fest verdrahteter Zahlen fuer Uebersichts- und Summenzeile, und eine Pruefung, die fordert, dass jede der betroffenen Dateien die neue Migration namentlich nennt |
| T-JAB-15 | Tampering | npm/pip/cargo-Installationen | low | accept | Dieser Plan installiert kein Paket; package.json und die Sperrdatei werden nicht angefasst. Das Paket-Legitimitaetsgatter faellt damit nicht an |
| </threat_model> |
- Der Migrationsstatus der lokalen Datenbank meldet keinen Rueckstand, und die Regelliste aus dem Systemkatalog zeigt die neuen Regeln so, wie die Migration sie beschreibt — nicht nur die Datei auf der Platte.
node apps/api/scripts/rls-scratch-check.mjsmeldet alle Pruefungen bestanden, einschliesslich der zwoelf namentlich geforderten neuen bzw. umgekehrten und der beiden aufsetzenden Pruefungen des Bereichsmodule-registry.npm --prefix apps/api run testist gruen, mit mindestens der zu Beginn von Aufgabe 1 selbst gemessenen Testzahl.npm --prefix apps/api run type-checkist sauber.npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.tsist gruen — die maschinelle Klammer zwischen Quelltext und Klassifikation..planning/WINDOWS.mdist in Kopf, Tabelle und JSON-Block widerspruchsfrei; #19 behoben, ein neuer Eintrag offen, #18/#20/#21/#23 unveraendert offen.DATABASE_URLin allen Compose-Dateien und Beispiel-Umgebungsdateien zeigt unveraendert auf die Rolle ohne_app-Zusatz;prisma/schema.prismaist unveraendert.- Manuell zu lesen, nicht maschinell zu pruefen: der neue Abschnitt der Kritikschrift fuehrt fuer jede der drei Regeln BEIDE Fehlerrichtungen und nicht nur die, die repariert wurde.
<success_criteria>
- Die drei Regeln greifen so weit, wie sie sollen, und das ist an der lebenden Datenbank gemessen statt aus der Migrationsdatei erschlossen.
- Kein Test und keine Pruefung behauptet noch, eines der drei Loecher sei erwartetes Verhalten — und keiner ist dabei verloren gegangen.
- Der eine Anwendungspfad, den die Reparatur still falsch gemacht haette, ist gebunden, und die Messung, die das belegt, laeuft bei jedem Werkzeuglauf mit.
- Die anwendungsseitige Mandanten-Gegenpruefung steht unveraendert.
- Keine Aufzeichnung beschreibt mehr ein Loch, das es nicht mehr gibt; keine gibt mehr eine Handlungsanweisung, die nach der Reparatur falsch waere.
- Was die Reparatur nicht loest, ist als eigener offener Punkt aufgeschrieben statt mit dem geschlossenen zu verschwinden.
- Der Schalter ist weiterhin aus.
</success_criteria>
Bericht nach `.planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-SUMMARY.md`.Er nennt ausdruecklich: die drei zu Beginn SELBST gemessenen Ausgangszahlen und die drei am Ende gemessenen; das Ergebnis der Bestandszaehlung ueber Mandantengrenzen zeigender Zeilen; den Falsifizierungsnachweis aus Aufgabe 2 mit Testnamen und Fehlermeldung; das Ergebnis der eigenen Messung zur fehlenden Benutzerdimension; und — als eigener Abschnitt — die Abweichung von der Aufgabenstellung, dass die Behauptung "kein Anwendungscode muss sich aendern" gemessen fuer genau einen Pfad nicht zutrifft, mit der Begruendung, warum das Binden dieses Pfades zur Reparatur gehoert und nicht daneben.