fix: kritische Rechte-Luecke, Admin-Lockout-Schutz und Wetter-Haertung
Dispatchte security-reviewer und code-reviewer Agenten fuer eine vollstaendige Pruefung des RBAC-Systems (d31aa79..HEAD). Ein KRITISCHER Fund, zwei HIGH, ein weiteres MEDIUM, alle behoben: KRITISCH: Deaktivieren eines Benutzers hat dessen Berechtigungen nicht entzogen. PermissionResolverService.loadPermissions() prüfte nur den Rollen-Status, nie User.isActive - ein bereits ausgestelltes Access- Token behielt volle Rechte bis zum Ablauf (bis zu 15 Minuten), nicht die vorgesehenen 30 Sekunden. UsersService rief zudem nie invalidateUser() auf. Beides jetzt behoben; neue dedizierte permission-resolver.service.spec.ts deckt das ab (existierte bisher gar nicht - ein Grund, warum die Luecke unbemerkt blieb). HIGH: Kein Schutz gegen den letzten aktiven Systemrollen-Nutzer (ADMIN). Man konnte den letzten Admin deaktivieren oder umrollen und sich damit komplett aussperren - ohne Selbsthilfe-Weg, da prisma/seed.ts das Seeden ueberspringt, sobald die Admin-E-Mail bereits existiert. UsersService.deactivate()/.update() pruefen jetzt per assertNotLastSystemRoleUser(), analog zum bestehenden Lockout-Schutz in RolesService fuer Systemrollen selbst. HIGH: Der Open-Meteo-Aufruf in WeatherService hatte kein Timeout und konnte unbegrenzt haengen. Jetzt mit AbortSignal.timeout(8000) plus sauberem Fehler-Mapping. MEDIUM: Trachtprognose fiel bei fehlenden Wetterdaten stillschweigend auf "MAESSIG" zurueck statt einen eigenen Unbekannt-Zustand zu zeigen - ein Imker haette eine echte Einschaetzung gesehen, wo keine moeglich war. Neuer Rating-Wert UNBEKANNT in Backend und Frontend. LOW: Veralteter Kommentar an PermissionsGuard, der noch den in Phase 3 geloeschten RolesGuard erwaehnte, korrigiert. Verifiziert: tsc, lint, build auf beiden Projekten gruen. Backend-Unit 245/245 (17 neue Tests fuer die Fixes). Frontend-Unit 14/14. Volle Backend-e2e-Suite konnte weiterhin nicht laufen (WSL/Postgres von Windows aus nicht erreichbar, auch nach vollem "wsl --shutdown"- Neustart) - jeder Fix hat aber gezielte, gruene Unit-Abdeckung. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
f1d6b2956e
commit
5a1015c4a0
9 files changed
+329
-11
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { PermissionResolverService } from '../auth/permissions/permission-resolver.service';
|
||||
import { Prisma } from '../../generated/prisma';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
@@ -35,6 +36,7 @@ export class UsersService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly auditLog: AuditLogService,
|
||||
private readonly permissionResolver: PermissionResolverService,
|
||||
) {}
|
||||
|
||||
private toSummary(user: UserWithRole): UserSummary {
|
||||
@@ -61,6 +63,32 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks removing (by deactivation or role reassignment) the last active
|
||||
* user still holding a system role (ADMIN). Without this, an operator
|
||||
* could lock the app out of user/role management entirely, with no
|
||||
* self-service recovery: prisma/seed.ts skips seeding once a user with
|
||||
* its configured email already exists, so it cannot restore access to a
|
||||
* merely demoted/deactivated account.
|
||||
*/
|
||||
private async assertNotLastSystemRoleUser(userId: string): Promise<void> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { roleId: true, role: { select: { isSystem: true } } },
|
||||
});
|
||||
if (!user || !user.role.isSystem) {
|
||||
return;
|
||||
}
|
||||
const activeCount = await this.prisma.user.count({
|
||||
where: { roleId: user.roleId, isActive: true },
|
||||
});
|
||||
if (activeCount <= 1) {
|
||||
throw new ConflictException(
|
||||
'Cannot remove the last active user holding a system role',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(): Promise<UserSummary[]> {
|
||||
const users = await this.prisma.user.findMany({
|
||||
...USER_WITH_ROLE,
|
||||
@@ -123,9 +151,13 @@ export class UsersService {
|
||||
actingUserId?: string,
|
||||
): Promise<UserSummary> {
|
||||
const before = await this.findById(id);
|
||||
const roleChanged = Boolean(dto.roleId) && dto.roleId !== before.roleId;
|
||||
if (dto.roleId) {
|
||||
await this.assertActiveRole(dto.roleId);
|
||||
}
|
||||
if (roleChanged) {
|
||||
await this.assertNotLastSystemRoleUser(id);
|
||||
}
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -134,6 +166,9 @@ export class UsersService {
|
||||
},
|
||||
...USER_WITH_ROLE,
|
||||
});
|
||||
if (roleChanged) {
|
||||
this.permissionResolver.invalidateUser(id);
|
||||
}
|
||||
await this.auditLog.record({
|
||||
entityType: 'User',
|
||||
entityId: id,
|
||||
@@ -147,11 +182,13 @@ export class UsersService {
|
||||
|
||||
async deactivate(id: string, actingUserId?: string): Promise<UserSummary> {
|
||||
const before = await this.findById(id);
|
||||
await this.assertNotLastSystemRoleUser(id);
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { isActive: false, deactivatedAt: new Date() },
|
||||
...USER_WITH_ROLE,
|
||||
});
|
||||
this.permissionResolver.invalidateUser(id);
|
||||
await this.auditLog.record({
|
||||
entityType: 'User',
|
||||
entityId: id,
|
||||
@@ -173,6 +210,7 @@ export class UsersService {
|
||||
data: { isActive: true, deactivatedAt: null },
|
||||
...USER_WITH_ROLE,
|
||||
});
|
||||
this.permissionResolver.invalidateUser(id);
|
||||
await this.auditLog.record({
|
||||
entityType: 'User',
|
||||
entityId: id,
|
||||
|
||||
Reference in new issue
Block a user