Audit log viewer
Outcome
Auditors pull a focused 30-day access history for a specific patient (or any other entity) and export it as CSV without needing SQL access.
Prerequisites
audit.readpermission. Tenant migration 109 grants this toCONFIG_MANAGER,OPERATIONS_ANALYST, andEMR_CLIENT_ADMIN. Grant via RBAC matrix for any custom role that needs the viewer.
Surfaces
| Path | What it shows |
|---|---|
/admin/audit | audit.access_log rows — every PHI / config access. |
| Same page, drawer | Linked rows from audit.legal_hold + audit.document_reference for context. |
Backend: apps/rcm-core/src/modules/audit/ — read-only routes at
/api/v1/audit/access-log, /access-log/:id, /access-log/export,
/legal-holds, /document-references.
30-day patient access pull (canonical workflow)
Open
/admin/audit.Set Entity type =
MEMBER(or whatever entity matters).Set Entity id = the patient UUID. Use the full UUID — the filter is exact-match.
Set From / To to the 30-day window. The filter inputs accept
YYYY-MM-DDTHH:mmand we tag UTC on the way out.Page through results; click any row to open the detail drawer with the actor, source IP, request id, and the tamper-evidence chain hashes (
prev_log_hash/log_hash).Click Export CSV. The endpoint refuses requests without a bounded date range or with a window wider than 90 days — narrow the range and retry.
The CSV header carries the same column set as the table plus the hash-chain fields, suitable for forensic review.
Legal-hold awareness
Rows whose accessed entity matches an active hold in audit.legal_hold
carry a red Hold badge inline. The detail drawer also lists the hold
in a banner above the metadata grid. The join is computed at request
time — a hold released after the row was written stops surfacing
immediately on the next refresh.
Retention chip
When the accessed entity has matching rows in
audit.document_reference (matched by related_entity_type +
related_entity_id), the drawer renders a retention chip per document:
| Class | Range |
|---|---|
Short | ≤ 3 yr |
Medium | 4 – 10 yr |
Long | > 10 yr or permanent |
Unknown retention strings render the raw class as a neutral chip so future retention classes don't break the UI.
Pitfalls
- PHI access-log viewer is per-tenant only. Cross-tenant platform
events live in
identity.tenant_audit(master DB). They surface in the platform admin viewer (see Platform impersonation + audit). /admin/audit-logis a different surface (UI-18 RBAC change audit). It is gated onconfig.readand only emits role / role_permission / user_role events. Do not redirect operators to it for HIPAA pulls — they will miss every other entity type.- CSV export uses keyset pagination over
(accessed_at, access_log_id)and streams in 500-row batches — a 90-day export of a busy tenant returns without buffering the full set. Larger windows are rejected at the route layer. - Hash chain is read-only. Verifying the chain is a separate back-office job (not built yet); the drawer just displays the fields so an investigator can copy them into the verifier when it ships.
Validation
| Check | Expected |
|---|---|
| 30-day patient pull | Returns rows + hash fields |
| CSV export ≤ 90 days | Streams successfully |
| Legal-hold rows | Display the badge |
| Retention chip on documents | Renders the right class |
Cross-references
- RBAC matrix editor for granting
audit.read. - Platform impersonation + audit
for
identity.tenant_audit(cross-tenant events). - PHI key rotation — search audit log around suspected key compromise windows.