feat(quick-260909-ipc): ldap-config.service.ts an forTenant() binden, Loesch-Fremdzugriff schliessen

Aufgabe 2 der Etappe 2: getConfig/createConfig/updateConfig sowie
addFieldMapping/removeFieldMapping laufen jetzt ueber forTenant(), gebunden
an den aus der Anfrage bekannten Mandanten. removeFieldMapping nimmt den
Mandanten neu als Pflichtparameter entgegen und der Controller holt ihn aus
dem Sitzungsnachweis statt nur die URL-Kennung weiterzureichen (T-IPC-01) --
ein Administrator konnte bisher die Feldzuordnung eines fremden Mandanten
loeschen, wenn er ihre Kennung kannte. getAllActiveConfigs() und die
Start-Nachverschluesselung bleiben bewusst uebergreifend, mit ausgeschriebener
Begruendung im Code (Befund B).

rls-access-inventory.spec.ts erkennt jetzt neben `this.prisma.<Modell>` auch
gebundene `<Name>.<Modell>`-Zugriffe (Befund F/G) und prueft eine neue
Stand-Spalte (gebunden/ungebunden/gemischt) im Klassifikationsdokument gegen
den Quelltext. Das macht zwei bisher unsichtbare, weil schon laenger
gebundene Fundstellen sichtbar (auth.service.ts/passwordResetToken,
ldap.service.ts/groupMembership) und deckt auf, dass
(ldap-config.service.ts, ldapConfig) tatsaechlich "beides" ist, nicht
"muss-mandantengebunden" (Befund B).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-09 14:01:28 +02:00
parent a0c9ef070f
commit 9a57fa79f5
5 changed files with 447 additions and 103 deletions
@@ -1,6 +1,17 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { LdapConfigService } from './ldap-config.service';
// forTenant gibt in den Bestandstests denselben Client zurueck (tenant
// scoping ist dort nicht unter Test) — ohne diesen Mock bricht die Datei am
// blanken Prisma-Ersatz beim ersten `$extends`-Aufruf (Befund F,
// 260909-ipc-PLAN.md). Der eigene Bindungs-Testblock unten biegt die
// Implementierung auf ein zweites, unterscheidbares Client-Objekt um.
vi.mock('../prisma/prisma-tenant.extension', () => ({
forTenant: vi.fn((p: unknown) => p),
}));
import { forTenant } from '../prisma/prisma-tenant.extension';
/**
* Das Bind-Passwort ist das einzige Zugangsdatum, das nicht gehasht werden
* kann — Tessera muss sich damit am Domain Controller anmelden, braucht es
@@ -176,3 +187,125 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
await expect(service.onApplicationBootstrap()).resolves.toBeUndefined();
});
});
/**
* Aufgabe 2 (260909-ipc, WINDOWS #20 Etappe 2, T-IPC-01/T-IPC-02): belegt,
* dass die drei pro-Mandant-Methoden gebunden laufen, die beiden
* uebergreifenden Methoden es NICHT tun, und dass das Loeschen einer
* Feldzuordnung ohne Mandant nicht mehr moeglich ist.
*/
describe('LdapConfigService — Bindung an forTenant() (260909-ipc)', () => {
let prisma: any;
let service: LdapConfigService;
beforeEach(() => {
vi.clearAllMocks();
prisma = {
ldapConfig: {
findUnique: vi.fn().mockResolvedValue(CONFIG_ROW),
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn((args: any) => Promise.resolve({ ...CONFIG_ROW, ...args.data })),
update: vi.fn((args: any) => Promise.resolve({ ...CONFIG_ROW, ...args.data })),
},
ldapFieldMapping: {
create: vi.fn((args: any) => Promise.resolve({ id: 'map1', isDefault: false, ...args.data })),
findUnique: vi.fn(),
delete: vi.fn((args: any) => Promise.resolve({ id: args.where.id })),
},
};
service = new LdapConfigService(prisma, crypto as any);
});
it('getConfig() bindet ueber forTenant() an den uebergebenen Mandanten', async () => {
await service.getConfig('t1');
expect(forTenant).toHaveBeenCalledWith(prisma, 't1');
expect(prisma.ldapConfig.findUnique).toHaveBeenCalledWith(
expect.objectContaining({ where: { tenantId: 't1' } }),
);
});
it('createConfig() bindet ueber forTenant() an den uebergebenen Mandanten', async () => {
await service.createConfig('t1', {
serverUrl: 'ldap://example',
baseDn: 'dc=example,dc=com',
} as any);
expect(forTenant).toHaveBeenCalledWith(prisma, 't1');
expect(prisma.ldapConfig.create).toHaveBeenCalled();
});
it('updateConfig() bindet ueber forTenant() an den uebergebenen Mandanten', async () => {
await service.updateConfig('t1', { serverUrl: 'ldap://anders' } as any);
expect(forTenant).toHaveBeenCalledWith(prisma, 't1');
expect(prisma.ldapConfig.update).toHaveBeenCalledWith(
expect.objectContaining({ where: { tenantId: 't1' } }),
);
});
it('addFieldMapping() nimmt den Mandanten entgegen und schreibt gebunden', async () => {
await service.addFieldMapping('t1', 'cfg1', {
ldapField: 'department',
tesseraField: 'department',
} as any);
expect(forTenant).toHaveBeenCalledWith(prisma, 't1');
expect(prisma.ldapFieldMapping.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ ldapConfigId: 'cfg1' }),
}),
);
});
it('removeFieldMapping() nimmt den Mandanten entgegen, liest gebunden und loescht gebunden', async () => {
prisma.ldapFieldMapping.findUnique.mockResolvedValue({
id: 'map1',
ldapConfigId: 'cfg1',
isDefault: false,
});
const result = await service.removeFieldMapping('t1', 'map1');
expect(forTenant).toHaveBeenCalledWith(prisma, 't1');
expect(prisma.ldapFieldMapping.findUnique).toHaveBeenCalledWith({
where: { id: 'map1' },
});
expect(prisma.ldapFieldMapping.delete).toHaveBeenCalledWith({
where: { id: 'map1' },
});
expect(result).toEqual({ id: 'map1' });
});
it('removeFieldMapping() liefert null, wenn die Zuordnung unter diesem Mandanten nicht sichtbar ist (T-IPC-01)', async () => {
// Simuliert die RLS-Wirkung: unter dem Mandantenkontext von t1 ist eine
// fremde Feldzuordnung (Mandant t2) unsichtbar — findUnique liefert null,
// genau wie es die echte Policy nach dem Scharfschalten taete.
prisma.ldapFieldMapping.findUnique.mockResolvedValue(null);
const result = await service.removeFieldMapping('t1', 'map-fremd');
expect(result).toBeNull();
expect(prisma.ldapFieldMapping.delete).not.toHaveBeenCalled();
});
it('removeFieldMapping() schuetzt Vorgabe-Zuordnungen weiterhin, auch gebunden', async () => {
prisma.ldapFieldMapping.findUnique.mockResolvedValue({
id: 'map1',
ldapConfigId: 'cfg1',
isDefault: true,
});
await expect(service.removeFieldMapping('t1', 'map1')).rejects.toThrow(
'Cannot delete default field mappings',
);
expect(prisma.ldapFieldMapping.delete).not.toHaveBeenCalled();
});
it('getAllActiveConfigs() bleibt bewusst uebergreifend — kein Mandantenkontext', async () => {
await service.getAllActiveConfigs();
expect(forTenant).not.toHaveBeenCalled();
});
it('onApplicationBootstrap() bleibt bewusst uebergreifend — kein Mandantenkontext', async () => {
prisma.ldapConfig.findMany.mockResolvedValue([]);
await service.onApplicationBootstrap();
expect(forTenant).not.toHaveBeenCalled();
});
});