feat(jts-03): module-grants.service.ts binden, beide Dokumente schliessen
Alle fuenf Methoden von ModuleGrantsService (assertTargetBelongsToTenant,
grant, revoke, getMatrix, getUserAccess) laufen jetzt ueber forTenant(); bei
den beiden Datenlieferungen teilen sich alle parallel abgesetzten
Teilabfragen denselben gebundenen Client. Die Mandanten-Gegenpruefung vor
jedem Erteilen bleibt ausdruecklich bestehen und bekommt einen Verweis auf
Befund F/T-JTS-03: die Regel auf ModuleGrant prueft nur die
Mandantenkennung der Zeile, nicht die referenzierte Gruppe. Der veraltete
Kommentar ueber der Mitgliedschaftsabfrage im Benutzer-Detail ("kein
forTenant hier") ist durch den neuen Stand ersetzt.
module-grants.service.spec.ts bekommt denselben Bindungsnachweis-Mock wie
groups.service.spec.ts (zwei unterscheidbare Clients ueber demselben
Speicher) und sechs neue Bindungsnachweise; alle 28 Bestandstests bleiben
gruen.
Beide Dokumente geschlossen: die Bereichsuebersicht fuer groups ist neu
gemessen (0 ungebunden, 31 gebunden — ein dokumentierter methodischer
Bodensatz, da die einfache Rohtrefferzaehlung die neun ueber `tx` gebundenen
Zugriffe innerhalb der drei Transaktionen nicht sieht), die
Klassen-Verteilung auf 62 Paare aktualisiert, und der als offen gefuehrte
Befund D aus dem ldap-Abschnitt der Fehlerrichtung ist mit Verweis auf
diesen Durchlauf als erledigt vermerkt (Nachtrag, nicht Neuschrieb). 743
Tests und die Typpruefung gruen; Schema, Migrationen und alle vier
Compose-Dateien unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -9,7 +9,18 @@ import { ModuleGrantsService } from './module-grants.service';
|
|||||||
* groups.service.spec.ts / module-access.service.spec.ts — keine Live-DB,
|
* groups.service.spec.ts / module-access.service.spec.ts — keine Live-DB,
|
||||||
* P2002 wird exakt wie ein echter Postgres-Client über den Fehlercode
|
* P2002 wird exakt wie ein echter Postgres-Client über den Fehlercode
|
||||||
* simuliert.
|
* simuliert.
|
||||||
|
*
|
||||||
|
* Bindung an forTenant() (260909-jts, Aufgabe 3, Befund C uebertragen von
|
||||||
|
* groups.service.spec.ts): derselbe Mock wie dort — der gebundene Client
|
||||||
|
* ist ein ZWEITES, von `prisma` unterscheidbares Objekt ueber DEMSELBEN
|
||||||
|
* Speicher, das protokolliert, welche Aufrufe ueber ihn liefen. Ein reiner
|
||||||
|
* Identitaets-Mock (`forTenant: vi.fn((p) => p)`) koennte einen
|
||||||
|
* vergessenen Bindungsaufruf nicht von einem ungebundenen Aufruf
|
||||||
|
* unterscheiden.
|
||||||
*/
|
*/
|
||||||
|
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||||
|
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
||||||
|
}));
|
||||||
|
|
||||||
function makeFakePrisma() {
|
function makeFakePrisma() {
|
||||||
const groups = new Map<string, any>();
|
const groups = new Map<string, any>();
|
||||||
@@ -17,6 +28,7 @@ function makeFakePrisma() {
|
|||||||
const memberships = new Map<string, Set<string>>(); // groupId -> Set<userId>
|
const memberships = new Map<string, Set<string>>(); // groupId -> Set<userId>
|
||||||
const membershipSources = new Map<string, string>(); // `${groupId}::${userId}` -> source
|
const membershipSources = new Map<string, string>(); // `${groupId}::${userId}` -> source
|
||||||
const activations = new Map<string, any>(); // key: tenantId::moduleId
|
const activations = new Map<string, any>(); // key: tenantId::moduleId
|
||||||
|
const boundCallLog: { tenantId: string; model: string; method: string }[] = [];
|
||||||
const grants = new Map<string, any>();
|
const grants = new Map<string, any>();
|
||||||
let grantCounter = 0;
|
let grantCounter = 0;
|
||||||
|
|
||||||
@@ -41,7 +53,7 @@ function makeFakePrisma() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const fake: any = {
|
||||||
__seedGroup(group: { id: string; tenantId: string; name: string; internalName?: string | null }) {
|
__seedGroup(group: { id: string; tenantId: string; name: string; internalName?: string | null }) {
|
||||||
groups.set(group.id, { internalName: null, ...group });
|
groups.set(group.id, { internalName: null, ...group });
|
||||||
},
|
},
|
||||||
@@ -166,8 +178,47 @@ function makeFakePrisma() {
|
|||||||
return rows;
|
return rows;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// --- Bindungsnachweis (260909-jts, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modelle, die `__makeBoundClient()` je Aufruf mit einem eigenen, das
|
||||||
|
* Herkunfts-Tenant protokollierenden Wrapper versieht. */
|
||||||
|
const BOUND_MODEL_NAMES = ['group', 'user', 'tenantModuleActivation', 'moduleGrant', 'groupMembership'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindungsnachweis: mindestens ein Aufruf von `<tenantId>.<model>.<method>`
|
||||||
|
* lief ueber den gebundenen Client (nicht ueber den rohen, ungebundenen
|
||||||
|
* Fake). Ein vergessener `forTenant()`-Aufruf hinterlaesst hier KEINEN
|
||||||
|
* Eintrag und laesst den Test fehlschlagen.
|
||||||
|
*/
|
||||||
|
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 seedBase(prisma: ReturnType<typeof makeFakePrisma>) {
|
function seedBase(prisma: ReturnType<typeof makeFakePrisma>) {
|
||||||
prisma.__seedGroup({ id: 'g1', tenantId: 't1', name: 'Gruppe A' });
|
prisma.__seedGroup({ id: 'g1', tenantId: 't1', name: 'Gruppe A' });
|
||||||
@@ -595,3 +646,82 @@ describe('ModuleGrantsService — Logging (D-23)', () => {
|
|||||||
expect(message).toContain('g1');
|
expect(message).toContain('g1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Bindung an forTenant() (260909-jts, Aufgabe 3) -------------------------
|
||||||
|
|
||||||
|
describe('ModuleGrantsService — Bindung an forTenant() (260909-jts)', () => {
|
||||||
|
it('grant() bindet die Mandanten-Gegenpruefung, die Aktivierungspruefung und moduleGrant.create an den uebergebenen Mandanten', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
seedBase(prisma);
|
||||||
|
const service = new ModuleGrantsService(prisma as any);
|
||||||
|
|
||||||
|
await service.grant('t1', { moduleId: 'mod-1', groupId: 'g1' });
|
||||||
|
|
||||||
|
expectBoundCall(prisma, 't1', 'group', 'findFirst');
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findUnique');
|
||||||
|
expectBoundCall(prisma, 't1', 'moduleGrant', 'create');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('grant() bindet auch die Mandanten-Gegenpruefung fuer eine userId und bleibt wirksam gegen einen fremden Benutzer (T-15-01)', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
seedBase(prisma);
|
||||||
|
prisma.__seedUser({ id: 'u-foreign', tenantId: 't2' });
|
||||||
|
const service = new ModuleGrantsService(prisma as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.grant('t1', { moduleId: 'mod-1', userId: 'u-foreign' }),
|
||||||
|
).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
|
||||||
|
expectBoundCall(prisma, 't1', 'user', 'findFirst');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revoke() bindet moduleGrant.deleteMany an den uebergebenen Mandanten', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
seedBase(prisma);
|
||||||
|
const service = new ModuleGrantsService(prisma as any);
|
||||||
|
await service.grant('t1', { moduleId: 'mod-1', groupId: 'g1' });
|
||||||
|
|
||||||
|
await service.revoke('t1', { moduleId: 'mod-1', groupId: 'g1' });
|
||||||
|
|
||||||
|
expectBoundCall(prisma, 't1', 'moduleGrant', 'deleteMany');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getMatrix() bindet alle drei parallelen Teilabfragen (tenantModuleActivation, group, moduleGrant) an DENSELBEN gebundenen Mandanten', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
seedBase(prisma);
|
||||||
|
const service = new ModuleGrantsService(prisma as any);
|
||||||
|
|
||||||
|
await service.getMatrix('t1');
|
||||||
|
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findMany');
|
||||||
|
expectBoundCall(prisma, 't1', 'group', 'findMany');
|
||||||
|
expectBoundCall(prisma, 't1', 'moduleGrant', 'findMany');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getUserAccess() bindet alle vier parallelen Teilabfragen (tenantModuleActivation, moduleGrant x2, groupMembership) an DENSELBEN gebundenen Mandanten', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
seedBase(prisma);
|
||||||
|
prisma.__seedMembership('g1', 'u1');
|
||||||
|
const service = new ModuleGrantsService(prisma as any);
|
||||||
|
await service.grant('t1', { moduleId: 'mod-1', groupId: 'g1' });
|
||||||
|
|
||||||
|
await service.getUserAccess('t1', 'u1');
|
||||||
|
|
||||||
|
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findMany');
|
||||||
|
expectBoundCall(prisma, 't1', 'moduleGrant', 'findMany');
|
||||||
|
expectBoundCall(prisma, 't1', 'groupMembership', 'findMany');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getUserAccess() bindet weiterhin die Mandanten-Gegenpruefung — sie wird durch die Bindung NICHT ersetzt', async () => {
|
||||||
|
const prisma = makeFakePrisma();
|
||||||
|
seedBase(prisma);
|
||||||
|
prisma.__seedUser({ id: 'u-foreign', tenantId: 't2' });
|
||||||
|
const service = new ModuleGrantsService(prisma as any);
|
||||||
|
|
||||||
|
await expect(service.getUserAccess('t1', 'u-foreign')).rejects.toBeInstanceOf(
|
||||||
|
NotFoundException,
|
||||||
|
);
|
||||||
|
|
||||||
|
expectBoundCall(prisma, 't1', 'user', 'findFirst');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schreibseite der Modul-Freigaben (PERM-03): Grants für Gruppen und für
|
* Schreibseite der Modul-Freigaben (PERM-03): Grants für Gruppen und für
|
||||||
@@ -47,8 +48,9 @@ export class ModuleGrantsService {
|
|||||||
groupId?: string,
|
groupId?: string,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
const group = await this.prisma.group.findFirst({
|
const group = await tenantPrisma.group.findFirst({
|
||||||
where: { id: groupId, tenantId },
|
where: { id: groupId, tenantId },
|
||||||
});
|
});
|
||||||
if (!group) {
|
if (!group) {
|
||||||
@@ -56,7 +58,7 @@ export class ModuleGrantsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await tenantPrisma.user.findFirst({
|
||||||
where: { id: userId, tenantId },
|
where: { id: userId, tenantId },
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -88,9 +90,20 @@ export class ModuleGrantsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Die Mandanten-Gegenpruefung bleibt ausdruecklich erhalten (T-JTS-03,
|
||||||
|
// 260909-jts, Aufgabe 1): die ausgelieferte Regel auf ModuleGrant
|
||||||
|
// prueft ausschliesslich die Mandantenkennung der Zeile selbst
|
||||||
|
// ("tenantId" = current_tenant_id()), NICHT die referenzierte Gruppe.
|
||||||
|
// Eine Zeile mit korrekter eigener Mandantenkennung, die auf die
|
||||||
|
// Gruppe eines fremden Mandanten zeigt, verletzt diese Regel
|
||||||
|
// nachweislich nicht (gemessen gegen die echte Migration in Aufgabe 1).
|
||||||
|
// Diese Anwendungspruefung ist damit der einzige Schutz gegen diese
|
||||||
|
// Form der Rechteausweitung und darf nicht als "macht jetzt die
|
||||||
|
// Datenbank" entfallen.
|
||||||
await this.assertTargetBelongsToTenant(tenantId, groupId, userId);
|
await this.assertTargetBelongsToTenant(tenantId, groupId, userId);
|
||||||
|
|
||||||
const activation = await this.prisma.tenantModuleActivation.findUnique({
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
|
||||||
where: { tenantId_moduleId: { tenantId, moduleId } },
|
where: { tenantId_moduleId: { tenantId, moduleId } },
|
||||||
});
|
});
|
||||||
if (!activation?.isActive) {
|
if (!activation?.isActive) {
|
||||||
@@ -102,7 +115,7 @@ export class ModuleGrantsService {
|
|||||||
const target = groupId ? `group=${groupId}` : `user=${userId}`;
|
const target = groupId ? `group=${groupId}` : `user=${userId}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const created = await this.prisma.moduleGrant.create({
|
const created = await tenantPrisma.moduleGrant.create({
|
||||||
data: {
|
data: {
|
||||||
tenantId,
|
tenantId,
|
||||||
moduleId,
|
moduleId,
|
||||||
@@ -116,7 +129,7 @@ export class ModuleGrantsService {
|
|||||||
return created;
|
return created;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err?.code === 'P2002') {
|
if (err?.code === 'P2002') {
|
||||||
const existing = await this.prisma.moduleGrant.findFirst({
|
const existing = await tenantPrisma.moduleGrant.findFirst({
|
||||||
where: {
|
where: {
|
||||||
tenantId,
|
tenantId,
|
||||||
moduleId,
|
moduleId,
|
||||||
@@ -148,7 +161,8 @@ export class ModuleGrantsService {
|
|||||||
const { moduleId, groupId, userId } = data;
|
const { moduleId, groupId, userId } = data;
|
||||||
const target = groupId ? `group=${groupId}` : `user=${userId}`;
|
const target = groupId ? `group=${groupId}` : `user=${userId}`;
|
||||||
|
|
||||||
await this.prisma.moduleGrant.deleteMany({
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
|
await tenantPrisma.moduleGrant.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
tenantId,
|
tenantId,
|
||||||
moduleId,
|
moduleId,
|
||||||
@@ -170,16 +184,17 @@ export class ModuleGrantsService {
|
|||||||
* hinweg stabil.
|
* hinweg stabil.
|
||||||
*/
|
*/
|
||||||
async getMatrix(tenantId: string) {
|
async getMatrix(tenantId: string) {
|
||||||
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
const [activations, groups, groupGrants] = await Promise.all([
|
const [activations, groups, groupGrants] = await Promise.all([
|
||||||
this.prisma.tenantModuleActivation.findMany({
|
tenantPrisma.tenantModuleActivation.findMany({
|
||||||
where: { tenantId, isActive: true },
|
where: { tenantId, isActive: true },
|
||||||
include: { module: true },
|
include: { module: true },
|
||||||
}),
|
}),
|
||||||
this.prisma.group.findMany({
|
tenantPrisma.group.findMany({
|
||||||
where: { tenantId },
|
where: { tenantId },
|
||||||
orderBy: { name: 'asc' },
|
orderBy: { name: 'asc' },
|
||||||
}),
|
}),
|
||||||
this.prisma.moduleGrant.findMany({
|
tenantPrisma.moduleGrant.findMany({
|
||||||
where: { tenantId, groupId: { not: null } },
|
where: { tenantId, groupId: { not: null } },
|
||||||
select: { moduleId: true, groupId: true },
|
select: { moduleId: true, groupId: true },
|
||||||
}),
|
}),
|
||||||
@@ -226,26 +241,30 @@ export class ModuleGrantsService {
|
|||||||
async getUserAccess(tenantId: string, userId: string) {
|
async getUserAccess(tenantId: string, userId: string) {
|
||||||
await this.assertTargetBelongsToTenant(tenantId, undefined, userId);
|
await this.assertTargetBelongsToTenant(tenantId, undefined, userId);
|
||||||
|
|
||||||
|
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||||
const [activations, groupGrants, directGrants, memberships] = await Promise.all([
|
const [activations, groupGrants, directGrants, memberships] = await Promise.all([
|
||||||
this.prisma.tenantModuleActivation.findMany({
|
tenantPrisma.tenantModuleActivation.findMany({
|
||||||
where: { tenantId, isActive: true },
|
where: { tenantId, isActive: true },
|
||||||
include: { module: true },
|
include: { module: true },
|
||||||
}),
|
}),
|
||||||
this.prisma.moduleGrant.findMany({
|
tenantPrisma.moduleGrant.findMany({
|
||||||
where: { tenantId, group: { memberships: { some: { userId } } } },
|
where: { tenantId, group: { memberships: { some: { userId } } } },
|
||||||
include: { group: true },
|
include: { group: true },
|
||||||
}),
|
}),
|
||||||
this.prisma.moduleGrant.findMany({
|
tenantPrisma.moduleGrant.findMany({
|
||||||
where: { tenantId, userId },
|
where: { tenantId, userId },
|
||||||
select: { moduleId: true },
|
select: { moduleId: true },
|
||||||
}),
|
}),
|
||||||
// Kein forTenant hier — dieselbe Begründung wie bei den drei
|
// Mandantengebunden seit 260909-jts (Aufgabe 3): der Kontext wird
|
||||||
// Abfragen oben: die Datenbankrolle umgeht RLS ohnehin (siehe
|
// über denselben tenantPrisma wie die drei Abfragen oben gesetzt —
|
||||||
// Migration 20260804130918_groups_rls_policies), der `where`-Filter
|
// es entsteht kein zweiter gebundener Client. Der `where`-Filter
|
||||||
// ist wie im Rest dieser Methode und in GroupsService der primäre
|
// über die Beziehung zur Gruppe (`group: { tenantId }`) bleibt
|
||||||
// Schutz. GroupMembership trägt keine eigene tenantId-Spalte, daher
|
// ZUSÄTZLICH stehen: GroupMembership trägt keine eigene tenantId-
|
||||||
// läuft der Mandantenfilter über die Relation `group: { tenantId }`.
|
// Spalte, und die ausgelieferte Regel auf dieser Tabelle bezieht
|
||||||
this.prisma.groupMembership.findMany({
|
// ihre Sichtbarkeit ausschließlich über die Gruppenseite (gemessen
|
||||||
|
// in Aufgabe 1) — der Anwendungsfilter ist deshalb nicht redundant,
|
||||||
|
// sondern das zweite Netz.
|
||||||
|
tenantPrisma.groupMembership.findMany({
|
||||||
where: { userId, group: { tenantId } },
|
where: { userId, group: { tenantId } },
|
||||||
include: { group: { select: { id: true, name: true, internalName: true } } },
|
include: { group: { select: { id: true, name: true, internalName: true } } },
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -146,6 +146,15 @@ interpretieren:
|
|||||||
ohne Standardgruppe zurück. `groups` ist ohnehin als nächster Bereich der
|
ohne Standardgruppe zurück. `groups` ist ohnehin als nächster Bereich der
|
||||||
Etappe 2 vorgesehen.
|
Etappe 2 vorgesehen.
|
||||||
|
|
||||||
|
**Nachtrag (260909-jts, Aufgabe 3): GESCHLOSSEN.** Der Bereich `groups`
|
||||||
|
ist umgestellt — `reassignDefaultBeforeDelete` und `ensureDefaultGroup`
|
||||||
|
laufen seit Aufgabe 2 dieses Plans vollständig über `forTenant()` bzw.
|
||||||
|
`withTenantTransaction()` (siehe Abschnitt "Bereich groups" unten und
|
||||||
|
`docs/mandantentrennung-zugriffsklassifikation.md`, Zeile
|
||||||
|
`groups.service.ts`/`group`, Stand `gebunden`). Die Reihenfolgebedingung
|
||||||
|
für Etappe 4 ist damit erfüllt. Der Befund oben bleibt unverändert stehen
|
||||||
|
— er beschreibt korrekt den Zustand zum Zeitpunkt der ldap-Umstellung.
|
||||||
|
|
||||||
- **Die offene Architekturfrage `req.tenantPrisma`.** `tenant.middleware.ts`
|
- **Die offene Architekturfrage `req.tenantPrisma`.** `tenant.middleware.ts`
|
||||||
und `tenant.guard.ts` setzen `req.tenantPrisma = forTenant(...)`, aber
|
und `tenant.guard.ts` setzen `req.tenantPrisma = forTenant(...)`, aber
|
||||||
kein Controller liest diesen Wert je. Dieser Durchlauf entscheidet NICHT,
|
kein Controller liest diesen Wert je. Dieser Durchlauf entscheidet NICHT,
|
||||||
|
|||||||
@@ -80,12 +80,24 @@ Bestandsaufnahme unten.
|
|||||||
Gemessen mit
|
Gemessen mit
|
||||||
`grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/<bereich> | grep -v spec | wc -l`
|
`grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/<bereich> | grep -v spec | wc -l`
|
||||||
bzw. `grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/<bereich> | grep -v spec | wc -l`
|
bzw. `grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/<bereich> | grep -v spec | wc -l`
|
||||||
am 2026-09-09, **nach** den Änderungen aus Aufgabe 2/3 dieses Plans (260909-ipc):
|
am 2026-09-09, **nach** den Änderungen aus Aufgabe 2/3 dieses Plans (260909-jts):
|
||||||
|
|
||||||
|
**Methodische Lücke, seit 260909-jts sichtbar:** die zweite Zählung sucht
|
||||||
|
ausschließlich den Namen `tenantPrisma` — die Konvention, die `ldap` und
|
||||||
|
(bis auf die drei Transaktionen) auch `groups` verwenden. Die drei
|
||||||
|
`withTenantTransaction()`-Aufrufe in `groups.service.ts` binden zusätzliche
|
||||||
|
neun Modellzugriffe über den Namen `tx` (den Transaktionsparameter), die
|
||||||
|
diese einfache Rohtrefferzählung strukturell NICHT sieht — anders als die
|
||||||
|
maschinelle, namensunabhängige Erkennung in `rls-access-inventory.spec.ts`
|
||||||
|
(Befund B), die auch diese Form erfasst. Die Zahl 31 unten ist deshalb der
|
||||||
|
Bodensatz, nicht die vollständige Zahl gebundener Zugriffe in `groups`; die
|
||||||
|
Bestandsaufnahme unten (Spalte "Stand", je (Datei, Modell)-Paar) ist die
|
||||||
|
autoritative Quelle.
|
||||||
|
|
||||||
| Bereich | Ungebunden | Gebunden | Hinweis |
|
| Bereich | Ungebunden | Gebunden | Hinweis |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| tenders | 62 | 0 | unverändert |
|
| tenders | 62 | 0 | unverändert |
|
||||||
| groups | 37 | 0 | unverändert |
|
| groups | 0 | 31 | **war 37/0** — Aufgabe 2/3 (260909-jts) haben `groups.service.ts` (12 Methoden) und `module-grants.service.ts` (5 Methoden) vollständig auf `forTenant()`/`withTenantTransaction()` umgestellt. Die neun zusätzlichen, über `tx` gebundenen Zugriffe innerhalb der drei Transaktionen zählt dieses einfache Muster nicht mit (siehe Methodenhinweis oben) |
|
||||||
| 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 | 21 | 0 | unverändert |
|
| dkv | 21 | 0 | unverändert |
|
||||||
| user | 17 | 0 | unverändert |
|
| user | 17 | 0 | unverändert |
|
||||||
@@ -96,26 +108,29 @@ am 2026-09-09, **nach** den Änderungen aus Aufgabe 2/3 dieses Plans (260909-ipc
|
|||||||
| 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** | **210** | **31** | Ungebunden: war 227 vor dieser Etappe (260909-eor-Stand), Delta = die 17 in Aufgabe 2/3 umgestellten `ldap`-Rohtreffer. Gebunden: war 5 (nur `auth`), jetzt zusätzlich 26 in `ldap` |
|
| **Summe** | **173** | **62** | Ungebunden: war 210 vor dieser Etappe (260909-ipc-Stand), Delta = die 37 in Aufgabe 2/3 (260909-jts) umgestellten `groups`-Rohtreffer. Gebunden: war 31, jetzt zusätzlich 31 in `groups` (Bodensatz — siehe Methodenhinweis oben) |
|
||||||
|
|
||||||
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 61 Paare)
|
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 62 Paare)
|
||||||
|
|
||||||
Stand 260909-ipc (Aufgabe 2): 59 Paare aus der urspruenglichen Zaehlung plus
|
Stand 260909-jts (Aufgabe 3): 61 Paare aus dem vorherigen Durchlauf
|
||||||
zwei bisher unentdeckte, weil bereits gebundene Fundstellen
|
(260909-ipc) plus ein bisher vollstaendig unsichtbares Paar
|
||||||
(`auth.service.ts`/`passwordResetToken`, `ldap.service.ts`/`groupMembership`),
|
(`groups.service.ts`/`tenantModuleActivation`), das erst die um
|
||||||
die erst die um gebundene Zugriffe erweiterte Erkennung (Befund G) sichtbar
|
Transaktionsparameter erweiterte Erkennung (Befund B, Aufgabe 2 dieses
|
||||||
macht — sie waren nie Teil der 227 `this.prisma.*`-Rohtrefferzahl, weil sie
|
Plans) sichtbar macht — der einzige Zugriff auf dieses Modell in dieser
|
||||||
schon vor diesem Plan über `forTenant()` liefen. Dazu die Korrektur von
|
Datei lief bis dahin ausschliesslich ueber den Rueckgabeparameter der
|
||||||
(`ldap-config.service.ts`, `ldapConfig`) von `muss-mandantengebunden` auf
|
interaktiven Transaktion in `ensureDefaultGroup` und war weder ueber
|
||||||
`beides` (Befund B).
|
`this.prisma.<Modell>` noch ueber `<gebundener Client>.<Modell>` erfassbar.
|
||||||
|
Die Zahl ist der Ausgabe der Pruefung in
|
||||||
|
`apps/api/src/prisma/rls-access-inventory.spec.ts` entnommen, nicht
|
||||||
|
geschaetzt.
|
||||||
|
|
||||||
| Klasse | Anzahl Paare |
|
| Klasse | Anzahl Paare |
|
||||||
|---|---|
|
|---|---|
|
||||||
| muss-mandantengebunden | 32 |
|
| muss-mandantengebunden | 33 |
|
||||||
| keine-mandantengebundene-tabelle | 16 |
|
| keine-mandantengebundene-tabelle | 16 |
|
||||||
| beides | 10 |
|
| beides | 10 |
|
||||||
| bewusst-uebergreifend | 3 |
|
| bewusst-uebergreifend | 3 |
|
||||||
| **Summe** | **61** |
|
| **Summe** | **62** |
|
||||||
|
|
||||||
## Der Hintergrunddienst als Falle — drei `beides`-Fälle
|
## Der Hintergrunddienst als Falle — drei `beides`-Fälle
|
||||||
|
|
||||||
@@ -134,16 +149,17 @@ betroffen:
|
|||||||
`resolveEmailForWrite` bewusst ungebunden (Befund A, T-IPC-04 — siehe
|
`resolveEmailForWrite` bewusst ungebunden (Befund A, T-IPC-04 — siehe
|
||||||
Bestandsaufnahme unten). Der als gefährlichster Punkt benannte
|
Bestandsaufnahme unten). Der als gefährlichster Punkt benannte
|
||||||
Löschzweig (`syncBoundGroupsForTenant`, WINDOWS #20) war bereits seit
|
Löschzweig (`syncBoundGroupsForTenant`, WINDOWS #20) war bereits seit
|
||||||
Etappe 1 gebunden. Offen bleibt eine Reihenfolgebedingung für Etappe 4,
|
Etappe 1 gebunden. **Die seinerzeit offen geführte Reihenfolgebedingung
|
||||||
NICHT Teil dieser Umstellung: die Übergabe unmittelbar vor der Löschung —
|
für Etappe 4 — die Übergabe unmittelbar vor der Löschung
|
||||||
`this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id)` und
|
(`this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id)` und
|
||||||
`ensureDefaultGroup(tenantId)` — liegt in `groups.service.ts` und ist
|
`ensureDefaultGroup(tenantId)` in `groups.service.ts`) — ist mit
|
||||||
nicht gebunden. Nach dem Scharfschalten würde `reassignDefaultBeforeDelete`
|
260909-jts (Aufgabe 2) GESCHLOSSEN:** beide Methoden laufen seither über
|
||||||
still `false` melden (kein Ersatzkandidat sichtbar), der Standard-Marker
|
`forTenant()`/`withTenantTransaction()` (siehe Bestandsaufnahme unten,
|
||||||
wandert nicht mit, und der Mandant bliebe nach einer Gruppenlöschung ohne
|
`groups.service.ts`/`group`, Stand `gebunden`). Nachtrag, nicht
|
||||||
Standardgruppe zurück — der Bereich `groups` muss deshalb vor Etappe 4
|
Neuschrieb: der ursprüngliche Befund bleibt oben lesbar, weil er den
|
||||||
umgestellt sein (siehe `docs/mandantentrennung-etappe2-fehlerrichtung.md`,
|
Zustand zum Zeitpunkt der ldap-Umstellung korrekt beschreibt und für
|
||||||
Abschnitt (e), Befund D).
|
spätere Etappen als Beleg dient, siehe auch
|
||||||
|
`docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt (e), Befund D.
|
||||||
- **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest): liest
|
- **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest): liest
|
||||||
`tenderMatch`/`tenderNotificationPref`/`user` bewusst über ALLE Mandanten
|
`tenderMatch`/`tenderNotificationPref`/`user` bewusst über ALLE Mandanten
|
||||||
in einem `findMany` (ein einziger globaler Cron-Job, kein Mandant im
|
in einem `findMany` (ein einziger globaler Cron-Job, kein Mandant im
|
||||||
@@ -181,11 +197,11 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
|||||||
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B). |
|
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | gebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. Seit 260909-jts gebunden, einschliesslich des Zugriffs innerhalb des Standardgruppen-Aufbaus (Befund B). |
|
||||||
| apps/api/src/groups/groups.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von `ensureDefaultGroup` ueber den Rueckgabeparameter (`tx.tenantModuleActivation.findMany`) — weder `this.prisma.<Modell>` noch `<gebundener Client>.<Modell>` sahen das, weil `tx` weder `this.prisma` noch aus einer `forTenant(`-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber `withTenantTransaction()`). |
|
| apps/api/src/groups/groups.service.ts | tenantModuleActivation | muss-mandantengebunden | gebunden | Welche Module ein Mandant aktiviert hat. Bis 260909-jts vollstaendig unsichtbar (Befund B, 260909-jts-PLAN.md): der einzige Zugriff dieser Datei lief innerhalb der interaktiven Transaktion von `ensureDefaultGroup` ueber den Rueckgabeparameter (`tx.tenantModuleActivation.findMany`) — weder `this.prisma.<Modell>` noch `<gebundener Client>.<Modell>` sahen das, weil `tx` weder `this.prisma` noch aus einer `forTenant(`-Zuweisung stammte. Die um Transaktionsparameter erweiterte Erkennung aus Aufgabe 2 macht dieses Paar erstmals sichtbar; die Stelle ist seit derselben Aufgabe gebunden (ueber `withTenantTransaction()`). |
|
||||||
| 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) — die Regel auf `GroupMembership` prueft nachweislich nur die Gruppenseite. |
|
| 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) — die Regel auf `GroupMembership` prueft nachweislich nur die Gruppenseite. |
|
||||||
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | ungebunden | Wie groups.service.ts. |
|
| 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 | ungebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. |
|
| 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 | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant. |
|
| 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, weil die Regel auf dieser Tabelle nur die Mandantenkennung der Zeile prüft, nicht die referenzierte Gruppe (Befund F, T-JTS-03, Aufgabe 1). |
|
||||||
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. |
|
| 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 | ungebunden | Zielbenutzer eines Grants innerhalb des Mandanten. |
|
| 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 | 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.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. |
|
||||||
@@ -235,7 +251,11 @@ verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
|||||||
`ldap` (260909-ipc) hat sich für den Dienst-internen Weg entschieden —
|
`ldap` (260909-ipc) hat sich für den Dienst-internen Weg entschieden —
|
||||||
`forTenant(this.prisma, tenantId)` wird in jeder umgestellten Methode neu
|
`forTenant(this.prisma, tenantId)` wird in jeder umgestellten Methode neu
|
||||||
erzeugt, wie es die vier Bestandsstellen in `ldap.service.ts` und die drei
|
erzeugt, wie es die vier Bestandsstellen in `ldap.service.ts` und die drei
|
||||||
in `auth.service.ts` bereits vormachten. Die Frage bleibt für alle
|
in `auth.service.ts` bereits vormachten. Der Bereich `groups` (260909-jts)
|
||||||
|
hat sich für denselben dienst-internen Weg entschieden — jede Methode in
|
||||||
|
`groups.service.ts` und `module-grants.service.ts` erzeugt ihren eigenen
|
||||||
|
`forTenant()`- bzw. `withTenantTransaction()`-Aufruf, gebundene Clients
|
||||||
|
werden nicht zwischen Methoden weitergereicht. Die Frage bleibt für alle
|
||||||
übrigen Bereiche der Etappe 2 offen.
|
übrigen Bereiche der Etappe 2 offen.
|
||||||
- Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
|
- Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
|
||||||
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
|
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
|
||||||
|
|||||||
Reference in New Issue
Block a user