41 lines
1.3 KiB
Python
41 lines
1.3 KiB
Python
|
|
"""Auditable outcome for every read and mutation.
|
||
|
|
|
||
|
|
One JSONL ledger per tenant per day. The record shape is
|
||
|
|
``common.schema.json#/$defs/audit_outcome``; it never carries request bodies,
|
||
|
|
only the action name, the resource id and the decision.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from hux.identity import Identity
|
||
|
|
from hux.store import TenantStore, now_iso
|
||
|
|
|
||
|
|
OUTCOMES = ("allow", "deny", "not_found", "conflict", "flag_off")
|
||
|
|
|
||
|
|
|
||
|
|
def record(store: TenantStore, identity: Identity, action: str, resource: str, outcome: str, reason: str = "") -> dict:
|
||
|
|
"""Append an audit outcome and return it."""
|
||
|
|
if outcome not in OUTCOMES:
|
||
|
|
raise ValueError(f"unknown outcome {outcome!r}")
|
||
|
|
entry = {
|
||
|
|
"at": now_iso(),
|
||
|
|
"identity": identity.record(),
|
||
|
|
"action": action,
|
||
|
|
"resource": resource[:200],
|
||
|
|
"outcome": outcome,
|
||
|
|
}
|
||
|
|
if reason:
|
||
|
|
entry["reason"] = reason[:200]
|
||
|
|
store.append("audit", now_iso()[:10], entry)
|
||
|
|
return entry
|
||
|
|
|
||
|
|
|
||
|
|
def recent(store: TenantStore, limit: int = 200) -> list[dict]:
|
||
|
|
"""Newest audit rows across day ledgers, newest last."""
|
||
|
|
rows: list[dict] = []
|
||
|
|
for name in reversed(store.ledgers("audit")):
|
||
|
|
rows = store.read("audit", name) + rows
|
||
|
|
if len(rows) >= limit:
|
||
|
|
break
|
||
|
|
return rows[-limit:]
|