mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-24 02:38:43 +02:00
feat(admin): tamper-evident privileged-action audit chain [operator §0.4]
A separate, low-volume audit trail for admin/setup/erasure actions, hash-chained so a DB-write attacker cannot silently rewrite history. Kept distinct from the high-volume user audit_logs (which keeps its enum action) — the chain applies only where the privacy-maximalist mandate requires it. - migration 20260626000001: admin_audit_logs (free-text action, actor_kind, actor_pubkey_prefix [prefix only, no social graph], target, details, real-peer ip, prev_hash, row_hash; monotonic seq). - db::admin_audit: append_admin_audit(&mut PgConnection) so the audit row commits in the SAME transaction as the state change it records (fail-closed, not a best-effort side effect); record_admin_audit (own tx) for standalone audits; verify_admin_audit_chain (boot/doctor walk — detects edit/delete/reorder/broken linkage); list_admin_audit (panel). row_hash = HMAC(integrity_secret, prev_hash || canonical(row)); appends serialize on a tx advisory lock so seq/prev_hash cannot race. - AdminAuditLog/NewAdminAudit models; 1 L1 test (link + verify + tamper-detect + restore).
This commit is contained in:
@@ -280,6 +280,34 @@ pub struct NewAdminKey {
|
||||
pub activation_deadline: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// A row in the tamper-evident privileged-action audit chain.
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct AdminAuditLog {
|
||||
pub id: Uuid,
|
||||
pub seq: i64,
|
||||
pub action: String,
|
||||
pub actor_kind: String,
|
||||
pub actor_pubkey_prefix: Option<String>,
|
||||
pub target: Option<String>,
|
||||
pub details: Option<serde_json::Value>,
|
||||
pub ip_address: Option<IpNetwork>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub prev_hash: String,
|
||||
pub row_hash: String,
|
||||
}
|
||||
|
||||
/// Input for appending a privileged-action audit row. `seq`, timestamps, and the
|
||||
/// hash-chain fields are computed by the db layer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewAdminAudit {
|
||||
pub action: String,
|
||||
pub actor_kind: String,
|
||||
pub actor_pubkey_prefix: Option<String>,
|
||||
pub target: Option<String>,
|
||||
pub details: Option<serde_json::Value>,
|
||||
pub ip_address: Option<IpNetwork>,
|
||||
}
|
||||
|
||||
/// Input for an admin session row. The id and access_jti are generated by the
|
||||
/// caller (the refresh token embeds the id, the access token embeds the jti).
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
Reference in New Issue
Block a user