feat(quick-260910-krx): Suchmaschinen gebunden, Katalog begruendet offen, Klassifikation nachgezogen

Aufgabe 3 — TDD zuerst (7 weitere Faelle in dashboard.service.spec.ts, 20
vorher/27 nach dieser Aufgabe), dann die Umstellung:

- getSearchProviders/addSearchProvider/removeSearchProvider laufen ueber
  forTenant(); removeSearchProvider fuehrt Besitzpruefung UND Schreibzugriff
  ueber DENSELBEN gebundenen Klienten. Die drei Vorgabe-Suchmaschinen aus
  der Konstante bleiben unveraendert vorangestellt.
- Der eine Katalogzugriff (this.prisma.module in getWidgets) bleibt
  begruendet ungebunden: Messung und Bedingung getrennt (Tabelle traegt
  heute keinen Zeilenschutz, wirkungslos statt katastrophal — katastrophal
  erst, wenn Etappe 3 eine Regel gibt), unter Berufung auf die bestehende
  Werkzeugpruefung module-tabelle-traegt-keinen-zeilenschutz statt einer
  neuen Behauptung. Ein Wachhund-Testfall haelt den Katalogzugriff aus dem
  Bindungsprotokoll heraus (und beweist zuerst, dass der Katalogpfad
  tatsaechlich durchlaufen wird, nicht nur theoretisch geprueft ist).
- docs/mandantentrennung-zugriffsklassifikation.md an allen fuenf
  handgepflegten Stellen nachgezogen: vier Bestandsaufnahme-Zeilen (inkl.
  eigenstaendiger Nachpruefung der widerlegten SearchProvider-Praemisse),
  Uebersichtszeile (13/0 -> 1/12), Summenzeile (95/147), Klassen-Verteilung
  (unveraendert 63 Paare, ausdruecklich vermerkt), Hintergrunddienst-
  Abschnitt (dashboard hat keinen sechsten Fall, mit Messanweisung), "Was
  diese Etappe NICHT entscheidet" (dienst-interner forTenant()-Weg wie alle
  sieben Bereiche vor ihm).
- .planning/WINDOWS.md traegt Eintrag #25 (offen, Tabelle + JSON): die
  beweisvernichtende Schleife (leeres Dashboard -> Neuaufbau ->
  automatisches Zurueckschreiben -> ueberschriebene Anordnung, Widget-
  Dubletten) samt der Vorabpruefung fuer Etappe 4 und dem Verweis auf #22
  fuer die verwandte Eindeutigkeitsfrage.

Zwei weitere Falsifizierungsnachweise durchgefuehrt: (1) den Katalogzugriff
probeweise gebunden (tenantPrisma.module.findMany) — acht Tests werden rot
mit "TypeError: Cannot read properties of undefined (reading 'findMany')",
weil `module` bewusst nicht in der Testdouble-Bindungsliste steht; Rueckbau
zurueckgenommen, 27/27 wieder gruen. (2) den Stand von dashboardLayout in
der Klassifikationsdatei probeweise auf "ungebunden" gesetzt —
rls-access-inventory.spec.ts wird rot mit "Abweichender Stand (Dokument vs.
Quelltext): ... dokumentiert=ungebunden, gemessen=gebunden"; Ruecknahme,
Testlauf wieder gruen (10/10).

Baseline gehalten: 858 Tests / 56 Dateien gruen, Typpruefung sauber,
Wegwerf-Werkzeug 87/87. Schalter bleibt aus.
This commit is contained in:
2026-09-11 09:03:23 +02:00
parent e0ce594c5a
commit 67b50240d6
5 changed files with 260 additions and 25 deletions
+36 -8
View File
@@ -274,9 +274,13 @@ export class DashboardService {
/**
* Returns the three default providers merged with any user-custom providers.
* Defaults are always returned even with an empty DB (no seed migration needed).
* The three defaults come from the TypeScript constant above (decision
* 05-02), never from the database — they are unaffected by the binding
* below and are always prepended unchanged.
*/
async getSearchProviders(userId: string) {
const custom = await this.prisma.searchProvider.findMany({
async getSearchProviders(userId: string, tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId);
const custom = await tenantPrisma.searchProvider.findMany({
where: { userId },
orderBy: { createdAt: 'asc' },
});
@@ -285,14 +289,19 @@ export class DashboardService {
}
/**
* Creates a user-custom search provider.
* Creates a user-custom search provider. `tenantId` stays a required
* parameter of this method — the only write path this model has (260910-krx,
* Aufgabe 1, Befund F, WINDOWS #19): no application path exists that
* creates a tenant-less row, which is why the RLS rule on `SearchProvider`
* was deliberately left unchanged/strict in migration 20260910120000.
*/
async addSearchProvider(
userId: string,
tenantId: string,
dto: CreateSearchProviderDto,
) {
return this.prisma.searchProvider.create({
const tenantPrisma = forTenant(this.prisma, tenantId);
return tenantPrisma.searchProvider.create({
data: {
userId,
tenantId,
@@ -305,11 +314,14 @@ export class DashboardService {
/**
* Removes a user-custom search provider.
* Verifies ownership — default providers (userId null) cannot be deleted (T-05-07).
* Verifies ownership — default providers (userId null) cannot be deleted
* (T-05-07) — REAL, same reasoning as `updateWidgetConfig`/`removeWidget`
* above: both queries run over the SAME bound client and tenant id.
*/
async removeSearchProvider(id: string, userId: string) {
async removeSearchProvider(id: string, userId: string, tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId);
// Default providers have hardcoded IDs that won't exist in DB
const provider = await this.prisma.searchProvider.findUnique({
const provider = await tenantPrisma.searchProvider.findUnique({
where: { id },
});
@@ -319,8 +331,24 @@ export class DashboardService {
);
}
return this.prisma.searchProvider.delete({
return tenantPrisma.searchProvider.delete({
where: { id },
});
}
}
// --- Modulkatalog: bewusst ungebunden (260910-krx, Aufgabe 3) --------------
//
// Der eine verbleibende ungebundene Modellzugriff dieser Datei (das
// `module`-Modell in `getWidgets`, ueber den ungebundenen Basisclient)
// betrifft den plattformweiten Modulkatalog (`Module`).
// MESSUNG (rls-scratch-check.mjs, Pruefung `module-tabelle-traegt-keinen-
// zeilenschutz`, uebernommen aus dem Bereich `module-registry`, 260910-exd
// Befund E): die Tabelle traegt heute KEINEN Zeilenschutz — `pg_class.
// relrowsecurity` ist `false`, eine Bindung waere heute WIRKUNGSLOS, nicht
// katastrophal. BEDINGUNG: sie wuerde katastrophal, WENN Etappe 3 dieser
// Tabelle eine Regel gibt — dann verschwaende der gesamte Katalog fuer jeden
// Mandanten. Die Katalogaufloesung, die dieser Dienst fuer den Widget-
// Modulfilter aufruft (`ModuleAccessService.getAccessibleModuleIds`), bindet
// bereits seit 260910-exd in ihrem eigenen Dienst — dieser Zugriff wird hier
// NICHT ein zweites Mal gebunden.