Returns meta-audit entries — who read the audit trail, when, from where, whose trail they read, and what came back. Platform admins only.
The filters are grouped on two axes and every parameter names the axis it belongs to, because the two answer different questions and conflating them is the defect this operation replaces:
- Reader axis —
readerTenantId,readerUsername,ipAddress: who performed the read. - Subject axis —
subjectTenantId,resource: whose trail was read, and through which endpoint.
subjectTenantId has THREE states. Absent places no predicate on the subject axis; a tenant UUID selects that tenant's trail; the literal NULL selects reads that addressed no single tenant, which is how a cross-tenant read is enumerated and which an absent-means-any model cannot express at all.
from and to are MANDATORY and the window is bounded. audit_access_log is not partitioned, so an unbounded read is a full table scan.
resource takes a NORMALIZED endpoint exactly as /stats/top-endpoints publishes it, so a ranking click carries the value it displayed. Values outside that published set are refused with 400: the braces in a normalized path are wildcards, so the accepted values are an allowlist rather than a pattern the caller composes.
The resource FILTER and the endpoint FIELD are deliberately not the same form, and reading one as the other is the mistake this operation must not invite. The filter is NORMALIZED (/api/v3/audit/tenants/{id}/crypto/timeline) because it names a ROUTE. The endpoint returned on every row is the RAW request URI the WORM row stores, with real identifier segments (/api/v3/audit/tenants/8d3b6c15-.../crypto/timeline), because it names ONE request and is SIGNED: rewriting it into the normalized form would invalidate the row's HMAC. The query reconciles the two by expanding each placeholder into a bounded wildcard, so neither value is rewritten.
An oversized size is REFUSED, not silently reduced — a clamped page answers 200 with a result the caller did not ask for.
The period export
Send Accept: text/csv and this same operation streams the period's whole result set as CSV instead of a page of JSON. Same filters, same fourteen columns, same order, same redaction — the export and the screen are one contract, so a column can never exist in one and not the other. A wildcard Accept is not a request for CSV: it selects the JSON page, because a client that expressed no preference must not be handed a bulk export of the whole period.
page and size do NOT apply to the export and are refused with 400 if sent — the export serves the period, and silently dropping a page bound would tell a caller they received twenty rows when they received ten thousand. The export is bounded by the deployment's audit.export.max-records.
Cells beginning =, +, -, @, TAB or CR are prefix-quoted, so a caller-authored endpoint or user agent cannot become a spreadsheet formula. The export writes exactly the fourteen wire fields: no signature, hash, sequence number or key version appears in it, for the same structural reason they appear in no JSON body.
Two identities on an impersonated read
readerUsername and readerUserId name the account the read RAN AS. When it ran inside an impersonation session, impersonatorUsername and impersonatorUserId name the operating human — and it is that human who is ACCOUNTABLE for the read. Both are absent on an ordinary read, and their absence is served explicitly rather than by omitting the keys, so "this read was not impersonated" and "this surface does not report impersonation" are never the same observation.
readerUsername as a FILTER selects the accountable reader — the operator on an impersonated row, the account on every other one. It is the one filter on this operation that is not a plain column match, and it is what makes a signal contributor's link land on exactly the rows that contributor was counted for. Filtering by the impersonated account instead would merge two operators who share one account into a single reader who performed neither read.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||