diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index 504dca78..1d198f3f 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -99,7 +99,7 @@ export async function createUser(req: Request, res: Response): Promise { } } -export async function updateUser(req: Request, res: Response): Promise { +export async function updateUser(req: AuthRequest, res: Response): Promise { try { const userId = parseInt(req.params.id); // `permissions` und `password` darf der generische Update nicht @@ -164,6 +164,41 @@ export async function updateUser(req: Request, res: Response): Promise { } : null; + // 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 + // Routine erscheinen. Es bekommt deshalb dieselbe Behandlung wie das + // Absenken einer Aufbewahrungsfrist – Bestaetigung, laute Protokollierung, + // und kein Selbstbedienen. + const setztDienstkonto = + typeof (data as any).isServiceAccount === 'boolean' && + (data as any).isServiceAccount !== before?.isServiceAccount; + const aktiviertDienstkonto = setztDienstkonto && (data as any).isServiceAccount === true; + + if (setztDienstkonto && req.user?.userId === userId) { + res.status(403).json({ + success: false, + error: + 'Das Dienstkonto-Kennzeichen lässt sich nicht am eigenen Konto setzen oder entfernen. ' + + 'Es stuft Anmeldungen dieses Kontos als Routine ein – wer das für sich selbst täte, ' + + 'könnte die eigenen Anmeldungen unauffällig machen. Bitte von einem anderen ' + + 'Administrator vornehmen lassen.', + } as ApiResponse); + return; + } + + if (aktiviertDienstkonto && 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.', + } as ApiResponse); + return; + } + const user = await userService.updateUser(userId, data as any); if (user) { // Audit: Geänderte Felder ermitteln und loggen @@ -172,6 +207,7 @@ export async function updateUser(req: Request, res: Response): Promise { const fieldLabels: Record = { email: 'E-Mail', firstName: 'Vorname', lastName: 'Nachname', isActive: 'Aktiv', hasGdprAccess: 'DSGVO-Zugriff', hasDeveloperAccess: 'Entwicklerzugriff', + isServiceAccount: 'Dienstkonto (Anmeldungen als Routine)', }; for (const [key, newVal] of Object.entries(data)) { if (['id', 'createdAt', 'updatedAt'].includes(key)) continue; @@ -191,7 +227,16 @@ export async function updateUser(req: Request, res: Response): Promise { await logChange({ req, action: 'UPDATE', resourceType: 'User', resourceId: user.id.toString(), - label: changeList ? `Benutzer ${user.firstName} ${user.lastName} aktualisiert: ${changeList}` : `Benutzer ${user.firstName} ${user.lastName} aktualisiert`, + label: setztDienstkonto + ? `Dienstkonto-Kennzeichen ${aktiviertDienstkonto ? 'GESETZT' : 'entfernt'} für ` + + `${user.email} – Anmeldungen werden künftig ` + + `${aktiviertDienstkonto ? 'als Routine' : 'wieder als kritisch'} geführt` + : changeList + ? `Benutzer ${user.firstName} ${user.lastName} aktualisiert: ${changeList}` + : `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, details: Object.keys(changes).length > 0 ? changes : undefined, }); } else { diff --git a/docs/todo.md b/docs/todo.md index 5b7767f3..833f0327 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,30 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔒 Dienstkonto-Kennzeichen gegatet, laut protokolliert, kein Selbstbedienen (Pentest R184)** (2026-08-24) + - Der Pentester hat sofort erkannt, was das Scharfschalten des Feldes + bedeutet: Ein Attribut, das die **Alarmstufe senkt**, war ueber den + normalen Benutzer-Update-Pfad setzbar – dieselbe Klasse wie R183-01, + nur neu gebaut. Wer sein eigenes Konto so markiert, laesst die eigenen + auffaelligen Anmeldungen als Routine erscheinen. Das ist die Waesche. + - Ist-Zustand vor dem Fix, alle drei seiner Sorgen bestaetigt: kein Gate, + Protokollierung nur als MEDIUM (Standardstufe fuer `User`), und jeder mit + `users:update` konnte es auf jedes Konto setzen – auch auf das eigene. + - Fix, analog zur Retention-Absenkung: + * **Bestaetigung** `{"confirm":"SERVICE_ACCOUNT"}` beim Aktivieren, mit + Klartext, was das bedeutet. + * **CRITICAL** statt MEDIUM, mit eigenem Label („Dienstkonto-Kennzeichen + GESETZT fuer … – Anmeldungen werden kuenftig als Routine gefuehrt“) sowie + Wer/Vorher/Nachher. + * **Kein Selbstbedienen**: am eigenen Konto ist das Kennzeichen weder + setzbar noch entfernbar (403 mit Begruendung) – muss ein anderer + Administrator vornehmen. + - Frontend sendet die Bestaetigung mit; der Haken im Formular IST die + Bestaetigung, der Betreiber merkt nichts davon. + - Verifiziert ueber den echten Controller: ohne Bestaetigung 400, mit 200, + am eigenen Konto 403, Protokolleintrag CRITICAL mit sprechendem Label. + `npm run build` (inkl. `tsc`) und Backend-`tsc` gruen. + - [x] **🖱️ Dienstkonto-Kennzeichen in der Benutzerverwaltung** (2026-08-24) - Nachgezogen: Das Feld `isServiceAccount` lag zwar in der Datenbank, war aber **nirgends setzbar** – weder im Formular noch ueber die API. Der diff --git a/frontend/src/pages/users/UserList.tsx b/frontend/src/pages/users/UserList.tsx index aa2202b0..3a1a3a64 100644 --- a/frontend/src/pages/users/UserList.tsx +++ b/frontend/src/pages/users/UserList.tsx @@ -328,6 +328,10 @@ function UserModal({ hasDeveloperAccess: formData.hasDeveloperAccess, hasGdprAccess: formData.hasGdprAccess, isServiceAccount: formData.isServiceAccount, + // Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos. + // Der Server verlangt dafür eine ausdrückliche Bestätigung; der Haken + // im Formular IST diese Bestätigung. + ...(formData.isServiceAccount ? { confirm: 'SERVICE_ACCOUNT' } : {}), whatsappNumber: formData.whatsappNumber || undefined, telegramUsername: formData.telegramUsername || undefined, signalNumber: formData.signalNumber || undefined, @@ -364,6 +368,10 @@ function UserModal({ hasDeveloperAccess: formData.hasDeveloperAccess, hasGdprAccess: formData.hasGdprAccess, isServiceAccount: formData.isServiceAccount, + // Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos. + // Der Server verlangt dafür eine ausdrückliche Bestätigung; der Haken + // im Formular IST diese Bestätigung. + ...(formData.isServiceAccount ? { confirm: 'SERVICE_ACCOUNT' } : {}), whatsappNumber: formData.whatsappNumber || undefined, telegramUsername: formData.telegramUsername || undefined, signalNumber: formData.signalNumber || undefined, diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 48f63957..16ff6326 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1548,11 +1548,11 @@ export const userApi = { const res = await api.get>(`/users/${id}`); return res.data; }, - create: async (data: { email: string; password: string; firstName: string; lastName: string; roleIds: number[]; customerId?: number; hasDeveloperAccess?: boolean; hasGdprAccess?: boolean; isServiceAccount?: boolean; whatsappNumber?: string; telegramUsername?: string; signalNumber?: string }) => { + create: async (data: { email: string; password: string; firstName: string; lastName: string; roleIds: number[]; customerId?: number; hasDeveloperAccess?: boolean; hasGdprAccess?: boolean; isServiceAccount?: boolean; confirm?: string; whatsappNumber?: string; telegramUsername?: string; signalNumber?: string }) => { const res = await api.post>('/users', data); return res.data; }, - update: async (id: number, data: Partial & { password?: string; roleIds?: number[] }) => { + update: async (id: number, data: Partial & { password?: string; roleIds?: number[]; isServiceAccount?: boolean; confirm?: string }) => { const res = await api.put>(`/users/${id}`, data); return res.data; },