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:
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user