Aufsicht und Eingriff getrennt: neuer Haken "Audit-Betrieb"
Die DSGVO-Rolle trug audit:* komplett, also auch audit:admin. Ein DSGVO-Beauftragter konnte damit seal-backlog, rehash und cleanup - seine eigene Beweisgrundlage ersetzen. Wer das Protokoll beaufsichtigt, darf es nicht umschreiben koennen. Dieselbe Klasse wie R184-02: falsche Domaene, zu breit gebuendelt. Der naheliegende Fix waere falsch gewesen. audit:admin einfach aus der DSGVO-Rolle zu streichen haette es heimatlos gemacht: Die Admin-Rolle ist ausdruecklich ohne audit/gdpr gebaut, einzige verbleibende Quelle waere der Entwicklerzugriff - der alles gibt. Prod versiegeln haette dann Vollzugriff vorausgesetzt. Deshalb eine eigene versteckte Rolle "Audit-Betrieb" (audit:read + audit:admin), zugewiesen ueber eine Checkbox wie DSGVO/Entwickler. DSGVO behaelt audit:read + audit:export + gdpr:*. Fuer kein bestehendes Konto weitet sich etwas aus; es wird enger, und wer eingreifen koennen soll, bekommt es ausdruecklich. Keine zusaetzliche Rechte-Huerde davor, weil das am Henne-Ei-Problem scheitert: Nach der Aufteilung haelt zunaechst niemand audit:admin, koennte ihn also auch niemand vergeben. Stattdessen wird die Vergabe laut - CRITICAL im Protokoll und PERMISSION_CHANGED/CRITICAL im Alarmkanal, samt Kennzeichen, ob sich jemand den Haken selbst gesetzt hat. Nebenbei geschlossen: setUserGdprAccess() legte die DSGVO-Rolle im Notfallpfad mit audit:* komplett an - eine zweite Liste, die dasselbe bedeuten sollte und die Buendelung stillschweigend zurueckgebracht haette. ACHTUNG beim Deploy: Bestehende DSGVO-Konten verlieren audit:admin. Wer Prod versiegeln will, muss sich vorher "Audit-Betrieb" ankreuzen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import bcrypt from 'bcryptjs';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import * as userService from '../services/user.service.js';
|
||||
import { logChange } from '../services/audit.service.js';
|
||||
import { AUDIT_OPS_ROLLE } from '../services/user.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
|
||||
import { pickUserCreate, pickUserUpdate, isValidEmail, sanitizePhoneField } from '../utils/sanitize.js';
|
||||
@@ -162,9 +163,26 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
|
||||
...beforeUser,
|
||||
hasGdprAccess: beforeUser.roles.some((ur) => ur.role.name === 'DSGVO'),
|
||||
hasDeveloperAccess: beforeUser.roles.some((ur) => ur.role.name === 'Developer'),
|
||||
hasAuditOpsAccess: beforeUser.roles.some((ur) => ur.role.name === AUDIT_OPS_ROLLE),
|
||||
}
|
||||
: null;
|
||||
|
||||
// Der Audit-Betrieb-Haken vergibt die EINGREIFENDEN Rechte am Protokoll
|
||||
// (versiegeln, neu berechnen, aufraeumen, Aufbewahrung). Ihn zu setzen ist
|
||||
// kein gewoehnliches Benutzer-Update: Er entscheidet, wer die
|
||||
// Beweisgrundlage ersetzen darf.
|
||||
//
|
||||
// Eine eigene Rechte-Huerde davorzusetzen scheitert am Henne-Ei-Problem -
|
||||
// nach der Aufteilung haelt zunaechst niemand `audit:admin`, und dann
|
||||
// koennte ihn auch niemand vergeben. Stattdessen wird die Vergabe LAUT:
|
||||
// CRITICAL im Protokoll und zusaetzlich in den Alarmkanal, so wie beim
|
||||
// Dienstkonto-Kennzeichen (R184-01).
|
||||
const setztAuditBetrieb =
|
||||
data.hasAuditOpsAccess !== undefined &&
|
||||
before !== null &&
|
||||
data.hasAuditOpsAccess !== (before as any).hasAuditOpsAccess;
|
||||
const aktiviertAuditBetrieb = data.hasAuditOpsAccess === true;
|
||||
|
||||
// Das Dienstkonto-Kennzeichen SENKT die Alarmstufe der Anmeldungen dieses
|
||||
// Kontos (Pentest R184). Damit ist es selbst ein Hebel zur Waesche: Wer sein
|
||||
// Konto so markiert, laesst die eigenen auffaelligen Anmeldungen als
|
||||
@@ -236,6 +254,7 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
|
||||
const fieldLabels: Record<string, string> = {
|
||||
email: 'E-Mail', firstName: 'Vorname', lastName: 'Nachname', isActive: 'Aktiv',
|
||||
hasGdprAccess: 'DSGVO-Zugriff', hasDeveloperAccess: 'Entwicklerzugriff',
|
||||
hasAuditOpsAccess: 'Audit-Betrieb (versiegeln, aufräumen, Aufbewahrung)',
|
||||
isServiceAccount: 'Dienstkonto (Anmeldungen als Routine)',
|
||||
};
|
||||
for (const [key, newVal] of Object.entries(data)) {
|
||||
@@ -265,7 +284,7 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
|
||||
: `Benutzer ${user.firstName} ${user.lastName} aktualisiert`,
|
||||
// Die Aenderung dieses Kennzeichens wird wie ihre Wirkung eingestuft:
|
||||
// Sie beeinflusst, wie kuenftige Anmeldungen bewertet werden.
|
||||
sensitivity: setztDienstkonto ? 'CRITICAL' : undefined,
|
||||
sensitivity: setztDienstkonto || setztAuditBetrieb ? 'CRITICAL' : undefined,
|
||||
details: Object.keys(changes).length > 0 ? changes : undefined,
|
||||
});
|
||||
|
||||
@@ -291,6 +310,28 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
|
||||
details: { betroffenesKonto: user.email, aktiviert: aktiviertDienstkonto },
|
||||
});
|
||||
}
|
||||
|
||||
if (setztAuditBetrieb) {
|
||||
const ctx = contextFromRequest(req);
|
||||
emitSecurityEvent({
|
||||
type: 'PERMISSION_CHANGED',
|
||||
severity: 'CRITICAL',
|
||||
message: aktiviertAuditBetrieb
|
||||
? `Konto ${user.email} darf ab jetzt am Audit-Protokoll EINGREIFEN: versiegeln, ` +
|
||||
'neu berechnen, aufräumen, Aufbewahrung ändern. Damit kann es die Grundlage ' +
|
||||
'ersetzen, gegen die Manipulation nachgewiesen wird.'
|
||||
: `Konto ${user.email} darf nicht mehr am Audit-Protokoll eingreifen.`,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userId: req.user?.userId,
|
||||
userEmail: req.user?.email,
|
||||
endpoint: ctx.endpoint,
|
||||
details: {
|
||||
betroffenesKonto: user.email,
|
||||
aktiviert: aktiviertAuditBetrieb,
|
||||
selbstvergabe: user.id === req.user?.userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await logChange({
|
||||
req, action: 'UPDATE', resourceType: 'User',
|
||||
|
||||
Reference in New Issue
Block a user