diff --git a/apps/api/src/groups/groups.service.spec.ts b/apps/api/src/groups/groups.service.spec.ts index c456321..a77580e 100644 --- a/apps/api/src/groups/groups.service.spec.ts +++ b/apps/api/src/groups/groups.service.spec.ts @@ -409,6 +409,28 @@ describe('GroupsService', () => { expect(result.name).toBe('Neu'); }); + it('lehnt name fuer eine Alt-Bindung (ldapDn gesetzt, ldapObjectGuid noch null) mit BadRequestException ab (WR-01, 16-REVIEW.md)', async () => { + // Eine Gruppe aus der alten Plan-15-06-Bindung: ldapDn ist gesetzt, + // aber der neue Rekonziliations-Schritt (syncBoundGroupsForTenant, + // Legacy-Backfill) hat sie noch nicht durchlaufen — ldapObjectGuid + // ist deshalb noch null. Ohne die ldapDn-Bedingung wuerde dieser + // direkte PATCH-Aufruf durchgehen, obwohl GroupFormModal.tsx dieselbe + // Gruppe bereits als importiert sperrt (isImported = ldapDn != null). + const prisma = makeFakePrisma(); + const service = new GroupsService(prisma as any); + const group = await service.create('t1', { name: 'Vertrieb' }); + await (prisma as any).group.update({ + where: { id: group.id }, + data: { ldapDn: 'cn=Vertrieb,dc=example,dc=com', ldapObjectGuid: null }, + }); + + await expect( + service.update('t1', group.id, { name: 'Umbenannt' }), + ).rejects.toBeInstanceOf(BadRequestException); + const list = await service.listForTenant('t1'); + expect(list.find((g) => g.id === group.id)!.name).toBe('Vertrieb'); + }); + it('setzt internalName auf einer importierten Gruppe, name bleibt unangetastet', async () => { const prisma = makeFakePrisma(); const service = new GroupsService(prisma as any); diff --git a/apps/api/src/groups/groups.service.ts b/apps/api/src/groups/groups.service.ts index c62fe15..253d259 100644 --- a/apps/api/src/groups/groups.service.ts +++ b/apps/api/src/groups/groups.service.ts @@ -107,10 +107,16 @@ export class GroupsService { * setzen. * * Namenssperre (D-03/D-07): trägt die geladene Gruppe einen gesetzten - * ldapObjectGuid, ist sie aus dem Verzeichnis importiert — ein `name` - * im Request wird dann mit BadRequestException abgelehnt, BEVOR die - * Leerstring-Prüfung läuft. Das ist eine Backend-Invariante, kein UI- - * Feld-Disable: ein direkter API-Aufruf kommt an ihr nicht vorbei. + * ldapObjectGuid ODER ldapDn, ist sie aus dem Verzeichnis importiert — + * ein `name` im Request wird dann mit BadRequestException abgelehnt, + * BEVOR die Leerstring-Prüfung läuft. Das ldapDn-Kriterium deckt eine + * Alt-Bindung aus Plan 15-06 ab, die den neuen Rekonziliations-Schritt + * (syncBoundGroupsForTenant, Legacy-Backfill) noch nicht durchlaufen hat + * und deshalb noch keinen ldapObjectGuid trägt — ohne diese zweite + * Bedingung wäre die Sperre bis zum ersten Sync-Lauf nur eine + * UI-Konvention (WR-01, 16-REVIEW.md). Das ist eine Backend-Invariante, + * kein UI-Feld-Disable: ein direkter API-Aufruf kommt an ihr nicht + * vorbei. * * internalName (D-04) läuft unabhängig von dieser Sperre — jede Gruppe, * importiert oder lokal, darf ihn setzen. undefined lässt die Spalte @@ -132,7 +138,7 @@ export class GroupsService { } = {}; if (data.name !== undefined) { - if (existing.ldapObjectGuid) { + if (existing.ldapObjectGuid || existing.ldapDn) { throw new BadRequestException( 'Der Name einer aus dem Verzeichnis übernommenen Gruppe wird dort gepflegt und kann hier nicht geändert werden', );