feat(quick-260910-exd): ModuleRegistryService binden, Klassifikation abschliessen
- module-registry.service.ts: findActiveForTenant, activateForTenant, isModuleActive binden je einen Aktivierungszugriff, deactivateForTenant bindet beide (Lesen+Schreiben) ueber EINEN Klienten unter tenantPrisma; alle sechs Katalogzugriffe (findAll/findBySlug/beide Existenzpruefungen/isModuleActive-Katalogsuche/seedModule) bleiben bewusst ungebunden, mit Kommentar der Messung von Bedingung trennt - isModuleActive-Kopfkommentar richtiggestellt: der Waechter ruft sie nicht auf (0 Aufrufer, TEIL 3 von Aufgabe 1) — Waechter nimmt findBySlug + ModuleAccessService.getAccessibleModuleIds - module-registry.service.spec.ts: NEU, Zwei-Klienten-Nachweis, deckt die bislang ungetestete Datei mit elf der siebzehn Zugriffe des Bereichs ab, inkl. der lauten (deactivate ohne Aktivierung) und stillen (isModuleActive ohne Aktivierung) Richtung und dem Katalog-Wachhund - tender-scheduler.service.spec.ts: forTenant() auf Identitaet gemockt (dieselbe Konvention wie ldap.service.spec.ts) — cross-area Bruch durch die Umstellung von activateForTenant behoben (Rule 1/3) - docs/mandantentrennung-zugriffsklassifikation.md: alle fuenf handgepflegten Stellen nachgezogen (Bestandsaufnahme, Uebersichtszeile 7/10, Summenzeile 108/134, Klassen-Verteilung unveraendert bei 63 Paaren, Hintergrunddienst-Abschnitt haelt die Abwesenheit eines sechsten Falls fest) — alle gemessen, nicht abgeschrieben, Befund K haelt exakt - docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag mit tatsaechlich umgesetzten Pfaden, beiden Falsifizierungsnachweisen (Testname+Meldung), und der Feststellung zum unveraenderten Controller-Kommentar - .planning/WINDOWS.md: neuer offener Eintrag #23 (deviation) — kein Signal unterscheidet "keine Freigabe" von "Abfrage fand nichts", mit Vorabpruefung fuer Etappe 4 und begruendeter Verwerfung einer Laufzeitwarnung - 833 Tests gruen (56 Dateien), Typpruefung sauber, Wegwerf-Werkzeug 66/66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
+16
-3
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
open_count: 5
|
open_count: 6
|
||||||
waived_count: 1
|
waived_count: 1
|
||||||
fixed_count: 16
|
fixed_count: 16
|
||||||
total_count: 22
|
total_count: 23
|
||||||
last_updated: 2026-09-10T08:17:20.009Z
|
last_updated: 2026-09-10T09:28:45.310Z
|
||||||
---
|
---
|
||||||
|
|
||||||
# Broken Windows Ledger
|
# Broken Windows Ledger
|
||||||
@@ -37,6 +37,7 @@ last_updated: 2026-09-10T08:17:20.009Z
|
|||||||
| 20 | 2 | unmet-truth | apps/api/src/prisma/prisma-tenant.extension.ts | | forTenant() setzt den Mandantenkontext auf einer anderen Verbindung als die Abfrage laeuft — die Mandantentrennung hat damit nie funktioniert, auch nicht dort, wo sie scheinbar benutzt wird. Die Erweiterung oeffnet prisma.$transaction, setzt app.current_tenant per set_config(..., true) auf tx, ruft dann aber query(args) auf, das ueber den AEUSSEREN Client dispatcht. set_config mit local=true gilt nur in der Transaktion und nur auf deren Verbindung. Am 2026-09-09 gegen die lokale Datenbank reproduziert: set_config landete auf Backend-PID 254999, die eigentliche Abfrage auf 255000, und dort war current_setting('app.current_tenant') NULL. Heute ohne sichtbare Folge, weil die Anwendungsrolle BYPASSRLS hat (#18) und deshalb ohnehin alles sieht. NACH dem Scharfschalten kehrt sich das um: die betroffenen Abfragen liefern dann NULL ZEILEN statt zu vieler. Besonders gefaehrlich in ldap.service.ts (Loeschzweig um Zeile 1559): der Sync deutet die Leere als 'Gruppe im Verzeichnis verschwunden' und loescht sie samt Mitgliedschaften und Modulfreigaben — aus einem stillen Trennungsfehler wuerde stiller Datenverlust. Zusatzbefund: von den 36 vermeintlichen forTenant-Vorkommen sind die meisten Kommentare, die erklaeren, warum forTenant FEHLT; echte Aufrufstellen sind 6, echte mandantengebundene Abfragen 9, alle in ldap.service.ts. Ausserdem setzen tenant.middleware.ts:44 und tenant.guard.ts:41 ein req.tenantPrisma, das in apps/api/src von NIEMANDEM gelesen wird. Muss vor jedem weiteren Umbau repariert werden, sonst baut alles Weitere auf einem Helfer auf, der nicht traegt. NACHTRAG (260909-eor, Aufgabe 1/4): der beschriebene Verbindungsfehler ist behoben (Array-Form von $transaction, prisma-tenant.extension.ts) und gegen eine Wegwerf-Datenbank mit einer Rolle ohne BYPASSRLS live nachgewiesen (rls-scratch-check.mjs, 8/8 Pruefungen bestanden). Bleibt dennoch bewusst OPEN, nicht fixed: die Wirkung unter der echten Anwendungsrolle tessera_app ist erst nach dem Scharfschalten (#18) beobachtbar — bis dahin bleibt #20 an dieselbe Bedingung gebunden wie #18 und #19. | open | | 2026-09-09T08:44:18.496Z | |
|
| 20 | 2 | unmet-truth | apps/api/src/prisma/prisma-tenant.extension.ts | | forTenant() setzt den Mandantenkontext auf einer anderen Verbindung als die Abfrage laeuft — die Mandantentrennung hat damit nie funktioniert, auch nicht dort, wo sie scheinbar benutzt wird. Die Erweiterung oeffnet prisma.$transaction, setzt app.current_tenant per set_config(..., true) auf tx, ruft dann aber query(args) auf, das ueber den AEUSSEREN Client dispatcht. set_config mit local=true gilt nur in der Transaktion und nur auf deren Verbindung. Am 2026-09-09 gegen die lokale Datenbank reproduziert: set_config landete auf Backend-PID 254999, die eigentliche Abfrage auf 255000, und dort war current_setting('app.current_tenant') NULL. Heute ohne sichtbare Folge, weil die Anwendungsrolle BYPASSRLS hat (#18) und deshalb ohnehin alles sieht. NACH dem Scharfschalten kehrt sich das um: die betroffenen Abfragen liefern dann NULL ZEILEN statt zu vieler. Besonders gefaehrlich in ldap.service.ts (Loeschzweig um Zeile 1559): der Sync deutet die Leere als 'Gruppe im Verzeichnis verschwunden' und loescht sie samt Mitgliedschaften und Modulfreigaben — aus einem stillen Trennungsfehler wuerde stiller Datenverlust. Zusatzbefund: von den 36 vermeintlichen forTenant-Vorkommen sind die meisten Kommentare, die erklaeren, warum forTenant FEHLT; echte Aufrufstellen sind 6, echte mandantengebundene Abfragen 9, alle in ldap.service.ts. Ausserdem setzen tenant.middleware.ts:44 und tenant.guard.ts:41 ein req.tenantPrisma, das in apps/api/src von NIEMANDEM gelesen wird. Muss vor jedem weiteren Umbau repariert werden, sonst baut alles Weitere auf einem Helfer auf, der nicht traegt. NACHTRAG (260909-eor, Aufgabe 1/4): der beschriebene Verbindungsfehler ist behoben (Array-Form von $transaction, prisma-tenant.extension.ts) und gegen eine Wegwerf-Datenbank mit einer Rolle ohne BYPASSRLS live nachgewiesen (rls-scratch-check.mjs, 8/8 Pruefungen bestanden). Bleibt dennoch bewusst OPEN, nicht fixed: die Wirkung unter der echten Anwendungsrolle tessera_app ist erst nach dem Scharfschalten (#18) beobachtbar — bis dahin bleibt #20 an dieselbe Bedingung gebunden wie #18 und #19. | open | | 2026-09-09T08:44:18.496Z | |
|
||||||
| 21 | 2 | deviation | apps/api/src/dkv/dkv-scheduler.service.ts | | DKV-Planer-Startpfad (DkvSchedulerService.onModuleInit -> DkvService.loadAnyActiveConfigForScheduler, vormals loadConfig() ohne Mandant) bleibt bewusst UNGEBUNDEN, als benannte Altlast aus 07-04 (260909-mir, Befund D). Zwei Zustaende, beide gehoeren genannt: HEUTE bereits falsch -- findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die uebrigen NIE (isActive-Pruefung kann den Planer sogar ganz leer laufen lassen, wenn ausgerechnet die gezogene Zeile inaktiv ist, obwohl ein zweiter Mandant aktiv waere). NACH DEM SCHARFSCHALTEN (#18) verstummt sie zusaetzlich -- dieselbe Abfrage liefert dann null, der Planer protokolliert 'no active config found' und richtet fuer JEDEN Mandanten nichts ein, ohne Alarm. Drei erwogene Formen prufen: (a) an einen konkret aufgeloesten Mandanten binden -- nicht moeglich, onModuleInit() hat beim Boot strukturell keinen Mandantenkontext. (b) Umbau auf einmal-abfragen-viele-bedienen -- abgelehnt, das ist die in 07-04 zurueckgestellte Mehrmandanten-Planung (neue Auftragsverwaltung je Mandant statt des heutigen setInterval() mit GENAU EINEM Auftrag) und damit eine Funktionsaenderung, kein Bindungsumbau. (c) Als benannte Altlast weiterfuehren, mit Markierung -- GEWAEHLT, Praezedenzfall LdapConfigService.getAllActiveConfigs() (260909-ipc, Befund B). Die Unsymmetrie zu diesem Praezedenzfall: getAllActiveConfigs ist HEUTE korrekt und verstummt erst spaeter: der DKV-Planer ist HEUTE bereits falsch UND verstummt zusaetzlich spaeter. Markierung dreifach: eigene benannte Methode loadAnyActiveConfigForScheduler() mit Kopfkommentar (dkv.service.ts), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Abschnitt (d4) in docs/mandantentrennung-etappe2-fehlerrichtung.md. Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (rls-preflight.mjs), NICHT in diesen Durchlauf. | open | | 2026-09-09T14:41:07.256Z | |
|
| 21 | 2 | deviation | apps/api/src/dkv/dkv-scheduler.service.ts | | DKV-Planer-Startpfad (DkvSchedulerService.onModuleInit -> DkvService.loadAnyActiveConfigForScheduler, vormals loadConfig() ohne Mandant) bleibt bewusst UNGEBUNDEN, als benannte Altlast aus 07-04 (260909-mir, Befund D). Zwei Zustaende, beide gehoeren genannt: HEUTE bereits falsch -- findFirst() ohne jede Bedingung zieht bei mehreren Mandanten EINEN beliebigen und bedient die uebrigen NIE (isActive-Pruefung kann den Planer sogar ganz leer laufen lassen, wenn ausgerechnet die gezogene Zeile inaktiv ist, obwohl ein zweiter Mandant aktiv waere). NACH DEM SCHARFSCHALTEN (#18) verstummt sie zusaetzlich -- dieselbe Abfrage liefert dann null, der Planer protokolliert 'no active config found' und richtet fuer JEDEN Mandanten nichts ein, ohne Alarm. Drei erwogene Formen prufen: (a) an einen konkret aufgeloesten Mandanten binden -- nicht moeglich, onModuleInit() hat beim Boot strukturell keinen Mandantenkontext. (b) Umbau auf einmal-abfragen-viele-bedienen -- abgelehnt, das ist die in 07-04 zurueckgestellte Mehrmandanten-Planung (neue Auftragsverwaltung je Mandant statt des heutigen setInterval() mit GENAU EINEM Auftrag) und damit eine Funktionsaenderung, kein Bindungsumbau. (c) Als benannte Altlast weiterfuehren, mit Markierung -- GEWAEHLT, Praezedenzfall LdapConfigService.getAllActiveConfigs() (260909-ipc, Befund B). Die Unsymmetrie zu diesem Praezedenzfall: getAllActiveConfigs ist HEUTE korrekt und verstummt erst spaeter: der DKV-Planer ist HEUTE bereits falsch UND verstummt zusaetzlich spaeter. Markierung dreifach: eigene benannte Methode loadAnyActiveConfigForScheduler() mit Kopfkommentar (dkv.service.ts), fortgeschriebener Kopfkommentar in dkv-scheduler.service.ts, Abschnitt (d4) in docs/mandantentrennung-etappe2-fehlerrichtung.md. Signal fuer das Verstummen gehoert in die Vorabpruefung von Etappe 4 (rls-preflight.mjs), NICHT in diesen Durchlauf. | open | | 2026-09-09T14:41:07.256Z | |
|
||||||
| 22 | quick-260910-das | deviation | apps/api/src/user/user.service.ts | | Plattformweite Eindeutigkeit von username/email (kein tenantId-Anteil im Unique-Index): die gemessene Kette unsichtbare Zeile -> falsches 'frei' -> harter Eindeutigkeitsfehler (SQLSTATE 23505) ist in dieser Etappe im Anwendungscode entschaerft (Konfliktmeldung bei create/update, Startsperre in admin-seed.service.ts abgefangen), nicht an der Ursache geloest. Die ehrliche Reparatur waere eine Schemaaenderung (Eindeutigkeit mit Mandantendimension) und ist als Produktentscheidung fuer Etappe 3 vorgemerkt. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich user' (u1/u4). | open | | 2026-09-10T08:17:20.009Z | |
|
| 22 | quick-260910-das | deviation | apps/api/src/user/user.service.ts | | Plattformweite Eindeutigkeit von username/email (kein tenantId-Anteil im Unique-Index): die gemessene Kette unsichtbare Zeile -> falsches 'frei' -> harter Eindeutigkeitsfehler (SQLSTATE 23505) ist in dieser Etappe im Anwendungscode entschaerft (Konfliktmeldung bei create/update, Startsperre in admin-seed.service.ts abgefangen), nicht an der Ursache geloest. Die ehrliche Reparatur waere eine Schemaaenderung (Eindeutigkeit mit Mandantendimension) und ist als Produktentscheidung fuer Etappe 3 vorgemerkt. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich user' (u1/u4). | open | | 2026-09-10T08:17:20.009Z | |
|
||||||
|
| 23 | quick-260910-exd | deviation | apps/api/src/module-registry/module-access.service.ts | | Es gibt heute KEIN Signal, das 'wirklich keine Freigabe' (USER hat tatsaechlich keinen Grant) von 'die Abfrage hat nichts gefunden' (z. B. eine nach dem Scharfschalten ungebunden gebliebene Abfrage) unterscheidet: dieselbe ForbiddenException-Meldung im Waechter, dieselbe leere Modulliste mit Status 200, kein Protokolleintrag. Nach dem Scharfschalten ist ein zu kleines Ergebnis in diesem Bereich TOTAL und lautlos -- JEDER Benutzer JEDES Mandanten verliert gleichzeitig jedes Modul, waehrend die Aktivierungs- und Freigabetabellen weiterhin Zeilen halten -- und sieht fuer den Betroffenen wie ein absichtlicher Rechteentzug aus, nicht wie ein Fehler (der Betroffene hat eine fertige, falsche Erklaerung zur Hand und meldet deshalb keinen Fehler). Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): aktive Aktivierungszeilen vorhanden, aber die Aufloesung liefert fuer einen bekannten Administrator eine leere Menge. Eine Laufzeitwarnung an den betroffenen Stellen wurde erwogen und begruendet verworfen (Dauerlaerm auf einer frischen Installation, dieselbe Begruendung wie bei getAllActiveConfigs im Bereich ldap und den fuenf Stellen im Bereich tenders). In module.guard.spec.ts als Testfall festgenagelt, damit die Aufzeichnung rot wird, sobald jemand ein unterscheidendes Signal einbaut. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich module-registry' (m3). | open | | 2026-09-10T09:28:45.310Z | |
|
||||||
|
|
||||||
````json
|
````json
|
||||||
[
|
[
|
||||||
@@ -303,6 +304,18 @@ last_updated: 2026-09-10T08:17:20.009Z
|
|||||||
"reason": "",
|
"reason": "",
|
||||||
"recorded_at": "2026-09-10T08:17:20.009Z",
|
"recorded_at": "2026-09-10T08:17:20.009Z",
|
||||||
"resolved_at": null
|
"resolved_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"kind": "deviation",
|
||||||
|
"phase": "quick-260910-exd",
|
||||||
|
"file": "apps/api/src/module-registry/module-access.service.ts",
|
||||||
|
"line": null,
|
||||||
|
"description": "Es gibt heute KEIN Signal, das 'wirklich keine Freigabe' (USER hat tatsaechlich keinen Grant) von 'die Abfrage hat nichts gefunden' (z. B. eine nach dem Scharfschalten ungebunden gebliebene Abfrage) unterscheidet: dieselbe ForbiddenException-Meldung im Waechter, dieselbe leere Modulliste mit Status 200, kein Protokolleintrag. Nach dem Scharfschalten ist ein zu kleines Ergebnis in diesem Bereich TOTAL und lautlos -- JEDER Benutzer JEDES Mandanten verliert gleichzeitig jedes Modul, waehrend die Aktivierungs- und Freigabetabellen weiterhin Zeilen halten -- und sieht fuer den Betroffenen wie ein absichtlicher Rechteentzug aus, nicht wie ein Fehler (der Betroffene hat eine fertige, falsche Erklaerung zur Hand und meldet deshalb keinen Fehler). Konkrete Vorabpruefung fuer Etappe 4 (rls-preflight.mjs): aktive Aktivierungszeilen vorhanden, aber die Aufloesung liefert fuer einen bekannten Administrator eine leere Menge. Eine Laufzeitwarnung an den betroffenen Stellen wurde erwogen und begruendet verworfen (Dauerlaerm auf einer frischen Installation, dieselbe Begruendung wie bei getAllActiveConfigs im Bereich ldap und den fuenf Stellen im Bereich tenders). In module.guard.spec.ts als Testfall festgenagelt, damit die Aufzeichnung rot wird, sobald jemand ein unterscheidendes Signal einbaut. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt 'Bereich module-registry' (m3).",
|
||||||
|
"status": "open",
|
||||||
|
"reason": "",
|
||||||
|
"recorded_at": "2026-09-10T09:28:45.310Z",
|
||||||
|
"resolved_at": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
````
|
````
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import { NotFoundException } from '@nestjs/common';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { ModuleRegistryService } from './module-registry.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModuleRegistryService — bisher OHNE Testdatei (260910-exd, Aufgabe 3,
|
||||||
|
* Befund C, zweite Form). Diese Datei haelt elf der siebzehn Zugriffe des
|
||||||
|
* Bereichs `module-registry`, darunter JEDEN Schreibpfad.
|
||||||
|
*
|
||||||
|
* Bindung an forTenant() (dasselbe Muster wie
|
||||||
|
* `module-grants.service.spec.ts` und `module-access.service.spec.ts`): der
|
||||||
|
* gebundene Klient ist ein ZWEITES, von `prisma` unterscheidbares Objekt
|
||||||
|
* ueber DEMSELBEN Speicher, das protokolliert, welche Aufrufe ueber ihn
|
||||||
|
* liefen. `module` wird NICHT gewrappt — der Katalogzugriff bleibt bewusst
|
||||||
|
* ungebunden (Aufgabe 1, Befund E).
|
||||||
|
*/
|
||||||
|
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||||
|
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BOUND_MODEL_NAMES = ['tenantModuleActivation'];
|
||||||
|
|
||||||
|
function makeFakePrisma() {
|
||||||
|
const modules = new Map<string, any>();
|
||||||
|
const activations = new Map<string, any>(); // key: tenantId::moduleId
|
||||||
|
const boundCallLog: { tenantId: string; model: string; method: string }[] = [];
|
||||||
|
|
||||||
|
const fake: any = {
|
||||||
|
__seedModule(m: { id: string; slug: string; name: string; version: string; category: string; description: any; icon?: string | null; isSystem?: boolean }) {
|
||||||
|
modules.set(m.id, { icon: null, isSystem: false, ...m });
|
||||||
|
},
|
||||||
|
__seedActivation(a: { tenantId: string; moduleId: string; isActive: boolean }) {
|
||||||
|
activations.set(`${a.tenantId}::${a.moduleId}`, { ...a, activatedAt: new Date() });
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
findMany: vi.fn(async () => Array.from(modules.values()).sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
|
findUnique: vi.fn(async ({ where }: any) => {
|
||||||
|
if (where.id) return modules.get(where.id) ?? null;
|
||||||
|
if (where.slug) return Array.from(modules.values()).find((m) => m.slug === where.slug) ?? null;
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
upsert: vi.fn(async ({ where, update, create }: any) => {
|
||||||
|
const existing = Array.from(modules.values()).find((m) => m.slug === where.slug);
|
||||||
|
if (existing) {
|
||||||
|
const updated = { ...existing, ...update };
|
||||||
|
modules.set(existing.id, updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
const record = { id: `mod-${modules.size + 1}`, ...create };
|
||||||
|
modules.set(record.id, record);
|
||||||
|
return record;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
tenantModuleActivation: {
|
||||||
|
findMany: vi.fn(async ({ where }: any) => {
|
||||||
|
return Array.from(activations.values())
|
||||||
|
.filter((a) => a.tenantId === where.tenantId && (where.isActive === undefined || a.isActive === where.isActive))
|
||||||
|
.map((a) => ({ ...a, module: modules.get(a.moduleId) ?? null }));
|
||||||
|
}),
|
||||||
|
findUnique: vi.fn(async ({ where }: any) => {
|
||||||
|
const { tenantId, moduleId } = where.tenantId_moduleId;
|
||||||
|
return activations.get(`${tenantId}::${moduleId}`) ?? null;
|
||||||
|
}),
|
||||||
|
upsert: vi.fn(async ({ where, update, create }: any) => {
|
||||||
|
const key = `${where.tenantId_moduleId.tenantId}::${where.tenantId_moduleId.moduleId}`;
|
||||||
|
const existing = activations.get(key);
|
||||||
|
const record = existing
|
||||||
|
? { ...existing, ...update }
|
||||||
|
: { ...create, activatedAt: new Date() };
|
||||||
|
activations.set(key, record);
|
||||||
|
return { ...record, module: modules.get(record.moduleId) ?? null };
|
||||||
|
}),
|
||||||
|
update: vi.fn(async ({ where, data }: any) => {
|
||||||
|
const { tenantId, moduleId } = where.tenantId_moduleId;
|
||||||
|
const key = `${tenantId}::${moduleId}`;
|
||||||
|
const existing = activations.get(key);
|
||||||
|
const record = { ...existing, ...data };
|
||||||
|
activations.set(key, record);
|
||||||
|
return { ...record, module: modules.get(record.moduleId) ?? null };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// --- Bindungsnachweis (260910-exd, Befund C uebertragen) ---------------
|
||||||
|
__boundCallLog: boundCallLog,
|
||||||
|
__makeBoundClient(tenantId: string) {
|
||||||
|
const bound: any = { __isBoundClient: true, __tenantId: tenantId };
|
||||||
|
for (const modelName of BOUND_MODEL_NAMES) {
|
||||||
|
const model = fake[modelName];
|
||||||
|
const wrapped: any = {};
|
||||||
|
for (const method of Object.keys(model)) {
|
||||||
|
wrapped[method] = async (...args: any[]) => {
|
||||||
|
boundCallLog.push({ tenantId, model: modelName, method });
|
||||||
|
return model[method](...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
bound[modelName] = wrapped;
|
||||||
|
}
|
||||||
|
return bound;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectBoundCall(prisma: any, tenantId: string, model: string, method: string) {
|
||||||
|
const found = prisma.__boundCallLog.some(
|
||||||
|
(c: any) => c.tenantId === tenantId && c.model === model && c.method === method,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
found,
|
||||||
|
`erwarteter gebundener Aufruf ${model}.${method}(tenant=${tenantId}) fehlt im Protokoll: ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectNeverBound(prisma: any, model: string) {
|
||||||
|
const found = prisma.__boundCallLog.some((c: any) => c.model === model);
|
||||||
|
expect(
|
||||||
|
found,
|
||||||
|
`Modell "${model}" darf nie im Bindungsprotokoll auftauchen (Katalog bleibt ungebunden): ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||||
|
).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ModuleRegistryService.findAll/findBySlug — Katalog (bewusst UNGEBUNDEN)', () => {
|
||||||
|
it('findAll liefert alle registrierten Module, sortiert nach Namen', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-b', slug: 'b', name: 'B-Modul', version: '1', category: 'ops', description: {} });
|
||||||
|
prisma.__seedModule({ id: 'mod-a', slug: 'a', name: 'A-Modul', version: '1', category: 'ops', description: {} });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.findAll();
|
||||||
|
|
||||||
|
expect(result.map((m: any) => m.id)).toEqual(['mod-a', 'mod-b']);
|
||||||
|
expectNeverBound(prisma, 'module');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('findBySlug findet ein Modul über seinen Slug', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.findBySlug('domaincheck');
|
||||||
|
|
||||||
|
expect(result?.id).toBe('mod-1');
|
||||||
|
expectNeverBound(prisma, 'module');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('findBySlug liefert null für einen unbekannten Slug, ohne zu werfen', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.findBySlug('unknown');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModuleRegistryService.findActiveForTenant', () => {
|
||||||
|
it('bindet den Lesezugriff an den Mandanten UND liefert die Katalogseite mit (die verbundene Modellform überlebt die Bindung)', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.findActiveForTenant('t1');
|
||||||
|
|
||||||
|
expect(result).toEqual([expect.objectContaining({ id: 'mod-1', name: 'Domaincheck' })]);
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findMany');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('die Aktivierungsliste eines zweiten Mandanten liefert keine Zeile des ersten', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.findActiveForTenant('t2');
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expectBoundCall(prisma, 't2', 'tenantModuleActivation', 'findMany');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModuleRegistryService.activateForTenant', () => {
|
||||||
|
it('erreicht die Katalog-Existenzprüfung UNGEBUNDEN und schreibt die Aktivierung GEBUNDEN, an den übergebenen Mandanten', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.activateForTenant('t1', 'mod-1');
|
||||||
|
|
||||||
|
expect(result.isActive).toBe(true);
|
||||||
|
expect(prisma.module.findUnique).toHaveBeenCalled();
|
||||||
|
expectNeverBound(prisma, 'module');
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'upsert');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wirft die Nicht-gefunden-Ausnahme für eine unbekannte Modulkennung, BEVOR irgendein gebundener Schreibzugriff im Protokoll steht', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
await expect(service.activateForTenant('t1', 'mod-unknown')).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
prisma.__boundCallLog,
|
||||||
|
`kein gebundener Aufruf erwartet, wenn die Katalog-Existenzpruefung bereits scheitert: ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModuleRegistryService.deactivateForTenant', () => {
|
||||||
|
it('bindet beide Aktivierungszugriffe (Lesen, Schreiben) an denselben Mandanten, über einen Klienten', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.deactivateForTenant('t1', 'mod-1');
|
||||||
|
|
||||||
|
expect(result.isActive).toBe(false);
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findUnique');
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'update');
|
||||||
|
expectNeverBound(prisma, 'module');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die LAUTE, harmlose Richtung dieses Bereichs (260910-exd, m3) — hier
|
||||||
|
* ausdruecklich festgenagelt, damit sie bei einem spaeteren Umbau nicht
|
||||||
|
* versehentlich in ein stilles `false` verwandelt wird.
|
||||||
|
*/
|
||||||
|
it('wirft die Nicht-gefunden-Ausnahme, wenn keine Aktivierung vorliegt (LAUT, nicht still)', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
await expect(service.deactivateForTenant('t1', 'mod-1')).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wirft die Nicht-gefunden-Ausnahme für eine unbekannte Modulkennung', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
await expect(service.deactivateForTenant('t1', 'mod-unknown')).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModuleRegistryService.isModuleActive', () => {
|
||||||
|
it('bindet ihren Aktivierungs-Lesezugriff und erreicht den Katalog ungebunden', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.isModuleActive('t1', 'domaincheck');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findUnique');
|
||||||
|
expectNeverBound(prisma, 'module');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die STILLE Richtung dieses Bereichs (260910-exd, m3) — ebenfalls
|
||||||
|
* festgenagelt, mit diesem Kommentar als Markierung: heute ohne Aufrufer
|
||||||
|
* (Aufgabe 1, TEIL 3), aber die Falle für morgen, sollte diese Methode
|
||||||
|
* verdrahtet werden.
|
||||||
|
*/
|
||||||
|
it('liefert `false`, ohne Aktivierung (STILL, keine Ausnahme)', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.isModuleActive('t1', 'domaincheck');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('liefert `false` für einen unbekannten Slug, ohne die Aktivierungstabelle überhaupt zu befragen', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.isModuleActive('t1', 'unknown-slug');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(prisma.__boundCallLog).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModuleRegistryService.seedModule — Katalogpflege beim Start (bewusst UNGEBUNDEN)', () => {
|
||||||
|
it('legt ein neues Modul an, wenn der Slug noch nicht existiert', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.seedModule({
|
||||||
|
slug: 'domaincheck',
|
||||||
|
name: 'Domaincheck',
|
||||||
|
version: '1.0.0',
|
||||||
|
category: 'ops',
|
||||||
|
description: { de: 'Test' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.slug).toBe('domaincheck');
|
||||||
|
expectNeverBound(prisma, 'module');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aktualisiert ein bestehendes Modul über denselben Slug (Upsert)', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Alt', version: '1', category: 'ops', description: {} });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
const result = await service.seedModule({
|
||||||
|
slug: 'domaincheck',
|
||||||
|
name: 'Neu',
|
||||||
|
version: '2.0.0',
|
||||||
|
category: 'ops',
|
||||||
|
description: { de: 'Test' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.name).toBe('Neu');
|
||||||
|
expect(result.version).toBe('2.0.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModuleRegistryService — Katalogzugriffe tauchen nie im Bindungsprotokoll auf (Wachhund, 260910-exd)', () => {
|
||||||
|
it('kein Katalogzugriff des Dienstes — weder Gesamtliste, noch Kennzeichen-Suche, noch Existenzprüfungen, noch Katalogpflege — taucht im Bindungsprotokoll auf', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||||
|
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||||
|
const service = new ModuleRegistryService(prisma as any);
|
||||||
|
|
||||||
|
await service.findAll();
|
||||||
|
await service.findBySlug('domaincheck');
|
||||||
|
await service.activateForTenant('t1', 'mod-1');
|
||||||
|
await service.deactivateForTenant('t1', 'mod-1');
|
||||||
|
await service.isModuleActive('t1', 'domaincheck');
|
||||||
|
await service.seedModule({
|
||||||
|
slug: 'domaincheck',
|
||||||
|
name: 'Domaincheck',
|
||||||
|
version: '1.0.0',
|
||||||
|
category: 'ops',
|
||||||
|
description: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expectNeverBound(prisma, 'module');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service managing the module registry and per-tenant activations.
|
* Service managing the module registry and per-tenant activations.
|
||||||
@@ -13,6 +14,13 @@ export class ModuleRegistryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all registered modules.
|
* Returns all registered modules.
|
||||||
|
*
|
||||||
|
* Bewusst UNGEBUNDEN (260910-exd, Aufgabe 1, Befund E): "Module" traegt
|
||||||
|
* heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht
|
||||||
|
* katastrophal. Katastrophal wuerde sie erst, WENN Etappe 3 dieser
|
||||||
|
* Tabelle eine Regel gibt — dann verschwaende der gesamte Katalog fuer
|
||||||
|
* jeden Mandanten. Diese Bedingung steht hier als Bedingung, nicht als
|
||||||
|
* heute beobachtbare Tatsache.
|
||||||
*/
|
*/
|
||||||
async findAll() {
|
async findAll() {
|
||||||
return this.prisma.module.findMany({
|
return this.prisma.module.findMany({
|
||||||
@@ -22,6 +30,12 @@ export class ModuleRegistryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a module by its unique slug.
|
* Finds a module by its unique slug.
|
||||||
|
*
|
||||||
|
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben. Diese
|
||||||
|
* Methode ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER
|
||||||
|
* Modulanfrage aufruft — eine Bindung wuerde jede Modulanfrage mit einer
|
||||||
|
* Meldung abweisen, die faelschlich von einer fehlenden Aktivierung
|
||||||
|
* spricht.
|
||||||
*/
|
*/
|
||||||
async findBySlug(slug: string) {
|
async findBySlug(slug: string) {
|
||||||
return this.prisma.module.findUnique({
|
return this.prisma.module.findUnique({
|
||||||
@@ -33,7 +47,8 @@ export class ModuleRegistryService {
|
|||||||
* Returns all active modules for a given tenant.
|
* Returns all active modules for a given tenant.
|
||||||
*/
|
*/
|
||||||
async findActiveForTenant(tenantId: string) {
|
async findActiveForTenant(tenantId: string) {
|
||||||
const activations = await this.prisma.tenantModuleActivation.findMany({
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
const activations = await tenantPrisma.tenantModuleActivation.findMany({
|
||||||
where: {
|
where: {
|
||||||
tenantId,
|
tenantId,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -43,7 +58,7 @@ export class ModuleRegistryService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return activations.map((a) => a.module);
|
return activations.map((a: { module: unknown }) => a.module);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +66,10 @@ export class ModuleRegistryService {
|
|||||||
* Per D-08: dynamic activation without restart.
|
* Per D-08: dynamic activation without restart.
|
||||||
*/
|
*/
|
||||||
async activateForTenant(tenantId: string, moduleId: string) {
|
async activateForTenant(tenantId: string, moduleId: string) {
|
||||||
// Verify module exists
|
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
|
||||||
|
// `findAll` oben (Aufgabe 1, Befund E). Laeuft VOR jedem gebundenen
|
||||||
|
// Schreibzugriff: eine unbekannte moduleId wirft, bevor der gebundene
|
||||||
|
// Klient ueberhaupt erzeugt wird.
|
||||||
const moduleExists = await this.prisma.module.findUnique({
|
const moduleExists = await this.prisma.module.findUnique({
|
||||||
where: { id: moduleId },
|
where: { id: moduleId },
|
||||||
});
|
});
|
||||||
@@ -59,7 +77,8 @@ export class ModuleRegistryService {
|
|||||||
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.tenantModuleActivation.upsert({
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
return tenantPrisma.tenantModuleActivation.upsert({
|
||||||
where: {
|
where: {
|
||||||
tenantId_moduleId: {
|
tenantId_moduleId: {
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -86,7 +105,8 @@ export class ModuleRegistryService {
|
|||||||
* Does not remove the activation record, preserving audit trail.
|
* Does not remove the activation record, preserving audit trail.
|
||||||
*/
|
*/
|
||||||
async deactivateForTenant(tenantId: string, moduleId: string) {
|
async deactivateForTenant(tenantId: string, moduleId: string) {
|
||||||
// Verify module exists
|
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
|
||||||
|
// `findAll` oben.
|
||||||
const moduleExists = await this.prisma.module.findUnique({
|
const moduleExists = await this.prisma.module.findUnique({
|
||||||
where: { id: moduleId },
|
where: { id: moduleId },
|
||||||
});
|
});
|
||||||
@@ -94,8 +114,13 @@ export class ModuleRegistryService {
|
|||||||
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EIN gebundener Klient fuer beide Aktivierungszugriffe dieser Methode
|
||||||
|
// (Lesen, Schreiben) — nicht ein Klient je Zugriff (260910-exd,
|
||||||
|
// Aufgabe 3, dieselbe Konvention wie `module-access.service.ts`).
|
||||||
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
|
||||||
// Check if activation record exists
|
// Check if activation record exists
|
||||||
const activation = await this.prisma.tenantModuleActivation.findUnique({
|
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
|
||||||
where: {
|
where: {
|
||||||
tenantId_moduleId: {
|
tenantId_moduleId: {
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -110,7 +135,7 @@ export class ModuleRegistryService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.tenantModuleActivation.update({
|
return tenantPrisma.tenantModuleActivation.update({
|
||||||
where: {
|
where: {
|
||||||
tenantId_moduleId: {
|
tenantId_moduleId: {
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -128,7 +153,15 @@ export class ModuleRegistryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a module (by slug) is active for a given tenant.
|
* Checks whether a module (by slug) is active for a given tenant.
|
||||||
* Used by ModuleGuard to gate access to module-specific endpoints.
|
*
|
||||||
|
* Richtiggestellt (260910-exd, Aufgabe 1, Befund G): der vorherige
|
||||||
|
* Kommentar behauptete, `ModuleGuard` benutze diese Methode — er tut es
|
||||||
|
* NICHT. Gemessen (Aufgabe 1, TEIL 3, `grep -rn "isModuleActive"
|
||||||
|
* apps/api/src apps/web/src packages`): genau EIN Treffer, die Definition
|
||||||
|
* selbst, kein Aufrufer. Der Waechter nimmt stattdessen `findBySlug` plus
|
||||||
|
* `ModuleAccessService.getAccessibleModuleIds`. Diese Methode bleibt
|
||||||
|
* TROTZDEM umgestellt: heute toter, ungebunden gelassener Code ist die
|
||||||
|
* Falle fuer den, der ihn morgen verdrahtet.
|
||||||
*/
|
*/
|
||||||
async isModuleActive(tenantId: string, moduleSlug: string): Promise<boolean> {
|
async isModuleActive(tenantId: string, moduleSlug: string): Promise<boolean> {
|
||||||
const module = await this.prisma.module.findUnique({
|
const module = await this.prisma.module.findUnique({
|
||||||
@@ -139,7 +172,8 @@ export class ModuleRegistryService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activation = await this.prisma.tenantModuleActivation.findUnique({
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
|
||||||
where: {
|
where: {
|
||||||
tenantId_moduleId: {
|
tenantId_moduleId: {
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -154,6 +188,11 @@ export class ModuleRegistryService {
|
|||||||
/**
|
/**
|
||||||
* Registers or updates a module in the registry by slug (upsert).
|
* Registers or updates a module in the registry by slug (upsert).
|
||||||
* Used during application startup to seed built-in modules.
|
* Used during application startup to seed built-in modules.
|
||||||
|
*
|
||||||
|
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben — mit einem
|
||||||
|
* zusaetzlichen Grund, den nur diese Methode hat: sie laeuft beim
|
||||||
|
* Anwendungsstart aus vier Seed-Dateien, ohne Anfrage und ohne Mandanten
|
||||||
|
* — ein gebundener Aufruf haette dort strukturell keinen Kontext.
|
||||||
*/
|
*/
|
||||||
async seedModule(manifest: {
|
async seedModule(manifest: {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|||||||
@@ -15,7 +15,17 @@ import { TenderSchedulerService } from './tender-scheduler.service';
|
|||||||
* tender-ingestion.service.spec.ts / ldap.service.spec.ts, driving the REAL
|
* tender-ingestion.service.spec.ts / ldap.service.spec.ts, driving the REAL
|
||||||
* ModuleRegistryService (unmocked) so the activation call path is genuine,
|
* ModuleRegistryService (unmocked) so the activation call path is genuine,
|
||||||
* not a stand-in.
|
* not a stand-in.
|
||||||
|
*
|
||||||
|
* forTenant() just returns the same client in these tests (identical
|
||||||
|
* convention to ldap.service.spec.ts) — tenant scoping/RLS binding is not
|
||||||
|
* what this file tests, only ModuleRegistryService.activateForTenant's
|
||||||
|
* poll-once-fan-out-many behavior. Needed since 260910-exd (Aufgabe 3)
|
||||||
|
* converted ModuleRegistryService.activateForTenant to forTenant(), and the
|
||||||
|
* hand-rolled fake below does not implement `$extends`.
|
||||||
*/
|
*/
|
||||||
|
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||||
|
forTenant: vi.fn((p: unknown) => p),
|
||||||
|
}));
|
||||||
|
|
||||||
function makeFakePrisma() {
|
function makeFakePrisma() {
|
||||||
const modules = new Map<string, any>();
|
const modules = new Map<string, any>();
|
||||||
|
|||||||
@@ -1457,6 +1457,89 @@ und verlöre ihr Signal.
|
|||||||
- Schema und Migrationen — geprüft und bewusst gelassen, keine
|
- Schema und Migrationen — geprüft und bewusst gelassen, keine
|
||||||
Schemaänderung in dieser Etappe.
|
Schemaänderung in dieser Etappe.
|
||||||
|
|
||||||
|
**Nachtrag (260910-exd, Aufgabe 3).** Wie in den vorherigen Durchläufen wird
|
||||||
|
der Text oben NICHT umgeschrieben — er beschreibt korrekt den Stand zum
|
||||||
|
Zeitpunkt der Messung (Aufgabe 1); dieser Nachtrag hält fest, was Aufgabe 2/3
|
||||||
|
tatsächlich umgesetzt haben.
|
||||||
|
|
||||||
|
*Tatsächlich umgesetzte Pfade gegen die in (m2) angekündigten gehalten:*
|
||||||
|
alle in (m2) genannten Pfade sind wie beschrieben umgestellt.
|
||||||
|
`ModuleAccessService.getAccessibleModuleIds` bindet Kurzschlusszweig,
|
||||||
|
Direktweg, Gruppenweg und Schnittmenge über EINEN Klienten je Aufruf;
|
||||||
|
`getCatalogFlags` bindet ihren eigenen Aktivierungs-Lesezugriff, die
|
||||||
|
geschachtelte `getAccessibleModuleIds`-Auflösung erzeugt ihren eigenen
|
||||||
|
Klienten; `findAccessibleModules` erreicht den Katalog weiterhin über den
|
||||||
|
ungebundenen Klienten. `ModuleRegistryService.findActiveForTenant`,
|
||||||
|
`activateForTenant` und `isModuleActive` binden ihren jeweiligen
|
||||||
|
Aktivierungszugriff; `deactivateForTenant` bindet beide Aktivierungszugriffe
|
||||||
|
(Lesen, Schreiben) über EINEN Klienten. Alle sechs Katalogzugriffe in
|
||||||
|
`module-registry.service.ts` (`findAll`, `findBySlug`, die beiden
|
||||||
|
Katalog-Existenzprüfungen, die Katalogsuche in `isModuleActive`,
|
||||||
|
`seedModule`) und der eine Katalogzugriff in `module-access.service.ts`
|
||||||
|
(`findAccessibleModules`) blieben wie angekündigt ungebunden, mit
|
||||||
|
Kommentaren, die Messung und Bedingung trennen. Beide falschen
|
||||||
|
Kopfkommentare aus Befund G sind behandelt: `isModuleActive`s Kommentar ist
|
||||||
|
richtiggestellt (mit Bezug auf die Aufrufermessung aus TEIL 3); der
|
||||||
|
irreführende Satz im Kopfkommentar von `module-registry.controller.ts`
|
||||||
|
("findActiveForTenant on ModuleRegistryService stays unchanged for Plan
|
||||||
|
15-03's marketplace catalog") wurde NICHT mitgeändert — der Controller ist
|
||||||
|
nicht Teil dieses Plans. Die Feststellung, dass diese Aussage falsch ist
|
||||||
|
(der Marktplatz-Katalog wird nachweislich von `getCatalogFlags` bedient,
|
||||||
|
nicht von `findActiveForTenant`), steht stattdessen hier in (m5).
|
||||||
|
|
||||||
|
*Deviation (Rule 1/3): eine cross-area Testabhängigkeit brach durch die
|
||||||
|
Umstellung.* `tender-scheduler.service.spec.ts` instanziiert
|
||||||
|
`ModuleRegistryService` unmocked gegen einen hand-gerollten Fake ohne
|
||||||
|
`$extends` (derselbe Zweck wie in `ldap.service.spec.ts`: der Aktivierungs-
|
||||||
|
Aufruf soll echt sein, nicht ein Stand-in). Nach der Umstellung von
|
||||||
|
`activateForTenant` auf `forTenant()` scheiterte dieser Test mit
|
||||||
|
`prisma.$extends is not a function`. Behoben mit derselben Konvention wie
|
||||||
|
`ldap.service.spec.ts` — `forTenant` in dieser einen Datei über `vi.mock`
|
||||||
|
auf eine Identitätsfunktion gelegt (`forTenant: vi.fn((p) => p)`), weil die
|
||||||
|
Datei RLS-Bindungsmechanik nicht testet, nur das Poll-once-fan-out-many-
|
||||||
|
Verhalten des Schedulers. Kein anderer Aufrufer von `new
|
||||||
|
ModuleRegistryService(...)` existiert im Quelltext (geprüft).
|
||||||
|
|
||||||
|
*Deviation (Rule 3): die Klassifikationsdokument-Stände für
|
||||||
|
`module-access.service.ts` wurden bereits in Aufgabe 2 nachgezogen*, nicht
|
||||||
|
erst in dieser Aufgabe — `rls-access-inventory.spec.ts` ist Teil der von
|
||||||
|
Aufgabe 2 verlangten vollständigen Testsuite und wäre sonst am Ende von
|
||||||
|
Aufgabe 2 bereits rot gewesen. Diese Abweichung von der Aufgabenaufteilung
|
||||||
|
(die Klassifikationsdatei war für Aufgabe 3 vorgesehen) ist auf das
|
||||||
|
Notwendige beschränkt: nur die `Stand`-Spalte der beiden betroffenen Zeilen,
|
||||||
|
keine Begründung, keine Zahlen. Die vollständige Nachziehung (fünf
|
||||||
|
Bestandsaufnahme-Zeilen inklusive `module-registry.service.ts`,
|
||||||
|
Übersichtszeile, Summenzeile, Klassen-Verteilung, Hintergrunddienst-
|
||||||
|
Abschnitt) erfolgte wie geplant in dieser Aufgabe.
|
||||||
|
|
||||||
|
*Falsifizierungsnachweise (Aufgabe 2 und 3), je einmal durchgeführt und
|
||||||
|
zurückgenommen:* in Aufgabe 2 wurde die Gruppenweg-Bindung der
|
||||||
|
Freigabe-Auflösung probeweise zurückgebaut (`tenantPrisma.moduleGrant` →
|
||||||
|
`this.prisma.moduleGrant` im Gruppenweg von `getAccessibleModuleIds`) —
|
||||||
|
genau `module-access.service.spec.ts`, Test "ModuleAccessService — Bindung
|
||||||
|
an forTenant() (260910-exd) > USER-Zweig bindet BEIDE
|
||||||
|
Freigabe-Lesezugriffe (Direktweg und Gruppenweg) UND den
|
||||||
|
Schnittmengen-Lesezugriff an DIESELBE Mandantenkennung", wurde rot, mit der
|
||||||
|
Meldung `expected 1 to be 2`; der Rückbau wurde zurückgenommen, derselbe
|
||||||
|
Testlauf danach wieder grün (23/23). In Aufgabe 3 wurde der
|
||||||
|
Schreibzugriff des Deaktivierens probeweise zurückgebaut
|
||||||
|
(`tenantPrisma.tenantModuleActivation.update` →
|
||||||
|
`this.prisma.tenantModuleActivation.update` in `deactivateForTenant`) —
|
||||||
|
genau `module-registry.service.spec.ts`, Test
|
||||||
|
"ModuleRegistryService.deactivateForTenant > bindet beide
|
||||||
|
Aktivierungszugriffe (Lesen, Schreiben) an denselben Mandanten, über einen
|
||||||
|
Klienten", wurde rot, mit der Meldung "erwarteter gebundener Aufruf
|
||||||
|
tenantModuleActivation.update(tenant=t1) fehlt im Protokoll:
|
||||||
|
[{"tenantId":"t1","model":"tenantModuleActivation","method":"findUnique"}]:
|
||||||
|
expected false to be true"; der Rückbau wurde zurückgenommen, derselbe
|
||||||
|
Testlauf danach wieder grün (16/16).
|
||||||
|
|
||||||
|
*Die offene WINDOWS-Aufzeichnung.* Eintrag #23 (`deviation`) hält fest, dass
|
||||||
|
es kein Signal gibt, das "wirklich keine Freigabe" von "die Abfrage hat
|
||||||
|
nichts gefunden" unterscheidet, mit der Vorabprüfung für Etappe 4 und der
|
||||||
|
begründeten Verwerfung einer Laufzeitwarnung — siehe (m3) oben und
|
||||||
|
`.planning/WINDOWS.md`.
|
||||||
|
|
||||||
## Verweis
|
## Verweis
|
||||||
|
|
||||||
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
||||||
|
|||||||
@@ -101,14 +101,14 @@ autoritative Quelle.
|
|||||||
| ldap | 4 | 26 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04) |
|
| ldap | 4 | 26 | **war 21/0** — Aufgabe 2/3 (260909-ipc) haben `ldap-config.service.ts` (5 Methoden) und `ldap.service.ts` (6 Methoden, 11 Abfragen) auf `forTenant()` umgestellt. Die 4 verbleibenden ungebundenen Treffer sind bewusst: `getAllActiveConfigs`/`onApplicationBootstrap` (Befund B) und `resolveEmailForWrite` (Befund A, T-IPC-04) |
|
||||||
| dkv | 1 | 22 | **war 21/0** — Aufgabe 2/3 (260909-mir) haben `dkv.service.ts` vollständig auf `forTenant()` umgestellt: Konfigurationspfade (`loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection`), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf `dkvInvoiceHistory` einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung |
|
| dkv | 1 | 22 | **war 21/0** — Aufgabe 2/3 (260909-mir) haben `dkv.service.ts` vollständig auf `forTenant()` umgestellt: Konfigurationspfade (`loadConfig`, `getConfigForApi`, `saveConfig`, `testConnection`), Historie, Fahrzeugstammdaten und der neue Besitzriegel vor dem Ausfuhrdatei-Download. Gebunden sind es 22 statt 21, weil der Riegel einen zusätzlichen Lesezugriff auf `dkvInvoiceHistory` einführt (T-MIR-03). Der eine verbleibende ungebundene Treffer ist der benannte Planer-Startpfad `loadAnyActiveConfigForScheduler()` (Befund D, WINDOWS #21) — bewusst, mit dreifacher Markierung |
|
||||||
| user | 8 | 14 | **war 17/0** — Aufgabe 2/3 (260910-das) haben `user.service.ts` (`findById`/`create`/`update`/`deactivate`/`delete` sowie die zwei neuen Plattform-Administratorsicht-Methoden), `admin-seed.service.ts` (Erstanlage des Administrators) und `user.controller.ts` (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf `forTenant()` umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: `findByUsername` in `user.service.ts` (plattformweit eindeutiger Schluessel, derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`), die Erstanlage-Pruefung und beide Zugriffe auf `tenant` in `admin-seed.service.ts`, sowie der neue Schleifentreiber `this.prisma.tenant.findMany` der beiden Plattform-Administratorsicht-Methoden in `user.service.ts` (`Tenant` traegt keinen Zeilenschutz) |
|
| user | 8 | 14 | **war 17/0** — Aufgabe 2/3 (260910-das) haben `user.service.ts` (`findById`/`create`/`update`/`deactivate`/`delete` sowie die zwei neuen Plattform-Administratorsicht-Methoden), `admin-seed.service.ts` (Erstanlage des Administrators) und `user.controller.ts` (Benutzerliste des ADMIN-Zweigs, alle drei Kennungswege ueber die Dienstmethoden, alle fuenf Selbstbedienungszugriffe) auf `forTenant()` umgestellt. Die 8 verbleibenden ungebundenen Rohtreffer sind bewusst: `findByUsername` in `user.service.ts` (plattformweit eindeutiger Schluessel, derselbe Fall wie `resolveEmailForWrite` im Bereich `ldap`), die Erstanlage-Pruefung und beide Zugriffe auf `tenant` in `admin-seed.service.ts`, sowie der neue Schleifentreiber `this.prisma.tenant.findMany` der beiden Plattform-Administratorsicht-Methoden in `user.service.ts` (`Tenant` traegt keinen Zeilenschutz) |
|
||||||
| module-registry | 17 | 0 | unverändert |
|
| module-registry | 7 | 10 | **war 17/0** — Aufgabe 2/3 (260910-exd) haben `module-access.service.ts` (`getAccessibleModuleIds`: Kurzschlusszweig, Direktweg, Gruppenweg, Schnittmenge; `getCatalogFlags`: eigener Aktivierungs-Lesezugriff) und `module-registry.service.ts` (`findActiveForTenant`, `activateForTenant`, `deactivateForTenant`, `isModuleActive`) auf `forTenant()` umgestellt. Die 7 verbleibenden ungebundenen Rohtreffer sind bewusst: der eine Katalogzugriff in `module-access.service.ts` (`findAccessibleModules`) und die sechs Katalogzugriffe in `module-registry.service.ts` (`findAll`, `findBySlug`, die beiden Katalog-Existenzpruefungen in `activateForTenant`/`deactivateForTenant`, die Katalogsuche in `isModuleActive`, `seedModule`) — der Modulkatalog (`Module`) traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal; katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt (Befund E) |
|
||||||
| dashboard | 13 | 0 | unverändert |
|
| dashboard | 13 | 0 | unverändert |
|
||||||
| auth | 8 | 5 | unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand |
|
| auth | 8 | 5 | unverändert gegenüber dem in Etappe 1 (260909-eor) gemessenen Stand |
|
||||||
| calendar | 12 | 0 | unverändert |
|
| calendar | 12 | 0 | unverändert |
|
||||||
| tenant | 8 | 0 | unverändert |
|
| tenant | 8 | 0 | unverändert |
|
||||||
| favorites | 7 | 0 | unverändert |
|
| favorites | 7 | 0 | unverändert |
|
||||||
| settings | 4 | 0 | unverändert |
|
| settings | 4 | 0 | unverändert |
|
||||||
| **Summe** | **118** | **124** | Ungebunden: war 127 nach 260909-mir, Delta = die 9 in Aufgabe 2/3 (260910-das) gesunkenen `user`-Rohtreffer (17→8). Gebunden: war 110, jetzt zusätzlich 14 in `user`. 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** | **108** | **134** | Ungebunden: war 118 nach 260910-das, Delta = die 10 in Aufgabe 2/3 (260910-exd) gesunkenen `module-registry`-Rohtreffer (17→7). Gebunden: war 124, jetzt zusätzlich 10 in `module-registry`. 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, 63 Paare)
|
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 63 Paare)
|
||||||
|
|
||||||
@@ -251,6 +251,16 @@ Verzweigung hinter einem optionalen Parameter, die jemand später
|
|||||||
`dkv-scheduler.service.ts`, Ledger-Eintrag WINDOWS #21. Das Signal für das
|
`dkv-scheduler.service.ts`, Ledger-Eintrag WINDOWS #21. Das Signal für das
|
||||||
Verstummen gehört in die Vorabprüfung von Etappe 4 (`rls-preflight.mjs`).
|
Verstummen gehört in die Vorabprüfung von Etappe 4 (`rls-preflight.mjs`).
|
||||||
|
|
||||||
|
**Stand 260910-exd — kein sechster Fall, gemessen statt angenommen.** Der
|
||||||
|
Bereich `module-registry` fügt diesem Abschnitt KEINEN sechsten Fall hinzu.
|
||||||
|
`ModuleRegistryService.seedModule()` (die Katalogpflege beim Start,
|
||||||
|
aufgerufen aus vier Seed-Dateien) schreibt zwar ohne Mandantenkontext — sie
|
||||||
|
iteriert aber über NICHTS je Mandant, sondern schreibt genau eine Zeile je
|
||||||
|
Aufruf auf den plattformweiten Katalog `Module`. Damit fehlt ihr die Bauform
|
||||||
|
der fünf oben geführten Fälle (übergreifend LESEN über alle Mandanten, dann
|
||||||
|
je Mandant BINDEN) — sie ist deshalb kein Kandidat für diese Liste. Dieser
|
||||||
|
Satz hält die Abwesenheit fest, damit sie nicht wie ein Übersehen aussieht.
|
||||||
|
|
||||||
## Bestandsaufnahme
|
## Bestandsaufnahme
|
||||||
|
|
||||||
Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit
|
Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit
|
||||||
@@ -288,11 +298,11 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
|||||||
| 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 | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. |
|
| 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. |
|
| 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 | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant. |
|
| 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 | ungebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. |
|
| apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | ungebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. |
|
||||||
| 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). |
|
| 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). |
|
||||||
| apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
| apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
||||||
|
|||||||
Reference in New Issue
Block a user