Wie wird auditiert?
Kurzantwort: Über Hibernate Envers. Ein Modell mit @Audited bekommt
eine Audit-Tabelle; jede Änderung erzeugt eine Revision. Die Revisionsdaten
stehen in der Tabelle revinfo und tragen neben Nummer und Zeitstempel auch
Benutzer-ID, Benutzername, IP-Adresse und User-Agent aus dem
RequestContext. Jedes Persistenzziel führt seine eigene revinfo. Ein
Rollback schreibt den alten Stand als neue Revision – die Historie wird nie
umgeschrieben.
Was auditiert wird
Abschnitt betitelt „Was auditiert wird“Ein Modell ist genau dann auditiert, wenn es selbst @Audited trägt. Die
Annotation auf einer Basisklasse macht kein Modell auditiert. Ein Modell ohne
@Audited bekommt keine Audit-Tabelle, erzeugt bei Änderungen keine Revision
und wird von Envers beim Lesen seiner Historie abgelehnt
(model-not-audited, HTTP 500).
Zusätzlich tragen die Modell-Basisklassen selbst @Audited – sonst würde Envers
die Eigenschaften einer @MappedSuperclass gar nicht auditieren und
_createdOn, _updatedOn und _userId fehlten in jeder Revision.
Im Modell wird das über @History(rollback = true|false) gesteuert; das
audited-Flag landet in MetaClassInfo.isAudited().
Dasselbe Flag steuert auch die Dateiversionierung – siehe Dateien und Storage.
Die Revisionsdaten
Abschnitt betitelt „Die Revisionsdaten“AuditRevisionEntity, Tabelle revinfo:
| Spalte | Inhalt |
|---|---|
id | Revisionsnummer (@RevisionNumber, IDENTITY) |
timestamp | Zeitstempel (@RevisionTimestamp, Millisekunden) |
user_id | RequestContext.userId |
username | RequestContext.userName |
ip_address | RequestContext.userIP (45 Zeichen, IPv6-tauglich) |
user_agent | RequestContext.userClientAgent (512 Zeichen) |
AuditRevisionListener füllt die vier Kontextfelder. Fehlt der
Request-Kontext, bleiben sie leer, ohne dass die Revision scheitert – ein
Hintergrundlauf ohne Benutzer schreibt trotzdem Historie.
flowchart LR
RC["RequestContext<br/>userId · userName · IP · User-Agent"] --> L[AuditRevisionListener]
W["Schreiboperation"] --> E[Hibernate Envers]
E -->|neue Revision| L
L --> RI[("revinfo")]
E --> AT[("<modell>_AUD")]
Ein Auditlog pro Datenbank
Abschnitt betitelt „Ein Auditlog pro Datenbank“EntityClassFilterService registriert AuditRevisionEntity in jeder
Persistenzeinheit – System- wie Mandantendatenbank. Jedes Ziel führt seine
eigene revinfo.
Ohne diese Registrierung würde Envers still seine eigene
DefaultRevisionEntity verwenden – der Audit-Trail verlöre seinen Urheber, ohne
dass es auffiele.
Historie lesen
Abschnitt betitelt „Historie lesen“POST /api/rest/<pfad>/{id}/history{ "response": ["id", "name", "status"], "parameter": { "page": 0, "limit": 20 }}Antwort:
{ "data": [ { "revision": { "id": "…", "name": "Muster GmbH", "status": "ACTIVE" }, "revisionMeta": { "ref": 17, "ts": 1785574268973, "ip": "10.0.0.5", "useragent": "Mozilla/5.0 …", "username": "d.mertins" }, "revisionType": "MOD" } ], "meta": { "totalCount": 3 }}| Eigenschaft | Verhalten |
|---|---|
| Reihenfolge | absteigend nach Revisionsnummer (neueste zuerst) |
| Pagination | über parameter.page / parameter.limit |
| Felder | nur die im Response Request angeforderten – Referenzen und Listen werden rekursiv mit eigenem Unter-Request kopiert |
| Recht | historyRole – nur lesend, deshalb keine Schreibrolle nötig |
| Änderungsart | revisionType: ADD, MOD, DEL |
Eine Löschung schneidet die Spur nicht ab: Die vollständige Revisionsliste bleibt lesbar, die Löschrevision nennt Urheber und Zeitpunkt, und der letzte Stand vor der Löschung steht in der vorhergehenden Revision.
Rollback
Abschnitt betitelt „Rollback“POST /api/rest/<pfad>/{id}/rollback/{revision}Ablauf im System-Layer:
sequenceDiagram
participant C as Client
participant S as System-Layer
participant P as Persistenz
participant F as Storage
C->>S: rollback(id, revision, ReadPayload)
S->>S: assertVisibleForWrite(id)
S->>S: rollbackAccessAllowedByClass – Rollback-Rolle
S->>S: aktuelle fileVersion merken
S->>S: ROLLBACK-Hooks (before)
S->>P: historyRollback(id, revision)
P->>P: Zustand der Revision zurückschreiben<br/>als NEUE Revision
S->>F: rollbackFileContent – Inhalt zurückkopieren
S->>S: ROLLBACK-Hooks (after), flush
S->>S: readObject(id, request)
S-->>C: wiederhergestellter Stand
Regeln:
- Wiederhergestellt werden die eigenen Zustandsdaten: skalare Attribute und eigene To-One-Referenzen.
- Sammlungen werden nicht wiederhergestellt.
- Ein gelöschtes Objekt wird nicht wiederbelebt.
- Eine Revision, die nicht zu diesem Objekt gehört, wird abgelehnt.
- Der wiederhergestellte Zustand wird als neue Revision geschrieben – die Historie bleibt vollständig und lückenlos.
- Zusätzlich zur
historyRoleist dierollbackRoleerforderlich, weil der Rollback schreibt.
Dateien beim Rollback
Abschnitt betitelt „Dateien beim Rollback“Vor dem Rollback merkt sich der System-Layer die aktuelle fileVersion. Danach
trägt der Datensatz die Version der Zielrevision; der Storage bekommt die alte
Kennung übergeben, bewahrt den aktuellen Inhalt darunter auf und kopiert die
Zielversion zurück. Trägt die Revision keine fileVersion (weil sie älter als
das Feld ist), bleibt der Inhalt unangetastet – ihn zu ersetzen wäre geraten.
Fehlerabbildung
Abschnitt betitelt „Fehlerabbildung“| Envers-Fehler | CDMS-Fehler |
|---|---|
NotAuditedException | SystemConfigurationException (model-not-audited) |
RevisionDoesNotExistException | EntityNotFoundException |
Envers-Typen verlassen das Persistenzmodul nicht.
Datenschutz
Abschnitt betitelt „Datenschutz“ip_address und user_agent sind personenbezogene Daten. Ihre Speicherung ist
projektseitig gegen die jeweiligen Lösch- und Aufbewahrungsanforderungen zu
prüfen.
Auditing ≠ Systemfelder
Abschnitt betitelt „Auditing ≠ Systemfelder“Nicht verwechseln:
| Mechanismus | was er liefert |
|---|---|
_createdOn / _updatedOn | Zeitpunkte am aktuellen Datensatz |
_userId | Besitzer eines benutzereigenen Modells |
| Envers-Revisionen | vollständige Änderungshistorie mit Urheber, Zeit, IP und Client |