audit:export gatete nichts - Export hing an audit:read

Bei der Gegenprobe zur neuen Rolle "Gegenbuch" gefunden: Ein Konto mit
ausschliesslich audit:read bekam auf GET /audit-logs/export eine 200. Die
Berechtigung audit:export stand im Katalog und in der Rollenverwaltung -
und wurde nirgends geprueft.

Der Unterschied ist nicht kosmetisch. Blaettern zeigt 50 Zeilen; der
Export liefert in einem Zug das gesamte Protokoll inklusive changesBefore
und changesAfter, also der vollstaendigen Vorher/Nachher-Datensaetze,
dazu resourceLabel mit Klartextnamen, IP-Adressen und User-Agents. Live
nachgewiesen auf Staging: 43 Eintraege mit gefuellter resourceLabel
allein fuer resourceType=Customer.

Damit konnte ausgerechnet das Dienstkonto des Gegenbuchs Personendaten
exportieren - das Konto, dessen Passwort im Klartext in der .env auf der
Notar-Maschine liegt, und dem README und Rollenname "nur Pruefwerte
lesen" zusichern.

/audit-logs/export verlangt jetzt audit:export. Betroffen ist genau eine
Rolle: Gegenbuch, und zwar gewollt. Die DSGVO-Rolle traegt audit:*
vollstaendig und behaelt den Export.

In der Oberflaeche erscheinen JSON- und CSV-Knopf nur noch mit
audit:export - sonst stuenden dort Knoepfe, die zuverlaessig 403 liefern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 12:56:53 +02:00
co-authored by Claude Opus 5
parent d2460fa7c0
commit 23505afc05
4 changed files with 63 additions and 14 deletions
+11 -1
View File
@@ -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);
+24
View File
@@ -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
@@ -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<AuditLogSearchParams>({
page: 1,
@@ -393,14 +398,18 @@ export default function AuditLogs() {
<RefreshCw className="w-4 h-4 mr-2" />
Aktualisieren
</Button>
{hasPermission('audit:export') && (
<Button variant="secondary" onClick={() => handleExport('json')}>
<Download className="w-4 h-4 mr-2" />
JSON
</Button>
)}
{hasPermission('audit:export') && (
<Button variant="secondary" onClick={() => handleExport('csv')}>
<Download className="w-4 h-4 mr-2" />
CSV
</Button>
)}
</div>
</div>
</Card>
+11 -5
View File
@@ -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://<crm>/api/audit-logs/checkpoint \
-H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://<crm>/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://<crm>/api/audit-logs/checkpoint -H "$B"
curl -s -o /dev/null -w 'export %{http_code}\n' https://<crm>/api/audit-logs/export -H "$B"
curl -s -o /dev/null -w 'seal-backlog %{http_code}\n' -X POST https://<crm>/api/audit-logs/seal-backlog -H "$B" -H 'Content-Type: application/json' -d '{}'
curl -s -o /dev/null -w 'kunden %{http_code}\n' https://<crm>/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