fix(quick-260914-ebg): Zielrollen-Riegel in UserController.update/remove — ADMIN kann SUPER_ADMIN nicht mehr aendern oder loeschen (WINDOWS #29)

- update(): Riegel nach der Mandantengrenze, vor der dto.role-Pruefung — Nicht-SUPER_ADMIN darf SUPER_ADMIN-Ziel nicht mehr aendern (Kennwort, isActive, Rolle, Anmeldename, E-Mail)
- remove(): derselbe Riegel nach der Mandantengrenze, vor userService.delete
- acht neue Tests (Test 9-16): drei Angriffsformen, SUPER_ADMIN-gegen-SUPER_ADMIN-Regression, ADMIN-gegen-USER-Regression, Reihenfolge-Ordnungstests je Handler
- RED-Lauf vor dem Riegel: Tests  2 failed | 14 passed (16) (Test 9, Test 13 rot); GREEN danach: Tests  16 passed (16)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 10:35:23 +02:00
parent e76c0f8a33
commit 759ea3b2ca
2 changed files with 133 additions and 0 deletions
+115
View File
@@ -277,4 +277,119 @@ describe('UserController', () => {
); );
}); });
}); });
describe('update/remove — Zielrolle SUPER_ADMIN (WINDOWS #29)', () => {
it('Test 9: ein Mandanten-Administrator kann den SUPER_ADMIN des eigenen Mandanten weder übernehmen (Kennwort setzen), noch aussperren (isActive=false), noch herabstufen (role=USER) — alle drei Angriffsformen werden mit der Zielrollen-Ausnahme abgelehnt, und der Dienst wird in keinem der drei Fälle aufgerufen', async () => {
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
userService.findById.mockResolvedValue({ id: 'boss', tenantId: 't1', role: Role.SUPER_ADMIN });
await expect(
controller.update('boss', { password: 'fresh-password' } as any, admin),
).rejects.toThrow('Cannot modify a SUPER_ADMIN user');
expect(userService.update).not.toHaveBeenCalled();
await expect(
controller.update('boss', { isActive: false } as any, admin),
).rejects.toThrow('Cannot modify a SUPER_ADMIN user');
expect(userService.update).not.toHaveBeenCalled();
await expect(
controller.update('boss', { role: Role.USER } as any, admin),
).rejects.toThrow('Cannot modify a SUPER_ADMIN user');
expect(userService.update).not.toHaveBeenCalled();
});
it('Test 10: ein SUPER_ADMIN kann einen anderen SUPER_ADMIN weiterhin ändern — der Zielrollen-Riegel gilt nur für Nicht-SUPER_ADMIN-Aufrufer', async () => {
const superAdmin = { role: Role.SUPER_ADMIN, tenantId: 't1', id: 'super1' };
userService.findByIdForPlatformAdmin.mockResolvedValue({
id: 'boss',
tenantId: 't1',
role: Role.SUPER_ADMIN,
});
userService.update.mockResolvedValue({
id: 'boss',
tenantId: 't1',
role: Role.SUPER_ADMIN,
passwordHash: 'h',
});
const result = await controller.update('boss', { password: 'fresh-password' } as any, superAdmin);
expect(userService.update).toHaveBeenCalledWith(
't1',
'boss',
expect.objectContaining({ password: 'fresh-password' }),
);
expect(result).not.toHaveProperty('passwordHash');
});
it('Test 11: ein Mandanten-Administrator kann einen USER seines Mandanten weiterhin ändern — Regressionsschutz, der Zielrollen-Riegel engt bestehende Wege nicht zusätzlich ein', async () => {
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
userService.findById.mockResolvedValue({ id: 'u1', tenantId: 't1', role: Role.USER });
userService.update.mockResolvedValue({ id: 'u1', tenantId: 't1', role: Role.USER });
await controller.update('u1', { displayName: 'Neu' } as any, admin);
expect(userService.update).toHaveBeenCalledWith(
't1',
'u1',
expect.objectContaining({ displayName: 'Neu' }),
);
});
it('Test 12: die Mandantengrenze wird VOR der Zielrollen-Prüfung geprüft — ein Administrator, der (bei einer fehlerhaften Auflösung) ein Ziel eines fremden Mandanten mit der obersten Rolle erhält, bekommt die Mandanten-Meldung, nicht die Zielrollen-Meldung, und erfährt so nichts über die Rolle des fremden Benutzers', async () => {
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
userService.findById.mockResolvedValue({ id: 'boss2', tenantId: 't2', role: Role.SUPER_ADMIN });
await expect(
controller.update('boss2', { displayName: 'Neu' } as any, admin),
).rejects.toThrow('Cannot modify users from other tenants');
expect(userService.update).not.toHaveBeenCalled();
});
it('Test 13: ein Mandanten-Administrator kann den SUPER_ADMIN des eigenen Mandanten nicht löschen — die Zielrollen-Ausnahme greift, und der Dienst wird nicht aufgerufen', async () => {
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
userService.findById.mockResolvedValue({ id: 'boss', tenantId: 't1', role: Role.SUPER_ADMIN });
await expect(controller.remove('boss', admin)).rejects.toThrow(
'Cannot delete a SUPER_ADMIN user',
);
expect(userService.delete).not.toHaveBeenCalled();
});
it('Test 14: ein SUPER_ADMIN kann einen anderen SUPER_ADMIN weiterhin löschen — der Zielrollen-Riegel gilt nur für Nicht-SUPER_ADMIN-Aufrufer', async () => {
const superAdmin = { role: Role.SUPER_ADMIN, tenantId: 't1', id: 'super1' };
userService.findByIdForPlatformAdmin.mockResolvedValue({
id: 'boss',
tenantId: 't1',
role: Role.SUPER_ADMIN,
});
userService.delete.mockResolvedValue({ message: 'User deleted' });
const result = await controller.remove('boss', superAdmin);
expect(userService.delete).toHaveBeenCalledWith('t1', 'boss');
expect(result).toEqual({ message: 'User deleted' });
});
it('Test 15: ein Mandanten-Administrator kann einen USER seines Mandanten weiterhin löschen — Regressionsschutz', async () => {
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
userService.findById.mockResolvedValue({ id: 'u1', tenantId: 't1', role: Role.USER });
userService.delete.mockResolvedValue({ message: 'User deleted' });
await controller.remove('u1', admin);
expect(userService.delete).toHaveBeenCalledWith('t1', 'u1');
});
it('Test 16: die Mandantengrenze wird VOR der Zielrollen-Prüfung geprüft — beim Löschen bekommt ein Administrator mit einem fremdmandantigen Ziel der obersten Rolle die Mandanten-Meldung, nicht die Zielrollen-Meldung', async () => {
const admin = { role: Role.ADMIN, tenantId: 't1', id: 'admin1' };
userService.findById.mockResolvedValue({ id: 'boss2', tenantId: 't2', role: Role.SUPER_ADMIN });
await expect(controller.remove('boss2', admin)).rejects.toThrow(
'Cannot delete users from other tenants',
);
expect(userService.delete).not.toHaveBeenCalled();
});
});
}); });
+18
View File
@@ -188,6 +188,17 @@ export class UserController {
throw new ForbiddenException('Cannot modify users from other tenants'); throw new ForbiddenException('Cannot modify users from other tenants');
} }
// Zielrollen-Riegel (WINDOWS #29, 260914-ebg): die Pruefung unten sichert
// nur die NEUE Zuweisung der obersten Rolle (dto.role) — dieser Riegel
// sichert das ZIEL, das die oberste Rolle bereits traegt, gegen JEDES
// Feld dieses DTO (Kennwort, isActive, Rolle, Anmeldename, E-Mail).
// Vorlage: `AuthService.adminResetPassword` (T-FH9-04). Die
// Mandantengrenze bleibt DAVOR, damit die Meldung nichts ueber die
// Rolle eines fremdmandantigen Benutzers verraet (T-EBG-04).
if (user.role === Role.SUPER_ADMIN && currentUser.role !== Role.SUPER_ADMIN) {
throw new ForbiddenException('Cannot modify a SUPER_ADMIN user');
}
// T-02-08: ADMIN cannot set role to SUPER_ADMIN // T-02-08: ADMIN cannot set role to SUPER_ADMIN
if (currentUser.role !== Role.SUPER_ADMIN && dto.role === Role.SUPER_ADMIN) { if (currentUser.role !== Role.SUPER_ADMIN && dto.role === Role.SUPER_ADMIN) {
throw new ForbiddenException('Cannot assign SUPER_ADMIN role'); throw new ForbiddenException('Cannot assign SUPER_ADMIN role');
@@ -244,6 +255,13 @@ export class UserController {
throw new ForbiddenException('Cannot delete users from other tenants'); throw new ForbiddenException('Cannot delete users from other tenants');
} }
// Zielrollen-Riegel (WINDOWS #29, 260914-ebg): derselbe Riegel wie in
// update() oben — ein Nicht-SUPER_ADMIN darf den SUPER_ADMIN seines
// Mandanten nicht loeschen.
if (user.role === Role.SUPER_ADMIN && currentUser.role !== Role.SUPER_ADMIN) {
throw new ForbiddenException('Cannot delete a SUPER_ADMIN user');
}
// Gebunden an den Mandanten des ZIELBENUTZERS, derselbe Grund wie bei // Gebunden an den Mandanten des ZIELBENUTZERS, derselbe Grund wie bei
// update() oben. // update() oben.
await this.userService.delete(user.tenantId, id); await this.userService.delete(user.tenantId, id);