Dienstkonto-Flag: Gate in beide Richtungen, richtige Rechte-Domaene (R184)

R184-01: Setzen war gegatet, Entfernen nicht - und das Entfernen ist der
gefaehrlichere Weg. Der Heartbeat-Wachhund fragt isServiceAccount:true ab,
haengt also am Live-Kennzeichen; die Hypothese des Pentesters war richtig,
Un-Flaggen kappt die Wache. Genau das stilllegen-und-auf-Stille-setzen, gegen
das der Tripwire gebaut wurde. Fix: Gate in beide Richtungen mit eigenem
Wortlaut beim Entfernen.

Wichtiger noch: Die Aenderung geht jetzt zusaetzlich in den Alarmkanal
(PERMISSION_CHANGED/CRITICAL), nicht nur ins Audit-Log - eine CRITICAL-Zeile
muss jemand lesen, das war die R183-02-Klasse.

R184-02: Das Kennzeichen hing an users:update, obwohl es ein
Audit-Governance-Eingriff ist - Geschwister von retention-shorten,
seal-backlog, rehash und cleanup, die alle audit:admin verlangen. Heute
deckungsgleich, aber jede kuenftige Rolle mit Benutzer-bearbeiten haette still
Login-Alarme-herunterstufen geerbt. Fix: zusaetzliche Pruefung auf audit:admin.

Verifiziert: Entfernen ohne Bestaetigung jetzt 400 statt 200, ohne audit:admin
403, zwei CRITICAL-Meldungen im Alarmkanal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 13:09:08 +02:00
co-authored by Claude Opus 5
parent b3a9ef6372
commit e504b8be96
3 changed files with 92 additions and 8 deletions
+58 -6
View File
@@ -4,6 +4,7 @@ import prisma from '../lib/prisma.js';
import * as userService from '../services/user.service.js';
import { logChange } from '../services/audit.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';
import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH } from '../utils/passwordGenerator.js';
@@ -175,6 +176,25 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
(data as any).isServiceAccount !== before?.isServiceAccount;
const aktiviertDienstkonto = setztDienstkonto && (data as any).isServiceAccount === true;
// R184-02: Das Kennzeichen ist ein Audit-Governance-Eingriff, kein
// gewoehnliches Benutzerfeld es ist Geschwister von retention-shorten,
// seal-backlog, rehash und cleanup, die alle `audit:admin` verlangen. Es
// haengt aber an der Route mit `users:update`. Heute deckungsgleich, weil
// nur die Administratorrolle beides hat aber jede kuenftige Rolle mit
// „Benutzer bearbeiten“ wuerde still „Login-Alarme herunterstufen“ erben.
// Deshalb hier zusaetzlich auf die Domaene pruefen, in der der Schaden
// liegt.
if (setztDienstkonto && !(req.user?.permissions || []).includes('audit:admin')) {
res.status(403).json({
success: false,
error:
'Das Dienstkonto-Kennzeichen ändert, wie Anmeldungen im Audit-Log bewertet werden. ' +
'Dafür genügt „Benutzer bearbeiten“ nicht es erfordert die Berechtigung ' +
'audit:admin, wie das Verkürzen von Aufbewahrungsfristen auch.',
} as ApiResponse);
return;
}
if (setztDienstkonto && req.user?.userId === userId) {
res.status(403).json({
success: false,
@@ -187,14 +207,23 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
return;
}
if (aktiviertDienstkonto && req.body?.confirm !== 'SERVICE_ACCOUNT') {
// Gate in BEIDE Richtungen (Pentest R184-01). Das Entfernen war ungegatet
// und es ist der gefaehrlichere Weg: Ein un-geflaggtes Konto faellt aus der
// Heartbeat-Wache, weil diese am Live-Kennzeichen haengt. Genau das
// „stilllegen und auf Stille setzen“, gegen das der Wachhund gebaut wurde.
if (setztDienstkonto && req.body?.confirm !== 'SERVICE_ACCOUNT') {
res.status(400).json({
success: false,
error:
`Damit werden künftige Anmeldungen von ${before?.email ?? 'diesem Konto'} im Audit-Log ` +
'als Routine geführt statt als kritisches Ereignis. Das ist für planmäßig arbeitende ' +
'Dienste gedacht (etwa das Gegenbuch) für ein Konto, das ein Mensch benutzt, wäre es ' +
'eine Tarnung. Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.',
error: aktiviertDienstkonto
? `Damit werden künftige Anmeldungen von ${before?.email ?? 'diesem Konto'} im ` +
'Audit-Log als Routine geführt statt als kritisches Ereignis. Das ist für ' +
'planmäßig arbeitende Dienste gedacht (etwa das Gegenbuch) für ein Konto, das ' +
'ein Mensch benutzt, wäre es eine Tarnung. ' +
'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.'
: `Damit fällt ${before?.email ?? 'dieses Konto'} aus der Überwachung heraus: Es wird ` +
'dann nicht mehr gemeldet, wenn es sich nicht mehr anmeldet. Bei einem laufenden ' +
'Dienst wie dem Gegenbuch hieße das, sein Ausfall bliebe unbemerkt. ' +
'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.',
} as ApiResponse);
return;
}
@@ -239,6 +268,29 @@ export async function updateUser(req: AuthRequest, res: Response): Promise<void>
sensitivity: setztDienstkonto ? 'CRITICAL' : undefined,
details: Object.keys(changes).length > 0 ? changes : undefined,
});
// Zusaetzlich in den Alarmkanal (Pentest R184-01). Eine CRITICAL-Zeile
// im Audit-Log muss jemand LESEN das ist die R183-02-Klasse. Das
// Entfernen des Kennzeichens nimmt das Konto aus der Heartbeat-Wache;
// ab dann faellt sein Ausbleiben nicht mehr auf. Diese Aenderung
// gehoert deshalb dorthin, wo automatisch reagiert wird.
if (setztDienstkonto) {
const ctx = contextFromRequest(req);
emitSecurityEvent({
type: 'PERMISSION_CHANGED',
severity: 'CRITICAL',
message: aktiviertDienstkonto
? `Konto ${user.email} als Dienstkonto markiert seine Anmeldungen gelten ab ` +
'jetzt als Routine statt als kritisches Ereignis.'
: `Konto ${user.email} ist KEIN Dienstkonto mehr es fällt damit aus der ` +
'Überwachung heraus: Ein Ausbleiben seiner Anmeldungen wird nicht mehr gemeldet.',
ipAddress: ctx.ipAddress,
userId: req.user?.userId,
userEmail: req.user?.email,
endpoint: ctx.endpoint,
details: { betroffenesKonto: user.email, aktiviert: aktiviertDienstkonto },
});
}
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'User',