Skip to content
Menu

Documentation

Audit

The audit log records committed security-sensitive actions: who did what to which resource. An audit row is written in the same unit as the change, so it exists if and only if the change committed.

What you get by default, and what you declare

You write Sandloom does for you
nothing, on a sensitive write (admin, money, security risk) one automatic audit row sandloom.sensitive_write: actor, route template, method, path parameters, risks and their sources, status; never the body (Route policy)
record_audit("tenancy.member.role_changed", actor_id=..., ...) an audit_log row in the current unit, committed with the change
nothing, on a denial a structured deny log and the access_denied_* metric (the request rolls back, so no audit row)
from sandloom.sdk import record_audit

record_audit(
    "tenancy.member.role_changed",
    actor_id=user.user_id,
    resource_type="membership",
    resource_id=str(membership_id),
    data={"from": "member", "to": "admin"},
)

record_audit(action, *, actor_id=None, actor_type="user", resource_type=None, resource_id=None, data=None, required=None, database=None) is synchronous: it queues the row on the current unit (required=None means required). Exact signature: sandloom.sdk.effects.record_audit.

Semantics

  • The row is written to audit_log in the current unit: it commits if and only if the business change commits. A rolled-back request leaves no audit row.
  • Keep personal data out of data: it is a security trail, not an analytics store. data passes through the log redactor before it is stored.
  • Required by default. A missing unit, a database without an audit store or a non-UUID actor_id raises AuditUnavailableError instead of dropping the record. required=False is the explicit best-effort form (those cases are skipped and logged); in module code it needs a # sandloom: best-effort <reason> marker on the call (SANDLOOM-C007).
  • The database. The audit row goes to the route's write store, the same database as the write (Write admission). In a unit opened on a named database, database="billing" names it explicitly; an ambiguous choice raises rather than auditing in the wrong database.
  • In tests under client or db_uow the audit row lands in the test transaction (Testing reference).

Audit and deny logging

Audit is for committed actions. An authorization denial raises and rolls back its unit, so it is recorded by the structured deny log and the access_denied_* metrics instead (Authorization). The operator exports the audit trail with sandloom security (see the operator reference).

What breaks it

Code What it catches
SANDLOOM-C007 record_audit(required=False) without a marker
SANDLOOM-R002 an effect outside any unit
Guarantees, precisely
  • The audit row commits with the change, in the write store (G-AUDIT-01, G-AUDIT-02).
  • One automatic, mandatory audit row per sensitive write; none for a read or a refused request (G-SENS-04).

The ledger with the enforcing tests: Guarantees & capabilities.

Design rationale