Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8829999e70 | |||
| 388690fdf0 | |||
| 5ad23d0537 |
@@ -26,14 +26,15 @@
|
|||||||
"name": "Zwischendurch auf Anweisung des Users: drei Datenbankregeln geschlossen (260910-jab)",
|
"name": "Zwischendurch auf Anweisung des Users: drei Datenbankregeln geschlossen (260910-jab)",
|
||||||
"status": "done",
|
"status": "done",
|
||||||
"commit": "03fb3bf"
|
"commit": "03fb3bf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "WINDOWS #27 geschlossen: vierte Erkennungsform, 72 Paare, #33 fuer die Rest-Empfaenger (260911-mkj)",
|
||||||
|
"status": "done",
|
||||||
|
"commit": "388690f"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"remaining_tasks": [
|
"remaining_tasks": [
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"name": "WINDOWS #27: Quick 260911-mkj — Plan 261e736 gueltig und geprueft, Executor gestartet; git log und .planning/quick/260911-mkj-*/ pruefen ob abgeschlossen",
|
|
||||||
"status": "in_progress"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"name": "Etappe 3b: Benutzerdimension in den Regeln — VOLLSTAENDIGER AUFTRAG in docs/mandantentrennung-etappe3-auftrag.md (Reihenfolge 3b -> 3a -> 3c)",
|
"name": "Etappe 3b: Benutzerdimension in den Regeln — VOLLSTAENDIGER AUFTRAG in docs/mandantentrennung-etappe3-auftrag.md (Reihenfolge 3b -> 3a -> 3c)",
|
||||||
@@ -92,6 +93,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"uncommitted_files": [],
|
"uncommitted_files": [],
|
||||||
"next_action": "1. Pruefen ob 260911-mkj fertig ist (git status, git log, SUMMARY vorhanden?) — wenn ja verifizieren, abbuchen, pushen. 2. docs/mandantentrennung-etappe3-auftrag.md lesen. 3. Etappe 3b als /gsd-quick --validate starten. NICHT nachfragen ausser beim Scharfschalten.",
|
"next_action": "docs/mandantentrennung-etappe3-auftrag.md lesen, dann Etappe 3b (Benutzerdimension) als /gsd-quick --validate starten. NICHT nachfragen ausser beim Scharfschalten.",
|
||||||
"context_notes": "USER-ANWEISUNG 2026-09-11: 'mach #27 und dann Etappe 3, nicht nachfragen. du machst alles, was ohne mich geht. am dienstag geht eine voll funktionsfaehige version live.' Live-Gehen braucht den Schalter NICHT (ein Mandant, BYPASSRLS-Stand laeuft heute auf alpha). Etappe 3 darf das Live-Gehen nicht blockieren. Nach jedem Durchlauf pushen. Die vorige Sitzung endete mit ~65% Kontext waehrend #27 lief; der Etappe-3-Auftrag wurde deshalb als eigenes Dokument geschrieben, gemessen statt erinnert. Etappe 2 lief ueber zwoelf Quick-Tasks plus einen Regel-Durchlauf, jeder mit Planer, Plan-Pruefer, Executor, Verifizierer. ZEHN Lieferungen wurden vom jeweils NAECHSTEN Schritt gefangen, nie vom eigenen: vier geschrumpfte Zaehlungen, zwei nicht committete Messungen, zwei Zusammenfassungen mit N statt N-1, handgepflegte Dokumentstellen uebersprungen, Falsifizierungsnachweise nur in Commit-Nachrichten, eine Wegwerf-Tabelle ohne createdAt/updatedAt, ein Selbstwiderspruch, ein Pruefer der etwas als plausibel durchwinkte, ein still fehlgeschlagener git add, und zwei Agenten die am Sitzungslimit NACH getaner Arbeit abbrachen (dkv-Executor, gwh-Planer — beide Male lag die Arbeit vollstaendig auf der Platte; git status ist die Wahrheit, nicht der Bericht). Die vollstaendige Liste steht in jedem Planer-Auftrag der spaeten Bereiche. Nebenfunde ohne Mandantenbezug, alle behoben: DKV-Download-Luecke, drohender DKV-Passwortverlust, Selbstloesch-Riegel der nie griff, Startfehler bei Neuinstallation, adminResetPassword ohne Mandanten- und Rollenpruefung, Widget-Besitzriegel bei Favoriten, sechs luegende Kommentare, und fuenf Bereiche ohne jede Testdatei."
|
"context_notes": "USER-ANWEISUNG 2026-09-11: 'mach #27 und dann Etappe 3, nicht nachfragen. du machst alles, was ohne mich geht. am dienstag geht eine voll funktionsfaehige version live.' Live-Gehen braucht den Schalter NICHT (ein Mandant, BYPASSRLS-Stand laeuft heute auf alpha). Etappe 3 darf das Live-Gehen nicht blockieren. Nach jedem Durchlauf pushen. Die vorige Sitzung endete mit ~65% Kontext waehrend #27 lief; der Etappe-3-Auftrag wurde deshalb als eigenes Dokument geschrieben, gemessen statt erinnert. Etappe 2 lief ueber zwoelf Quick-Tasks plus einen Regel-Durchlauf, jeder mit Planer, Plan-Pruefer, Executor, Verifizierer. ZEHN Lieferungen wurden vom jeweils NAECHSTEN Schritt gefangen, nie vom eigenen: vier geschrumpfte Zaehlungen, zwei nicht committete Messungen, zwei Zusammenfassungen mit N statt N-1, handgepflegte Dokumentstellen uebersprungen, Falsifizierungsnachweise nur in Commit-Nachrichten, eine Wegwerf-Tabelle ohne createdAt/updatedAt, ein Selbstwiderspruch, ein Pruefer der etwas als plausibel durchwinkte, ein still fehlgeschlagener git add, und zwei Agenten die am Sitzungslimit NACH getaner Arbeit abbrachen (dkv-Executor, gwh-Planer — beide Male lag die Arbeit vollstaendig auf der Platte; git status ist die Wahrheit, nicht der Bericht). Die vollstaendige Liste steht in jedem Planer-Auftrag der spaeten Bereiche. Nebenfunde ohne Mandantenbezug, alle behoben: DKV-Download-Luecke, drohender DKV-Passwortverlust, Selbstloesch-Riegel der nie griff, Startfehler bei Neuinstallation, adminResetPassword ohne Mandanten- und Rollenpruefung, Widget-Besitzriegel bei Favoriten, sechs luegende Kommentare, und fuenf Bereiche ohne jede Testdatei."
|
||||||
}
|
}
|
||||||
+2
-1
@@ -390,6 +390,7 @@ None yet.
|
|||||||
| 260911-e2s | Mandantentrennung Etappe 2, Bereich tenant — von anderer Art: alle 8 Zugriffe gehen auf die Mandantentabelle SELBST, die per Definition keinen Mandanten hat. **'Nichts zu binden' war trotzdem falsch, und der Grund ist der wichtigste Fund seit dem kaputten Helfer in Etappe 1:** drei der acht Zugriffe (`findAll`, `findOne`, `remove` im Controller) zaehlen ueber `include: { _count: { select: { users } } }` in die GESCHUETZTE Tabelle `User` hinein — Prisma 6.19 rendert das als `LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)`, das unter DEREN Regel laeuft. Nach dem Scharfschalten haette die Mandantenliste des Plattform-Admins fuer jeden Mandanten 0 Benutzer gezeigt, und der Loeschriegel T-02-09 waere vakuum geworden (der Fremdschluessel faengt es noch, aber als 500 statt 400). Behoben per Fan-out je Mandant ueber gebundenen Client, Muster aus `UserService.findAllForPlatformAdmin`. **Die Bestandsaufnahme ist fuer Relationszugriffe strukturell blind** — sie sieht nur `this.prisma.<Modell>`, nicht was ein `include:` in eine zweite Tabelle hineinrechnet. Alle 19 `include:`-Stellen und alle `_count`-Stellen einzeln beurteilt, vom Orchestrator UND vom Verifizierer unabhaengig gegengeprueft (der Plan-Pruefer hatte diesen Punkt als 'plausibel' durchgewinkt statt ihn zu pruefen): nur diese drei waren gefaehrlich. Der MECHANISMUS bleibt offen und ist als WINDOWS #27 festgehalten — der Planer wollte keinen Eintrag, weil die Instanz behoben ist; Orchestrator und Verifizierer sahen das anders, weil eine Luecke im Messwerkzeug, die nachweislich einen echten Defekt verborgen hat, genau dafuer ins Ledger gehoert. **Die seit Etappe 1 offene Architekturfrage ist entschieden:** `req.tenantPrisma` wurde bei jeder Anfrage gebaut und NIRGENDS gelesen; neun Bereiche haben die Konvention auf dienst-internes `forTenant()` festgelegt. Middleware geloescht (sie war nirgends registriert — der Auftrag irrte bei `app.module.ts:57`, dort ist der Guard verdrahtet), Guard ohne Prisma-Abhaengigkeit, setzt nur noch `req.tenantId` (22 Leser in 9 Dateien) und den `x-tenant-id`-Wechsel fuer SUPER_ADMIN (4 Frontend-Stellen) — beides erstmals getestet; Guard und Middleware hatten NIE Tests, 'ihre Tests' in Etappe 1 war eine Annahme. Totes Kabel, das wie eine Sicherung aussieht, ist schlimmer als keins. Ausnahmeliste in `rls-access-inventory.spec.ts` geleert und mit Wachhund versehen. Drei Kommentare berichtigt, die `TenantMiddleware`/`req.tenantPrisma` als lebendig beschrieben. Executor fing einen still fehlgeschlagenen `git add` (2 von 7 Dateien) selbst an `git status` und lieferte nach. **Verifiziert 10/10 mit vier eigenhaendigen Falsifizierungen** (Header-Wechsel zweimal gebrochen, Fan-out gebrochen, Wachhund ausgeloest — je exakt die benannten Tests rot; 911/911 Tests, 59 Dateien, Typpruefung sauber, 110/110 Live-Pruefungen; 64 Paare und Klassenverteilung 32/17/13/2 nachgerechnet) | 2026-09-11 | 652e762,11f5731,17dca0d,c8de72e | [260911-e2s-mandantentrennung-etappe-2-bereich-tenan](./quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/) |
|
| 260911-e2s | Mandantentrennung Etappe 2, Bereich tenant — von anderer Art: alle 8 Zugriffe gehen auf die Mandantentabelle SELBST, die per Definition keinen Mandanten hat. **'Nichts zu binden' war trotzdem falsch, und der Grund ist der wichtigste Fund seit dem kaputten Helfer in Etappe 1:** drei der acht Zugriffe (`findAll`, `findOne`, `remove` im Controller) zaehlen ueber `include: { _count: { select: { users } } }` in die GESCHUETZTE Tabelle `User` hinein — Prisma 6.19 rendert das als `LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)`, das unter DEREN Regel laeuft. Nach dem Scharfschalten haette die Mandantenliste des Plattform-Admins fuer jeden Mandanten 0 Benutzer gezeigt, und der Loeschriegel T-02-09 waere vakuum geworden (der Fremdschluessel faengt es noch, aber als 500 statt 400). Behoben per Fan-out je Mandant ueber gebundenen Client, Muster aus `UserService.findAllForPlatformAdmin`. **Die Bestandsaufnahme ist fuer Relationszugriffe strukturell blind** — sie sieht nur `this.prisma.<Modell>`, nicht was ein `include:` in eine zweite Tabelle hineinrechnet. Alle 19 `include:`-Stellen und alle `_count`-Stellen einzeln beurteilt, vom Orchestrator UND vom Verifizierer unabhaengig gegengeprueft (der Plan-Pruefer hatte diesen Punkt als 'plausibel' durchgewinkt statt ihn zu pruefen): nur diese drei waren gefaehrlich. Der MECHANISMUS bleibt offen und ist als WINDOWS #27 festgehalten — der Planer wollte keinen Eintrag, weil die Instanz behoben ist; Orchestrator und Verifizierer sahen das anders, weil eine Luecke im Messwerkzeug, die nachweislich einen echten Defekt verborgen hat, genau dafuer ins Ledger gehoert. **Die seit Etappe 1 offene Architekturfrage ist entschieden:** `req.tenantPrisma` wurde bei jeder Anfrage gebaut und NIRGENDS gelesen; neun Bereiche haben die Konvention auf dienst-internes `forTenant()` festgelegt. Middleware geloescht (sie war nirgends registriert — der Auftrag irrte bei `app.module.ts:57`, dort ist der Guard verdrahtet), Guard ohne Prisma-Abhaengigkeit, setzt nur noch `req.tenantId` (22 Leser in 9 Dateien) und den `x-tenant-id`-Wechsel fuer SUPER_ADMIN (4 Frontend-Stellen) — beides erstmals getestet; Guard und Middleware hatten NIE Tests, 'ihre Tests' in Etappe 1 war eine Annahme. Totes Kabel, das wie eine Sicherung aussieht, ist schlimmer als keins. Ausnahmeliste in `rls-access-inventory.spec.ts` geleert und mit Wachhund versehen. Drei Kommentare berichtigt, die `TenantMiddleware`/`req.tenantPrisma` als lebendig beschrieben. Executor fing einen still fehlgeschlagenen `git add` (2 von 7 Dateien) selbst an `git status` und lieferte nach. **Verifiziert 10/10 mit vier eigenhaendigen Falsifizierungen** (Header-Wechsel zweimal gebrochen, Fan-out gebrochen, Wachhund ausgeloest — je exakt die benannten Tests rot; 911/911 Tests, 59 Dateien, Typpruefung sauber, 110/110 Live-Pruefungen; 64 Paare und Klassenverteilung 32/17/13/2 nachgerechnet) | 2026-09-11 | 652e762,11f5731,17dca0d,c8de72e | [260911-e2s-mandantentrennung-etappe-2-bereich-tenan](./quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/) |
|
||||||
| 260911-fh9 | Mandantentrennung Etappe 2, Bereich auth — die drei in Etappe 1 bewusst ausgelassenen Wege (`getMe`, `changePassword`, `adminResetPassword`) gebunden, alle drei brauchten neue Signaturen (nahmen nur `userId`). Der Anmeldeweg ueber die drei SECURITY-DEFINER-Funktionen NICHT angefasst, per `pg_proc` belegt (weiterhin genau 9 Spalten, auch nachdem die Wegwerf-Tabelle `User` 5 fehlende Spalten bekam). Verbleibende 3 'ungebundene' Stellen sind die `$queryRaw`-Anmeldesuchen, keine Modellzugriffe. **Falle, die der Auftrag selbst gestellt hatte:** Selbstbedienung darf NICHT an `req.tenantId` binden — der Guard laesst SUPER_ADMIN diese Kennung per `x-tenant-id` umschalten (Marktplatz), 'mein Profil' haette ihn sich selbst gegenueber unsichtbar gemacht; gebunden wird an den Mandanten aus dem Sitzungsnachweis (`@CurrentUser().tenantId`), der Controller enthaelt null Verweise auf `req.tenantId`/`x-tenant-id`. **Zwei Loecher in `adminResetPassword` geschlossen, keines davon ein Mandantenproblem:** der Weg pruefte weder den Mandanten des Ziels noch dessen Rolle — ein ADMIN konnte das Passwort eines SUPER_ADMIN ueberschreiben. Beides jetzt dicht, SUPER_ADMIN-Pfad ueber `UserService.findByIdForPlatformAdmin`; `AuthModule` importiert `UserModule`, zyklusfrei. Der Schwesterweg `PATCH /users/:id` hat dieselbe Rollenluecke (T-02-08 prueft nur das ZUWEISEN der Rolle, nicht die bestehende Rolle des Ziels) — ausserhalb der Erlaubnisliste, als WINDOWS #29 festgehalten. **Umgekehrte Fehlerrichtung ist hier leise, nicht laut:** `getMe`-Leere wird zu 200 mit leerem Rumpf, `header.tsx` tut bei `if (u)` nichts — 'nicht angemeldet' und 'Zeile unsichtbar' sind derselbe Wert (WINDOWS #28); `changePassword`-Leere liest sich als `networkError`. Identitaets-Attrappe (ldap-Form) durch asymmetrischen Doppel ersetzt: ungebundener Nachbau ohne Modelle, gebundener ohne `$queryRaw` — beide Grenzen einzeln falsifizierbar. **Verifiziert 8/8** (951/951 Tests, 60 Dateien, Typpruefung sauber, 120/120 Live-Pruefungen; zwei Falsifizierungen vom Pruefer eigenhaendig reproduziert — genau 4 bzw. 2 benannte Tests rot) | 2026-09-11 | 9782bea,92aa8c4,f68beb3 | [260911-fh9-mandantentrennung-etappe-2-bereich-auth-](./quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/) |
|
| 260911-fh9 | Mandantentrennung Etappe 2, Bereich auth — die drei in Etappe 1 bewusst ausgelassenen Wege (`getMe`, `changePassword`, `adminResetPassword`) gebunden, alle drei brauchten neue Signaturen (nahmen nur `userId`). Der Anmeldeweg ueber die drei SECURITY-DEFINER-Funktionen NICHT angefasst, per `pg_proc` belegt (weiterhin genau 9 Spalten, auch nachdem die Wegwerf-Tabelle `User` 5 fehlende Spalten bekam). Verbleibende 3 'ungebundene' Stellen sind die `$queryRaw`-Anmeldesuchen, keine Modellzugriffe. **Falle, die der Auftrag selbst gestellt hatte:** Selbstbedienung darf NICHT an `req.tenantId` binden — der Guard laesst SUPER_ADMIN diese Kennung per `x-tenant-id` umschalten (Marktplatz), 'mein Profil' haette ihn sich selbst gegenueber unsichtbar gemacht; gebunden wird an den Mandanten aus dem Sitzungsnachweis (`@CurrentUser().tenantId`), der Controller enthaelt null Verweise auf `req.tenantId`/`x-tenant-id`. **Zwei Loecher in `adminResetPassword` geschlossen, keines davon ein Mandantenproblem:** der Weg pruefte weder den Mandanten des Ziels noch dessen Rolle — ein ADMIN konnte das Passwort eines SUPER_ADMIN ueberschreiben. Beides jetzt dicht, SUPER_ADMIN-Pfad ueber `UserService.findByIdForPlatformAdmin`; `AuthModule` importiert `UserModule`, zyklusfrei. Der Schwesterweg `PATCH /users/:id` hat dieselbe Rollenluecke (T-02-08 prueft nur das ZUWEISEN der Rolle, nicht die bestehende Rolle des Ziels) — ausserhalb der Erlaubnisliste, als WINDOWS #29 festgehalten. **Umgekehrte Fehlerrichtung ist hier leise, nicht laut:** `getMe`-Leere wird zu 200 mit leerem Rumpf, `header.tsx` tut bei `if (u)` nichts — 'nicht angemeldet' und 'Zeile unsichtbar' sind derselbe Wert (WINDOWS #28); `changePassword`-Leere liest sich als `networkError`. Identitaets-Attrappe (ldap-Form) durch asymmetrischen Doppel ersetzt: ungebundener Nachbau ohne Modelle, gebundener ohne `$queryRaw` — beide Grenzen einzeln falsifizierbar. **Verifiziert 8/8** (951/951 Tests, 60 Dateien, Typpruefung sauber, 120/120 Live-Pruefungen; zwei Falsifizierungen vom Pruefer eigenhaendig reproduziert — genau 4 bzw. 2 benannte Tests rot) | 2026-09-11 | 9782bea,92aa8c4,f68beb3 | [260911-fh9-mandantentrennung-etappe-2-bereich-auth-](./quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/) |
|
||||||
| 260911-gwh | **Mandantentrennung Etappe 2, Bereiche favorites + settings — LETZTER Durchlauf, Etappe 2 abgeschlossen.** 7 `favoriteLink`-Zugriffe und 3 `smtpConfig`-Anfragepfade gebunden; genau ein `smtpConfig`-Zugriff bleibt bewusst offen: der Startpfad, umbenannt in `loadAnySmtpConfigForStartupTransport()` — SECHSTER Fall der Hintergrunddienst-Falle (`findFirst()` ohne Mandanten beim Hochfahren in `mail.module.ts`; heute bedient er einen willkuerlichen Mandanten, nach dem Scharfschalten null), beide Zustaende am Ort, WINDOWS #30. **Befund K geschlossen:** `getDecryptedSmtpConfig(tenantId)` bindet — die Reihenfolgebedingung fuer Etappe 4 aus dem tenders-Lauf ist erfuellt und in Kritikschrift (t4)/(d4) und Klassifikation als erfuellt vermerkt. **Widget-Besitzriegel in `favorites.create()` eingebaut, weil GEMESSEN noetig:** Pruefung 7 zeigt, dass ein gebundenes Anlegen mit fremder `widgetId` GELINGT — die Fremdschluessel-Pruefung umgeht den Zeilenschutz; vom Verifizierer live reproduziert und der Riegel durch Rueckbau falsifiziert (genau 4 Tests rot). Beide Bereiche hatten keine Testdatei fuer ihren Dienst; `favorites.service.spec.ts` (23) und `settings.service.spec.ts` (20) neu, `nodemailer` gemockt. Ledger #31/#32 fuer die stille Leere (leere Favoritenleiste = 'nie etwas gespeichert'; fehlende SMTP-Konfiguration = 'nicht eingerichtet', obwohl die Zugangsdaten da sind). Der Planer scheiterte am Sitzungslimit NACH dem Schreiben des Plans, VOR der Rueckmeldung — Plan lag vollstaendig auf der Platte (1226 Zeilen, Struktur gueltig), vom Orchestrator committet, vom Pruefer als Erstleser gegen den Baum gehalten. **Verifiziert 9/9** (994/994 Tests, 62 Dateien, Typpruefung sauber, 137/137 Live-Pruefungen; Uebersicht 68/178, Klassenverteilung 33+17+13+2=65 und Migrations-Zaehlung 4+3+16=23 vom Pruefer nachgerechnet) | 2026-09-11 | 88896d3,8f2c13a,b5f22e2,1240932 | [260911-gwh-mandantentrennung-etappe-2-bereiche-favo](./quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/) |
|
| 260911-gwh | **Mandantentrennung Etappe 2, Bereiche favorites + settings — LETZTER Durchlauf, Etappe 2 abgeschlossen.** 7 `favoriteLink`-Zugriffe und 3 `smtpConfig`-Anfragepfade gebunden; genau ein `smtpConfig`-Zugriff bleibt bewusst offen: der Startpfad, umbenannt in `loadAnySmtpConfigForStartupTransport()` — SECHSTER Fall der Hintergrunddienst-Falle (`findFirst()` ohne Mandanten beim Hochfahren in `mail.module.ts`; heute bedient er einen willkuerlichen Mandanten, nach dem Scharfschalten null), beide Zustaende am Ort, WINDOWS #30. **Befund K geschlossen:** `getDecryptedSmtpConfig(tenantId)` bindet — die Reihenfolgebedingung fuer Etappe 4 aus dem tenders-Lauf ist erfuellt und in Kritikschrift (t4)/(d4) und Klassifikation als erfuellt vermerkt. **Widget-Besitzriegel in `favorites.create()` eingebaut, weil GEMESSEN noetig:** Pruefung 7 zeigt, dass ein gebundenes Anlegen mit fremder `widgetId` GELINGT — die Fremdschluessel-Pruefung umgeht den Zeilenschutz; vom Verifizierer live reproduziert und der Riegel durch Rueckbau falsifiziert (genau 4 Tests rot). Beide Bereiche hatten keine Testdatei fuer ihren Dienst; `favorites.service.spec.ts` (23) und `settings.service.spec.ts` (20) neu, `nodemailer` gemockt. Ledger #31/#32 fuer die stille Leere (leere Favoritenleiste = 'nie etwas gespeichert'; fehlende SMTP-Konfiguration = 'nicht eingerichtet', obwohl die Zugangsdaten da sind). Der Planer scheiterte am Sitzungslimit NACH dem Schreiben des Plans, VOR der Rueckmeldung — Plan lag vollstaendig auf der Platte (1226 Zeilen, Struktur gueltig), vom Orchestrator committet, vom Pruefer als Erstleser gegen den Baum gehalten. **Verifiziert 9/9** (994/994 Tests, 62 Dateien, Typpruefung sauber, 137/137 Live-Pruefungen; Uebersicht 68/178, Klassenverteilung 33+17+13+2=65 und Migrations-Zaehlung 4+3+16=23 vom Pruefer nachgerechnet) | 2026-09-11 | 88896d3,8f2c13a,b5f22e2,1240932 | [260911-gwh-mandantentrennung-etappe-2-bereiche-favo](./quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/) |
|
||||||
|
| 260911-mkj | **WINDOWS #27 geschlossen — die Bestandsaufnahme sieht jetzt Relationszugriffe.** Vierte Erkennungsform in `rls-access-inventory.spec.ts`: `include:`/`select:`/`_count:` werden ueber `schema.prisma` (zur Testzeit gelesen) auf das Zielmodell aufgeloest und als (Datei, Modell)-Fundstelle gefuehrt, gebunden oder ungebunden je nach umschliessendem Klienten. Zwei Wachhunde, die LAUT werden statt still: Empfaenger ausserhalb der vier Formen (raw vs. matched) und nicht aufloesbare Konstanten — beide vom Verifizierer live gebrochen und rot gesehen. Gemessen mit Prototyp, Vorhersage exakt getroffen: 7 neue Paare, 3 Stand-Aenderungen, eine Klassenaenderung (`ldap-config.service.ts`/`ldapFieldMapping` -> `beides`/`gemischt`, weil `getAllActiveConfigs()` ueber `include: { fieldMappings }` in die geschuetzte Tabelle reicht — genau die #27-Form, bisher unsichtbar, kein neuer Gefahrenfall). Klassifikation 65 -> 72 Paare (35/21/14/2). Acht gepinnte Proben, darunter die beiden #27-Formen (`_count.select.users` auf `this.prisma.tenant` -> `user` ungebunden; auf gebundenem Klienten -> gebunden). **Zwei Dateien standen in KEINER Erkennungsform** (`tenders.seed.ts` mit Client als Funktionsparameter, `backfill-tender-source.ts` mit eigenem `new PrismaClient()`) — heute harmlos, als WINDOWS #33 eigenstaendig festgehalten statt still in #27 mitgeschlossen. Planer fing einen Fehler im eigenen Prototyp (Lookahead beim Schema-Parsen, ohne den alle Listenrelationen am Zeilenende verloren gingen). **Verifiziert 6/6** (1007/1007 Tests, Typpruefung sauber, nur die Spec unter `apps/api/src` angefasst). Info vom Pruefer: der Lookahead ist nicht durch einen eigenen Regressionstest gedeckt — der raw/matched-Wachhund ist der eigentliche Schutz | 2026-09-11 | 5ad23d0,388690f | [260911-mkj-windows-27-schliessen-relations-blindste](./quick/260911-mkj-windows-27-schliessen-relations-blindste/) |
|
||||||
| 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) |
|
| 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) |
|
||||||
| 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) |
|
| 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) |
|
||||||
|
|
||||||
@@ -433,6 +434,6 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
|||||||
|
|
||||||
Last session: 2026-09-11T08:01:13.009Z
|
Last session: 2026-09-11T08:01:13.009Z
|
||||||
Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus.
|
Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus.
|
||||||
Stopped at: **ETAPPE 2 DER MANDANTENTRENNUNG ABGESCHLOSSEN (2026-09-11).** Alle zwoelf Bereiche umgestellt und einzeln verifiziert: ldap 7/7, groups 9/9, tenders 8/9+Fix, dkv 9/9, user 10/10, module-registry 9/9, dashboard 10/11+Fix, calendar 12/12, tenant 10/10, auth 8/8, favorites+settings 9/9; dazu die drei Datenbankregeln (260910-jab, 11/11). Endstand: 994 Tests in 62 Dateien (Ausgang 701/53), 137 Live-Pruefungen gegen die Wegwerf-Datenbank (Ausgang 8), Klassifikation 65 Paare / 68 ungebunden / 178 gebunden — jeder ungebundene Zugriff liegt auf einer plattformglobalen Tabelle oder einem benannten Startpfad, keiner aus Versehen. Alles gepusht. DER SCHALTER IST WEITER AUS. NAECHSTE SCHRITTE: (A) ETAPPE 3 — die zwei Produktentscheidungen des Users vom 2026-09-10: Anmeldenamen pro Mandant (Schema `@@unique([tenantId, username/email])`, Anmeldeweg muss den Mandanten VOR der Suche kennen, SECURITY-DEFINER-Funktionen mit zwei Gleichheitsbedingungen) und Benutzerdimension in den Regeln (zweite Sitzungsvariable `app.current_user`/`current_user_id()`, forTenant() um userId erweitern, Regeln der zehn nutzerbezogenen Tabellen). Dazu Systemkontext fuer die sechs Hintergrunddienst-Faelle (#21, #30 und die vier `beides`-Uebergaben aus tenders/ldap). (B) VOR ETAPPE 4 ZWINGEND: WINDOWS #27 (Relations-Blindstelle der Bestandsaufnahme) schliessen — sonst stuetzt sich die Vorabpruefung auf ein Werkzeug, das `include:`/`_count:` in fremde Tabellen nicht sieht. (C) ETAPPE 4 — Scharfschalten mit rls-preflight.mjs; die Vorabpruefung muss die stillen Leere-Faelle #23/#25/#26/#28/#31/#32 abdecken. DER USER HAT AUSDRUECKLICH GESAGT: beim Scharfschalten anhalten und fragen. Ledger: 14 offen von 32.
|
Stopped at: **ETAPPE 2 DER MANDANTENTRENNUNG ABGESCHLOSSEN (2026-09-11).** Alle zwoelf Bereiche umgestellt und einzeln verifiziert: ldap 7/7, groups 9/9, tenders 8/9+Fix, dkv 9/9, user 10/10, module-registry 9/9, dashboard 10/11+Fix, calendar 12/12, tenant 10/10, auth 8/8, favorites+settings 9/9; dazu die drei Datenbankregeln (260910-jab, 11/11). Endstand: 994 Tests in 62 Dateien (Ausgang 701/53), 137 Live-Pruefungen gegen die Wegwerf-Datenbank (Ausgang 8), Klassifikation 65 Paare / 68 ungebunden / 178 gebunden — jeder ungebundene Zugriff liegt auf einer plattformglobalen Tabelle oder einem benannten Startpfad, keiner aus Versehen. Alles gepusht. DER SCHALTER IST WEITER AUS. NAECHSTE SCHRITTE: (A) ETAPPE 3 — die zwei Produktentscheidungen des Users vom 2026-09-10: Anmeldenamen pro Mandant (Schema `@@unique([tenantId, username/email])`, Anmeldeweg muss den Mandanten VOR der Suche kennen, SECURITY-DEFINER-Funktionen mit zwei Gleichheitsbedingungen) und Benutzerdimension in den Regeln (zweite Sitzungsvariable `app.current_user`/`current_user_id()`, forTenant() um userId erweitern, Regeln der zehn nutzerbezogenen Tabellen). Dazu Systemkontext fuer die sechs Hintergrunddienst-Faelle (#21, #30 und die vier `beides`-Uebergaben aus tenders/ldap). (B) WINDOWS #27 ist GESCHLOSSEN (260911-mkj) — die Bestandsaufnahme sieht Relationszugriffe, 72 Paare. (C) ETAPPE 4 — Scharfschalten mit rls-preflight.mjs; die Vorabpruefung muss die stillen Leere-Faelle #23/#25/#26/#28/#31/#32 abdecken. DER USER HAT AUSDRUECKLICH GESAGT: beim Scharfschalten anhalten und fragen. Ledger: 14 offen von 33. VOLLSTAENDIGER ETAPPE-3-AUFTRAG: docs/mandantentrennung-etappe3-auftrag.md (Reihenfolge 3b Benutzerdimension -> 3a Anmeldenamen -> 3c Systemkontext; 3a enthaelt die eine offene Produktfrage; nichts davon blockiert das Live-Gehen am Dienstag 2026-09-15).
|
||||||
Resume file: None
|
Resume file: None
|
||||||
Last activity: 2026-09-10 - Completed quick task 260910-jab: Die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) geschlossen
|
Last activity: 2026-09-10 - Completed quick task 260910-jab: Die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) geschlossen
|
||||||
|
|||||||
+19
-6
@@ -2,9 +2,9 @@
|
|||||||
schema_version: 1
|
schema_version: 1
|
||||||
open_count: 14
|
open_count: 14
|
||||||
waived_count: 1
|
waived_count: 1
|
||||||
fixed_count: 17
|
fixed_count: 18
|
||||||
total_count: 32
|
total_count: 33
|
||||||
last_updated: 2026-09-11T11:57:50.484Z
|
last_updated: 2026-09-11T14:48:15.447Z
|
||||||
---
|
---
|
||||||
|
|
||||||
# Broken Windows Ledger
|
# Broken Windows Ledger
|
||||||
@@ -41,12 +41,13 @@ last_updated: 2026-09-11T11:57:50.484Z
|
|||||||
| 24 | quick-260910-jab | deviation | apps/api/src/tenders/tender-rss-feed.service.ts | | Was das Schliessen von WINDOWS #19 NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle (TenderRssFeedSource, tenantId NULL) weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil jede Schreibregel (Einfuegen/Aendern/Entfernen) ausdruecklich einen Mandanten verlangt (tenant_insert_policy/tenant_update_policy/tenant_delete_policy, 20260910120000_rls_widen_membership_grant_and_platform_read). Betroffen sind zwei Pfade in TenderRssFeedSourceService: createPlatform() (setzt tenantId=NULL, ein gebundenes INSERT liefe in die WITH-CHECK-Klausel und wuerde abgewiesen) und remove() (deckt fuer Administratoren auch das Entfernen einer plattformweiten Zeile ab; ein gebundenes DELETE traefe sie nie). Beide bleiben deshalb bewusst ungebunden — das ist KEINE Folge dieser Reparatur, sondern bestand bereits vor 260910-jab identisch, weil die vom Ledger vorgegebene #19-Semantik Schreibzugriffe ausdruecklich an einen Mandanten bindet. Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): ein Verwaltungsweg fuer plattformweite Zeilen (Anlegen/Entfernen unter der Anwendungsrolle) muss gebaut werden, BEVOR die Rolle umgeschaltet wird — sonst kann kein Administrator nach dem Scharfschalten mehr eine plattformweite Quelle pflegen. Eigener Eintrag, damit dieser Rest nicht mit #19 verschwindet. | open | | 2026-09-10T12:35:40.000Z | |
|
| 24 | quick-260910-jab | deviation | apps/api/src/tenders/tender-rss-feed.service.ts | | Was das Schliessen von WINDOWS #19 NICHT loest: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle (TenderRssFeedSource, tenantId NULL) weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil jede Schreibregel (Einfuegen/Aendern/Entfernen) ausdruecklich einen Mandanten verlangt (tenant_insert_policy/tenant_update_policy/tenant_delete_policy, 20260910120000_rls_widen_membership_grant_and_platform_read). Betroffen sind zwei Pfade in TenderRssFeedSourceService: createPlatform() (setzt tenantId=NULL, ein gebundenes INSERT liefe in die WITH-CHECK-Klausel und wuerde abgewiesen) und remove() (deckt fuer Administratoren auch das Entfernen einer plattformweiten Zeile ab; ein gebundenes DELETE traefe sie nie). Beide bleiben deshalb bewusst ungebunden — das ist KEINE Folge dieser Reparatur, sondern bestand bereits vor 260910-jab identisch, weil die vom Ledger vorgegebene #19-Semantik Schreibzugriffe ausdruecklich an einen Mandanten bindet. Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): ein Verwaltungsweg fuer plattformweite Zeilen (Anlegen/Entfernen unter der Anwendungsrolle) muss gebaut werden, BEVOR die Rolle umgeschaltet wird — sonst kann kein Administrator nach dem Scharfschalten mehr eine plattformweite Quelle pflegen. Eigener Eintrag, damit dieser Rest nicht mit #19 verschwindet. | open | | 2026-09-10T12:35:40.000Z | |
|
||||||
| 25 | quick-260910-krx | deviation | apps/web/src/lib/stores/dashboard-store.ts | | Die beweisvernichtende Auspraegung der umgekehrten Fehlerrichtung im Bereich dashboard: ein nach dem Scharfschalten (WINDOWS #18) zu klein gebliebenes Leseergebnis auf getLayout sieht nicht wie ein Fehler aus, sondern wie eine leere Vorgabeanordnung. Drei Stellen greifen ineinander (docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich dashboard', (w3)): (1) DashboardService.getLayout liefert bei fehlendem Datensatz {lg:[],md:[],sm:[],xs:[],xxs:[]} statt eines Fehlers; (2) apps/web/src/lib/stores/dashboard-store.ts, loadDashboard setzt layouts/widgets ungeprueft auf das Ergebnis, der catch-Zweig feuert nur bei Netzwerk-/Statusfehlern, nicht bei einer erfolgreichen leeren Antwort; (3) dieselbe Datei, setEditMode(false) schreibt bei isDirty automatisch zurueck, sobald der Bearbeitungsmodus verlassen wird — ohne dass der Nutzer auf Speichern klickt. Die Folge: der Nutzer haelt ein leeres Dashboard fuer einen Fehler des Widget-Systems oder fuer verlorene Einstellungen ('das Widget-System spinnt', 'meine Einstellungen sind weg'), baut seine Anordnung neu auf (addWidget legt echte neue WidgetInstance-Zeilen an, keine Eindeutigkeitsbedingung ueber (userId, widgetType), Dubletten haeufen sich bei wiederholtem Neuaufbau an), und das automatische Zurueckschreiben ueberschreibt die layouts-Spalte der urspruenglichen Zeile — die einzige Aufzeichnung der urspruenglichen Anordnung ist verloren, bevor irgendjemand die Ursache untersuchen konnte. Zusaetzlich, kleiner: apps/web/src/components/dashboard/widgets/search-widget.tsx laesst bei einem zu kleinen custom-Ergebnis die eigenen Suchmaschinen des Nutzers aus der Auswahlliste verschwinden (der Rueckfallzweig auf DEFAULT_PROVIDERS feuert nie, weil getSearchProviders die drei Vorgaben immer voranstellt), und handleSearch faellt bei unbekannter Auswahl auf providers[0] (Google) zurueck — eine fuer ein internes Werkzeug gedachte Suchanfrage ginge dann an eine externe Suchmaschine. Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): eine physisch vorhandene DashboardLayout-Zeile fuer einen bekannten Benutzer, aber der gebundene Lesezugriff fuer dessen Mandanten liefert null — das unterscheidet den echten Erstbenutzer-Fall vom Trennungsfehler. An dieselbe Bedingung gebunden wie #18 — beobachtbar erst nach dem Scharfschalten. Das Frontend wird von 260910-krx NICHT geaendert, dieser Eintrag beschreibt es nur. Die verwandte, strukturelle Eindeutigkeitsfrage von DashboardLayout.userId (plattformweit @unique ohne Mandantenanteil) ist derselbe Fall wie WINDOWS #22 im Bereich user — dort mitgefuehrt, kein zweiter Eintrag hier. | open | | 2026-09-11T09:01:00.000Z | |
|
| 25 | quick-260910-krx | deviation | apps/web/src/lib/stores/dashboard-store.ts | | Die beweisvernichtende Auspraegung der umgekehrten Fehlerrichtung im Bereich dashboard: ein nach dem Scharfschalten (WINDOWS #18) zu klein gebliebenes Leseergebnis auf getLayout sieht nicht wie ein Fehler aus, sondern wie eine leere Vorgabeanordnung. Drei Stellen greifen ineinander (docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich dashboard', (w3)): (1) DashboardService.getLayout liefert bei fehlendem Datensatz {lg:[],md:[],sm:[],xs:[],xxs:[]} statt eines Fehlers; (2) apps/web/src/lib/stores/dashboard-store.ts, loadDashboard setzt layouts/widgets ungeprueft auf das Ergebnis, der catch-Zweig feuert nur bei Netzwerk-/Statusfehlern, nicht bei einer erfolgreichen leeren Antwort; (3) dieselbe Datei, setEditMode(false) schreibt bei isDirty automatisch zurueck, sobald der Bearbeitungsmodus verlassen wird — ohne dass der Nutzer auf Speichern klickt. Die Folge: der Nutzer haelt ein leeres Dashboard fuer einen Fehler des Widget-Systems oder fuer verlorene Einstellungen ('das Widget-System spinnt', 'meine Einstellungen sind weg'), baut seine Anordnung neu auf (addWidget legt echte neue WidgetInstance-Zeilen an, keine Eindeutigkeitsbedingung ueber (userId, widgetType), Dubletten haeufen sich bei wiederholtem Neuaufbau an), und das automatische Zurueckschreiben ueberschreibt die layouts-Spalte der urspruenglichen Zeile — die einzige Aufzeichnung der urspruenglichen Anordnung ist verloren, bevor irgendjemand die Ursache untersuchen konnte. Zusaetzlich, kleiner: apps/web/src/components/dashboard/widgets/search-widget.tsx laesst bei einem zu kleinen custom-Ergebnis die eigenen Suchmaschinen des Nutzers aus der Auswahlliste verschwinden (der Rueckfallzweig auf DEFAULT_PROVIDERS feuert nie, weil getSearchProviders die drei Vorgaben immer voranstellt), und handleSearch faellt bei unbekannter Auswahl auf providers[0] (Google) zurueck — eine fuer ein internes Werkzeug gedachte Suchanfrage ginge dann an eine externe Suchmaschine. Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): eine physisch vorhandene DashboardLayout-Zeile fuer einen bekannten Benutzer, aber der gebundene Lesezugriff fuer dessen Mandanten liefert null — das unterscheidet den echten Erstbenutzer-Fall vom Trennungsfehler. An dieselbe Bedingung gebunden wie #18 — beobachtbar erst nach dem Scharfschalten. Das Frontend wird von 260910-krx NICHT geaendert, dieser Eintrag beschreibt es nur. Die verwandte, strukturelle Eindeutigkeitsfrage von DashboardLayout.userId (plattformweit @unique ohne Mandantenanteil) ist derselbe Fall wie WINDOWS #22 im Bereich user — dort mitgefuehrt, kein zweiter Eintrag hier. | open | | 2026-09-11T09:01:00.000Z | |
|
||||||
| 26 | quick-260911-cwh | deviation | apps/web/src/components/dashboard/widgets/calendar-widget.tsx | | Bereich calendar: zu kleines Leseergebnis auf getSources/fetchAndCacheEvents sieht aus wie 'keine Quelle eingerichtet' bzw. 'keine Termine' (calendar-widget.tsx, calendar-settings-panel.tsx); das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand (calendar-widget.tsx catch->setEvents([]), calendar-settings-panel.tsx .catch(()=>{}) auf fetchSources); der Nutzer legt seine Quelle neu an und tippt Exchange-/CalDAV-Zugangsdaten ein zweites Mal in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt unsichtbar liegen und wird nach Behebung zur Dublette; Vorabpruefung fuer Etappe 4: physisch vorhandene CalendarSource-Zeilen je Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung vergleichen (docs/mandantentrennung-etappe2-fehlerrichtung.md (k4)(e)); an dieselbe Bedingung gebunden wie WINDOWS #18; Familie mit #23 (module-registry) und #25 (dashboard); das Frontend wird von 260911-cwh NICHT geaendert. | open | | 2026-09-11T07:57:36.769Z | |
|
| 26 | quick-260911-cwh | deviation | apps/web/src/components/dashboard/widgets/calendar-widget.tsx | | Bereich calendar: zu kleines Leseergebnis auf getSources/fetchAndCacheEvents sieht aus wie 'keine Quelle eingerichtet' bzw. 'keine Termine' (calendar-widget.tsx, calendar-settings-panel.tsx); das Frontend verschluckt zusaetzlich LAUTE Fehler derselben Pfade in denselben leeren Zustand (calendar-widget.tsx catch->setEvents([]), calendar-settings-panel.tsx .catch(()=>{}) auf fetchSources); der Nutzer legt seine Quelle neu an und tippt Exchange-/CalDAV-Zugangsdaten ein zweites Mal in ein scheinbar defektes System ein, die urspruengliche Zeile bleibt unsichtbar liegen und wird nach Behebung zur Dublette; Vorabpruefung fuer Etappe 4: physisch vorhandene CalendarSource-Zeilen je Mandant ueber die Wartungsrolle zaehlen und mit der gebundenen Zaehlung vergleichen (docs/mandantentrennung-etappe2-fehlerrichtung.md (k4)(e)); an dieselbe Bedingung gebunden wie WINDOWS #18; Familie mit #23 (module-registry) und #25 (dashboard); das Frontend wird von 260911-cwh NICHT geaendert. | open | | 2026-09-11T07:57:36.769Z | |
|
||||||
| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | open | | 2026-09-11T09:08:00.435Z | |
|
| 27 | 2 | unmet-truth | apps/api/src/prisma/rls-access-inventory.spec.ts | | Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht. | fixed | | 2026-09-11T09:08:00.435Z | 2026-09-11T14:48:15.447Z |
|
||||||
| 28 | quick-260911-fh9 | deviation | apps/web/src/components/layout/header.tsx | | Bereich auth: getMe liefert nach dem Scharfschalten null, der Controller antwortet 200 mit leerem Rumpf, fetchCurrentUser (auth-actions.ts) macht daraus null, header.tsx und account-settings-form.tsx tun bei null nichts — die Portalhuelle rendert ohne angemeldeten Benutzer; changePassword liest sich als networkError (nicht als falsches Kennwort); adminResetPassword als 'User not found' ohne UI-Aufrufer. 'nicht angemeldet' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert — an dieselbe Bedingung gebunden wie WINDOWS #18; Familie #23/#25/#26; Etappe-4-Vorabpruefung: bekannten Benutzer ueber die Wartungsrolle lesen und den gebundenen findUnique unter seinem Claim-Mandanten daneben halten. Das Frontend wird von 260911-fh9 NICHT geaendert. | open | | 2026-09-11T10:00:29.558Z | |
|
| 28 | quick-260911-fh9 | deviation | apps/web/src/components/layout/header.tsx | | Bereich auth: getMe liefert nach dem Scharfschalten null, der Controller antwortet 200 mit leerem Rumpf, fetchCurrentUser (auth-actions.ts) macht daraus null, header.tsx und account-settings-form.tsx tun bei null nichts — die Portalhuelle rendert ohne angemeldeten Benutzer; changePassword liest sich als networkError (nicht als falsches Kennwort); adminResetPassword als 'User not found' ohne UI-Aufrufer. 'nicht angemeldet' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert — an dieselbe Bedingung gebunden wie WINDOWS #18; Familie #23/#25/#26; Etappe-4-Vorabpruefung: bekannten Benutzer ueber die Wartungsrolle lesen und den gebundenen findUnique unter seinem Claim-Mandanten daneben halten. Das Frontend wird von 260911-fh9 NICHT geaendert. | open | | 2026-09-11T10:00:29.558Z | |
|
||||||
| 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | open | | 2026-09-11T10:00:38.418Z | |
|
| 29 | quick-260911-fh9 | unmet-truth | apps/api/src/user/user.controller.ts | | Bereich auth: adminResetPassword schliesst die Rechteausweitung ADMIN -> SUPER_ADMIN im eigenen Handler (T-FH9-04), der Schwesterweg PATCH /users/:id tut das nicht. UserController.update (T-02-08) prueft nur, ob die Rolle SUPER_ADMIN NEU ZUGEWIESEN wird (dto.role === Role.SUPER_ADMIN), nicht ob das ZIEL diese Rolle bereits HAT — password/isActive gehen fuer ein bestehendes SUPER_ADMIN-Ziel ungeprueft durch; remove prueft ueberhaupt keine Rolle des Ziels, nur Selbstloeschung und Mandantengrenze. Kein Mandantenproblem, sondern Rechteausweitung INNERHALB des Mandanten. Reparatur in einem Satz: die Rolle des ZIELS pruefen (ist user.role === Role.SUPER_ADMIN und der Aufrufer nicht SUPER_ADMIN, ForbiddenException) — die Vorlage steht seit 260911-fh9 in AuthService.adminResetPassword. Ausserhalb der Erlaubnisliste dieses Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem zweiten Administrator zu schliessen. | open | | 2026-09-11T10:00:38.418Z | |
|
||||||
| 30 | quick-260911-gwh | deviation | apps/api/src/mail/mail.module.ts | | Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a). | open | | 2026-09-11T11:57:36.656Z | |
|
| 30 | quick-260911-gwh | deviation | apps/api/src/mail/mail.module.ts | | Startpfad des Mailmoduls (SettingsService.loadAnySmtpConfigForStartupTransport(), vormals getStartupSmtpConfig()) als SECHSTER Fall der Hintergrunddienst-Falle bleibt bewusst UNGEBUNDEN. HEUTE bereits falsch: findFirst() ohne Bedingung zieht bei mehreren Mandanten den SMTP-Server und Absender EINES beliebigen Mandanten fuer ALLE Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten (T-GWH-03, Nutzung fremder Zugangsdaten). NACH DEM SCHARFSCHALTEN (#18) liefert dieselbe Abfrage null, mail.module.ts faellt auf MAIL_*/TESSERA_SMTP_*/localhost:1025 zurueck, MailService faengt den Transportfehler (T-02-12) -- KEINE Protokollzeile, das Verstummen ist doppelt verdeckt (Unsymmetrie zu ldap.getAllActiveConfigs [heute korrekt] UND zu dkv WINDOWS #21 [verstummt mit Protokollzeile]). Drei erwogene Formen: an einen aufgeloesten Mandanten binden (unmoeglich, kein Kontext beim Start); Mehrmandanten-Versand (abgelehnt als Funktion -- Vorlage steht in DkvMailService/TenderMailService, Transport je Versand aus getDecryptedSmtpConfig(tenantId)); als benannte Altlast weiterfuehren mit Markierung (GEWAEHLT). Eigener Eintrag statt Anschluss an #21: andere Datei, andere Reparatur, andere Verdeckungsform. Signal fuer rls-preflight.mjs gehoert in Etappe 4. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s4)(a). | open | | 2026-09-11T11:57:36.656Z | |
|
||||||
| 31 | quick-260911-gwh | deviation | apps/web/src/components/dashboard/widgets/favorites-widget.tsx | | Bereich favorites: ein nach dem Scharfschalten (#18) zu klein gebliebenes Leseergebnis auf list() sieht aus wie 'Noch keine Favoriten.' (favorites-widget.tsx Zeile um 212, de.json favorites.empty) -- fetchFavorites (favorites-api.ts) reicht die leere Liste durch. 'Nie einen gespeichert' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert []. Etappe-4-Vorabpruefung (f4)(d): fuer einen bekannten Nutzer/Widget die Favoritenzahl ueber die Wartungsrolle und ueber den gebundenen findMany daneben halten. Familie #23/#25/#26/#28. Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich favorites' (f3)/(f4). | open | | 2026-09-11T11:57:50.276Z | |
|
| 31 | quick-260911-gwh | deviation | apps/web/src/components/dashboard/widgets/favorites-widget.tsx | | Bereich favorites: ein nach dem Scharfschalten (#18) zu klein gebliebenes Leseergebnis auf list() sieht aus wie 'Noch keine Favoriten.' (favorites-widget.tsx Zeile um 212, de.json favorites.empty) -- fetchFavorites (favorites-api.ts) reicht die leere Liste durch. 'Nie einen gespeichert' und 'Zeile unsichtbar' sind fuer das Frontend derselbe Wert []. Etappe-4-Vorabpruefung (f4)(d): fuer einen bekannten Nutzer/Widget die Favoritenzahl ueber die Wartungsrolle und ueber den gebundenen findMany daneben halten. Familie #23/#25/#26/#28. Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich favorites' (f3)/(f4). | open | | 2026-09-11T11:57:50.276Z | |
|
||||||
| 32 | quick-260911-gwh | deviation | apps/web/src/components/settings/smtp-settings-form.tsx | | Bereich settings: getSmtpConfig liefert nach dem Scharfschalten (#18) 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 SmtpConfig_tenantId_key (PrismaClientUnknownRequestError, gemessen in Aufgabe 1 Pruefung 8) -- nach diesem Lauf ist saveSmtpConfig gebunden und trifft die eigene Zeile, dieser Rest bestand nur unter der ungebundenen Form vor dieser Aenderung. Dieselbe 200-leerer-Rumpf-Kette wie #28. Etappe-4-Vorabpruefung (s4)(e). Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s3)/(s4). | open | | 2026-09-11T11:57:50.484Z | |
|
| 32 | quick-260911-gwh | deviation | apps/web/src/components/settings/smtp-settings-form.tsx | | Bereich settings: getSmtpConfig liefert nach dem Scharfschalten (#18) 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 SmtpConfig_tenantId_key (PrismaClientUnknownRequestError, gemessen in Aufgabe 1 Pruefung 8) -- nach diesem Lauf ist saveSmtpConfig gebunden und trifft die eigene Zeile, dieser Rest bestand nur unter der ungebundenen Form vor dieser Aenderung. Dieselbe 200-leerer-Rumpf-Kette wie #28. Etappe-4-Vorabpruefung (s4)(e). Das Frontend wird von 260911-gwh NICHT geaendert. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich settings' (s3)/(s4). | open | | 2026-09-11T11:57:50.484Z | |
|
||||||
|
| 33 | quick-260911-mkj | unmet-truth | apps/api/src/tenders/tenders.seed.ts | | Modellaufrufe auf Empfaengern, die weder this.prisma noch eine const X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: tenders/tenders.seed.ts (Funktionsparameter prisma: PrismaService, tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) und tenders/backfill-tender-source.ts (eigenstaendiges Skript mit new PrismaClient(), tender.findMany/update, durch RELATION_SPEC_EXCEPTIONS laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER <Kennung>.<Modell>.<Operation>(-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut. | open | | 2026-09-11T14:48:09.723Z | |
|
||||||
|
|
||||||
````json
|
````json
|
||||||
[
|
[
|
||||||
@@ -369,10 +370,10 @@ last_updated: 2026-09-11T11:57:50.484Z
|
|||||||
"file": "apps/api/src/prisma/rls-access-inventory.spec.ts",
|
"file": "apps/api/src/prisma/rls-access-inventory.spec.ts",
|
||||||
"line": null,
|
"line": null,
|
||||||
"description": "Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht.",
|
"description": "Die maschinelle Bestandsaufnahme (rls-access-inventory.spec.ts) ist fuer Relationszugriffe strukturell blind. Sie erkennt nur direkte Zugriffe der Form this.prisma.<Modell> bzw. <gebundener Client>.<Modell>. Ein Zugriff, der ueber include:/_count:/select: in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar — obwohl Prisma daraus eine Unterabfrage auf diese zweite Tabelle macht, die unter DEREN Regel laeuft. Nachgewiesen in 260911-e2s: drei Zugriffe in tenant.controller.ts zaehlten ueber include: { _count: { select: { users } } } in die geschuetzte Tabelle User hinein (Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...)); nach dem Scharfschalten haette die Mandantenliste des Plattform-Administrators fuer jeden Mandanten 0 Benutzer gezeigt und der Loeschriegel T-02-09 waere vakuum geworden. Diese drei Stellen sind behoben (Fan-out je Mandant ueber gebundenen Client). Zur Planungszeit wurden alle 19 include:-Stellen und alle _count-Stellen in apps/api/src einzeln beurteilt, vom Orchestrator und vom Verifizierer unabhaengig gegengeprueft: nur diese drei waren gefaehrlich (tenders zaehlt auf dem plattformglobalen Katalog ohne Zeilenschutz, groups zaehlt ueber einen bereits gebundenen Client in eine Tabelle desselben Mandanten). OFFEN bleibt der MECHANISMUS: jede kuenftige include:/_count:-Stelle in eine fremd geschuetzte Tabelle bleibt fuer die Pruefung unsichtbar. Zu schliessen, indem der Detektor include:/select:/_count:-Bloecke auf Modellnamen durchsucht und die Zieltabelle als eigene Fundstelle fuehrt — oder durch eine Pruefung, die jede include:-Stelle einer expliziten Freigabeliste unterwirft. Gehoert vor das Scharfschalten (Etappe 4), weil die Vorabpruefung sich sonst auf eine Bestandsaufnahme stuetzt, die diese Form nicht sieht.",
|
||||||
"status": "open",
|
"status": "fixed",
|
||||||
"reason": "",
|
"reason": "",
|
||||||
"recorded_at": "2026-09-11T09:08:00.435Z",
|
"recorded_at": "2026-09-11T09:08:00.435Z",
|
||||||
"resolved_at": null
|
"resolved_at": "2026-09-11T14:48:15.447Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 28,
|
"id": 28,
|
||||||
@@ -433,6 +434,18 @@ last_updated: 2026-09-11T11:57:50.484Z
|
|||||||
"reason": "",
|
"reason": "",
|
||||||
"recorded_at": "2026-09-11T11:57:50.484Z",
|
"recorded_at": "2026-09-11T11:57:50.484Z",
|
||||||
"resolved_at": null
|
"resolved_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"kind": "unmet-truth",
|
||||||
|
"phase": "quick-260911-mkj",
|
||||||
|
"file": "apps/api/src/tenders/tenders.seed.ts",
|
||||||
|
"line": null,
|
||||||
|
"description": "Modellaufrufe auf Empfaengern, die weder this.prisma noch eine const X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: tenders/tenders.seed.ts (Funktionsparameter prisma: PrismaService, tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) und tenders/backfill-tender-source.ts (eigenstaendiges Skript mit new PrismaClient(), tender.findMany/update, durch RELATION_SPEC_EXCEPTIONS laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER <Kennung>.<Modell>.<Operation>(-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut.",
|
||||||
|
"status": "open",
|
||||||
|
"reason": "",
|
||||||
|
"recorded_at": "2026-09-11T14:48:09.723Z",
|
||||||
|
"resolved_at": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
````
|
````
|
||||||
|
|||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260911-mkj
|
||||||
|
plan: 01
|
||||||
|
subsystem: testing
|
||||||
|
tags: [prisma, rls, multi-tenancy, static-analysis, vitest]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: quick-260911-e2s
|
||||||
|
provides: "Bestandsaufnahme-Grundgeruest (rls-access-inventory.spec.ts, drei Erkennungsformen), WINDOWS #27 aufgedeckt"
|
||||||
|
provides:
|
||||||
|
- "Vierte Erkennungsform in rls-access-inventory.spec.ts: Relationszugriffe (include:/select:/_count:/Relationsfilter) werden ueber schema.prisma auf ihr Zielmodell aufgeloest und als eigene Fundstelle gefuehrt"
|
||||||
|
- "72 (Datei, Modell)-Paare in der Bestandsaufnahme (65 -> 72, sieben neue, drei fortgeschriebene Staende)"
|
||||||
|
- "WINDOWS #27 fixed mit Nachweis; neuer Ledger-Eintrag #33 fuer die von allen vier Formen unerreichbare Restmenge (tenders.seed.ts, backfill-tender-source.ts)"
|
||||||
|
affects: [etappe-4-scharfschalten, rls-preflight]
|
||||||
|
|
||||||
|
actuals:
|
||||||
|
tokens: 19657
|
||||||
|
tasks: 2
|
||||||
|
commits: 2
|
||||||
|
plan_head_before: 926359b067596b8d627660d926831b885e74d8d9
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Kontextstapel-Parser (scanRelationKeys) fuer verschachtelte Prisma-Query-Objekte, aufgeloest gegen ein zur Testzeit geparstes schema.prisma"
|
||||||
|
- "String-Blanking (blankStringLiterals) fuer klammertiefen-sichere Argumentbereichs-Erkennung, getrennt von der unveraenderten Kommentarfrei-Quelle der Formen 1-3"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- apps/api/src/prisma/rls-access-inventory.spec.ts
|
||||||
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||||
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||||
|
- .planning/WINDOWS.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Vierte Erkennung schreibt Relationsziele direkt in unboundModels/boundModels (Klientenname = Modellname mit kleinem Anfangsbuchstaben) statt eines eigenen Fundstellentyps, damit findAccessSites/computeStandByKey und die bestehenden Vergleichstests unveraendert bleiben"
|
||||||
|
- "ldap-config.service.ts/ldapFieldMapping wechselt von muss-mandantengebunden auf beides — der Elternpfad getAllActiveConfigs() ist bereits als Hintergrunddienst-Fall an Etappe 3 uebergeben, kein neuer Befund"
|
||||||
|
- "RELATION_SPEC_EXCEPTIONS bleibt schmal (eine Datei, backfill-tender-source.ts); die zweite unerreichbare Datei (tenders.seed.ts, kein Relationszugriff) wird stattdessen als eigener Ledger-Eintrag #33 gefuehrt, nicht stillschweigend mit #27 mitgeschlossen"
|
||||||
|
|
||||||
|
requirements-completed: [WINDOWS-27, ETAPPE-4-VORAUSSETZUNG]
|
||||||
|
|
||||||
|
duration: ~45min
|
||||||
|
completed: 2026-09-11
|
||||||
|
status: complete
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase quick-260911-mkj: WINDOWS #27 schliessen — Relations-Blindstelle Summary
|
||||||
|
|
||||||
|
**Vierte Erkennungsform in `rls-access-inventory.spec.ts` macht Relationszugriffe (`include:`/`select:`/`_count:`/Relationsfilter) sichtbar, die Prisma als Unterabfrage auf eine zweite Tabelle unter DEREN Regel rendert — Bestandsaufnahme waechst von 65 auf 72 Paare, WINDOWS #27 ist geschlossen.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~45 min
|
||||||
|
- **Completed:** 2026-09-11
|
||||||
|
|
||||||
|
## Nachweis WINDOWS #27
|
||||||
|
|
||||||
|
**Zwischenmessung (Aufgabe 1, VOR dem Nachziehen der Bestandsaufnahme) — woertlich, wie im Plan verlangt:**
|
||||||
|
|
||||||
|
```
|
||||||
|
× jede im Quelltext gefundene (Datei, Modell)-Fundstelle ist im Dokument eingetragen
|
||||||
|
Fehlende Eintraege im Dokument:
|
||||||
|
apps/api/src/groups/module-grants.service.ts::module
|
||||||
|
apps/api/src/ldap/ldap-config.service.ts::tenant
|
||||||
|
apps/api/src/module-registry/module-access.service.ts::group
|
||||||
|
apps/api/src/module-registry/module-access.service.ts::groupMembership
|
||||||
|
apps/api/src/tenders/tender-digest.scheduler.ts::tender
|
||||||
|
apps/api/src/tenders/tender-digest.scheduler.ts::tenderSavedSearch
|
||||||
|
apps/api/src/tenders/tenders.controller.ts::tenderSource
|
||||||
|
|
||||||
|
× der eingetragene Stand stimmt mit dem im Quelltext gemessenen ueberein
|
||||||
|
Abweichender Stand (Dokument vs. Quelltext):
|
||||||
|
apps/api/src/ldap/ldap-config.service.ts::ldapFieldMapping — dokumentiert=gebunden, gemessen=gemischt
|
||||||
|
apps/api/src/module-registry/module-registry.service.ts::module — dokumentiert=ungebunden, gemessen=gemischt
|
||||||
|
apps/api/src/tenders/tender-matching.service.ts::tender — dokumentiert=ungebunden, gemessen=gemischt
|
||||||
|
|
||||||
|
Test Files 1 failed (1)
|
||||||
|
Tests 2 failed | 22 passed (24)
|
||||||
|
```
|
||||||
|
|
||||||
|
Das ist EXAKT die zur Planungszeit gemessene Untergrenze: 7 fehlende Paare,
|
||||||
|
3 Stand-Abweichungen. Der Detektor SIEHT die Relationszugriffe, bevor das
|
||||||
|
Dokument nachgezogen ist — der geforderte Beweis der Wirkung.
|
||||||
|
|
||||||
|
**Zahlen (Aufgabe 1/2, aus der Ausgabe von `rls-access-inventory.spec.ts` und den Gates entnommen, nicht geschaetzt):**
|
||||||
|
|
||||||
|
- 7 neue Paare, 3 fortgeschriebene Staende, davon 1 Klassenwechsel (`ldap-config.service.ts`/`ldapFieldMapping`: `muss-mandantengebunden` → `beides`)
|
||||||
|
- 1 Waechter-(a)-Ausnahme (`RELATION_SPEC_EXCEPTIONS`: `apps/api/src/tenders/backfill-tender-source.ts`), 0 Waechter-(b)-Verstoesse
|
||||||
|
- Bestandsaufnahme: 65 → **72 Paare** (35 muss-mandantengebunden, 21 keine-mandantengebundene-tabelle, 14 beides, 2 bewusst-uebergreifend — exakt die zur Planungszeit projizierte Verteilung)
|
||||||
|
- Acht gepinnte Proben, darunter A (WINDOWS #27 ungebunden) und B (WINDOWS #27 gebunden, derselbe Aufruf auf einem `forTenant(`-Klienten) — beide bestanden
|
||||||
|
- Gesamtsuite: **1007/1007 Tests, 62/62 Dateien gruen** (Baseline 994/62); Typpruefung sauber; `rls-scratch-check.mjs` **137/137** bestanden
|
||||||
|
- WINDOWS-Ledger: #27 `fixed`; neuer Eintrag **#33** fuer die von allen vier Erkennungsformen unerreichbare Restmenge (`tenders/tenders.seed.ts`, `tenders/backfill-tender-source.ts`). Kopf nach diesem Lauf: `open_count: 14`, `total_count: 33`.
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- **Task 1** (`test(quick-260911-mkj): vierte Erkennungsform fuer Relationszugriffe, WINDOWS #27`, Commit `5ad23d0`):
|
||||||
|
- `analyzeFile` in `analyzeSource(source, relPath)` herausgeloest (testbar gegen Probestrings)
|
||||||
|
- `parseSchemaRelations()` liest `schema.prisma` zur Testzeit, `SCHEMA_RELATIONS` (Map Modell -> Map Feld -> Zielmodell), `CLIENT_NAME_TO_MODEL` fuer den Anker; Lookahead `(?=\s|$)` bewusst gesetzt (ohne ihn verliert die Feldzeilen-Regex jede Listenrelation am Zeilenende — der im Plan benannte erste Prototyp-Fehler, hier vermieden)
|
||||||
|
- Vierte Erkennung: Ankerregex nur ueber bekannte Empfaenger (`this.prisma`, `boundNames`, Transaktionsparameter beider Formen), Argumentbereich per Klammertiefe auf einer string-geblankten Kopie (`blank`), Wertform-Aufloesung gegen gleichnamige Konstanten derselben Datei, Kontextstapel-Scan (`scanRelationKeys`) fuer verschachtelte Relationsfelder inkl. `_count: true`
|
||||||
|
- Drei neue Waechter (Rohzahl vs. erkannte Modellaufrufe, Stale-Check `RELATION_SPEC_EXCEPTIONS`, `unresolvedRelationSpecValues` leer), zwei Schema-Tests, acht gepinnte Proben — alle 24 Tests der Spec-Datei gruen
|
||||||
|
- Bestandsaufnahme fortgeschrieben: sieben neue Zeilen, drei geaenderte Staende, Kopfabsatz "Erkennungsluecke GESCHLOSSEN" ersetzt den alten "seit 260911-e2s vermessen"-Absatz
|
||||||
|
- **Task 2** (`docs(quick-260911-mkj): Abschnitte abgeleitet, WINDOWS #27 geschlossen, Restmenge als eigener Eintrag`, Commit `388690f`):
|
||||||
|
- Klassifikationsdokument: Uebersichtsabsatz ("Zweite methodische Luecke ... geschlossen"), Klassen-Verteilung (Ueberschrift + Summenzeile + Tabelle, aus der Bestandsaufnahme GEZAEHLT: 35/21/14/2/72), Hintergrunddienst-Nachtrag zum ersten Fall (`getAllActiveConfigs()` reicht auch in `LdapFieldMapping` hinein — kein neuer Fall, Ueberschrift bleibt "sechs Faelle")
|
||||||
|
- Kritikschrift: `Nachtrag (260911-mkj)` unter `(n4)` im `## Bereich tenant` (verweist auf Befund G/(b) und die Restmenge), Vermerk im `## Etappe 2 — Abschluss`, dass #27 geschlossen ist
|
||||||
|
- Ledger: neuer Eintrag `#33` angelegt (Ledger fuer die Restmenge), dann `#27` auf `fixed` gesetzt, dann der `WINDOWS #TBD-MKJ`-Platzhalter im Spec-Kommentar durch `WINDOWS #33` ersetzt
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
None — plan executed exactly as specified for the actual detector/document logic.
|
||||||
|
|
||||||
|
### Documented Drift (nicht auto-fixed, nicht Teil dieses Plans)
|
||||||
|
|
||||||
|
**1. [Vorbestehende Abweichung, nicht dieser Plan] `docs/mandantentrennung-etappe3-auftrag.md` und Teile von `.planning/HANDOFF.json` weichen bereits VOR Beginn dieser Ausfuehrung von der Basis `cc26197` ab.**
|
||||||
|
- **Gefunden bei:** dem Allowlist-Diff-Gate in Aufgabe 1 (`git diff --name-only cc26197`)
|
||||||
|
- **Ursache:** Commit `926359b` ("docs: Auftrag fuer Etappe 3 der Mandantentrennung, gemessen statt erinnert", Autor Schalli) landete auf `main`, BEVOR diese Ausfuehrung begann — die im Auftrag genannte Basis-HEAD `261e736` war zu diesem Zeitpunkt bereits um einen fremden Commit veraltet.
|
||||||
|
- **Pruefung:** `git show --stat 926359b` zeigt ausschliesslich `docs/mandantentrennung-etappe3-auftrag.md` (neu) und `.planning/HANDOFF.json` — nichts unter `apps/api/src`, kein Schema, keine Migration, keine Compose-/Umgebungsdatei. Ausserhalb des Geltungsbereichs dieses Plans, nicht angefasst.
|
||||||
|
- **Auswirkung auf die Verify-Gates:** das im Plan definierte Allowlist-Gate (`git diff --name-only cc26197` gegen eine feste Dateiliste) meldet `docs/mandantentrennung-etappe3-auftrag.md` als "unerwartete Aenderung", weil es diesen fremden, bereits gemergten Commit mitzaehlt. Die tatsaechlich von dieser Ausfuehrung veraenderten Dateien sind ausschliesslich die vier oben genannten (bestaetigt durch `git status --short` vor jedem Commit und durch die beiden Commits `5ad23d0`/`388690f` selbst — je genau die im Plan als Deliverables genannten Dateien, keine mehr).
|
||||||
|
- **Nicht behoben, weil:** ausserhalb der Erlaubnisliste dieses Plans (nur die Spec-Datei, `apps/api/prisma/**`, zwei benannte Dokumente, `.planning/`) — ein fremder, bereits abgeschlossener Commit gehoert nicht in diesen Auftrag.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
None — dieser Plan aendert ausschliesslich Testcode und Dokumentation, keine UI/keine Laufzeitpfade.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None — die Aenderungen liegen vollstaendig innerhalb des im Plan definierten `threat_model` (T-MKJ-01 bis T-MKJ-05), keine neue Angriffsflaeche ausserhalb dessen.
|
||||||
|
|
||||||
|
## BEFUND — nicht behoben
|
||||||
|
|
||||||
|
Keiner. Die Zwischenmessung lieferte exakt 7 fehlende Paare / 3 Stand-Abweichungen (die geplante Untergrenze, keine zusaetzlichen ungenannten Funde); jedes der sieben neuen Paare wurde einzeln am Quelltext geprueft (siehe Zeilenverweise in der Bestandsaufnahme-Tabelle) und ist entweder `keine-mandantengebundene-tabelle` (Elternbindung wirkungslos, nicht schaedlich) oder `muss-mandantengebunden`/`gebunden` (Relationsfilter auf einem bereits gebundenen Klienten in eine Tabelle desselben Mandanten). Keine ungebundene Einbindung in eine geschuetzte Tabelle auf ungeschuetztem Elternpfad, die nicht bereits als Hintergrunddienst-Fall benannt waere.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `apps/api/src/prisma/rls-access-inventory.spec.ts`: FOUND, enthaelt `analyzeSource`, `parseSchemaRelations`, `SCHEMA_RELATIONS`, `RELATION_SPEC_EXCEPTIONS`, 24 `it(`-Bloecke, alle gruen
|
||||||
|
- `docs/mandantentrennung-zugriffsklassifikation.md`: FOUND, 72 (Datei, Modell)-Paare, Klassen-Verteilung 35/21/14/2
|
||||||
|
- `docs/mandantentrennung-etappe2-fehlerrichtung.md`: FOUND, `Nachtrag (260911-mkj)` unter `(n4)` und im Abschluss-Abschnitt
|
||||||
|
- `.planning/WINDOWS.md`: FOUND, `#27` Status `fixed`, `#33` neu mit Status `open`, Kopfzaehler (`open_count: 14`, `total_count: 33`) konsistent mit den Tabellenzeilen
|
||||||
|
- Commit `5ad23d0`: FOUND in `git log --oneline --all`
|
||||||
|
- Commit `388690f`: FOUND in `git log --oneline --all`
|
||||||
|
- Baseline: 1007/1007 Tests, 62/62 Dateien gruen; `npm --prefix apps/api run type-check` sauber; `rls-scratch-check.mjs` 137/137 bestanden
|
||||||
|
- Schalter aus, kein Dienstcode, kein Schema/Migration, keine Compose-/Umgebungsdatei veraendert (bestaetigt per `git status --short` vor jedem Commit)
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260911-mkj
|
||||||
|
verified: 2026-09-11T16:56:00Z
|
||||||
|
status: passed
|
||||||
|
score: 6/6 must-haves verified
|
||||||
|
covered_files: [".planning/WINDOWS.md", ".planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-PLAN.md", ".planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-SUMMARY.md", "apps/api/src/prisma/rls-access-inventory.spec.ts", "docs/mandantentrennung-etappe2-fehlerrichtung.md", "docs/mandantentrennung-zugriffsklassifikation.md"]
|
||||||
|
covered_digest: "v1:sha256:d22ed49a10962fbdcd4ce1b6d931c9f4bf40f3caab84bb2af3b05b80fddba855"
|
||||||
|
behavior_unverified: 0
|
||||||
|
overrides_applied: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Task quick-260911-mkj: WINDOWS #27 schliessen — Relations-Blindstelle Verification Report
|
||||||
|
|
||||||
|
**Task Goal:** Vierte Erkennungsform in `rls-access-inventory.spec.ts` fuer Relationszugriffe (`include:`/`select:`/`_count:`) hinzufuegen, Bestandsaufnahme fortschreiben, WINDOWS #27 schliessen, Restmenge als eigener Ledger-Eintrag fuehren.
|
||||||
|
**Verified:** 2026-09-11T16:56:00Z
|
||||||
|
**Status:** passed
|
||||||
|
**Commits reviewed:** 5ad23d0, 388690f (base cc26197)
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### Observable Truths
|
||||||
|
|
||||||
|
| # | Truth | Status | Evidence |
|
||||||
|
|---|-------|--------|----------|
|
||||||
|
| 1 | Der Detektor kann nicht still unterberichten (Waechter faellt laut, nicht `\|\| echo 0`) | ✓ VERIFIED | Falsified live: added `someClient.tenant.findMany({ include: { users: true } })` on an unknown receiver in a throwaway file (`apps/api/src/tmp-verify-probe/tmp-probe.service.ts`) → named test `jede include:/select:/_count:-Angabe liegt innerhalb eines erkannten Modellaufrufs...` went red (`1 failed \| 23 passed`). Also injected `select: IMPORTED_SELECT_XYZ` (unresolved identifier) in a second throwaway file → `unresolvedRelationSpecValues ist ueberall leer` went red (`2 failed \| 22 passed`). Both probes removed; suite restored to 24/24 green; `git status --short` clean before/after. |
|
||||||
|
| 2 | Acht gepinnte Proben, darunter WINDOWS #27 ungebunden UND gebunden | ✓ VERIFIED | `rls-access-inventory.spec.ts:773-913`: Probe A (`this.prisma.tenant` → `unboundModels ⊇ {tenant, user}`), Probe B (same on `forTenant(` client → `boundModels ⊇ {tenant, user}`), plus real ldap form, nested `where` chain, scalar-select negative probe, `_count: true`, unknown receiver, constant resolution/unresolved — all 8 present and passing. |
|
||||||
|
| 3 | 7 new pairs + 3 Stand-changes are in the classification doc with class/Stand/reason; `ldapFieldMapping` reads `beides`/`gemischt` | ✓ VERIFIED | All 10 required table rows found verbatim in `docs/mandantentrennung-zugriffsklassifikation.md`. Recomputed class distribution independently from the table: `muss-mandantengebunden 35, keine-mandantengebundene-tabelle 21, beides 14, bewusst-uebergreifend 2` = 72, matching the claimed 35/21/14/2. `ldapFieldMapping` row (line 597) carries reason text referencing `getAllActiveConfigs()`/`include: { fieldMappings: true }`. |
|
||||||
|
| 4 | Ledger #33 exists, is open, names both zero-coverage files, not folded into #27 | ✓ VERIFIED | `.planning/WINDOWS.md` line 50: `\| 33 \| quick-260911-mkj \| unmet-truth \|...` status `open`, description names both `tenders/tenders.seed.ts` and `tenders/backfill-tender-source.ts`. Line 44: `#27` status `fixed`. Header counters (`open_count: 14`, `total_count: 33`) match table row counts exactly (33 rows, 14 open). |
|
||||||
|
| 5 | Documented drift (926359b) is pre-existing, not caused by this execution | ✓ VERIFIED | `926359b` (docs-only: `.planning/HANDOFF.json`, `docs/mandantentrennung-etappe3-auftrag.md`) is an ancestor of `5ad23d0`. `git diff 926359b -- docs/mandantentrennung-etappe3-auftrag.md .planning/HANDOFF.json` against current HEAD is empty — neither executor commit touched these files further. The allow-list gate (`git diff --name-only cc26197`) does flag `docs/mandantentrennung-etappe3-auftrag.md` as unexpected, exactly as SUMMARY documents. |
|
||||||
|
| 6 | Constraints held: no schema/migration/compose/env change, switch off, no service code converted | ✓ VERIFIED | `git diff --name-only cc26197 -- apps/api/prisma` empty. `git diff --name-only cc26197 \| grep -E '^(docker-compose\|apps/api/\.env\|\.env)'` empty. Only file changed under `apps/api/src` is the spec (`apps/api/src/prisma/rls-access-inventory.spec.ts`) — confirmed by grep. |
|
||||||
|
|
||||||
|
**Score:** 6/6 truths verified (0 present-behavior-unverified)
|
||||||
|
|
||||||
|
### Required Artifacts
|
||||||
|
|
||||||
|
| Artifact | Expected | Status | Details |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| `apps/api/src/prisma/rls-access-inventory.spec.ts` | `analyzeSource`, `parseSchemaRelations`, `SCHEMA_RELATIONS`, 4th form, `RELATION_SPEC_EXCEPTIONS`, 3 watchdogs, 2 schema tests, 8 pinned probes | ✓ VERIFIED | All present (lines 209-360 for schema parsing/4th-form, 754-913 for describe block); 24 `it(` total, 24/24 green. |
|
||||||
|
| `docs/mandantentrennung-zugriffsklassifikation.md` | 7 new rows, 3 revised Stand, rewritten gap paragraph, dated overview paragraph, `Stand 260911-mkj` paragraph + new class-distribution table, background-service nachtrag | ✓ VERIFIED | All present and recomputed to match (see truth 3 evidence). Heading stays "sechs Fälle" (no phantom new case). |
|
||||||
|
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `Nachtrag (260911-mkj)` under `(n4)`, note under `## Etappe 2 — Abschluss` | ✓ VERIFIED | Line 2474 `Nachtrag (260911-mkj)` inside `(n4)` block; `## Etappe 2 — Abschluss` section references `#27 ... seit 260911-mkj geschlossen`. |
|
||||||
|
| `.planning/WINDOWS.md` | #27 `fixed`, new entry `quick-260911-mkj` for out-of-form receivers | ✓ VERIFIED | See truth 4. |
|
||||||
|
|
||||||
|
### Key Link Verification
|
||||||
|
|
||||||
|
| From | To | Via | Status | Details |
|
||||||
|
|------|----|----|--------|---------|
|
||||||
|
| `analyzeSource()` fourth form | `SCHEMA_RELATIONS` → `boundModels`/`unboundModels` | direct write, same client-name-lowercasing as doc's `Modell` column | ✓ WIRED | `scanRelationKeys()` (lines 303-360) writes directly into the same sets consumed by `findAccessSites`/`computeStandByKey`, which the pre-existing comparison tests (`jede im Quelltext gefundene (Datei, Modell)-Fundstelle ist im Dokument eingetragen`, `der eingetragene Stand stimmt`) already exercise — confirmed green. |
|
||||||
|
| Raw count vs. matched count | `RELATION_SPEC_EXCEPTIONS` | watchdog test | ✓ WIRED | Falsified live (see truth 1). |
|
||||||
|
|
||||||
|
### Behavioral Spot-Checks
|
||||||
|
|
||||||
|
| Behavior | Command | Result | Status |
|
||||||
|
|----------|---------|--------|--------|
|
||||||
|
| Detector red on out-of-form unbound `include:` | inject throwaway file, run spec | `1 failed \| 23 passed` | ✓ PASS |
|
||||||
|
| Detector red on unresolved constant identifier | inject throwaway file, run spec | `2 failed \| 22 passed` (cumulative) | ✓ PASS |
|
||||||
|
| Spec green after cleanup | `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` | 24/24 passed | ✓ PASS |
|
||||||
|
| Full suite baseline | `npm --prefix apps/api run test` | 1007/1007 tests, 62/62 files | ✓ PASS (matches orchestrator's pre-measured baseline exactly) |
|
||||||
|
| Type-check | `npm --prefix apps/api run type-check` | exit 0, no output | ✓ PASS |
|
||||||
|
| Allow-list diff against `cc26197` | `git diff --name-only cc26197` | only spec + 2 docs + `.planning/**` (plus pre-existing, untouched `docs/mandantentrennung-etappe3-auftrag.md` drift) | ✓ PASS (as documented) |
|
||||||
|
|
||||||
|
### Anti-Patterns Found
|
||||||
|
|
||||||
|
| File | Line | Pattern | Severity | Impact |
|
||||||
|
|------|------|---------|----------|--------|
|
||||||
|
| `apps/api/src/prisma/rls-access-inventory.spec.ts` | 203-207 | Code comment claims the `(?=\s\|$)` lookahead in `parseSchemaRelations`'s field pattern is necessary to avoid losing list relations (`User[]` at line end) — reverting it (`(?:\[\]\|\?)?` kept, trailing lookahead removed) was tried live against the current `schema.prisma` and against the pinned `Tenant.users -> User` test; **no test went red**, and `SCHEMA_RELATIONS` still resolved identically (34 relation fields, same set) with or without the lookahead. | ℹ️ Info | Does not indicate an actual under-reporting risk today — `\w+` already stops before `[`/`?`, so the guard is currently redundant for this schema. It does mean the specific historical-bug narrative in the comment is not backed by a dedicated regression test; a future schema/regex change that reintroduces the described failure mode would not be caught by any existing assertion. Not a blocker: the primary anti-under-report protections (raw-vs-matched watchdog, unresolved-value watchdog) were independently falsified and confirmed live (see truth 1). Reverted cleanly, `git status --short` confirmed clean before continuing. |
|
||||||
|
|
||||||
|
### Requirements Coverage
|
||||||
|
|
||||||
|
Quick task — no formal `.planning/REQUIREMENTS.md` entries exist for `WINDOWS-27`/`ETAPPE-4-VORAUSSETZUNG` (expected; quick tasks declare requirements inline in PLAN frontmatter, not the milestone requirements ledger). Both requirement IDs are addressed by the verified truths above.
|
||||||
|
|
||||||
|
### Human Verification Required
|
||||||
|
|
||||||
|
None. This phase is entirely static analysis (a Vitest spec) and Markdown documentation — no UI, no runtime service behavior, no external integration. All claims were verifiable by direct command execution and falsification.
|
||||||
|
|
||||||
|
### Gaps Summary
|
||||||
|
|
||||||
|
None. All six derived must-haves (detector cannot silently under-report; eight pinned probes including both #27 forms; seven new pairs / three Stand changes / ldapFieldMapping reclass; Ledger #33 open and correctly scoped; documented pre-existing drift; constraints held) are verified against the actual codebase, not just against SUMMARY.md's narrative. The one ℹ️ Info finding (lookahead-revert falsification produced no red test) is a documentation/robustness nuance, not a functional gap — the detector's actual anti-under-report mechanism (the raw/matched watchdog) was independently and successfully falsified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Verified: 2026-09-11T16:56:00Z_
|
||||||
|
_Verifier: Claude (gsd-verifier)_
|
||||||
@@ -35,11 +35,27 @@ import { describe, expect, it } from 'vitest';
|
|||||||
* erkannte Zuweisung ODER jeder Aufruf von `withTenantTransaction(` zaehlt
|
* erkannte Zuweisung ODER jeder Aufruf von `withTenantTransaction(` zaehlt
|
||||||
* als gebunden (das Hilfsmittel bindet den Kontext selbst, direkt auf dem
|
* als gebunden (das Hilfsmittel bindet den Kontext selbst, direkt auf dem
|
||||||
* Transaktionsparameter) — alles andere zaehlt als ungebunden.
|
* Transaktionsparameter) — alles andere zaehlt als ungebunden.
|
||||||
|
*
|
||||||
|
* Erweitert in 260911-mkj (WINDOWS #27): keine der drei Formen oben sieht
|
||||||
|
* einen Zugriff, der ueber `include:`/`select:`/`_count:` aus einem
|
||||||
|
* erkannten Modellaufruf HERAUS in eine ZWEITE Tabelle reicht — Prisma
|
||||||
|
* rendert das als Unterabfrage/Join auf die zweite Tabelle unter DEREN
|
||||||
|
* Regel, aber weder `this.prisma.<Modell>` noch `<gebundener
|
||||||
|
* Client>.<Modell>` noch `<tx>.<Modell>` enthalten das Zielmodell als
|
||||||
|
* eigenen Text. Die vierte Erkennung loest Relationsfelder ueber
|
||||||
|
* `schema.prisma` (`parseSchemaRelations`, `SCHEMA_RELATIONS`) auf ihr
|
||||||
|
* Zielmodell auf und traegt das Zielmodell als eigene Fundstelle derselben
|
||||||
|
* Datei ein — gebunden, wenn der Empfaenger des Ankeraufrufs gebunden ist,
|
||||||
|
* sonst ungebunden. Ein Waechter (Rohzahl `include|select|_count` ueber den
|
||||||
|
* ganzen kommentarfreien Quelltext gegen die innerhalb erkannter Aufrufe
|
||||||
|
* gezaehlte Zahl) haelt die Grenze der Erkennung laut, nicht still — siehe
|
||||||
|
* `RELATION_SPEC_EXCEPTIONS` unten.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_SRC_DIR = join(__dirname, '..');
|
const API_SRC_DIR = join(__dirname, '..');
|
||||||
const REPO_ROOT = join(__dirname, '../../../..');
|
const REPO_ROOT = join(__dirname, '../../../..');
|
||||||
const DOC_PATH = join(REPO_ROOT, 'docs/mandantentrennung-zugriffsklassifikation.md');
|
const DOC_PATH = join(REPO_ROOT, 'docs/mandantentrennung-zugriffsklassifikation.md');
|
||||||
|
const SCHEMA_PATH = join(__dirname, '../../prisma/schema.prisma');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dateien, in denen ein `forTenant(`-Aufruf bewusst NICHT der erkannten
|
* Dateien, in denen ein `forTenant(`-Aufruf bewusst NICHT der erkannten
|
||||||
@@ -77,6 +93,22 @@ const FORTENANT_ASSIGNMENT_EXCEPTIONS = new Set<string>([]);
|
|||||||
*/
|
*/
|
||||||
const INTERACTIVE_TRANSACTION_EXCEPTIONS = new Set<string>([]);
|
const INTERACTIVE_TRANSACTION_EXCEPTIONS = new Set<string>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dateien, in denen eine `include:`/`select:`/`_count:`-Angabe ausserhalb
|
||||||
|
* jedes von der vierten Erkennung erfassten Modellaufrufs liegt (260911-mkj,
|
||||||
|
* WINDOWS #27). Gemessen zur Planungszeit: `backfill-tender-source.ts` ist
|
||||||
|
* ein eigenstaendiges Skript mit eigenem `new PrismaClient()` — sein
|
||||||
|
* `select:` (Zeile 34) liegt auf der plattformglobalen Tabelle `Tender`
|
||||||
|
* (Migration 20260909140000, Gruppe b, kein Zeilenschutz); der Empfaenger
|
||||||
|
* `prisma` dieser Datei ist fuer KEINE der vier Erkennungsformen erreichbar
|
||||||
|
* (weder `this.prisma` noch eine `forTenant(`-Zuweisung noch ein
|
||||||
|
* Transaktionsparameter). Zusammen mit `tenders.seed.ts`
|
||||||
|
* (Funktionsparameter `prisma: PrismaService`, keine Relationsangabe,
|
||||||
|
* deshalb hier nicht gelistet) als eigener Ledger-Eintrag gefuehrt:
|
||||||
|
* WINDOWS #33.
|
||||||
|
*/
|
||||||
|
const RELATION_SPEC_EXCEPTIONS = new Set<string>(['apps/api/src/tenders/backfill-tender-source.ts']);
|
||||||
|
|
||||||
const STAND_TOKENS = ['gebunden', 'ungebunden', 'gemischt'] as const;
|
const STAND_TOKENS = ['gebunden', 'ungebunden', 'gemischt'] as const;
|
||||||
type Stand = (typeof STAND_TOKENS)[number];
|
type Stand = (typeof STAND_TOKENS)[number];
|
||||||
|
|
||||||
@@ -88,6 +120,9 @@ interface FileAnalysis {
|
|||||||
assignmentFormCalls: number;
|
assignmentFormCalls: number;
|
||||||
rawInteractiveTransactionCount: number;
|
rawInteractiveTransactionCount: number;
|
||||||
matchedInteractiveTransactionCount: number;
|
matchedInteractiveTransactionCount: number;
|
||||||
|
rawRelationSpecCount: number;
|
||||||
|
matchedRelationSpecCount: number;
|
||||||
|
unresolvedRelationSpecValues: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function listTsFiles(dir: string): string[] {
|
function listTsFiles(dir: string): string[] {
|
||||||
@@ -116,8 +151,216 @@ function stripComments(source: string): string {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyzeFile(absPath: string, relPath: string): FileAnalysis {
|
function escapeRegExp(value: string): string {
|
||||||
const source = stripComments(readFileSync(absPath, 'utf-8'));
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sucht ab `openIndex` (der Position von `openChar`) die passende
|
||||||
|
* schliessende Klammer per Klammertiefe. Verwendet fuer sowohl `(`/`)`
|
||||||
|
* (Argumentbereich eines Ankeraufrufs) als auch `{`/`}` (Objektliteral
|
||||||
|
* einer aufgeloesten Konstante) — 260911-mkj, WINDOWS #27.
|
||||||
|
*/
|
||||||
|
function findMatchingBracket(text: string, openIndex: number, openChar: string, closeChar: string): number {
|
||||||
|
let depth = 0;
|
||||||
|
for (let i = openIndex; i < text.length; i++) {
|
||||||
|
const ch = text[i];
|
||||||
|
if (ch === openChar) depth++;
|
||||||
|
else if (ch === closeChar) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersetzt den INHALT jedes Zeichenkettenliterals (einfach, doppelt,
|
||||||
|
* Backtick) durch nichts, die Anfuehrungszeichen bleiben — NUR fuer die
|
||||||
|
* vierte Erkennung (260911-mkj, WINDOWS #27): eine Zeichenkette wie
|
||||||
|
* `contains: '{('` darf die Klammertiefenzaehlung des Argumentbereichs
|
||||||
|
* nicht zerreissen. Die Formen 1-3 arbeiten weiter auf dem unveraenderten
|
||||||
|
* kommentarfreien Quelltext (`source`), damit eine Vorlagen-Interpolation
|
||||||
|
* wie `${tx.user.count()}` fuer sie sichtbar bleibt.
|
||||||
|
*/
|
||||||
|
function blankStringLiterals(text: string): string {
|
||||||
|
return text.replace(
|
||||||
|
/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`/g,
|
||||||
|
(literal) => literal[0] + literal[literal.length - 1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lowerFirst(name: string): string {
|
||||||
|
return name.length > 0 ? name.charAt(0).toLowerCase() + name.slice(1) : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest `schema.prisma` zur TESTZEIT (dieselbe Idiomatik wie das Lesen der
|
||||||
|
* Migrationen in `rls-coverage.spec.ts`) und bildet je `model`-Block eine
|
||||||
|
* Map Feld -> Zielmodell, aber NUR fuer Felder, deren Typ selbst ein
|
||||||
|
* Modellname ist (260911-mkj, WINDOWS #27).
|
||||||
|
*
|
||||||
|
* Der Lookahead `(?=\s|$)` ist zwingend: eine Feldzeile wie
|
||||||
|
* `users User[]` in `Tenant` steht am ZEILENENDE. Ohne den Lookahead
|
||||||
|
* verliert das Muster jede Listenrelation, deren Typname direkt vor dem
|
||||||
|
* Zeilenende steht — der erste Fehler des Planungs-Prototyps, `Tenant`
|
||||||
|
* hatte damit scheinbar keine Relation. Nicht wiederholen.
|
||||||
|
*/
|
||||||
|
function parseSchemaRelations(schemaSource: string): Map<string, Map<string, string>> {
|
||||||
|
const modelBlockPattern = /^model\s+(\w+)\s*\{([\s\S]*?)^\}/gm;
|
||||||
|
const blocks: Array<{ name: string; body: string }> = [];
|
||||||
|
for (const m of schemaSource.matchAll(modelBlockPattern)) {
|
||||||
|
if (m[1] !== undefined && m[2] !== undefined) {
|
||||||
|
blocks.push({ name: m[1], body: m[2] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const modelNames = new Set(blocks.map((b) => b.name));
|
||||||
|
|
||||||
|
const fieldPattern = /^\s*(\w+)\s+(\w+)(?:\[\]|\?)?(?=\s|$)/gm;
|
||||||
|
const relations = new Map<string, Map<string, string>>();
|
||||||
|
for (const { name, body } of blocks) {
|
||||||
|
const fieldMap = new Map<string, string>();
|
||||||
|
for (const fm of body.matchAll(fieldPattern)) {
|
||||||
|
const fieldName = fm[1];
|
||||||
|
const fieldType = fm[2];
|
||||||
|
if (fieldName && fieldType && modelNames.has(fieldType)) {
|
||||||
|
fieldMap.set(fieldName, fieldType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
relations.set(name, fieldMap);
|
||||||
|
}
|
||||||
|
return relations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA_RELATIONS = parseSchemaRelations(readFileSync(SCHEMA_PATH, 'utf-8'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kleiner Anfangsbuchstabe -> Modellname (`Tenant` -> `tenant`,
|
||||||
|
* `LdapFieldMapping` -> `ldapFieldMapping`) — derselbe Schluessel wie in
|
||||||
|
* der Spalte `Modell` der Bestandsaufnahme und in `this.prisma.<Modell>`
|
||||||
|
* (260911-mkj, WINDOWS #27).
|
||||||
|
*/
|
||||||
|
const CLIENT_NAME_TO_MODEL = new Map<string, string>(
|
||||||
|
[...SCHEMA_RELATIONS.keys()].map((modelName) => [lowerFirst(modelName), modelName]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const RELATION_ANCHOR_OPERATIONS = [
|
||||||
|
'findMany',
|
||||||
|
'findFirst',
|
||||||
|
'findUnique',
|
||||||
|
'findFirstOrThrow',
|
||||||
|
'findUniqueOrThrow',
|
||||||
|
'create',
|
||||||
|
'createMany',
|
||||||
|
'createManyAndReturn',
|
||||||
|
'update',
|
||||||
|
'updateMany',
|
||||||
|
'updateManyAndReturn',
|
||||||
|
'upsert',
|
||||||
|
'delete',
|
||||||
|
'deleteMany',
|
||||||
|
'count',
|
||||||
|
'aggregate',
|
||||||
|
'groupBy',
|
||||||
|
];
|
||||||
|
const RELATION_ANCHOR_OPERATIONS_PATTERN = RELATION_ANCHOR_OPERATIONS.join('|');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loest `select: NAME`/`include: NAME` gegen eine im selben Quelltext
|
||||||
|
* definierte Objektliteral-Konstante `const NAME = { ... }` auf
|
||||||
|
* (260911-mkj, WINDOWS #27, Waechter (b)). Findet sich keine, ist der
|
||||||
|
* Aufrufer dafuer zustaendig, die Kennung als unaufloesbar zu vermerken.
|
||||||
|
*/
|
||||||
|
function resolveConstantObjectLiteral(blank: string, name: string): string | null {
|
||||||
|
const declPattern = new RegExp(`\\bconst\\s+${name}\\b[^=]*=\\s*\\{`);
|
||||||
|
const m = declPattern.exec(blank);
|
||||||
|
if (!m) return null;
|
||||||
|
const braceStart = m.index + m[0].length - 1;
|
||||||
|
const braceEnd = findMatchingBracket(blank, braceStart, '{', '}');
|
||||||
|
if (braceEnd === -1) return null;
|
||||||
|
return blank.slice(braceStart, braceEnd + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelationScanFrame {
|
||||||
|
context: string;
|
||||||
|
enteringKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Laeuft mit einem Kontextstapel ueber den Argumentbereich eines erkannten
|
||||||
|
* Modellaufrufs (260911-mkj, WINDOWS #27, PLAN.md Aufgabe 1 Schritt 3(f)).
|
||||||
|
* Start-Kontext ist das Modell des Ankers. Ein Schluessel, der ein
|
||||||
|
* Relationsfeld des AKTUELLEN Kontextmodells ist, traegt das Zielmodell als
|
||||||
|
* eigene Fundstelle ein (gebunden/ungebunden nach dem Empfaenger des
|
||||||
|
* Ankers) und wird zum Kontext des naechsten `{`; `_count: true` direkt
|
||||||
|
* unter `include`/`select` traegt ALLE Relationen des aktuellen
|
||||||
|
* Kontextmodells ein. Jeder andere Schluessel laesst den Kontext
|
||||||
|
* unveraendert (`where`, `data`, `some`, `every`, `none`, `is`, `isNot`,
|
||||||
|
* `connect`, `create`, Operatoren wie `contains`, Skalare) — `{` schiebt
|
||||||
|
* den vorgemerkten (sonst den aktuellen) Kontext, `}` nimmt ihn zurueck,
|
||||||
|
* `,` loescht die Vormerkung.
|
||||||
|
*/
|
||||||
|
function scanRelationKeys(
|
||||||
|
region: string,
|
||||||
|
initialContext: string,
|
||||||
|
isBound: boolean,
|
||||||
|
unboundModels: Set<string>,
|
||||||
|
boundModels: Set<string>,
|
||||||
|
): void {
|
||||||
|
const stack: RelationScanFrame[] = [{ context: initialContext, enteringKey: null }];
|
||||||
|
let pendingContext: string | null = null;
|
||||||
|
let pendingKey: string | null = null;
|
||||||
|
|
||||||
|
const tokenPattern = /\{|\}|,|\b([A-Za-z_]\w*)\s*:/g;
|
||||||
|
let match: RegExpExecArray | null = tokenPattern.exec(region);
|
||||||
|
while (match !== null) {
|
||||||
|
const token = match[0];
|
||||||
|
if (token === '{') {
|
||||||
|
const currentContext = stack[stack.length - 1].context;
|
||||||
|
stack.push({ context: pendingContext ?? currentContext, enteringKey: pendingKey });
|
||||||
|
pendingContext = null;
|
||||||
|
pendingKey = null;
|
||||||
|
} else if (token === '}') {
|
||||||
|
if (stack.length > 1) stack.pop();
|
||||||
|
pendingContext = null;
|
||||||
|
pendingKey = null;
|
||||||
|
} else if (token === ',') {
|
||||||
|
pendingContext = null;
|
||||||
|
pendingKey = null;
|
||||||
|
} else {
|
||||||
|
const keyName = match[1] ?? null;
|
||||||
|
const currentContext = stack[stack.length - 1].context;
|
||||||
|
const relTarget = keyName ? SCHEMA_RELATIONS.get(currentContext)?.get(keyName) : undefined;
|
||||||
|
if (keyName && relTarget) {
|
||||||
|
const clientName = lowerFirst(relTarget);
|
||||||
|
(isBound ? boundModels : unboundModels).add(clientName);
|
||||||
|
pendingContext = relTarget;
|
||||||
|
pendingKey = keyName;
|
||||||
|
} else if (keyName === '_count') {
|
||||||
|
const parentKey = stack[stack.length - 1].enteringKey;
|
||||||
|
const afterColon = region.slice(match.index + match[0].length);
|
||||||
|
const isLiteralTrue = /^\s*true\b/.test(afterColon);
|
||||||
|
if (isLiteralTrue && (parentKey === 'include' || parentKey === 'select')) {
|
||||||
|
const relations = SCHEMA_RELATIONS.get(currentContext);
|
||||||
|
if (relations) {
|
||||||
|
for (const target of relations.values()) {
|
||||||
|
(isBound ? boundModels : unboundModels).add(lowerFirst(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingContext = currentContext;
|
||||||
|
pendingKey = keyName;
|
||||||
|
} else {
|
||||||
|
pendingContext = currentContext;
|
||||||
|
pendingKey = keyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match = tokenPattern.exec(region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeSource(rawSource: string, relPath: string): FileAnalysis {
|
||||||
|
const source = stripComments(rawSource);
|
||||||
|
|
||||||
const unboundModels = new Set<string>();
|
const unboundModels = new Set<string>();
|
||||||
for (const m of source.matchAll(/this\.prisma\.([a-zA-Z]+)/g)) {
|
for (const m of source.matchAll(/this\.prisma\.([a-zA-Z]+)/g)) {
|
||||||
@@ -160,6 +403,13 @@ function analyzeFile(absPath: string, relPath: string): FileAnalysis {
|
|||||||
? 0
|
? 0
|
||||||
: [...source.matchAll(/\.\$transaction\(\s*async\b/g)].length;
|
: [...source.matchAll(/\.\$transaction\(\s*async\b/g)].length;
|
||||||
|
|
||||||
|
// Sammelt je Datei die Parameternamen BEIDER interaktiver Transaktionsformen
|
||||||
|
// mit ihrer Bindung — 260911-mkj nutzt diese Mengen als zusaetzliche
|
||||||
|
// erkannte Empfaengerformen der vierten Erkennung (bislang wurden sie nur
|
||||||
|
// lokal verbraucht).
|
||||||
|
const txBoundParams: string[] = [];
|
||||||
|
const txUnboundParams: string[] = [];
|
||||||
|
|
||||||
// Form 1: `<empfaenger>.$transaction(async (<param>) => ...)`. <empfaenger>
|
// Form 1: `<empfaenger>.$transaction(async (<param>) => ...)`. <empfaenger>
|
||||||
// ist gebunden, wenn er in boundNames steht (aus der Zuweisungsform oben).
|
// ist gebunden, wenn er in boundNames steht (aus der Zuweisungsform oben).
|
||||||
const directInteractiveMatches = [
|
const directInteractiveMatches = [
|
||||||
@@ -172,6 +422,8 @@ function analyzeFile(absPath: string, relPath: string): FileAnalysis {
|
|||||||
const param = m[2];
|
const param = m[2];
|
||||||
if (!param) continue;
|
if (!param) continue;
|
||||||
const isBound = boundNames.has(receiver);
|
const isBound = boundNames.has(receiver);
|
||||||
|
if (isBound) txBoundParams.push(param);
|
||||||
|
else txUnboundParams.push(param);
|
||||||
const re = new RegExp(`\\b${param}\\.([a-zA-Z]+)`, 'g');
|
const re = new RegExp(`\\b${param}\\.([a-zA-Z]+)`, 'g');
|
||||||
for (const mm of source.matchAll(re)) {
|
for (const mm of source.matchAll(re)) {
|
||||||
if (!mm[1]) continue;
|
if (!mm[1]) continue;
|
||||||
@@ -192,12 +444,73 @@ function analyzeFile(absPath: string, relPath: string): FileAnalysis {
|
|||||||
for (const m of withTenantTransactionMatches) {
|
for (const m of withTenantTransactionMatches) {
|
||||||
const param = m[1];
|
const param = m[1];
|
||||||
if (!param) continue;
|
if (!param) continue;
|
||||||
|
txBoundParams.push(param);
|
||||||
const re = new RegExp(`\\b${param}\\.([a-zA-Z]+)`, 'g');
|
const re = new RegExp(`\\b${param}\\.([a-zA-Z]+)`, 'g');
|
||||||
for (const mm of source.matchAll(re)) {
|
for (const mm of source.matchAll(re)) {
|
||||||
if (mm[1]) boundModels.add(mm[1]);
|
if (mm[1]) boundModels.add(mm[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vierte Erkennung (260911-mkj, WINDOWS #27): Relationszugriffe. `blank`
|
||||||
|
// ist NUR fuer diese Form — Zeichenkettenliteral-Inhalte sind entfernt,
|
||||||
|
// damit eine Zeichenkette wie `contains: '{('` die Klammertiefenzaehlung
|
||||||
|
// nicht zerreisst.
|
||||||
|
const blank = blankStringLiterals(source);
|
||||||
|
|
||||||
|
const allReceiverNames = new Set<string>([
|
||||||
|
'this.prisma',
|
||||||
|
...boundNames,
|
||||||
|
...txBoundParams,
|
||||||
|
...txUnboundParams,
|
||||||
|
]);
|
||||||
|
const boundReceiverNames = new Set<string>([...boundNames, ...txBoundParams]);
|
||||||
|
const receiverAlternation = [...allReceiverNames].map(escapeRegExp).join('|');
|
||||||
|
const anchorPattern = new RegExp(
|
||||||
|
`\\b(${receiverAlternation})\\.([a-zA-Z]+)\\.(?:${RELATION_ANCHOR_OPERATIONS_PATTERN})\\(`,
|
||||||
|
'g',
|
||||||
|
);
|
||||||
|
|
||||||
|
const unresolvedRelationSpecValues: string[] = [];
|
||||||
|
let matchedRelationSpecCount = 0;
|
||||||
|
|
||||||
|
for (const m of blank.matchAll(anchorPattern)) {
|
||||||
|
const receiver = m[1];
|
||||||
|
const modelClientName = m[2];
|
||||||
|
if (!receiver || !modelClientName || m.index === undefined) continue;
|
||||||
|
|
||||||
|
const isBound = boundReceiverNames.has(receiver);
|
||||||
|
const openIndex = m.index + m[0].length - 1;
|
||||||
|
const closeIndex = findMatchingBracket(blank, openIndex, '(', ')');
|
||||||
|
if (closeIndex === -1) continue;
|
||||||
|
|
||||||
|
let region = blank.slice(openIndex, closeIndex + 1);
|
||||||
|
matchedRelationSpecCount += (region.match(/\b(?:include|select|_count)\s*:/g) || []).length;
|
||||||
|
|
||||||
|
// Wertform (Waechter (b)): eine Kennung als include:/select:-Wert wird
|
||||||
|
// gegen eine gleichnamige, in derselben Datei definierte
|
||||||
|
// Objektliteral-Konstante aufgeloest, sonst als unaufloesbar vermerkt.
|
||||||
|
region = region.replace(
|
||||||
|
/\b(include|select)\s*:\s*([A-Za-z_]\w*)\b(?!\s*[.(])/g,
|
||||||
|
(full: string, keyword: string, ident: string) => {
|
||||||
|
if (ident === 'true' || ident === 'false') return full;
|
||||||
|
const resolved = resolveConstantObjectLiteral(blank, ident);
|
||||||
|
if (resolved) return `${keyword}: ${resolved}`;
|
||||||
|
unresolvedRelationSpecValues.push(`${relPath}: ${keyword}: ${ident}`);
|
||||||
|
return full;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialContext = CLIENT_NAME_TO_MODEL.get(modelClientName);
|
||||||
|
if (initialContext) {
|
||||||
|
scanRelationKeys(region, initialContext, isBound, unboundModels, boundModels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rohzahl ueber den GANZEN kommentarfreien Quelltext (nicht `blank`) — eine
|
||||||
|
// Angabe in einer Vorlagen-Interpolation soll raw zaehlen und damit laut
|
||||||
|
// werden, nicht still verschwinden (Waechter (a)).
|
||||||
|
const rawRelationSpecCount = (source.match(/\b(?:include|select|_count)\s*:/g) || []).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
file: relPath,
|
file: relPath,
|
||||||
unboundModels,
|
unboundModels,
|
||||||
@@ -206,9 +519,17 @@ function analyzeFile(absPath: string, relPath: string): FileAnalysis {
|
|||||||
assignmentFormCalls: assignmentMatches.length,
|
assignmentFormCalls: assignmentMatches.length,
|
||||||
rawInteractiveTransactionCount,
|
rawInteractiveTransactionCount,
|
||||||
matchedInteractiveTransactionCount: directInteractiveMatches.length,
|
matchedInteractiveTransactionCount: directInteractiveMatches.length,
|
||||||
|
rawRelationSpecCount,
|
||||||
|
matchedRelationSpecCount,
|
||||||
|
unresolvedRelationSpecValues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function analyzeFile(absPath: string, relPath: string): FileAnalysis {
|
||||||
|
const rawSource = readFileSync(absPath, 'utf-8');
|
||||||
|
return analyzeSource(rawSource, relPath);
|
||||||
|
}
|
||||||
|
|
||||||
function analyzeAllFiles(): FileAnalysis[] {
|
function analyzeAllFiles(): FileAnalysis[] {
|
||||||
const files = listTsFiles(API_SRC_DIR);
|
const files = listTsFiles(API_SRC_DIR);
|
||||||
return files
|
return files
|
||||||
@@ -391,4 +712,203 @@ describe('mandantentrennung-zugriffsklassifikation.md deckt den Quelltext vollst
|
|||||||
}
|
}
|
||||||
expect(violations, violations.join('\n')).toEqual([]);
|
expect(violations, violations.join('\n')).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('jede include:/select:/_count:-Angabe liegt innerhalb eines erkannten Modellaufrufs oder die Datei steht in der begruendeten Ausnahmeliste (260911-mkj, WINDOWS #27)', () => {
|
||||||
|
const violations: string[] = [];
|
||||||
|
for (const a of analyses) {
|
||||||
|
const unmatched = a.rawRelationSpecCount - a.matchedRelationSpecCount;
|
||||||
|
if (unmatched > 0 && !RELATION_SPEC_EXCEPTIONS.has(a.file)) {
|
||||||
|
violations.push(
|
||||||
|
`${a.file}: ${unmatched} include:/select:/_count:-Angabe(n) ausserhalb eines erkannten Modellaufrufs`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(violations, violations.join('\n')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keine veraltete RELATION_SPEC_EXCEPTIONS-Liste: jede Datei existiert und traegt tatsaechlich einen Ueberschuss include:/select:/_count: ausserhalb eines erkannten Modellaufrufs (260911-mkj)', () => {
|
||||||
|
const staleEntries: string[] = [];
|
||||||
|
const analysesByFile = new Map(analyses.map((a) => [a.file, a]));
|
||||||
|
for (const file of [...RELATION_SPEC_EXCEPTIONS]) {
|
||||||
|
if (!existsSync(join(REPO_ROOT, file))) {
|
||||||
|
staleEntries.push(`${file}: Datei existiert nicht mehr`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const analysis = analysesByFile.get(file);
|
||||||
|
const unmatched = analysis ? analysis.rawRelationSpecCount - analysis.matchedRelationSpecCount : 0;
|
||||||
|
if (unmatched <= 0) {
|
||||||
|
staleEntries.push(
|
||||||
|
`${file}: enthaelt keinen Ueberschuss include:/select:/_count: mehr ausserhalb eines erkannten Modellaufrufs — die Ausnahme ist ueberholt und gehoert entfernt`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(staleEntries, staleEntries.join('\n')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unresolvedRelationSpecValues ist ueberall leer: jeder include:/select:-Wert ist ein Objektliteral, `true` oder eine in derselben Datei definierte Konstante (260911-mkj)', () => {
|
||||||
|
const unresolved = analyses.flatMap((a) => a.unresolvedRelationSpecValues);
|
||||||
|
expect(unresolved, unresolved.join('\n')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('vierte Erkennung: Relationszugriffe (WINDOWS #27, 260911-mkj)', () => {
|
||||||
|
it('SCHEMA_RELATIONS pinnt die drei gemessenen Kern-Relationen', () => {
|
||||||
|
expect(SCHEMA_RELATIONS.get('Tenant')?.get('users')).toBe('User');
|
||||||
|
expect(SCHEMA_RELATIONS.get('Group')?.get('memberships')).toBe('GroupMembership');
|
||||||
|
expect(SCHEMA_RELATIONS.get('LdapConfig')?.get('fieldMappings')).toBe('LdapFieldMapping');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('jedes Relationsziel in SCHEMA_RELATIONS ist ein Modellname, und SCHEMA_RELATIONS deckt alle `model`-Bloecke des Schemas ab', () => {
|
||||||
|
const modelNames = new Set(SCHEMA_RELATIONS.keys());
|
||||||
|
for (const [, fields] of SCHEMA_RELATIONS) {
|
||||||
|
for (const target of fields.values()) {
|
||||||
|
expect(modelNames.has(target)).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const schemaSource = readFileSync(SCHEMA_PATH, 'utf-8');
|
||||||
|
const modelLineCount = (schemaSource.match(/^model\s+\w+\s*\{/gm) || []).length;
|
||||||
|
expect(SCHEMA_RELATIONS.size).toBe(modelLineCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Probe A (WINDOWS #27, ungebunden): `include: { _count: { select: { users: true } } }` auf this.prisma.tenant liefert unboundModels mit tenant UND user, user NICHT in boundModels', () => {
|
||||||
|
const probe = `
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async run() {
|
||||||
|
return this.prisma.tenant.findMany({
|
||||||
|
include: { _count: { select: { users: true } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = analyzeSource(probe, 'apps/api/src/probe/probe-a.service.ts');
|
||||||
|
expect([...result.unboundModels]).toEqual(expect.arrayContaining(['tenant', 'user']));
|
||||||
|
expect(result.boundModels.has('user')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Probe B (WINDOWS #27, gebunden): derselbe Aufruf auf einem forTenant(-Klienten liefert boundModels mit tenant UND user, user/tenant NICHT in unboundModels', () => {
|
||||||
|
const probe = `
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async run(tenantId: string) {
|
||||||
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
return tenantPrisma.tenant.findMany({
|
||||||
|
include: { _count: { select: { users: true } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = analyzeSource(probe, 'apps/api/src/probe/probe-b.service.ts');
|
||||||
|
expect([...result.boundModels]).toEqual(expect.arrayContaining(['tenant', 'user']));
|
||||||
|
expect(result.unboundModels.has('user')).toBe(false);
|
||||||
|
expect(result.unboundModels.has('tenant')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reale ldap-Form: `include: { tenant: true, fieldMappings: true }` auf this.prisma.ldapConfig.findMany liefert unboundModels mit ldapConfig, tenant UND ldapFieldMapping', () => {
|
||||||
|
const probe = `
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async getAllActiveConfigs() {
|
||||||
|
return this.prisma.ldapConfig.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: { tenant: true, fieldMappings: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = analyzeSource(probe, 'apps/api/src/probe/probe-ldap.service.ts');
|
||||||
|
expect([...result.unboundModels]).toEqual(
|
||||||
|
expect.arrayContaining(['ldapConfig', 'tenant', 'ldapFieldMapping']),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verschachtelte where-Kette auf einem forTenant(-Klienten liefert boundModels mit moduleGrant, group UND groupMembership', () => {
|
||||||
|
const probe = `
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async run(tenantId: string, userId: string) {
|
||||||
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
return tenantPrisma.moduleGrant.findMany({
|
||||||
|
where: { tenantId, group: { memberships: { some: { userId } } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = analyzeSource(probe, 'apps/api/src/probe/probe-chain.service.ts');
|
||||||
|
expect([...result.boundModels]).toEqual(
|
||||||
|
expect.arrayContaining(['moduleGrant', 'group', 'groupMembership']),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Negativprobe (skalarer select + Zeichenkette mit Klammern): liefert unboundModels GENAU {group} — kein Relationsmodell, die Klammern in der Zeichenkette zerreissen den Argumentbereich nicht', () => {
|
||||||
|
const probe = `
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async run() {
|
||||||
|
return this.prisma.group.findMany({
|
||||||
|
select: { id: true, name: true },
|
||||||
|
where: { name: { contains: '{(' } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = analyzeSource(probe, 'apps/api/src/probe/probe-negative.service.ts');
|
||||||
|
expect([...result.unboundModels].sort()).toEqual(['group']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`_count: true` liefert ALLE Relationen von Tenant (user, ldapConfig, group, moduleGrant)', () => {
|
||||||
|
const probe = `
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async run() {
|
||||||
|
return this.prisma.tenant.findMany({ include: { _count: true } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = analyzeSource(probe, 'apps/api/src/probe/probe-count-true.service.ts');
|
||||||
|
expect([...result.unboundModels]).toEqual(
|
||||||
|
expect.arrayContaining(['user', 'ldapConfig', 'group', 'moduleGrant']),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unbekannter Empfaenger (Funktionsparameter) faellt in Waechter (a): rawRelationSpecCount 1, matchedRelationSpecCount 0, kein user in beiden Mengen', () => {
|
||||||
|
const probe = `
|
||||||
|
class ProbeService {
|
||||||
|
async run(client: any) {
|
||||||
|
return client.tenant.findMany({ include: { users: true } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = analyzeSource(probe, 'apps/api/src/probe/probe-unknown.service.ts');
|
||||||
|
expect(result.rawRelationSpecCount).toBe(1);
|
||||||
|
expect(result.matchedRelationSpecCount).toBe(0);
|
||||||
|
expect(result.unboundModels.has('user')).toBe(false);
|
||||||
|
expect(result.boundModels.has('user')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Konstante als select-Wert wird aufgeloest; eine nicht definierte Kennung landet in unresolvedRelationSpecValues', () => {
|
||||||
|
const resolvedProbe = `
|
||||||
|
const SAFE = { id: true, users: true };
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async run() {
|
||||||
|
return this.prisma.tenant.findMany({ select: SAFE });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const resolvedResult = analyzeSource(resolvedProbe, 'apps/api/src/probe/probe-constant.service.ts');
|
||||||
|
expect(resolvedResult.unboundModels.has('user')).toBe(true);
|
||||||
|
|
||||||
|
const unresolvedProbe = `
|
||||||
|
class ProbeService {
|
||||||
|
constructor(private readonly prisma: any) {}
|
||||||
|
async run() {
|
||||||
|
return this.prisma.tenant.findMany({ select: IMPORTED_SELECT });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const unresolvedResult = analyzeSource(unresolvedProbe, 'apps/api/src/probe/probe-unresolved.service.ts');
|
||||||
|
expect(unresolvedResult.unresolvedRelationSpecValues).toHaveLength(1);
|
||||||
|
expect(unresolvedResult.unresolvedRelationSpecValues[0]).toContain('IMPORTED_SELECT');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2471,6 +2471,28 @@ Mandantenzahl belanglos, dieselbe Form wie
|
|||||||
Wartungsrolle gegen die gebundene Fan-out-Zaehlung ist dieselbe Pruefung wie
|
Wartungsrolle gegen die gebundene Fan-out-Zaehlung ist dieselbe Pruefung wie
|
||||||
im Bereich `user` — kein eigener Eintrag noetig.
|
im Bereich `user` — kein eigener Eintrag noetig.
|
||||||
|
|
||||||
|
**Nachtrag (260911-mkj):** WINDOWS #27 — die in (b) beschriebene
|
||||||
|
Erkennungsluecke der Bestandsaufnahme — ist geschlossen. Eine vierte
|
||||||
|
Erkennungsform in `rls-access-inventory.spec.ts` (`analyzeSource`,
|
||||||
|
`SCHEMA_RELATIONS`) loest Relationsfelder ueber `schema.prisma` auf ihr
|
||||||
|
Zielmodell auf und traegt das Zielmodell als eigene Fundstelle ein — der
|
||||||
|
Nachweis steht gegen KONTROLLIERTE Proben im Spec, nicht gegen die drei
|
||||||
|
realen Stellen aus (b): die einzige gefaehrliche Auspraegung
|
||||||
|
(`tenant.controller.ts`, ungebundener aeusserer Aufruf auf ungeschuetzter
|
||||||
|
Tabelle mit Einbindung in eine geschuetzte Tabelle) traegt die Form seit
|
||||||
|
(n3)/Aufgabe 3 (dem Fan-out-Ersatz) nicht mehr — deshalb Probestrings, die
|
||||||
|
dieselbe Form nachbilden (ungebunden und gebunden). Gemessen: SIEBEN neue
|
||||||
|
(Datei, Modell)-Paare, DREI fortgeschriebene Staende, davon
|
||||||
|
`ldap-config.service.ts`/`ldapFieldMapping` als einziger Klassenwechsel
|
||||||
|
(`muss-mandantengebunden` -> `beides`, siehe (b) oben: `getAllActiveConfigs`
|
||||||
|
reicht auch in `LdapFieldMapping` hinein, nicht nur in `Tenant`). Verbleibende
|
||||||
|
Grenze, LAUT gehalten statt still: ein Empfaenger ausserhalb aller vier
|
||||||
|
Erkennungsformen bleibt fuer JEDE Form unsichtbar — gemessen
|
||||||
|
`tenders/tenders.seed.ts` (Funktionsparameter `prisma: PrismaService`) und
|
||||||
|
`tenders/backfill-tender-source.ts` (eigenstaendiges Skript, eigener `new
|
||||||
|
PrismaClient()`), beide als eigener Ledger-Eintrag gefuehrt (nicht
|
||||||
|
stillschweigend mit #27 mitgeschlossen).
|
||||||
|
|
||||||
### (n5) Was dieser Durchlauf bewusst nicht anfasst
|
### (n5) Was dieser Durchlauf bewusst nicht anfasst
|
||||||
|
|
||||||
- Der direkte Prisma-Zugriff im Controller (Muster wie `user.controller.ts`)
|
- Der direkte Prisma-Zugriff im Controller (Muster wie `user.controller.ts`)
|
||||||
@@ -3072,7 +3094,8 @@ Bedingung gebunden wie #18), #21 (dkv-Planer-Startpfad), #22
|
|||||||
(module-registry, unterscheidbares Signal fehlt), #24 (plattformweite
|
(module-registry, unterscheidbares Signal fehlt), #24 (plattformweite
|
||||||
RSS-Verwaltung unter der Anwendungsrolle), #25 (dashboard, beweisvernichtende
|
RSS-Verwaltung unter der Anwendungsrolle), #25 (dashboard, beweisvernichtende
|
||||||
Fehlerrichtung), #26 (calendar, verschluckte Leere), #27
|
Fehlerrichtung), #26 (calendar, verschluckte Leere), #27
|
||||||
(Relationszugriffe für die Bestandsaufnahme unsichtbar), #28 (auth,
|
(Relationszugriffe für die Bestandsaufnahme unsichtbar, seit 260911-mkj
|
||||||
|
geschlossen), #28 (auth,
|
||||||
verschluckte Leere), plus die drei neuen Einträge dieses Laufs (Startpfad
|
verschluckte Leere), plus die drei neuen Einträge dieses Laufs (Startpfad
|
||||||
des Mailmoduls, verschluckte Leere `favorites`, verschluckte Leere
|
des Mailmoduls, verschluckte Leere `favorites`, verschluckte Leere
|
||||||
`settings` — Nummern siehe Aufgabe 3 dieses Plans).
|
`settings` — Nummern siehe Aufgabe 3 dieses Plans).
|
||||||
|
|||||||
@@ -131,6 +131,25 @@ Bodensatz, nicht die vollständige Zahl gebundener Zugriffe in `groups`; die
|
|||||||
Bestandsaufnahme unten (Spalte "Stand", je (Datei, Modell)-Paar) ist die
|
Bestandsaufnahme unten (Spalte "Stand", je (Datei, Modell)-Paar) ist die
|
||||||
autoritative Quelle.
|
autoritative Quelle.
|
||||||
|
|
||||||
|
**Zweite methodische Lücke, seit 260911-mkj geschlossen — aber nur in der
|
||||||
|
Bestandsaufnahme:** die beiden Rohtreffer-Greps dieser Tabelle
|
||||||
|
(`this\.prisma\.[a-zA-Z]*` bzw. `tenantPrisma\.[a-zA-Z]*\.`) zählen
|
||||||
|
Relationsziele (`include:`, `select:`, `_count:`, Relationsfilter in
|
||||||
|
`where:`/`orderBy:`) strukturell NICHT — ein `include: { module: true }`
|
||||||
|
auf `tenantPrisma.tenantModuleActivation` erzeugt keinen Rohtreffer auf
|
||||||
|
`module`, weil `module` in der Datei nie als `tenantPrisma.module` steht.
|
||||||
|
Die Spalten und die Summenzeile behalten deshalb ihre bisherige Bedeutung
|
||||||
|
und ihre bisherigen Werte (weiterhin 68 ungebunden / 178 gebunden, NACH-
|
||||||
|
GERECHNET, nicht abgeschrieben) — sie zählen weiterhin nur direkte
|
||||||
|
Modellaufrufe. Autoritativ für Relationszugriffe ist allein die
|
||||||
|
Bestandsaufnahme unten, deren vierte Erkennungsform
|
||||||
|
(`rls-access-inventory.spec.ts`, `analyzeSource`) die Ziele als eigene
|
||||||
|
Paare führt: sieben der 72 Paare dort tauchen in KEINER Rohtrefferzahl
|
||||||
|
dieser Übersicht auf, zum Beispiel
|
||||||
|
`module-registry/module-access.service.ts`/`group` (nur über den
|
||||||
|
Relationsfilter `group: { memberships: { some: { userId } } }` sichtbar,
|
||||||
|
niemals als `tenantPrisma.group` im Quelltext).
|
||||||
|
|
||||||
| Bereich | Ungebunden | Gebunden | Hinweis |
|
| Bereich | Ungebunden | Gebunden | Hinweis |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| tenders | 35 | 27 | **war 62/0**, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat `tender-rss-feed.service.ts`/`listForUser` zusätzlich auf `forTenant()` umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (`createPlatform`/`remove`, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
|
| tenders | 35 | 27 | **war 62/0**, dann 36/26 nach 260909-laa — 260910-jab (Aufgabe 2) hat `tender-rss-feed.service.ts`/`listForUser` zusätzlich auf `forTenant()` umgestellt (WINDOWS #19 geschlossen, Befund F: ungebunden hätte die Reparatur den Pfad sonst still auf nur die plattformweiten Zeilen reduziert): ein Rohtreffer wandert von ungebunden nach gebunden (36→35, 26→27). Die 35 verbleibenden ungebundenen Treffer sind die zwölf bewusst nicht angefassten Paare (D-03-Katalog, zwei Fan-out-Adapter) plus die zwei bewusst ungebundenen RSS-Pfade (`createPlatform`/`remove`, WINDOWS #24) plus die übergreifenden Hälften der beiden Hintergrunddienste (Etappe-3-Übergabe) |
|
||||||
@@ -147,7 +166,7 @@ autoritative Quelle.
|
|||||||
| settings | 1 | 3 | **war 4/0** — 260911-gwh (Aufgabe 2) hat `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` auf `forTenant()` umgestellt (3 gebundene `smtpConfig`-Rohtreffer). Der eine verbleibende ungebundene Rohtreffer ist der umbenannte Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (Befund D, WINDOWS #30) — bewusst, mit dreifacher Markierung; Befund K (`tenders`/`dkv` hängen an `getDecryptedSmtpConfig`) ist damit erfüllt |
|
| settings | 1 | 3 | **war 4/0** — 260911-gwh (Aufgabe 2) hat `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` auf `forTenant()` umgestellt (3 gebundene `smtpConfig`-Rohtreffer). Der eine verbleibende ungebundene Rohtreffer ist der umbenannte Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (Befund D, WINDOWS #30) — bewusst, mit dreifacher Markierung; Befund K (`tenders`/`dkv` hängen an `getDecryptedSmtpConfig`) ist damit erfüllt |
|
||||||
| **Summe** | **68** | **178** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), dann 83 nach 260911-cwh (`calendar` 12→0), unverändert nach 260911-e2s (`tenant` bleibt bei 8 ungebundenen Rohtreffern), dann 78 nach 260911-fh9 (`auth` 8→3), jetzt 68 nach 260911-gwh (`favorites` 7→0, `settings` 4→1). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), dann 159 nach 260911-cwh (zusätzlich 12 in `calendar`), dann 162 nach 260911-e2s (zusätzlich 3 in `tenant`), dann 167 nach 260911-fh9 (zusätzlich 5 in `auth`), jetzt 178 nach 260911-gwh (zusätzlich 8 in `favorites`, 3 in `settings`). Dies ist der ENDSTAND der Etappe 2: jeder verbleibende ungebundene Rohtreffer ist einer der in diesem Dokument benannten, bewusst ungebundenen Fälle. Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
|
| **Summe** | **68** | **178** | Ungebunden: war 118 nach 260910-das, dann 108 nach 260910-exd (module-registry 17→7), dann 107 nach 260910-jab (`tenders` 36→35, `listForUser` gebunden), dann 95 nach 260910-krx (`dashboard` 13→1), dann 83 nach 260911-cwh (`calendar` 12→0), unverändert nach 260911-e2s (`tenant` bleibt bei 8 ungebundenen Rohtreffern), dann 78 nach 260911-fh9 (`auth` 8→3), jetzt 68 nach 260911-gwh (`favorites` 7→0, `settings` 4→1). Gebunden: war 124, dann 134 nach 260910-exd (zusätzlich 10 in `module-registry`), dann 135 nach 260910-jab (zusätzlich 1 in `tenders`), dann 147 nach 260910-krx (zusätzlich 12 in `dashboard`), dann 159 nach 260911-cwh (zusätzlich 12 in `calendar`), dann 162 nach 260911-e2s (zusätzlich 3 in `tenant`), dann 167 nach 260911-fh9 (zusätzlich 5 in `auth`), jetzt 178 nach 260911-gwh (zusätzlich 8 in `favorites`, 3 in `settings`). Dies ist der ENDSTAND der Etappe 2: jeder verbleibende ungebundene Rohtreffer ist einer der in diesem Dokument benannten, bewusst ungebundenen Fälle. Diese Übersicht ist eine Buchführungshilfe; **autoritativ ist die Fundstellentabelle unten**, die `rls-access-inventory.spec.ts` bei jedem Lauf gegen den Quelltext prüft |
|
||||||
|
|
||||||
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 65 Paare)
|
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 72 Paare)
|
||||||
|
|
||||||
Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf
|
Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf
|
||||||
(260909-ipc) plus ein bisher vollstaendig unsichtbares Paar
|
(260909-ipc) plus ein bisher vollstaendig unsichtbares Paar
|
||||||
@@ -236,13 +255,36 @@ bestaetigt). Zwei Paare aendern nur ihre `Stand`-Spalte, keine ihrer Klasse:
|
|||||||
der ENDSTAND der Etappe 2 — die Zahl ist der Ausgabe von
|
der ENDSTAND der Etappe 2 — die Zahl ist der Ausgabe von
|
||||||
`rls-access-inventory.spec.ts` entnommen, nicht geschaetzt.
|
`rls-access-inventory.spec.ts` entnommen, nicht geschaetzt.
|
||||||
|
|
||||||
|
**Stand 260911-mkj (Aufgabe 1/2):** 72 Paare — 65 aus dem Endstand der
|
||||||
|
Etappe 2 plus SIEBEN neue Paare der vierten Erkennungsform (WINDOWS #27):
|
||||||
|
`groups/module-grants.service.ts`/`module` (`keine-mandantengebundene-tabelle`,
|
||||||
|
`gebunden`), `ldap/ldap-config.service.ts`/`tenant`
|
||||||
|
(`keine-mandantengebundene-tabelle`, `ungebunden`),
|
||||||
|
`module-registry/module-access.service.ts`/`group` UND `/groupMembership`
|
||||||
|
(beide `muss-mandantengebunden`, `gebunden`),
|
||||||
|
`tenders/tender-digest.scheduler.ts`/`tender`
|
||||||
|
(`keine-mandantengebundene-tabelle`, `gebunden`) UND `/tenderSavedSearch`
|
||||||
|
(`muss-mandantengebunden`, `gebunden`),
|
||||||
|
`tenders/tenders.controller.ts`/`tenderSource`
|
||||||
|
(`keine-mandantengebundene-tabelle`, `ungebunden`). Drei Paare aendern ihren
|
||||||
|
Stand, eines davon zusaetzlich die Klasse:
|
||||||
|
`ldap-config.service.ts`/`ldapFieldMapping` wechselt von
|
||||||
|
`muss-mandantengebunden`/`gebunden` auf `beides`/`gemischt` — der
|
||||||
|
uebergreifende Planer-Lesepfad `getAllActiveConfigs()` reicht ueber
|
||||||
|
`include: { fieldMappings: true }` in `LdapFieldMapping` hinein, dieselbe
|
||||||
|
Unterabfrage-Form, die WINDOWS #27 aufgedeckt hat.
|
||||||
|
`module-registry.service.ts`/`module` und `tender-matching.service.ts`/
|
||||||
|
`tender` wechseln je von `ungebunden` auf `gemischt`, ihre Klasse bleibt.
|
||||||
|
Die Zahl 72 ist der Ausgabe von `rls-access-inventory.spec.ts` entnommen,
|
||||||
|
nicht geschaetzt.
|
||||||
|
|
||||||
| Klasse | Anzahl Paare |
|
| Klasse | Anzahl Paare |
|
||||||
|---|---|
|
|---|---|
|
||||||
| muss-mandantengebunden | 33 |
|
| muss-mandantengebunden | 35 |
|
||||||
| keine-mandantengebundene-tabelle | 17 |
|
| keine-mandantengebundene-tabelle | 21 |
|
||||||
| beides | 13 |
|
| beides | 14 |
|
||||||
| bewusst-uebergreifend | 2 |
|
| bewusst-uebergreifend | 2 |
|
||||||
| **Summe** | **65** |
|
| **Summe** | **72** |
|
||||||
|
|
||||||
## Der Hintergrunddienst als Falle — sechs Fälle
|
## Der Hintergrunddienst als Falle — sechs Fälle
|
||||||
|
|
||||||
@@ -278,6 +320,19 @@ beim Start — mit einer zusätzlichen Verdeckungsschicht, siehe unten:
|
|||||||
Zustand zum Zeitpunkt der ldap-Umstellung korrekt beschreibt und für
|
Zustand zum Zeitpunkt der ldap-Umstellung korrekt beschreibt und für
|
||||||
spätere Etappen als Beleg dient, siehe auch
|
spätere Etappen als Beleg dient, siehe auch
|
||||||
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt (e), Befund D.
|
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt (e), Befund D.
|
||||||
|
|
||||||
|
**Nachtrag (260911-mkj):** WINDOWS #27 geschlossen — derselbe Bereich
|
||||||
|
`ldap` traegt
|
||||||
|
einen zweiten, bislang unsichtbaren Planer-Lesezugriff:
|
||||||
|
`LdapConfigService.getAllActiveConfigs()` (`ldap-config.service.ts`)
|
||||||
|
reicht ueber `include: { tenant: true, fieldMappings: true }` sowohl in
|
||||||
|
`Tenant` (schutzlos, harmlos) als auch in `LdapFieldMapping` (geschuetzt
|
||||||
|
ueber Join auf `LdapConfig`) hinein. Nach dem Scharfschalten verstummt
|
||||||
|
die Feldzuordnung mit der Elternzeile, nicht getrennt von ihr — der
|
||||||
|
Etappe-3-Systemkontext muss BEIDE Tabellen sehen. Die Bestandsaufnahme
|
||||||
|
fuehrt `ldapFieldMapping` deshalb seither als `beides`/`gemischt` statt
|
||||||
|
`muss-mandantengebunden`/`gebunden` (siehe Bestandsaufnahme unten,
|
||||||
|
`ldap-config.service.ts`/`ldapFieldMapping`).
|
||||||
- **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest) — **Stand
|
- **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest) — **Stand
|
||||||
260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende
|
260909-laa, Aufgabe 3: Je-Treffer-Hälfte geschlossen, übergreifende
|
||||||
Hälfte an Etappe 3 übergeben.** Liest `tenderMatch` (Kandidatenabfrage,
|
Hälfte an Etappe 3 übergeben.** Liest `tenderMatch` (Kandidatenabfrage,
|
||||||
@@ -473,16 +528,45 @@ oder — seit 260909-ipc, Befund G — in `<gebundener Client>.<Modell>`
|
|||||||
verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
||||||
260909-ipc maschinell gegen den Quelltext geprüft), Begründung.
|
260909-ipc maschinell gegen den Quelltext geprüft), Begründung.
|
||||||
|
|
||||||
**Erkennungslücke, seit 260911-e2s vermessen (Aufgabe 1, (n4)(b)):** die
|
**Erkennungslücke GESCHLOSSEN (260911-mkj, WINDOWS #27):** bis 260911-e2s sah
|
||||||
Bestandsaufnahme sieht ausschließlich (Datei, Modell)-Paare über
|
die Bestandsaufnahme ausschließlich (Datei, Modell)-Paare über
|
||||||
`this.prisma.<Modell>` bzw. `<gebundener Client>.<Modell>` — eine
|
`this.prisma.<Modell>` bzw. `<gebundener Client>.<Modell>` — eine
|
||||||
Relationseinbindung (`include:`, Relationszähler `_count`) in eine ZWEITE
|
Relationseinbindung (`include:`, `select:`, Relationszähler `_count`) in
|
||||||
Tabelle erzeugt kein eigenes Paar und ist für das Werkzeug strukturell
|
eine ZWEITE Tabelle erzeugte kein eigenes Paar und war für das Werkzeug
|
||||||
unsichtbar. Alle 19 `include:`-Stellen und alle `_count`-Stellen des
|
strukturell unsichtbar, obwohl Prisma sie als Unterabfrage/Join auf die
|
||||||
API-Quelltexts wurden einzeln nachgesehen; die einzige gefährliche
|
zweite Tabelle unter DEREN Regel rendert. Eine vierte Erkennungsform in
|
||||||
Ausprägung (ungebundener äußerer Aufruf auf einer UNGESCHÜTZTEN Tabelle,
|
`rls-access-inventory.spec.ts` (`analyzeSource`, `SCHEMA_RELATIONS`) löst
|
||||||
Einbindung in eine GESCHÜTZTE Tabelle) war `tenant.controller.ts` — hier in
|
Relationsfelder über `schema.prisma` auf ihr Zielmodell auf und trägt das
|
||||||
Aufgabe 3 behoben (Fan-out ersetzt den Relationszähler).
|
Zielmodell seither als eigene Fundstelle derselben Datei — gebunden, wenn
|
||||||
|
der Empfänger des erkannten Modellaufrufs gebunden ist, sonst ungebunden.
|
||||||
|
Das erfasst `include:`, `select:`, `_count: { select: ... }`, `_count:
|
||||||
|
true`, Relationsfilter in `where:`, `orderBy:` über Relationen und
|
||||||
|
verschachtelte Schreibzugriffe in `data:`.
|
||||||
|
|
||||||
|
Bewusst NICHT gesehen, und wie das begrenzt ist: ein Empfänger außerhalb der
|
||||||
|
vier Erkennungsformen (`this.prisma`, eine `const X = forTenant(`-Zuweisung,
|
||||||
|
ein Transaktionsparameter, `withTenantTransaction(`) — begrenzt durch den
|
||||||
|
Waechter Rohzahl (`include|select|_count` über den gesamten kommentarfreien
|
||||||
|
Quelltext) gegen die innerhalb erkannter Aufrufe gezählte Zahl, mit
|
||||||
|
begründeter Ausnahmeliste `RELATION_SPEC_EXCEPTIONS`; ein `include:`/
|
||||||
|
`select:`-Wert aus einer fremden Datei oder ohne aufzulösende Konstante —
|
||||||
|
begrenzt durch den Wertform-Waechter (`unresolvedRelationSpecValues`); die
|
||||||
|
Kurzschreibweise `include: { x }` — gemessen null Vorkommen im gesamten
|
||||||
|
API-Quelltext, kein Prisma-Idiom für diese drei Schlüssel; eine
|
||||||
|
Vorlagen-Interpolation (`${tx.user.count()}`) — zählt in der Rohzahl und
|
||||||
|
fällt damit laut, statt still zu verschwinden.
|
||||||
|
|
||||||
|
Gemessen (260911-mkj, Ausgabe von `rls-access-inventory.spec.ts`): SIEBEN
|
||||||
|
neue Paare, DREI fortgeschriebene Stände, EINE Ausnahmedatei
|
||||||
|
(`RELATION_SPEC_EXCEPTIONS`). Die einzige zur Planungszeit bereits bekannte
|
||||||
|
gefährliche Ausprägung (ungebundener äußerer Aufruf auf einer
|
||||||
|
UNGESCHÜTZTEN Tabelle, Einbindung in eine GESCHÜTZTE Tabelle) war
|
||||||
|
`tenant.controller.ts` — bereits in 260911-e2s behoben (Fan-out ersetzt den
|
||||||
|
Relationszähler), von der vierten Erkennung erneut bestätigt: kein
|
||||||
|
Relationszugriff mehr dort. Seit 260911-mkj führt die Spalte `Modell` auch
|
||||||
|
RELATIONSZIELE, die in der Datei selbst nie als `<Klient>.<Modell>` stehen,
|
||||||
|
sondern nur über den Schlüsselpfad eines erkannten Modellaufrufs sichtbar
|
||||||
|
werden.
|
||||||
|
|
||||||
| Datei | Modell | Klasse | Stand | Begründung |
|
| Datei | Modell | Klasse | Stand | Begründung |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
@@ -505,19 +589,23 @@ Aufgabe 3 behoben (Fan-out ersetzt den Relationszähler).
|
|||||||
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; `addUserToDefaultGroup` prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02). Bis 20260910120000_rls_widen_membership_grant_and_platform_read pruefte die Regel auf `GroupMembership` nachweislich nur die Gruppenseite — seit 260910-jab prueft sie beide Seiten, die Anwendungspruefung bleibt trotzdem bestehen (Schalter weiterhin aus, #18). |
|
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | gebunden | Nutzerverwaltung innerhalb eines Mandanten. Seit 260909-jts gebunden; `addUserToDefaultGroup` prueft seither zusaetzlich, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02). Bis 20260910120000_rls_widen_membership_grant_and_platform_read pruefte die Regel auf `GroupMembership` nachweislich nur die Gruppenseite — seit 260910-jab prueft sie beide Seiten, die Anwendungspruefung bleibt trotzdem bestehen (Schalter weiterhin aus, #18). |
|
||||||
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | gebunden | Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — `assertTargetBelongsToTenant` erzeugt seinen eigenen Kontext, `getMatrix` teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen. |
|
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | gebunden | Wie groups.service.ts. Seit 260909-jts (Aufgabe 3) gebunden — `assertTargetBelongsToTenant` erzeugt seinen eigenen Kontext, `getMatrix` teilt sich einen Kontext mit den beiden anderen parallelen Teilabfragen. |
|
||||||
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. Seit 260909-jts gebunden; der `where`-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1). |
|
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. Seit 260909-jts gebunden; der `where`-Filter über die Beziehung zur Gruppe bleibt zusätzlich stehen, weil die Regel auf dieser Tabelle nachweislich nur die Gruppenseite prüft (Aufgabe 1). |
|
||||||
|
| apps/api/src/groups/module-grants.service.ts | module | keine-mandantengebundene-tabelle | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): nur ueber `include: { module: true }` auf `tenantPrisma.tenantModuleActivation.findMany` erreicht (`getMatrix`, Zeilen 196 und 253). `Module` traegt plattformweit keinen Zeilenschutz (Migration 20260909140000, Gruppe b) — die Bindung des Elternaufrufs ist fuer die Unterabfrage wirkungslos, nicht schaedlich, dieselbe Einordnung wie `module-registry.service.ts`/`module` unten. |
|
||||||
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (`assertTargetBelongsToTenant`) bleibt zusätzlich bestehen. Bis 20260910120000_rls_widen_membership_grant_and_platform_read prüfte die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe/den referenzierten Benutzer (Befund F, T-JTS-03, Aufgabe 1) — seit 260910-jab prüft sie beide Ziele zusätzlich, die Anwendungsprüfung bleibt trotzdem der erste Schutz (Schalter weiterhin aus, #18). |
|
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung vor jedem Erteilen (`assertTargetBelongsToTenant`) bleibt zusätzlich bestehen. Bis 20260910120000_rls_widen_membership_grant_and_platform_read prüfte die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe/den referenzierten Benutzer (Befund F, T-JTS-03, Aufgabe 1) — seit 260910-jab prüft sie beide Ziele zusätzlich, die Anwendungsprüfung bleibt trotzdem der erste Schutz (Schalter weiterhin aus, #18). |
|
||||||
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden. |
|
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden. |
|
||||||
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | gebunden | Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen. |
|
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | gebunden | Zielbenutzer eines Grants innerhalb des Mandanten. Seit 260909-jts gebunden; die Mandanten-Gegenprüfung bleibt bestehen. |
|
||||||
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
||||||
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber `forTenant()` (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung. |
|
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | beides | gemischt | Klassenkorrektur (260911-mkj, WINDOWS #27): wechselt von `muss-mandantengebunden` auf `beides`. Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten als Parameter entgegen und laufen vollstaendig ueber `forTenant()` (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung. Die vierte Erkennung (260911-mkj) macht sichtbar, dass der bewusst uebergreifende Planer-Lesepfad `getAllActiveConfigs()` ueber `include: { fieldMappings: true }` in `LdapFieldMapping` hineinreicht — dieselbe Unterabfrage-Form wie die drei `tenant`-Zaehler aus WINDOWS #27, hier aber KEIN neuer Befund: der Elternpfad ist als erster Fall der Hintergrunddienst-Falle bereits an Etappe 3 uebergeben, die Feldzuordnungen teilen nach dem Scharfschalten sein Schicksal (Regel auf `LdapFieldMapping` ueber Join auf `LdapConfig`, Migration 20260618112133). Die Klasse folgt der Elternzeile `ldapConfig` (`beides`). |
|
||||||
|
| apps/api/src/ldap/ldap-config.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `getAllActiveConfigs()` (Zeile 311) `include: { tenant: true, fieldMappings: true }` auf `this.prisma.ldapConfig.findMany`; `Tenant` traegt keinen Zeilenschutz (260911-e2s Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). |
|
||||||
| apps/api/src/ldap/ldap.service.ts | group | beides | gebunden | AD-Abgleich: `listGroups` (die "bereits importiert"-Markierung) und `importGroupsByDn` (die Idempotenzpruefung ueber `ldapObjectGuid`) sind mit Aufgabe 3 (260909-ipc) auf `forTenant()` umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder `group`-Zugriff dieser Datei gebunden. Die Klasse bleibt `beides`, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei `ldapConfig` unten — nicht, weil heute noch ein ungebundener Zugriff bestuende. |
|
| apps/api/src/ldap/ldap.service.ts | group | beides | gebunden | AD-Abgleich: `listGroups` (die "bereits importiert"-Markierung) und `importGroupsByDn` (die Idempotenzpruefung ueber `ldapObjectGuid`) sind mit Aufgabe 3 (260909-ipc) auf `forTenant()` umgestellt — zusammen mit den bereits vorher gebundenen Stellen (Anlage, Mitgliedschafts- und Gruppenabgleich) ist damit jeder `group`-Zugriff dieser Datei gebunden. Die Klasse bleibt `beides`, weil ein zukuenftiger uebergreifender Lesezugriff (z. B. ein neuer Planer-Pfad) hier ebenso legitim waere wie bei `ldapConfig` unten — nicht, weil heute noch ein ungebundener Zugriff bestuende. |
|
||||||
| apps/api/src/ldap/ldap.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `Group`. `syncGroupMembershipsForTenant` laeuft vollstaendig ueber `forTenant()` (Plan 16-03/16-05) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
|
| apps/api/src/ldap/ldap.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `Group`. `syncGroupMembershipsForTenant` laeuft vollstaendig ueber `forTenant()` (Plan 16-03/16-05) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
|
||||||
| apps/api/src/ldap/ldap.service.ts | ldapConfig | beides | gebunden | Die `lastSyncAt`-Fortschreibung am Ende von `syncUsersForTenant` ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen `forTenant()`-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige `ldapConfig`-Zugriff dieser Datei gebunden. |
|
| apps/api/src/ldap/ldap.service.ts | ldapConfig | beides | gebunden | Die `lastSyncAt`-Fortschreibung am Ende von `syncUsersForTenant` ist mit Aufgabe 3 (260909-ipc) auf den in derselben Methode bereits vorhandenen `forTenant()`-Client umgestellt — es entsteht kein zweiter. Damit ist der einzige `ldapConfig`-Zugriff dieser Datei gebunden. |
|
||||||
| apps/api/src/ldap/ldap.service.ts | user | beides | gemischt | Mit Aufgabe 3 (260909-ipc) sind `upsertMappedUser` (Identitaetssuche und Aktualisierung), `searchUsers` (die "bereits importiert"-Markierung), `importUsersByDn` (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in `syncUsersForTenant` auf `forTenant()` umgestellt. `resolveEmailForWrite` bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): `email`/`username` sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere `muss-mandantengebunden` gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage `beides`. Der Loeschzweig um `syncBoundGroupsForTenant` (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds. |
|
| apps/api/src/ldap/ldap.service.ts | user | beides | gemischt | Mit Aufgabe 3 (260909-ipc) sind `upsertMappedUser` (Identitaetssuche und Aktualisierung), `searchUsers` (die "bereits importiert"-Markierung), `importUsersByDn` (Dedup und ldapDn-Nachtrag) und die Deaktivierungsschleife in `syncUsersForTenant` auf `forTenant()` umgestellt. `resolveEmailForWrite` bleibt ausdruecklich UNGEBUNDEN (Befund A, T-IPC-04): `email`/`username` sind plattformweit eindeutig, eine mandantengebundene Suche saehe einen fremden Halter nicht mehr und meldete faelschlich "frei" — die geloeste Klasse waere `muss-mandantengebunden` gewesen, bleibt wegen dieser einen bewusst uebergreifenden Abfrage `beides`. Der Loeschzweig um `syncBoundGroupsForTenant` (WINDOWS #20) ist bereits seit Etappe 1 gebunden und war nie Teil dieses Befunds. |
|
||||||
|
| apps/api/src/module-registry/module-access.service.ts | group | muss-mandantengebunden | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): Relationsfilter `where: { tenantId, group: { memberships: { some: { userId } } } }` auf `tenantPrisma.moduleGrant.findMany` (Zeile 73, `getAccessibleModuleIds`, Gruppenweg). Prisma rendert das als Unterabfrage auf `Group` unter DEREN Regel; der Klient ist gebunden, `Group` gehoert demselben Mandanten. |
|
||||||
|
| apps/api/src/module-registry/module-access.service.ts | groupMembership | muss-mandantengebunden | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): derselbe Relationsfilter wie bei `group` oben, zwei Ebenen tief (`group > memberships`) auf `tenantPrisma.moduleGrant.findMany` (Zeile 73). Prisma rendert das als Unterabfrage auf `GroupMembership` unter DEREN Regel; gebunden, derselbe Mandant. |
|
||||||
| apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. |
|
| apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. |
|
||||||
| apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260910-exd (Aufgabe 2) laufen Direktweg und Gruppenweg von `getAccessibleModuleIds` ueber `forTenant()`, EIN Klient je Methode. |
|
| apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant. Seit 260910-exd (Aufgabe 2) laufen Direktweg und Gruppenweg von `getAccessibleModuleIds` ueber `forTenant()`, EIN Klient je Methode. |
|
||||||
| apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. Seit 260910-exd (Aufgabe 2) laufen Kurzschlusszweig, Schnittmengenabfrage und der eigene Lesezugriff von `getCatalogFlags` ueber `forTenant()`. |
|
| apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. Seit 260910-exd (Aufgabe 2) laufen Kurzschlusszweig, Schnittmengenabfrage und der eigene Lesezugriff von `getCatalogFlags` ueber `forTenant()`. |
|
||||||
| apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit. Bleibt bewusst ungebunden (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. `findBySlug` ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER Modulanfrage aufruft. |
|
| apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | gemischt | Stand-Aenderung (260911-mkj, WINDOWS #27): `ungebunden` -> `gemischt`, Klasse bleibt. Modulkatalog ist plattformweit. Der ungebundene Anteil bleibt bewusst so (260910-exd, Aufgabe 1, Befund E): keine Regel auf der Tabelle, eine Bindung waere heute wirkungslos, nicht katastrophal — katastrophal erst, WENN Etappe 3 dieser Tabelle eine Regel gibt. `findBySlug` ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER Modulanfrage aufruft. Die neu sichtbare gebundene Haelfte stammt ausschliesslich aus `include: { module: true }` auf `tenantPrisma.tenantModuleActivation` (Zeilen 56, 97, 148) — fuer die schutzlose Katalogtabelle wirkungslos, nicht schaedlich. |
|
||||||
| apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant. Seit 260910-exd (Aufgabe 3) laufen `findActiveForTenant`, `activateForTenant`, beide Zugriffe von `deactivateForTenant` (ueber EINEN Klienten) und `isModuleActive` ueber `forTenant()`, je Methode EIN Klient. |
|
| apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Aktivierung je Mandant. Seit 260910-exd (Aufgabe 3) laufen `findActiveForTenant`, `activateForTenant`, beide Zugriffe von `deactivateForTenant` (ueber EINEN Klienten) und `isModuleActive` ueber `forTenant()`, je Methode EIN Klient. |
|
||||||
| apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | gemischt | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. Seit 260911-gwh (Aufgabe 2) laufen `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` ueber `forTenant()`. Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (WINDOWS #30, sechster Fall der Hintergrunddienst-Falle) — keine uebersehene Fundstelle, dieselbe Form wie `dkv.service.ts`/`dkvModuleConfig`. Befund K (`tender-mail.service.ts`/`dkv-mail.service.ts` haengen an `getDecryptedSmtpConfig`) ist damit erfuellt. |
|
| apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | gemischt | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. Seit 260911-gwh (Aufgabe 2) laufen `getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig` ueber `forTenant()`. Die Mischung stammt ausschliesslich vom einen benannten, bewusst ungebundenen Planer-Startpfad `loadAnySmtpConfigForStartupTransport()` (WINDOWS #30, sechster Fall der Hintergrunddienst-Falle) — keine uebersehene Fundstelle, dieselbe Form wie `dkv.service.ts`/`dkvModuleConfig`. Befund K (`tender-mail.service.ts`/`dkv-mail.service.ts` haengen an `getDecryptedSmtpConfig`) ist damit erfuellt. |
|
||||||
| apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | `Tenant` ist die Mandantentabelle selbst — hat keine eigene `tenantId`-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b). Gemessen 260911-e2s (Aufgabe 1, Prüfung 1/2): keine Regel auf `Tenant` in irgendeiner der 34 ausgelieferten Migrationen einschließlich `20260910120000_rls_widen_membership_grant_and_platform_read`; gebunden und ungebunden liefern über Roh-SQL UND generierten Client dieselben Zeilen. |
|
| apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | `Tenant` ist die Mandantentabelle selbst — hat keine eigene `tenantId`-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b). Gemessen 260911-e2s (Aufgabe 1, Prüfung 1/2): keine Regel auf `Tenant` in irgendeiner der 34 ausgelieferten Migrationen einschließlich `20260910120000_rls_widen_membership_grant_and_platform_read`; gebunden und ungebunden liefern über Roh-SQL UND generierten Client dieselben Zeilen. |
|
||||||
@@ -527,14 +615,16 @@ Aufgabe 3 behoben (Fan-out ersetzt den Relationszähler).
|
|||||||
| apps/api/src/tenders/adapters/rss.adapter.ts | tenderRssFeedSource | bewusst-uebergreifend | ungebunden | Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter. |
|
| apps/api/src/tenders/adapters/rss.adapter.ts | tenderRssFeedSource | bewusst-uebergreifend | ungebunden | Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter. |
|
||||||
| apps/api/src/tenders/tender-dedup.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03) |
|
| apps/api/src/tenders/tender-dedup.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03) |
|
||||||
| apps/api/src/tenders/tender-dedup.service.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
| apps/api/src/tenders/tender-dedup.service.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
||||||
|
| apps/api/src/tenders/tender-digest.scheduler.ts | tender | keine-mandantengebundene-tabelle | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `include: { tender: true, savedSearch: true }` auf `tenantPrisma.tenderMatch.findMany` (Zeile 149) innerhalb der Mandantenschleife des Planers. `Tender` ist der plattformglobale Katalog (D-03) — die Bindung des Elternaufrufs ist fuer die Unterabfrage wirkungslos, nicht schaedlich. |
|
||||||
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | gemischt | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf) — seit 260909-laa (Aufgabe 3) bleibt die Kandidatenabfrage (`findMany` mit `distinct`) bewusst ungebunden, die Je-Treffer-Abfrage (`findMany` nach Mandant) und die Stempelung (`updateMany`) je Kandidatenzeile laufen über `forTenant()`. |
|
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | gemischt | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf) — seit 260909-laa (Aufgabe 3) bleibt die Kandidatenabfrage (`findMany` mit `distinct`) bewusst ungebunden, die Je-Treffer-Abfrage (`findMany` nach Mandant) und die Stempelung (`updateMany`) je Kandidatenzeile laufen über `forTenant()`. |
|
||||||
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | gebunden | Präferenzen werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten der jeweiligen Zeile. |
|
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | gebunden | Präferenzen werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten der jeweiligen Zeile. |
|
||||||
|
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderSavedSearch | muss-mandantengebunden | gebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `include: { savedSearch: true }` plus `orderBy` ueber `savedSearch` auf `tenantPrisma.tenderMatch.findMany` (Zeile 149) innerhalb der Mandantenschleife des Planers. `TenderSavedSearch` ist mandantengebunden und ueber denselben gebundenen Klienten erreicht. |
|
||||||
| apps/api/src/tenders/tender-digest.scheduler.ts | user | beides | gebunden | E-Mail-Adressen für den Versand werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`. |
|
| apps/api/src/tenders/tender-digest.scheduler.ts | user | beides | gebunden | E-Mail-Adressen für den Versand werden je Kandidatenzeile gelesen. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`. |
|
||||||
| apps/api/src/tenders/tender-email-config.service.ts | tenderEmailConfig | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigene Postfachanbindung (Phase 17, D-01) — anders als der Fan-out-Adapter oben, hier ist der Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getConfigForApi` (beide Lesezugriffe), `saveConfig` und `testConnection` vollständig über `forTenant()`. |
|
| apps/api/src/tenders/tender-email-config.service.ts | tenderEmailConfig | muss-mandantengebunden | gebunden | Nutzer-CRUD für die eigene Postfachanbindung (Phase 17, D-01) — anders als der Fan-out-Adapter oben, hier ist der Mandant aus der Anfrage bekannt. Seit 260909-laa (Aufgabe 2) laufen `getConfigForApi` (beide Lesezugriffe), `saveConfig` und `testConnection` vollständig über `forTenant()`. |
|
||||||
| apps/api/src/tenders/tender-fingerprint-backfill.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Einmaliges Backfill-Skript über den plattformweiten `Tender`-Katalog (D-03). |
|
| apps/api/src/tenders/tender-fingerprint-backfill.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Einmaliges Backfill-Skript über den plattformweiten `Tender`-Katalog (D-03). |
|
||||||
| apps/api/src/tenders/tender-ingestion.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "Multi-tenant safety (D-03, T-10-09): uses the plain, non-tenant-scoped ... queries ... these are platform-global". |
|
| apps/api/src/tenders/tender-ingestion.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "Multi-tenant safety (D-03, T-10-09): uses the plain, non-tenant-scoped ... queries ... these are platform-global". |
|
||||||
| apps/api/src/tenders/tender-ingestion.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, kein `tenantId` (Migration 20260909140000, Gruppe b). |
|
| apps/api/src/tenders/tender-ingestion.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, kein `tenantId` (Migration 20260909140000, Gruppe b). |
|
||||||
| apps/api/src/tenders/tender-matching.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein `tenantId`. |
|
| apps/api/src/tenders/tender-matching.service.ts | tender | keine-mandantengebundene-tabelle | gemischt | Stand-Aenderung (260911-mkj, WINDOWS #27): `ungebunden` -> `gemischt`, Klasse bleibt. Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein `tenantId`. Die neu sichtbare gebundene Haelfte stammt aus `include: { tender: true }` auf `tenantPrisma.tenderMatch.findMany` (Zeile 139) im Sofortmeldungs-Dispatch — fuer die schutzlose Katalogtabelle wirkungslos, nicht schaedlich. |
|
||||||
| apps/api/src/tenders/tender-matching.service.ts | tenderMatch | beides | gebunden | Sofortmeldung — siehe Abschnitt "Der Hintergrunddienst als Falle". Seit 260909-laa (Aufgabe 3) laufen sowohl die Treffer-Anlage (`upsert`) als auch der Instant-Dispatch (`findMany`/`updateMany`) vollständig über `forTenant()`, EIN gebundener Client je Profil. |
|
| apps/api/src/tenders/tender-matching.service.ts | tenderMatch | beides | gebunden | Sofortmeldung — siehe Abschnitt "Der Hintergrunddienst als Falle". Seit 260909-laa (Aufgabe 3) laufen sowohl die Treffer-Anlage (`upsert`) als auch der Instant-Dispatch (`findMany`/`updateMany`) vollständig über `forTenant()`, EIN gebundener Client je Profil. |
|
||||||
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert). |
|
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Gespeicherte Suchprofile ALLER Mandanten werden gegen neue Treffer geprüft — bewusst übergreifend, Etappe-3-Übergabe (260909-laa, Aufgabe 3, unverändert). |
|
||||||
| apps/api/src/tenders/tender-matching.service.ts | user | beides | gebunden | E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten des jeweiligen Profils. |
|
| apps/api/src/tenders/tender-matching.service.ts | user | beides | gebunden | E-Mail-Adressen für die Sofortmeldung. Seit 260909-laa (Aufgabe 3) läuft der einzige Zugriff über `forTenant()`, gebunden an den Mandanten des jeweiligen Profils. |
|
||||||
@@ -544,6 +634,7 @@ Aufgabe 3 behoben (Fan-out ersetzt den Relationszähler).
|
|||||||
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
|
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
|
||||||
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen `setTriage`/`listForUser`/`favoriteIds` vollständig über `forTenant()`. |
|
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | gebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. Seit 260909-laa (Aufgabe 2) laufen `setTriage`/`listForUser`/`favoriteIds` vollständig über `forTenant()`. |
|
||||||
| apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). |
|
| apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). |
|
||||||
|
| apps/api/src/tenders/tenders.controller.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | NEUE Fundstelle (260911-mkj, WINDOWS #27): `getTender` (Zeile 612) `include: { sources: { select: ... } }` auf `this.prisma.tender.findUnique`; `TenderSource` plattformweit ohne Zeilenschutz (dieselbe Einordnung wie `tender-dedup.service.ts`/`tenderSource`). |
|
||||||
| apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. |
|
| apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. |
|
||||||
| apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. |
|
| apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. |
|
||||||
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an und liest beim Start alle Mandanten fuer die Standardgruppen-Reparatur — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). Fuenfter und bislang einziger bereits vollstaendig richtiger Fall der Hintergrunddienst-Falle (Befund K, siehe Abschnitt unten). |
|
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an und liest beim Start alle Mandanten fuer die Standardgruppen-Reparatur — `Tenant` hat keine `tenantId`-Spalte und traegt keinen Zeilenschutz (Aufgabe 1, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). Fuenfter und bislang einziger bereits vollstaendig richtiger Fall der Hintergrunddienst-Falle (Befund K, siehe Abschnitt unten). |
|
||||||
|
|||||||
Reference in New Issue
Block a user