Skip to main content

Audit entries

Every create, update and delete across the services writes an entry, as does every token mint. The audit API is how they are read.

Entry types

entryTypeWritten byOrganization
OrganizationAccount APICaller's
UserAccount APICaller's
GroupAccount APICaller's
UserApiKeyAccount APICaller's
TagGroupLibrary set APICaller's
TagLibrary set APICaller's
VariableSetLibrary set APICaller's
VariableLibrary set APICaller's
LibraryImportLibrary set APICaller's
AuthenticationAuth UINone — written before a JWT exists
JWTIdentity APINone or the caller's, depending on when it is written
JWTRefreshIdentity APICaller's
JWKIdentity APINone

Actions

action
CreateA record was created, or a token minted
UpdateA record was changed
DeleteA record was removed
RefreshA session token was renewed
SignA signing operation
MergeA merging library import
ReplaceA replacing library import
LinkIdentityA second provider identity was attached to an existing account
LogoutA session ended
DeniedA login was refused
ProvisionDeniedNo account existed and no account pattern permitted creating one
ApiKeyDeniedAn API key named nobody, or had been revoked or had expired

Before and after state

beforeState and afterState are JSON strings, serialised by the writing service.

ActionState
CreateafterState only
UpdateBoth
DeletebeforeState only

Whether they are returned at all depends on the caller's permissions — see global entries below.

Global entries

Entries with an empty organization ID belong to no organization: the logins, logouts and token mints written before a JWT exists. They are only ever the business of somebody holding the global audit permissions.

Organizational entriesGlobal entries
Appear in the listAuditListAuditGlobalList
Before/after state visibleAuditReadAuditGlobalRead

AuditGlobalList widens the list to include global entries alongside the caller's own. Both global permissions are marked global, so handing them out needs PermissionsAllGlobal.

Because they are two separate axes, a single page can list an entry and withhold what it changed. That is by design: an entry appearing at all is often the useful fact, and the state is the sensitive part.

Unauthenticated writes

POST / on the audit API is the only endpoint in the product that accepts an unauthenticated write, because Authentication, JWT and JWK entries are written before a token exists.

Without a JWT, the entryType must appear in the service's Audit:AuthNotRequired allow-list, which ships as:

["FailedLogin", "Authentication", "JWT", "JWK"]

Anything else is 401. See Configuration.

Versions

An entry written with an ID that already exists becomes another version of it. That is how the log records successive changes to one entity: the entries share an ID and differ by version.

DELETE /{id} removes every version, and answers with what it removed.

Import is one entry, not many

A library import writes a single entry with the action Merge or Replace, the organization as the entity, and the counts as the after state.

Two reasons. The audit API is reached over HTTP, so a file with hundreds of variables in it would spend longer being audited than imported. And what happened was one act, which the counts describe in full.

The notes are deliberately left out of the entry. Re-importing an unchanged file is the ordinary repeat case and produces a note per entity left alone — worth telling the caller, not worth shipping to another service as a record of nothing having changed.

Reading the log

curl -X POST "$AUDIT_API/filter?take=25&sortField=LoggedAt&sortDescending=true" \
-H "x-tagshape-auth: $JWT" \
-H "Content-Type: application/json" \
-d '[{"field":"EntryType","operatorType":"eq","value":"Variable"}]'

Names for the loggedBy IDs come from GET /users/names on the account API, which asks for AuditList rather than UserList for precisely this reason — whoever may read the log may see who wrote each entry.