Drei Aufgaben: zuerst messen (fuenf Policies wortgleich aus der ausgelieferten Migration, dazu die drei Sonderfaelle dieses Bereichs — fehlende Benutzerdimension, nullbarer Mandant bei den RSS-Quellen, Einfuegen auf eine unsichtbare Zeile), dann die fuenf Nutzerdienste binden, dann die Je-Treffer-Haelften der beiden Hintergrunddienste und beide Dokumente schliessen. Zur Planungszeit gemessen statt zitiert: 743 Tests gruen, Typpruefung sauber, Wegwerf-Werkzeug 23/23. Von den 62 Rohtreffern ist genau einer kein Modellzugriff. Der Bereich hat keine mandantengebundene Transaktion, das Hilfsmittel aus 260909-jts wird hier nicht gebraucht. Keine der sieben Testdateien kennt die Erweiterung — sie waeren nach der Umstellung aus dem falschen Grund rot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
53 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-260909-laa | 01 | execute | 1 | true |
|
|
|
|
Zweck: Dieser Bereich haelt Geschaeftsgeheimnisse einzelner Nutzer — welche Ausschreibungen ein Unternehmen beobachtet, welche es gespeichert, welche es verworfen hat, und die Postfach-Zugangsdaten, aus denen es sie speist. Ein Quer-Lesen ist hier kein Datenschutzmangel, sondern Wettbewerbsspionage. Dazu kommt eine Fehlerform, die die beiden vorherigen Bereiche nicht hatten: zwei Benachrichtigungswege, die bei zu kleinem Leseergebnis nicht falsch handeln, sondern GAR NICHT — und niemand meldet eine Warnung, die nie ankam.
Ergebnis: Die Kritikschrift bekommt einen tenders-Abschnitt samt der neuen,
lautlosen Fehlerform. Das Messwerkzeug bekommt die fuenf Policies dieses Bereichs
und drei Messungen, die es bisher nirgends gab: dass die Policies keine
Benutzerdimension haben, dass eine plattformweite Zeile ohne Mandant unter jedem
Kontext unsichtbar ist, und wie sich ein gebundenes upsert auf eine unsichtbare
Zeile verhaelt. Fuenf Nutzerdienste und die Je-Treffer-Haelften zweier
Hintergrunddienste sind gebunden, zwoelf Paare sind nachweislich und begruendet
NICHT gebunden, und das Klassifikationsdokument weist beides maschinell nach.
Aufgabe 1 fuehrt bewusst, obwohl sie keinen Nutzernutzen liefert: sie ist der Durchstich durch die gesamte Kette (ausgelieferte Policy -> Rolle ohne BYPASSRLS -> Bindungsmuster -> die drei Sonderfaelle dieses Bereichs) und beantwortet die Fragen, auf denen die Umstellung ruht, mit einer Messung statt mit einer Annahme. Erst danach wird Dienstcode angefasst.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
<planning_time_findings>
Alles Folgende wurde am 2026-09-09 zur Planungszeit am lebenden Baum gemessen. Die Zahlen und Zeilenangaben aus dem Auftrag waren Hinweise zum Aufschlagen, keine Aenderungsvollmacht — jede Fundstelle wurde einzeln aufgeschlagen.
Ausgangsstand (jetzt gemessen, nicht aus einem Bericht zitiert):
npm --prefix apps/api run test-> 53 Dateien, 743 Tests, gruen, 5,19 s.npm --prefix apps/api run test -- src/tenders-> 29 Dateien, 378 Tests, gruen.npm --prefix apps/api run type-check-> Rueckgabewert 0.docker inspect tessera-ctl-db-1 ...-> 172.19.0.2. Eine Container-Adresse ist veraenderlich und wird bei der Ausfuehrung neu ermittelt, nicht von hier abgeschrieben.TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs-> "Alle 23 Pruefungen bestanden.", Rueckgabewert 0. Die Lastprobe lief mit 24 Verletzungen von 40 fuer Form (ii) und 0 von 40 fuer Form (iii) — das Fundament ist damit JETZT belegt.git statussauber, HEAD4cf7cea.
Die 62 Rohtreffer, in die Treffer hineingesehen. Die Bereichsuebersicht misst mit
grep -ro "this\.prisma\.[a-zA-Z]*"; [a-zA-Z]* erlaubt auch null Zeichen. Gemessen:
62 Rohtreffer, davon genau einer kein Modellzugriff —
tender-fingerprint-backfill.service.ts:89, this.prisma.$transaction. Tatsaechliche
Modellzugriffe: 61. Die dokumentierte Bereichszahl 62 ist als Rohtrefferzahl
korrekt, taugt aber wieder nicht als Arbeitsvorrat. (Beim Bereich groups waren es
drei solche Treffer, hier einer — die Korrektur war also noetig und nicht
uebertragbar.)
Befund A — dieser Bereich hat KEINE mandantengebundene Transaktion, und das ist
die Antwort auf den Vorbehalt im Kopf der Erweiterung.
grep -rn '\$transaction(\s*async' apps/api/src --include=*.ts | grep -v spec
liefert ausserhalb von prisma-tenant.extension.ts null Treffer — nach der
groups-Umstellung gibt es im gesamten Quelltext keine interaktive Transaktion mehr
ausserhalb des Hilfsmittels selbst. Die einzige Transaktion in tenders ist die
Array-Form in tender-fingerprint-backfill.service.ts auf der plattformweiten
Tabelle Tender (D-03) und damit ausserhalb jeder Mandantenbindung. Der
Kopfkommentar von prisma-tenant.extension.ts verlangt woertlich, vor jedem NEUEN
Fall mit eigener Transaktion erneut zu messen — in diesem Bereich faellt kein
solcher Fall an. withTenantTransaction() wird hier deshalb NICHT gebraucht und
darf auch nicht eingefuehrt werden: die beiden mehrschrittigen Stellen
(saveConfig liest und schreibt, createForUser zaehlt und schreibt) sind HEUTE
nicht atomar; sie in eine Transaktion zu heben waere eine Verhaltensaenderung
jenseits dieses Auftrags.
Befund B — die fuenf umzustellenden Dienste, einzeln aufgeschlagen. Alle fuenf
sind Nutzer-CRUD; der Mandant ist an jeder Aufrufstelle bereits bekannt (siehe
Befund C). Alle betroffenen Tabellen ausser TenderRssFeedSource haben ein NICHT
nullbares tenantId (in apps/api/prisma/schema.prisma nachgesehen).
| Datei | Modellzugriffe | Methoden ohne heutigen tenantId-Parameter |
|---|---|---|
tender-saved-search.service.ts |
6 auf tenderSavedSearch |
list, update, remove |
tender-rss-feed.service.ts |
5 auf tenderRssFeedSource |
listForUser, createPlatform, remove |
tender-email-config.service.ts |
5 auf tenderEmailConfig |
getConfigForApi (2 Zugriffe), testConnection |
tender-triage.service.ts |
3 auf tenderTriage |
listForUser, favoriteIds |
tender-notification-pref.service.ts |
2 auf tenderNotificationPref |
getForUser |
Befund C — der Mandant liegt an jeder Aufrufstelle bereits vor und wird nur
weggeworfen. TendersController.extractTriageContext(req) liefert
{ userId, tenantId, role } und wirft 403, wenn eines fehlt. Zehn Aufrufstellen
destrukturieren heute nur { userId } bzw. { userId, role } und werfen den
Mandanten weg (Zeilen 178, 267, 323, 346, 385, 460, 506, 540, 555, 575 — zur
Planungszeit gezaehlt, bei der Ausfuehrung neu aufschlagen). Kein einziger neuer
Aufloesungsweg ist noetig; es ist ein Durchreichen, kein Umbau der Steuerung.
Befund D — WINDOWS #19 ist hier keine ferne Sorge, sondern der Grund, warum drei
RSS-Pfade NICHT binden duerfen. TenderRssFeedSource.tenantId ist nullbar; eine
plattformweite Quelle (userId = null, tenantId = null, darunter die geseedete
service.bund.de-Quelle) traegt keinen Mandanten. Die ausgelieferte Policy lautet
"tenantId" = current_tenant_id() und vergleicht NULL nie gleich. Daraus folgt
fuer die drei Pfade, die plattformweite Zeilen beruehren:
listForUserliest{ OR: [{userId: null}, {userId}] }— gebunden verschwaenden nach dem Scharfschalten die plattformweiten Quellen fuer JEDEN Mandanten.createPlatformschreibttenantId = null— ein gebundenes Einfuegen liefe in die WITH-CHECK-Wirkung derselben Policy.removedeckt den Verwaltungsfall ueber{userId: null}ab; gebunden koennte niemand mehr eine plattformweite Quelle entfernen. Diesen einendeleteManyin zwei Anweisungen zu zerlegen, um die persoenliche Haelfte zu binden, wuerde genau das Pruef-/Nutzungsfenster wieder oeffnen, das der Dateikopf ausdruecklich vermeidet — also nicht tun.
Nur createForUser (Zaehler + Anlage, beide ausschliesslich auf persoenlichen
Zeilen mit gesetztem Mandanten) kann und muss binden. Die Datei endet damit im
Stand gemischt. Das ist die Bestaetigung der Grenze, nicht ihr Ueberschreiten:
die Policy-Semantik wird NICHT angefasst, keine Migration geschrieben.
Befund E — die Policies dieses Bereichs haben keine Benutzerdimension. Alle
fuenf lauten schlicht "tenantId" = current_tenant_id()
(20260909140000_rls_remaining_tenant_tables, wortgleich nachgelesen). Zwei Nutzer
DESSELBEN Mandanten sind fuereinander damit vollstaendig sichtbar. Der Schutz gegen
Quer-Lesen zwischen Nutzern — Suchprofile, Triage-Zustand, Postfachanbindung —
haengt ausschliesslich an der anwendungsseitigen userId-Filterung, die alle fuenf
Dienste heute schon fuehren. Sie darf bei der Umstellung nicht mit dem Argument
"macht jetzt ohnehin die Datenbank" entfallen. Dieselbe Klasse Befund wie T-JTS-02/
T-JTS-03 im Bereich groups, hier aber mit hoeherem Einsatz, weil es
Geschaeftsgeheimnisse sind. Zu messen, nicht aus dem Policy-Text zu schliessen.
Befund F — die Kehrseite der Bindung: upsert auf einen Schluessel ohne
Mandantendimension. Drei Schreibpfade nutzen upsert auf einem @@unique, das
keinen Mandanten enthaelt: tenderEmailConfig (userId @unique),
tenderNotificationPref (userId @unique), tenderTriage
(@@unique([userId, tenderId])), dazu tenderMatch
(@@unique([tenderId, savedSearchId])) in Aufgabe 3. Ist die vorhandene Zeile unter
dem gebundenen Kontext unsichtbar (weil ihr denormalisiertes tenantId veraltet
ist — genau der Fall, den der Kopf von tender-email-config.service.ts selbst
benennt: "a user's tenant can in principle change"), faellt upsert in den
Anlage-Zweig und laeuft in die plattformweite Eindeutigkeitsbedingung. Aus einem
stillen Ueberschreiben wird ein harter Fehler. Das ist als Richtung besser als ein
Datenleck, aber es muss als verstaendliche Meldung herauskommen und nicht als 500.
tender-saved-search.service.ts fuehrt das Muster bereits vor (P2002 ->
ConflictException mit deutschem Text) — abschreiben statt neu erfinden.
Befund G — keine Luecke der ldap-Klasse (Aufloesung ueber die Kennung allein), mit
einer Einschraenkung. Alle Besitzpruefungen wurden einzeln nachgesehen:
tenderSavedSearch.update/remove lesen zwar ueber id allein, pruefen danach aber
existing.userId !== userId und kollabieren Fehlen und Fremdbesitz zu derselben
404 — das ist das gewuenschte Muster. tenderRssFeedSource.remove ist bereits ein
einziger bedingter deleteMany mit der Besitzbedingung in der Datenbank. Triage,
Praeferenz und Postfach sind ueber userId verschluesselt. Es gibt hier also
KEINE Wiederholung des ldap-Fundes. Die eine Beobachtung, die trotzdem gehoert
festgehalten zu werden: ein Administrator eines beliebigen Mandanten kann ueber
remove eine PLATTFORMWEITE RSS-Quelle entfernen, die alle Mandanten speist. Das
ist eine Produkt-/Zustaendigkeitsfrage im Umfeld von WINDOWS #19, KEIN Auftrag
dieser Aufgabe — festhalten, nicht reparieren.
Befund H — die Testlage: die zweite Fehlerform, nicht die erste.
grep -rn "forTenant\|prisma-tenant\|vi.mock" apps/api/src/tenders/*.spec.ts
liefert fuer alle sieben betroffenen Testdateien keinen einzigen Treffer auf die
Erweiterung. Es gibt also keinen Identitaets-Mock wie bei ldap — es gibt gar
keinen, genau wie bei groups. Nach der Umstellung liefe
forTenant(this.prisma, tenantId) gegen einen handgeschriebenen In-Memory-Fake ohne
$extends, und JEDER Test der Datei stuerzte ab: rot aus dem falschen Grund. Alle
sieben Dateien brauchen den Zwei-Client-Nachweis aus 260909-jts (__makeBoundClient
ueber DEMSELBEN Speicher, forTenant gemockt). Die vorhandenen Fakes sind
wiederverwendbar und werden nicht weggeworfen.
Eine achte Datei ist betroffen, aber anders: tenders.controller.spec.ts uebergibt
ausschliesslich FAKE-Dienste (makeFakeTriageService() usw.), nie die echten. Sie
braucht keinen Mock der Erweiterung, sondern nur nachgezogene Erwartungen an die um
tenantId erweiterten Aufrufe.
Befund I — eine bestehende Schutzpruefung, die man mit einem Kommentar rot machen
kann. tender-ingestion.service.spec.ts enthaelt den Test "never calls
forTenant()", der den QUELLTEXT von tender-ingestion.service.ts liest und gegen
ein Vorkommen dieses Bezeichners prueft. In dieser einen Datei darf deshalb auch
kein ERKLAERENDER Kommentar den Bezeichner nennen. Sie steht ohnehin auf der
Nicht-Anfassen-Liste; hier nur festgehalten, damit niemand sie beim Nachziehen der
Begruendungen "freundlich kommentiert" und den Lauf rot macht.
Befund J — welcher Code Leere als Abwesenheit deutet, und die neue lautlose Form (Vorarbeit fuer Aufgabe 1, dort auszuformulieren und zu ergaenzen, nicht abzuschreiben).
Sichtbare Formen (Anzeige bleibt leer, jemand merkt es):
tenderSavedSearchService.list (leere Profilliste), tenderTriageService.listForUser
(keine Gelesen-/Favoriten-Markierung in der Trefferliste),
tenderNotificationPrefService.getForUser (Sonderfall: kein Treffer bedeutet hier
nicht "leer", sondern der Vorgabewert daily — ein zu kleines Leseergebnis setzt
einen Nutzer, der off gewaehlt hat, stillschweigend auf taeglich zurueck; die eine
Stelle des Bereichs, an der zu wenig Lesen zu MEHR Handlung fuehrt),
tenderEmailConfigService.getConfigForApi (Oberflaeche meldet "kein Postfach" fuer
einen Nutzer, der eines hat), tenderRssFeedSourceService.listForUser/remove
(leere Quellenliste, bzw. 404 beim Entfernen).
Lautlose Formen — die zusaetzliche Fehlerform dieses Bereichs, fuenf Stellen:
tender-digest.scheduler.ts if (!candidates.length) return; (der GESAMTE Digest
tut fuer alle Mandanten nichts), if (!matches.length) continue; und
if (!user || !user.email) continue; (dieser Nutzer bekommt keine Post);
tender-matching.service.ts if (!fresh.length) continue; und
if (!user || !user.email) continue; (dieser Nutzer bekommt keinen Sofort-Alarm).
Keine dieser Stellen protokolliert etwas. Eine ausbleibende Warnung erzeugt keine
Fehlermeldung, keinen Protokolleintrag und keine Beschwerde.
Entlastung in dieselbe Richtung, ebenfalls nachgesehen statt geschlossen: weil
notifiedAt nur nach erfolgreichem Versand gestempelt wird, bleiben die betroffenen
TenderMatch-Zeilen auf notifiedAt IS NULL stehen und werden bei jedem Lauf erneut
versucht. Es geht also nichts verloren, es kommt nur nichts an — und daraus ergibt
sich das einzige nachpruefbare Signal dieser Fehlerform: eine wachsende Zahl von
TenderMatch-Zeilen mit notifiedAt IS NULL bei gleichzeitig fehlendem
Versandprotokoll. Dieses Signal gehoert in die Vorabpruefung von Etappe 4
(rls-preflight.mjs), NICHT in diesen Durchlauf.
Eine Laufzeitwarnung an den fuenf Stellen wurde erwogen und VERWORFEN, aus demselben
Grund wie bei getAllActiveConfigs im ldap-Durchlauf: "kein Konto mit Adresse" ist
seit WINDOWS #15 ein regulaerer Zustand, und der Digest laeuft taeglich. Eine
Warnung waere Dauerlaerm und verloere ihr Signal.
Befund K — die Abhaengigkeit vom noch nicht umgestellten Bereich settings.
tender-mail.service.ts holt die SMTP-Angaben ueber
SettingsService.getDecryptedSmtpConfig(tenantId); fehlt sie, liefern beide
Versandmethoden false und der Aufrufer laesst notifiedAt auf NULL stehen. Der
Bereich settings (4 Rohtreffer) ist noch nicht umgestellt. Nach dem Scharfschalten
faende diese ungebundene Abfrage keine SMTP-Zeile mehr — Ergebnis: kein Versand fuer
niemanden, mit Wiederholung bei jedem Lauf. Das ist eine Reihenfolgebedingung fuer
Etappe 4, genau wie Befund D des ldap-Durchlaufs es fuer groups war. Festhalten,
nicht hier loesen.
Befund L — die zwoelf Paare, die nicht angefasst werden duerfen. Zehn Paare der
Klasse keine-mandantengebundene-tabelle (tender-dedup.service.ts x2,
tender-fingerprint-backfill.service.ts, tender-ingestion.service.ts x2,
tender-matching.service.ts/tender, tender-scheduler.service.ts,
tenders.controller.ts x2, tenders.module.ts) und zwei der Klasse
bewusst-uebergreifend (adapters/email-alert.adapter.ts,
adapters/rss.adapter.ts). Stichprobenweise gegen D-03 und die Dateikoepfe
geprueft: tender-dedup.service.ts und tender-ingestion.service.ts tragen die
Anweisung im Kopf ausgeschrieben, tenders.module.ts ebenso, beide Adapter
begruenden ihren Fan-out im Dateikopf. Bei der Ausfuehrung ist jede der zwoelf
Zeilen einzeln gegen D-03 bzw. den Dateikopf zu pruefen, nicht gegen diese Aufzaehlung.
Gewaehltes Muster (bewusst, nicht stillschweigend): Der Mandantenkontext wird
weiterhin IM DIENST erzeugt (const tenantPrisma = forTenant(this.prisma, tenantId) as any;),
wie in ldap, groups und auth.service.ts. Der offene Befund req.tenantPrisma
(gesetzt in tenant.middleware.ts und tenant.guard.ts, nirgends gelesen) wird auch
von diesem Durchlauf AUSDRUECKLICH NICHT entschieden. Die Namenskonvention
tenantPrisma wird eingehalten, weil die Rohtrefferzaehlung des
Klassifikationsdokuments an ihr haengt.
Nicht angefasst: apps/api/prisma/schema.prisma, apps/api/prisma/migrations/,
alle vier Compose-Dateien, .env.example, .env.prod.example. DATABASE_URL bleibt
auf der Rolle tessera mit BYPASSRLS — das Scharfschalten ist Etappe 4. Am
Verzeichnis (AD) wird nichts geaendert. apps/web wird nicht beruehrt: die
Umstellung ist rein dienstintern, kein Vertrag einer HTTP-Route aendert sich.
</planning_time_findings>
TEIL 1, apps/api/scripts/rls-scratch-check.mjs: einen fuenften Abschnitt
runTendersAreaChecks(adminUrl, scratchRoleUrl, results) nach dem Vorbild des
vorhandenen runGroupsAreaChecks ergaenzen und in main() nach diesem, aber VOR
runTransactionShapeMeasurement aufrufen — die Transaktionsmessung setzt auf den von
runGroupsAreaChecks angelegten Tabellen auf und darf ihre Voraussetzung nicht
verlieren; das ist beim Einhaengen zu pruefen, nicht anzunehmen.
Die fuenf Policies werden NICHT im Werkzeug neu getippt. Sie kommen alle aus dem
Migrationsverzeichnis, das auf _rls_remaining_tenant_tables endet — das vorhandene
readRemainingTenantTablesMigrationSql() liest es bereits, extractPolicySql()
schneidet je Tabelle heraus. Gebraucht werden TenderEmailConfig,
TenderNotificationPref, TenderRssFeedSource, TenderSavedSearch, TenderTriage.
Findet die Extraktion eine der fuenf nicht, meldet der Abschnitt eine
FEHLGESCHLAGENE Pruefung tenders-policies-aus-migration-gefunden und bricht ab —
das Werkzeug darf nicht still mit einer geratenen Policy weitermessen.
Der Abschnitt legt in der Wegwerf-Datenbank schlanke Tabellen an, die genau die
Spalten tragen, die die Policies und die Messungen brauchen. Die Nullbarkeit von
TenderRssFeedSource."tenantId" und die drei Eindeutigkeitsbedingungen ohne
Mandantendimension sind dabei KEIN Beiwerk, sondern der Gegenstand: sie muessen
angelegt werden wie im echten Schema (TenderEmailConfig.userId eindeutig,
TenderNotificationPref.userId eindeutig, TenderTriage(userId, tenderId)
eindeutig). Danach ENABLE plus FORCE ROW LEVEL SECURITY, die fuenf extrahierten
Policies, die Rechtevergabe an die Wegwerf-Rolle und Testzeilen: je Mandant
(TENANT-A, TENANT-B) je eine Zeile pro Tabelle, in TenderSavedSearch fuer TENANT-A
ZWEI Zeilen von ZWEI verschiedenen Nutzern, und in TenderRssFeedSource zusaetzlich
eine plattformweite Zeile ohne Mandanten und ohne Besitzer.
Gemessen wird unter der Rolle ohne BYPASSRLS ueber das vorhandene
forTenantQuery-Hilfsmittel, mit diesen Kennungen:
tendersavedsearch-gebunden-nur-eigener-mandant— der gebundene SELECT unter TENANT-A liefert die Zeilen von A und keine von B.tendersavedsearch-ungebunden-null-zeilen— DERSELBE SELECT ohne vorher gesetzten Kontext liefert null Zeilen. Das ist die Belegzeile, die den ganzen Abschnitt der Kritikschrift traegt; sie muss an der echten, ausgelieferten Policy haengen.tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar— der gebundene SELECT unter TENANT-A liefert AUCH die Zeile des zweiten Nutzers. Diese Pruefung gilt als bestanden, wenn die fremde Zeile sichtbar ist: sie belegt Befund E, naemlich dass die Policy keine Benutzerdimension hat. Der Meldetext sagt das ausdruecklich UND nennt die Folge — die anwendungsseitigeuserId-Filterung bleibt der einzige Schutz gegen Quer-Lesen zwischen Nutzern und darf nicht entfernt werden. Ohne diesen Zusatz koennte eine bestandene Pruefung mit "ist abgesichert" verwechselt werden.tenderemailconfig-gebunden-nur-eigener-mandant,tendertriage-gebunden-nur-eigener-mandant,tendernotificationpref-gebunden-nur-eigener-mandant— je eine Pruefung nach demselben Muster wie die erste.tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar— der gebundene SELECT liefert unter TENANT-A UND unter TENANT-B jeweils NICHT die plattformweite Zeile ohne Mandanten. Bestanden, wenn sie unter beiden Kontexten fehlt. Der Meldetext benennt WINDOWS #19 und die Folge:listForUserdarf nicht gebunden werden, solange die Policy-Semantik unveraendert ist.tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt— ein gebundenes INSERT unter TENANT-A mittenantId = NULLwird abgewiesen; die Abweisung ist das bestandene Ergebnis. Der Meldetext nennt die Folge:createPlatformdarf nicht gebunden werden.tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit— unter TENANT-A ein INSERT fuer ein Paar (userId,tenderId), dessen Zeile existiert, aber zu TENANT-B gehoert und daher unsichtbar ist. Bestanden, wenn der Fehler eine Verletzung der Eindeutigkeitsbedingung ist (nicht eine Policy-Abweisung). Der Meldetext benennt Befund F und die Folge fuer Aufgabe 2: aus einem stillen Ueberschreiben wird ein harter Fehler, der als verstaendliche Meldung herauskommen muss.
Das Werkzeug raeumt weiterhin ausschliesslich seine fest verdrahtete Wegwerf-Datenbank ab und bekommt keine steuerbaren Namen (T-EOR-07 bleibt gueltig). Kein bestehender Abschnitt wird veraendert; alle 23 bisherigen Pruefungen muessen unveraendert weiterlaufen.
TEIL 2, Beleg statt Behauptung fuer Befund A: nachmessen, dass dieser Bereich keine
mandantengebundene Transaktion enthaelt — mit
grep -rn '\$transaction(' apps/api/src/tenders --include=*.ts | grep -v spec. Das
Ergebnis (Zahl der Treffer, betroffene Datei, Form) wird in der Kritikschrift
festgehalten, samt der Feststellung, dass der im Kopf von
prisma-tenant.extension.ts verlangte erneute Test fuer diesen Bereich damit
beantwortet ist: kein neuer Fall, withTenantTransaction() wird nicht gebraucht.
Faellt das Ergebnis anders aus als in Befund A beschrieben, gilt die MESSUNG, und
die Abweichung wird ausgeschrieben, bevor Aufgabe 2 beginnt.
TEIL 3, docs/mandantentrennung-etappe2-fehlerrichtung.md um einen Abschnitt
## Bereich tenders ERWEITERN, nicht ein zweites Dokument anlegen. Die Leitfrage
aus Abschnitt (a) gilt unveraendert weiter und wird nicht wiederholt; der neue
Abschnitt verweist darauf und haelt im Kopf fest, dass er den Bereich tenders zum
Zeitpunkt seiner Umstellung beschreibt (Quick-Task 260909-laa).
Inhalt, in ganzen Saetzen auf Deutsch, mit derselben Gliederung wie der groups-Abschnitt:
(t1) Die Messung — die TATSAECHLICH beobachtete Ausgabe des Laufs, hineinkopiert, nicht nacherzaehlt, mit Datum und der bei der Ausfuehrung ermittelten Adresse. Die Belegzeile ausdruecklich benennen.
(t2) Signaltabelle je umgestelltem Pfad: Pfad, Verhalten bei zu wenig Ergebnis,
konkretes Signal mit Ort. Es muessen alle in Aufgabe 2 und 3 umgestellten Pfade
vorkommen, inklusive des Sonderfalls getForUser (Vorgabewert daily statt leer)
und der drei RSS-Pfade, die bewusst ungebunden bleiben und nach dem Scharfschalten
eine leere Liste bzw. eine 404 liefern.
(t3) Welcher Code Leere als Abwesenheit deutet — getrennt nach der SICHTBAREN und
der LAUTLOSEN Form. Die lautlose Form ist der Kern dieses Abschnitts und bekommt
eigenen Raum: fuenf namentlich benannte Stellen, die Feststellung, dass keine davon
etwas protokolliert, die Entlastung ueber das offen bleibende notifiedAt samt dem
daraus folgenden einzigen nachpruefbaren Signal (wachsende Zahl unbenachrichtigter
Treffer ohne Versandprotokoll), und die begruendete Verwerfung einer
Laufzeitwarnung.
(t4) Was dieser Durchlauf bewusst nicht loest: WINDOWS #19 samt der drei davon
betroffenen RSS-Pfade (mit dem Messergebnis als Beleg), die uebergreifenden
Haelften der beiden Hintergrunddienste als Etappe-3-Uebergabe, die Abhaengigkeit
vom noch nicht umgestellten Bereich settings (Befund K) als Reihenfolgebedingung
fuer Etappe 4, die offene Architekturfrage req.tenantPrisma, und die Beobachtung
aus Befund G, dass ein Administrator eines beliebigen Mandanten eine plattformweite
RSS-Quelle entfernen kann.
(t5) Was dieser Durchlauf bewusst NICHT anfasst: die zwoelf Paare des
plattformweiten Katalogs und der beiden Fan-out-Adapter, mit der Feststellung, dass
sie geprueft und deliberat ungebunden sind — nicht uebersehen. Der Hinweis aus
Befund I gehoert hierher: in tender-ingestion.service.ts darf auch kein
erklaerender Kommentar den von der dortigen Schutzpruefung gesuchten Bezeichner
nennen.
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 && test -z "$(git diff --name-only HEAD -- apps/api/prisma 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 (23 bisherige plus die neuen des tenders-Abschnitts) mit Rueckgabewert 0; npm --prefix apps/api run test meldet weiterhin 743 Tests gruen und die Typpruefung ist sauber; docs/mandantentrennung-etappe2-fehlerrichtung.md traegt einen Abschnitt ## Bereich tenders mit der tatsaechlich beobachteten Ausgabe, einer Signaltabelle, dem eigenen Unterabschnitt zur lautlosen Fehlerform mit fuenf namentlich benannten Stellen, und den beiden Abschnitten zu dem, was bewusst offen bzw. unangetastet bleibt; Schema, Migrationen, Compose- und Beispiel-Umgebungsdateien sind unveraendert.
- Der vorhandene In-Memory-Fake jeder Testdatei bekommt
__makeBoundClient(tenantId): einen je Modell protokollierenden Wrapper um DIESELBEN Maps, sodass ein Aufruf ueber den ungebundenen Fake und ein Aufruf ueber den gebundenen Client unterscheidbar sind.forTenantwird pervi.mock('../prisma/prisma-tenant.extension', ...)darauf gelenkt. Eine reine Identitaet ((p) => p) genuegt NICHT — sie ist genau der ldap-Fehler, bei dem der Test in keiner Richtung etwas merkt. - Je Dienst mindestens ein Test der Form "Methode X bindet ueber forTenant() an den
uebergebenen Mandanten" (
expect(forTenant).toHaveBeenCalledWith(prisma, 't1')PLUS der Nachweis, dass der Modellzugriff auf dem GEBUNDENEN Client stattfand, ueber das Protokoll des Wrappers) — fuer jede umgestellte Methode, nicht nur eine Stichprobe. - Fuer
tender-rss-feed.service.tszusaetzlich der Gegentest:listForUser,createPlatformundremovebinden NICHT (expect(forTenant).not.toHaveBeenCalled()), undlistForUserliefert weiterhin die plattformweite Zeile ohne Besitzer mit. - Fuer die drei
upsert-Pfade (tenderEmailConfig,tenderNotificationPref,tenderTriage) je ein Test, dass eine Eindeutigkeitsverletzung (P2002) als verstaendliche deutsche Meldung herauskommt und nicht als roher Fehler. - Alle bestehenden Besitz-/IDOR-Tests jeder Datei bleiben unveraendert bestehen und
gruen — die anwendungsseitige
userId-Filterung wird durch die Bindung NICHT ersetzt (Befund E). - Falsifizieren, nicht behaupten: nach dem Umbau probeweise EINE Bindung
zurueckbauen und belegen, dass mindestens ein Test dadurch rot wird. Das Ergebnis
gehoert in den Bericht; der Rueckbau wird danach rueckgaengig gemacht.
Bindungsregel, die ueber jede einzelne Fundstelle entscheidet: gebunden wird genau
dann, wenn die beruehrte Zeilenmenge garantiert ein nicht-nullbares
tenantIdtraegt, das dem Mandanten des Aufrufers entspricht. Grundlage ist die Messung aus Aufgabe 1, nicht dieser Text — weicht die Messung ab, gilt die Messung, und die Abweichung wird im Bericht ausgeschrieben.
Muster durchgehend wie in groups/ldap:
const tenantPrisma = forTenant(this.prisma, tenantId) as any;, Name tenantPrisma
beibehalten (die Rohtrefferzaehlung des Klassifikationsdokuments haengt daran). Der
gebundene Client wird je Methode einmal erzeugt, nicht je Zugriff.
VOLLSTAENDIG BINDEN, alle Zugriffe:
tender-saved-search.service.ts—list,create,update(Lesepruefung UND Schreibzugriff),remove(Lesepruefung UND Loeschung).list,updateundremovebekommentenantIdals zusaetzlichen Parameter.tender-triage.service.ts—setTriage(hattenantIdbereits),listForUserundfavoriteIdsbekommentenantId.tender-notification-pref.service.ts—setForUser(hattenantIdbereits),getForUserbekommttenantId.tender-email-config.service.ts—saveConfig(hattenantIdueberctx),getConfigForApiundtestConnectionbekommentenantId. Beide Lesezugriffe ingetConfigForApibinden. Die Sicherheitszusagen des Dateikopfs bleiben unangetastet: die sichere Feldauswahl gilt weiter, der rohe Lesezugriff bleibt methodenlokal, entschluesselte Zugangsdaten werden nicht protokolliert und nicht zurueckgegeben.
TEILWEISE BINDEN — tender-rss-feed.service.ts:
createForUserbindet BEIDES, den Zaehler und die Anlage.listForUser,createPlatformundremovebleiben UNGEBUNDEN. Jede der drei bekommt einen kurzen Kommentar, der WINDOWS #19 nennt und die konkrete Folge einer Bindung benennt (plattformweite Quellen verschwaenden fuer jeden Mandanten; das Einfuegen ohne Mandanten wuerde abgewiesen; plattformweite Quellen liessen sich nicht mehr entfernen). Den einen bedingtendeleteManyin zwei Anweisungen zu zerlegen ist ausdruecklich NICHT erlaubt — das oeffnete das Pruef-/Nutzungsfenster wieder, das der Dateikopf vermeidet. Die Policy-Semantik wird NICHT angefasst, keine Migration geschrieben.
FEHLERBEHANDLUNG (Befund F, getragen von der Messung aus Aufgabe 1): die drei
upsert-Pfade auf Eindeutigkeitsbedingungen ohne Mandantendimension bekommen eine
Behandlung des Prisma-Fehlercodes P2002, die eine verstaendliche deutsche Meldung
liefert statt eines rohen Fehlers. Muster wortgleich aus
tender-saved-search.service.ts uebernehmen (dort bereits vorhanden), nicht neu
erfinden. Der Text nennt die Ursache in Alltagssprache; keine Fachbegriffe, keine
Fehlercodes im Text.
STEUERUNG, tenders.controller.ts: die zehn Aufrufstellen, die heute nur
{ userId } bzw. { userId, role } destrukturieren, reichen tenantId mit durch.
extractTriageContext liefert ihn bereits und wird NICHT veraendert; es entsteht
kein neuer Aufloesungsweg und keine neue Quelle fuer Mandant oder Nutzer. Die
Reihenfolge der Routen bleibt unangetastet (statische Routen vor @Get(':id') —
sonst 404-Verschattung, die kein Test dieser Ebene faengt).
tenders.controller.spec.ts: die Erwartungen an die Fake-Dienste um den neuen
tenantId-Parameter nachziehen. Die Datei braucht KEINEN Mock der Erweiterung
(sie uebergibt ausschliesslich Fake-Dienste, nie die echten).
NICHT ANFASSEN in dieser Aufgabe: tender-digest.scheduler.ts,
tender-matching.service.ts (das ist Aufgabe 3), die zwoelf Paare aus Befund L,
Schema, Migrationen, Compose-Dateien, Umgebungsdateien, apps/web. Keine neue
Transaktion einfuehren (Befund A).
npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example apps/web)"
Alle 743 bisherigen Tests plus die neuen Bindungsnachweise sind gruen und die Typpruefung ist sauber; die vier Dienste mit nicht-nullbarem Mandanten laufen vollstaendig ueber forTenant(), tender-rss-feed.service.ts bindet createForUser und begruendet die drei ungebundenen Pfade im Code mit WINDOWS #19; alle bestehenden Besitz-/IDOR-Tests sind unveraendert gruen; die Schutzpruefung "never calls forTenant()" in tender-ingestion.service.spec.ts ist weiterhin gruen; ein probeweiser Rueckbau EINER Bindung macht mindestens einen Test rot und das ist im Bericht festgehalten; Schema, Migrationen, Compose-, Umgebungsdateien und apps/web sind unveraendert.
- Je Datei ein Test, dass die UEBERGREIFENDE Abfrage NICHT bindet
(
tenderMatch.findManyder Kandidatenliste im Digest,tenderSavedSearch.findManyin der Sofortmeldung) und dass die Zugriffe INNERHALB der Schleife auf dem gebundenen Client laufen, mit dem Mandanten der jeweiligen Zeile. - Ein Test mit ZWEI Mandanten in einem Lauf, der belegt, dass je Durchlauf der Schleife mit dem Mandanten DIESER Zeile gebunden wird und nicht einmal global mit dem ersten.
- Ein Test der lautlosen Fehlerform: liefert der gebundene Lesezugriff auf den
Benutzer nichts, wird nichts versendet UND
notifiedAtbleibt NULL (der Treffer bleibt also wiederholbar). Das ist die Zusage, auf der die Entlastung in der Kritikschrift beruht — sie muss von einem Test getragen werden, nicht von einer Behauptung. - Falsifizieren wie in Aufgabe 2: eine Bindung probeweise zurueckbauen, roten Test belegen, zuruecknehmen, Ergebnis in den Bericht. Die Trennlinie zwischen Etappe 2 und Etappe 3 wird hier gezogen und im Code ausgeschrieben. Etappe 3 (Systemkontext fuer Hintergrundlaeufe) wird NICHT vorweggenommen.
tender-digest.scheduler.ts:
- Die Kandidatenabfrage (
tenderMatch.findManymitnotifiedAt: null,distinct) bleibt UNGEBUNDEN — sie ist der bewusste Fan-out ueber alle Mandanten. Ein Kommentar benennt sie als Etappe-3-Uebergabe. - Damit die Schleife ueberhaupt binden KANN, braucht sie einen Mandanten. Die
Kandidatenabfrage waehlt deshalb zusaetzlich das denormalisierte
tenantIdder Treffer-Zeile aus. Dass diese Erweiterung zusammen mitdistinctdas erwartete Ergebnis liefert, ist am Fake der Testdatei UND an der Prisma-Typpruefung nachzuweisen, nicht anzunehmen. Der Sonderfall, dass ein Nutzer Treffer unter zwei verschiedenen Mandanten haben koennte (denormalisierter Wert, Mandantenwechsel), wird nicht geloest, sondern im Kommentar und in der Kritikschrift benannt. - Innerhalb der Schleife binden: die Praeferenz-Abfrage, die Treffer-Abfrage und die
Benutzer-Abfrage, alle an den Mandanten der Kandidatenzeile. Der Schreibzugriff,
der
notifiedAtstempelt, bindet ebenfalls. - Der Aufruf des Mailversands bleibt unveraendert; welcher Mandant die SMTP-Angaben bestimmt, wird nicht geaendert.
tender-matching.service.ts:
- Die Profil-Abfrage (
tenderSavedSearch.findManyohne Filter) bleibt UNGEBUNDEN, mit Kommentar als Etappe-3-Uebergabe. - Der Lesezugriff auf den plattformweiten Katalog bleibt UNGEBUNDEN (D-03), mit Kommentar.
- Innerhalb der Profilschleife binden, jeweils an
tenantIddes Profils bzw. des Treffers: die Anlage der Treffer, die Abfrage der noch nicht benachrichtigten Treffer, die Benutzer-Abfrage und der Schreibzugriff, dernotifiedAtstempelt. Der gebundene Client wird EINMAL je Profil erzeugt, nicht je Treffer — sonst entsteht pro Zeile eine eigene Transaktion. - Die vorhandene Fehlerbehandlung je Profil (ein defektes Profil darf den Lauf nicht abbrechen) bleibt unveraendert.
MASCHINELLE ABSICHERUNG UND DOKUMENTE:
apps/api/src/prisma/rls-access-inventory.spec.tslaufen lassen und AUS SEINER AUSGABE den Stand je Paar ablesen. Der gemessene Wert gewinnt; er wird nicht aus diesem Plan abgeschrieben. Erwartungsgemaess entstehen fuer diesen Bereich gemischte Staende (Dateien, in denen dasselbe Modell gebunden UND ungebunden vorkommt) — das ist der korrekte Ausdruck derbeides-Klasse und kein Mangel. Zeigt die Pruefung ein bisher unbekanntes Paar oder eine Erkennungsluecke, wird sie geschlossen wie in 260909-jts (dort war es der Transaktionsparameter), bevor das Dokument nachgezogen wird.docs/mandantentrennung-zugriffsklassifikation.mdnachziehen: die Stand-Spalte aller 23 tenders-Paare auf den gemessenen Wert; die Bereichszeiletendersin der Uebersicht mit den dort dokumentierten Zaehlbefehlen NEU messen (nicht rechnen) und die Summenzeile mitfuehren; die Klassenverteilung pruefen und nur aendern, wenn die Pruefung tatsaechlich eine andere Paarzahl meldet. Der Abschnitt "Der Hintergrunddienst als Falle" bekommt fuer die beiden tenders-Dateien einen Nachtrag im Stil des ldap-Eintrags: Je-Treffer-Haelfte geschlossen, uebergreifende Haelfte ausdruecklich an Etappe 3 uebergeben. Der urspruengliche Text bleibt lesbar stehen, es wird nachgetragen und nicht neu geschrieben.- Die zwoelf Paare aus Befund L einzeln gegen D-03 bzw. den jeweiligen Dateikopf
pruefen und ihre Begruendungsspalte so schaerfen, dass sie als BEWUSST ungebunden
lesbar ist und nicht als offener Rest.
tender-ingestion.service.tsselbst wird dabei NICHT bearbeitet (Befund I). docs/mandantentrennung-etappe2-fehlerrichtung.md: den in Aufgabe 1 geschriebenen Abschnitt um das ergaenzen, was erst jetzt tatsaechlich vorliegt — welche Haelften geschlossen sind, was an Etappe 3 uebergeben ist, und der Nachtrag zur Reihenfolgebedingung gegenueber dem Bereichsettings(Befund K). DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && npm --prefix apps/api run test && npm --prefix apps/api run type-check && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && test -z "$(git diff --name-only HEAD -- apps/api/prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example apps/web)" Alle Tests (743 plus die neuen) und die Typpruefung sind gruen, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden;rls-access-inventory.spec.tsund das Klassifikationsdokument stimmen fuer alle 23 tenders-Paare ueberein, jede Zeile traegt einen gemessenen Stand; die zwoelf bewusst ungebundenen Paare sind als bewusst lesbar undtender-ingestion.service.tsist unveraendert; beide Hintergrunddienste binden je Schleifendurchlauf an den Mandanten der jeweiligen Zeile und lassen ihre uebergreifende Abfrage kommentiert ungebunden; beide Dokumente sind geschlossen; Schema, Migrationen, Compose-, Umgebungsdateien undapps/websind unveraendert.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Browser -> API | Sitzungsnachweis (JWT) und Modul-Wachter; userId/tenantId/role kommen ausschliesslich aus extractTriageContext, nie aus Rumpf oder Abfragezeichenkette |
| API -> PostgreSQL | Row-Level-Security. Der Schalter ist AUS (Rolle tessera mit BYPASSRLS); die Policies wirken heute nicht, sind aber ausgeliefert |
| Planer -> PostgreSQL | Zwei Cron-Laeufe ohne Anfragekontext (Digest, Sofortmeldung) — kein Mandant aus einer Sitzung, nur aus der gelesenen Zeile |
| API -> fremdes Postfach / fremder RSS-Host | Ausgehende Verbindungen mit gespeicherten, verschluesselten Zugangsdaten bzw. mit vom Nutzer gesetzten URLs |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-LAA-01 | Information Disclosure | Suchprofile, Triage-Zustand, Benachrichtigungseinstellung, Postfachanbindung — Quer-Lesen zwischen MANDANTEN | high | mitigate | Aufgabe 2 bindet alle Lesepfade der vier Dienste mit nicht-nullbarem Mandanten an forTenant(); Aufgabe 1 misst an der ausgelieferten Policy unter einer Rolle ohne BYPASSRLS, dass gebunden nur die eigene Zeile und ungebunden gar keine sichtbar ist |
| T-LAA-02 | Information Disclosure | dieselben Daten — Quer-Lesen zwischen NUTZERN desselben Mandanten | high | mitigate | Die Policies haben keine Benutzerdimension; Aufgabe 1 misst das ausdruecklich (tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar). Die anwendungsseitige userId-Filterung bleibt der einzige Schutz und wird in Aufgabe 2 nicht entfernt; alle bestehenden IDOR-Tests bleiben gruen |
| T-LAA-03 | Tampering | Schreibpfade der fuenf Dienste — Anlegen/Aendern/Loeschen auf fremde Rechnung | high | mitigate | Gebundene Schreibzugriffe; die Migration verzichtet bewusst auf eine getrennte WITH-CHECK-Klausel, die USING-Bedingung gilt damit auch fuer neue Zeilen. Zusaetzlich bleiben die Besitzpruefungen im Dienst bestehen |
| T-LAA-04 | Information Disclosure | gespeicherte Postfach-Zugangsdaten (encryptedInboxCreds) |
high | mitigate | Aufgabe 2 laesst die sichere Feldauswahl unveraendert, bindet den rohen Lesezugriff ebenfalls und haelt entschluesselte Zugangsdaten methodenlokal — keine Protokollierung, keine Rueckgabe. Bestehende Zusagen des Dateikopfs werden nicht aufgeweicht |
| T-LAA-05 | Denial of Service | Benachrichtigungswege: ein zu kleines Leseergebnis sendet nichts, lautlos | high | mitigate | Aufgabe 1 schreibt die fuenf Stellen namentlich in die Kritikschrift samt dem einzigen nachpruefbaren Signal; Aufgabe 3 sichert per Test zu, dass notifiedAt bei ausbleibendem Versand NULL bleibt und der Treffer damit wiederholbar ist. Eine Laufzeitwarnung wurde erwogen und begruendet verworfen |
| T-LAA-06 | Denial of Service | WINDOWS #19 — plattformweite RSS-Quellen (tenantId nullbar) |
medium | transfer | Aufgabe 1 misst die Grenze (plattformweite Zeile unter jedem Kontext unsichtbar, gebundenes Einfuegen ohne Mandant abgewiesen); Aufgabe 2 bindet die drei betroffenen Pfade deshalb NICHT und begruendet es im Code. Die Policy-Semantik gehoert zu Etappe 3 und wird hier nicht angefasst |
| T-LAA-07 | Denial of Service | gebundenes upsert auf einen Eindeutigkeitsschluessel ohne Mandantendimension bei veraltetem denormalisiertem Mandanten |
medium | mitigate | Aufgabe 1 misst das Verhalten; Aufgabe 2 uebersetzt die Eindeutigkeitsverletzung nach dem bereits vorhandenen Muster in eine verstaendliche deutsche Meldung statt eines rohen Fehlers |
| T-LAA-08 | Tampering | ein Administrator eines beliebigen Mandanten kann eine plattformweite RSS-Quelle entfernen, die alle Mandanten speist | low | accept | Produkt-/Zustaendigkeitsfrage im Umfeld von WINDOWS #19, kein Auftrag dieses Durchlaufs. In Aufgabe 1 als Beobachtung in der Kritikschrift festgehalten, nicht repariert |
| T-LAA-09 | Elevation of Privilege | vorzeitiges Scharfschalten der Datenbankrolle im Rahmen dieses Durchlaufs | high | mitigate | DATABASE_URL, alle vier Compose-Dateien und beide Beispiel-Umgebungsdateien bleiben unveraendert; jede Aufgabe prueft das maschinell ueber ein git diff --name-only-Gate im <verify>-Block |
| T-LAA-SC | Tampering | Lieferkette (npm) | low | accept | Dieser Durchlauf installiert kein Paket — kein npm install, keine neue Abhaengigkeit. Das Paket-Legitimitaets-Gate faellt damit nicht an; wird waehrend der Ausfuehrung doch eine Installation noetig, ist das ein Anlass zum Anhalten und Nachfragen, nicht zum Nachziehen |
| </threat_model> |
<success_criteria>
- Die fuenf Nutzer-CRUD-Dienste sind nach der Bindungsregel umgestellt: vier
vollstaendig,
tender-rss-feed.service.tsteilweise mit im Code begruendeter Grenze zu WINDOWS #19. - Die Je-Treffer-Haelften der beiden Hintergrunddienste binden an den Mandanten der jeweils gelesenen Zeile; ihre uebergreifenden Abfragen sind unveraendert und als Etappe-3-Uebergabe kommentiert.
- Die zwoelf Paare des plattformweiten Katalogs und der beiden Fan-out-Adapter sind unveraendert und im Klassifikationsdokument als bewusst ungebunden lesbar.
docs/mandantentrennung-etappe2-fehlerrichtung.mdtraegt einentenders-Abschnitt mit gemessener Belegzeile, Signaltabelle und einem eigenen Unterabschnitt zur lautlosen Fehlerform.- Alle sieben umgestellten Testdateien koennen bei einer vergessenen Bindung rot werden; das ist durch Rueckbau belegt, nicht behauptet.
- Alle vier Verifikationsschritte oben sind gruen. </success_criteria>