Skip to main content

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

TablePurpose
audit_eventCentral append-only trail. Relational event_category (AUDIT/ACCESS/SECURITY), event_type (AuditEventType), action (AuditEventAction), outcome, initiator_type, tenant/facility, PHI-safe summaries only.
access_logInteractive auth events; event_type is AccessEventType.
security_eventSecurity-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). AuditEventRecordRequestDto deliberately omits tenant, actor, timestamp and initiator; forging those would make a trusted trail worse than none.
  • event_type and action are 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 on security_event). Jackson rejects an unrecognised value with 400; migration audit/003 normalises 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_category is a closed enum (TERM-008E). Same failure mode as TERM-008C — free text invented reasons filters could never find. Jackson rejects unknowns with 400; migration audit/004 normalises 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. CoreAuditRecorder swallows 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.

  • 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