fix(16): WR-01 name lock in GroupsService.update() also checks ldapDn

A legacy binding from plan 15-06 has ldapDn set but ldapObjectGuid stays
null until the first syncBoundGroupsForTenant() run backfills it. Until
then, GroupsService.update() let a direct PATCH rename through even
though GroupFormModal.tsx already treats the same group as AD-bound
(isImported = ldapDn != null) — the D-03 name lock was only a UI
convention for that window, not the backend invariant the 16-02 summary
claimed.
This commit is contained in:
2026-08-06 16:56:30 +02:00
parent 7464d32625
commit dd59bf592f
2 changed files with 33 additions and 5 deletions
@@ -409,6 +409,28 @@ describe('GroupsService', () => {
expect(result.name).toBe('Neu'); 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 () => { it('setzt internalName auf einer importierten Gruppe, name bleibt unangetastet', async () => {
const prisma = makeFakePrisma(); const prisma = makeFakePrisma();
const service = new GroupsService(prisma as any); const service = new GroupsService(prisma as any);
+11 -5
View File
@@ -107,10 +107,16 @@ export class GroupsService {
* setzen. * setzen.
* *
* Namenssperre (D-03/D-07): trägt die geladene Gruppe einen gesetzten * Namenssperre (D-03/D-07): trägt die geladene Gruppe einen gesetzten
* ldapObjectGuid, ist sie aus dem Verzeichnis importiert — ein `name` * ldapObjectGuid ODER ldapDn, ist sie aus dem Verzeichnis importiert —
* im Request wird dann mit BadRequestException abgelehnt, BEVOR die * ein `name` im Request wird dann mit BadRequestException abgelehnt,
* Leerstring-Prüfung läuft. Das ist eine Backend-Invariante, kein UI- * BEVOR die Leerstring-Prüfung läuft. Das ldapDn-Kriterium deckt eine
* Feld-Disable: ein direkter API-Aufruf kommt an ihr nicht vorbei. * 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, * internalName (D-04) läuft unabhängig von dieser Sperre — jede Gruppe,
* importiert oder lokal, darf ihn setzen. undefined lässt die Spalte * importiert oder lokal, darf ihn setzen. undefined lässt die Spalte
@@ -132,7 +138,7 @@ export class GroupsService {
} = {}; } = {};
if (data.name !== undefined) { if (data.name !== undefined) {
if (existing.ldapObjectGuid) { if (existing.ldapObjectGuid || existing.ldapDn) {
throw new BadRequestException( throw new BadRequestException(
'Der Name einer aus dem Verzeichnis übernommenen Gruppe wird dort gepflegt und kann hier nicht geändert werden', 'Der Name einer aus dem Verzeichnis übernommenen Gruppe wird dort gepflegt und kann hier nicht geändert werden',
); );