diff --git a/apps/api/src/user/user.controller.spec.ts b/apps/api/src/user/user.controller.spec.ts index c709c1a..eeb6151 100644 --- a/apps/api/src/user/user.controller.spec.ts +++ b/apps/api/src/user/user.controller.spec.ts @@ -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(); + }); + }); }); diff --git a/apps/api/src/user/user.controller.ts b/apps/api/src/user/user.controller.ts index 96d62b5..b765f17 100644 --- a/apps/api/src/user/user.controller.ts +++ b/apps/api/src/user/user.controller.ts @@ -188,6 +188,17 @@ export class UserController { 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 if (currentUser.role !== Role.SUPER_ADMIN && dto.role === Role.SUPER_ADMIN) { throw new ForbiddenException('Cannot assign SUPER_ADMIN role'); @@ -244,6 +255,13 @@ export class UserController { 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 // update() oben. await this.userService.delete(user.tenantId, id);