Aufgabe 3 — TDD zuerst (7 weitere Faelle in dashboard.service.spec.ts, 20
vorher/27 nach dieser Aufgabe), dann die Umstellung:
- getSearchProviders/addSearchProvider/removeSearchProvider laufen ueber
forTenant(); removeSearchProvider fuehrt Besitzpruefung UND Schreibzugriff
ueber DENSELBEN gebundenen Klienten. Die drei Vorgabe-Suchmaschinen aus
der Konstante bleiben unveraendert vorangestellt.
- Der eine Katalogzugriff (this.prisma.module in getWidgets) bleibt
begruendet ungebunden: Messung und Bedingung getrennt (Tabelle traegt
heute keinen Zeilenschutz, wirkungslos statt katastrophal — katastrophal
erst, wenn Etappe 3 eine Regel gibt), unter Berufung auf die bestehende
Werkzeugpruefung module-tabelle-traegt-keinen-zeilenschutz statt einer
neuen Behauptung. Ein Wachhund-Testfall haelt den Katalogzugriff aus dem
Bindungsprotokoll heraus (und beweist zuerst, dass der Katalogpfad
tatsaechlich durchlaufen wird, nicht nur theoretisch geprueft ist).
- docs/mandantentrennung-zugriffsklassifikation.md an allen fuenf
handgepflegten Stellen nachgezogen: vier Bestandsaufnahme-Zeilen (inkl.
eigenstaendiger Nachpruefung der widerlegten SearchProvider-Praemisse),
Uebersichtszeile (13/0 -> 1/12), Summenzeile (95/147), Klassen-Verteilung
(unveraendert 63 Paare, ausdruecklich vermerkt), Hintergrunddienst-
Abschnitt (dashboard hat keinen sechsten Fall, mit Messanweisung), "Was
diese Etappe NICHT entscheidet" (dienst-interner forTenant()-Weg wie alle
sieben Bereiche vor ihm).
- .planning/WINDOWS.md traegt Eintrag #25 (offen, Tabelle + JSON): die
beweisvernichtende Schleife (leeres Dashboard -> Neuaufbau ->
automatisches Zurueckschreiben -> ueberschriebene Anordnung, Widget-
Dubletten) samt der Vorabpruefung fuer Etappe 4 und dem Verweis auf #22
fuer die verwandte Eindeutigkeitsfrage.
Zwei weitere Falsifizierungsnachweise durchgefuehrt: (1) den Katalogzugriff
probeweise gebunden (tenantPrisma.module.findMany) — acht Tests werden rot
mit "TypeError: Cannot read properties of undefined (reading 'findMany')",
weil `module` bewusst nicht in der Testdouble-Bindungsliste steht; Rueckbau
zurueckgenommen, 27/27 wieder gruen. (2) den Stand von dashboardLayout in
der Klassifikationsdatei probeweise auf "ungebunden" gesetzt —
rls-access-inventory.spec.ts wird rot mit "Abweichender Stand (Dokument vs.
Quelltext): ... dokumentiert=ungebunden, gemessen=gebunden"; Ruecknahme,
Testlauf wieder gruen (10/10).
Baseline gehalten: 858 Tests / 56 Dateien gruen, Typpruefung sauber,
Wegwerf-Werkzeug 87/87. Schalter bleibt aus.
Aufgabe 2 — TDD zuerst (20 Faelle in dashboard.service.spec.ts, 8 vorher/12
neu, Zwei-Klienten-Nachweis ueber __makeBoundClient nach dem Muster von
module-access.service.spec.ts), dann die Umstellung:
- getLayout/saveLayout laufen GEMEINSAM gebunden (ein Testfall nagelt das
fest); saveLayout uebersetzt eine gebundene Konflikt-Schreibung
(PrismaClientUnknownRequestError, gemessen in Aufgabe 1 — NICHT P2002)
in eine deutsche ConflictException.
- getWidgets/addWidget/updateWidgetConfig/removeWidget laufen gebunden;
die drei Besitzpruefungen ueber die Benutzerkennung bleiben unveraendert
bestehen (die Regeln dieses Bereichs kennen keine Benutzerdimension).
updateWidgetConfig/removeWidget fuehren Besitzpruefung UND Schreibzugriff
ueber DENSELBEN gebundenen Klienten.
- dashboard.controller.ts reicht den bereits aufgeloesten Mandanten bei
getLayout/updateWidgetConfig/removeWidget durch (keine neue
Vertrauensquelle, weiterhin aus extractContext/Sitzungsnachweis).
- Der Modulkatalog und die vier Suchmaschinenzugriffe bleiben in dieser
Aufgabe unveraendert (Aufgabe 3).
- Falsifizierungsnachweis durchgefuehrt: tenantPrisma.widgetInstance.delete
probeweise auf this.prisma zurueckgebaut — Test "Widget entfernen: ebenso,
beide Abfragen ueber denselben Klienten" wird rot mit "erwarteter
gebundener Aufruf widgetInstance.delete(tenant=tenant-1) fehlt im
Protokoll"; Rueckbau zurueckgenommen, Testlauf wieder gruen (20/20).
Zwei dokumentierte Abweichungen (Rule 3): (1) Befund A hatte fuer
widgetInstance sieben Treffer vorhergesagt, gemessen sind sechs (macht
zusammen mit dashboardLayout acht statt neun) — der Verify-Schwellwert
wird entsprechend auf >=8 gelesen. (2) Die Stand-Spalte fuer
dashboardLayout/widgetInstance in der Klassifikationsdatei wird bereits
hier minimal nachgezogen (nicht erst in Aufgabe 3), weil
rls-access-inventory.spec.ts sonst am Ende dieser Aufgabe rot waere —
derselbe Praezedenzfall wie 260910-exd, Aufgabe 2.
Baseline gehalten: 851 Tests / 56 Dateien gruen (839 + 12 neue), Typpruefung
sauber, Wegwerf-Werkzeug 87/87.
Aufgabe 1 — misst die umgekehrte Fehlerrichtung des Bereichs dashboard an
den Regeln nach Migration 20260910120000, VOR der Umstellung:
- rls-scratch-check.mjs bekommt einen neunten Abschnitt
(runDashboardAreaChecks) mit 13 neuen, namentlich benannten Pruefungen
gegen die aus der ausgelieferten Migration geschnittenen Regeln fuer
DashboardLayout, WidgetInstance und SearchProvider. Alle 87 Pruefungen
bestehen (74 bisherige + 13 neue).
- Die Konfliktmessung (Befund K) ist gemessen, nicht angenommen: ein
gebundenes INSERT ... ON CONFLICT auf eine unter dem Mandanten
unsichtbare Zeile scheitert laut mit SQLSTATE 42501. Zusaetzlich am
echten generierten Prisma Client gemessen: prisma.dashboardLayout.upsert()
wirft PrismaClientUnknownRequestError (nicht P2002) — das tenders-Muster
laesst sich deshalb nicht woertlich uebernehmen.
- Die widerlegte Praemisse zu SearchProvider (WINDOWS #19) ist in diesem
Durchlauf eigenstaendig nachgeprueft, mit benannter Suchreichweite.
- docs/mandantentrennung-etappe2-fehlerrichtung.md bekommt den Abschnitt
"## Bereich dashboard" (w1)-(w5) samt der beweisvernichtenden Schleife
(leeres Dashboard -> Neuaufbau -> automatisches Zurueckschreiben ->
ueberschriebene Anordnung) und einen Nachtrag im Abschnitt
"## Bereich module-registry" zur geerbten Bindungsentlastung.
Baseline gehalten: 839 Tests / 56 Dateien gruen, Typpruefung sauber.
Schalter bleibt aus.
- TenderRssFeedSourceService.listForUser() nimmt jetzt (userId, tenantId)
entgegen und laeuft ueber einen gebundenen Klienten (forTenant) — die neue
Leseregel schliesst plattformweite Zeilen ein, die Reparatur haette den
ungebundenen Pfad sonst still auf nur die plattformweiten Zeilen reduziert
(Befund F). createPlatform/remove bleiben bewusst ungebunden, Kommentare an
der neuen Regel richtiggestellt.
- TendersController.listRssFeeds reicht die Mandantenkennung aus dem
Aufrufzusammenhang durch.
- Vier Aufzeichnungen im Quelltext (module-access.service.ts,
groups.service.ts, module-grants.service.ts, rls-coverage.spec.ts) sagen
jetzt, dass die Datenbankregel seit 20260910120000_rls_widen_membership_
grant_and_platform_read beide Seiten prueft; die Anwendungspruefungen
bleiben unveraendert bestehen (zweites Netz, wirkt vor dem Scharfschalten
als einziger Schutz).
- Zwei-Klienten-Nachweis in module-grants.service.spec.ts ergaenzt (Kommentar,
warum die beiden Cross-Tenant-Tests nach der Regelaenderung nicht entfallen
duerfen) und in tender-rss-feed.service.spec.ts umgekehrt (listForUser
bindet jetzt).
- Rule 1: implizites any beim Destrukturieren in listRssFeeds (feeds ist seit
der Bindung `any`) mit expliziter Annotation behoben.
- Falsifizierungsnachweis durchgefuehrt: Bindungsaufruf zurueckgenommen,
genau ein Test wurde rot (AssertionError, 0 statt der erwarteten
Aufrufe), Ruecknahme rueckgaengig gemacht.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- Neue, handgeschriebene Migration 20260910120000_rls_widen_membership_grant_and_platform_read:
GroupMembership prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer),
ModuleGrant prueft zusaetzlich die referenzierte Gruppe/den referenzierten Benutzer
(mit Leer-Zulassung, D-04), TenderRssFeedSource bekommt vier nach Befehl getrennte
Regeln statt einer (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt
weiterhin einen Mandanten). SearchProvider bewusst unveraendert (Befund E: Praemisse
widerlegt). Lokal angewandt und gegen den Systemkatalog der lebenden Datenbank
gemessen. Der Schalter bleibt aus (Rolle tessera).
- rls-scratch-check.mjs: die drei loch-behauptenden Pruefungen umgekehrt (nicht
geloescht), Gegenmessungen ueber die Wartungsrolle ergaenzt, vier Befehlsrichtungen
fuer TenderRssFeedSource gemessen, neuer Abschnitt fuer SearchProvider, Extraktion
auf die neue Migration umgeleitet und um eine mehrfach-treffer-faehige Form ergaenzt
(extractAllPolicySql).
- migration-sql.spec.ts: neuer Beschreibungsblock fuer die neue Migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- module-registry.service.ts: findActiveForTenant, activateForTenant,
isModuleActive binden je einen Aktivierungszugriff, deactivateForTenant
bindet beide (Lesen+Schreiben) ueber EINEN Klienten unter tenantPrisma;
alle sechs Katalogzugriffe (findAll/findBySlug/beide
Existenzpruefungen/isModuleActive-Katalogsuche/seedModule) bleiben
bewusst ungebunden, mit Kommentar der Messung von Bedingung trennt
- isModuleActive-Kopfkommentar richtiggestellt: der Waechter ruft sie
nicht auf (0 Aufrufer, TEIL 3 von Aufgabe 1) — Waechter nimmt findBySlug
+ ModuleAccessService.getAccessibleModuleIds
- module-registry.service.spec.ts: NEU, Zwei-Klienten-Nachweis, deckt die
bislang ungetestete Datei mit elf der siebzehn Zugriffe des Bereichs ab,
inkl. der lauten (deactivate ohne Aktivierung) und stillen (isModuleActive
ohne Aktivierung) Richtung und dem Katalog-Wachhund
- tender-scheduler.service.spec.ts: forTenant() auf Identitaet gemockt
(dieselbe Konvention wie ldap.service.spec.ts) — cross-area Bruch durch
die Umstellung von activateForTenant behoben (Rule 1/3)
- docs/mandantentrennung-zugriffsklassifikation.md: alle fuenf
handgepflegten Stellen nachgezogen (Bestandsaufnahme, Uebersichtszeile
7/10, Summenzeile 108/134, Klassen-Verteilung unveraendert bei 63 Paaren,
Hintergrunddienst-Abschnitt haelt die Abwesenheit eines sechsten Falls
fest) — alle gemessen, nicht abgeschrieben, Befund K haelt exakt
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag mit
tatsaechlich umgesetzten Pfaden, beiden Falsifizierungsnachweisen
(Testname+Meldung), und der Feststellung zum unveraenderten
Controller-Kommentar
- .planning/WINDOWS.md: neuer offener Eintrag #23 (deviation) — kein Signal
unterscheidet "keine Freigabe" von "Abfrage fand nichts", mit
Vorabpruefung fuer Etappe 4 und begruendeter Verwerfung einer
Laufzeitwarnung
- 833 Tests gruen (56 Dateien), Typpruefung sauber, Wegwerf-Werkzeug 66/66
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- module-access.service.ts: getAccessibleModuleIds (Kurzschlusszweig,
Direktweg, Gruppenweg, Schnittmenge) und getCatalogFlags' eigener
Aktivierungs-Lesezugriff laufen ueber forTenant(), EIN Klient je Methode
unter dem Namen tenantPrisma; der Katalogzugriff in findAccessibleModules
bleibt bewusst ungebunden (Modulkatalog traegt keine Regel), mit Kommentar
der Messung und Bedingung trennt
- Bestehende where-Filter mit tenantId bleiben als zweites Netz stehen
- module-access.service.spec.ts: Zwei-Klienten-Nachweis ueber
__makeBoundClient (Muster aus module-grants.service.spec.ts), alle 15
bestehenden Faelle erhalten, neue Faelle fuer jede in <behavior> genannte
Bindungseigenschaft inkl. Wachhund gegen eine kuenftige Katalogbindung
- module.guard.spec.ts: ein Fall, der die Abwesenheit eines
unterscheidenden Signals fuer "keine Freigabe" vs. "Abfrage fand nichts"
festnagelt
- Falsifizierungsnachweis durchgefuehrt: Gruppenweg-Bindung probeweise
zurueckgebaut, Test "USER-Zweig bindet BEIDE Freigabe-Lesezugriffe..."
wurde rot ("expected 1 to be 2"), Ruecknahme bestaetigt wieder gruen
- mandantentrennung-zugriffsklassifikation.md: Stand fuer
module-access.service.ts/moduleGrant und /tenantModuleActivation auf
gebunden nachgezogen (Rule 3 — sonst waere rls-access-inventory.spec.ts
rot geblieben); die uebrigen vier Bestandsaufnahme-Stellen bleiben
Aufgabe 3 vorbehalten
- 817 Tests gruen (55 Dateien), Typpruefung sauber, Wegwerf-Werkzeug 66/66
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- rls-scratch-check.mjs: achter Abschnitt runModuleRegistryAreaChecks mit 13
benannten Pruefungen gegen die echten, aus den ausgelieferten Migrationen
geschnittenen Regeln (Group/GroupMembership/ModuleGrant/TenantModuleActivation),
neu angelegt nur: Modulkatalog-Tabelle ohne Zeilenschutz, Eindeutigkeitsindex
auf TenantModuleActivation, zwei Direkt-Freigaben, eine fremde Mitgliedschaft
- Alle 66 Pruefungen bestanden (53 bisherige + 13 neue), 810 Tests gruen,
Typpruefung sauber
- docs/mandantentrennung-etappe2-fehlerrichtung.md: neuer Abschnitt
"Bereich module-registry" mit den fuenf Unterabschnitten (m1-m5), inklusive
Praezisierung aus Befund E (Katalogbindung ist HEUTE wirkungslos, nicht
katastrophal — die Bedingung wird als Bedingung notiert) und der
unbeschoenigten Antwort auf die Signalfrage ("keines")
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- user.controller.ts: alle sieben Zugriffe binden. ADMIN-Zweig der
Benutzerliste laeuft ueber forTenant() mit weiterhin bestehender
Mandantenbedingung im where; SUPER_ADMIN-Zweig ueber die neue
UserService.findAllForPlatformAdmin(). Die drei Wege ueber die Kennung
loesen den Zielbenutzer rollenabhaengig ueber resolveTargetUser() auf
(ADMIN gebunden an eigenen Mandanten, SUPER_ADMIN uebergreifend); der
Schreibzugriff bei update/delete bindet an den Mandanten des
Zielbenutzers, nicht des Aufrufers, damit die uebergreifende
Verwaltung durch die oberste Rolle erhalten bleibt
- Selbstloesch-Riegel (Befund H) repariert: verglich bisher gegen
currentUser.sub, ein Feld, das der Sitzungsnachweis nicht traegt --
der Riegel griff nie. Jetzt gegen currentUser.id. Verhaltensaenderung:
ein Administrator kann sein eigenes Konto nun nicht mehr loeschen
- Alle fuenf Selbstbedienungszugriffe (Bild hochladen/loeschen/
ausliefern, Akzentfarbe) binden an die Mandantenkennung aus dem
Sitzungsnachweis
- user.controller.spec.ts (neu): Zwei-Klienten-Nachweis fuer die
vorher testlose Steuerungsschicht, 8 Testfaelle, Falsifizierungsnachweis
fuer eine gebundene Stelle sowie Rot-vor-Reparatur-Nachweis fuer den
Selbstloesch-Riegel (siehe SUMMARY)
- docs/mandantentrennung-zugriffsklassifikation.md: alle vier
handgepflegten Stellen nachgezogen (Uebersichtszeile 8/14, Summenzeile
118/124, Klassen-Verteilung 63 Paare, Hintergrunddienst-Abschnitt auf
fuenf Faelle inkl. admin-seed.service.ts als erster beidseitig
korrekter Fall) sowie zwei Klassenkorrekturen (user.service.ts/user
und admin-seed.service.ts/user je auf "beides")
- docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag zum
user-Abschnitt mit den tatsaechlich umgesetzten Pfaden, der
geschlossenen Luecke und den Falsifizierungsnachweisen
- 810 Tests gruen (8 neue in user.controller.spec.ts), Typpruefung
sauber, Wegwerf-Werkzeug meldet weiterhin alle 53 Pruefungen bestanden
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- user.service.ts: findById/update/deactivate/delete bekommen einen
Pflicht-Mandanten und laufen ueber forTenant(); create/update
uebersetzen die plattformweite Eindeutigkeitsverletzung (P2002) in eine
deutsche Konfliktmeldung ohne Halter/Mandant zu nennen; zwei neue
Methoden (findAllForPlatformAdmin, findByIdForPlatformAdmin) bilden die
Plattform-Administratorsicht als Schleife ueber alle Mandanten mit je
einem gebundenen Lesezugriff nach; findByUsername bleibt bewusst
ungebunden, Kopfkommentar richtiggestellt (Anmeldeweg laeuft seit
Etappe 1 ueber SECURITY-DEFINER-Funktionen, kein Aufrufer mehr)
- admin-seed.service.ts: Erstanlage-Pruefung bleibt ungebunden (mit
Begruendung), Erstanlage des Administrators bindet an den unmittelbar
zuvor angelegten Mandanten (Befund J-Korrektur); P2002 bei der Anlage
wird wie "Administrator existiert bereits" behandelt statt den Start
abzubrechen -- jeder andere Fehler bricht weiterhin ab
- user.controller.ts: die vier Aufrufstellen der geaenderten Signaturen
auf currentUser.tenantId umgestellt (Signatur-Minimalanpassung; die
Rollenlogik inkl. Plattform-Administratorsicht folgt in Aufgabe 3)
- Zwei-Klienten-Nachweis in beiden Testdateien (Muster
groups.service.spec.ts), Falsifizierungsnachweis fuer beide Bereiche
durchgefuehrt und zurueckgenommen (siehe SUMMARY)
- docs/mandantentrennung-zugriffsklassifikation.md: Zwischenstand fuer
(user.service.ts, user) und (admin-seed.service.ts, user) auf gemischt
korrigiert, neue Zeile (user.service.ts, tenant) ergaenzt -- volle
Klassenkorrektur mit Begruendung sowie die vier handgepflegten
Uebersichtstabellen folgen in Aufgabe 3
- .planning/WINDOWS.md: offener Eintrag fuer die plattformweite
Eindeutigkeit von username/email (Produktentscheidung fuer Etappe 3)
- 802 Tests gruen (13 neue in user.service.spec.ts, 4 neue in
admin-seed.service.spec.ts), Typpruefung sauber, Wegwerf-Werkzeug
meldet weiterhin alle 53 Pruefungen bestanden
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- runUserAreaChecks in rls-scratch-check.mjs: 12 neue Pruefungen gegen die
ausgelieferte User-Policy (baut auf der vom Anmeldeweg-Abschnitt
angelegten Tabelle auf, legt zusaetzlich Tenant ohne Zeilenschutz an)
- Belegt: ungebundene Suche nach vorhandenem Benutzernamen liefert 0
Zeilen, gebundene Suche nach fremdem Benutzernamen ebenso ("frei"), und
das anschliessende gebundene INSERT scheitert hart an SQLSTATE 23505
(Eindeutigkeitsverletzung), nicht an 42501 (Zeilenschutz)
- SQLSTATE wird aus err.meta.code gelesen, nicht err.code (das bei
$executeRaw-Fehlern immer den generischen Prisma-Code P2010 traegt,
empirisch gegen tessera-ctl-db-1 geprueft)
- docs/mandantentrennung-etappe2-fehlerrichtung.md: neuer Abschnitt
"Bereich user" (u1-u5) mit der tatsaechlich beobachteten Ausgabe,
Signaltabelle, der vollstaendigen Kette (Befund L) und den Grenzen zu
auth.service.ts/ldap.service.ts
- Teil 2/3 gemessen: keine Transaktion in apps/api/src/user (Befund B
haelt), findByUsername hat genau einen Treffer, die eigene Definition
(Befund D haelt)
- 789 Tests weiterhin gruen, Typpruefung sauber, Wegwerf-Werkzeug meldet
alle 53 Pruefungen bestanden (41 bisherige + 12 neue)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- apps/api/src/dkv/dkv.service.spec.ts (neu): Zwei-Klienten-Nachweis nach
dem Muster aus groups.service.spec.ts/tender-triage.service.spec.ts —
dieser Bereich hatte vorher KEINE Testdatei (Befund J). 7 Testfaelle
decken getConfigForApi, saveConfig (Zugangsdaten-Erhaltung), testConnection,
die Verarbeitungsstrecke und den bewusst ungebundenen Planer-Startpfad ab
- dkv.service.ts: loadConfig(tenantId?) in zwei Methoden geteilt —
loadConfig(tenantId) [Pflicht-Mandant, gebunden] und die neue, eigene
Methode loadAnyActiveConfigForScheduler() [bewusst UNGEBUNDEN, eigener
Kopfkommentar mit beiden Zustaenden]. getConfigForApi/saveConfig/
testConnection/_runPipeline binden je EINEN Klienten pro Methode
vollstaendig ueber forTenant()
- dkv-scheduler.service.ts: Kopfkommentar fortgeschrieben (beide Zustaende,
Praezedenzfall, Unsymmetrie), Aufruf auf loadAnyActiveConfigForScheduler()
umgestellt — an der Ablauflogik des Planers nichts geaendert
- .planning/WINDOWS.md: Eintrag #21 (deviation) fuer die benannte Altlast
des Planer-Startpfads angelegt
- docs/mandantentrennung-zugriffsklassifikation.md: dkvModuleConfig-Zeile
auf den jetzt gemessenen Stand "gemischt" nachgezogen (Rule 3 — noetig,
damit rls-access-inventory.spec.ts nach der Aufteilung von loadConfig()
gruen bleibt; die uebrigen zwei dkv-Zeilen und die Uebersichtstabelle
bleiben Aufgabe 3 vorbehalten)
- Falsifizierungsnachweis erbracht: getConfigForApi's erster gebundener
Client probeweise durch this.prisma ersetzt, genau Test 1 wurde rot
(6 andere blieben gruen), Rueckbau zurueckgenommen, Dateien identisch
zum Ausgangsstand bestaetigt
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
- rls-scratch-check.mjs: runDkvAreaChecks() misst die drei ausgelieferten
Policies (DkvInvoiceHistory/DkvModuleConfig/DkvVehicleMaster) wortgleich
aus der Migration, plus die Nebenlaeufigkeitsform von getHistory()
(Promise.all ueber zwei gebundene Einzelabfragen); alle 9 neuen plus
32 bestehende Pruefungen bestehen (41 gesamt)
- Belegt Befund H (kein P2002-Fall, Mandant ist Teil des zusammengesetzten
Schluessels) und Befund I (gebundenes INSERT mit fremder tenantId wird
ohne eigene WITH-CHECK-Klausel trotzdem abgewiesen) an der echten
Datenbank statt am Policy-Text
- docs/mandantentrennung-etappe2-fehlerrichtung.md: neuer Abschnitt
"Bereich dkv" mit der dritten Fehlerform der Etappe (ein Einzelobjekt
wird null, wo null bereits "nicht eingerichtet" bedeutet), der
Signaltabelle je umzustellendem Pfad, den sieben Stellen aus Befund K
(zerstoerend/lautlos/irrefuehrend) und der ausgeschriebenen
Planer-Entscheidung (Form c, mit Unsymmetrie zum ldap-Praezedenzfall)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
tender-digest.scheduler.ts: die uebergreifende Kandidatenabfrage
(tenderMatch.findMany mit distinct:['userId']) bleibt bewusst ungebunden
und waehlt zusaetzlich das denormalisierte tenantId der Treffer-Zeile mit
aus; innerhalb der Schleife binden tenderNotificationPref.findUnique,
tenderMatch.findMany/updateMany und user.findUnique an den Mandanten
DIESER Kandidatenzeile.
tender-matching.service.ts: die Profilabfrage (tenderSavedSearch.findMany)
und der Lesezugriff auf den plattformweiten Tender-Katalog (D-03) bleiben
ungebunden; innerhalb der Profilschleife bindet die Treffer-Anlage
(tenderMatch.upsert), im nachgelagerten Instant-Dispatch binden
tenderMatch.findMany/updateMany und user.findUnique — je EIN gebundener
Client pro Profil, nicht neu je Treffer.
Beide Dateien tragen Codekommentare, die die uebergreifenden Abfragen
ausdruecklich als Etappe-3-Uebergabe benennen — die Trennlinie zwischen
Etappe 2 und Etappe 3 wird hier gezogen, nicht verwischt.
Alle drei betroffenen Testdateien (inkl. der gemeinsamen
Integrationsdatei) bekommen den Zwei-Client-Nachweis, Tests fuer
Zwei-Mandanten-Laeufe und die lautlose Fehlerform (kein Versand,
notifiedAt bleibt NULL). Falsifiziert: ein probeweiser Rueckbau der
user.findUnique-Bindung im Instant-Dispatch von tender-matching.service.ts
machte genau den erwarteten Test rot, danach zurueckgenommen.
rls-access-inventory.spec.ts gemessen und docs/mandantentrennung-zugriffsklassifikation.md
nachgezogen: Stand der fuenf betroffenen Paare (tender-digest.scheduler.ts/
tenderMatch,tenderNotificationPref,user; tender-matching.service.ts/
tenderMatch,user) auf gemischt bzw. gebunden; Bereichsuebersicht und
Klassen-Verteilung neu gemessen (tenders jetzt 36 ungebunden/26 gebunden);
"Der Hintergrunddienst als Falle" um den Abschluss beider Dateien ergaenzt.
docs/mandantentrennung-etappe2-fehlerrichtung.md: (t4) um den Nachtrag
ergaenzt, dass die Je-Treffer-Haelften geschlossen sind und die
uebergreifenden Haelften an Etappe 3 uebergeben bleiben.
770 Tests gruen, Typpruefung sauber, Wegwerf-Werkzeug 32/32, kein
Schema-/Migrations-/Compose-/Umgebungsdatei-Diff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
tender-saved-search.service.ts, tender-triage.service.ts,
tender-notification-pref.service.ts und tender-email-config.service.ts
laufen jetzt vollstaendig ueber forTenant() — vier neue Parameter (list,
update, remove, listForUser, favoriteIds, getForUser, getConfigForApi,
testConnection bekommen tenantId), die anwendungsseitige userId-Filterung
bleibt unveraendert (Befund E: die Policies haben keine Benutzerdimension).
tender-rss-feed.service.ts bindet nur createForUser (Zaehler + Anlage,
beide ausschliesslich auf persoenlichen Zeilen); listForUser, createPlatform
und remove bleiben mit Codekommentar bewusst ungebunden (WINDOWS #19 —
eine gebundene plattformweite Zeile waere unter jedem Mandanten unsichtbar,
ein gebundenes Einfuegen ohne Mandant wuerde abgewiesen).
tender-notification-pref.service.ts und tender-email-config.service.ts
uebersetzen eine P2002-Verletzung auf dem tenantlosen upsert-Schluessel
(Befund F) in eine verstaendliche deutsche Meldung statt eines rohen
Fehlers.
tenders.controller.ts reicht tenantId an den acht betroffenen
Aufrufstellen durch extractTriageContext() durch (kein neuer
Aufloesungsweg); die drei RSS-Aufrufstellen bleiben unveraendert, da ihre
Dienstmethoden nicht binden.
Alle sieben angefassten Testdateien bekommen den Zwei-Client-Nachweis
(__makeBoundClient ueber demselben Speicher) und Bindungstests je
umgestellter Methode; tender-rss-feed.service.spec.ts zusaetzlich den
Gegentest, dass die drei unveraendert bleibenden Pfade forTenant() NICHT
aufrufen. Falsifiziert: ein probeweiser Rueckbau der list()-Bindung in
tender-saved-search.service.ts machte genau den erwarteten Bindungstest
rot, danach zurueckgenommen.
docs/mandantentrennung-zugriffsklassifikation.md: Stand der fuenf Paare
auf gebunden bzw. gemischt nachgezogen; tenderRssFeedSource von
muss-mandantengebunden auf beides umklassifiziert (derselbe Praezedenzfall
wie ldapConfig in 260909-ipc).
761 Tests gruen (743 + 18 neue Bindungsnachweise), Typpruefung sauber,
Wegwerf-Werkzeug 32/32, kein Schema-/Migrations-/Compose-/
Umgebungsdatei-Diff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
rls-scratch-check.mjs bekommt einen fuenften Abschnitt (runTendersAreaChecks)
mit den fuenf Policies von TenderEmailConfig/TenderNotificationPref/
TenderRssFeedSource/TenderSavedSearch/TenderTriage, wortgleich aus der
ausgelieferten Migration extrahiert. Neun neue Pruefungen belegen: die
Policies haben keine Benutzerdimension (Befund E), eine plattformweite
RSS-Zeile ist unter jedem Mandantenkontext unsichtbar und ein gebundenes
Einfuegen ohne Mandant wird abgewiesen (WINDOWS #19), und ein gebundenes
upsert auf eine unsichtbare Zeile verletzt die Eindeutigkeitsbedingung,
nicht die Policy (Befund F). Alle 32 Pruefungen (23 bisherige + 9 neue)
bestehen. Nachmessung bestaetigt Befund A: genau eine $transaction in
diesem Bereich, Array-Form auf der plattformweiten Tabelle Tender,
ausserhalb jeder Mandantenbindung — withTenantTransaction() wird hier
nicht gebraucht.
docs/mandantentrennung-etappe2-fehlerrichtung.md bekommt einen
`## Bereich tenders`-Abschnitt mit der beobachteten Messausgabe, einer
Signaltabelle je umzustellendem Pfad und einem eigenen Unterabschnitt zur
lautlosen Fehlerform der beiden Hintergrunddienste (fuenf benannte
Stellen, keine protokolliert etwas).
743 Tests gruen, Typpruefung sauber, kein Schema-/Migrations-/Compose-/
Umgebungsdatei-Diff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
Alle fuenf Methoden von ModuleGrantsService (assertTargetBelongsToTenant,
grant, revoke, getMatrix, getUserAccess) laufen jetzt ueber forTenant(); bei
den beiden Datenlieferungen teilen sich alle parallel abgesetzten
Teilabfragen denselben gebundenen Client. Die Mandanten-Gegenpruefung vor
jedem Erteilen bleibt ausdruecklich bestehen und bekommt einen Verweis auf
Befund F/T-JTS-03: die Regel auf ModuleGrant prueft nur die
Mandantenkennung der Zeile, nicht die referenzierte Gruppe. Der veraltete
Kommentar ueber der Mitgliedschaftsabfrage im Benutzer-Detail ("kein
forTenant hier") ist durch den neuen Stand ersetzt.
module-grants.service.spec.ts bekommt denselben Bindungsnachweis-Mock wie
groups.service.spec.ts (zwei unterscheidbare Clients ueber demselben
Speicher) und sechs neue Bindungsnachweise; alle 28 Bestandstests bleiben
gruen.
Beide Dokumente geschlossen: die Bereichsuebersicht fuer groups ist neu
gemessen (0 ungebunden, 31 gebunden — ein dokumentierter methodischer
Bodensatz, da die einfache Rohtrefferzaehlung die neun ueber `tx` gebundenen
Zugriffe innerhalb der drei Transaktionen nicht sieht), die
Klassen-Verteilung auf 62 Paare aktualisiert, und der als offen gefuehrte
Befund D aus dem ldap-Abschnitt der Fehlerrichtung ist mit Verweis auf
diesen Durchlauf als erledigt vermerkt (Nachtrag, nicht Neuschrieb). 743
Tests und die Typpruefung gruen; Schema, Migrationen und alle vier
Compose-Dateien unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
prisma-tenant.extension.ts bekommt withTenantTransaction(prisma, tenantId, fn)
— die interaktive Transaktion auf dem UNgebundenen Client mit set_config als
erster Anweisung direkt auf tx, die in Aufgabe 1 als einzige der drei
gemessenen Formen sowohl die Einzelmessung als auch eine Lastprobe unter
echter Nebenlaeufigkeit bestand (die Array-Form auf dem gebundenen Client
verteilt jede Operation auf eine eigene Teiltransaktion; die interaktive Form
auf dem gebundenen Client brach unter 40 parallelen Aufrufen mit P2028 ab).
rls-access-inventory.spec.ts bekommt eine dritte Erkennung fuer
Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion
(zwei Formen: direkter Empfaenger.$transaction(async...) und das neue
Hilfsmittel withTenantTransaction(...)) — macht das Paar
(groups.service.ts, tenantModuleActivation) erstmals sichtbar, das bislang
keine Pruefung dieses Projekts je gesehen hat.
groups.service.ts: alle zwoelf Methoden inklusive der drei Transaktionen
(update() isDefault:true, reassignDefaultBeforeDelete(), ensureDefaultGroup())
laufen jetzt ueber den Mandantenkontext. Zaehler und Transaktion in
ensureDefaultGroup() sind gemeinsam gebunden (T-JTS-05). addUserToDefaultGroup()
prueft neu, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02) —
die Regel auf GroupMembership prueft nachweislich nur die Gruppenseite.
groups.service.spec.ts bekommt zwei unterscheidbare Clients ueber demselben
Speicher-Fake (Muster aus 260909-ipc, auf die interaktive Form uebertragen)
und 13 neue Bindungsnachweise; alle 42 Bestandstests bleiben gruen.
Klassifikationsdokument nachgezogen. 737 Tests und die Typpruefung gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
rls-scratch-check.mjs bekommt einen vierten Abschnitt (Group/GroupMembership/
ModuleGrant/TenantModuleActivation, Policies woertlich aus den ausgelieferten
Migrationen) sowie eine eigene Messung, welche der drei Transaktionsformen
(Array auf gebundenem Client, interaktiv auf gebundenem Client, interaktiv auf
ungebundenem Client mit set_config auf tx) den Mandantenkontext tatsaechlich
auf derselben Verbindung traegt. Ergebnis: Form (i) versagt nachweisbar
(unterschiedliche pg_backend_pid() je Teilschritt); Form (ii) und (iii)
bestehen die Einzelmessung, aber eine zusaetzliche Lastprobe mit 40 parallelen
Aufrufen zeigt, dass Form (ii) unter echter Nebenlaeufigkeit mit P2028
(Transaction API error) abbricht, waehrend Form (iii) 0 Verletzungen zeigt.
Die Kritikschrift bekommt einen eigenen groups-Abschnitt mit den tatsaechlich
beobachteten Werten, der Signaltabelle je Pfad und der Liste der Stellen, die
Leere als Abwesenheit deuten (inkl. der einen Stelle, an der zu wenig Lesen zu
viel Schreiben ausloest). Kein Dienstcode angefasst; 719 Tests und die
Typpruefung bleiben gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
Aufgabe 3 der Etappe 2: elf Abfragen in sechs Methoden (listGroups,
upsertMappedUser, searchUsers, importUsersByDn, importGroupsByDn,
syncUsersForTenant) laufen jetzt ueber forTenant(), teils mit einem neu
erzeugten, teils mit dem in derselben Methode bereits vorhandenen gebundenen
Client. resolveEmailForWrite bleibt ausdruecklich ungebunden (Befund A,
T-IPC-04): email/username sind plattformweit eindeutig, eine Bindung wuerde
einen fremden Halter uebersehen und eine saubere Kollisionsmeldung in einen
P2002-Abbruch verwandeln.
Ein neuer Testblock biegt forTenant() auf ein zweites, unterscheidbares
Client-Objekt um (der bisherige Identitaets-Mock haette die Umstellung nicht
bemerkt, Befund F) und belegt damit, dass die Adressabfrage weiterhin am
ungebundenen und der Rest am gebundenen Client landet. Alle 67 Bestandstests
bleiben unveraendert gruen.
docs/mandantentrennung-zugriffsklassifikation.md ist fuer den Bereich ldap
geschlossen: gemessener Stand je Fundstelle, neu gerechnete Bereichsuebersicht
(gebunden getrennt von ungebunden gezaehlt) und die Uebergabe des
Standardgruppen-Punkts an den Bereich groups vor Etappe 4 dokumentiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
Aufgabe 2 der Etappe 2: getConfig/createConfig/updateConfig sowie
addFieldMapping/removeFieldMapping laufen jetzt ueber forTenant(), gebunden
an den aus der Anfrage bekannten Mandanten. removeFieldMapping nimmt den
Mandanten neu als Pflichtparameter entgegen und der Controller holt ihn aus
dem Sitzungsnachweis statt nur die URL-Kennung weiterzureichen (T-IPC-01) --
ein Administrator konnte bisher die Feldzuordnung eines fremden Mandanten
loeschen, wenn er ihre Kennung kannte. getAllActiveConfigs() und die
Start-Nachverschluesselung bleiben bewusst uebergreifend, mit ausgeschriebener
Begruendung im Code (Befund B).
rls-access-inventory.spec.ts erkennt jetzt neben `this.prisma.<Modell>` auch
gebundene `<Name>.<Modell>`-Zugriffe (Befund F/G) und prueft eine neue
Stand-Spalte (gebunden/ungebunden/gemischt) im Klassifikationsdokument gegen
den Quelltext. Das macht zwei bisher unsichtbare, weil schon laenger
gebundene Fundstellen sichtbar (auth.service.ts/passwordResetToken,
ldap.service.ts/groupMembership) und deckt auf, dass
(ldap-config.service.ts, ldapConfig) tatsaechlich "beides" ist, nicht
"muss-mandantengebunden" (Befund B).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
Aufgabe 1 der Etappe 2: erweitert das Wegwerf-Werkzeug rls-scratch-check.mjs
um fuenf Messungen des forTenant()-Musters gegen die echte, aus der
ausgelieferten Migration geschnittene LdapConfig/LdapFieldMapping-Policy
unter einer Rolle ohne BYPASSRLS. Belegt insbesondere, dass ein ungebundener
Zugriff nach dem Scharfschalten 0 Zeilen liefert, nicht alle -- die
Fehlerrichtung dreht sich um. Die neue Kritikschrift
docs/mandantentrennung-etappe2-fehlerrichtung.md haelt das schriftlich fest,
mit Signaltabelle je Pfad und den vier Stellen, die Leere als Abwesenheit
deuten. Kein Dienstcode angefasst.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
WINDOWS #18/#20, Aufgabe 3: docs/mandantentrennung-zugriffsklassifikation.md
haelt fuer jede der 227 this.prisma.*-Fundstellen (32 Dateien, zusammengefasst
zu 59 Datei-Modell-Paaren) eine Klasse fest — muss-mandantengebunden (31),
keine-mandantengebundene-tabelle (16), bewusst-uebergreifend (3, mit
ausgeschriebenem Grund) oder beides (9, der Hintergrunddienst-Sonderfall:
uebergreifend lesen, je Zeile mandantengebunden schreiben — betrifft
ldap.service.ts, tender-digest.scheduler.ts, tender-matching.service.ts).
rls-access-inventory.spec.ts ermittelt die Fundstellen bei jedem Testlauf neu
aus dem Quelltext und vergleicht sie gegen die Tabelle im Dokument — Datei und
Modellname als Schluessel, keine Zeilennummer. Scheitert nachweislich, sobald
eine Fundstelle fehlt oder ein Eintrag verwaist (per Testlauf geprueft, danach
zurueckgesetzt).
Zwei belegte Befunde im Dokument festgehalten: req.tenantPrisma wird gesetzt,
aber nirgends gelesen; WINDOWS #19 (nullbares tenantId bei SearchProvider/
TenderRssFeedSource) bleibt benannter Blocker fuer Etappe 3.
docs/mandantentrennung-datenbankrolle.md verweist jetzt auf das neue
Dokument und korrigiert die ueberholte Zahl 182 auf den nachgemessenen Stand
(227/59). WINDOWS.md #18/#19 um Nachtrag auf diesen Plan ergaenzt; #20 (der
in Aufgabe 1 gemessene und behobene forTenant()-Verbindungsdefekt) als
"fixed" markiert.
Deviation (Rule 3, blockierend fuer die Bestandsaufnahme-Pruefung):
auth.service.ts-Kommentar umformuliert, der zuvor woertlich
"this.prisma.user.findUnique" als erklaerenden Text enthielt und dadurch
einen Eigentreffer der grep-basierten Inventur-Pruefung erzeugte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
WINDOWS #18/#20, Aufgabe 2: der Anmeldeweg muss den passenden Benutzer
finden, bevor sein Mandant bekannt ist — unter der kuenftigen Rolle ohne
BYPASSRLS (tessera_app) wuerde ein gewoehnlicher SELECT auf "User" sonst
null Zeilen liefern und die Anmeldung waere unmoeglich.
Drei SECURITY-DEFINER-Funktionen (STABLE, fester Suchpfad public/pg_temp,
fester Spaltensatz, LIMIT 1, Ausfuehrungsrecht ausschliesslich fuer
tessera_app) ersetzen die drei pre-tenant Lesezugriffe in auth.service.ts:
- auth_lookup_user_by_username (validateUser)
- auth_lookup_user_by_email (requestPasswordReset)
- auth_lookup_reset_token (resetPassword)
Sobald der Benutzer und damit sein Mandant bekannt sind, laufen alle
Schreibzugriffe (lastLoginAt, passwordHash, Reset-Token) ueber forTenant(),
gebunden an genau diesen Mandanten (Aufgabe 1). getMe/changePassword/
adminResetPassword bleiben bewusst unangetastet — sie kennen den Mandanten
bereits aus dem Sitzungsnachweis und gehoeren in Etappe 2.
rls-scratch-check.mjs um einen zweiten Abschnitt erweitert: spielt die
Migration in die Wegwerf-Datenbank ein und misst live unter der Rolle ohne
BYPASSRLS — Anmeldesuche findet den Benutzer, unbekannter Name liefert
nichts ohne zu werfen, gewoehnlicher SELECT auf "User" liefert null Zeilen.
Alle 8 Pruefungen (5 aus Aufgabe 1 + 3 neue) bestehen gegen die lokale
Datenbank. Volle Testsuite (695 Tests) und type-check bleiben gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
WINDOWS #20: set_config() lief auf einer anderen Postgres-Verbindung als die
eigentliche Abfrage, weil die interaktive Callback-Form von $transaction
verwendet wurde. Ersetzt durch die Array-Form, die set_config und Abfrage als
eine Transaktion auf einer Verbindung ausfuehrt (Prismas empfohlenes Muster
fuer RLS-ueber-Extensions). Injektionsfestigkeit (T-02-05) bleibt ueber ein
getaggtes $executeRaw-Template statt $executeRawUnsafe erhalten.
- prisma-tenant.extension.spec.ts: prueft die Form des Aufrufs (Array mit
zwei Eintraegen, Rueckgabewert ist der zweite Eintrag) ohne laufende
Datenbank
- rls-scratch-check.mjs: neues Werkzeug, das eine Wegwerf-Datenbank anlegt
und live misst — gleiche Backend-Verbindung, gesetzter Kontext, keine
Fremdmandanten-Zeilen, keine Zeilen ohne Kontext. Alle 5 Pruefungen
bestehen gegen die lokale Datenbank.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
- Migration 20260909140000_rls_remaining_tenant_tables ergaenzt ENABLE +
FORCE ROW LEVEL SECURITY und je eine tenant_isolation_policy fuer alle 16
noch offenen Tabellen mit tenantId
- Kopfkommentar korrigiert die zu pauschale D-03-Aussage aus
20260804130918: nur die drei Tender-Tabellen OHNE tenantId sind davon
betroffen, die sechs MIT tenantId bekommen jetzt eine Policy — die alte
Migrationsdatei bleibt unveraendert
- rls-coverage.spec.ts misst die Abdeckung aus Schema und Migrationen
statt Text zu vergleichen (rot mit 16 gemeldeten Luecken vor der
Migration, jetzt gruen); waechst automatisch mit kuenftigen Modellen und
erzwingt bei jedem neuen tenantId-losen Modell eine bewusste Entscheidung
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
- apps/api/scripts/rls-preflight.mjs misst fuenf benannte Eigenschaften
(rollenrechte, kontext-setzbar, ohne-kontext-leer, mit-kontext-sichtbar,
schreibrechte) jeweils in einer eigenen Transaktion gegen eine per
TESSERA_PREFLIGHT_DATABASE_URL angegebene Verbindung; --print-plan
verbindet nicht, das Werkzeug schreibt in keiner Betriebsart
- 5 Tests in rls-preflight.spec.ts (rot vor dem Werkzeug, jetzt gruen)
- docs/mandantentrennung-datenbankrolle.md: Befund, Sperrgrund (182
unskalierte Zugriffe, Anmeldeweg), Handgriffe des Betreibers samt
Kennwortsetzung, Freigabebedingung und Rueckweg; docs/README.md verweist
darauf
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
- apps/api/scripts/migrate-and-start.sh: TESSERA_MIGRATE_DATABASE_URL fuer
den Migrationsschritt, DATABASE_URL bleibt unveraendert fuer den
Laufzeitschritt; leer/nicht gesetzt = bisheriges Verhalten
- Dockerfile kopiert scripts/ und ruft das Skript als CMD auf; exec statt
&&-Verkettung, damit Signale den Node-Prozess erreichen
- docker-compose.yml/.prod.yml reichen TESSERA_MIGRATE_DATABASE_URL durch
(leerer Vorgabewert); docker-compose.dev.yml/.ci.yml unveraendert
- .env.example erklaert beide Variablen mit Platzhaltern, Umstellung bleibt
auskommentiert
- 5 Tests in start-script.spec.ts (rot vor dem Skript, jetzt gruen)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
- Migration 20260909130000_rls_app_role legt tessera_app mit NOSUPERUSER
NOBYPASSRLS wiederholbar an bzw. konvergiert eine vorhandene Rolle darauf
- Kein Kennwort im SQL, Datenbankname und Eigentuemer dynamisch gebildet
- 7 Tests in rls-app-role.spec.ts (rot vor der Migration, jetzt gruen)
- Rolle wird von niemandem benutzt — WINDOWS #18 bleibt offen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
- User.email auf optional gestellt (Migration geschrieben, NICHT
ausgefuehrt); Eindeutigkeitsindex unangetastet, NULL bleibt in Postgres
je verschieden
- Neuer Kollisionsentscheider (resolveEmailForWrite) in ldap.service.ts:
eine bereits vergebene Adresse wird nie umgehaengt (T-Q3-01) — das
zuerst angelegte Konto behaelt sie, jedes weitere Konto entsteht ohne
Adresse (gesperrte Nutzerentscheidung 2026-09-09, WINDOWS #15)
- Entscheider in upsertMappedUser (Sync) UND importUsersByDn (Handimport)
verdrahtet, damit der zweite Anlageweg nicht als Luecke bestehen bleibt
- LdapSyncResult um emailConflicts/skippedNoLogin/entryFailures erweitert;
rohe ORM-Ausnahmetexte gehen nur noch an logger.error, nie in den
Bericht (T-Q3-02)
- UserService.create nimmt die Adresse optional entgegen; Tender-Digest
und Instant-Alert ueberspringen Empfaenger ohne Adresse (continue)
- Fuenf neue Testfaelle vorab gegen den unveraenderten Bestand rot
gelaufen (erwartete Ursachen bestaetigt); 651/651 API-Tests gruen,
prisma validate und type-check sauber
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
- TenderEmailConfigService.testConnection(userId, dto) mit Rueckfall auf
gespeicherte, entschluesselte Zugangsdaten bei leeren Feldern
- TendersController: POST email-config/test, userId aus Auth-Kontext,
deklariert vor @Get(':id')
- Beide Provider (ImapProvider/ExchangeInboxProvider) optional angehaengt,
bestehende 2-Arg-Konstruktoraufrufe bleiben typkorrekt
- Reihenfolge-Waechter und IDOR-Testfall ergaenzt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
Die Gegenprobe im Browser hat drei Stellen gefunden, die der erste Durchgang nicht
erwischt hat — darunter zwei gut sichtbare Schaltflaechen:
Aenderungen speichern -> Änderungen speichern
Oeffnen -> Öffnen
Eine Aenderung ... -> Eine Änderung ...
Die Ursache ist dieselbe fuer alle drei und steckte im Waechter selbst: sein
Verdachtsmuster /(ae|oe|ue|ss)/ war case-sensitiv. "Aenderungen" beginnt mit "Ae",
nicht mit "ae", und ist deshalb durchgerutscht — der Waechter konnte gar nicht
anschlagen. Muster jetzt case-insensitiv; damit erfasst es auch die
grossgeschriebenen Formen.
Durch die schaerfere Pruefung melden sich neu die Abkuerzungen RSS, RSSGenerator und
SSL. Sie tragen ein doppeltes S ohne Umlaut-Bezug und stehen jetzt auf der
Positivliste.
Ausserdem zwei Meldungen des LDAP-Abgleichs korrigiert, die dem Administrator in der
Oberflaeche angezeigt werden (result.errors landet in der Fehlerliste der
LDAP-Seite): "ungueltiger ldapObjectGuid-Wert" und "Base-DN-Konfiguration pruefen.
Nicht geloescht." Drei Tests pinnen diese Texte bewusst und wurden mitgezogen.
Bewusst NICHT angefasst: die Warnung in crypto.service.ts. Sie geht ueber
logger.warn ins Protokoll und nicht an einen Nutzer.
Unabhaengig gegengeprueft: von allen Tokens in de.json, die ae/oe/ue tragen, ist
keines mehr eine Ersatzschreibung. 642 API-Tests und 225 Web-Tests gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
Korrigiert die deutschen Zeichenketten in beiden Mails aus
mail.service.ts (Betreff, Passwort-Reset-Fliesstext,
Willkommens-Mail) sowie die domaincheck-Modulbeschreibung in
domaincheck.seed.ts auf echte Umlaute. Wortlaut identisch, nur
Zeichen korrigiert; die englischen Zweige bleiben unveraendert.
seedModule() ist ein upsert mit update-Zweig und laeuft in
onModuleInit — ein API-Neustart schreibt die korrigierte
Beschreibung ueber bestehende Datenbankzeilen, ohne Migration oder
Backfill.
pnpm --filter @tessera/api run build: sauber.
pnpm --filter @tessera/api run type-check: sauber.
pnpm --filter @tessera/api run test: 46 Testdateien, 642 Tests gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
Nach der Formelaenderung (v1 -> v2) traegt jede Bestandszeile einen Hash
der alten Formel und wuerde nie wieder auf einen neuen treffen. Die
Produktionsdatenbank wird von Hand nicht angefasst, deshalb rechnet ein
neuer Dienst beim Start alle veralteten Zeilen automatisch nach — an der
Versionsmarke aus tender-fingerprint.ts erkannt, gleiche Bauform wie die
LDAP-Bind-Passwort-Nachverschluesselung.
- TenderFingerprintBackfillService: OnApplicationBootstrap, seitenweise
(500 Zeilen), pro Seite eine Transaktion, Fehler werden geloggt und
geschluckt statt geworfen
- In tenders.module.ts VOR TenderSchedulerService eingetragen, damit die
Nachrechnung vor der Cron-Registrierung laeuft
- Vier Tests: leere DB, alter+leerer Hash werden beide erfasst, zweiter
Lauf schreibt nichts mehr, Datenbankfehler wirft den Haken nicht
Gemessen an der lokalen Datenbank (16.255 Zeilen): Fingerabdruck-Gruppen
mit mehr als einer Zeile vorher=0, nachher=26, veraltete Zeilen
danach=0, davon Gruppen mit widersprechenden Wert-Groessenordnungen=2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
Zwei Schaeden aus derselben Wurzel behoben (WINDOWS.md #11, beim Messen
gefunden): der Aktualisierungszweig aendert Titel/Vergabestelle/Frist
einer bestehenden Zeile, schrieb den Fingerabdruck dabei aber nie mit —
der gespeicherte Wert driftete vom Inhalt der Zeile weg und Stufe 3 fand
die Zeile nie wieder (an der Live-DB an drei Gruppen gemessen).
- fingerprint wird einmal in resolve() berechnet und sowohl im
Anlegezweig als auch im Aktualisierungszweig geschrieben
- Neue Stufe-3-Pruefung: valueBucketsContradict() als Veto auf einen
Fingerabdruck-Treffer, exakter Groessenordnungsvergleich, keine
Aehnlichkeitssuche
- toNumberOrNull() haendelt Prisma Decimal und den einfachen
Zahlenwert der Test-Fakes gleichermassen
- Vier neue Tests fuer die Wert-Faelle, bestehender Update-Test um die
Fingerabdruck-Erwartung erweitert, alle vorherigen Tests bleiben gruen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
Die dritte Dedup-Stufe hashte bisher fuenf Segmente, darunter CPV-Division
und geschaetzten Wert — beide werden von Nicht-DOE-Quellen systematisch
nie geliefert, wodurch dieselbe Ausschreibung aus DOE und aus einem
Scraper nie denselben Fingerabdruck ergab (WINDOWS.md #11).
- tenderFingerprint() nimmt nur noch buyerName, title, deadlineAt entgegen
- Versionsmarke FINGERPRINT_VERSION_PREFIX ("v2:") vor dem Hash, damit
Task 3 veraltete Zeilen erkennen kann
- valueBucketsContradict() als eigene, exportierte Funktion fuer den
Wert-Veto in Task 2 — kein Hash-Bestandteil mehr
- backfill-tender-source.ts an die neue Signatur angepasst
- Testsuite auf das neue Verhalten umgeschrieben inkl. Goldwert-Test
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
- RssAdapter.fetchTenders tags every record from a feed with a tenantId
with the same D-13 ownerTenantId origin marking email-alert records
carry since Phase 14; platform-wide feeds (no tenantId) stay unmarked.
The pure parseFeed mapping is untouched — tagging happens in the
fan-out loop that knows which row a batch came from
- Extracted the service.bund.de seed out of TendersModule.onModuleInit
into seedServiceBundRssFeed() (tenders.seed.ts, same pattern as the
existing seedTendersModule), so the find-then-create idempotency added
in Task 1 is unit-tested directly instead of only via a Nest bootstrap
- New rss-feed-migration-sql.spec.ts: text-only check of the Task 1
migration file (nullable columns, dropped/created indexes, no
existing-row mutation, correct ordering)
- Files modified: apps/api/src/tenders/adapters/rss.adapter.ts, apps/api/src/tenders/tenders.module.ts, apps/api/src/tenders/tenders.seed.ts, apps/api/src/tenders/adapters/rss.adapter.spec.ts, apps/api/src/tenders/tenders.seed.spec.ts, apps/api/src/tenders/rss-feed-migration-sql.spec.ts
- remove(id, {userId, isAdmin}) replaces remove(id): single conditional
deleteMany (id AND (owned-by-caller OR admin-on-platform-feed)) — no
TOCTOU window, ownership check lives in the DB condition. Deletes
nothing -> NotFoundException (never Forbidden, no existence leak)
- createForUser rejects a caller's 21st personal feed with a clear
German message (T-17-10); platform-wide feeds are not counted
- DELETE /rss-feeds/:feedId moves from @Roles(ADMIN,SUPER_ADMIN) to
@UseModule('tender-radar') — ownership check does the gating now
- Tests use a Prisma double that actually evaluates the where condition
(not a double that always "succeeds") for both deleteMany and count
- Files modified: apps/api/src/tenders/tender-rss-feed.service.ts, apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.spec.ts
- TenderRssFeedSource.userId/tenantId (nullable): null = platform-wide
(admin-managed, includes the existing service.bund.de default),
set = personal feed owned by exactly one user
- Migration replaces url @unique with @@unique([userId, url]) — two
users can now follow the same address independently; existing rows
keep an empty owner (platform-wide, unchanged behavior)
- Service: listForUser/createForUser/createPlatform replace list/create
- Controller: GET/POST /rss-feeds move from @Roles(ADMIN,SUPER_ADMIN) to
@UseModule('tender-radar'); POST with scope:'platform' still requires
ADMIN/SUPER_ADMIN, checked inline (T-17-08)
- tenders.module.ts seed switched from upsert-on-url to find-then-create
(Rule 3, pulled forward from Task 3): the new compound unique index
requires a non-null userId in Prisma's generated type, so a
platform-wide row can no longer be addressed via upsert
- Files modified: apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260812110000_tender_rss_feed_owner/migration.sql, apps/api/src/tenders/tender-rss-feed.service.ts, apps/api/src/tenders/dto/tender-rss-feed.dto.ts, apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.module.ts, apps/api/src/tenders/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.spec.ts
Der mandantenuebergreifende Sammelabruf in email-alert.adapter.ts bleibt
mechanisch unveraendert (findMany({isActive:true}) in einem Zug,
Fehlerbehandlung je Zeile) — geaendert wird nur die Warnmeldung (Zeilen-id
+ Besitzer statt Mandant, T-17-03) und die Klassendoku.
- Neue Tests: zwei aktive Postfaecher DESSELBEN Mandanten werden beide mit
ihren jeweils eigenen Zugangsdaten abgeholt; ein kaputtes Postfach
blockiert das andere nicht und protokolliert eine Warnung ohne
Zugangsdaten/Adresse; die Herkunftsmarkierung folgt dem Mandantenfeld
der jeweiligen Zeile (zwei Mandanten -> zwei Werte). Erwartungswerte von
Hand geschrieben, nicht ueber die Produktivfunktion erzeugt.
- tender-email-config.service.spec.ts (bereits in der Task-2-Migration
mitgeliefert) deckt zusaetzlich: tenantId wird beim Anlegen mitgeschrieben,
zwei Nutzer desselben Mandanten erzeugen zwei Zeilen statt eine zu
ueberschreiben.
- email-config-migration-sql.spec.ts (neu, Vorbild
doe-url-migration-sql.spec.ts): prueft die Reihenfolge der
Hand-Migration textuell — Zuordnung vor Loeschung, Pflicht erst nach
Befuellung, alte Eindeutigkeit runter/neue rauf, gewoehnlicher
tenantId-Index bleibt stehen.
src/tenders: 335/335 gruen. API gesamt: 603/603. Web gesamt: 192/192.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Alert-Postfach gehoert jetzt dem einzelnen Nutzer (userId @unique) statt
dem Mandanten (D-01) — ein zweiter Kollege desselben Mandanten kann sein
eigenes Postfach anbinden. tenantId bleibt denormalisiert (SMTP-Aufloesung,
Herkunftsmarkierung), wird auf create UND update mitgeschrieben.
- Handgeschriebene Migration (prisma migrate dev verweigert die
nicht-interaktive Shell): befuellt Bestandszeilen mit dem aeltesten
aktiven Administrator ihres Mandanten, entfernt verwaiste Zeilen ohne
Administrator, ersetzt die tenantId-Eindeutigkeit durch userId.
Lokal getestet (0 Bestandszeilen lokal und auf alpha — Zaehlung im
Task-1-Checkpoint), Index-Ergebnis verifiziert.
- TenderEmailConfigService.getConfigForApi/saveConfig auf userId als
Schluessel umgestellt; saveConfig nimmt {userId, tenantId}.
- TendersController: email-config-Routen von @Roles(ADMIN,SUPER_ADMIN)
auf @UseModule('tender-radar') umgestellt (Postfach ist jetzt
Nutzereinstellung); Route-Reihenfolge vor @Get(':id') unveraendert.
- Neue Seite /modules/tender-radar/my-sources ("Meine Quellen") mit dem
unveraenderten EmailAlertConfigForm; Hinweistext benennt D-05 (Tender
bleibt plattform-global — nur wer Quellen einspeist aendert sich).
- tenders.controller.spec.ts an neue Service-Signatur angepasst (Rule 3,
nicht im Plan gelistet, aber zum Kompilieren/Bestehen erforderlich).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CALENDAR_ENCRYPTION_KEY was named after the calendar module because that
module needed encryption first, in Phase 5. Every feature since has shared the
same key -- SMTP, the DKV and tender mailboxes, and as of today the LDAP bind
password -- so the name has been describing one of five users rather than the
thing itself, and each new feature inherited the confusion.
TESSERA_ENCRYPTION_KEY is the name now. The old one is still read, because
renaming outright would stop every existing installation at the next start:
their .env carries the old name, and compose was just made to fail hard on a
missing key. When only the old name is present the API logs a deprecation
warning naming both, and when both are set the new one wins -- otherwise a
half-migrated .env would encrypt with one key and decrypt with the other.
CalendarCryptoService becomes CryptoService in its own global CryptoModule.
Four modules used to import CalendarModule purely to reach the provider, which
read as a dependency on calendars where there was none; that import is gone.
Compose keeps the hard failure: without either name the stack refuses to
start. Verified in both files for all three cases -- neither name set (abort),
only the old name (starts), only the new name (starts).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The LDAP bind password was the only credential still stored in clear text.
CalendarSource, SmtpConfig, DkvModuleConfig and TenderEmailConfig have been
AES-256-GCM encrypted for a while; LDAP simply predated the encryption service
and was never brought along.
Hashing is not an option here: Tessera has to replay this password to bind
against the directory, so it must stay recoverable. Encryption at rest covers
the case a hash cannot help with either way -- a database dump or backup
leaving the host without the key, which lives in the application environment.
It does not protect against a compromised host, and does not pretend to.
Reuses CalendarCryptoService, the same provider SettingsModule, DkvModule and
TendersModule already inject, rather than introducing a second crypto path.
The name is a historical accident and is noted as such in LdapModule; renaming
it touches five modules and belongs in its own change.
Decryption sits in getConfig()/getAllActiveConfigs(), the two methods every
consumer already goes through, so callers keep reading a plain `bindPassword`
and the controller keeps masking it to '********' in responses.
The migration only renames the column -- SQL cannot encrypt, since the key is
not in the database. An idempotent bootstrap backfill encrypts rows written
before this change, and until it has run the read path passes a legacy
plaintext value through unchanged so the sync does not break in that window.
A failed decrypt throws rather than returning null: a wrong key must not read
as "no password configured" and silently turn an authenticated bind into an
anonymous one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The doe-opendata adapter stored the OCDS document's own `uri` as sourceUrl.
That is the API address of the record and serves OCDS JSON by design, so
anyone following the link from the results list, the detail view, or an alert
mail landed on raw JSON instead of the notice.
Build the human-readable page from the notice id the row already carries
instead. `/ui/de/search/details?noticeId=...` is the redirect target of
`/ui/de/notices/...`, so it needs no redirect. Verified in a browser for both
id shapes the feed uses -- numeric (25673764 -> "Feuerwehr-Geraetehaus Miehlen
Fliesenarbeiten") and UUID (7085ba12-... -> "Holzfassade"). The page is a
single-page app that answers 200 with an identical shell for any id, so this
had to be checked on rendered content; a status code proves nothing.
The adapter alone only fixes new ingests, so a backfill migration rewrites the
rows already stored -- in Tender and in TenderSource, since the detail view
lists per-source links separately. It touches only rows still pointing at
/api/notices/ and only ids of a shape that was actually verified, which makes
it idempotent and keeps an unexpected id from being pasted into a URL. Counted
read-only against the live database beforehand: 2846 DOE rows affected, none
skipped.
Closes the 2026-08-05 backlog item, which was deliberately held until Phase 16
was done.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The existence sweep in syncBoundGroupsForTenant() built its filter by
interpolating a byte-wise \xx escape of the stored objectGUID into a filter
string. ldapts parses that string before encoding it and does not turn the
escape sequences back into the bytes they stand for, so the assertion value
that reached the directory was a different value and matched nothing.
Measured read-only against a real Active Directory on 2026-08-11, probing a
group whose GUID had just been read from that same directory:
(objectGUID=\1e\4b...) 0 hits
(objectGUID=\1E\4B...) 0 hits
EqualityFilter{attribute, value: <16 bytes>} 1 hit, correct DN
(cn=Domain Admins) [control] 1 hit
Both the narrow base-DN sweep and the wider WR-03 move-detection sweep shared
that filter, so neither could ever hit: every AD-bound group looked deleted and
would have been removed together with its GroupMembership and ModuleGrant rows
on the first real sync, after handing off the default-group marker.
Build the filter as an EqualityFilter over the raw Buffer instead, and drop
escapeLdapFilterBuffer() -- it has no remaining caller and is the trap the code
walked into. escapeLdapFilterValue() is untouched: escaping STRING values into
a filter is correct and still in use.
The existing spec mocks matched on the escaped string, which is how the broken
shape passed review. They now match on the filter object's Buffer value, and
two added tests fail if a stringly-typed objectGUID filter ever comes back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
syncBoundGroupsForTenant()'s existence sweep only searched the configured
base DNs, so an AD group MOVED to an OU outside that subtree (still
present in the directory) was indistinguishable from a genuine
disappearance and got deleted along with its memberships/module grants —
a silent access loss from a non-destructive AD operation, and a bigger
blast radius than D-05 ("group genuinely gone") was accepted for.
Before concluding disappearance, a second (objectGUID=...) sweep now runs
against each base DN's own domain root (skipped when a base DN already IS
its domain root — the common case, nothing wider to search). A hit there
is reported as an error line and the group is left untouched; only when
the wide sweep also finds nothing is deletion (SC-4/D-05/D-06) actually
established — mirroring the existing conservative stance already taken
for a legacy binding whose DN no longer resolves. Deleting on uncertainty
was the failure mode; this closes it without widening it.