docs(quick-260909-laa): Plan fuer Etappe 2 Bereich tenders
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
This commit is contained in:
+750
@@ -0,0 +1,750 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260909-laa
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
autonomous: true
|
||||||
|
requirements: [WINDOWS-20, ETAPPE-2-TENDERS]
|
||||||
|
|
||||||
|
files_modified:
|
||||||
|
- apps/api/scripts/rls-scratch-check.mjs
|
||||||
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||||
|
- apps/api/src/tenders/tender-saved-search.service.ts
|
||||||
|
- apps/api/src/tenders/tender-saved-search.service.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-triage.service.ts
|
||||||
|
- apps/api/src/tenders/tender-triage.service.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-notification-pref.service.ts
|
||||||
|
- apps/api/src/tenders/tender-notification-pref.service.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-email-config.service.ts
|
||||||
|
- apps/api/src/tenders/tender-email-config.service.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-rss-feed.service.ts
|
||||||
|
- apps/api/src/tenders/tender-rss-feed.service.spec.ts
|
||||||
|
- apps/api/src/tenders/tenders.controller.ts
|
||||||
|
- apps/api/src/tenders/tenders.controller.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-digest.scheduler.ts
|
||||||
|
- apps/api/src/tenders/tender-digest.scheduler.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-matching.service.ts
|
||||||
|
- apps/api/src/tenders/tender-matching.service.spec.ts
|
||||||
|
- apps/api/src/tenders/tender-notifications.integration.spec.ts
|
||||||
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||||
|
|
||||||
|
estimate:
|
||||||
|
tokens: 150000
|
||||||
|
raw_tokens: 150000
|
||||||
|
tasks: 3
|
||||||
|
confidence: low
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Jeder Zugriff des Bereichs `tenders`, der auf Rechnung genau eines Mandanten eine Tabelle mit nicht-nullbarem `tenantId` beruehrt, laeuft ueber einen gebundenen Client — die fuenf Nutzer-CRUD-Dienste vollstaendig, die beiden Hintergrunddienste in ihrer Je-Treffer-Haelfte."
|
||||||
|
- "Die zehn Paare des plattformweiten Ausschreibungskatalogs (D-03) und die zwei Fan-out-Adapter bleiben unangetastet, und das Klassifikationsdokument weist sie als BEWUSST ungebunden aus, nicht als offene Arbeit."
|
||||||
|
- "Die Grenze zu WINDOWS #19 (nullbares `tenantId` bei `TenderRssFeedSource`) ist gemessen, nicht angenommen: eine plattformweite Zeile ist unter JEDEM Mandantenkontext unsichtbar und ein gebundenes Einfuegen ohne Mandant wird abgewiesen. Die Policy-Semantik wurde NICHT angefasst."
|
||||||
|
- "Es existiert ein `tenders`-Abschnitt der Kritikschrift, der je umgestelltem Pfad das konkrete Signal nennt UND die zusaetzliche Fehlerform dieses Bereichs abdeckt: ein Benachrichtigungsweg, der nichts liest, sendet nichts — lautlos, nutzersichtbar nur als Ausbleiben."
|
||||||
|
- "Dass die ausgelieferten Policies dieses Bereichs KEINE Benutzerdimension haben — ein Nutzer desselben Mandanten bleibt fuer die Datenbank sichtbar — ist gemessen und festgehalten; die anwendungsseitige `userId`-Filterung bleibt deshalb der einzige Schutz gegen Quer-Lesen zwischen Nutzern und wird nicht entfernt."
|
||||||
|
- "Alle sieben angefassten Testdateien koennen rot werden, wenn eine Fundstelle ungebunden bleibt — nachgewiesen ueber zwei unterscheidbare Clients, nicht behauptet."
|
||||||
|
- "Die uebergreifenden Haelften der beiden Hintergrunddienste sind unveraendert und als Etappe-3-Uebergabe benannt; die Umstellung hat nicht in Etappe 3 hineingegriffen."
|
||||||
|
- "Klassifikationsdokument und `rls-access-inventory.spec.ts` zeigen fuer alle 23 Paare des Bereichs denselben, maschinell gemessenen Stand."
|
||||||
|
- "743+ Tests und die Typpruefung sind gruen, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; Schema, Migrationen, alle vier Compose-Dateien und beide Beispiel-Umgebungsdateien sind unveraendert; der Schalter bleibt AUS."
|
||||||
|
artifacts:
|
||||||
|
- apps/api/scripts/rls-scratch-check.mjs
|
||||||
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||||
|
- apps/api/src/tenders/tender-saved-search.service.ts
|
||||||
|
- apps/api/src/tenders/tender-triage.service.ts
|
||||||
|
- apps/api/src/tenders/tender-notification-pref.service.ts
|
||||||
|
- apps/api/src/tenders/tender-email-config.service.ts
|
||||||
|
- apps/api/src/tenders/tender-rss-feed.service.ts
|
||||||
|
- apps/api/src/tenders/tenders.controller.ts
|
||||||
|
- apps/api/src/tenders/tender-digest.scheduler.ts
|
||||||
|
- apps/api/src/tenders/tender-matching.service.ts
|
||||||
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||||
|
key_links:
|
||||||
|
- "gebundener Client <-> die fuenf Policies `tenant_isolation_policy` auf TenderEmailConfig/TenderNotificationPref/TenderRssFeedSource/TenderSavedSearch/TenderTriage, wortgleich aus der ausgelieferten Migration `_rls_remaining_tenant_tables` extrahiert statt im Werkzeug nachgetippt"
|
||||||
|
- "`extractTriageContext` <-> die zehn Steuerungs-Aufrufstellen, die den Mandanten heute wegwerfen und ihn kuenftig durchreichen muessen — die einzige Stelle, an der ein vergessener Parameter den Umbau unvollstaendig macht"
|
||||||
|
- "nullbares `tenantId` von TenderRssFeedSource <-> `listForUser`/`createPlatform`/`remove` — die drei Pfade, die eine Bindung nach dem Scharfschalten strukturell zerstoeren wuerde (WINDOWS #19)"
|
||||||
|
- "gebundener Lesezugriff in der Schleife <-> die fuenf `continue`/`return`-Stellen der beiden Benachrichtigungswege, an denen ein zu kleines Leseergebnis lautlos zu 'nichts senden' wird"
|
||||||
|
- "`@@unique`-Schluessel ohne Mandantendimension (userId, userId_tenderId, tenderId_savedSearchId) <-> gebundenes `upsert` auf eine unsichtbare Zeile — die Stelle, an der aus stillem Ueberschreiben ein harter Fehler wird"
|
||||||
|
- "`rls-access-inventory.spec.ts` <-> Stand-Spalte des Klassifikationsdokuments fuer alle 23 Paare, einschliesslich der zwoelf bewusst ungebundenen"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Der Bereich `tenders` ist der dritte Bereich der Etappe 2 — und der erste, der
|
||||||
|
mehrheitlich NICHT aus Umbau besteht. Von 23 Paaren sind fuenf umzustellen, zehn
|
||||||
|
duerfen nicht angefasst werden, zwei sind bewusste Fan-outs, und sechs zerfallen in
|
||||||
|
eine uebergreifende Haelfte (Etappe 3) und eine mandantengebundene Haelfte (hier).
|
||||||
|
|
||||||
|
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.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@~/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/STATE.md
|
||||||
|
@docs/mandantentrennung-zugriffsklassifikation.md
|
||||||
|
@docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||||
|
@apps/api/src/prisma/prisma-tenant.extension.ts
|
||||||
|
@apps/api/src/prisma/rls-access-inventory.spec.ts
|
||||||
|
@apps/api/scripts/rls-scratch-check.mjs
|
||||||
|
@apps/api/src/groups/groups.service.ts
|
||||||
|
@apps/api/src/groups/groups.service.spec.ts
|
||||||
|
@apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql
|
||||||
|
@apps/api/src/tenders/tender-saved-search.service.ts
|
||||||
|
@apps/api/src/tenders/tender-triage.service.ts
|
||||||
|
@apps/api/src/tenders/tender-notification-pref.service.ts
|
||||||
|
@apps/api/src/tenders/tender-email-config.service.ts
|
||||||
|
@apps/api/src/tenders/tender-rss-feed.service.ts
|
||||||
|
@apps/api/src/tenders/tenders.controller.ts
|
||||||
|
@apps/api/src/tenders/tender-digest.scheduler.ts
|
||||||
|
@apps/api/src/tenders/tender-matching.service.ts
|
||||||
|
@CLAUDE.md
|
||||||
|
</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 status` sauber, HEAD `4cf7cea`.
|
||||||
|
|
||||||
|
**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:
|
||||||
|
|
||||||
|
- `listForUser` liest `{ OR: [{userId: null}, {userId}] }` — gebunden verschwaenden
|
||||||
|
nach dem Scharfschalten die plattformweiten Quellen fuer JEDEN Mandanten.
|
||||||
|
- `createPlatform` schreibt `tenantId = null` — ein gebundenes Einfuegen liefe in
|
||||||
|
die WITH-CHECK-Wirkung derselben Policy.
|
||||||
|
- `remove` deckt den Verwaltungsfall ueber `{userId: null}` ab; gebunden koennte
|
||||||
|
niemand mehr eine plattformweite Quelle entfernen. Diesen einen `deleteMany` in
|
||||||
|
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>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="tracer">
|
||||||
|
<name>Aufgabe 1: Die drei Sonderfaelle dieses Bereichs messen und die Fehlerrichtung fuer tenders schreiben</name>
|
||||||
|
<precondition>Der Container `tessera-ctl-db-1` laeuft; seine Adresse per `docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}'` NEU ermitteln (zur Planungszeit 172.19.0.2 — eine Container-Adresse ist veraenderlich und darf nicht aus diesem Plan abgeschrieben werden).</precondition>
|
||||||
|
<files>apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
||||||
|
<action>
|
||||||
|
Zuerst messen, dann die Kritik aus der Messung schreiben — nicht umgekehrt. Kein
|
||||||
|
Dienstcode in dieser Aufgabe.
|
||||||
|
|
||||||
|
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 anwendungsseitige `userId`-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: `listForUser` darf nicht gebunden
|
||||||
|
werden, solange die Policy-Semantik unveraendert ist.
|
||||||
|
- `tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt` — ein gebundenes
|
||||||
|
INSERT unter TENANT-A mit `tenantId = NULL` wird abgewiesen; die Abweisung ist das
|
||||||
|
bestandene Ergebnis. Der Meldetext nennt die Folge: `createPlatform` darf 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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>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)"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>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.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Aufgabe 2: Die fuenf Nutzer-CRUD-Dienste binden und den Mandanten durch die Steuerung reichen</name>
|
||||||
|
<files>apps/api/src/tenders/tender-saved-search.service.ts, apps/api/src/tenders/tender-saved-search.service.spec.ts, apps/api/src/tenders/tender-triage.service.ts, apps/api/src/tenders/tender-triage.service.spec.ts, apps/api/src/tenders/tender-notification-pref.service.ts, apps/api/src/tenders/tender-notification-pref.service.spec.ts, apps/api/src/tenders/tender-email-config.service.ts, apps/api/src/tenders/tender-email-config.service.spec.ts, apps/api/src/tenders/tender-rss-feed.service.ts, apps/api/src/tenders/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.controller.spec.ts</files>
|
||||||
|
<behavior>
|
||||||
|
Der Nachweis kommt VOR der Umstellung, sonst beweist er nichts (Befund H). Je
|
||||||
|
Dienst zuerst der Zwei-Client-Nachweis nach dem Muster aus
|
||||||
|
`apps/api/src/groups/groups.service.spec.ts`, dann der Umbau.
|
||||||
|
|
||||||
|
- 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. `forTenant` wird per `vi.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.ts` zusaetzlich der Gegentest: `listForUser`,
|
||||||
|
`createPlatform` und `remove` binden NICHT (`expect(forTenant).not.toHaveBeenCalled()`),
|
||||||
|
und `listForUser` liefert 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.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Bindungsregel, die ueber jede einzelne Fundstelle entscheidet: gebunden wird genau
|
||||||
|
dann, wenn die beruehrte Zeilenmenge garantiert ein nicht-nullbares `tenantId`
|
||||||
|
traegt, 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`, `update` und
|
||||||
|
`remove` bekommen `tenantId` als zusaetzlichen Parameter.
|
||||||
|
- `tender-triage.service.ts` — `setTriage` (hat `tenantId` bereits), `listForUser`
|
||||||
|
und `favoriteIds` bekommen `tenantId`.
|
||||||
|
- `tender-notification-pref.service.ts` — `setForUser` (hat `tenantId` bereits),
|
||||||
|
`getForUser` bekommt `tenantId`.
|
||||||
|
- `tender-email-config.service.ts` — `saveConfig` (hat `tenantId` ueber `ctx`),
|
||||||
|
`getConfigForApi` und `testConnection` bekommen `tenantId`. Beide Lesezugriffe in
|
||||||
|
`getConfigForApi` binden. 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`:
|
||||||
|
|
||||||
|
- `createForUser` bindet BEIDES, den Zaehler und die Anlage.
|
||||||
|
- `listForUser`, `createPlatform` und `remove` bleiben 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 bedingten `deleteMany` in 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).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>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)"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>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.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Aufgabe 3: Die Je-Treffer-Haelften der beiden Hintergrunddienste binden und beide Dokumente schliessen</name>
|
||||||
|
<files>apps/api/src/tenders/tender-digest.scheduler.ts, apps/api/src/tenders/tender-digest.scheduler.spec.ts, apps/api/src/tenders/tender-matching.service.ts, apps/api/src/tenders/tender-matching.service.spec.ts, apps/api/src/tenders/tender-notifications.integration.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
||||||
|
<behavior>
|
||||||
|
Wie in Aufgabe 2: erst der Zwei-Client-Nachweis, dann der Umbau. Beide Testdateien
|
||||||
|
und die gemeinsame Integrationsdatei haben heute keinen Mock der Erweiterung und
|
||||||
|
wuerden sonst aus dem falschen Grund rot (Befund H).
|
||||||
|
|
||||||
|
- Je Datei ein Test, dass die UEBERGREIFENDE Abfrage NICHT bindet
|
||||||
|
(`tenderMatch.findMany` der Kandidatenliste im Digest, `tenderSavedSearch.findMany`
|
||||||
|
in 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 `notifiedAt` bleibt 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.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
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.findMany` mit `notifiedAt: 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 `tenantId` der
|
||||||
|
Treffer-Zeile aus. Dass diese Erweiterung zusammen mit `distinct` das 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 `notifiedAt` stempelt, 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.findMany` ohne 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 `tenantId` des Profils bzw. des
|
||||||
|
Treffers: die Anlage der Treffer, die Abfrage der noch nicht benachrichtigten
|
||||||
|
Treffer, die Benutzer-Abfrage und der Schreibzugriff, der `notifiedAt` stempelt.
|
||||||
|
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.ts` laufen 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 der `beides`-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.md` nachziehen: die Stand-Spalte
|
||||||
|
aller 23 tenders-Paare auf den gemessenen Wert; die Bereichszeile `tenders` in 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.ts` selbst 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 Bereich `settings` (Befund K).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>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)"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Alle Tests (743 plus die neuen) und die Typpruefung sind gruen, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; `rls-access-inventory.spec.ts` und das Klassifikationsdokument stimmen fuer alle 23 tenders-Paare ueberein, jede Zeile traegt einen gemessenen Stand; die zwoelf bewusst ungebundenen Paare sind als bewusst lesbar und `tender-ingestion.service.ts` ist 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 und `apps/web` sind unveraendert.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `npm --prefix apps/api run test` — 743 bisherige Tests plus die neu
|
||||||
|
hinzugekommenen Bindungsnachweise, alle gruen, keine ausgelassene Datei.
|
||||||
|
2. `npm --prefix apps/api run type-check` — Rueckgabewert 0.
|
||||||
|
3. `DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}')` und
|
||||||
|
`TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs`
|
||||||
|
— alle Pruefungen bestanden (23 bisherige plus die neuen), Rueckgabewert 0.
|
||||||
|
4. `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`
|
||||||
|
— leer. Schema, Migrationen, Compose, Umgebungsdateien und das Web bleiben
|
||||||
|
unberuehrt, der Schalter bleibt aus.
|
||||||
|
5. `rls-access-inventory.spec.ts` und `docs/mandantentrennung-zugriffsklassifikation.md`
|
||||||
|
stimmen fuer alle 23 tenders-Paare ueberein — nachgewiesen dadurch, dass die
|
||||||
|
Pruefung gruen ist, nicht durch Nachzaehlen von Hand.
|
||||||
|
6. Der Rueckbau-Nachweis aus Aufgabe 2 und Aufgabe 3 ist im Bericht festgehalten:
|
||||||
|
welche Bindung probeweise entfernt wurde, welcher Test dadurch rot wurde.
|
||||||
|
7. Ein lokal fehlender Mailserver (`ENOTFOUND mailhog`) ist umgebungsbedingt und
|
||||||
|
kein Mangel — nicht "reparieren".
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Die fuenf Nutzer-CRUD-Dienste sind nach der Bindungsregel umgestellt: vier
|
||||||
|
vollstaendig, `tender-rss-feed.service.ts` teilweise 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.md` traegt einen `tenders`-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>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/quick/260909-laa-mandantentrennung-etappe-2-bereich-tende/260909-laa-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user