1227 lines
116 KiB
Markdown
1227 lines
116 KiB
Markdown
---
|
|
phase: quick-260911-gwh
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
autonomous: true
|
|
requirements: [WINDOWS-18, ETAPPE-2-FAVORITES, ETAPPE-2-SETTINGS]
|
|
|
|
files_modified:
|
|
- apps/api/scripts/rls-scratch-check.mjs
|
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
|
- apps/api/src/favorites/favorites.service.ts
|
|
- apps/api/src/favorites/favorites.service.spec.ts
|
|
- apps/api/src/favorites/favorites.controller.ts
|
|
- apps/api/src/settings/settings.service.ts
|
|
- apps/api/src/settings/settings.service.spec.ts
|
|
- apps/api/src/mail/mail.module.ts
|
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
|
- docs/anleitung-entwicklung.md
|
|
- .planning/WINDOWS.md
|
|
|
|
estimate:
|
|
tokens: 210000
|
|
raw_tokens: 210000
|
|
tasks: 3
|
|
confidence: low
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Beide Bereiche sind GEMESSEN, bevor eine Zeile Quelltext angefasst wird: `runFavoritesAreaChecks` und `runSettingsAreaChecks` messen an den Regeln fuer `FavoriteLink` und `SmtpConfig` WORTGLEICH aus `20260909140000_rls_remaining_tenant_tables` (Regelstand eindeutig, die Widen-Migration hat keine eigene Regel fuer beide), je Bereich mindestens einen Pfad ueber den GENERIERTEN Client an einer Wegwerf-Tabelle, deren Spaltenmenge zur Laufzeit gegen die SKALAREN Felder des Modells geprueft wird (`createdAt`/`updatedAt` eingeschlossen)."
|
|
- "Der Startpfad des Mailmoduls ist als SECHSTER Fall der Hintergrunddienst-Falle erkannt, gemessen und markiert, NICHT gebunden und NICHT zu einem Mehrmandanten-Versand umgebaut: die Methode traegt einen Namen, der sie nicht mit einem Anfrageweg verwechseln laesst, ihr Kopf nennt BEIDE Zustaende (heute: beliebiger Mandant liefert den SMTP-Server fuer ALLE Systemmails; nach dem Scharfschalten: `null`, und die Rueckfallkette des Moduls verdeckt das Verstummen mit einem falschen Transport statt es zu melden), die Unsymmetrie zu `ldap`/`dkv` (Rueckfallkette statt blosser Leere) und die Entscheidung EIGENER Ledger-Eintrag statt Anschluss an #21 — mit Grund."
|
|
- "Befund K (`tenders` (t4)) und die Uebergabe in `dkv` (d4) sind ERFUELLT und stehen als solche an drei Stellen: `getDecryptedSmtpConfig(tenantId)` — der EINZIGE Versandpfad von `tender-mail.service.ts` und `dkv-mail.service.ts` — laeuft ueber `tenantPrisma`; ein Nachtrag unter Befund K in (t4), ein Nachtrag im Uebergaben-Absatz von (d4), und die Ordnungsnotiz im Klassifikationsdokument sagen es ausdruecklich, damit die Etappe-4-Vorabpruefung keine veraltete Bedingung mitschleppt."
|
|
- "Alle sieben Zugriffe in `favorites.service.ts` und die drei Anfragewege in `settings.service.ts` laufen je Methode ueber GENAU EINEN Klienten `tenantPrisma`; die Besitzpruefungen (findUnique, dann Vergleich der Benutzerkennung, dann Schreibzugriff auf DEMSELBEN Klienten) bleiben bestehen; `create` prueft zusaetzlich, dass das Ziel-Widget dem Aufrufer gehoert (der Fremdschluessel umgeht den Zeilenschutz — gemessen, Pruefung 7)."
|
|
- "Die umgekehrte Fehlerrichtung ist je Bereich in ihrer TATSAECHLICHEN Auspraegung benannt und gemessen: eine leere Favoritenleiste liest sich als `Noch keine Favoriten.` (favorites-widget.tsx); eine unsichtbare SMTP-Zeile liest sich als leeres Formular (200 mit leerem Rumpf, `res.json()` wirft, `.catch(() => {})` verschluckt — dieselbe Kette wie #28), waehrend die Zugangsdaten physisch noch da sind und ein erneutes Speichern unter der ungebundenen Form am Eindeutigkeitsindex scheitert (gemessen, Konstruktorname und `code` woertlich)."
|
|
- "Die Testlage traegt: `favorites.service.spec.ts` und `settings.service.spec.ts` existieren NEU mit dem Zwei-Klienten-Nachbau (ungebunden OHNE Anfrage-Modelle, gebunden OHNE `findFirst`), der Icon-Dienst und `nodemailer` sind Attrappen (kein echter Transport — lokal gibt es keinen `mailhog`), und vier Falsifizierungsnachweise sind durchgefuehrt, zurueckgenommen und woertlich notiert."
|
|
- "Etappe 2 ist mit diesem Lauf VOLLSTAENDIG: die Uebersichtstabelle, Summenzeile, Bestandsaufnahme, Klassen-Verteilung und der Hintergrunddienst-Abschnitt des Klassifikationsdokuments stehen auf ihrem Endstand fuer Etappe 2 — DERIVIERT gegatet, nicht abgeschrieben —, jede Fundstelle in `apps/api/src` ist gebunden oder mit geschriebenem Grund an der Stelle ungebunden, und die Kritikschrift traegt einen Abschluss-Abschnitt mit den Zahlen aus den eigenen Messanweisungen sowie dem, was fuer Etappe 3 bleibt."
|
|
- "Baseline gehalten am Ende JEDER Aufgabe: mindestens 951 Tests gruen in mindestens 60 Dateien (nach Aufgabe 2 mehr, in mindestens 62 Dateien), Typpruefung sauber, Werkzeug alle Pruefungen bestanden (mindestens 135). Schalter AUS, Schema und Migrationen unveraendert, keine Compose- oder Umgebungsdatei, nichts in Active Directory; Erlaubnisliste gegen `46f0e78`."
|
|
artifacts:
|
|
- "apps/api/scripts/rls-scratch-check.mjs — zwei neue Abschnitte `runFavoritesAreaChecks` (Wegwerf-Tabelle `FavoriteLink` mit Fremdschluessel auf die bestehende Wegwerf-Tabelle `WidgetInstance`) und `runSettingsAreaChecks` (Wegwerf-Tabelle `SmtpConfig` mit Eindeutigkeitsindex auf `tenantId`), je mindestens sieben namentlich benannte Pruefungen, in `main()` NACH `runAuthAreaChecks` und VOR `runTransactionShapeMeasurement`"
|
|
- "docs/mandantentrennung-etappe2-fehlerrichtung.md — Abschnitte `## Bereich favorites` ((f1)-(f5)) und `## Bereich settings` ((s1)-(s5)) sowie `## Etappe 2 — Abschluss`, alle VOR `## Verweis`; Nachtraege unter Befund K in (t4) und im Uebergaben-Absatz von (d4)"
|
|
- "apps/api/src/favorites/favorites.service.ts — `list(tenantId, userId, widgetId)`, `create(tenantId, userId, dto)`, `update(tenantId, id, userId, dto)`, `remove(tenantId, id, userId)`, `getIconBytes(tenantId, id, userId)` je mit EINEM Klienten `tenantPrisma`; Widget-Besitzriegel in `create`"
|
|
- "apps/api/src/favorites/favorites.service.spec.ts — NEU: Zwei-Klienten-Nachbau fuer `favoriteLink` und `widgetInstance`, Icon-Dienst als Attrappe, Wachhund, Grenzfaelle"
|
|
- "apps/api/src/favorites/favorites.controller.ts — reicht `tenantId` aus `extractContext` an alle fuenf Dienstmethoden durch; Kopfkommentar nennt die Mandantenquelle und den dashboard-Praezedenzfall"
|
|
- "apps/api/src/settings/settings.service.ts — `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` je mit EINEM Klienten `tenantPrisma`; der Startpfad umbenannt in `loadAnySmtpConfigForStartupTransport()`, weiterhin auf dem ungebundenen Klienten, mit Kopfkommentar beider Zustaende, Unsymmetrie, Reparaturweg und Ledger-Nummer"
|
|
- "apps/api/src/settings/settings.service.spec.ts — NEU: Zwei-Klienten-Nachbau fuer `smtpConfig` (ungebunden NUR `findFirst`, gebunden NUR `findUnique`/`upsert`), `nodemailer` und `CryptoService` als Attrappen, Wachhund je Anfrageweg, Null-Klienten-Nachweis fuer den Startpfad"
|
|
- "apps/api/src/mail/mail.module.ts — ruft den umbenannten Startpfad auf; Modulkommentar nennt beide Zustaende und die Rueckfallkette als Verdeckung"
|
|
- "docs/mandantentrennung-zugriffsklassifikation.md — Uebersichtszeilen `favorites`/`settings` mit neu gemessenen Zahlen, Summenzeile, Bestandsaufnahme-Zeilen (`favoriteLink` auf `gebunden`, `smtpConfig` auf `gemischt`, neue Zeile `favorites.service.ts`/`widgetInstance`), Klassen-Verteilung mit Stand-Vermerk und neuer Summe, Hintergrunddienst-Abschnitt mit sechstem Fall und angepasster Ueberschrift, `Was diese Etappe NICHT entscheidet` mit dem Mehrmandanten-Versand-Punkt und der erfuellten Befund-K-Bedingung"
|
|
- "docs/anleitung-entwicklung.md — der Absatz, der `FavoriteLink` als Tabelle OHNE Regel und `DkvService.loadConfig()` als ungebunden nennt, auf den gemessenen Stand gebracht (drei Regel-Migrationen, 23 Tabellen)"
|
|
- ".planning/WINDOWS.md — drei neue OFFENE Eintraege ueber `gsd-tools windows append`: Startpfad des Mailmoduls (eigener Eintrag), verschluckte Leere `favorites` (favorites-widget.tsx), verschluckte Leere `settings` (smtp-settings-form.tsx)"
|
|
key_links:
|
|
- "`tender-mail.service.ts:158` und `dkv-mail.service.ts:47` rufen `settingsService.getDecryptedSmtpConfig(tenantId)` — der Mandant kommt aus der jeweiligen gebundenen Schleife (260909-laa, 260909-mir). Bindet diese Methode, ist Befund K geschlossen; bleibt sie ungebunden, geht nach dem Scharfschalten KEINE Ausschreibungs- und KEINE DKV-Mail mehr raus (tender: `warn` + `null`, dkv: `throw`)."
|
|
- "`mail.module.ts:30` ruft den Startpfad in einer asynchronen `useFactory` VOR jedem Anfragekontext; liefert er `null`, greift Prioritaet 2-4 (Umgebungsvariablen, zuletzt `localhost:1025`) — `MailService.sendPasswordResetEmail` faengt jeden Transportfehler (T-02-12) und antwortet 200: das Verstummen ist doppelt verdeckt."
|
|
- "`FavoriteLink.widgetId` -> `WidgetInstance.id` (Fremdschluessel, ON DELETE CASCADE, 20260708090000): Fremdschluesselpruefungen laufen in PostgreSQL an der Zeilenschutz-Regel VORBEI — der Bezug auf ein fremdes Widget ist auf Datenbankebene nicht gesperrt (Pruefung 7 misst das); der Besitzriegel in `create` ist deshalb anwendungsseitig."
|
|
- "`favorites.controller.ts` `extractContext` liest `req.tenantId ?? req.user?.tenantId` — dieselbe Quelle wie `dashboard.controller.ts`, unter dessen Bindung `WidgetInstance` liegt. Eine andere Quelle wuerde Widget und Link unter einem `x-tenant-id`-Wechsel an verschiedene Mandanten binden; `favorites-api.ts` sendet die Kopfzeile nicht (gemessen)."
|
|
- "`settings.controller.ts` liest `req.tenantId` — fuer eine ADMIN-Konfigurationsseite die RICHTIGE Quelle (D-10: SUPER_ADMIN konfiguriert ueber `x-tenant-id` einen anderen Mandanten); die Selbstbedienungs-Begruendung aus `auth` (Claim) gilt hier NICHT, der Controller bleibt unveraendert."
|
|
- "`SmtpConfig_tenantId_key` (20260629130000) ist der Eindeutigkeitsindex, an dem ein ungebundenes `upsert` auf eine unsichtbare Zeile scheitert — die `dashboard`-Lehre (260910-krx, `PrismaClientUnknownRequestError` statt P2002) zur Ausfuehrungszeit erneut messen, nicht abschreiben."
|
|
---
|
|
|
|
<!-- planner-discipline-allow: this.prisma.favoriteLink -->
|
|
<!-- planner-discipline-allow: this\.prisma\.favoriteLink -->
|
|
<!-- planner-discipline-allow: this.prisma.smtpConfig -->
|
|
<!-- planner-discipline-allow: this\.prisma\.smtpConfig -->
|
|
<!-- planner-discipline-allow: getStartupSmtpConfig -->
|
|
<!-- planner-discipline-allow: forTenant -->
|
|
<!-- planner-discipline-allow: tenantId -->
|
|
<!-- planner-discipline-allow: tenantPrisma.findFirst -->
|
|
<!-- planner-discipline-allow: findFirst -->
|
|
<!-- planner-discipline-allow: PrismaService -->
|
|
<!-- planner-discipline-allow: fünf Fälle -->
|
|
|
|
<objective>
|
|
Etappe 2 der Mandantentrennung, LETZTER Lauf: die Bereiche `favorites` (sieben
|
|
ungebundene Zugriffe auf `favoriteLink` in fuenf Methoden) und `settings`
|
|
(vier ungebundene Zugriffe auf `smtpConfig` in vier Methoden) — zwei kleine
|
|
Bereiche gleicher Bauform, als EIN Durchlauf, weil keiner allein drei
|
|
Aufgaben traegt.
|
|
|
|
Zweck, dreifach. Erstens: `settings` haelt die SMTP-Zugangsdaten je Mandant,
|
|
und `getDecryptedSmtpConfig(tenantId)` ist der einzige Versandpfad fuer
|
|
Ausschreibungs-Meldungen und DKV-Ausfuhren. Der `tenders`-Lauf hat als Befund
|
|
K festgehalten: bleibt diese Methode ungebunden, geht nach dem Scharfschalten
|
|
fuer NIEMANDEN eine Mail raus. Dieser Lauf schliesst diese Bedingung und sagt
|
|
das an jeder Stelle, an der sie steht. Zweitens: `getStartupSmtpConfig()` ist
|
|
der SECHSTE Fall der Hintergrunddienst-Falle — ein Startpfad ohne
|
|
Mandantenkontext, der heute eine BELIEBIGE Zeile zieht (der SMTP-Server eines
|
|
beliebigen Mandanten bedient alle Systemmails) und nach dem Scharfschalten
|
|
`null` liefert; anders als bei `dkv` (WINDOWS #21) verdeckt hier eine
|
|
Rueckfallkette das Verstummen mit einem falschen Transport. Der Pfad wird
|
|
gemessen, benannt, markiert und als eigener Ledger-Eintrag gefuehrt — NICHT
|
|
gebunden, NICHT zu einem Mehrmandanten-Versand umgebaut (das ist eine
|
|
Funktion, dieselbe Klasse wie #21). Drittens: `favorites` ist Nutzerdaten
|
|
mit der findUnique-dann-loeschen-Bauform, die dieses Vorhaben zweimal falsch
|
|
und zweimal richtig vorgefunden hat — sie wird gelesen und gemessen, und der
|
|
Fremdschluessel auf `WidgetInstance` (der den Zeilenschutz umgeht) bekommt
|
|
einen anwendungsseitigen Besitzriegel.
|
|
|
|
Nach diesem Lauf ist Etappe 2 vollstaendig: jede klassifizierte Fundstelle in
|
|
`apps/api/src` ist gebunden oder mit geschriebenem Grund an der Stelle
|
|
ungebunden. Die fuenf handgepflegten Dokumentstellen erreichen ihren Endstand,
|
|
die Kritikschrift bekommt einen Abschluss-Abschnitt aus ihren eigenen Zahlen.
|
|
|
|
**Der Schalter bleibt AUS.** `DATABASE_URL` zeigt weiterhin auf die Rolle
|
|
`tessera` mit `BYPASSRLS`. Schema und Migrationen werden NICHT angefasst.
|
|
Nichts wird in Active Directory geaendert. Kein echter SMTP-Transport wird
|
|
aufgebaut (lokal gibt es keinen `mailhog`; `ENOTFOUND mailhog` waere
|
|
Umgebung, nicht Befund).
|
|
</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
|
|
@docs/anleitung-entwicklung.md
|
|
@apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql
|
|
@apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql
|
|
@apps/api/prisma/migrations/20260708090000_add_favorite_link/migration.sql
|
|
@apps/api/prisma/migrations/20260629130000_add_missing_tables/migration.sql
|
|
@apps/api/prisma/schema.prisma
|
|
@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/favorites/favorites.service.ts
|
|
@apps/api/src/favorites/favorites.controller.ts
|
|
@apps/api/src/favorites/favorites.module.ts
|
|
@apps/api/src/favorites/icon-discovery.service.ts
|
|
@apps/api/src/favorites/icon-discovery.service.spec.ts
|
|
@apps/api/src/favorites/dto/create-favorite.dto.ts
|
|
@apps/api/src/favorites/dto/update-favorite.dto.ts
|
|
@apps/api/src/settings/settings.service.ts
|
|
@apps/api/src/settings/settings.controller.ts
|
|
@apps/api/src/settings/settings.module.ts
|
|
@apps/api/src/settings/dto/smtp-config.dto.ts
|
|
@apps/api/src/mail/mail.module.ts
|
|
@apps/api/src/mail/mail.service.ts
|
|
@apps/api/src/tenders/tender-mail.service.ts
|
|
@apps/api/src/tenders/tender-mail.service.spec.ts
|
|
@apps/api/src/dkv/dkv-mail.service.ts
|
|
@apps/api/src/dkv/dkv.service.ts
|
|
@apps/api/src/dkv/dkv-scheduler.service.ts
|
|
@apps/api/src/dkv/dkv.service.spec.ts
|
|
@apps/api/src/calendar/calendar.service.spec.ts
|
|
@apps/api/src/dashboard/dashboard.service.spec.ts
|
|
@apps/api/src/dashboard/dashboard.controller.ts
|
|
@apps/api/src/tenant/tenant.guard.ts
|
|
@apps/web/src/lib/favorites-api.ts
|
|
@apps/web/src/components/dashboard/widgets/favorites-widget.tsx
|
|
@apps/web/src/lib/settings-api.ts
|
|
@apps/web/src/components/settings/smtp-settings-form.tsx
|
|
@apps/web/src/messages/de.json
|
|
@.planning/WINDOWS.md
|
|
@.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-PLAN.md
|
|
@.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.md
|
|
</context>
|
|
|
|
<planning_time_findings>
|
|
|
|
Alle Zahlen unten sind zur Planungszeit am 2026-09-11 gegen HEAD `46f0e78`
|
|
GEMESSEN, mit der jeweils angegebenen Anweisung. Sie leiten die Untersuchung,
|
|
sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder Aenderung
|
|
erneut gelesen, jede Zahl zur Ausfuehrungszeit erneut gemessen, und weicht
|
|
eine Messung ab, gilt die Messung und nicht dieser Plan. Baseline laut
|
|
`260911-fh9-SUMMARY.md`: 951 Tests gruen in 60 Dateien, Typpruefung sauber,
|
|
Werkzeug 120/120 — zur Ausfuehrungszeit VOR der ersten Aenderung selbst
|
|
nachmessen.
|
|
|
|
**Befund A — die Zahlen halten: 7/0 und 4/0, beide Bereiche ohne einen
|
|
einzigen gebundenen Zugriff.**
|
|
`grep -rno "this\.prisma\.[a-zA-Z]*" apps/api/src/favorites apps/api/src/settings | grep -v spec`:
|
|
sieben Treffer in `favorites.service.ts` (Zeilen 34 `list`/findMany, 55
|
|
`create`/create, 74 `update`/findUnique, 103 `update`/update, 114
|
|
`remove`/findUnique, 120 `remove`/delete, 138 `getIconBytes`/findUnique) und
|
|
vier in `settings.service.ts` (39 `getSmtpConfig`/findUnique, 74
|
|
`saveSmtpConfig`/upsert, 97 `getDecryptedSmtpConfig`/findUnique, 193
|
|
`getStartupSmtpConfig`/findFirst). Gebunden: null. Nach diesem Plan:
|
|
`favorites` 0 ungebunden / 7 gebunden (8, wenn der Besitzriegel aus Befund F
|
|
einen `widgetInstance`-Lesezugriff hinzufuegt); `settings` 1 ungebunden (der
|
|
Startpfad) / 3 gebunden. Summenzeile 78 -> 67 und 167 -> 177 (bzw. 178).
|
|
Bestandsaufnahme: `favorites.service.ts`/`favoriteLink` von `ungebunden` auf
|
|
`gebunden`; `settings.service.ts`/`smtpConfig` von `ungebunden` auf
|
|
`gemischt` (dieselbe Form wie `dkv.service.ts`/`dkvModuleConfig`: die
|
|
Mischung stammt ausschliesslich vom benannten Startpfad); NEUE Zeile
|
|
`favorites.service.ts`/`widgetInstance` (`muss-mandantengebunden`,
|
|
`gebunden`), wenn Befund F den Riegel bestaetigt -> 65 Paare, 33/17/13/2.
|
|
Alle Zahlen zur Ausfuehrungszeit aus den Messanweisungen des Dokuments
|
|
ableiten, nicht von hier abschreiben.
|
|
|
|
**Befund B — die Regeln sind eindeutig und wortgleich extrahierbar.**
|
|
`20260909140000_rls_remaining_tenant_tables` traegt fuer `FavoriteLink`
|
|
(Zeile 88-92) und `SmtpConfig` (102-106) je ENABLE + FORCE + `CREATE POLICY
|
|
tenant_isolation_policy ... USING ("tenantId" = current_tenant_id())`;
|
|
`grep -c "FavoriteLink\|SmtpConfig" 20260910120000_.../migration.sql`: 0 —
|
|
die Widen-Migration hat keine eigene Regel fuer beide (Regelstand eindeutig,
|
|
Form von `calendarsource-regelstand-eindeutig`). Spalten: `FavoriteLink`
|
|
(20260708090000) hat 10 skalare Spalten (`id`, `userId`, `tenantId`,
|
|
`widgetId`, `title`, `url`, `iconUrl`, `position`, `createdAt`, `updatedAt`)
|
|
und den Fremdschluessel `FavoriteLink_widgetId_fkey` auf `"WidgetInstance"("id")
|
|
ON DELETE CASCADE`; `model FavoriteLink` hat zusaetzlich das RELATIONSFELD
|
|
`widgetInstance` — die Wegwerf-Tabelle muss ueber
|
|
`readSchemaModelScalarFieldNames('FavoriteLink')` (der Helfer aus 260911-fh9)
|
|
verglichen werden, nicht ueber `readSchemaModelFieldNames`. `SmtpConfig`
|
|
(20260629130000) hat 10 skalare Spalten (`id`, `tenantId`, `host`, `port`,
|
|
`encryption`, `username`, `encryptedPassword`, `fromAddress`, `createdAt`,
|
|
`updatedAt`) und den Eindeutigkeitsindex `SmtpConfig_tenantId_key` (Zeile
|
|
169) — ohne diesen Index misst Pruefung 8 des settings-Abschnitts nichts.
|
|
`updatedAt` hat in beiden Migrationen keine Vorgabe; die Wegwerf-Tabellen
|
|
bekommen fuer die Roh-SQL-Einfuegungen `DEFAULT CURRENT_TIMESTAMP` mit
|
|
Vermerk als Abweichung, die nur das Nachruesten betrifft (Form von
|
|
260911-e2s/fh9).
|
|
|
|
**Befund C — die Wegwerf-Tabelle `WidgetInstance` existiert bereits, ohne
|
|
`createdAt`/`updatedAt`, mit drei Zeilen.** `runDashboardAreaChecks`
|
|
(`rls-scratch-check.mjs:2461-2467`) legt `"WidgetInstance"` mit `id`,
|
|
`userId`, `tenantId`, `widgetType` an und fuegt `widget-a1` (user-a1,
|
|
TENANT-A), `widget-a2` (user-a2, TENANT-A), `widget-b1` (user-b1, TENANT-B)
|
|
ein; der gebundene Loeschversuch unter TENANT-A auf `widget-b1` (Zeile 2687)
|
|
trifft null Zeilen, `widget-rejected` wird abgewiesen — alle drei Zeilen
|
|
sollten stehen, wenn der neue Abschnitt laeuft: zur Ausfuehrungszeit ueber
|
|
die Wartungsrolle NACHMESSEN, nicht annehmen. Der Fremdschluessel der
|
|
Wegwerf-Tabelle `FavoriteLink` zeigt auf DIESE Tabelle (WINDOWS #27:
|
|
Relationen sind fuer die Bestandsaufnahme unsichtbar — hier wird die
|
|
Relation deshalb ausdruecklich mitgebaut und gemessen). Der neue Abschnitt
|
|
ist ein Blatt: NACH `runAuthAreaChecks`, VOR `runTransactionShapeMeasurement`;
|
|
keine spaetere Pruefung setzt auf seinen Tabellen auf.
|
|
|
|
**Befund D — der Startpfad, Glied fuer Glied gelesen.**
|
|
`settings.service.ts:193` `findFirst()` ohne jede Bedingung; einziger
|
|
Aufrufer `mail.module.ts:30` in `MailerModule.forRootAsync({ useFactory:
|
|
async ... })` — BEIM START, vor jedem Anfragekontext, Kommentar dort:
|
|
"findFirst — single-tenant default". Ergebnis wird zum EINEN Transport des
|
|
`MailerService`, den `MailService.sendPasswordResetEmail` (einziger Aufrufer
|
|
ausserhalb des Mailmoduls: `auth.service.ts:246`, `requestPasswordReset`)
|
|
und `sendWelcomeEmail` benutzen. HEUTE bei mehreren Mandanten: der
|
|
SMTP-Server und die Absenderadresse eines BELIEBIGEN Mandanten bedienen die
|
|
Kennwort-Zuruecksetzungs-Mails ALLER Mandanten — Mandant B versendet ueber
|
|
den Server von Mandant A, mit dessen Absender (das ist Nutzung fremder
|
|
Zugangsdaten, nicht nur Sichtbarkeit; T-GWH-03). NACH DEM SCHARFSCHALTEN:
|
|
`findFirst` liefert `null` -> `mail.module.ts` faellt auf Prioritaet 2
|
|
(`MAIL_*`), 3 (`TESSERA_SMTP_*`), 4 (`localhost:1025`, Mailhog) zurueck ->
|
|
`MailService` faengt jeden Transportfehler (`mail.service.ts:69-81`, T-02-12)
|
|
und der Controller antwortet 200. Das Verstummen ist damit DOPPELT verdeckt:
|
|
erst durch die Rueckfallkette (ein falscher, aber vorhandener Transport statt
|
|
Leere), dann durch das absichtliche Verschlucken im Versand. Das ist die
|
|
UNSYMMETRIE zu den beiden Praezedenzfaellen: `getAllActiveConfigs` (ldap)
|
|
ist heute korrekt und verstummt spaeter; `loadAnyActiveConfigForScheduler`
|
|
(dkv, #21) ist heute falsch und verstummt spaeter mit Protokollzeile; der
|
|
Mail-Startpfad ist heute falsch und verstummt spaeter OHNE Protokollzeile,
|
|
weil eine Rueckfallkette ihn ueberdeckt. ENTSCHEIDUNG (zur Ausfuehrungszeit
|
|
nach der Lesung zu bestaetigen oder mit Grund zu verwerfen): EIGENER
|
|
Ledger-Eintrag, nicht Anschluss an #21 — #21 ist gegen
|
|
`dkv-scheduler.service.ts` gefuehrt, seine Reparatur ist die
|
|
Mehrmandanten-Planung (Auftraege je Mandant); die Reparatur HIER ist eine
|
|
andere Funktion mit bereits vorhandener Vorlage im Code: Transport je Versand
|
|
aus `getDecryptedSmtpConfig(tenantId)` bauen, wie `DkvMailService` und
|
|
`TenderMailService` es seit Phase 07/12 tun — `requestPasswordReset` kennt
|
|
den Mandanten aus der Funktionszeile, `MailService` muesste ihn nur
|
|
entgegennehmen. Das ist ein Feature (Umbau des Mailmoduls), NICHT dieser
|
|
Auftrag — steht so im Auftrag und in `<hard_constraints>`. Umbenennung nach
|
|
dem dkv-Praezedenzfall: `loadAnySmtpConfigForStartupTransport()`; kein Dokument
|
|
unter `docs/` nennt den alten Namen (`grep -rn getStartupSmtpConfig docs`:
|
|
null), die Phasen-Artefakte unter `.planning/phases` sind Geschichte und
|
|
werden nicht angefasst.
|
|
|
|
**Befund E — der Versandpfad und Befund K, Glied fuer Glied.**
|
|
`tender-mail.service.ts:158` (`resolveTransport`, privat; bei `null`:
|
|
`logger.warn(... skipping tender mail send (will retry next run))`, kein
|
|
Wurf) und `dkv-mail.service.ts:47` (`sendExportEmail`; bei `null`: `throw new
|
|
Error('No SMTP configuration found for tenant ...')`) rufen
|
|
`getDecryptedSmtpConfig(tenantId)`; der Mandant kommt bei beiden aus der
|
|
gebundenen Schleife des aufrufenden Bereichs (260909-laa, 260909-mir).
|
|
`testSmtpConfig` ruft sie ebenfalls (Rueckgriff auf gespeicherte
|
|
Zugangsdaten) und hat KEINEN eigenen Datenbankzugriff. Die
|
|
Reihenfolgebedingung steht an zwei Stellen der Kritikschrift ((t4) Befund K,
|
|
Zeile 625-634; (d4) Uebergaben-Absatz, Zeile 935-944) und NIRGENDS im
|
|
Klassifikationsdokument (`grep -n "settings" docs/mandantentrennung-zugriffsklassifikation.md`:
|
|
nur Uebersichtszeile 147 und Bestandsaufnahme 453). Nach diesem Plan: an
|
|
beiden Stellen ein NACHTRAG (Form von (e) Befund D, Zeile 143-155: Original
|
|
bleibt stehen, Nachtrag darunter), und der neue Hintergrunddienst-Absatz im
|
|
Klassifikationsdokument sagt, dass die Bedingung mit 260911-gwh erfuellt ist.
|
|
|
|
**Befund F — die Besitzpruefungen: dreimal richtig, einmal fehlend, und der
|
|
Fremdschluessel umgeht den Zeilenschutz.** `update`, `remove`, `getIconBytes`
|
|
tun `findUnique({ where: { id } })`, vergleichen `link.userId !== userId`, und
|
|
schreiben/loeschen dann ueber die Kennung — nach der Bindung auf DEMSELBEN
|
|
Klienten ist das die richtige Form (fremde Zeile: `findUnique` null ->
|
|
`NotFoundException`; dkv Befund G — ein gebundenes UPDATE/DELETE ueber die
|
|
Kennung allein traefe eine fremde Zeile still — greift nicht, weil die
|
|
Vorpruefung davor steht; mit dem generierten Client wirft `delete` bei null
|
|
Zeilen zudem P2025, Pruefung 6 misst das). `create` dagegen prueft NICHT, ob
|
|
`dto.widgetId` dem Aufrufer gehoert: es schreibt `userId`/`tenantId` des
|
|
Aufrufers und `widgetId` aus dem Rumpf. PostgreSQL prueft Fremdschluessel AN
|
|
DER ZEILENSCHUTZ-REGEL VORBEI (dokumentiertes Verhalten: referentielle
|
|
Integritaet umgeht Row Security) — ein gebundenes `create` unter TENANT-A mit
|
|
`widgetId` eines TENANT-B-Widgets wuerde demnach GELINGEN, und der
|
|
Unterschied zwischen "Widget existiert nicht" (FK-Verletzung, 500) und
|
|
"existiert bei einem fremden Mandanten" (gelingt) ist ein Existenzorakel
|
|
ueber Mandantengrenzen (T-GWH-05, medium). Pruefung 7 MISST das, statt es
|
|
anzunehmen; faellt sie so aus, baut Aufgabe 2 einen Besitzriegel in `create`
|
|
ein: gebundener `widgetInstance.findUnique({ where: { id: dto.widgetId },
|
|
select: { userId: true } })` auf DEMSELBEN `tenantPrisma`, `null` oder
|
|
fremde Benutzerkennung -> `NotFoundException('Widget not found')` (ohne
|
|
Aussage ueber fremde Mandanten). Das fuegt ein NEUES (Datei, Modell)-Paar
|
|
hinzu (Befund A). Faellt Pruefung 7 anders aus, gilt die Messung, der Riegel
|
|
entfaellt, und das steht mit Belegzeile im SUMMARY.
|
|
|
|
**Befund G — der Icon-Proxy erreicht die Datenbank NICHT und nimmt KEINE
|
|
Client-URL.** `grep -n "prisma\|Prisma" icon-discovery.service.ts`: null
|
|
Treffer; `getIconBytes` holt nur die GESPEICHERTE `iconUrl` einer Zeile, die
|
|
der Aufrufer besitzt (T-QFIP-01, Controller `GET /favorites/:id/icon` nimmt
|
|
nur die Kennung); `discoverFavoriteIconUrl(url)` bei `create`/`update` nimmt
|
|
die Nutzer-URL, aber nur fuer den SSRF-gesicherten Abruf (privater
|
|
IP-Bereich, gesperrte Hostnamen, manuelle Weiterleitung — T-08-05,
|
|
`icon-discovery.service.spec.ts` deckt das). Mandantenbezug: keiner — die
|
|
URL ist nicht mandantenskopiert, der Dienst haelt keinen Zustand je Mandant.
|
|
Ergebnis: der Proxy bleibt UNVERAENDERT; in der Testdatei ist er eine
|
|
Attrappe (`discoverFavoriteIconUrl`, `fetchIconBytes` als `vi.fn`), und der
|
|
Test nagelt fest, dass `fetchIconBytes` mit der GESPEICHERTEN URL aufgerufen
|
|
wird, nie mit einer aus dem Aufruf.
|
|
|
|
**Befund H — die Mandantenquelle je Bereich, beide unterschiedlich und beide
|
|
richtig.** `favorites.controller.ts` `extractContext`: `req.tenantId ??
|
|
req.user?.tenantId` — WORTGLEICH mit `dashboard.controller.ts:44-45`, unter
|
|
dessen Bindung `WidgetInstance` liegt. `FavoriteLink` haengt am Widget;
|
|
wuerde `favorites` an das Claim binden (wie `auth` fuer Selbstbedienung),
|
|
`dashboard` aber an die Guard-Kennung, laegen Widget und Link unter einem
|
|
`x-tenant-id`-Wechsel eines SUPER_ADMIN in verschiedenen Mandanten.
|
|
`favorites-api.ts` sendet die Kopfzeile nicht (`grep -rn "x-tenant-id"
|
|
apps/web/src`: nur die vier Marktplatz-Stellen) — die Entscheidung haengt an
|
|
der Bauform, nicht am heutigen Aufrufer. ENTSCHEIDUNG: `extractContext`
|
|
bleibt, alle fuenf Aufrufe reichen `tenantId` durch; Kopfkommentar nennt den
|
|
dashboard-Praezedenzfall und warum NICHT der auth-Praezedenzfall.
|
|
`settings.controller.ts` liest `req.tenantId` — fuer eine
|
|
ADMIN-Konfigurationsseite ist das die richtige Quelle (D-10: SUPER_ADMIN
|
|
konfiguriert ueber `x-tenant-id` einen anderen Mandanten); der Controller
|
|
bleibt UNVERAENDERT und steht nicht in der Erlaubnisliste.
|
|
|
|
**Befund I — die umgekehrte Fehlerrichtung je Bereich, gelesen.**
|
|
(1) `favorites`: `list` liefert `[]` -> `GET /favorites?widgetId=` 200 `[]`
|
|
-> `fetchFavorites` (`favorites-api.ts:22-29`) gibt `[]` -> `favorites-widget.tsx:212-213`
|
|
zeigt `t('favorites.empty')` = `Noch keine Favoriten.` (`de.json`, Zeile um
|
|
219). "Zeile unsichtbar" und "nie einen gespeichert" sind fuer das Frontend
|
|
derselbe Wert — Familie #23/#25/#26/#28. `update`/`remove`/`getIconBytes`:
|
|
`NotFoundException('FavoriteLink not found')` 404 -> `favorites-api.ts` wirft
|
|
`Failed to update/delete favorite` -> `catch` im Widget setzt
|
|
`t('favorites.error')`. (2) `settings`: `getSmtpConfig` liefert `null` ->
|
|
Controller gibt `null` zurueck -> NestJS sendet 200 mit LEEREM Rumpf (die
|
|
Adapter-Kette aus (h3), 260911-fh9) -> `fetchSmtp` (`settings-api.ts:51-57`)
|
|
prueft nur `404` und `!res.ok`, dann `res.json()` auf den leeren Rumpf ->
|
|
wirft -> `smtp-settings-form.tsx:76-78` `.catch(() => { /* Silent fail */ })`
|
|
-> leeres Formular: "SMTP nicht eingerichtet", waehrend die Zugangsdaten
|
|
physisch da sind. Traegt der Administrator sie neu ein, laeuft `saveSmtpConfig`
|
|
als `upsert({ where: { tenantId } })`: unter der UNGEBUNDENEN Form (heute, nach
|
|
dem Scharfschalten) ist die Zeile unsichtbar, der Upsert versucht INSERT und
|
|
scheitert am Eindeutigkeitsindex `SmtpConfig_tenantId_key` — die
|
|
`dashboard`-Lehre (260910-krx: `PrismaClientUnknownRequestError`, NICHT
|
|
P2002). Pruefung 8 misst Konstruktorname und `code` woertlich, statt sie
|
|
vorwegzunehmen. `getDecryptedSmtpConfig` null: tender `warn` + kein Versand
|
|
mit Wiederholung je Lauf, dkv `throw` (Befund E). (3) Startpfad: Befund D.
|
|
Etappe-4-Vorabpruefung je Bereich: fuer einen bekannten Mandanten die
|
|
`SmtpConfig`-Zeile ueber die Wartungsrolle lesen und den gebundenen
|
|
`findUnique` daneben halten; fuer einen bekannten Nutzer/Widget die
|
|
Favoritenzahl ueber die Wartungsrolle und ueber den gebundenen `findMany`.
|
|
|
|
**Befund J — Testlage: eine Spec fuer den Icon-Proxy, keine fuer die beiden
|
|
Dienste; Vorlagen stehen bereit.** `ls apps/api/src/favorites/`: nur
|
|
`icon-discovery.service.spec.ts`; `ls apps/api/src/settings/`: keine Spec.
|
|
Vorlagen: `calendar.service.spec.ts` (260911-cwh — Bereich ohne Testdatei,
|
|
`makeFakePrisma` mit In-Memory-Zeilen, `__makeBoundClient`, `_applySelect`,
|
|
`boundCallLog`), `dkv.service.spec.ts` (ungebundener Nachbau OHNE die
|
|
Anfrage-Modelle), `dashboard.service.spec.ts:565` (Wachhund),
|
|
`tender-mail.service.spec.ts:22-30` (`makeSettingsService`-Attrappe: zeigt,
|
|
welche Rueckgabeform die Aufrufer erwarten — `host`, `port`, `encryption`,
|
|
`username`, `fromAddress`, `decryptedPassword`). `nodemailer` wird in der
|
|
neuen settings-Spec per `vi.mock('nodemailer')` ersetzt (`createTransport`
|
|
liefert `{ verify: vi.fn(), sendMail: vi.fn() }`) — KEIN echter Transport
|
|
(Umgebung: kein `mailhog`). `CryptoService` als Attrappe (`encrypt: (s) =>
|
|
'enc(' + s + ')'`, `decrypt` strippt).
|
|
|
|
**Befund K — kein weiterer Hintergrunddienst, keine Transaktion, keine
|
|
Relationseinbindung — bis auf den Startpfad.** `grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout\|\$transaction(\|\$queryRaw\|\$executeRaw\|include:\|_count" apps/api/src/favorites apps/api/src/settings --include=*.ts | grep -v spec`:
|
|
genau EIN Treffer, `icon-discovery.service.ts:255` — ein `setTimeout` fuer
|
|
den Abbruch eines HTTP-Abrufs, kein Planer (dieselbe Form wie
|
|
`ics.provider.ts:100` in 260911-cwh). `select:` kommt zweimal vor
|
|
(`settings.service.ts:41`, `:78`), beide ausschliesslich skalare Felder von
|
|
`SmtpConfig`, keine Relation (WINDOWS #27 ohne Auspraegung). Der einzige
|
|
Hintergrund-Zugriff ist der Startpfad aus Befund D — er lebt NICHT in
|
|
`settings`, sondern wird von `mail.module.ts` beim Start aufgerufen; als
|
|
sechster Fall gehoert er unter `mail.module.ts` /
|
|
`SettingsService.loadAnySmtpConfigForStartupTransport()`.
|
|
|
|
**Befund L — die Buchfuehrung und ein veralteter Absatz in der Anleitung.**
|
|
Uebersichtszeilen `favorites` (heute `| favorites | 7 | 0 | unverändert |`)
|
|
und `settings` (`| settings | 4 | 0 | unverändert |`), Summenzeile (78/167),
|
|
Bestandsaufnahme-Zeilen 431 und 453, Klassen-Verteilung (64 Paare,
|
|
32/17/13/2), Hintergrunddienst-Abschnitt (Ueberschrift `## Der
|
|
Hintergrunddienst als Falle — fünf Fälle`, Zeile 237; kein Gate ausserhalb
|
|
des Dokuments referenziert diesen Wortlaut — `grep -rn "fünf Fälle\|fuenf
|
|
Faelle" apps/api/src apps/api/scripts`: null), `Was diese Etappe NICHT
|
|
entscheidet` (Zeile 486). Dazu `docs/anleitung-entwicklung.md:310-316`: der
|
|
Absatz behauptet, Regeln laegen nur auf sieben Tabellen und `FavoriteLink`
|
|
trage KEINE, und `DkvService.loadConfig()` nutze den ungebundenen Klienten —
|
|
beides seit `20260909140000` (16 weitere Tabellen, gemessen: `grep -c
|
|
"ENABLE ROW LEVEL SECURITY"` liefert 4 + 3 + 16 = 23) bzw. seit 260909-mir
|
|
falsch. Der Absatz nennt `FavoriteLink` beim Namen und liegt damit in diesem
|
|
Bereich; er wird in Aufgabe 3 auf den gemessenen Stand gebracht (scoped
|
|
`Edit`, nur dieser Absatz).
|
|
|
|
</planning_time_findings>
|
|
|
|
<tasks>
|
|
|
|
<task type="tracer">
|
|
<name>Aufgabe 1: Die Fehlerrichtung fuer BEIDE Bereiche MESSEN und aufschreiben — Favoritenleiste, SMTP-Zugangsdaten, Startpfad — ueber den generierten Client</name>
|
|
<precondition>Der lokale Datenbank-Container `tessera-ctl-db-1` laeuft; `docker inspect tessera-ctl-db-1` liefert eine Adresse. Ohne ihn kann das Wegwerf-Werkzeug nichts messen und die Aufgabe ist zu stoppen, nicht zu schaetzen.</precondition>
|
|
<files>apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
|
<read_first>apps/api/src/prisma/prisma-tenant.extension.ts (Kopf VOLLSTAENDIG), apps/api/scripts/rls-scratch-check.mjs (Kopf, `report`, `forTenantQuery`, `withAdminPrisma`, `sqlStateOf`, `readRlsWidenMigrationSql`, `readRemainingTenantTablesMigrationSql`, `extractPolicySql`, `readSchemaModelScalarFieldNames`, `runDashboardAreaChecks` VOLLSTAENDIG (Anlage von "WidgetInstance", welche Zeilen am Ende stehen), `runDkvAreaChecks` (die Pruefung `dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile` als Vorbild fuer den Startpfad), `runCalendarAreaChecks` VOLLSTAENDIG (Blatt-Form, Spaltenpruefung, Client-Pruefungen), `runAuthAreaChecks` (die Client-Pruefungen mit Konstruktorname/`code`), `buildInlineExtendedClient`, `main`), apps/api/prisma/schema.prisma (model FavoriteLink, model SmtpConfig, model WidgetInstance), apps/api/prisma/migrations/20260708090000_add_favorite_link/migration.sql (VOLLSTAENDIG), apps/api/prisma/migrations/20260629130000_add_missing_tables/migration.sql (CREATE TABLE "SmtpConfig" und den Index `SmtpConfig_tenantId_key`), apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql (die Bloecke FavoriteLink, SmtpConfig, WidgetInstance), apps/api/src/favorites/favorites.service.ts (VOLLSTAENDIG), apps/api/src/favorites/favorites.controller.ts (VOLLSTAENDIG), apps/api/src/favorites/icon-discovery.service.ts (Kopf, `discoverFavoriteIconUrl`, `fetchIconBytes`), apps/api/src/settings/settings.service.ts (VOLLSTAENDIG), apps/api/src/settings/settings.controller.ts (VOLLSTAENDIG), apps/api/src/mail/mail.module.ts (VOLLSTAENDIG), apps/api/src/mail/mail.service.ts (`sendPasswordResetEmail`, den `try/catch`), apps/api/src/tenders/tender-mail.service.ts (`resolveTransport`), apps/api/src/dkv/dkv-mail.service.ts (`sendExportEmail`), apps/api/src/dkv/dkv.service.ts (Kopfkommentar von `loadAnyActiveConfigForScheduler` VOLLSTAENDIG), apps/api/src/dkv/dkv-scheduler.service.ts (Kopf), apps/api/src/dashboard/dashboard.controller.ts (`extractContext`), apps/web/src/lib/favorites-api.ts, apps/web/src/components/dashboard/widgets/favorites-widget.tsx (den `useEffect` um Zeile 63 und die Leerstelle um Zeile 212), apps/web/src/lib/settings-api.ts (`fetchSmtp`), apps/web/src/components/settings/smtp-settings-form.tsx (den `useEffect` um Zeile 62), apps/web/src/messages/de.json (Schluessel `favorites.empty`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitte `## Bereich dkv` (d1)-(d5) und `## Bereich auth` (h1)-(h5) vollstaendig, als Form; (t4) Befund K; (e) Befund D samt Nachtrag als Form fuer Nachtraege), .planning/WINDOWS.md (Eintrag #21 VOLLSTAENDIG)</read_first>
|
|
<action>
|
|
TEIL 1 — die Messung, zwei Abschnitte. Erweitere `apps/api/scripts/rls-scratch-check.mjs`
|
|
um `runFavoritesAreaChecks(adminUrl, scratchRoleUrl, results)` und
|
|
`runSettingsAreaChecks(adminUrl, scratchRoleUrl, results)` und rufe beide in
|
|
`main()` in dieser Reihenfolge NACH `runAuthAreaChecks` und VOR
|
|
`runTransactionShapeMeasurement` auf. Beide sind Blaetter (keine spaetere
|
|
Pruefung setzt auf ihren Tabellen auf); halte die Reihenfolgebedingung im
|
|
Kopfkommentar fest, wie `runCalendarAreaChecks` es vormacht. Beide beginnen
|
|
mit der Regelstand-Pruefung (die Widen-Migration darf keine eigene Regel fuer
|
|
die Tabelle tragen — Form `calendarsource-regelstand-eindeutig`), schneiden
|
|
die Regel WORTGLEICH aus `20260909140000_rls_remaining_tenant_tables`
|
|
(`extractPolicySql`), legen die Wegwerf-Tabelle mit SAEMTLICHEN skalaren
|
|
Spalten an, pruefen die Spaltenmenge zur Laufzeit gegen
|
|
`readSchemaModelScalarFieldNames(<Modell>)` (bei `FavoriteLink` ist das
|
|
Relationsfeld `widgetInstance` auszufiltern — deshalb der Helfer aus
|
|
260911-fh9, nicht `readSchemaModelFieldNames`) und brechen ab, wenn die
|
|
Spaltenpruefung durchfaellt (Form von Pruefung 8 in `runCalendarAreaChecks`).
|
|
`updatedAt` bekommt `DEFAULT CURRENT_TIMESTAMP` als vermerkte Abweichung
|
|
(Prisma setzt den Wert clientseitig). Alle Pruefungen laufen unter der Rolle
|
|
ohne `BYPASSRLS`; Vergleichswerte kommen ueber die Wartungsrolle. Jede
|
|
Pruefung hat eine Belegausgabe mit den beobachteten Werten; wo ein Fehler
|
|
erwartet wird, Konstruktorname und `code` woertlich — rate das Ergebnis nicht
|
|
vorweg.
|
|
|
|
`runFavoritesAreaChecks` — die Wegwerf-Tabelle `"FavoriteLink"` bekommt den
|
|
Fremdschluessel `FOREIGN KEY ("widgetId") REFERENCES "WidgetInstance"("id")
|
|
ON DELETE CASCADE` auf die von `runDashboardAreaChecks` angelegte Tabelle
|
|
(Befund C — vorher ueber die Wartungsrolle MESSEN, welche `WidgetInstance`-
|
|
Zeilen stehen, und die Belegausgabe der ersten Pruefung nennt sie).
|
|
Zeilen: `fav-a1-1`, `fav-a1-2` (user-a1, TENANT-A, widget-a1), `fav-a2-1`
|
|
(user-a2, TENANT-A, widget-a2), `fav-b1-1` (user-b1, TENANT-B, widget-b1);
|
|
`fav-a1-1` mit `iconUrl`, `fav-a1-2` ohne. Mindestens sieben Pruefungen:
|
|
|
|
1. `favoritelink-regelstand-eindeutig`.
|
|
2. `favoritelink-wegwerftabelle-deckt-alle-spalten-des-generierten-clients`
|
|
— zehn skalare Felder; die Belegausgabe nennt zusaetzlich die gemessenen
|
|
`WidgetInstance`-Zeilen.
|
|
3. `favoritelink-liste-generierter-client-ungebunden-liefert-leere-liste` —
|
|
die tragende Belegzeile: `prisma.favoriteLink.findMany({ where: { userId:
|
|
'user-a1', widgetId: 'widget-a1' }, orderBy: [{ position: 'asc' }, {
|
|
title: 'asc' }] })` — die Form von `list` — UNGEBUNDEN: `[]`, waehrend die
|
|
Wartungsrolle zwei Zeilen sieht. Die Belegausgabe sagt beim Namen: das ist
|
|
der Wert, aus dem `favorites-widget.tsx` `Noch keine Favoriten.` macht.
|
|
4. `favoritelink-liste-generierter-client-gebunden-eigener-mandant-liefert-eigene-zeilen`
|
|
— dieselbe Abfrage ueber `buildInlineExtendedClient(prisma, 'TENANT-A')`:
|
|
genau zwei Zeilen, beide `userId === 'user-a1'`, `widgetId === 'widget-a1'`;
|
|
die Zeile von `user-a2` ist NICHT dabei (die anwendungsseitige
|
|
Benutzerfilterung), aber — zweite Aussage derselben Messung, eigene
|
|
Belegausgabe — ein gebundener `findMany({ where: { widgetId: 'widget-a2' } })`
|
|
unter TENANT-A liefert die Zeile des Kollegen: die Regel kennt keine
|
|
Benutzerdimension (dieselbe Lehre wie `calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar`).
|
|
5. `favoritelink-besitzpruefung-generierter-client-gebunden-fremder-mandant-liefert-null`
|
|
— `findUnique({ where: { id: 'fav-a1-1' } })` gebunden unter TENANT-B:
|
|
`null`. Das ist die Datenbankseite der Vorpruefung in `update`/`remove`/
|
|
`getIconBytes` (T-GWH-02).
|
|
6. `favoritelink-gebundenes-loeschen-ueber-kennung-allein-fremder-mandant-scheitert-laut`
|
|
— `delete({ where: { id: 'fav-a1-1' } })` ueber den GENERIERTEN Client
|
|
gebunden unter TENANT-B: bestanden genau dann, wenn ein Fehler geworfen
|
|
wird UND die Wartungsrolle die Zeile danach noch liest. Konstruktorname und
|
|
`code` woertlich (der generierte Client meldet null getroffene Zeilen bei
|
|
`delete` anders als Roh-SQL — dkv Befund G in der Client-Form).
|
|
7. `favoritelink-fremdschluessel-prueft-am-zeilenschutz-vorbei` — gebundenes
|
|
`create` unter TENANT-A mit `data: { id: 'fav-a1-fremdes-widget', userId:
|
|
'user-a1', tenantId: 'TENANT-A', widgetId: 'widget-b1', title, url,
|
|
position: 0 }` (das Widget gehoert TENANT-B und ist unter TENANT-A
|
|
UNSICHTBAR — vorher mit gebundenem `widgetInstance.findUnique` messen und
|
|
ausgeben). Belegausgabe nennt, ob der INSERT gelang oder mit welchem
|
|
Konstruktor/`code` er scheiterte. Bestanden ist die Pruefung in BEIDEN
|
|
Faellen — sie ist eine Messung, deren Ergebnis Aufgabe 2 steuert (Befund
|
|
F): gelingt der INSERT, umgeht der Fremdschluessel den Zeilenschutz und
|
|
der Besitzriegel wird gebaut; die Zeile danach ueber die Wartungsrolle
|
|
wieder entfernen. Dazu: die zweite Haelfte derselben Pruefung — gebundenes
|
|
`create` unter TENANT-A mit `widgetId: 'widget-gibt-es-nicht'` — muss
|
|
scheitern (FK-Verletzung, Konstruktorname/`code` woertlich): der
|
|
Unterschied zwischen beiden Antworten ist das Existenzorakel (T-GWH-05).
|
|
8. `favoritelink-gebundenes-anlegen-eigener-mandant-gelingt` — gebundenes
|
|
`create` unter TENANT-A mit `widgetId: 'widget-a1'` gelingt, die
|
|
Wartungsrolle liest die Zeile mit `tenantId === 'TENANT-A'`, `createdAt`
|
|
und `updatedAt` gesetzt.
|
|
|
|
`runSettingsAreaChecks` — die Wegwerf-Tabelle `"SmtpConfig"` bekommt den
|
|
Eindeutigkeitsindex `CREATE UNIQUE INDEX "SmtpConfig_tenantId_key" ON
|
|
"SmtpConfig"("tenantId")` wortgleich aus `20260629130000` (ohne ihn misst
|
|
Pruefung 8 nichts). Zeilen: `smtp-a` (TENANT-A, host `smtp-a.example.invalid`,
|
|
`encryptedPassword` `enc(a-passwort-platzhalter)`, `fromAddress`
|
|
`a@example.invalid`), `smtp-b` (TENANT-B, entsprechend). Mindestens acht
|
|
Pruefungen:
|
|
|
|
1. `smtpconfig-regelstand-eindeutig`.
|
|
2. `smtpconfig-wegwerftabelle-deckt-alle-spalten-des-generierten-clients` —
|
|
zehn skalare Felder; zusaetzlich `pg_indexes` liest den Index
|
|
`SmtpConfig_tenantId_key` (Belegausgabe nennt `indexdef`).
|
|
3. `smtpconfig-startpfad-generierter-client-ungebunden-liefert-null` —
|
|
`prisma.smtpConfig.findFirst()` OHNE Bedingung, ungebunden: `null`,
|
|
obwohl zwei Zeilen existieren. Die Belegausgabe sagt beim Namen: das ist
|
|
der Wert, mit dem `mail.module.ts` nach dem Scharfschalten auf
|
|
Umgebungsvariablen und zuletzt `localhost:1025` zurueckfaellt — ein
|
|
falscher Transport statt einer Meldung.
|
|
4. `smtpconfig-startpfad-ueber-wartungsrolle-zieht-beliebige-zeile` — dieselbe
|
|
Abfrage ueber die Wartungsrolle (`BYPASSRLS`, die HEUTIGE Lage): genau
|
|
EINE Zeile, deren `tenantId` die Belegausgabe nennt, mit dem Satz, dass
|
|
nichts in der Abfrage bestimmt, WELCHER Mandant gezogen wird, und dass
|
|
dessen Server und Absender ab Start alle Kennwort-Zuruecksetzungs-Mails
|
|
ALLER Mandanten tragen (T-GWH-03).
|
|
5. `smtpconfig-versandpfad-generierter-client-ungebunden-liefert-null` —
|
|
`findUnique({ where: { tenantId: 'TENANT-A' } })` ungebunden: `null`,
|
|
waehrend die Wartungsrolle die Zeile liest. Die Belegausgabe sagt: das ist
|
|
Befund K — `tender-mail.service.ts` protokolliert `No SMTP configuration`
|
|
und ueberspringt, `dkv-mail.service.ts` wirft; kein Versand fuer niemanden.
|
|
6. `smtpconfig-versandpfad-generierter-client-gebunden-eigener-mandant-liefert-zugangsdaten`
|
|
— gebunden unter TENANT-A: Zeile mit `encryptedPassword ===
|
|
'enc(a-passwort-platzhalter)'`, `host`, `fromAddress` wie eingefuegt.
|
|
7. `smtpconfig-versandpfad-generierter-client-gebunden-fremder-mandant-liefert-null`
|
|
— `findUnique({ where: { tenantId: 'TENANT-A' } })` gebunden unter
|
|
TENANT-B: `null` — die verschluesselten Zugangsdaten von A sind fuer B
|
|
unsichtbar (T-GWH-01).
|
|
8. `smtpconfig-ungebundenes-upsert-auf-unsichtbare-zeile-scheitert-laut` —
|
|
die Form von `saveSmtpConfig`: `upsert({ where: { tenantId: 'TENANT-A' },
|
|
create: { id: 'smtp-a-neu', tenantId: 'TENANT-A', host:
|
|
'smtp-a-neu.example.invalid', fromAddress: 'a@example.invalid' }, update:
|
|
{ host: 'smtp-a-neu.example.invalid' } })` UNGEBUNDEN: bestanden genau
|
|
dann, wenn ein Fehler geworfen wird UND die Wartungsrolle danach weiterhin
|
|
`smtp-a.example.invalid` liest. Konstruktorname und `code` woertlich; die
|
|
Belegausgabe haelt daneben, was 260910-krx fuer `DashboardLayout` gemessen
|
|
hat (`PrismaClientUnknownRequestError`), und sagt, ob es hier gleich oder
|
|
anders ausfaellt. Das ist die Kette "leeres Formular -> Administrator
|
|
traegt neu ein -> Speichern scheitert".
|
|
9. `smtpconfig-gebundenes-upsert-eigener-mandant-aktualisiert` — dasselbe
|
|
`upsert` gebunden unter TENANT-A: gelingt, die Wartungsrolle liest den
|
|
neuen Host fuer `smtp-a`, `smtp-b` unveraendert, `updatedAt` gesetzt.
|
|
|
|
Ergaenzt wird nur, gestrichen wird nicht; nenne im SUMMARY die tatsaechlich
|
|
gezaehlte Zahl je Abschnitt.
|
|
|
|
TEIL 2 — die Codeaussagen, jede mit ihrer Reichweite. Fuehre die
|
|
Nachpruefungen aus den Befunden A, D, E, F, G, H, I, K und L tatsaechlich aus
|
|
und notiere jeweils Anweisung oder Datei-und-Zeile: (A) die elf Rohtreffer;
|
|
(D) die Aufrufer des Startpfads in `apps/api/src` (genau einer), die
|
|
Rueckfallkette in `mail.module.ts` mit ihren vier Prioritaeten, der
|
|
`try/catch` in `mail.service.ts`, die Aufrufer von `MailService` ausserhalb
|
|
des Mailmoduls; (E) die zwei Aufrufer von `getDecryptedSmtpConfig` ausserhalb
|
|
von `settings` und ihr Verhalten bei `null`, plus `testSmtpConfig`; (F) die
|
|
drei Besitzpruefungen und der fehlende Widget-Riegel in `create`; (G) der
|
|
Icon-Proxy ohne Datenbankzugriff, `getIconBytes` nimmt nur die Kennung; (H)
|
|
`extractContext` in beiden Controllern nebeneinander, die `x-tenant-id`-
|
|
Sender im Frontend; (I) die beiden Frontend-Ketten Glied fuer Glied; (K) die
|
|
eine Anweisung; (L) die Zeilen der Anleitung. Faellt eine Nachpruefung ANDERS
|
|
aus als in den Planungsbefunden, gilt die Messung — schreibe sie auf und
|
|
benenne die Abweichung ausdruecklich.
|
|
|
|
TEIL 3 — die Kritikschrift. Erweitere
|
|
`docs/mandantentrennung-etappe2-fehlerrichtung.md` um ZWEI Abschnitte
|
|
unmittelbar VOR `## Verweis`, in der Form der vorhandenen Bereichsabschnitte:
|
|
`## Bereich favorites` mit (f1)-(f5) und danach `## Bereich settings` mit
|
|
(s1)-(s5) (Buchstaben `f` und `s` sind frei — gemessen mit `grep -n "^### ("`).
|
|
|
|
`## Bereich favorites`:
|
|
- `### (f1) Die Messung` — die tatsaechlich beobachtete Werkzeugausgabe
|
|
woertlich eingerueckt, die tragende Belegzeile (Pruefung 3) benannt, der
|
|
Fremdschluessel als mitgebaute Relation (Befund C, WINDOWS #27), das
|
|
Ergebnis von Pruefung 7 mit dem Satz, was daraus fuer Aufgabe 2 folgt.
|
|
- `### (f2) Signaltabelle je umzustellendem Pfad` — je Methode eine Zeile
|
|
(`list`, `create`, `update`, `remove`, `getIconBytes`): Verhalten nach dem
|
|
Scharfschalten ohne diesen Plan, konkretes Signal mit Statuscode und
|
|
woertlicher Meldung, ob das Frontend es durchlaesst (Datei und Zeile).
|
|
- `### (f3) Welcher Code Leere als Abwesenheit deutet` — die Kette aus
|
|
Befund I (1) mit allen Gliedern, dem woertlichen Text `Noch keine
|
|
Favoriten.`, und dem Satz, dass "nie einen gespeichert" und "Zeile
|
|
unsichtbar" derselbe Wert sind (Familie #23/#25/#26/#28).
|
|
- `### (f4) Was dieser Durchlauf bewusst nicht löst` — (a) die fehlende
|
|
Benutzerdimension der Regel (Kollege desselben Mandanten auf
|
|
Datenbankebene sichtbar; Etappe-3-Entscheidung (2)); (b) das Frontend
|
|
(nicht angefasst; Ledger-Eintrag in Aufgabe 3); (c) die Mandantenquelle —
|
|
warum der dashboard-Praezedenzfall und nicht der auth-Praezedenzfall
|
|
(Befund H); (d) die Etappe-4-Vorabpruefung aus Befund I.
|
|
- `### (f5) Was dieser Durchlauf bewusst nicht anfasst` — der Icon-Proxy
|
|
(Befund G, mit der Messung, dass er die Datenbank nicht erreicht und keine
|
|
Client-URL nimmt), die DTOs, `dashboard.controller.ts` (nur gelesen), das
|
|
Frontend, Schema und Migrationen.
|
|
|
|
`## Bereich settings`:
|
|
- `### (s1) Die Messung` — Werkzeugausgabe woertlich, die tragenden
|
|
Belegzeilen (Pruefung 3 fuer den Startpfad, 5 fuer Befund K, 8 fuer den
|
|
Speicherkonflikt) benannt, der Eindeutigkeitsindex als Voraussetzung der
|
|
Konfliktmessung, das Ergebnis von Pruefung 8 neben der dashboard-Messung.
|
|
- `### (s2) Signaltabelle je Pfad` — `getSmtpConfig`, `saveSmtpConfig`,
|
|
`getDecryptedSmtpConfig` (getrennt fuer den tenders- und den dkv-Aufrufer),
|
|
`testSmtpConfig`, und der Startpfad (mit seiner Rueckfallkette als eigener
|
|
Spalteninhalt): heute, nach dem Scharfschalten, Signal, Frontend.
|
|
- `### (s3) Welcher Code Leere als Abwesenheit deutet` — die Kette aus
|
|
Befund I (2) mit allen Gliedern (`settings.controller.ts` gibt `null`,
|
|
Adapter sendet leeren Rumpf, `settings-api.ts` `res.json()` wirft,
|
|
`smtp-settings-form.tsx` verschluckt), der Satz, dass das Formular
|
|
"nicht eingerichtet" zeigt, waehrend die Zugangsdaten physisch da sind,
|
|
und dass ein erneutes Speichern unter der ungebundenen Form am Index
|
|
scheitert (Pruefung 8); die Versandpfade mit ihren zwei verschiedenen
|
|
Reaktionen auf `null` (warn/skip vs. throw).
|
|
- `### (s4) Was dieser Durchlauf bewusst nicht löst` — (a) DER STARTPFAD in
|
|
einem eigenen Absatz, in der Form von (d4): beide Zustaende (heute:
|
|
beliebiger Mandant traegt alle Systemmails, T-GWH-03; nach dem
|
|
Scharfschalten: `null`, Rueckfallkette, `try/catch` — doppelt verdeckt),
|
|
die drei gepruefte Formen (binden: unmoeglich, kein Kontext beim Start;
|
|
Mehrmandanten-Versand: abgelehnt als Funktion, mit dem Satz, dass die
|
|
Vorlage — Transport je Versand aus `getDecryptedSmtpConfig(tenantId)` —
|
|
bereits in `DkvMailService`/`TenderMailService` steht und `MailService`
|
|
den Mandanten von `requestPasswordReset` bekommen koennte; Altlast mit
|
|
Markierung: GEWAEHLT), die UNSYMMETRIE zu `ldap` UND `dkv` (Befund D:
|
|
Rueckfallkette statt Leere, keine Protokollzeile), und die ENTSCHEIDUNG
|
|
eigener Ledger-Eintrag statt Anschluss an #21 mit Grund (andere Datei,
|
|
andere Reparatur, andere Verdeckungsform) — oder, falls die Lesung zur
|
|
Ausfuehrungszeit anders ausfaellt, die andere Entscheidung mit Grund;
|
|
(b) BEFUND K IST ERFUELLT — ein eigener Absatz, der sagt, dass die
|
|
Reihenfolgebedingung aus (t4) und (d4) mit Aufgabe 2 dieses Laufs erfuellt
|
|
ist, dass beide Stellen einen Nachtrag bekommen (Aufgabe 3), und dass die
|
|
Etappe-4-Vorabpruefung diese Bedingung NICHT mehr fuehren muss; (c) das
|
|
Frontend (Ledger-Eintrag in Aufgabe 3, Familie #28 — dieselbe
|
|
200-leerer-Rumpf-Kette); (d) die Mandantenquelle `req.tenantId` im
|
|
Controller als RICHTIG fuer eine ADMIN-Seite (Befund H, D-10) — bewusst
|
|
nicht auf das Claim umgestellt, mit dem Unterschied zu `auth`; (e) die
|
|
Etappe-4-Vorabpruefung.
|
|
- `### (s5) Was dieser Durchlauf bewusst nicht anfasst` —
|
|
`settings.controller.ts`, das DTO, `tender-mail.service.ts`,
|
|
`dkv-mail.service.ts`, `mail.service.ts` (nur gelesen; `mail.module.ts`
|
|
wird in Aufgabe 2 fuer die Umbenennung angefasst — hier angekuendigt),
|
|
das Frontend, Schema und Migrationen, `nodemailer`.
|
|
|
|
Aendere in dieser Aufgabe KEINE Datei unter `apps/api/src`, KEINE unter
|
|
`apps/api/prisma` und KEINE unter `apps/web`.
|
|
</action>
|
|
<verify>
|
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in favoritelink-regelstand-eindeutig favoritelink-wegwerftabelle-deckt-alle-spalten-des-generierten-clients favoritelink-liste-generierter-client-ungebunden-liefert-leere-liste favoritelink-liste-generierter-client-gebunden-eigener-mandant-liefert-eigene-zeilen favoritelink-besitzpruefung-generierter-client-gebunden-fremder-mandant-liefert-null favoritelink-gebundenes-loeschen-ueber-kennung-allein-fremder-mandant-scheitert-laut favoritelink-fremdschluessel-prueft-am-zeilenschutz-vorbei favoritelink-gebundenes-anlegen-eigener-mandant-gelingt smtpconfig-regelstand-eindeutig smtpconfig-wegwerftabelle-deckt-alle-spalten-des-generierten-clients smtpconfig-startpfad-generierter-client-ungebunden-liefert-null smtpconfig-startpfad-ueber-wartungsrolle-zieht-beliebige-zeile smtpconfig-versandpfad-generierter-client-ungebunden-liefert-null smtpconfig-versandpfad-generierter-client-gebunden-eigener-mandant-liefert-zugangsdaten smtpconfig-versandpfad-generierter-client-gebunden-fremder-mandant-liefert-null smtpconfig-ungebundenes-upsert-auf-unsichtbare-zeile-scheitert-laut smtpconfig-gebundenes-upsert-eigener-mandant-aktualisiert; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && N=$(echo "$OUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden\.$/\1/p') && { test -n "$N" && test "$N" -ge 137 || { echo "PRUEFUNGSZAHL: ${N:-nicht alle bestanden}, erwartet mindestens 137 (120 bisherige plus mindestens 8 + 9 neue)"; exit 1; }; } && S=apps/api/scripts/rls-scratch-check.mjs && grep -q 'async function runFavoritesAreaChecks' "$S" && grep -q 'async function runSettingsAreaChecks' "$S" && grep -q "readSchemaModelScalarFieldNames('FavoriteLink')" "$S" && grep -q "readSchemaModelScalarFieldNames('SmtpConfig')" "$S" && grep -q 'REFERENCES "WidgetInstance"' "$S" && grep -q 'SmtpConfig_tenantId_key' "$S" && grep -q "extractPolicySql(remainingMigrationSql, 'FavoriteLink')\|extractPolicySql(.*'FavoriteLink')" "$S" && grep -q "extractPolicySql(.*'SmtpConfig')" "$S" && awk '/await runAuthAreaChecks\(/{a=NR} /await runFavoritesAreaChecks\(/{f=NR} /await runSettingsAreaChecks\(/{s=NR} /await runTransactionShapeMeasurement\(/{t=NR} END{ if(!(a&&f&&s&&t&&a<f&&f<s&&s<t)){print "REIHENFOLGE in main(): runFavoritesAreaChecks und runSettingsAreaChecks muessen nach runAuthAreaChecks und vor runTransactionShapeMeasurement stehen"; exit 1} }' "$S" && D=docs/mandantentrennung-etappe2-fehlerrichtung.md && grep -q '^## Bereich favorites$' "$D" && grep -q '^## Bereich settings$' "$D" && for X in f1 f2 f3 f4 f5 s1 s2 s3 s4 s5; do grep -qE "^### \($X\) " "$D" || { echo "FEHLENDER UNTERABSCHNITT: ($X)"; exit 1; }; done && awk '/^## Bereich favorites$/{f=1; next} /^## /{f=0} f && /Noch keine Favoriten\./{e=1} f && /favorites-widget\.tsx/{w=1} f && /favorites-api\.ts/{a=1} f && /icon-discovery\.service\.ts/{i=1} f && /WidgetInstance/{r=1} f && /dashboard\.controller\.ts/{c=1} END{ if(!e||!w||!a){print "(f3): die Frontend-Kette (favorites-widget.tsx, favorites-api.ts, Noch keine Favoriten.) ist nicht benannt"; exit 1} if(!i){print "(f5): der Icon-Proxy ist nicht benannt"; exit 1} if(!r){print "(f1): der Fremdschluessel auf WidgetInstance ist nicht benannt"; exit 1} if(!c){print "(f4): die Mandantenquelle / der dashboard-Praezedenzfall ist nicht benannt"; exit 1} }' "$D" && awk '/^## Bereich settings$/{f=1; next} /^## /{f=0} f && /mail\.module\.ts/{m=1} f && /smtp-settings-form\.tsx/{w=1} f && /settings-api\.ts/{a=1} f && /tender-mail\.service\.ts/{t=1} f && /dkv-mail\.service\.ts/{d=1} f && /Befund K/{k=1} f && /#21/{l=1} f && /SmtpConfig_tenantId_key/{u=1} f && /localhost:1025/{p=1} f && /req\.tenantId/{q=1} END{ if(!m||!p){print "(s4): der Startpfad / die Rueckfallkette (mail.module.ts, localhost:1025) ist nicht benannt"; exit 1} if(!w||!a){print "(s3): die Frontend-Kette (smtp-settings-form.tsx, settings-api.ts) ist nicht benannt"; exit 1} if(!t||!d||!k){print "(s2)/(s4): Befund K mit beiden Versandpfaden ist nicht benannt"; exit 1} if(!l){print "(s4): die Entscheidung zu WINDOWS #21 ist nicht benannt"; exit 1} if(!u){print "(s1): der Eindeutigkeitsindex ist nicht benannt"; exit 1} if(!q){print "(s4): die Mandantenquelle des Controllers ist nicht benannt"; exit 1} }' "$D" && awk '/^## Bereich settings$/{f=1} /^## Verweis$/{ if(f){ok=1} f=0 } END{ if(!ok){print "ABSCHNITT ## Bereich settings steht nicht unmittelbar vor ## Verweis"; exit 1} }' "$D" && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -ge 951 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mindestens 951"; exit 1; }; } && npm --prefix apps/api run type-check && git rev-parse --verify 46f0e78 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 46f0e78 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 46f0e78) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }</automated>
|
|
</verify>
|
|
<done>`apps/api/scripts/rls-scratch-check.mjs` hat die Abschnitte `runFavoritesAreaChecks` und `runSettingsAreaChecks` in der richtigen Reihenfolge, mit mindestens acht bzw. neun namentlich benannten Pruefungen ueber den generierten Client an Wegwerf-Tabellen, deren Spaltenmenge zur Laufzeit gegen die skalaren Felder des Schemas geprueft wird; die `FavoriteLink`-Tabelle traegt den Fremdschluessel auf `WidgetInstance`, die `SmtpConfig`-Tabelle den Eindeutigkeitsindex; der Startpfad ist als `findFirst()` ohne Bedingung in BEIDEN Zustaenden gemessen (Wartungsrolle: beliebige Zeile; Wegwerf-Rolle: `null`), der Versandpfad als Befund K, der Speicherkonflikt mit Konstruktorname und `code` woertlich, der Fremdschluessel-Durchgriff mit Ergebnis; alle Pruefungen bestehen (mindestens 137). `docs/mandantentrennung-etappe2-fehlerrichtung.md` hat `## Bereich favorites` ((f1)-(f5)) und `## Bereich settings` ((s1)-(s5)) unmittelbar vor `## Verweis`, mit woertlicher Werkzeugausgabe, Signaltabellen, beiden Frontend-Ketten, dem Startpfad-Absatz mit beiden Zustaenden, Unsymmetrie und Ledger-Entscheidung, und dem Absatz, dass Befund K erfuellt ist. Baseline gehalten (mindestens 951 Tests, Typpruefung sauber). Unter `apps/api/src`, `apps/api/prisma`, `apps/web` und den Compose-/Umgebungsdateien ist nichts geaendert.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 2: Beide Dienste binden (ein Klient je Methode), den Startpfad umbenennen und markieren, den Widget-Besitzriegel einbauen, zwei neue Testdateien mit dem Zwei-Klienten-Nachbau</name>
|
|
<files>apps/api/src/favorites/favorites.service.ts, apps/api/src/favorites/favorites.service.spec.ts, apps/api/src/favorites/favorites.controller.ts, apps/api/src/settings/settings.service.ts, apps/api/src/settings/settings.service.spec.ts, apps/api/src/mail/mail.module.ts</files>
|
|
<read_first>apps/api/src/favorites/favorites.service.ts (VOLLSTAENDIG, erneut), apps/api/src/favorites/favorites.controller.ts (VOLLSTAENDIG, erneut), apps/api/src/settings/settings.service.ts (VOLLSTAENDIG, erneut), apps/api/src/mail/mail.module.ts (VOLLSTAENDIG, erneut), apps/api/src/calendar/calendar.service.spec.ts (VOLLSTAENDIG — die Vorlage fuer einen Bereich ohne Testdatei), apps/api/src/dkv/dkv.service.spec.ts (`makeFakePrisma`, `__makeBoundClient`, der Kopfkommentar zur Falsifizierung), apps/api/src/dashboard/dashboard.service.spec.ts (Zeilen 545-570, Wachhund), apps/api/src/tenders/tender-mail.service.spec.ts (Zeilen 1-60, die erwartete Rueckgabeform von `getDecryptedSmtpConfig`), apps/api/src/dkv/dkv.service.ts (Kopfkommentar von `loadAnyActiveConfigForScheduler` — die Vorlage fuer den Startpfad-Kommentar), apps/api/src/dkv/dkv-scheduler.service.ts (Kopf, Zeilen 20-35 und 80-86), apps/api/src/prisma/rls-access-inventory.spec.ts (`analyzeFile` — die Zuweisungsform `const X = forTenant(`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitte `## Bereich favorites` und `## Bereich settings` aus Aufgabe 1, besonders (f1) Pruefung 7, (f4)(c), (s4)(a))</read_first>
|
|
<behavior>
|
|
ZUERST die Testlage, DANN der Umbau — je Bereich eine NEUE Testdatei in der
|
|
Form von `calendar.service.spec.ts`: `vi.mock` auf das Bindungshilfsmittel
|
|
wird auf `prisma.__makeBoundClient(tenantId)` umgeleitet; ein handgerollter
|
|
`makeFakePrisma()` mit In-Memory-Zeilen und Bindungsprotokoll
|
|
`boundCallLog` (`{ tenantId, model, method }`); der UNGEBUNDENE Nachbau hat
|
|
KEINES der Anfrage-Modelle (ein versehentlich ungebundener Modellzugriff
|
|
scheitert mit "Cannot read properties of undefined" — die dkv-Form der
|
|
Falsifizierung); jeder Fall eigenstaendig, jeder mit sprechendem Namen; die
|
|
Zahl der Faelle wird am Ende ABGEZAEHLT und im SUMMARY genannt.
|
|
|
|
`apps/api/src/favorites/favorites.service.spec.ts` — Zeilen fuer
|
|
`favoriteLink` (Map ueber die Kennung, alle zehn skalaren Felder) und
|
|
`widgetInstance` (Map ueber die Kennung: `id`, `userId`, `tenantId`,
|
|
`widgetType`); der gebundene Klient bietet `favoriteLink.findMany` (Filter
|
|
`where` ueber `userId`/`widgetId`, Sortierung nach `position`, dann `title`),
|
|
`findUnique`, `create`, `update` (wirft bei unsichtbarer Zeile einen Fehler
|
|
mit `code: 'P2025'`), `delete` (ebenso) und `widgetInstance.findUnique` (mit
|
|
`select`); alle liefern nur Zeilen, deren `tenantId` dem Klienten entspricht.
|
|
`IconDiscoveryService` als Attrappe: `discoverFavoriteIconUrl: vi.fn(async
|
|
(url) => 'https://icons.invalid/' + encodeURIComponent(url))`,
|
|
`fetchIconBytes: vi.fn(async () => ({ contentType: 'image/png', body:
|
|
Buffer.from('png') }))`; `normalizeUrl` bleibt die ECHTE Funktion (sie ist
|
|
rein). Faelle:
|
|
|
|
- `list(t1, user-a1, widget-a1)`: liefert nur die Zeilen von `user-a1` fuer
|
|
`widget-a1`, sortiert nach `position` aufsteigend, bei Gleichstand nach
|
|
`title`; die Zeile von `user-a2` und die von `widget-a2` fehlen.
|
|
- `list` unter FREMDEM Mandanten (`t2`, Zeilen unter `t1`): `[]` — kein
|
|
Fehler (das ist der Wert, aus dem das Widget `Noch keine Favoriten.`
|
|
macht).
|
|
- `list` ohne `widgetId`: `BadRequestException`, KEIN Klient erzeugt.
|
|
- `create(t1, user-a1, dto)` ohne `iconUrl`: URL normalisiert
|
|
(`ctl.de` -> `https://ctl.de`), Icon-Suche mit der NORMALISIERTEN URL
|
|
aufgerufen, gebundener `create` mit `userId`, `tenantId: 't1'`,
|
|
`widgetId`, `position: 0` als Vorgabe.
|
|
- `create` mit `iconUrl`: KEINE Icon-Suche, gespeicherter Wert wie
|
|
uebergeben.
|
|
- `create` mit `widgetId` eines Widgets, das einem ANDEREN Benutzer
|
|
desselben Mandanten gehoert: `NotFoundException` mit Meldung woertlich
|
|
`Widget not found`, KEIN `create`, KEINE Icon-Suche (T-GWH-05).
|
|
- `create` mit `widgetId` eines Widgets unter FREMDEM Mandanten:
|
|
`NotFoundException` `Widget not found` — die Meldung nennt weder Halter
|
|
noch Mandanten; KEIN `create`.
|
|
- `create` mit unbekannter `widgetId`: dieselbe `NotFoundException`.
|
|
- `update(t1, id, user-a1, dto)`, eigene Zeile: Titel/URL/Position
|
|
gemergt, gebundener `update`; `iconUrl` explizit `null` im DTO -> Icon-Suche
|
|
gegen die EFFEKTIVE URL (neue, sonst gespeicherte); `iconUrl` gesetzt ->
|
|
keine Icon-Suche.
|
|
- `update`, Zeile eines ANDEREN Benutzers desselben Mandanten:
|
|
`NotFoundException` `FavoriteLink not found`, KEIN Schreibzugriff.
|
|
- `update`, Zeile unter FREMDEM Mandanten (Klient `t2`, Zeile `t1`):
|
|
`NotFoundException`, KEIN Schreibzugriff — der gebundene `findUnique`
|
|
liefert `null`, bevor irgendetwas geschrieben wird.
|
|
- `remove`: eigene Zeile geloescht (Map leer fuer diese Kennung); fremder
|
|
Benutzer -> `NotFoundException`, Zeile bleibt; fremder Mandant ->
|
|
`NotFoundException`, Zeile bleibt.
|
|
- `getIconBytes`: eigene Zeile mit `iconUrl` -> `fetchIconBytes` GENAU mit
|
|
der GESPEICHERTEN URL aufgerufen, Rueckgabe durchgereicht; Zeile ohne
|
|
`iconUrl` -> `NotFoundException`, `fetchIconBytes` NICHT aufgerufen;
|
|
fremder Mandant -> `NotFoundException`; `fetchIconBytes` wirft ->
|
|
`HttpException` mit Status 502.
|
|
- Wachhund je Methode: genau EIN gebundener Klient je Aufruf
|
|
(`vi.mocked(forTenant).mock.calls.length`), Suche, Widget-Pruefung und
|
|
Schreibzugriff auf DEMSELBEN — das Protokoll zeigt fuer `create` zwei
|
|
Eintraege (`widgetInstance.findUnique`, `favoriteLink.create`) mit
|
|
derselben Mandantenkennung.
|
|
|
|
`apps/api/src/settings/settings.service.spec.ts` — Zeilen fuer `smtpConfig`
|
|
(Map ueber `tenantId`, alle zehn skalaren Felder). GRENZE als Bauform: der
|
|
UNGEBUNDENE Nachbau bietet fuer `smtpConfig` AUSSCHLIESSLICH `findFirst`
|
|
(der Startpfad) — kein `findUnique`, kein `upsert`; der GEBUNDENE Klient
|
|
bietet AUSSCHLIESSLICH `findUnique` (mit `select`) und `upsert` (Merge oder
|
|
Anlage, `select` angewandt) — KEIN `findFirst`. Damit scheitert ein
|
|
gebundener Startpfad ebenso hart wie ein ungebundener Anfrageweg.
|
|
`CryptoService` als Attrappe (`encrypt: vi.fn((s) => 'enc(' + s + ')')`,
|
|
`decrypt: vi.fn((s) => s.slice(4, -1))`); `nodemailer` per
|
|
`vi.mock('nodemailer', ...)` ersetzt: `createTransport` liefert
|
|
`{ verify: vi.fn(async () => true), sendMail: vi.fn(async () => ({})) }`,
|
|
konfigurierbar auf Wurf. Faelle:
|
|
|
|
- `getSmtpConfig(t1)`: Zeile mit `encryptedPassword` (der Controller macht
|
|
daraus `hasPassword`), OHNE weitere Felder ausserhalb des `select`;
|
|
fremder Mandant (`t2`): `null`.
|
|
- `saveSmtpConfig(t1, dto)` mit Kennwort: `encrypt` GENAU mit dem Klartext
|
|
aufgerufen; gebundener `upsert` mit `where.tenantId === 't1'`, `create`
|
|
und `update` tragen `encryptedPassword: 'enc(...)'`; Rueckgabe OHNE
|
|
`encryptedPassword` (SMTP_SAFE_SELECT); der Klartext taucht in KEINEM
|
|
Protokolleintrag auf.
|
|
- `saveSmtpConfig` OHNE Kennwort (leer oder fehlend): `encrypt` NICHT
|
|
aufgerufen, `update` traegt KEINEN Schluessel `encryptedPassword`
|
|
(bestehendes bleibt), `username` fehlend -> `null`.
|
|
- `saveSmtpConfig` unter `t2`, wenn nur `t1` eine Zeile hat: der gebundene
|
|
Nachbau legt fuer `t2` an (die Zeile von `t1` bleibt unveraendert) — die
|
|
richtige Semantik NACH der Bindung.
|
|
- `getDecryptedSmtpConfig(t1)`: `decrypt` mit `encryptedPassword`
|
|
aufgerufen, Rueckgabe mit `host`, `port`, `encryption`, `username`,
|
|
`fromAddress`, `decryptedPassword` (genau die Form, die
|
|
`tender-mail.service.spec.ts` als `BASE_SMTP_CONFIG` erwartet); ohne
|
|
`encryptedPassword` -> `decryptedPassword: null`, `decrypt` NICHT
|
|
aufgerufen; fremder Mandant -> `null`, `decrypt` NICHT aufgerufen
|
|
(T-GWH-01).
|
|
- `testSmtpConfig(t1, dto)` ohne Kennwort/Benutzername im DTO: greift ueber
|
|
den GEBUNDENEN Klienten auf die gespeicherten Werte zurueck
|
|
(`createTransport` mit `auth.user` aus der Zeile, `auth.pass` entschluesselt),
|
|
`verify` aufgerufen, `{ success: true }`; mit `testTo`: `sendMail` statt
|
|
`verify`, `from === dto.fromAddress`, `to === dto.testTo`; `verify` wirft ->
|
|
`{ success: false }`, kein Wurf nach aussen; fremder Mandant ohne
|
|
Kennwort im DTO: `auth` ohne Benutzer (`undefined`), kein Fehler.
|
|
- Startpfad `loadAnySmtpConfigForStartupTransport()`: laeuft ueber den
|
|
UNGEBUNDENEN Nachbau (`findFirst`), liefert `{ host, port, secure,
|
|
requireTLS, username, password, fromAddress }` mit `secure` fuer
|
|
`ssl-tls` und `requireTLS` fuer `starttls`, `password` entschluesselt;
|
|
leerer Nachbau -> `null`; und der NULL-KLIENTEN-NACHWEIS:
|
|
`vi.mocked(forTenant).mock.calls.length === 0` nach dem Aufruf — der
|
|
Startpfad erzeugt KEINEN gebundenen Klienten, gemessen, nicht behauptet.
|
|
- Wachhund je Anfrageweg: genau EIN gebundener Klient je Aufruf von
|
|
`getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig`;
|
|
`testSmtpConfig` erzeugt genau einen (ueber `getDecryptedSmtpConfig`),
|
|
wenn es zurueckgreift, und KEINEN, wenn Kennwort und Benutzername im DTO
|
|
stehen.
|
|
</behavior>
|
|
<action>
|
|
TEIL 1 — `favorites.service.ts`. Stelle alle fuenf Methoden um, je Methode
|
|
EIN Klient in der Zuweisungsform, die `rls-access-inventory.spec.ts` erkennt:
|
|
Konstante `tenantPrisma` aus dem Bindungshilfsmittel mit `this.prisma` und
|
|
dem uebergebenen Mandanten, `as any`. Der Mandant wird ERSTER Parameter
|
|
jeder Methode (Konvention der Vorgaenger): `list(tenantId, userId,
|
|
widgetId)`, `create(tenantId, userId, dto)` (heute `create(userId,
|
|
tenantId, dto)` — Reihenfolge tauschen, den Aufrufer mit), `update(tenantId,
|
|
id, userId, dto)`, `remove(tenantId, id, userId)`, `getIconBytes(tenantId,
|
|
id, userId)`. Die Besitzpruefungen (`findUnique`, Vergleich `link.userId !==
|
|
userId`, dann `update`/`delete` ueber die Kennung) bleiben WORTGLEICH und
|
|
laufen auf DEMSELBEN Klienten wie der Schreibzugriff. Meldungen, Sortierung,
|
|
Normalisierung, Icon-Suche, 502-Uebersetzung bleiben WORTGLEICH.
|
|
|
|
In `create`, VOR der Icon-Suche, der neue Besitzriegel (T-GWH-05, nur wenn
|
|
Pruefung 7 aus Aufgabe 1 den Durchgriff des Fremdschluessels bestaetigt hat —
|
|
sonst entfaellt er, mit Belegzeile im SUMMARY): `tenantPrisma.widgetInstance
|
|
.findUnique({ where: { id: dto.widgetId }, select: { userId: true } })`; ist
|
|
das Ergebnis `null` oder `widget.userId !== userId`, `NotFoundException`
|
|
mit Meldung woertlich `Widget not found` — keine Aussage ueber fremde
|
|
Mandanten oder fremde Halter, dieselbe Antwort fuer "gibt es nicht", "gehoert
|
|
einem Kollegen" und "liegt bei einem fremden Mandanten" (das schliesst das
|
|
Existenzorakel). Der Riegel steht VOR der Icon-Suche, damit ein fremdes
|
|
Widget keinen Netzabruf ausloest.
|
|
|
|
Kopfkommentar der Klasse: warum gebunden (260911-gwh), dass der Mandant aus
|
|
`extractContext` des Controllers kommt (Befund H: dieselbe Quelle wie
|
|
`dashboard`, weil der Link am Widget haengt — ausdruecklich NICHT der
|
|
auth-Praezedenzfall, mit Grund), dass die Regel keine Benutzerdimension hat
|
|
und die `userId`-Filter deshalb bleiben (Etappe-3-Entscheidung (2)), und
|
|
dass der Fremdschluessel am Zeilenschutz vorbei prueft (Pruefung 7, mit
|
|
Ergebnis) — deshalb der Riegel. Kommentare in dieser Datei duerfen den
|
|
ungebundenen Zugriff auf das Favoritenmodell NICHT woertlich nennen (das
|
|
Gate zaehlt ueber die ganze Datei).
|
|
|
|
TEIL 2 — `favorites.controller.ts`. `extractContext` bleibt WORTGLEICH;
|
|
alle fuenf Handler reichen `tenantId` als ersten Parameter durch —
|
|
`list(tenantId, userId, widgetId)`, `create(tenantId, userId, dto)`,
|
|
`getIconBytes(tenantId, id, userId)`, `update(tenantId, id, userId, dto)`,
|
|
`remove(tenantId, id, userId)`. Kopfkommentar der Klasse ergaenzen: die
|
|
Mandantenquelle ist die vom Guard gesetzte Anfragekennung (fuer SUPER_ADMIN
|
|
per Kopfzeile umschaltbar), WORTGLEICH mit `dashboard.controller.ts`, weil
|
|
`FavoriteLink` ueber `widgetId` an `WidgetInstance` haengt und beide unter
|
|
derselben Kennung liegen muessen; das Favoriten-Frontend sendet die Kopfzeile
|
|
heute nicht (gemessen), die Entscheidung haengt an der Bauform.
|
|
|
|
TEIL 3 — `settings.service.ts`. `getSmtpConfig`, `saveSmtpConfig`,
|
|
`getDecryptedSmtpConfig` je mit EINEM Klienten `tenantPrisma`
|
|
(Zuweisungsform); `select`, `SMTP_SAFE_SELECT`, Verschluesselung,
|
|
Rueckgabeformen WORTGLEICH. `testSmtpConfig` hat keinen eigenen
|
|
Datenbankzugriff und bleibt bis auf Kommentare unveraendert.
|
|
|
|
Der Startpfad: benenne `getStartupSmtpConfig()` um in
|
|
`loadAnySmtpConfigForStartupTransport()` (dkv-Praezedenzfall: ein Name, den
|
|
niemand fuer einen Anfrageweg haelt), lasse ihn auf `this.prisma` mit dem
|
|
unveraenderten `findFirst()` ohne Bedingung, und schreibe den Kopfkommentar
|
|
nach der Vorlage von `loadAnyActiveConfigForScheduler` NEU: BLEIBT bewusst
|
|
UNGEBUNDEN (260911-gwh, sechster Fall der Hintergrunddienst-Falle); HEUTE
|
|
bereits falsch — bei mehreren Mandanten traegt der SMTP-Server und Absender
|
|
eines BELIEBIGEN Mandanten alle Kennwort-Zuruecksetzungs- und
|
|
Willkommensmails (T-GWH-03); NACH DEM SCHARFSCHALTEN `null`, und
|
|
`mail.module.ts` faellt auf Umgebungsvariablen und zuletzt `localhost:1025`
|
|
zurueck, `MailService` faengt den Transportfehler — das Verstummen erzeugt
|
|
KEINE Protokollzeile (die Unsymmetrie zu `ldap` UND `dkv`); binden wuerde den
|
|
Pfad garantiert leer laufen lassen (kein Kontext beim Start); der Umbau auf
|
|
Transport je Versand aus `getDecryptedSmtpConfig(tenantId)` — die Form, die
|
|
`DkvMailService`/`TenderMailService` bereits haben, `MailService` muesste
|
|
den Mandanten von `requestPasswordReset` entgegennehmen — ist eine
|
|
Funktionsaenderung, NICHT dieser Auftrag; Verweis auf den Ledger-Eintrag
|
|
(die Nummer vergibt erst Aufgabe 3 — hier zunaechst woertlich der
|
|
Platzhalter `WINDOWS #TBD-GWH`, den Aufgabe 3 in DIESER Datei und in
|
|
`mail.module.ts` durch die vergebene Nummer ersetzt; das Gate von Aufgabe 3
|
|
zaehlt den Platzhalter auf null) und auf (s4). Kommentare in dieser Datei duerfen den ungebundenen Zugriff auf das
|
|
SMTP-Modell nicht woertlich nennen (das Gate zaehlt genau EINE Stelle in der
|
|
Datei — den Startpfad im Code).
|
|
|
|
TEIL 4 — `mail.module.ts`. Aufruf auf den neuen Namen umstellen; den
|
|
Modulkommentar und den Zeilenkommentar an der Aufrufstelle so umschreiben,
|
|
dass beide Zustaende stehen (der Satz "single-tenant default" verschwindet
|
|
und wird durch die beiden Zustaende und den Verweis auf den Ledger-Eintrag
|
|
ersetzt). KEINE Aenderung an der Rueckfallkette selbst, KEIN Umbau des
|
|
Transports.
|
|
|
|
TEIL 5 — die beiden Testdateien aus `<behavior>`. Danach VIER
|
|
Falsifizierungsnachweise, jeder zurueckgenommen und mit Testname und
|
|
Fehlermeldung woertlich notiert: (a) ersetze in `list` probeweise den
|
|
gebundenen Klienten durch den ungebundenen Basisclient —
|
|
`favorites.service.spec.ts` wird rot (erwartete Form: der Nachbau hat kein
|
|
ungebundenes Favoritenmodell); (b) entferne probeweise den Widget-Riegel in
|
|
`create` — genau die drei `Widget not found`-Faelle werden rot; (c) ersetze
|
|
in `getDecryptedSmtpConfig` probeweise den gebundenen Klienten durch den
|
|
ungebundenen — `settings.service.spec.ts` wird rot (kein `findUnique` auf
|
|
dem ungebundenen Nachbau); (d) binde probeweise den Startpfad — der
|
|
Null-Klienten-Nachweis UND der Startpfad-Fall werden rot (der gebundene
|
|
Nachbau hat kein `findFirst`). Zaehle jeweils, wie viele Faelle rot wurden,
|
|
und nenne die Zahl.
|
|
|
|
Aendere keine Datei ausserhalb der sechs genannten. Insbesondere: KEINE
|
|
Aenderung an `settings.controller.ts`, den DTOs, `icon-discovery.service.ts`,
|
|
`mail.service.ts`, `tender-mail.service.ts`, `dkv-mail.service.ts`,
|
|
`dashboard.controller.ts`.
|
|
</action>
|
|
<verify>
|
|
<automated>npm --prefix apps/api run test -- src/favorites/favorites.service.spec.ts src/settings/settings.service.spec.ts && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -gt 951 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 951"; exit 1; }; } && TF=$(echo "$TOUT" | sed -nE 's/^ *Test Files +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$TF" && test "$TF" -ge 62 || { echo "TESTDATEIEN: ${TF:-unbekannt}, erwartet mindestens 62"; exit 1; }; } && npm --prefix apps/api run type-check && F=apps/api/src/favorites/favorites.service.ts && SRC=$(grep -vE '^\s*(//|\*|/\*)' "$F") && test 0 -eq "$(grep -c 'this\.prisma\.favoriteLink' "$F")" && B=$(printf '%s\n' "$SRC" | grep -o 'tenantPrisma\.favoriteLink\.' | wc -l | tr -d ' ') && { test "$B" -eq 7 || { echo "BINDUNG: $B gebundene Favoritenzugriffe, erwartet genau 7"; exit 1; }; } && C=$(printf '%s\n' "$SRC" | grep -o 'forTenant(this\.prisma, tenantId)' | wc -l | tr -d ' ') && { test "$C" -eq 5 || { echo "KLIENTEN: $C Aufrufstellen des Bindungshilfsmittels in favorites.service.ts, erwartet genau 5"; exit 1; }; } && for M in list create update remove getIconBytes; do REG=$(awk -v m="async $M(" 'index($0,m){f=1} f{print} f&&/^ \}$/{f=0}' "$F" | grep -vE '^\s*(//|\*|/\*)'); K=$(printf '%s\n' "$REG" | grep -c 'forTenant(this\.prisma, tenantId)'); test "$K" -eq 1 || { echo "METHODE $M: $K Klienten, erwartet genau 1"; exit 1; }; U=$(printf '%s\n' "$REG" | grep -c 'this\.prisma\.[a-zA-Z]'); test "$U" -eq 0 || { echo "METHODE $M: $U ungebundene Modellzugriffe"; exit 1; }; done && grep -q 'async list(tenantId: string, userId: string, widgetId: string)' "$F" && grep -q 'async create(tenantId: string, userId: string, dto: CreateFavoriteDto)' "$F" && grep -q 'Widget not found' "$F" && grep -q 'tenantPrisma\.widgetInstance\.findUnique' "$F" && G=apps/api/src/settings/settings.service.ts && GS=$(grep -vE '^\s*(//|\*|/\*)' "$G") && U2=$(printf '%s\n' "$GS" | grep -c 'this\.prisma\.smtpConfig') && { test "$U2" -eq 1 || { echo "STARTPFAD: $U2 ungebundene SMTP-Zugriffe im Code von settings.service.ts, erwartet genau 1 (nur der Startpfad)"; exit 1; }; } && test 1 -eq "$(grep -c 'this\.prisma\.smtpConfig' "$G")" && B2=$(printf '%s\n' "$GS" | grep -o 'tenantPrisma\.smtpConfig\.' | wc -l | tr -d ' ') && { test "$B2" -eq 3 || { echo "BINDUNG: $B2 gebundene SMTP-Zugriffe, erwartet genau 3"; exit 1; }; } && C2=$(printf '%s\n' "$GS" | grep -o 'forTenant(this\.prisma, tenantId)' | wc -l | tr -d ' ') && { test "$C2" -eq 3 || { echo "KLIENTEN: $C2 Aufrufstellen in settings.service.ts, erwartet genau 3"; exit 1; }; } && grep -q 'async loadAnySmtpConfigForStartupTransport()' "$G" && REG=$(awk 'index($0,"async loadAnySmtpConfigForStartupTransport("){f=1} f{print} f&&/^ \}$/{f=0}' "$G" | grep -vE '^\s*(//|\*|/\*)') && test 1 -eq "$(printf '%s\n' "$REG" | grep -c 'this\.prisma\.smtpConfig\.findFirst()')" && test 0 -eq "$(printf '%s\n' "$REG" | grep -c 'forTenant(')" && awk 'index($0,"async loadAnySmtpConfigForStartupTransport("){exit} {print}' "$G" | tail -40 | grep -q 'UNGEBUNDEN' && test 0 -eq "$(grep -rc 'getStartupSmtpConfig' apps/api/src | awk -F: '{s+=$2} END{print s}')" && grep -q 'loadAnySmtpConfigForStartupTransport()' apps/api/src/mail/mail.module.ts && test 0 -eq "$(grep -c 'single-tenant default' apps/api/src/mail/mail.module.ts)" && test 0 -eq "$(grep -c 'tenantPrisma\.findFirst\|tenantPrisma\.smtpConfig\.findFirst' "$G")" && H=apps/api/src/favorites/favorites.controller.ts && for M in 'list(tenantId, userId, widgetId)' 'create(tenantId, userId, dto)' 'getIconBytes(' 'update(tenantId, id, userId, dto)' 'remove(tenantId, id, userId)'; do grep -qF "this.favoritesService.$M" "$H" || { echo "CONTROLLER: Aufruf $M nicht in der erwarteten Form"; exit 1; }; done && grep -q 'tenantId,$' "$H" && SF=apps/api/src/favorites/favorites.service.spec.ts && grep -q '__makeBoundClient' "$SF" && grep -q 'mock.calls.length' "$SF" && grep -q 'Widget not found' "$SF" && grep -q 'fetchIconBytes' "$SF" && SS=apps/api/src/settings/settings.service.spec.ts && grep -q '__makeBoundClient' "$SS" && grep -q "vi.mock('nodemailer'" "$SS" && grep -q 'mock.calls.length' "$SS" && grep -q 'loadAnySmtpConfigForStartupTransport' "$SS" && grep -q 'toBe(0)\|toHaveLength(0)\|toEqual(0)' "$SS" && git rev-parse --verify 46f0e78 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 46f0e78 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 46f0e78) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|apps/api/src/favorites/favorites\.service\.ts|apps/api/src/favorites/favorites\.service\.spec\.ts|apps/api/src/favorites/favorites\.controller\.ts|apps/api/src/settings/settings\.service\.ts|apps/api/src/settings/settings\.service\.spec\.ts|apps/api/src/mail/mail\.module\.ts|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }</automated>
|
|
</verify>
|
|
<done>`favorites.service.ts`: alle fuenf Methoden nehmen den Mandanten als ersten Parameter und laufen je ueber genau EINEN Klienten `tenantPrisma` (sieben gebundene Favoritenzugriffe, fuenf Aufrufstellen des Bindungshilfsmittels, ein gebundener `widgetInstance`-Lesezugriff als Besitzriegel in `create` mit `Widget not found`); kein ungebundener Favoritenzugriff, auch nicht in Kommentaren. `favorites.controller.ts` reicht `tenantId` an alle fuenf Aufrufe durch, `extractContext` unveraendert, Kopfkommentar nennt Quelle und Praezedenzfall. `settings.service.ts`: `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` je ueber EINEN Klienten (drei gebundene Zugriffe, drei Aufrufstellen); der Startpfad heisst `loadAnySmtpConfigForStartupTransport()`, laeuft unveraendert auf dem ungebundenen Klienten mit `findFirst()` ohne Bedingung — die einzige ungebundene Stelle der Datei —, traegt den Kopfkommentar mit beiden Zustaenden, Unsymmetrie, Reparaturweg, Ledger-Verweis; der alte Name kommt in `apps/api/src` nicht mehr vor; `mail.module.ts` ruft den neuen Namen auf und nennt beide Zustaende statt "single-tenant default". Beide Testdateien existieren neu mit dem Zwei-Klienten-Nachbau (ungebunden ohne Anfrage-Modelle, gebunden ohne `findFirst`), jedem in `<behavior>` genannten Fall, Wachhund und Null-Klienten-Nachweis; alle vier Falsifizierungsnachweise durchgefuehrt, zurueckgenommen, woertlich notiert. Testzahl gestiegen, mindestens 62 Testdateien, Typpruefung sauber, Schema/DTOs/Controller `settings`/Icon-Proxy/Versanddienste unveraendert.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Aufgabe 3: Alle Dokumentstellen auf den Endstand der Etappe 2 bringen (sechster Fall, Befund K erfuellt, Abschluss-Abschnitt), drei Ledger-Eintraege anlegen, Nummern eintragen, Gates falsifizieren</name>
|
|
<files>docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md, docs/anleitung-entwicklung.md, .planning/WINDOWS.md, apps/api/src/settings/settings.service.ts, apps/api/src/mail/mail.module.ts</files>
|
|
<read_first>docs/mandantentrennung-zugriffsklassifikation.md (VOLLSTAENDIG: Uebersichtstabelle samt Messanweisung und die Zeilen `favorites`/`settings`, Summenzeile, Klassen-Verteilung samt Tabelle, Hintergrunddienst-Abschnitt VOLLSTAENDIG (Ueberschrift, Einleitungsabsatz, der Absatz zum fuenften Fall, die Stand-Absaetze), beide Bestandsaufnahme-Zeilen, `Was diese Etappe NICHT entscheidet`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Kopf (a)-(e) mit dem Nachtrag unter Befund D als Form; (t4) Befund K; (d4) Uebergaben-Absatz; die neuen Abschnitte aus Aufgabe 1; `## Verweis`), docs/anleitung-entwicklung.md (Zeilen 300-325), apps/api/src/prisma/rls-access-inventory.spec.ts (`computeStandByKey`, `parseDocEntries`), .planning/WINDOWS.md (Kopfzaehler, Eintraege #21, #25, #26, #28 als Form), .planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-SUMMARY.md (wie die beiden Ledger-Eintraege dort angelegt wurden — der `gsd-tools windows append`-Aufruf), apps/api/src/settings/settings.service.ts (den Kopfkommentar des Startpfads mit dem Platzhalter), apps/api/src/mail/mail.module.ts (den Modulkommentar mit dem Platzhalter)</read_first>
|
|
<action>
|
|
TEIL 1 — die Ledger-Eintraege, ueber `gsd-tools windows append` (damit
|
|
Tabelle, JSON-Block und Kopfzaehler zusammenpassen — nicht von Hand), je mit
|
|
`--kind`, `--phase quick-260911-gwh`, `--file`, `--description`:
|
|
|
|
(1) `--kind deviation --file apps/api/src/mail/mail.module.ts` — der
|
|
Startpfad des Mailmoduls als SECHSTER Fall der Hintergrunddienst-Falle
|
|
(`SettingsService.loadAnySmtpConfigForStartupTransport()`, vormals mit dem
|
|
alten Namen), BLEIBT bewusst UNGEBUNDEN. Zwei Zustaende: HEUTE bereits
|
|
falsch — `findFirst()` ohne Bedingung zieht bei mehreren Mandanten den
|
|
SMTP-Server und Absender EINES beliebigen Mandanten fuer alle Systemmails
|
|
(Kennwort-Zuruecksetzung, Willkommen) ALLER Mandanten; NACH DEM
|
|
SCHARFSCHALTEN (#18) `null`, und `mail.module.ts` faellt auf `MAIL_*`,
|
|
`TESSERA_SMTP_*`, zuletzt `localhost:1025` zurueck, `MailService` faengt den
|
|
Transportfehler (T-02-12) — KEINE Protokollzeile, das Verstummen ist doppelt
|
|
verdeckt (die Unsymmetrie zu #21 und zu `getAllActiveConfigs`). Drei
|
|
erwogene Formen wie in #21 (binden unmoeglich; Mehrmandanten-Versand
|
|
abgelehnt als Funktion — mit dem Satz, dass die Vorlage in
|
|
`DkvMailService`/`TenderMailService` steht und `MailService` den Mandanten
|
|
von `requestPasswordReset` bekommen koennte; Altlast mit Markierung GEWAEHLT).
|
|
Warum EIGENER Eintrag statt #21: andere Datei, andere Reparatur, andere
|
|
Verdeckungsform. Markierung: umbenannte Methode mit Kopfkommentar,
|
|
Modulkommentar in `mail.module.ts`, (s4)(a). Signal fuer das Verstummen
|
|
gehoert in `rls-preflight.mjs` (Etappe 4). Falls die Lesung in Aufgabe 1
|
|
(s4)(a) auf Anschluss an #21 entschieden hat, entfaellt dieser Eintrag und
|
|
der Grund steht im SUMMARY — die Nummer im Code verweist dann auf #21.
|
|
|
|
(2) `--kind deviation --file apps/web/src/components/dashboard/widgets/favorites-widget.tsx`
|
|
— Bereich `favorites`: ein nach dem Scharfschalten zu klein gebliebenes
|
|
Leseergebnis auf `list` sieht aus wie `Noch keine Favoriten.`
|
|
(`favorites-widget.tsx`, Zeile um 212; `fetchFavorites` in
|
|
`favorites-api.ts` reicht `[]` durch) — "nie einen gespeichert" und "Zeile
|
|
unsichtbar" sind fuer das Frontend derselbe Wert; Etappe-4-Vorabpruefung
|
|
aus (f4)(d); an dieselbe Bedingung gebunden wie #18; Familie
|
|
#23/#25/#26/#28; das Frontend wird von 260911-gwh NICHT geaendert.
|
|
|
|
(3) `--kind deviation --file apps/web/src/components/settings/smtp-settings-form.tsx`
|
|
— Bereich `settings`: `getSmtpConfig` liefert nach dem Scharfschalten
|
|
`null`, der Controller antwortet 200 mit leerem Rumpf, `fetchSmtp`
|
|
(`settings-api.ts`) laeuft mit `res.json()` auf den leeren Rumpf und wirft,
|
|
`smtp-settings-form.tsx` verschluckt das in `.catch(() => {})` — leeres
|
|
Formular, "nicht eingerichtet", waehrend die Zugangsdaten physisch da sind;
|
|
ein erneutes Speichern unter der ungebundenen Form scheitert am
|
|
Eindeutigkeitsindex (Pruefung 8, mit gemessenem Konstruktor/`code`) — nach
|
|
diesem Lauf ist `saveSmtpConfig` gebunden und trifft die eigene Zeile;
|
|
dieselbe 200-leerer-Rumpf-Kette wie #28; Etappe-4-Vorabpruefung aus
|
|
(s4)(e); Frontend NICHT geaendert.
|
|
|
|
Pruefe nach dem Anlegen, dass jeder Eintrag in Tabelle UND JSON-Block steht
|
|
und die Kopfzaehler (`open_count`, `total_count`) mit den Zeilen
|
|
uebereinstimmen. Ersetze dann den Platzhalter `WINDOWS #TBD-GWH` in
|
|
`apps/api/src/settings/settings.service.ts` und `apps/api/src/mail/mail.module.ts`
|
|
durch die vergebene Nummer von Eintrag (1) (bzw. `#21`, falls angeschlossen)
|
|
— sonst nichts an diesen beiden Dateien.
|
|
|
|
TEIL 2 — die Klassifikation. Ziehe `docs/mandantentrennung-zugriffsklassifikation.md`
|
|
an ALLEN handgepflegten Stellen nach, jede einzeln nachgesehen, keine
|
|
ueberflogen (Fehler 4 des Vorhabens):
|
|
|
|
1. Uebersichtszeilen `favorites` und `settings` mit den NEU GEMESSENEN Zahlen
|
|
aus der im Dokument genannten Messanweisung (beide Spalten), im
|
|
etablierten Stil mit Vermerk des vorherigen Standes (`**war 7/0**`,
|
|
`**war 4/0**`): fuer `favorites`, welche fuenf Methoden gebunden wurden
|
|
und dass der Besitzriegel einen zusaetzlichen gebundenen
|
|
`widgetInstance`-Lesezugriff einfuehrt (wenn gebaut — Zahl aus der
|
|
Messung); fuer `settings`, dass die drei Anfragewege gebunden sind und der
|
|
eine verbleibende ungebundene Rohtreffer der benannte Startpfad ist
|
|
(Verweis auf den sechsten Fall unten, den Ledger-Eintrag, (s4)(a)).
|
|
2. Summenzeile mit fortgeschriebener Herkunftsspur (ungebunden und gebunden
|
|
je um die Differenzen aus Schritt 1) UND dem Satz, dass dies der Endstand
|
|
der Etappe 2 ist: jeder verbleibende ungebundene Rohtreffer ist einer der
|
|
im Dokument benannten, bewusst ungebundenen Faelle.
|
|
3. Bestandsaufnahme: `favorites.service.ts | favoriteLink` Stand von
|
|
`ungebunden` auf `gebunden`, Begruendung NEU (fuenf Methoden, ein Klient je
|
|
Methode, Besitzpruefungen bleiben, Mandantenquelle wie `dashboard`,
|
|
260911-gwh); NEUE Zeile `apps/api/src/favorites/favorites.service.ts |
|
|
widgetInstance | muss-mandantengebunden | gebunden` mit Begruendung
|
|
(Besitzriegel vor dem Anlegen, weil der Fremdschluessel am Zeilenschutz
|
|
vorbei prueft — Pruefung 7) — NUR, wenn der Riegel gebaut wurde;
|
|
`settings.service.ts | smtpConfig` Stand von `ungebunden` auf `gemischt`,
|
|
Begruendung NEU in der Form der `dkv.service.ts`/`dkvModuleConfig`-Zeile
|
|
(die Mischung stammt ausschliesslich vom benannten Startpfad, keine
|
|
uebersehene Fundstelle; Befund K erfuellt). Ohne Schritt 3 ist
|
|
`rls-access-inventory.spec.ts` am Ende dieser Aufgabe rot (Stand-Vergleich
|
|
und, bei neuer Fundstelle, Vollstaendigkeit).
|
|
4. Klassen-Verteilung: ein `**Stand 260911-gwh (Aufgabe 3):**`-Absatz — bei
|
|
gebautem Riegel: 65 Paare, eine neue Fundstelle
|
|
(`favorites.service.ts`/`widgetInstance`, `muss-mandantengebunden`), die
|
|
Zahl der Ausgabe der Pruefung entnommen, Tabelle `muss-mandantengebunden`
|
|
33 und Summe 65, Ueberschrift auf 65 Paare; sonst der
|
|
"unveraendert, ausdruecklich festgehalten"-Absatz in der Form von
|
|
260911-fh9. In beiden Faellen der Satz, dass zwei Paare nur ihre
|
|
Stand-Spalte aendern, und dass dies der Endstand der Etappe 2 ist.
|
|
5. Hintergrunddienst-Abschnitt: Ueberschrift von `fünf Fälle` auf `sechs
|
|
Fälle`; im Einleitungsabsatz der Satz, dass der sechste Fall (seit
|
|
260911-gwh) von derselben Bauart wie der fuenfte ist; ein NEUER Absatz
|
|
`**Der sechste Fall, gleicher Bauart wie der fünfte — `mail.module.ts` /
|
|
`SettingsService.loadAnySmtpConfigForStartupTransport()`** (260911-gwh,
|
|
Befund D, WINDOWS #<Nr.>): offen, als benannte Altlast weitergeführt.`
|
|
unmittelbar NACH dem Absatz zum fuenften Fall und VOR dem
|
|
`**Stand 260910-exd**`-Absatz, in dessen Form: beide Zustaende, warum
|
|
Binden keine Loesung ist, warum der Umbau eine Funktion ist, die
|
|
Unsymmetrie zu BEIDEN Praezedenzfaellen (Rueckfallkette, keine
|
|
Protokollzeile), die dreifache Markierung, das Signal fuer
|
|
`rls-preflight.mjs`, UND der Satz, dass die Reihenfolgebedingung Befund K
|
|
(`tenders` (t4), `dkv` (d4)) fuer den VERSANDPFAD `getDecryptedSmtpConfig`
|
|
mit diesem Lauf ERFUELLT ist — die Etappe-4-Vorabpruefung fuehrt sie nicht
|
|
mehr. Dazu ein PLAIN-Absatz `**Stand 260911-gwh — der Bereich `favorites`
|
|
fügt diesem Abschnitt keinen weiteren Fall hinzu, gemessen statt
|
|
angenommen (Befund K).**` mit der Anweisung und dem `setTimeout`-Treffer
|
|
im Icon-Proxy (Form von 260911-cwh).
|
|
6. Abschnitt `Was diese Etappe NICHT entscheidet`: ein NEUER Punkt — wie
|
|
das Mailmodul kuenftig je Mandant versendet (Transport je Versand aus
|
|
`getDecryptedSmtpConfig(tenantId)`, Vorlage `DkvMailService`/
|
|
`TenderMailService`; `MailService` braucht dafuer den Mandanten, den
|
|
`requestPasswordReset` aus der Funktionszeile hat) — eine Funktion,
|
|
Verweis auf (s4)(a) und den Ledger-Eintrag; und ein ZWEITER Punkt in der
|
|
durchgestrichenen Form (`~~...~~ **Aufgelöst (260911-gwh):**`) NUR, wenn
|
|
der Abschnitt heute einen Punkt zur settings-Reihenfolgebedingung fuehrt
|
|
(gemessen: keinen — dann stattdessen ein Satz am Ende des neuen
|
|
Hintergrunddienst-Absatzes, wie in Schritt 5 verlangt).
|
|
|
|
TEIL 3 — die Kritikschrift. (a) Nachtrag unter Befund K in (t4), in der
|
|
Form des Nachtrags unter Befund D in (e): Original bleibt stehen, darunter
|
|
ein Absatz `**Nachtrag (260911-gwh):**` — `getDecryptedSmtpConfig(tenantId)`
|
|
laeuft seit Aufgabe 2 dieses Laufs ueber `forTenant()`, die
|
|
Reihenfolgebedingung ist erfuellt, Verweis auf (s4)(b) und die
|
|
Bestandsaufnahme-Zeile `settings.service.ts`/`smtpConfig`. (b) Derselbe
|
|
Nachtrag unter dem Uebergaben-Absatz in (d4) (nur der `settings`-Teil;
|
|
`dkv.seed.ts`/`module-registry` ist seit 260910-exd gebunden — pruefen und,
|
|
falls zutreffend, in demselben Nachtrag mit einem Satz nennen). (c) Ein
|
|
NEUER Abschnitt `## Etappe 2 — Abschluss` unmittelbar NACH `## Bereich
|
|
settings` und VOR `## Verweis`: was Etappe 2 in Summe geliefert hat — die
|
|
Zahl der Laeufe (aus STATE.md gezaehlt: zwoelf Bereichs-/Regel-Laeufe von
|
|
260909-ipc bis 260911-gwh, nachzaehlen), die Summenzeile der
|
|
Uebersichtstabelle VORHER (aus dem Klassifikationsdokument: der `**war
|
|
...**`-Ausgangswert, 227 Rohtreffer laut Kopf) und NACHHER (die Summenzeile
|
|
nach Schritt 2 oben, woertlich abgeleitet), die Zahl der Paare und die
|
|
Klassen-Verteilung (aus Schritt 4), die Liste der BEWUSST ungebundenen
|
|
Reste je Bereich mit ihrem Grund in je einem Satz (tenders: D-03-Katalog,
|
|
Fan-out-Adapter, RSS-Verwaltung #24, uebergreifende Haelften; ldap:
|
|
`getAllActiveConfigs`, `resolveEmailForWrite`; dkv: #21; user:
|
|
`findByUsername`, Erstanlage, Treiber; module-registry/dashboard:
|
|
Modulkatalog; auth: drei Anmeldefunktionen; tenant: die Mandantentabelle;
|
|
settings: der sechste Fall) — jeden Bereich aus seinem eigenen Abschnitt
|
|
dieses Dokuments bzw. der Uebersichtszeile abgeschrieben, nicht aus dem
|
|
Gedaechtnis —, die Zahl der offenen Ledger-Eintraege dieser Etappe (aus
|
|
WINDOWS.md gezaehlt, mit Nummern), die Zahl der Pruefungen des Werkzeugs
|
|
(die Zeile `Alle N Pruefungen bestanden.` des letzten Laufs) und der Tests
|
|
(letzte Testausgabe). Dann, was fuer Etappe 3 bleibt, je ein Satz mit
|
|
Verweis auf die Stelle: Anmeldeweg unter je Mandant eindeutigen Namen
|
|
((h4)(a)); Benutzerdimension der Regeln (Etappe-3-Entscheidung (2), (k4)/
|
|
(f4)); Modulkatalog-Regel (Befund E `module-registry`); Kennzeichnung der
|
|
`bewusst-uebergreifend`-Stellen; Mandantenwechsel im Digest ((t4)). Und was
|
|
Etappe 4 VOR dem Scharfschalten pruefen muss (`rls-preflight.mjs`): die in
|
|
den Bereichsabschnitten benannten Vorabpruefungen, als Liste mit Verweis —
|
|
OHNE die erfuellte Befund-K-Bedingung.
|
|
|
|
TEIL 4 — die Anleitung. Bringe in `docs/anleitung-entwicklung.md` den
|
|
Absatz um Zeile 310-316 (`app.current_tenant` wird von Postgres
|
|
Row-Level-Security ausgewertet ...) und den Folgeabsatz (`Was ein Entwickler
|
|
nie vergessen darf`) mit scoped `Edit` auf den gemessenen Stand: die Regeln
|
|
liegen seit `20260909140000_rls_remaining_tenant_tables` auf 23 Tabellen
|
|
(`grep -c "ENABLE ROW LEVEL SECURITY"` ueber die drei Migrationen, zur
|
|
Ausfuehrungszeit gezaehlt), `FavoriteLink` und `SmtpConfig` eingeschlossen;
|
|
die Tabellen OHNE Regel sind die im Klassifikationsdokument als
|
|
`keine-mandantengebundene-tabelle` gefuehrten (`Tenant`, `Module`,
|
|
`Tender`, ... — aus der Bestandsaufnahme ableiten, nicht raten);
|
|
`DkvService.loadConfig()` ist seit 260909-mir gebunden — das Beispiel fuer
|
|
"manuelles `tenantId`-Filter" ersetzen durch die tatsaechliche Regel: jeder
|
|
Zugriff auf eine mandantengebundene Tabelle laeuft ueber einen dienst-intern
|
|
mit `forTenant()` gebundenen Klienten `tenantPrisma`, die `where`-Filter
|
|
ueber `userId` bleiben zusaetzlich; Verweis auf die beiden
|
|
Mandantentrennungs-Dokumente. Nichts sonst in dieser Datei.
|
|
|
|
TEIL 5 — zwei Falsifizierungsnachweise fuer die Dokument-Gates, jeder
|
|
zurueckgenommen und mit Meldung woertlich notiert: (a) setze die
|
|
Bestandsaufnahme-Zeile `favorites.service.ts | favoriteLink` probeweise
|
|
zurueck auf `ungebunden` — `rls-access-inventory.spec.ts` muss rot werden;
|
|
(b) setze die Uebersichtszeile `settings` probeweise auf eine falsche Zahl —
|
|
das herleitende Gate dieser Aufgabe muss fehlschlagen.
|
|
|
|
Aendere keine Datei ausserhalb der sechs genannten.
|
|
</action>
|
|
<verify>
|
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && SOUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$SOUT" | tail -1 && N=$(echo "$SOUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden\.$/\1/p') && { test -n "$N" && test "$N" -ge 137 || { echo "WERKZEUG: ${N:-nicht alle} Pruefungen bestanden, erwartet mindestens 137"; exit 1; }; } && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -gt 951 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 951"; exit 1; }; } && npm --prefix apps/api run type-check && test 0 -eq "$(grep -rc 'TBD-GWH' apps/api/src docs | awk -F: '{s+=$2} END{print s}')" && grep -qE 'WINDOWS #[0-9]+' apps/api/src/settings/settings.service.ts && grep -qE 'WINDOWS #[0-9]+' apps/api/src/mail/mail.module.ts && K=docs/mandantentrennung-zugriffsklassifikation.md && FU=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/favorites | grep -v spec | wc -l | tr -d ' ') && FB=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/favorites | grep -v spec | wc -l | tr -d ' ') && SU=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/settings | grep -v spec | wc -l | tr -d ' ') && SB=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/settings | grep -v spec | wc -l | tr -d ' ') && { test "$FU" -eq 0 || { echo "favorites: $FU ungebundene Rohtreffer, erwartet 0"; exit 1; }; } && { test "$FB" -ge 7 || { echo "favorites: $FB gebundene Rohtreffer, erwartet mindestens 7"; exit 1; }; } && { test "$SU" -eq 1 || { echo "settings: $SU ungebundene Rohtreffer, erwartet 1"; exit 1; }; } && { test "$SB" -eq 3 || { echo "settings: $SB gebundene Rohtreffer, erwartet 3"; exit 1; }; } && { grep -qE "^\| favorites \| ${FU} \| ${FB} \| \*\*war 7/0\*\*" "$K" || { echo "UEBERSICHTSZEILE favorites nennt nicht ${FU}/${FB} im etablierten Stil"; exit 1; }; } && { grep -qE "^\| settings \| ${SU} \| ${SB} \| \*\*war 4/0\*\*" "$K" || { echo "UEBERSICHTSZEILE settings nennt nicht ${SU}/${SB} im etablierten Stil"; exit 1; }; } && TU=0 && TB=0 && for d in apps/api/src/*/; do u=$(grep -ro "this\.prisma\.[a-zA-Z]*" "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); b=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); TU=$((TU+u)); TB=$((TB+b)); done && { grep -qE "^\| \*\*Summe\*\* \| \*\*${TU}\*\* \| \*\*${TB}\*\* \|" "$K" || { echo "SUMMENZEILE nennt nicht die abgeleiteten Werte ${TU}/${TB}"; exit 1; }; } && grep -qE '^\| apps/api/src/favorites/favorites\.service\.ts \| favoriteLink \| muss-mandantengebunden \| gebunden \|' "$K" && grep -qE '^\| apps/api/src/settings/settings\.service\.ts \| smtpConfig \| muss-mandantengebunden \| gemischt \|' "$K" && { if grep -q 'tenantPrisma\.widgetInstance\.' apps/api/src/favorites/favorites.service.ts; then grep -qE '^\| apps/api/src/favorites/favorites\.service\.ts \| widgetInstance \| muss-mandantengebunden \| gebunden \|' "$K" || { echo "BESTANDSAUFNAHME: neue Zeile favorites.service.ts/widgetInstance fehlt"; exit 1; }; fi; } && P=$(grep -cE '^\| apps/api/src/[^|]+ \| [a-zA-Z]+ \| (muss-mandantengebunden|bewusst-uebergreifend|keine-mandantengebundene-tabelle|beides) \| (gebunden|ungebunden|gemischt) \|' "$K") && { grep -qE "^## Klassen-Verteilung \(nach \(Datei, Modell\)-Fundstellen, ${P} Paare\)" "$K" || { echo "KLASSEN-VERTEILUNG: Ueberschrift nennt nicht die gezaehlten ${P} Paare"; exit 1; }; } && { grep -qE "^\| \*\*Summe\*\* \| \*\*${P}\*\* \|$" "$K" || { echo "KLASSEN-VERTEILUNG: Summe nennt nicht ${P}"; exit 1; }; } && MM=$(grep -cE '^\| apps/api/src/[^|]+ \| [a-zA-Z]+ \| muss-mandantengebunden \|' "$K") && { grep -qE "^\| muss-mandantengebunden \| ${MM} \|$" "$K" || { echo "KLASSEN-VERTEILUNG: muss-mandantengebunden nennt nicht ${MM}"; exit 1; }; } && grep -q '^\*\*Stand 260911-gwh (Aufgabe 3)' "$K" && grep -q '^## Der Hintergrunddienst als Falle — sechs Fälle$' "$K" && test 0 -eq "$(grep -c 'fünf Fälle' "$K")" && awk '/^## Der Hintergrunddienst als Falle/{f=1; next} /^## /{f=0} f && /sechste Fall/{s=1} f && /loadAnySmtpConfigForStartupTransport/{m=1} f && /mail\.module\.ts/{o=1} f && /Befund K/{k=1} f && /favorites/{v=1} f && /localhost:1025|Rückfallkette|Rueckfallkette/{r=1} END{ if(!s||!m||!o){print "HINTERGRUNDDIENST: der sechste Fall (mail.module.ts / loadAnySmtpConfigForStartupTransport) fehlt"; exit 1} if(!k){print "HINTERGRUNDDIENST: die erfuellte Befund-K-Bedingung ist nicht benannt"; exit 1} if(!v){print "HINTERGRUNDDIENST: der favorites-Absatz fehlt"; exit 1} if(!r){print "HINTERGRUNDDIENST: die Rueckfallkette ist nicht benannt"; exit 1} }' "$K" && awk '/^## Was diese Etappe NICHT entscheidet/{f=1; next} /^## /{f=0} f && /getDecryptedSmtpConfig/{g=1} f && /260911-gwh/{w=1} END{ if(!g||!w){print "NICHT ENTSCHEIDET: der Punkt zum Mehrmandanten-Versand (260911-gwh) fehlt"; exit 1} }' "$K" && D=docs/mandantentrennung-etappe2-fehlerrichtung.md && grep -q '^## Etappe 2 — Abschluss$' "$D" && awk '/^## Bereich settings$/{s=NR} /^## Etappe 2 — Abschluss$/{a=NR} /^## Verweis$/{v=NR} END{ if(!(s&&a&&v&&s<a&&a<v)){print "ABSCHLUSS-ABSCHNITT steht nicht zwischen Bereich settings und Verweis"; exit 1} }' "$D" && awk -v tu="$TU" -v tb="$TB" -v p="$P" '/^## Etappe 2 — Abschluss$/{f=1; next} /^## /{f=0} f && index($0, tu){u=1} f && index($0, tb){b=1} f && index($0, p " Paare"){q=1} f && /rls-preflight\.mjs/{r=1} f && /Etappe 3/{e=1} f && /WINDOWS/{w=1} END{ if(!u||!b){print "ABSCHLUSS: die abgeleitete Summenzeile (" tu "/" tb ") ist nicht genannt"; exit 1} if(!q){print "ABSCHLUSS: die Paarzahl (" p " Paare) ist nicht genannt"; exit 1} if(!r||!e||!w){print "ABSCHLUSS: Etappe 3, rls-preflight.mjs oder die Ledger-Eintraege sind nicht genannt"; exit 1} }' "$D" && awk '/^### \(t4\)/{f=1} /^### \(t5\)/{f=0} f && /Nachtrag \(260911-gwh\)/{n=1} END{ if(!n){print "(t4): Nachtrag unter Befund K fehlt"; exit 1} }' "$D" && awk '/^### \(d4\)/{f=1} /^### \(d5\)/{f=0} f && /Nachtrag \(260911-gwh\)/{n=1} END{ if(!n){print "(d4): Nachtrag im Uebergaben-Absatz fehlt"; exit 1} }' "$D" && A=docs/anleitung-entwicklung.md && test 0 -eq "$(grep -c 'FavoriteLink` — tragen zwar eine `tenantId`-Spalte, aber \*\*keine\*\*' "$A")" && test 0 -eq "$(grep -c 'nutzt den plain, UNGEBUNDENEN' "$A")" && grep -q '20260909140000_rls_remaining_tenant_tables' "$A" && grep -q 'tenantPrisma' "$A" && W=.planning/WINDOWS.md && OC=$(sed -nE 's/^open_count: ([0-9]+)$/\1/p' "$W") && TC=$(sed -nE 's/^total_count: ([0-9]+)$/\1/p' "$W") && ROWS=$(grep -cE '^\| [0-9]+ \| ' "$W") && { test "$ROWS" -eq "$TC" || { echo "WINDOWS: total_count=$TC, Tabellenzeilen=$ROWS"; exit 1; }; } && OPENROWS=$(grep -E '^\| [0-9]+ \| ' "$W" | grep -c '| open |') && { test "$OPENROWS" -eq "$OC" || { echo "WINDOWS: open_count=$OC, offene Zeilen=$OPENROWS"; exit 1; }; } && test "$(grep -cE '^\| [0-9]+ \| quick-260911-gwh \|' "$W")" -ge 2 && grep -q 'favorites-widget\.tsx' "$W" && grep -q 'smtp-settings-form\.tsx' "$W" && git rev-parse --verify 46f0e78 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 46f0e78 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 46f0e78) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|docs/mandantentrennung-zugriffsklassifikation\.md|docs/anleitung-entwicklung\.md|apps/api/src/favorites/favorites\.service\.ts|apps/api/src/favorites/favorites\.service\.spec\.ts|apps/api/src/favorites/favorites\.controller\.ts|apps/api/src/settings/settings\.service\.ts|apps/api/src/settings/settings\.service\.spec\.ts|apps/api/src/mail/mail\.module\.ts|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; } && test -z "$(git diff --name-only 46f0e78 -- docker-compose.yml docker-compose.*.yml .env .env.* apps/api/.env apps/api/.env.* 2>/dev/null)"</automated>
|
|
</verify>
|
|
<done>Drei neue offene Ledger-Eintraege (Startpfad des Mailmoduls — oder, mit Grund, Anschluss an #21 —, verschluckte Leere `favorites`, verschluckte Leere `settings`) stehen in Tabelle UND JSON-Block, die Kopfzaehler stimmen, der Platzhalter im Code ist durch die vergebene Nummer ersetzt. Alle handgepflegten Stellen von `docs/mandantentrennung-zugriffsklassifikation.md` sind nachgezogen und DERIVIERT gegatet: Uebersichtszeilen `favorites`/`settings` mit neu gemessenen Zahlen, Summenzeile aus der Messanweisung, Bestandsaufnahme (`favoriteLink` gebunden, `smtpConfig` gemischt, `widgetInstance` neu falls gebaut), Klassen-Verteilung mit Stand-Vermerk und aus der Tabelle gezaehlter Paarzahl, Hintergrunddienst-Abschnitt mit sechstem Fall, angepasster Ueberschrift, erfuellter Befund-K-Bedingung und favorites-Absatz, `Was diese Etappe NICHT entscheidet` mit dem Mehrmandanten-Versand-Punkt. Die Kritikschrift traegt die Nachtraege in (t4) und (d4) und den Abschnitt `## Etappe 2 — Abschluss` mit den aus den eigenen Messanweisungen abgeleiteten Zahlen, den bewusst ungebundenen Resten je Bereich, den Ledger-Nummern, dem Etappe-3-Rest und der Etappe-4-Vorabpruefliste. Die Anleitung nennt `FavoriteLink` nicht mehr als Tabelle ohne Regel und `loadConfig()` nicht mehr als ungebunden. Beide Dokument-Falsifizierungen durchgefuehrt, zurueckgenommen, woertlich notiert. Werkzeug alle Pruefungen bestanden (mindestens 137), Testzahl ueber 951, Typpruefung sauber, Schalter aus, Schema/Migrationen/Compose/Umgebung unveraendert, Erlaubnisliste gegen `46f0e78` gehalten.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
|
|
Konfiguriert: ASVS-Stufe 1, blockierend ab `high`.
|
|
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Angemeldeter Benutzer -> `/favorites` (`list`, `create`, `update`, `remove`, `:id/icon`) | Kennung im Pfad und `widgetId` im Rumpf sind frei waehlbare Eingaben; der Mandant kommt aus `extractContext` (Guard-Kennung, wie `dashboard`), die Benutzerkennung aus dem Sitzungsnachweis. Die Regel auf `FavoriteLink` kennt keine Benutzerdimension. |
|
|
| `FavoriteLink.widgetId` -> `WidgetInstance.id` (Fremdschluessel) | PostgreSQL prueft referentielle Integritaet AM ZEILENSCHUTZ VORBEI: ein Bezug auf ein unsichtbares Widget wird auf Datenbankebene nicht gesperrt (Pruefung 7 misst). |
|
|
| Favoriten-Dienst -> Icon-Proxy -> Internet | `discoverFavoriteIconUrl` nimmt die Nutzer-URL fuer einen SSRF-gesicherten Abruf (T-08-05); `fetchIconBytes` nimmt NUR die gespeicherte URL einer Zeile, die der Aufrufer besitzt (T-QFIP-01). Kein Datenbankzugriff, kein Mandantenzustand. |
|
|
| ADMIN / SUPER_ADMIN -> `/settings/smtp` | `req.tenantId` (fuer SUPER_ADMIN per `x-tenant-id` umschaltbar) ist hier die GEWOLLTE Quelle (D-10); Zugangsdaten verschluesselt (AES-256-GCM), nie im GET. |
|
|
| `tender-mail.service.ts` / `dkv-mail.service.ts` -> `getDecryptedSmtpConfig(tenantId)` | Der Mandant kommt aus der gebundenen Schleife des Aufrufers; der entschluesselte Wert lebt nur im Methodenrumpf (T-07-10). |
|
|
| `mail.module.ts` (Start) -> Startpfad -> `MailerService` | Kein Mandantenkontext; eine BELIEBIGE Zeile wird zum Transport ALLER Systemmails; nach dem Scharfschalten `null` -> Rueckfallkette -> `localhost:1025`. |
|
|
| API -> PostgreSQL, Tabellen `FavoriteLink`/`SmtpConfig` | Regel `"tenantId" = current_tenant_id()` (20260909140000); nach dem Scharfschalten liefert ein ungebundener Zugriff null Zeilen (Pruefungen 3/5), ein ungebundenes `upsert` scheitert am Eindeutigkeitsindex (Pruefung 8). |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-GWH-01 | Information Disclosure | `settings.service.ts` `getSmtpConfig`/`getDecryptedSmtpConfig`, Mandantengrenze | high | mitigate | Ohne Bindung liest ein ADMIN von Mandant A ueber eine gefaelschte oder falsch aufgeloeste Mandantenkennung die (verschluesselten bzw. entschluesselten) SMTP-Zugangsdaten von B; nach dem Scharfschalten sind ungebundene Zugriffe leer statt fremd. Gebundener Klient je Methode; Pruefung 7 (fremder Mandant: `null`) ueber den generierten Client; Testfaelle (fremder Mandant: `null`, `decrypt` nicht aufgerufen); Falsifizierung (c). |
|
|
| T-GWH-02 | Tampering / Elevation of Privilege | `favorites.service.ts` `update`/`remove`/`getIconBytes`, Mandantengrenze | high | mitigate | Ein Nutzer von Mandant A aendert oder loescht ueber die Kennung eine Favoritenzeile von Mandant B. Heute schuetzt nur der `userId`-Vergleich; nach der Bindung liefert die Vorpruefung `null` (Pruefung 5), ein gebundenes `delete` ueber die Kennung allein wirft (Pruefung 6). Beide Zugriffe auf DEMSELBEN Klienten; Testfaelle (fremder Mandant: `NotFoundException`, kein Schreibzugriff); Falsifizierung (a). |
|
|
| T-GWH-03 | Elevation of Privilege / Information Disclosure | `mail.module.ts` Startpfad, Nutzung fremder Zugangsdaten | high | transfer | HEUTE bei mehreren Mandanten: der SMTP-Server und Absender EINES beliebigen Mandanten tragen Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (Nutzung fremder Zugangsdaten, Absenderfaelschung aus Sicht des Empfaengers). Kein Bindungsproblem — beim Start gibt es keinen Mandanten; der Umbau auf Transport je Versand ist eine Funktion (Vorlage: `DkvMailService`/`TenderMailService`). Gemessen (Pruefung 4), benannt (Kopfkommentar beider Zustaende, (s4)(a)), als OFFENER Ledger-Eintrag mit konkretem Reparaturweg uebergeben. Heute ein Mandant — die Bedingung tritt mit dem zweiten ein. |
|
|
| T-GWH-04 | Denial of Service (still) | `mail.module.ts` Rueckfallkette nach dem Scharfschalten | high | transfer | `null` -> `MAIL_*` -> `TESSERA_SMTP_*` -> `localhost:1025`; `MailService` faengt den Transportfehler, der Controller antwortet 200: Kennwort-Zuruecksetzung kommt nie an, ohne Protokollzeile beim Start. Gemessen (Pruefung 3), beide Zustaende an drei Stellen benannt, Signal fuer `rls-preflight.mjs` (Etappe 4) benannt; NICHT in diesem Lauf behoben (kein Umbau des Transports, kein Schalter). |
|
|
| T-GWH-05 | Information Disclosure | `favorites.service.ts` `create`, Fremdschluessel auf `WidgetInstance` | medium | mitigate | Der Unterschied zwischen "Widget gibt es nicht" (FK-Verletzung, 500) und "gehoert einem fremden Mandanten" (gelingt, weil der Fremdschluessel am Zeilenschutz vorbei prueft) ist ein Existenzorakel ueber Mandantengrenzen; dazu haengt eine fremde Zeile am Widget eines anderen (Kaskade). Pruefung 7 misst; Besitzriegel vor dem Anlegen auf dem gebundenen Klienten mit EINER Antwort (`Widget not found`) fuer alle drei Faelle; Testfaelle; Falsifizierung (b). |
|
|
| T-GWH-06 | Denial of Service (still) / Tampering | `saveSmtpConfig` unter der ungebundenen Form | medium | mitigate | Leeres Formular nach dem Scharfschalten verleitet zum Neueintrag; das ungebundene `upsert` findet die eigene Zeile nicht und scheitert am Eindeutigkeitsindex (Pruefung 8, Konstruktor/`code` gemessen). Gebundenes `upsert` trifft die eigene Zeile (Pruefung 9); Testfall. Die Frontend-Kette bleibt als Ledger-Eintrag (Familie #28). |
|
|
| T-GWH-07 | Server-Side Request Forgery | `icon-discovery.service.ts` | medium | accept | Nutzer-URL fuer die Icon-Suche; SSRF-Schutz (private IP-Bereiche, gesperrte Hostnamen, manuelle Weiterleitung, Zeitueberschreitung) besteht seit T-08-05 und ist in `icon-discovery.service.spec.ts` getestet; `fetchIconBytes` nimmt nur die gespeicherte URL einer eigenen Zeile (T-QFIP-01). Kein Datenbankzugriff, kein Mandantenbezug (Befund G) — unveraendert, nur gelesen. Der Besitzriegel in `create` steht VOR der Icon-Suche, damit ein fremdes Widget keinen Abruf ausloest. |
|
|
| T-GWH-08 | Information Disclosure | `FavoriteLink`, Kollege desselben Mandanten | low | accept | Die Regel kennt keine Benutzerdimension: auf Datenbankebene sieht ein gebundener Klient die Favoriten des Kollegen (Pruefung 4, zweite Aussage). Die anwendungsseitigen `userId`-Filter bleiben; Etappe-3-Entscheidung (2). Bestehend, benannt in (f4)(a). |
|
|
| T-GWH-09 | Repudiation / Information Disclosure | `favorites-widget.tsx`, `smtp-settings-form.tsx` | medium | accept | Die umgekehrte Fehlerrichtung ist verschluckt: `[]` wird zu `Noch keine Favoriten.`, `null` wird zu einem leeren Formular. Gemessen (Pruefungen 3/5), in (f3)/(s3) beschrieben, je ein Ledger-Eintrag (Familie #23/#25/#26/#28), Etappe-4-Vorabpruefung benannt. Das Frontend wird nicht geaendert (Umfang). |
|
|
| T-GWH-10 | Spoofing | `favorites.controller.ts`, Mandantenquelle | low | accept | `extractContext` liest die Guard-Kennung (fuer SUPER_ADMIN umschaltbar) — dieselbe Quelle wie `dashboard`, unter dessen Bindung das referenzierte Widget liegt; eine abweichende Quelle wuerde Widget und Link trennen. Kein Mandantenfeld im DTO (gemessen). Bewusst NICHT das Claim, mit Grund in (f4)(c). |
|
|
| T-GWH-11 | Denial of Service | `settings.service.spec.ts` -> `nodemailer` | low | mitigate | Ein echter Transport im Test liefe gegen `mailhog` (lokal nicht vorhanden: `ENOTFOUND`). `vi.mock('nodemailer')` ersetzt `createTransport`; kein Test oeffnet eine Verbindung; gegatet. |
|
|
| T-GWH-SC | Tampering | Paketinstallation | low | accept | Dieser Plan installiert kein Paket (npm/pip/cargo) und fuegt keine Abhaengigkeit hinzu. Das Legitimitaets-Gate faellt nicht an; ausdruecklich festgehalten statt schweigend ausgelassen. |
|
|
|
|
</threat_model>
|
|
|
|
<verification>
|
|
|
|
Nach Abschluss aller drei Aufgaben:
|
|
|
|
1. `node apps/api/scripts/rls-scratch-check.mjs` meldet alle Pruefungen
|
|
bestanden (120 bisherige plus mindestens 8 + 9 neue, mindestens 137),
|
|
Rueckgabewert 0; `runFavoritesAreaChecks` und `runSettingsAreaChecks`
|
|
stehen nach `runAuthAreaChecks` und vor `runTransactionShapeMeasurement`.
|
|
2. `npm --prefix apps/api run test` meldet mehr als 951 Tests gruen in
|
|
mindestens 62 Dateien (60 bisherige plus `favorites.service.spec.ts` und
|
|
`settings.service.spec.ts`).
|
|
3. `npm --prefix apps/api run type-check` ist sauber.
|
|
4. `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts`
|
|
ist gruen — `favorites.service.ts`/`favoriteLink` steht auf `gebunden`,
|
|
`settings.service.ts`/`smtpConfig` auf `gemischt`, die neue Fundstelle
|
|
`favorites.service.ts`/`widgetInstance` (falls gebaut) ist eingetragen.
|
|
5. In `favorites.service.ts`: kein ungebundener Favoritenzugriff (auch nicht
|
|
in Kommentaren), sieben gebundene, fuenf Aufrufstellen des
|
|
Bindungshilfsmittels, ein Klient je Methode, `Widget not found`. In
|
|
`settings.service.ts`: genau EIN ungebundener SMTP-Zugriff (der Startpfad,
|
|
`findFirst()` ohne Bedingung, in `loadAnySmtpConfigForStartupTransport()`),
|
|
drei gebundene, kein gebundener `findFirst`; der alte Name kommt in
|
|
`apps/api/src` nicht mehr vor; `mail.module.ts` nennt beide Zustaende und
|
|
die Ledger-Nummer.
|
|
6. `git diff --name-only 46f0e78 -- apps/api/prisma` ist leer — Schema und
|
|
Migrationen unangetastet; keine Compose- oder Umgebungsdatei geaendert.
|
|
7. Alle handgepflegten Stellen der Klassifikation sind maschinell gegatet
|
|
und gruen; die Zaehlgates LEITEN ihre Werte aus den im Dokument genannten
|
|
Messanweisungen ab (Uebersichtszeilen, Summenzeile ueber alle Bereiche,
|
|
Paarzahl aus der Bestandsaufnahme-Tabelle).
|
|
8. Der Umfang ist als ERLAUBNISLISTE gegatet: jede Datei, die sich
|
|
gegenueber `46f0e78` geaendert hat, ist eine der elf in `files_modified`
|
|
genannten (oder liegt unter `.planning/`). Unter `apps/api/prisma`,
|
|
`apps/web`, `apps/api/src/settings/settings.controller.ts`,
|
|
`apps/api/src/favorites/icon-discovery.service.ts`,
|
|
`apps/api/src/mail/mail.service.ts`, `apps/api/src/tenders`,
|
|
`apps/api/src/dkv` und den DTOs hat sich nichts geaendert.
|
|
9. `.planning/WINDOWS.md`: neue offene Eintraege in Tabelle UND JSON-Block,
|
|
Kopfzaehler stimmen mit den Zeilen ueberein; der Platzhalter `TBD-GWH`
|
|
kommt nirgends mehr vor.
|
|
10. Befund K steht an drei Stellen als ERFUELLT: (t4) Nachtrag, (d4)
|
|
Nachtrag, Hintergrunddienst-Abschnitt der Klassifikation.
|
|
11. `docs/mandantentrennung-etappe2-fehlerrichtung.md` traegt
|
|
`## Etappe 2 — Abschluss` mit den aus den eigenen Messanweisungen
|
|
abgeleiteten Zahlen (Summenzeile, Paarzahl), zwischen `## Bereich
|
|
settings` und `## Verweis`.
|
|
12. `DATABASE_URL` zeigt unveraendert auf die Rolle `tessera`; nichts in
|
|
Active Directory; kein echter SMTP-Transport in irgendeinem Test.
|
|
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
|
|
- Beide Bereiche sind gemessen, bevor gebaut wurde: zwei neue
|
|
Werkzeug-Abschnitte ueber den generierten Client, mit Fremdschluessel
|
|
(favorites) und Eindeutigkeitsindex (settings) als mitgebauten
|
|
Voraussetzungen der Messung.
|
|
- Der Startpfad des Mailmoduls ist als sechster Fall der
|
|
Hintergrunddienst-Falle erkannt, in beiden Zustaenden gemessen, umbenannt,
|
|
markiert (Kopfkommentar, Modulkommentar, Klassifikation, Kritikschrift,
|
|
Ledger) und NICHT gebunden, NICHT umgebaut; die Unsymmetrie zu `ldap` und
|
|
`dkv` (Rueckfallkette, keine Protokollzeile) und die Entscheidung zum
|
|
eigenen Ledger-Eintrag sind mit Grund aufgeschrieben.
|
|
- Befund K ist erfuellt und steht als erfuellt an allen drei Stellen, so dass
|
|
die Etappe-4-Vorabpruefung keine veraltete Bedingung fuehrt.
|
|
- Alle Anfragewege beider Bereiche laufen ueber genau EINEN Klienten
|
|
`tenantPrisma` je Methode; die Besitzpruefungen bleiben; `create` prueft das
|
|
Widget; beide Controller-Entscheidungen zur Mandantenquelle sind begruendet
|
|
(dashboard-Praezedenzfall fuer favorites, ADMIN-Seite fuer settings).
|
|
- Die umgekehrte Fehlerrichtung ist je Bereich in ihrer tatsaechlichen
|
|
Auspraegung benannt (`Noch keine Favoriten.`; leeres Formular ueber die
|
|
200-leerer-Rumpf-Kette; Speicherkonflikt gemessen) und als Ledger-Eintrag
|
|
je Bereich festgehalten.
|
|
- Zwei neue Testdateien mit dem Zwei-Klienten-Nachbau, ohne echten
|
|
Transport; sechs Falsifizierungsnachweise (vier in Aufgabe 2, zwei an den
|
|
Dokument-Gates in Aufgabe 3) durchgefuehrt, zurueckgenommen, woertlich
|
|
notiert.
|
|
- Etappe 2 ist vollstaendig: die fuenf handgepflegten Dokumentstellen stehen
|
|
auf ihrem Endstand, deriviert gegatet; die Kritikschrift hat einen
|
|
Abschluss-Abschnitt aus ihren eigenen Zahlen; die Anleitung widerspricht
|
|
dem Stand nicht mehr; Erlaubnisliste gegen `46f0e78`; Baseline gehalten
|
|
nach jeder Aufgabe; Schalter aus; Schema, Migrationen, Frontend, Active
|
|
Directory unberuehrt.
|
|
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Nach jeder Aufgabe committen (Praefix `feat(260911-gwh): ...` fuer Aufgabe 2,
|
|
`docs(quick-260911-gwh): ...` fuer Aufgabe 1 und 3, wie die Vorgaenger),
|
|
nach dem letzten Commit pushen (schlichtes `git push`, die Push-URL zeigt auf
|
|
`localhost:3002`). Bei jedem Commit, der eine Datei loescht oder mehrere
|
|
Pfade zugleich hinzufuegt, `git status --porcelain` VOR und NACH `git add`
|
|
ansehen (Fehler 11 des Vorhabens). Am Ende
|
|
`.planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-SUMMARY.md`
|
|
anlegen: tatsaechlich gezaehlte Pruefungs- und Testzahlen je Aufgabe, das
|
|
Ergebnis von Pruefung 7 (Fremdschluessel) und Pruefung 8 (Konfliktform) mit
|
|
Konstruktor/`code` woertlich, alle sechs Falsifizierungsnachweise mit
|
|
Testname und Meldung woertlich, jede Abweichung von den Planungsbefunden
|
|
ausdruecklich, die Ledger-Nummern und die Entscheidung zu #21 mit Grund, und
|
|
ein Absatz, der den Endstand der Etappe 2 in Zahlen nennt (aus dem
|
|
Abschluss-Abschnitt der Kritikschrift, nicht neu gerechnet). Kein naechster
|
|
Bereichs-Lauf: Etappe 2 ist abgeschlossen; was folgt, ist Etappe 3
|
|
(Kennzeichnung der uebergreifenden Stellen, Benutzerdimension, Anmeldeweg)
|
|
und die Etappe-4-Vorabpruefung.
|
|
</output>
|