Audit — How it works
Overview
The audit engine owns the central, PHI-safe trail of who did what: audit_event for general /
clinical / access / security summaries, plus structurally separate access_log and security_event
tables for login/logout and lockout/break-glass/rate-limit rows. Core engines append in-process via
CoreAuditRecorder / AuditEventRecorder; domain modules reach the same table over Feign
(AuditCommandClient) with attribution derived from the session — never from the request body.
Data model & ownership
| Table | Purpose |
|---|---|
audit_event | Central append-only trail. Relational event_category (AUDIT/ACCESS/SECURITY), event_type (AuditEventType), action (AuditEventAction), outcome, initiator_type, tenant/facility, PHI-safe summaries only. |
access_log | Interactive auth events; event_type is AccessEventType. |
security_event | Security-relevant events; event_type is SecurityEventType; reason_category is SecurityEventReasonCategory (TERM-008E). |
Migrations live under core/src/main/resources/db/changelog/audit/.
Key rules & invariants
- Summaries are PHI-safe by contract. Display and resource summaries must never carry a patient name, identifier value, or other PHI — a leaky audit trail defeats the access controls on the record.
- Attribution is server-derived on the write endpoint (GAP-002).
AuditEventRecordRequestDtodeliberately omits tenant, actor, timestamp and initiator; forging those would make a trusted trail worse than none. event_typeandactionare closed enums (TERM-008C). Sibling tables typed their discriminators in M10-003; these two columns stayed free text and accepted any spelling (including values that belong onsecurity_event). Jackson rejects an unrecognised value with 400; migrationaudit/003normalises case/whitespace then CHECK-constrains, and HALTs on dirty data rather than inventing a type. There is no silent default that strips or invents meaning.security_event.reason_categoryis a closed enum (TERM-008E). Same failure mode as TERM-008C — free text invented reasons filters could never find. Jackson rejects unknowns with 400; migrationaudit/004normalises then CHECK-constrains and HALTs on dirty data. The column stays nullable (absence is legitimate). Values:ACCOUNT_LOCKED,EMERGENCY_ACCESS,AUTH_RATE_LIMIT_IP,AUTH_RATE_LIMIT_IP_USERNAME,SERVICE_NOT_AUTHORIZED,DELEGATED_CONTEXT_REQUIRED.- In-process recording is best-effort.
CoreAuditRecorderswallows failures so an unreachable audit sink never fails a clinical write or turns an access denial into a 500. - Soft-delete columns exist via
AuditTrail, but the engine exposes no void/update path for these rows — the trail is append-only from the API's point of view.
API
See the API Reference. Endpoint groups under /api/v1/audit:
POST /audit-events— record (domain modules; attribution from session)GET /audit-events— paginated search (eventCategory,eventType,outcome, …)GET /access-logs,GET /security-events— sibling search surfaces
Configuration & feature flags
None specific to the audit engine. Recording always runs when a call site invokes the recorder.
Related features
- access-control — break-glass and access decisions leave both security events and central audit rows
- identity-access — visibility / consent / grant changes project into identity
audit and often a central
audit_event - clinical / form — clinical writes and form submit/void use
CoreAuditRecorder.recordClinicalWrite