diff --git a/backend/src/routes/auditLog.routes.ts b/backend/src/routes/auditLog.routes.ts index 89905533..991bb0b4 100644 --- a/backend/src/routes/auditLog.routes.ts +++ b/backend/src/routes/auditLog.routes.ts @@ -16,7 +16,17 @@ router.use(authenticate); router.get('/', requirePermission('audit:read'), auditLogController.getAuditLogs); // Audit-Logs exportieren -router.get('/export', requirePermission('audit:read'), auditLogController.exportAuditLogs); +// +// Verlangt `audit:export`, nicht `audit:read`. Die Berechtigung stand im +// Katalog und in der Rollenverwaltung, gatete aber NICHTS - jeder Leser +// konnte das vollstaendige Protokoll in einem Zug herausziehen. +// +// Das ist etwas anderes als Blaettern: Der Export liefert `changesBefore` und +// `changesAfter`, also die vollstaendigen Vorher/Nachher-Datensaetze, dazu +// `resourceLabel` mit Klartextnamen, IP-Adressen und User-Agents. Aufgefallen +// am Dienstkonto des Gegenbuchs: Es soll ausschliesslich Pruefwerte lesen - +// und konnte Personendaten exportieren. +router.get('/export', requirePermission('audit:export'), auditLogController.exportAuditLogs); // Retention-Policies router.get('/retention-policies', requirePermission('audit:admin'), auditLogController.getRetentionPolicies); diff --git a/docs/todo.md b/docs/todo.md index fb873384..32f3d02f 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,30 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔒 `audit:export` gatete nichts – Export hing an `audit:read`** (2026-08-26) + - **Bei der Gegenprobe zur neuen Rolle `Gegenbuch` gefunden:** Ein Konto mit + ausschließlich `audit:read` bekam auf `GET /audit-logs/export` **200**. + Die Berechtigung `audit:export` stand im Katalog und in der + Rollenverwaltung – und wurde **nirgends** geprüft. + - **Der Unterschied ist nicht kosmetisch.** Blättern zeigt 50 Zeilen; der + Export liefert in einem Zug das gesamte Protokoll inklusive + `changesBefore`/`changesAfter` – also der vollständigen Vorher/Nachher- + Datensätze – dazu `resourceLabel` mit Klartextnamen, IP-Adressen und + User-Agents. Live nachgewiesen: 43 Einträge mit gefüllter + `resourceLabel` allein für `resourceType=Customer`. + - Damit konnte ausgerechnet das Dienstkonto des Gegenbuchs, dessen Passwort + im Klartext in der `.env` auf der Notar-Maschine liegt, Personendaten + exportieren – während README und Rollenname „nur Prüfwerte lesen" + versprachen. + - `/audit-logs/export` verlangt jetzt `audit:export`. Betroffen ist genau + eine Rolle: `Gegenbuch` (gewollt). Die DSGVO-Rolle hat `audit:*` + vollständig und behält den Export. + - Oberfläche: JSON- und CSV-Knopf werden nur noch mit `audit:export` + angezeigt – sonst stünden dort Knöpfe, die zuverlässig 403 liefern. + - Dateien: `backend/src/routes/auditLog.routes.ts`, + `frontend/src/pages/settings/AuditLogs.tsx`, + `tools/audit-notary/README.md` + - [x] **🔑 Rolle „Gegenbuch": Leserecht aufs Audit-Protokoll ohne `audit:admin`** (2026-08-26) - **Beim Selbst-Nachprüfen eines Deploys aufgefallen:** Das Gegenbuch- Dienstkonto auf Staging meldete beim Login diff --git a/frontend/src/pages/settings/AuditLogs.tsx b/frontend/src/pages/settings/AuditLogs.tsx index 1a1073e7..1d4cdbd8 100644 --- a/frontend/src/pages/settings/AuditLogs.tsx +++ b/frontend/src/pages/settings/AuditLogs.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; import { auditLogApi, AuditLogSearchParams, authApi } from '../../services/api'; import type { AuditLog, AuditAction, AuditSensitivity } from '../../types'; import Card from '../../components/ui/Card'; @@ -279,6 +280,10 @@ function DetailModal({ log, onClose }: DetailModalProps) { export default function AuditLogs() { const navigate = useNavigate(); + // Export haengt an `audit:export`, Lesen an `audit:read` – ein Konto darf + // blaettern duerfen, ohne das gesamte Protokoll herausziehen zu koennen. + // Ohne diese Abfrage stuenden hier Knoepfe, die zuverlaessig 403 liefern. + const { hasPermission } = useAuth(); const [page, setPage] = useState(1); const [filters, setFilters] = useState({ page: 1, @@ -393,14 +398,18 @@ export default function AuditLogs() { Aktualisieren - - + {hasPermission('audit:export') && ( + + )} + {hasPermission('audit:export') && ( + + )} diff --git a/tools/audit-notary/README.md b/tools/audit-notary/README.md index b8f1ad3c..b984d4ef 100644 --- a/tools/audit-notary/README.md +++ b/tools/audit-notary/README.md @@ -206,15 +206,21 @@ Mit `audit:read` allein kann dieses Konto **nur Prüfwerte lesen** – keine Kundendaten, keine Verträge, nichts ändern und nichts versiegeln. Selbst wenn die Zugangsdaten abhandenkommen, ist damit nichts anzufangen. -Gegenprobe nach dem Einrichten – die erste Zeile muss 200 geben, die zweite 403: +Gegenprobe nach dem Einrichten – **200, dann dreimal 403**: ```bash -curl -s -o /dev/null -w '%{http_code}\n' https:///api/audit-logs/checkpoint \ - -H "Authorization: Bearer $TOKEN" -curl -s -o /dev/null -w '%{http_code}\n' -X POST https:///api/audit-logs/seal-backlog \ - -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' +B="Authorization: Bearer $TOKEN" +curl -s -o /dev/null -w 'checkpoint %{http_code}\n' https:///api/audit-logs/checkpoint -H "$B" +curl -s -o /dev/null -w 'export %{http_code}\n' https:///api/audit-logs/export -H "$B" +curl -s -o /dev/null -w 'seal-backlog %{http_code}\n' -X POST https:///api/audit-logs/seal-backlog -H "$B" -H 'Content-Type: application/json' -d '{}' +curl -s -o /dev/null -w 'kunden %{http_code}\n' https:///api/customers -H "$B" ``` +Der Export gehört ausdrücklich dazu: Er liefert `changesBefore`/`changesAfter`, +also die vollständigen Vorher/Nachher-Datensätze samt Klartextnamen. Prüfwerte +lesen und das Protokoll herausziehen sind zwei verschiedene Dinge – deshalb +hängt der Export an `audit:export`, das die Rolle `Gegenbuch` nicht hat. + Für Produktion und Test jeweils ein eigenes Konto in der jeweiligen Instanz. > Jede Anmeldung erscheint im Audit-Log der jeweiligen Instanz. Das ist so