77 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-exd | 01 | execute | 1 | true |
|
|
|
|
Zweck: dieser Bereich ENTSCHEIDET bei jeder Anfrage, wer welches Modul benutzen darf. Bleibt hier eine Abfrage ungebunden, sieht das nach dem Scharfschalten nicht wie ein Fehler aus, sondern wie ein Rechteentzug — die Seitenleiste ist leer, der Marktplatz zeigt nichts, jede Modulroute antwortet mit 403. Fuer den Betroffenen ist das nicht von "ein Administrator hat mir das weggenommen" zu unterscheiden, und niemand meldet einen Fehler fuer ein Recht, das er fuer absichtlich entzogen haelt. Das ist die sichtbarste Auspraegung der umgekehrten Fehlerrichtung im gesamten Vorhaben und zugleich die am wenigsten meldungswahrscheinliche.
Ergebnis: gebundene Aktivierungs- und Freigabestufe, ein begruendet ungebundener Katalog, eine gemessene Kritikschrift, eine Testlage, die einen vergessenen Bindungsaufruf rot macht, und zwei Dokumente, die am Ende nachweislich mit dem Quelltext uebereinstimmen.
Der Schalter bleibt AUS. DATABASE_URL zeigt weiterhin auf die Rolle
tessera mit BYPASSRLS. 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 unten sind zur Planungszeit am 2026-09-10 gegen HEAD f657e24
GEMESSEN, mit der jeweils angegebenen Anweisung. Sie leiten die Untersuchung,
sie sind KEINE Bearbeitungsvollmacht — die Dateien werden vor jeder Aenderung
erneut gelesen.
Befund A — die Zahl haelt. Siebzehn Rohtreffer, genau wie die
Klassifikation sie fuehrt. Gemessen mit
grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/module-registry | grep -v spec | sort | uniq -c:
module-access.service.ts sechs (module einmal, moduleGrant zweimal,
tenantModuleActivation dreimal), module-registry.service.ts elf (module
sechsmal, tenantModuleActivation fuenfmal), module.guard.ts und
module-registry.controller.ts je null. Nach dkv (21) und user (17) der
dritte Bereich in Folge, in dem beim Hineinschauen nichts schrumpft. Auch die
fuenf (Datei, Modell)-Paare der Bestandsaufnahme stimmen mit dem Quelltext
ueberein — anders als im Bereich user (eine sachlich falsche Zeile) und im
Bereich dkv (ein ganz fehlendes Paar) ist hier keine Korrektur noetig. Die
BEGRUENDUNGEN der beiden Katalogzeilen sind trotzdem zu duenn, siehe Befund E.
Befund B — keine Transaktion in diesem Bereich. Gemessen mit
grep -rn '\$transaction(' apps/api/src/module-registry --include=*.ts | grep -v spec:
null Treffer, Rueckgabewert 1. Der im Kopfkommentar von
prisma-tenant.extension.ts ausdruecklich verlangte erneute Test vor jedem
neuen forTenant()-Fall mit eigener Transaktion ist damit fuer diesen Bereich
beantwortet: es faellt kein neuer Fall an, withTenantTransaction() wird hier
nicht gebraucht und nicht eingefuehrt.
Befund C — drei der vier bekannten Testformen liegen hier GLEICHZEITIG vor. Das ist die Form, die dieser Bereich der Liste hinzufuegt:
module-access.service.spec.ts(262 Zeilen, 15 Faelle) hat einen handgerollten Prisma-Nachbau mittenantModuleActivation,moduleGrantundmodule, aber KEINE Attrappe fuer das Bindungshilfsmittel. Das ist diegroups/tenders-Form: nach der Umstellung riefe das echteforTenant()eine$extends-Methode auf einem schlichten Objekt auf, das keine hat — die Tests wuerden scheitern, aber aus dem falschen Grund, und ein vergessener Bindungsaufruf waere von einem vorhandenen nicht zu unterscheiden.module-registry.service.tshat UEBERHAUPT KEINE Testdatei. Das ist diedkv-Form, und sie trifft hier elf der siebzehn Zugriffe, darunter jeden einzelnen Schreibpfad des Bereichs (Aktivieren, Deaktivieren, Katalogpflege beim Start).module.guard.spec.ts(119 Zeilen) stellt beide Dienste als Attrappe und beruehrt keine Datenbank. Von der Bindung nicht betroffen — aber genau diese Datei ist der Ort, an dem sich festnageln laesst, dass der Waechter bei einer leeren Menge nichts hat, woran er die Ursache erkennen koennte.module-registry.controller.tshat ebenfalls keine Testdatei. Er haelt keinen Datenbankzugriff und wird von diesem Plan nicht angefasst.
Befund D — der Waechter haelt keinen eigenen Zugriff. module.guard.ts:
null this.prisma-Treffer. Er ruft moduleRegistryService.findBySlug(slug)
(Katalog, bleibt ungebunden) und moduleAccessService.getAccessibleModuleIds()
(wird gebunden) und wirft bei leerer Menge
ForbiddenException('Module ... is not accessible for this user'). Seine
Richtigkeit ist damit vollstaendig eine Funktion dessen, was der Dienst
zurueckgibt — die Bindung gehoert in den Dienst, und der Waechter braucht
keine.
Befund E — der Modulkatalog traegt ueberhaupt keinen Zeilenschutz, und die
naheliegende Begruendung fuer seine Nichtbindung ist deshalb FALSCH. Gemessen
mit grep -n 'ENABLE ROW LEVEL SECURITY' apps/api/prisma/migrations/*/migration.sql:
23 Tabellen ueber alle ausgelieferten Migrationen, "Module" ist NICHT
darunter. Das Modell traegt im Schema auch kein tenantId
(apps/api/prisma/schema.prisma, Zeilen 97-110).
Daraus folgt eine Praezisierung, die dieser Plan ausdruecklich vornimmt statt sie zu uebergehen: die Aussage "eine Bindung wuerde den Katalog fuer jeden Mandanten unsichtbar machen" trifft HEUTE nicht zu. Ohne Regel auf der Tabelle aendert eine Bindung an der Ergebnismenge nichts — sie setzt nur eine Sitzungsvariable und kostet eine zusaetzliche Transaktion je Abfrage. Die Nichtbindung ist trotzdem richtig, aber aus zwei anderen Gruenden, und beide gehoeren so und nicht anders ins Dokument: erstens waere ein gebundener Katalogzugriff eine Unwahrheit in der Aufzeichnung (er behauptete eine Mandantendimension, die die Tabelle nicht hat); zweitens WUERDE die Bindung genau dann katastrophal, wenn Etappe 3 dieser Tabelle eine Regel gaebe — dann verschwaende der gesamte Katalog fuer jeden Mandanten. Der zweite Grund ist eine BEDINGUNG und wird als Bedingung notiert, nicht als heute beobachtbare Tatsache. Genau diese Unterscheidung ist Aufgabe 1 zu messen.
Befund F — der zweite Verbraucher, in einem noch nicht umgestellten
Bereich. apps/api/src/dashboard/dashboard.service.ts:128 ruft ebenfalls
getAccessibleModuleIds(). Weil die Bindung im Dienst sitzt, wird der
Widget-Filter des Dashboards mit umgestellt, ohne dass eine Datei des Bereichs
dashboard angefasst wird — eine Reihenfolge-ENTLASTUNG, nicht eine Falle.
Heute ruht dieser Pfad: apps/api/src/dashboard/widget-module-map.ts:24 fuehrt
WIDGET_MODULE_MAP als leeres Objekt (D-22 aus 15-05), weshalb
dashboard.service.ts vorher zurueckkehrt und die Aufloesung gar nicht
erreicht. Gemessen, nicht angenommen.
Befund G — zwei Kopfkommentare behaupten etwas Unwahres, und beide Methoden
haben null Aufrufer. Gemessen mit
grep -rn "isModuleActive\|findActiveForTenant" apps/api/src apps/web/src packages:
isModuleActive— genau EIN Treffer, die Definition selbst. Ihr Kopfkommentar sagt "Used by ModuleGuard to gate access to module-specific endpoints". Der Waechter ruft sie nicht auf; er nimmtfindBySlugplusgetAccessibleModuleIds.findActiveForTenant— zwei Treffer: die Definition und eine Prosa-Erwaehnung im Kopfkommentar des Controllers ("stays unchanged for Plan 15-03's marketplace catalog"). Der Marktplatz-Katalog wird vongetCatalogFlagsbedient, nicht von dieser Methode.
Das ist Fehler 3 der Fehlerliste dieses Vorhabens in Reinform (eine Behauptung ist keine Lieferung), hier in einem Kommentar statt in einem Bericht. Beide Kommentare werden richtiggestellt, und beide Methoden werden TROTZDEM umgestellt: heute toter, ungebunden gelassener Code ist die Falle fuer den, der ihn morgen verdrahtet.
Befund H — die Entlastung dieses Bereichs, gegen den Quelltext gehalten.
TenantModuleActivation traegt @@unique([tenantId, moduleId])
(schema.prisma:120). Die beiden partiellen Eindeutigkeitsindizes auf
ModuleGrant fuehren ebenfalls mit der Mandantenkennung
(20260804130130_add_groups_and_module_grants/migration.sql:105-108:
("tenantId","moduleId","groupId") bzw. ("tenantId","moduleId","userId")).
Die Kette, die die Bereiche tenders (TenderTriage) und user (username)
beide tragen — unsichtbare Zeile, falsches "frei", harter Eindeutigkeitsfehler
— kann in diesem Bereich strukturell NICHT auftreten, weil jeder
Eindeutigkeitsschluessel den Mandanten enthaelt und eine fremde Zeile deshalb
gar nicht in dieselbe Schluesselstelle faellt. Das ist der erste Bereich von
sechs, in dem die Kette nicht umgangen, sondern strukturell abwesend ist. Wird
in Aufgabe 1 gemessen, nicht behauptet.
Befund I — der Gruppenpfad bekommt durch die Bindung eine Verteidigung, die
die Regel auf ModuleGrant allein nicht hat. Die Gruppen-Abfrage in
getAccessibleModuleIds filtert
{ tenantId, group: { memberships: { some: { userId } } } }. Gebunden laeuft
diese Beziehungsdurchquerung ueber Group (Regel: tenantId) und
GroupMembership (Regel ueber den Join auf Group). Eine Freigabezeile mit
korrekter eigener Mandantenkennung, aber FREMDER Gruppenkennung — genau die
Zeile, von der T-JTS-03 gemessen hat, dass die Regel auf ModuleGrant sie
NICHT abweist — wird auf der Leseseite unsichtbar, sobald gebunden wird, weil
die verbundene Gruppe unsichtbar ist. Heute ist
assertTargetBelongsToTenant in module-grants.service.ts der EINZIGE Schutz
davor. Nach dem Scharfschalten kommt ein zweiter dazu, aber erst dann — die
Schreibseiten-Pruefung bleibt deshalb unangetastet und wird ausdruecklich nicht
als "macht jetzt die Datenbank" gestrichen. Beide Richtungen sind in Aufgabe 1
zu messen: gebunden ausgeschlossen, ueber die Wartungsrolle mit BYPASSRLS
enthalten.
Befund J — der zweistufige Zugriff, im Code nachgesehen. ADMIN und
SUPER_ADMIN ueberspringen die Freigabestufe, aber NICHT die Aktivierungsstufe:
der Kurzschlusszweig liest ausschliesslich tenantModuleActivation. Der
Vorgabezustand ist geschlossen (grantedIds.length === 0 liefert eine leere
Menge, ohne die Aktivierungsstufe ueberhaupt zu befragen). Die Schnittmenge
(D-02) filtert die aktiven Aktivierungen auf die freigegebenen Modulkennungen.
Alle drei Mandantenkennungen stammen aus dem Sitzungsnachweis (T-15-10) und
stehen bereits heute in jedem where — die Bindung AENDERT deshalb an keinem
heutigen Ergebnis etwas, sie zieht nur die Grenze ein zweites Mal, in der
Datenbank. Eine Bindung an der falschen Stufe kann daher nicht oeffnen; sie
kann nur schliessen. Die einzige Form, die oeffnen wuerde, waere eine Bindung
an den falschen Mandanten, und die ist ausgeschlossen, solange die Kennung aus
dem Sitzungsnachweis kommt. Das ist zu pruefen, nicht zu glauben — Aufgabe 2
nagelt es in Tests fest.
Befund K — der Bodensatz der Uebersichtszeile nach der Umstellung. Aus Befund A folgt rechnerisch: sieben ungebundene Rohtreffer (die Katalogzugriffe) und zehn gebundene. Diese Zahlen stehen hier als Erwartung, NICHT als Vorgabewert: die Pruefungen in Aufgabe 3 leiten sie zur Laufzeit erneut aus dem Quelltext ab und halten das Dokument gegen das Gemessene, nicht gegen diese Zeile.
Gemessene Ausgangslage (2026-09-10, HEAD f657e24):
npm --prefix apps/api run test meldet 810 Tests in 55 Dateien gruen (die
Aufgabenstellung nannte 54 Dateien — 55 ist der gemessene Wert);
node apps/api/scripts/rls-scratch-check.mjs meldet "Alle 53 Pruefungen
bestanden."; der Datenbankcontainer tessera-ctl-db-1 war unter 172.19.0.2
erreichbar (Adresse bei jedem Lauf neu ermitteln, nie abschreiben).
</planning_time_findings>
Aufgabe 1: Die Fehlerrichtung fuer diesen Bereich MESSEN und aufschreiben — an den echten, ausgelieferten Regeln Der Container `tessera-ctl-db-1` laeuft und ist ueber seine Container-Adresse erreichbar; die Adresse wird zur Laufzeit ermittelt, nicht aus diesem Plan abgeschrieben. apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md Diese Aufgabe ist der duenne, durchgehende Faden dieses Plans: sie beruehrt das Messwerkzeug, die echte Datenbank und die Kritikschrift und beweist damit end-to-end, dass die Aussage dieses Durchlaufs traegt, BEVOR eine Zeile Produktivcode umgestellt wird. Sie aendert keinen Produktivcode.Dreizehn neue Pruefungen in einem achten Abschnitt runModuleRegistryAreaChecks,
aufgerufen in main() NACH runUserAreaChecks und VOR
runTransactionShapeMeasurement. Der Abschnitt setzt auf den Tabellen auf, die
runGroupsAreaChecks bereits anlegt (Group, GroupMembership, ModuleGrant,
TenantModuleActivation, samt der wortgleich aus den ausgelieferten Migrationen
geschnittenen Regeln) — er legt sie NICHT neu an und tippt keine Regel nach.
Findet die Extraktion eine benoetigte Regel nicht, meldet der Abschnitt eine
FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Regel
weiterzumessen; das ist die Form, die runGroupsAreaChecks bereits vormacht.
Vorbereitend legt der Abschnitt ueber die Verwaltungsrolle an:
(a) eine Tabelle "Module" OHNE Zeilenschutz — die zu messende Eigenschaft
selbst, nach dem Muster der Tabelle "Tenant" im Abschnitt des Bereichs user,
mit zwei Katalogzeilen (mod-1, mod-2);
(b) den Eindeutigkeitsindex auf ("tenantId","moduleId") fuer
"TenantModuleActivation", wortgleich zur Schemadefinition — ohne ihn liesse
sich Pruefung 12 nicht messen;
(c) je eine Direkt-Freigabezeile pro Mandant (userId, kein groupId);
(d) die Mitgliedschaft (group-b, user-a), die runGroupsAreaChecks unter
gebundenem Kontext bewusst NICHT anlegen konnte (die Regel wies sie ab) — sie
wird hier ueber die Verwaltungsrolle gesetzt, weil Pruefung 6 sonst aus dem
falschen Grund bestuende: sie wuerde nichts ausschliessen, weil es nichts zu
finden gaebe. Der Aufbau muss so sein, dass Pruefung 6 FEHLSCHLUEGE, wenn die
Bindung nicht ausschloesse.
Die Zeile grant-foreign-group (Mandantenkennung TENANT-A, Gruppenkennung
group-b), die runGroupsAreaChecks bereits erfolgreich einfuegt, wird
wiederverwendet und nicht neu erzeugt.
Die dreizehn Pruefungen, je mit dieser Kennung, jede mit dem tatsaechlich beobachteten Wert im Meldetext (nicht mit einer Wiederholung der Erwartung):
modulegrant-ungebunden-null-zeilen— die Belegzeile dieses Abschnitts: der IDENTISCHE Lesezugriff auf"ModuleGrant"ohne vorherigesset_configliefert 0 Zeilen, nicht die tatsaechlich vorhandenen. Die beobachtete Zahl der tatsaechlich vorhandenen Zeilen gehoert in den Meldetext.tenantmoduleactivation-ungebunden-null-zeilen— dasselbe fuer die Aktivierungstabelle. Diese Tabelle ist die, die der Kurzschluss fuer ADMIN/SUPER_ADMIN als EINZIGE liest — hier faellt der Verwaltungszugriff aus.admin-kurzschluss-gebunden-liefert-nur-eigene-aktivierungen— gebunden an TENANT-A liefert die aktive Aktivierungsliste ausschliesslich A-Zeilen. Der Rollen-Kurzschluss kann durch die Bindung nicht ueber die Mandantengrenze hinaus wachsen.direktfreigabe-gebunden-nur-eigene-zeile— gebunden an TENANT-A liefert die Direkt-Freigabe-Abfrage ueber die Benutzerkennung nur die A-Zeile.gruppenpfad-gebunden-folgt-der-gruppenregel— gebunden an TENANT-A liefert der Drei-Tabellen-Weg (Freigabe ueber Gruppe ueber Mitgliedschaft) fueruser-agenau die A-Freigabe. Das bildet die verschachtelte Beziehungsabfrage des Dienstes nach.gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus— derselbe Weg, gebunden an TENANT-A, liefertgrant-foreign-groupNICHT, obwohl die Regel aufModuleGrantdiese Zeile nachweislich durchlaesst (T-JTS-03) und die Mitgliedschaft(group-b, user-a)vorhanden ist. Der Meldetext benennt T-JTS-03 und sagt, dass diese Verteidigung erst nach dem Scharfschalten greift.gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit— die Gegenmessung: derselbe Weg, ausgefuehrt ueber die Verwaltungsrolle mitBYPASSRLS, LIEFERT die fremde Zeile. Damit steht fest, dass der Ausschluss in Pruefung 6 von der Bindung kommt und nicht vom Aufbau. Praezedenzfall fuer diese Messform ueber die Wartungsrolle:user-fan-out-je-mandant-gebunden-liefert-alle-zeilen.module-tabelle-traegt-keinen-zeilenschutz— haelt zwei Quellen gegeneinander: der ungebundene Lesezugriff liefert BEIDE Katalogzeilen, UNDpg_class.relrowsecurityfuer"Module"istfalse. Die Aussage haengt damit nicht allein daran, dass dieser Abschnitt selbst keinen Zeilenschutz eingeschaltet hat.katalog-bindung-aendert-heute-nichts-an-der-ergebnismenge— gebunden an TENANT-A und ungebunden liefern identische Katalogzeilen. Das ist die Praezisierung aus Befund E, gemessen: die Nichtbindung des Katalogs ist heute keine Rettung vor Unsichtbarkeit, sondern eine Frage der Wahrhaftigkeit der Aufzeichnung — und wird erst dann zur Rettung, wenn Etappe 3 dieser Tabelle eine Regel gibt. Der Meldetext haelt genau diese Bedingung fest.gebundener-join-auf-den-katalog-liefert-den-modulnamen— gebunden an TENANT-A liefert der Verbund aus Aktivierung und Katalog die Aktivierungszeile MIT ihrem Modulnamen. Das bildet dieinclude-Form nach, diefindActiveForTenantbenutzt und diemodule-grants.service.tsbereits gebunden fuehrt: die Bindung verliert die Katalogseite nicht.aktivierung-gebundenes-einfuegen-fremder-mandant-abgelehnt— ein gebundenes Einfuegen unter TENANT-A mit fremder Mandantenkennung wird abgewiesen. Der Meldetext nennt die gemessene Fehlerklasse (Zeilenschutz-Ablehnung) und liest sie mit derselben Hilfsfunktion aus, die der Abschnitt des Bereichsuserdafuer eingefuehrt hat (sqlStateOf, wegen des generischen Prisma-Codes bei fehlgeschlagenem Rohzugriff).aktivierung-eindeutigkeit-traegt-den-mandanten-keine-unsichtbare-kollision— die Entlastung aus Befund H, gemessen: bei vorhandener, unter TENANT-A unsichtbarer Zeile(TENANT-B, mod-2)GELINGT das gebundene Einfuegen von(TENANT-A, mod-2). Der Meldetext stellt das ausdruecklich der Kette aus den Bereichentendersundusergegenueber und benennt sie namentlich, damit die Entlastung nicht als beilaeufig gelesen wird.freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung— eine Textmessung statt einer Datenbankmessung: die beiden partiellen Eindeutigkeitsindizes auf"ModuleGrant"werden aus der ausgelieferten Migration20260804130130_add_groups_and_module_grantsgelesen, und es wird geprueft, dass beide mit der Mandantenkennung beginnen. Damit gilt die Entlastung aus Pruefung 12 belegt auch fuer die Freigabetabelle, ohne dass beide partiellen Indizes im Wegwerf-Schema nachgebaut werden muessen. Findet die Extraktion die Indizes nicht, ist die Pruefung FEHLGESCHLAGEN, nicht uebersprungen.
TEIL 2, Beleg statt Behauptung fuer Befund B: die Anweisung aus Befund B wird
ausgefuehrt und ihre Ausgabe samt Rueckgabewert in die Kritikschrift
uebernommen, in derselben Form wie im Abschnitt des Bereichs user.
TEIL 3, Beleg statt Behauptung fuer Befund G: die Aufrufermessung fuer
isModuleActive und findActiveForTenant wird ausgefuehrt und ihre Ausgabe
uebernommen — sie traegt die Richtigstellung beider Kopfkommentare in Aufgabe 3.
Danach die Kritikschrift: docs/mandantentrennung-etappe2-fehlerrichtung.md
bekommt einen Abschnitt ## Bereich module-registry, gesetzt NACH dem Abschnitt
## Bereich user und VOR ## Verweis, mit denselben Unterabschnitten wie die
fuenf vorherigen Bereiche:
### (m1) Die Messung— die TATSAECHLICH beobachtete Ausgabe des Laufs (nicht eine abgeschriebene Erwartung), die Belegzeile namentlich benannt, dazu Teil 2 und Teil 3.### (m2) Signaltabelle je umgestelltem Pfad— je Pfad das Verhalten bei zu wenig Ergebnis und das konkrete Signal mit seinem Ort. Mindestens: die Aufloesung im Kurzschlusszweig, die Aufloesung im Benutzerzweig (Direktweg und Gruppenweg getrennt), die Schnittmengenabfrage, die Modulliste fuer die Seitenleiste, die Katalogflaggen fuer den Marktplatz, die Aktivierungsliste, das Aktivieren, das Deaktivieren, die Aktiv-Abfrage, sowie die zwei bewusst ungebundenen Katalogpfade als ausdrueckliche Grenze (nach dem Muster der RSS-Zeile im Abschnitttenders: sie beschreiben nicht heutiges Verhalten, sondern was eine KUENFTIGE Bindung anrichten wuerde).### (m3) Welcher Code Leere als Abwesenheit deutet— die namentliche Liste, nach Wirkung sortiert, MIT eigener Behandlung des Problems "leer heisst kein Recht" (siehe unten).### (m4) Was dieser Durchlauf bewusst nicht loest### (m5) Was dieser Durchlauf bewusst NICHT anfasst
Der Unterabschnitt (m3) traegt diesen Abschnitt und muss namentlich benennen:
den Rueckgabepunkt bei leerer Freigabemenge, den Kurzschlusszweig bei leerer
Aktivierungsliste, den Rueckgabepunkt bei leerer Modulliste, die Katalogflaggen
(deren Leere im Controller zu ZWEI falschen Flaggen wird und damit eine ANDERE
Unwahrheit erzeugt als der Sperrhinweis: "nicht aktiviert" statt "nicht
freigegeben"), die 403-Stelle im Waechter, die Aktiv-Abfrage mit ihrem
Vorgabewert false (heute ohne Aufrufer, morgen eine Falle), und — als
Gegenrichtung, damit der Abschnitt nicht nur aus Alarm besteht — die
Deaktivierungs-Ausnahme, die bei Leere LAUT wird.
Und dann die Frage, die dieser Bereich vor allen anderen beantworten muss: welches Signal unterscheidet "wirklich keine Freigabe" von "die Abfrage hat nichts gefunden"? Die Untersuchung dazu ist Teil dieser Aufgabe, das Ergebnis gehoert in den Text, und wenn die Antwort "keines" lautet, dann steht das so da — schlicht, ohne Beschoenigung, mit den drei Stellen, die heute identisch aussehen (dieselbe 403-Meldung, dieselbe leere Liste mit Status 200, kein Protokolleintrag), und mit dem Zusatz, der diesen Bereich von allen vorherigen unterscheidet: der Betroffene hat eine fertige, falsche Erklaerung zur Hand und meldet deshalb keinen Fehler.
Die eine Asymmetrie, die sich zu einem Signal machen LIESSE, gehoert benannt und
an Etappe 4 uebergeben, nicht hier geloest: nach dem Scharfschalten ist ein zu
kleines Ergebnis in diesem Bereich TOTAL und nicht selektiv — jeder Benutzer
jedes Mandanten verliert gleichzeitig jedes Modul, waehrend die Aktivierungs- und
Freigabetabellen weiterhin Zeilen halten. Die Vorabpruefung von Etappe 4
(rls-preflight.mjs) kann genau das feststellen: aktive Aktivierungszeilen
vorhanden, aber die Aufloesung liefert fuer einen bekannten Administrator eine
leere Menge. Eine Laufzeitwarnung an den genannten Stellen wird ERWOGEN und
VERWORFEN, mit derselben Begruendung wie bei getAllActiveConfigs im Bereich
ldap und den fuenf Stellen im Bereich tenders: eine leere Menge ist fuer
einen Benutzer ohne Freigabe und auf einer frischen Installation der
Normalzustand, eine Warnung waere Dauerlaerm und verloere ihr Signal.
In (m4) gehoeren mindestens: T-JTS-02 und T-JTS-03 bleiben aufgezeichnet und
ungefixt (Befund I — die Leseseite bekommt eine zweite Verteidigung, aber erst
nach dem Scharfschalten, und die Schreibseiten-Gegenpruefung bleibt deshalb der
heutige Schutz); die Regel fuer den Modulkatalog in Etappe 3 (Befund E, als
Bedingung formuliert); die offene Architekturfrage req.tenantPrisma, die auch
dieser Bereich nicht entscheidet. In (m5): der Waechter, der Controller, das
Frontend, Schema und Migrationen — jeweils mit der Feststellung, dass sie
geprueft und bewusst gelassen sind.
Nichts in dieser Aufgabe wird behauptet, was nicht in derselben Aufgabe gemessen
und mit dem Werkzeug festgeschrieben wurde. Eine Messung, die nicht eingecheckt
ist, ist keine Messung — das ist Fehler 2 der Fehlerliste dieses Vorhabens.
Lies zuerst apps/api/scripts/rls-scratch-check.mjs vollstaendig, mindestens
aber runGroupsAreaChecks (Aufbau der vier Tabellen und ihrer Regeln),
runUserAreaChecks (die Form fuer eine Tabelle OHNE Zeilenschutz und die
Hilfsfunktion zum Auslesen der Fehlerklasse), extractPolicySql,
readGroupsRlsPoliciesMigrationSql, readRemainingTenantTablesMigrationSql,
report, forTenantQuery, withAdminPrisma und main. Uebernimm ihre Form,
erfinde keine zweite.
Ergaenze runModuleRegistryAreaChecks(adminUrl, scratchRoleUrl, results) mit dem
in <behavior> beschriebenen Aufbau und den dreizehn dort namentlich
festgelegten Pruefungen, und rufe die Funktion in main() nach
runUserAreaChecks und vor runTransactionShapeMeasurement auf. Der Abschnitt
schneidet keine Regel neu — er benutzt die von runGroupsAreaChecks bereits
angelegten Tabellen und Regeln. Neu angelegt werden nur die Katalogtabelle ohne
Zeilenschutz, der Eindeutigkeitsindex auf der Aktivierungstabelle, die beiden
Direkt-Freigabezeilen und die Mitgliedschaft, die Pruefung 6 ueberhaupt erst
aussagekraeftig macht.
Fuehre den Lauf gegen die echte Datenbank aus (Container-Adresse zur Laufzeit
ermitteln) und uebernimm die TATSAECHLICHE Ausgabe in die Kritikschrift. Wenn
eine Pruefung anders ausfaellt als in <behavior> erwartet, ist das ein BEFUND
und wird als Befund aufgeschrieben — die Erwartung wird nicht nachtraeglich an
das Ergebnis angepasst und das Ergebnis nicht an die Erwartung.
Fuehre die Anweisungen aus TEIL 2 und TEIL 3 aus und uebernimm ihre Ausgabe samt Rueckgabewert woertlich.
Schreibe danach den Abschnitt ## Bereich module-registry in
docs/mandantentrennung-etappe2-fehlerrichtung.md, mit den fuenf
Unterabschnitten aus <behavior>. Setze ihn nach ## Bereich user und vor
## Verweis. Schreibe die vorhandenen Abschnitte NICHT um — sie beschreiben
korrekt den Stand zum Zeitpunkt ihrer Messung.
Aendere in dieser Aufgabe KEINEN Produktivcode: kein forTenant() in
apps/api/src/module-registry, keine Aenderung an Schema, Migrationen,
Compose- oder Beispiel-Umgebungsdateien.
Wenn eine der dreizehn Pruefungen aus einem Grund nicht messbar ist, den dieser
Plan nicht vorhergesehen hat, dann melde das als Befund und miss ersatzweise die
naechstliegende Eigenschaft — aber lasse die Pruefung nicht schweigend aus und
ersetze sie nicht durch eine Behauptung im Text.
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && 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 modulegrant-ungebunden-null-zeilen tenantmoduleactivation-ungebunden-null-zeilen admin-kurzschluss-gebunden-liefert-nur-eigene-aktivierungen direktfreigabe-gebunden-nur-eigene-zeile gruppenpfad-gebunden-folgt-der-gruppenregel gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit module-tabelle-traegt-keinen-zeilenschutz katalog-bindung-aendert-heute-nichts-an-der-ergebnismenge gebundener-join-auf-den-katalog-liefert-den-modulnamen aktivierung-gebundenes-einfuegen-fremder-mandant-abgelehnt aktivierung-eindeutigkeit-traegt-den-mandanten-keine-unsichtbare-kollision freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung; 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.$' && grep -q '^## Bereich module-registry$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in m1 m2 m3 m4 m5; do grep -qE "^### ($S) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && 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 docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
Das Wegwerf-Werkzeug meldet alle Pruefungen bestanden (die 53 bisherigen plus die dreizehn namentlich geprueften dieses Bereichs) mit Rueckgabewert 0; jede der dreizehn Kennungen steht einzeln als bestanden in der Ausgabe; die Belegzeile zur Unsichtbarkeit nennt in ihrem Meldetext die tatsaechlich vorhandene Zeilenzahl, die Zeile zur Katalogtabelle haelt Lesezugriff und Systemkatalog gegeneinander, die Zeile zum Gruppenpfad benennt T-JTS-03, und die Zeile zur Eindeutigkeit stellt die Entlastung den Bereichen tenders und user namentlich gegenueber; docs/mandantentrennung-etappe2-fehlerrichtung.md traegt einen Abschnitt ## Bereich module-registry mit allen fuenf Unterabschnitten, der tatsaechlich beobachteten Ausgabe, den Belegen aus TEIL 2 und TEIL 3, der Signaltabelle einschliesslich der zwei bewusst ungebundenen Katalogpfade als Grenze, der namentlichen Liste der Leere-als-Abwesenheit-Stellen samt Gegenrichtung, und einer ausdruecklichen, unbeschoenigten Antwort auf die Frage nach dem unterscheidenden Signal samt der an Etappe 4 uebergebenen Vorabpruefung und der begruendeten Verwerfung einer Laufzeitwarnung; npm --prefix apps/api run test meldet weiterhin 810 Tests gruen und die Typpruefung ist sauber; unter apps/api/src, in Schema, Migrationen sowie in Compose- und Beispiel-Umgebungsdateien ist nichts geaendert.
Muster fuer die Testdatei, wortgleich uebernommen aus
apps/api/src/groups/module-grants.service.spec.ts (dem bereits umgestellten
Nachbarn auf denselben Modellen), nicht neu erfunden:
- Die Erweiterung
../prisma/prisma-tenant.extensionwird gemockt, sodassforTenant(prisma, tenantId)anprisma.__makeBoundClient(tenantId)delegiert.withTenantTransactionwird NICHT gemockt und nicht gebraucht — dieser Bereich hat keine Transaktion (Befund B). - Der gebundene Klient ist ein ZWEITES, vom ungebundenen unterscheidbares Objekt
ueber DEMSELBEN Speicher, das protokolliert, welche Aufrufe ueber ihn liefen
(Modellname, Methodenname, Mandantenkennung). Ein reiner Identitaets-Mock
koennte einen vergessenen Bindungsaufruf nicht von einem ungebundenen
unterscheiden — das ist der Grund, aus dem der Bereich
ldapseinerzeit an dieser Stelle nichts bewies. - Alle 15 bestehenden Faelle der Datei bleiben inhaltlich erhalten. Der bestehende Nachbau filtert bereits auf die Schnittmengen-Bedingung und unterscheidet Direktweg von Gruppenweg; diese Logik wird ueber den gebundenen Klienten hinweg beibehalten, nicht ersetzt.
Neue Faelle in dieser Datei (Nachweis VOR Umbau, also zuerst rot):
- Der Kurzschlusszweig bindet seinen Aktivierungs-Lesezugriff an die Mandantenkennung aus dem Sitzungsnachweis.
- Der Benutzerzweig bindet BEIDE Freigabe-Lesezugriffe (Direktweg und Gruppenweg) UND den Schnittmengen-Lesezugriff, und ALLE gebundenen Aufrufe einer Aufloesung tragen DIESELBE Mandantenkennung — nie zwei verschiedene in derselben Aufloesung.
- Der Vorgabezustand bleibt geschlossen und ueberlebt die Bindung: ein Benutzer ohne jede Freigabe erhaelt eine leere Menge, UND der Schnittmengen-Lesezugriff wird gar nicht erst ausgefuehrt (das Protokoll enthaelt ihn nicht). Diese Reihenfolge ist eine Eigenschaft, die nicht verloren gehen darf.
- Der Rollen-Kurzschluss waechst durch die Bindung nicht: eine Aufloesung fuer einen zweiten Mandanten leitet keine Module des ersten ab, und die gebundene Kennung im Protokoll ist die des zweiten.
- Die Modulliste fuer die Seitenleiste erreicht den Katalog ueber den UNGEBUNDENEN Klienten: der Katalog-Lesezugriff taucht im Bindungsprotokoll NICHT auf, waehrend er auf dem ungebundenen Nachbau nachweislich stattfindet. Das ist der Testfall, der jemanden erwischt, der spaeter den Katalog bindet.
- Die Katalogflaggen binden ihren eigenen Aktivierungs-Lesezugriff, und die
darin geschachtelte Aufloesung erzeugt ihren eigenen gebundenen Klienten mit
derselben Mandantenkennung — dieselbe Konvention, die
module-grants.service.tsmit seiner Mandanten-Gegenpruefung bereits vormacht (eine geschachtelte Methode erzeugt ihren eigenen Klienten, gebundene Klienten werden nicht zwischen Methoden weitergereicht).
In module.guard.spec.ts kommt genau EIN Fall dazu, und er hat einen anderen
Zweck als alle uebrigen: er nagelt die ABWESENHEIT eines unterscheidenden
Signals fest. Zwei Aufrufe des Waechters — einer, bei dem die Aufloesung eine
leere Menge liefert, weil der Benutzer tatsaechlich keine Freigabe hat, und
einer, bei dem sie eine leere Menge liefert, obwohl es Freigaben gibt — muessen
heute dieselbe Ausnahme mit derselben Meldung ergeben. Der Test haelt beide
Meldungen gegeneinander und ist damit die maschinelle Fassung des Satzes aus
Aufgabe 1: es gibt heute kein Signal. Sein Kopfkommentar sagt ausdruecklich, dass
dieser Test rot werden SOLL, sobald jemand ein Signal einbaut — und dass dann
sowohl er als auch der Abschnitt (m3) der Kritikschrift nachzuziehen sind.
Erst danach die Umstellung von module-access.service.ts:
- In der Aufloesung wird EIN gebundener Klient erzeugt, unter dem Namen
tenantPrisma, vor der Rollenverzweigung, und von allen vier mandantengebundenen Zugriffen dieser Methode benutzt (Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge). Nicht ein Klient je Modellzugriff. - Die bestehenden
where-Filter mit der Mandantenkennung bleiben ALLE stehen. Sie sind nicht redundant, sondern das zweite Netz — dieselbe Begruendung, diemodule-grants.service.tsfuer seine Filter bereits im Code fuehrt, mit dem Verweis auf die Messung, dass die Regel auf der Mitgliedschaftstabelle nur die Gruppenseite prueft (T-JTS-02) und die Regel auf der Freigabetabelle nur die Mandantenkennung der Zeile (T-JTS-03). - Die Modulliste fuer die Seitenleiste erzeugt fuer ihren Katalogzugriff KEINEN gebundenen Klienten. An dieser Stelle steht ein Kommentar, der die Nichtbindung begruendet — mit der Messung aus Aufgabe 1 (die Tabelle traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos) UND mit der Bedingung, unter der die Entscheidung neu zu treffen waere (sobald Etappe 3 dieser Tabelle eine Regel gibt, verschwaende der Katalog fuer jeden Mandanten). Die Bedingung wird als Bedingung formuliert, nicht als heutige Tatsache.
- Die Katalogflaggen erzeugen ihren eigenen gebundenen Klienten fuer den
Aktivierungs-Lesezugriff; die geschachtelte Aufloesung erzeugt weiterhin ihren
eigenen. Beide laufen wie bisher nebenlaeufig — das ist genau die Form, die
module-grants.service.tsmit drei bzw. vier parallelen gebundenen Teilabfragen bereits faehrt.
Kommentar-Hygiene, mit Praezedenzfall — gilt fuer Aufgabe 2 UND Aufgabe 3.
Die Verifikation beider Aufgaben zaehlt und sucht im Quelltext nach
Zugriffsformen, und beide Zaehlungen koennen von einem Kommentar verfaelscht
werden, der eine solche Form in Code-Schreibweise zitiert. Zwei Formen sind
betroffen: der Name des gebundenen Klienten unmittelbar gefolgt vom Namen des
Katalogmodells (Negativ-Gate: darf im Quelltext ueberhaupt nicht vorkommen), und
der Name des ungebundenen Klienten unmittelbar gefolgt vom Namen eines Modells
(Zaehl-Gate: die Zahl muss exakt der Zahl der tatsaechlichen Katalogzugriffe
entsprechen). Formuliere jede Begruendung deshalb in WORTEN — "der Katalogzugriff
laeuft bewusst ueber den ungebundenen Klienten" — und niemals in
Code-Schreibweise, auch nicht in einem Kommentar, auch nicht als Beispiel.
Praezedenzfall im Projekt: in tenders.controller.ts wurde die
Kommentarformulierung aus genau diesem Grund umgestellt. Schlaegt ein Gate an
einer Begruendung statt am Code an, ist die Begruendung umzuformulieren, nicht
das Gate abzuschwaechen.
Falsifizierungsnachweis, Pflicht und im SUMMARY festzuhalten (Fehler 5 der
Fehlerliste): baue nach der Umstellung EINEN Bindungsaufruf probeweise zurueck —
den Gruppenweg der Freigabe-Aufloesung, weil er der Pfad ist, dessen Ausfall die
groesste Wirkung haette — halte fest, welcher Test dadurch rot wird und mit
welcher Meldung, nimm den Rueckbau zurueck und halte fest, dass derselbe Lauf
danach wieder gruen ist. Ohne diesen Nachweis ist nicht belegt, dass die neuen
Tests ueberhaupt etwas pruefen.
Lies apps/api/src/groups/module-grants.service.spec.ts (Attrappen-Aufbau,
__makeBoundClient, Bindungsprotokoll, die Hilfsfunktion zum Pruefen eines
gebundenen Aufrufs, der Block der Bindungstests am Dateiende) und
apps/api/src/module-registry/module-access.service.spec.ts vollstaendig, bevor
du etwas aenderst.
Baue module-access.service.spec.ts auf den Zwei-Klienten-Nachweis um und
ergaenze die in <behavior> genannten neuen Faelle. Fuehre den Lauf aus und
halte fest, welche der neuen Faelle VOR der Umstellung rot sind — das ist der
Nachweis, dass sie etwas pruefen.
Ergaenze in module.guard.spec.ts den einen Fall aus <behavior> samt seinem
Kopfkommentar.
Stelle danach module-access.service.ts nach <behavior> um. Halte dich an die
Konvention des bereits umgestellten Nachbarn module-grants.service.ts: ein
Klient je Methode unter dem Namen tenantPrisma, bestehende where-Filter
bleiben stehen, geschachtelte Methoden erzeugen ihren eigenen Klienten.
Fuehre den Falsifizierungsnachweis aus <behavior> durch und halte sein
Ergebnis fuer das SUMMARY fest.
Fasse in dieser Aufgabe module-registry.service.ts NICHT an — sie gehoert
vollstaendig zu Aufgabe 3. Fasse apps/api/src/groups, apps/api/src/dashboard,
apps/api/src/auth, apps/api/src/ldap, apps/api/src/user, Schema,
Migrationen, Compose- und Beispiel-Umgebungsdateien an keiner Stelle an.
Wenn die Umstellung eine Signatur oder eine Aufrufstelle ausserhalb dieser drei
Dateien erzwingt, dann ist das ein Befund: halte ihn fest, entscheide begruendet,
und schreibe die Abweichung ins SUMMARY, statt sie stillschweigend zu machen.
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 && npm --prefix apps/api run test && npm --prefix apps/api run type-check && grep -q '__makeBoundClient' apps/api/src/module-registry/module-access.service.spec.ts && grep -q "vi.mock('../prisma/prisma-tenant.extension'" apps/api/src/module-registry/module-access.service.spec.ts && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/module-registry/module-access.service.ts && BOUND=$(grep -o "tenantPrisma.[a-zA-Z]." apps/api/src/module-registry/module-access.service.ts | wc -l | tr -d ' ') && { test "$BOUND" -ge 5 || { echo "BINDUNG: nur $BOUND gebundene Modellzugriffe in module-access.service.ts, erwartet mindestens 5"; exit 1; }; } && U=$(grep -o "this.prisma.[a-zA-Z]" apps/api/src/module-registry/module-access.service.ts | wc -l | tr -d ' ') && { test "$U" -eq 1 || { echo "KATALOG: $U ungebundene Rohtreffer in module-access.service.ts, erwartet genau 1 (nur der Katalogzugriff)"; exit 1; }; } && { if grep -qE 'tenantPrisma.module.' apps/api/src/module-registry/module-access.service.ts; then echo "KATALOG WURDE GEBUNDEN oder die Begruendung nennt die verbotene Zeichenfolge woertlich"; exit 1; fi; } && npm --prefix apps/api run test -- src/module-registry/module-access.service.spec.ts src/module-registry/module.guard.spec.ts && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/groups apps/api/src/dashboard apps/api/src/auth apps/api/src/ldap apps/api/src/user apps/api/src/module-registry/module-registry.service.ts apps/api/src/module-registry/module.guard.ts apps/api/src/module-registry/module-registry.controller.ts docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"
module-access.service.spec.ts nutzt den Zwei-Klienten-Nachweis ueber __makeBoundClient, alle 15 bestehenden Faelle sind inhaltlich erhalten, und die in <behavior> genannten neuen Faelle sind vorhanden und gruen — einschliesslich des Falls, der belegt, dass der Katalogzugriff NICHT im Bindungsprotokoll auftaucht; module.guard.spec.ts traegt den Fall, der die Abwesenheit eines unterscheidenden Signals festnagelt, samt Kopfkommentar zu seinem Zweck; in module-access.service.ts laufen der Kurzschlusszweig, beide Freigabewege, die Schnittmenge und der Aktivierungszugriff der Katalogflaggen ueber forTenant() mit dem Namen tenantPrisma, ein Klient je Methode, und ausschliesslich der Katalogzugriff bleibt ungebunden — mit einem Kommentar, der die Messung aus Aufgabe 1 UND die Bedingung fuer eine kuenftige Neubewertung nennt, ohne die vom Gate gesuchte Zeichenfolge zu enthalten; alle bestehenden where-Filter mit der Mandantenkennung stehen weiterhin und tragen die Begruendung (T-JTS-02/T-JTS-03), warum sie nicht redundant sind; die Testzahl liegt bei mindestens 810 und der Lauf ist gruen, die Typpruefung ist sauber, das Wegwerf-Werkzeug meldet weiterhin alle Pruefungen bestanden; der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und im SUMMARY mit Testnamen und Fehlermeldung festgehalten; module-registry.service.ts, der Waechter, der Controller, die Bereiche groups/dashboard/auth/ldap/user, Schema, Migrationen sowie Compose- und Beispiel-Umgebungsdateien sind unveraendert.
Faelle der neuen Testdatei:
- Die Aktivierungsliste bindet ihren Lesezugriff an den uebergebenen Mandanten
UND liefert die Katalogseite weiterhin mit (die verbundene Modellform
ueberlebt die Bindung) — belegt in Aufgabe 1 durch
gebundener-join-auf-den-katalog-liefert-den-modulnamen. - Die Aktivierungsliste eines zweiten Mandanten liefert keine Zeile des ersten.
- Das Aktivieren erreicht die Katalog-Existenzpruefung UNGEBUNDEN und schreibt die Aktivierung GEBUNDEN, an den uebergebenen Mandanten.
- Das Aktivieren wirft die Nicht-gefunden-Ausnahme fuer eine unbekannte Modulkennung, und zwar BEVOR irgendein gebundener Schreibzugriff im Protokoll steht — die Reihenfolge ist Teil der Eigenschaft.
- Das Deaktivieren bindet alle DREI Aktivierungszugriffe (Lesen der Aktivierung, Schreiben, sowie die Existenzpruefung der Aktivierung) an DENSELBEN Mandanten, ueber EINEN Klienten.
- Das Deaktivieren wirft die Nicht-gefunden-Ausnahme, wenn keine Aktivierung
vorliegt — die LAUTE, harmlose Richtung dieses Bereichs, hier ausdruecklich
als solche festgenagelt, damit sie bei einem spaeteren Umbau nicht
versehentlich in ein stilles
falseverwandelt wird. - Die Aktiv-Abfrage bindet ihren Aktivierungs-Lesezugriff und erreicht den
Katalog ungebunden; ohne Aktivierung liefert sie
false— die STILLE Richtung, ebenfalls festgenagelt, mit einem Kommentar, der sie als solche benennt. - Kein Katalogzugriff des Dienstes — weder die Gesamtliste, noch die Kennzeichen-Suche, noch die Existenzpruefungen, noch die Katalogpflege beim Start — taucht im Bindungsprotokoll auf. Das ist derselbe Wachhund wie in Aufgabe 2, hier fuer sechs Zugriffe statt fuer einen.
Erst danach die Umstellung von module-registry.service.ts, je Methode ein
Klient unter dem Namen tenantPrisma:
- Aktivierungsliste: Lesezugriff GEBUNDEN, verbundene Modellform bleibt.
- Aktivieren: Katalog-Existenzpruefung UNGEBUNDEN, Aktivierungsschreibzugriff GEBUNDEN.
- Deaktivieren: Katalog-Existenzpruefung UNGEBUNDEN, beide Aktivierungszugriffe GEBUNDEN, ueber einen Klienten.
- Aktiv-Abfrage: Katalogsuche UNGEBUNDEN, Aktivierungs-Lesezugriff GEBUNDEN.
- Gesamtliste, Kennzeichen-Suche, Katalogpflege beim Start: UNGEBUNDEN, mit derselben Begruendungsform wie in Aufgabe 2 (Messung plus Bedingung), und bei der Katalogpflege zusaetzlich mit dem Grund, den nur sie hat: sie laeuft beim Anwendungsstart aus vier Seed-Dateien, ohne Anfrage und ohne Mandanten — ein gebundener Aufruf haette dort strukturell keinen Kontext.
- Die Kennzeichen-Suche bekommt zusaetzlich einen Satz, den keine andere Katalogstelle braucht: sie ist die Stelle, die der Waechter bei JEDER Modulanfrage aufruft, und eine Bindung wuerde jede Modulanfrage mit einer Meldung abweisen, die faelschlich von einer fehlenden Aktivierung spricht.
Die zwei Richtigstellungen (Befund G), mit der Messung aus Aufgabe 1 belegt: der Kopfkommentar der Aktiv-Abfrage behauptet, der Waechter benutze sie — er tut es nicht, sie hat null Aufrufer. Der Kopfkommentar der Aktivierungsliste wird vom Controller-Kommentar als Lieferant des Marktplatz-Katalogs benannt — auch sie hat null Aufrufer, der Marktplatz laeuft ueber die Katalogflaggen. Beide Kommentare werden richtiggestellt und BEIDE Methoden trotzdem umgestellt, mit dem ausgeschriebenen Grund: heute toter, ungebunden gelassener Code ist die Falle fuer den, der ihn morgen verdrahtet. Der irrefuehrende Satz im Kopfkommentar des Controllers wird NICHT mitgeaendert — der Controller ist nicht Teil dieses Plans; stattdessen wird die Feststellung in (m5) der Kritikschrift aufgenommen, damit sie nicht verloren geht.
Die Dokumente. In docs/mandantentrennung-zugriffsklassifikation.md:
- Fuenf Bestandsaufnahme-Zeilen. Die drei Aktivierungs-/Freigabe-Zeilen wechseln
auf
gebunden, mit Begruendung im Stil der bereits umgestellten Bereiche (Quick-Kennung, was umgestellt wurde, und warum die anwendungsseitigen Filter stehen bleiben). Die zwei Katalog-Zeilen BLEIBEN aufungebunden, bekommen aber eine Begruendung, die den Unterschied zwischen "noch offen" und "bewusst und dauerhaft so" traegt: die Messung aus Aufgabe 1 (keine Regel auf der Tabelle), der Grund fuer die Nichtbindung (Wahrhaftigkeit der Aufzeichnung), und die Bedingung, unter der neu zu entscheiden waere (eine Regel in Etappe 3). Die Stand-Spalte kennt nur drei Werte — die Unterscheidung MUSS deshalb in der Begruendung stehen, sie kann nirgends sonst stehen. - Der Abschnitt zur Hintergrunddienst-Falle: dieser Bereich fuegt KEINEN sechsten Fall hinzu, und das gehoert gemessen statt angenommen — die Katalogpflege beim Start schreibt zwar ohne Mandantenkontext, iteriert aber ueber nichts je Mandant und hat damit nicht die Bauform der fuenf gefuehrten Faelle. Dieser Satz kommt in den Abschnitt, mit Begruendung, damit die Abwesenheit belegt ist und nicht wie ein Uebersehen aussieht.
- Uebersichtszeile, Summenzeile, Klassen-Verteilung samt Ueberschrift: alle vier Zahlen werden am Code bzw. an der Bestandsaufnahme NEU ERMITTELT und eingetragen. Die Erwartung aus Befund K (sieben ungebunden, zehn gebunden) wird NICHT abgeschrieben — sie wird gemessen und darf abweichen; weicht sie ab, ist die Abweichung ein Befund fuers SUMMARY.
In docs/mandantentrennung-etappe2-fehlerrichtung.md kommt ans Ende des
Abschnitts ## Bereich module-registry ein Nachtrag im Stil der vorherigen
Bereiche: der Text darueber wird NICHT umgeschrieben, der Nachtrag haelt fest,
was Aufgabe 2 und 3 tatsaechlich umgesetzt haben, haelt die tatsaechlich
umgestellten Pfade gegen die in (m2) angekuendigten, und nennt BEIDE
Falsifizierungsnachweise (aus Aufgabe 2 und aus dieser Aufgabe) mit Testname und
Fehlermeldung. Ein Nachweis, der nur in einer Commit-Meldung steht, gilt nicht
als festgehalten — das ist Fehler 5 der Fehlerliste.
In .planning/WINDOWS.md kommt ein neuer offener Eintrag der Klasse
deviation auf apps/api/src/module-registry/module-access.service.ts: es gibt
heute kein Signal, das "wirklich keine Freigabe" von "die Abfrage hat nichts
gefunden" unterscheidet; nach dem Scharfschalten ist der Ausfall total und
lautlos und sieht fuer den Betroffenen wie ein absichtlicher Entzug aus; die
konkrete Vorabpruefung fuer Etappe 4 (aktive Aktivierungszeilen vorhanden, aber
die Aufloesung liefert fuer einen bekannten Administrator eine leere Menge) ist
benannt; die Laufzeitwarnung ist erwogen und begruendet verworfen. Die Datei
fuehrt den Bestand ZWEIMAL — als Tabelle und als JSON-Block; beide muessen den
neuen Eintrag tragen, sonst ist der Bestand in sich widerspruechlich.
Falsifizierungsnachweis, Pflicht und im SUMMARY festzuhalten: baue einen
Bindungsaufruf dieser Aufgabe probeweise zurueck — den Schreibzugriff des
Deaktivierens, weil er der einzige gebundene Schreibzugriff des Bereichs mit
sichtbarer Wirkung ist — halte fest, welcher Test rot wird und mit welcher
Meldung, nimm den Rueckbau zurueck, halte fest, dass derselbe Lauf danach wieder
gruen ist.
Lies apps/api/src/module-registry/module-registry.service.ts vollstaendig und
die Attrappen-Form aus apps/api/src/groups/module-grants.service.spec.ts
erneut, falls sie nicht mehr im Kontext ist.
Lege apps/api/src/module-registry/module-registry.service.spec.ts neu an, mit
den in <behavior> genannten Faellen, und fuehre den Lauf aus, BEVOR du den
Dienst umstellst — halte fest, welche Faelle rot sind.
Stelle danach module-registry.service.ts nach <behavior> um, mit denselben
Konventionen wie in Aufgabe 2 (ein Klient je Methode unter dem Namen
tenantPrisma, bestehende Filter bleiben). Der Absatz zur Kommentar-Hygiene aus
Aufgabe 2 gilt hier unveraendert und ist hier sogar strenger wirksam: diese Datei
hat sechs Katalogzugriffe, und das Zaehl-Gate verlangt genau diese sechs — eine
Begruendung in Code-Schreibweise wuerde die Zaehlung verfaelschen.
Stelle beide Kopfkommentare aus Befund G richtig, mit Bezug auf die Aufrufermessung aus Aufgabe 1.
Ziehe danach docs/mandantentrennung-zugriffsklassifikation.md nach: fuenf
Bestandsaufnahme-Zeilen, der Abschnitt zur Hintergrunddienst-Falle, und alle
Handzaehlungen. Ermittle die Rohtrefferzahlen fuer die Uebersichtszeile mit den
im Dokument selbst genannten Messanweisungen NEU, statt sie abzuschreiben, und
trage die gemessenen Werte ein. Laufe danach die maschinelle Pruefung
rls-access-inventory.spec.ts und ziehe nach, was sie beanstandet — sie ist die
Autoritaet fuer die Bestandsaufnahme, nicht dieser Plan.
Ergaenze den Nachtrag in der Kritikschrift und den neuen Eintrag in
.planning/WINDOWS.md (Tabelle UND JSON-Block).
Fuehre den Falsifizierungsnachweis aus <behavior> durch und halte sein
Ergebnis fuer das SUMMARY fest.
Fasse apps/api/src/groups, apps/api/src/dashboard, apps/api/src/auth,
apps/api/src/ldap, apps/api/src/user, den Waechter, den Controller, Schema,
Migrationen sowie Compose- und Beispiel-Umgebungsdateien an keiner Stelle an.
DATABASE_URL bleibt unveraendert auf der Rolle tessera — das Scharfschalten
ist Etappe 4.
Weicht eine der vier Handzaehlungen von der Erwartung aus Befund K ab, dann ist
die Messung massgeblich und die Abweichung ein Befund fuers SUMMARY — nicht
umgekehrt.
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 && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && test -f apps/api/src/module-registry/module-registry.service.spec.ts && grep -q '__makeBoundClient' apps/api/src/module-registry/module-registry.service.spec.ts && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/module-registry/module-registry.service.ts && CU=$(grep -o "this.prisma.[a-zA-Z]" apps/api/src/module-registry/module-registry.service.ts | wc -l | tr -d ' ') && { test "$CU" -eq 6 || { echo "KATALOG: $CU ungebundene Rohtreffer in module-registry.service.ts, erwartet genau 6 (nur die Katalogzugriffe)"; exit 1; }; } && for F in apps/api/src/module-registry/module-access.service.ts apps/api/src/module-registry/module-registry.service.ts; do if grep -qE 'tenantPrisma.module.' "$F"; then echo "KATALOG WURDE GEBUNDEN oder die Begruendung nennt die verbotene Zeichenfolge woertlich: $F"; exit 1; fi; done && test 0 -eq "$(grep -rn '$transaction(' apps/api/src/module-registry --include=.ts | grep -v spec | grep -vE '^[^:]:[0-9]+::space:(*|//|/*)' | wc -l | tr -d ' ')" && grep -qE '^| apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | gebunden |' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden |' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden |' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden |' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^| apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden |' docs/mandantentrennung-zugriffsklassifikation.md && U=$(grep -ro "this.prisma.[a-zA-Z]" apps/api/src/module-registry | grep -v spec | wc -l | tr -d ' ') && B=$(grep -ro "tenantPrisma.[a-zA-Z]." apps/api/src/module-registry | grep -v spec | wc -l | tr -d ' ') && { test "$U" -lt 17 || { echo "UEBERSICHTSZEILE: ungebundene Rohtreffer in apps/api/src/module-registry sind $U, also nicht gesunken — es wurde nichts umgestellt"; exit 1; }; } && { test "$B" -gt 0 || { echo "UEBERSICHTSZEILE: gebundene Rohtreffer in apps/api/src/module-registry sind $B"; exit 1; }; } && { grep -qE "^| module-registry | ${U} | ${B} | **war 17/0**" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE module-registry nennt nicht die neu gemessenen Zahlen ${U}/${B} im etablierten Stil"; 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 && awk -F'|' '$2 ~ /^ *apps/api/src// { k=$4; gsub(/^ +| +$/,"",k); cls[k]++; pairs++ } $2 ~ /^ *(muss-mandantengebunden|keine-mandantengebundene-tabelle|beides|bewusst-uebergreifend) *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *$/ { k=$2; gsub(/^ +| +$/,"",k); v=$3; gsub(/[^0-9]/,"",v); tab[k]=v+0; tn++ } $2 ~ /^ ***Summe** *$/ && $4 ~ /^ *$/ { v=$3; gsub(/[^0-9]/,"",v); tsum=v+0; tseen=1 } /^## Klassen-Verteilung/ { h=$0; gsub(/[^0-9]/,"",h); hp=h+0; hseen=1 } END { if (tn+0 != 4 || !tseen || !hseen) { print "KLASSEN-VERTEILUNG nicht erkannt: Klassenzeilen " tn ", Summenzeile " tseen ", Ueberschrift " hseen; exit 1 } if (tsum != pairs+0) { print "KLASSEN-SUMME stimmt nicht: Bestandsaufnahme hat " pairs " Paare, Tabellensumme nennt " tsum; exit 1 } if (hp != pairs+0) { print "UEBERSCHRIFT der Klassen-Verteilung nennt " hp " Paare, Bestandsaufnahme hat " pairs; exit 1 } s=0; for (k in tab) { if (tab[k] != cls[k]+0) { print "KLASSE " k ": Tabelle nennt " tab[k] ", Bestandsaufnahme zaehlt " cls[k]+0; exit 1 } s+=tab[k] } if (s != pairs+0) { print "KLASSENZEILEN ergeben " s ", Bestandsaufnahme hat " pairs; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk 'BEGIN{split("ein zwei drei vier x sechs sieben acht neun",w," "); w[5]="fünf"} /^## Der Hintergrunddienst als Falle/{seen=1; head=$0; f=1; next} /^## /{f=0} f && /^- **/{n++} f && /^\*\*Der .* Fall, anderer Bauart/{n++} f && /module-registry/{m=1} END{ if(!seen){print "ABSCHNITT Hintergrunddienst nicht gefunden"; exit 1} want="## Der Hintergrunddienst als Falle — " w[n] " Fälle"; if(head != want){printf "HINTERGRUNDDIENST-UEBERSCHRIFT nennt \"%s\", gezaehlt wurden %d Faelle, erwartet \"%s\"\n", head, n, want; exit 1} if(!m){print "ABSCHNITT Hintergrunddienst nennt diesen Bereich nicht — die Abwesenheit eines sechsten Falls ist nicht belegt"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && python3 -c " import re,sys s=open('.planning/WINDOWS.md',encoding='utf-8').read() rows=[l for l in s.splitlines() if re.match(r'^\| \d+ \|', l)] ids=re.findall(r'\"id\": (\d+),', s) if len(rows)!=len(ids): print('WINDOWS: %d Tabellenzeilen, %d JSON-Eintraege' % (len(rows), len(ids))); sys.exit(1) if not any(re.match(r'^\| 23 \|', l) for l in rows): print('WINDOWS: Eintrag 23 fehlt in der Tabelle'); sys.exit(1) if '23' not in ids: print('WINDOWS: Eintrag 23 fehlt im JSON-Block'); sys.exit(1) if 'module-registry' not in s: print('WINDOWS: kein Eintrag nennt diesen Bereich'); sys.exit(1) " && grep -q 'Nachtrag (260910-exd' docs/mandantentrennung-etappe2-fehlerrichtung.md && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/groups apps/api/src/dashboard apps/api/src/auth apps/api/src/ldap apps/api/src/user apps/api/src/module-registry/module.guard.ts apps/api/src/module-registry/module-registry.controller.ts docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"</automated> </verify> <done>apps/api/src/module-registry/module-registry.service.spec.tsexistiert, nutzt den Zwei-Klienten-Nachweis und deckt die ingenannten Faelle ab, einschliesslich der beiden ausdruecklich festgenagelten Richtungen (die laute Nicht-gefunden-Ausnahme beim Deaktivieren, das stillefalseder Aktiv-Abfrage) und des Wachhunds, der jeden der sechs Katalogzugriffe aus dem Bindungsprotokoll heraushaelt; inmodule-registry.service.tslaufen die fuenf Aktivierungszugriffe ueberforTenant()unter dem NamentenantPrisma, ein Klient je Methode, und die sechs Katalogzugriffe bleiben ungebunden — jeder mit einer Begruendung, die Messung und Bedingung trennt, die Katalogpflege zusaetzlich mit ihrem eigenen Startgrund und die Kennzeichen-Suche zusaetzlich mit der Folge, die eine Bindung fuer jede Modulanfrage haette; beide falschen Kopfkommentare sind mit Bezug auf die Aufrufermessung richtiggestellt und beide Methoden trotzdem umgestellt, mit ausgeschriebenem Grund; der Testlauf ist gruen mit mindestens 810 Tests, die Typpruefung sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; rls-access-inventory.spec.tslaeuft gruen und stimmt mit den fuenf Bestandsaufnahme-Zeilen des Bereichs ueberein; ALLE FUENF handgepflegten Stellen sind nachgezogen UND einzeln maschinell gegatet — die Uebersichtszeile gegen die am Code neu ermittelten Rohtrefferzahlen, die Summenzeile gegen die Addition der zwoelf Bereichszeilen, die Klassen-Verteilung gegen die ueber die Bestandsaufnahme nachgezaehlten Klassen samt Summe, die Ueberschrift der Klassen-Verteilung gegen dieselbe Paarzahl, und der Abschnitt zur Hintergrunddienst-Falle gegen die aus seinen eigenen Faellen abgeleitete Zahl im Ueberschriftstext PLUS den Beleg, dass dieser Bereich dort ausdruecklich genannt ist; keine dieser fuenf Pruefungen kann von einer stehen gebliebenen Fassung bestanden werden; die Kritikschrift traegt den Nachtrag mit den tatsaechlich umgesetzten Pfaden und BEIDEN Falsifizierungsnachweisen samt Testname und Fehlermeldung;.planning/WINDOWS.mdtraegt den neuen offenen Eintrag in Tabelle UND JSON-Block, mit der benannten Vorabpruefung fuer Etappe 4 und der begruendeten Verwerfung einer Laufzeitwarnung; der Waechter, der Controller, die Bereichegroups/dashboard/auth/ldap/user, Schema, Migrationen sowie Compose- und Beispiel-Umgebungsdateien sind unveraendert und DATABASE_URLzeigt weiterhin auf die Rolletessera`.
<threat_model>
Konfiguriert: ASVS-Stufe 1, blockierend ab high.
Trust Boundaries
| Boundary | Description |
|---|---|
| Browser/Benutzer → Modul-API | Mandantenkennung, Benutzerkennung und Rolle stammen ausschliesslich aus dem validierten Sitzungsnachweis (request.user, gesetzt von der JWT-Pruefstrategie ueber das httpOnly-Cookie), nie aus Koerper oder Parametern (T-03-04/T-15-10). Alles jenseits dieser Grenze ist nicht vertrauenswuerdig. |
| Benutzer → Modul, das sein Mandant nicht aktiviert hat | Die Aktivierungsstufe. Heute ALLEIN vom Anwendungscode gezogen (where mit der Mandantenkennung), nach diesem Plan zusaetzlich von der Datenbank — aber erst wirksam nach Etappe 4. |
| Benutzer → Modul, das ihm nicht freigegeben ist | Die Freigabestufe, Vorgabezustand geschlossen. ADMIN und SUPER_ADMIN umgehen sie ausdruecklich (D-03), aber NICHT die Aktivierungsstufe. |
| Mandant A → Freigabe- und Aktivierungszeilen des Mandanten B | Die Grenze, um die es in diesem Plan geht. |
| Freigabezeile → referenzierte Gruppe eines fremden Mandanten | Die Grenze, die die Regel auf der Freigabetabelle NACHWEISLICH nicht zieht (T-JTS-03). Heute allein von der Schreibseiten-Gegenpruefung in module-grants.service.ts gezogen. |
| API → PostgreSQL | Die Zeilenschutz-Grenze. Heute wirkungslos, weil die Rolle BYPASSRLS traegt (WINDOWS #18) — dieser Plan bereitet sie vor, schaltet sie NICHT scharf. |
| API → plattformweiter Modulkatalog | Eine bewusst durchlaessige Grenze: der Katalog gehoert keinem Mandanten und traegt heute keinen Zeilenschutz. |
| Anwendungsstart → Datenbank | Die Katalogpflege laeuft ohne Anfrage, ohne Sitzungsnachweis und ohne Mandanten. |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-EXD-01 | Elevation of Privilege | module-access.service.ts, Aktivierungsstufe |
high | mitigate | Ein Benutzer des Mandanten A erhaelt Zugriff auf ein nur fuer B aktiviertes Modul. Beide Aktivierungs-Lesezugriffe der Aufloesung werden gebunden; der where-Filter mit der Mandantenkennung bleibt zusaetzlich stehen. Gemessen in Aufgabe 1: admin-kurzschluss-gebunden-liefert-nur-eigene-aktivierungen. |
| T-EXD-02 | Tampering | module-registry.service.ts, Aktivieren/Deaktivieren |
high | mitigate | Eine Aktivierung wird unter dem falschen Mandanten geschrieben oder gelesen. Alle fuenf Aktivierungszugriffe gebunden, je Methode EIN Klient. Gemessen: aktivierung-gebundenes-einfuegen-fremder-mandant-abgelehnt. |
| T-EXD-03 | Elevation of Privilege | module-access.service.ts, Rollen-Kurzschluss |
high | mitigate | Der ADMIN/SUPER_ADMIN-Kurzschluss wird durch eine falsch gebundene Abfrage verbreitert. Die Kennung stammt aus dem Sitzungsnachweis und steht bereits im where; die Bindung kann deshalb nur einschraenken, nie oeffnen (Befund J). In Aufgabe 2 als Test festgenagelt: eine Aufloesung fuer einen zweiten Mandanten leitet keine Module des ersten ab. |
| T-EXD-04 | Elevation of Privilege | module-access.service.ts, Vorgabezustand |
critical | mitigate | Der geschlossene Vorgabezustand faellt offen — eine leere Freigabemenge fuehrt versehentlich zur vollen Aktivierungsliste. In Aufgabe 2 als Test festgenagelt: ohne Freigaben leere Menge UND der Schnittmengen-Lesezugriff findet gar nicht erst statt. |
| T-EXD-05 | Denial of Service | module-access.service.ts, module.guard.ts |
high | mitigate | Die umgekehrte Fehlerrichtung: ein zu kleines Ergebnis entzieht einem berechtigten Benutzer lautlos jeden Zugriff und liest sich als absichtlicher Entzug. Vollstaendige Bindung beider Stufen; Signaltabelle und namentliche Liste der Leere-als-Abwesenheit-Stellen in der Kritikschrift; offener Eintrag im Broken-Windows-Register mit der Vorabpruefung fuer Etappe 4. |
| T-EXD-06 | Repudiation | module.guard.ts |
high | accept | Es gibt heute KEIN Signal, das "wirklich keine Freigabe" von "die Abfrage hat nichts gefunden" unterscheidet — beide erzeugen dieselbe 403-Meldung, dieselbe leere Liste mit Status 200 und keinen Protokolleintrag. Bewusst akzeptiert und AUFGEZEICHNET statt durch eine Laufzeitwarnung ueberdeckt, die im Normalbetrieb Dauerlaerm waere (dieselbe Begruendung wie bei getAllActiveConfigs im Bereich ldap). In Aufgabe 2 als Test festgenagelt, damit die Aufzeichnung rot wird, sobald jemand ein Signal einbaut. |
| T-EXD-07 | Tampering | ModuleGrant-Regel, referenzierte Gruppe |
high | transfer | Die ausgelieferte Regel prueft nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe (T-JTS-03). Bleibt aufgezeichnet und ungefixt; der Schutz bleibt bei der Schreibseiten-Gegenpruefung in module-grants.service.ts, die dieser Plan nicht anfasst. Die Bindung fuegt auf der LESESEITE eine zweite Verteidigung hinzu, aber erst nach Etappe 4 — gemessen in Aufgabe 1, Pruefungen 6 und 7. |
| T-EXD-08 | Information Disclosure | Modulkatalog (Module) |
medium | mitigate | Der plattformweite Katalog wird versehentlich gebunden und verschwindet, sobald Etappe 3 ihm eine Regel gibt. Negativ-Gate in beiden Verifikationen, plus ein Testfall je Datei, der jeden Katalogzugriff aus dem Bindungsprotokoll heraushaelt. |
| T-EXD-09 | Spoofing | Sitzungsnachweis | low | accept | Mandantenkennung aus Koerper oder Parametern statt aus dem Sitzungsnachweis. Bereits in Phase 15 behandelt (T-03-04/T-15-10); dieser Plan aendert daran nichts und schwaecht es nicht. |
| T-EXD-10 | Tampering | Eindeutigkeitsschluessel dieses Bereichs | low | accept | Die Kette aus den Bereichen tenders und user (unsichtbare Zeile, falsches "frei", harter Eindeutigkeitsfehler) kann hier strukturell nicht auftreten, weil alle drei betroffenen Eindeutigkeitsschluessel mit der Mandantenkennung fuehren. Gemessen in Aufgabe 1, Pruefungen 12 und 13 — akzeptiert auf Grundlage einer Messung, nicht einer Annahme. |
| T-EXD-SC | Tampering | Paketinstallation | low | accept | Dieser Plan installiert kein Paket (npm/pip/cargo) und fuegt keine Abhaengigkeit hinzu. Das Legitimitaets-Gate faellt damit nicht an; ausdruecklich festgehalten statt schweigend ausgelassen. |
</threat_model>
Nach Abschluss aller drei Aufgaben:
node apps/api/scripts/rls-scratch-check.mjsmeldet alle Pruefungen bestanden (53 bisherige plus 13 neue), Rueckgabewert 0.npm --prefix apps/api run testmeldet mindestens 810 Tests gruen; die Dateizahl ist um mindestens eine gestiegen (die neue Testdatei fuer die Aktivierungsverwaltung).npm --prefix apps/api run type-checkist sauber.npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.tsist gruen — die Bestandsaufnahme stimmt mit dem Quelltext ueberein.- Alle fuenf handgepflegten Stellen der Klassifikation sind maschinell gegatet und gruen.
git diff --name-only HEADnennt keine Datei unterapps/api/prisma, keine Compose- oder Beispiel-Umgebungsdatei, und keine Datei der Bereichegroups,dashboard,auth,ldap,user.DATABASE_URLzeigt unveraendert auf die Rolletessera.
<success_criteria>
- Die siebzehn Zugriffe des Bereichs sind vollstaendig entschieden: zehn gebunden, sieben begruendet ungebunden, keiner unentschieden.
- Die Begruendung fuer jeden ungebundenen Zugriff trennt Messung von Bedingung — sie behauptet nichts, was heute nicht gilt, und sie verschweigt nicht, was morgen gelten wuerde.
- Die umgekehrte Fehlerrichtung ist an der echten, ausgelieferten Regel gemessen und je Pfad mit ihrem konkreten Signal beschrieben.
- Die Frage nach dem unterscheidenden Signal ist beantwortet, und wenn die Antwort "keines" lautet, steht sie so im Text UND als Testfall UND als offener Eintrag im Broken-Windows-Register.
- Beide Falsifizierungsnachweise sind durchgefuehrt, zurueckgenommen und im SUMMARY festgehalten — mit Testname und Fehlermeldung, nicht als Behauptung.
- Baseline gehalten am Ende jeder Aufgabe, nicht nur am Ende des Plans.
- Der Schalter ist weiterhin AUS.
</success_criteria>
<next_stages>
Nach diesem Bereich verbleiben in Etappe 2 fuenf Bereiche, nach heutiger
Rohtrefferzahl: dashboard (13), calendar (12), tenant (8), favorites (7),
auth (5, gemischt), settings (4).
Zwei Reihenfolgebedingungen aus diesem Durchlauf, fuer die Planung der folgenden Bereiche festgehalten:
dashboardbekommt seinen Modulzugriffs-Filter durch DIESEN Durchlauf bereits richtig, weil die Bindung im Dienst sitzt (Befund F). Der Bereichdashboardmuss dafuer nichts nachholen; seine eigenen dreizehn Zugriffe bleiben davon unberuehrt.settingsist weiterhin die offene Reihenfolgebedingung des Bereichstenders(Befund K dort, SMTP-Zugangsdaten) und ist mit vier Rohtreffern der kleinste verbleibende Bereich.
</next_stages>
Erstelle `.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-SUMMARY.md`, wenn alle drei Aufgaben abgeschlossen sind.