Investigate audit access along the reader and subject axes

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 axisreaderTenantId, readerUsername, ipAddress: who performed the read.
  • Subject axissubjectTenantId, 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.

Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…
Query Params
date-time
required

Window start, inclusive (ISO-8601). Mandatory.

date-time
required

Window end, inclusive (ISO-8601). Mandatory.

string

READER axis: the ACCOUNTABLE reader - the operating human on a row written inside an impersonation session, and the login identity on every other row. It is NOT a match on readerUsername as RETURNED: on an impersonated row that field names the account the read ran as, and filtering by it would merge two operators sharing one account into a reader who performed neither read

uuid

READER axis: the reading principal's own tenant

string

READER axis: the claimed client address

string

SUBJECT axis: a tenant UUID, or the literal NULL for reads that addressed no single tenant. Absent means no subject predicate.

string

SUBJECT axis: a NORMALIZED endpoint, exactly as /stats/top-endpoints publishes it. NOT the form the endpoint FIELD returns, which is the raw stored URI - see the operation description

string

The domain outcome — SUCCESS or FAILURE, never an HTTP status. Any other value is refused, never served as an empty page

int32

Inclusive lower bound on the rows the read delivered

int32
Defaults to 0

Page number (0-based). JSON only — see the CSV note above.

int32
Defaults to 20

Page size. JSON only — see the CSV note above.

Headers
string
enum
Defaults to application/problem+json

Generated from available response content types

Allowed:
Responses

Language
Credentials
Bearer
JWT
URL
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
text/csv