//! Writes to the security audit log. Recording never fails the request: a store error is logged //! and the action continues. Passwords and tokens never go into an entry. use forge_core::{AuditAction, AuditEntry, Store, User}; use serde_json::Value; use topcoat::context::Cx; use crate::client_ip::client_ip; /// Who did it. `Anonymous` is an unauthenticated attempt, `Cli` is the admin command line. pub enum Actor<'a> { User(&'a User), Anonymous, Cli, } /// What it was done to. Empty kind and id for actions with no object (such as a login). #[derive(Default)] pub struct Target<'a> { pub kind: &'a str, pub id: &'a str, pub name: &'a str, } /// Appends one entry. `cx` supplies the client IP for web requests; the CLI passes `None` and /// gets an empty IP. `data` is an optional JSON object (`Value::Null` stores `{}`). pub async fn record( store: &Store, cx: Option<&Cx>, actor: Actor<'_>, action: AuditAction, target: Target<'_>, data: Value, ) { let mut entry = AuditEntry::new(action); match actor { Actor::User(user) => { entry.actor_id = Some(user.id.clone()); entry.actor_name = user.username.clone(); } Actor::Anonymous => {} Actor::Cli => entry.actor_name = "cli".to_string(), } entry.target_kind = target.kind.to_string(); entry.target_id = target.id.to_string(); entry.target_name = target.name.to_string(); entry.ip = cx.map(|cx| client_ip(cx).to_string()).unwrap_or_default(); if !data.is_null() { entry.data = data.to_string(); } if let Err(err) = store.record_audit(&entry).await { tracing::error!(action = action.as_str(), error = %err, "failed to record audit entry"); } }