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_login 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.datapasses 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_idraisesAuditUnavailableErrorinstead of dropping the record.required=Falseis 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
clientordb_uowthe 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¶
- ADR-0005: the effect model
- ADR-0009: route policy, "What risk triggers"