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:
@@ -16,7 +16,17 @@ router.use(authenticate);
|
|||||||
router.get('/', requirePermission('audit:read'), auditLogController.getAuditLogs);
|
router.get('/', requirePermission('audit:read'), auditLogController.getAuditLogs);
|
||||||
|
|
||||||
// Audit-Logs exportieren
|
// 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
|
// Retention-Policies
|
||||||
router.get('/retention-policies', requirePermission('audit:admin'), auditLogController.getRetentionPolicies);
|
router.get('/retention-policies', requirePermission('audit:admin'), auditLogController.getRetentionPolicies);
|
||||||
|
|||||||
@@ -97,6 +97,30 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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)
|
- [x] **🔑 Rolle „Gegenbuch": Leserecht aufs Audit-Protokoll ohne `audit:admin`** (2026-08-26)
|
||||||
- **Beim Selbst-Nachprüfen eines Deploys aufgefallen:** Das Gegenbuch-
|
- **Beim Selbst-Nachprüfen eines Deploys aufgefallen:** Das Gegenbuch-
|
||||||
Dienstkonto auf Staging meldete beim Login
|
Dienstkonto auf Staging meldete beim Login
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { auditLogApi, AuditLogSearchParams, authApi } from '../../services/api';
|
import { auditLogApi, AuditLogSearchParams, authApi } from '../../services/api';
|
||||||
import type { AuditLog, AuditAction, AuditSensitivity } from '../../types';
|
import type { AuditLog, AuditAction, AuditSensitivity } from '../../types';
|
||||||
import Card from '../../components/ui/Card';
|
import Card from '../../components/ui/Card';
|
||||||
@@ -279,6 +280,10 @@ function DetailModal({ log, onClose }: DetailModalProps) {
|
|||||||
|
|
||||||
export default function AuditLogs() {
|
export default function AuditLogs() {
|
||||||
const navigate = useNavigate();
|
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 [page, setPage] = useState(1);
|
||||||
const [filters, setFilters] = useState<AuditLogSearchParams>({
|
const [filters, setFilters] = useState<AuditLogSearchParams>({
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -393,14 +398,18 @@ export default function AuditLogs() {
|
|||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
Aktualisieren
|
Aktualisieren
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" onClick={() => handleExport('json')}>
|
{hasPermission('audit:export') && (
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Button variant="secondary" onClick={() => handleExport('json')}>
|
||||||
JSON
|
<Download className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
JSON
|
||||||
<Button variant="secondary" onClick={() => handleExport('csv')}>
|
</Button>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
)}
|
||||||
CSV
|
{hasPermission('audit:export') && (
|
||||||
</Button>
|
<Button variant="secondary" onClick={() => handleExport('csv')}>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
CSV
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -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
|
Kundendaten, keine Verträge, nichts ändern und nichts versiegeln. Selbst wenn
|
||||||
die Zugangsdaten abhandenkommen, ist damit nichts anzufangen.
|
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
|
```bash
|
||||||
curl -s -o /dev/null -w '%{http_code}\n' https://<crm>/api/audit-logs/checkpoint \
|
B="Authorization: Bearer $TOKEN"
|
||||||
-H "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 '%{http_code}\n' -X POST https://<crm>/api/audit-logs/seal-backlog \
|
curl -s -o /dev/null -w 'export %{http_code}\n' https://<crm>/api/audit-logs/export -H "$B"
|
||||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'
|
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.
|
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
|
> Jede Anmeldung erscheint im Audit-Log der jeweiligen Instanz. Das ist so
|
||||||
|
|||||||
Reference in New Issue
Block a user