1use std::sync::Arc;
2
3use crate::db::{ConnExt, Database};
4use crate::error::Result;
5use crate::id;
6
7use super::backend::AuditLogBackend;
8use super::entry::AuditEntry;
9
10#[derive(Clone)]
20pub struct AuditLog(Arc<dyn AuditLogBackend>);
21
22impl AuditLog {
23 pub fn new(db: Database) -> Self {
25 Self(Arc::new(SqliteAuditBackend { db }))
26 }
27
28 pub fn from_backend(backend: Arc<dyn AuditLogBackend>) -> Self {
30 Self(backend)
31 }
32
33 pub async fn record(&self, entry: &AuditEntry) -> Result<()> {
40 self.0.record(entry).await
41 }
42
43 pub async fn record_silent(&self, entry: &AuditEntry) {
45 if let Err(e) = self.0.record(entry).await {
46 tracing::error!(
47 error = %e,
48 action = %entry.action(),
49 actor = %entry.actor(),
50 "audit log write failed"
51 );
52 }
53 }
54
55 #[cfg(any(test, feature = "test-helpers"))]
60 pub fn memory() -> (Self, Arc<MemoryAuditBackend>) {
61 let backend = Arc::new(MemoryAuditBackend {
62 entries: std::sync::Mutex::new(Vec::new()),
63 });
64 (Self(backend.clone()), backend)
65 }
66}
67
68struct SqliteAuditBackend {
69 db: Database,
70}
71
72impl AuditLogBackend for SqliteAuditBackend {
73 fn record<'a>(
74 &'a self,
75 entry: &'a AuditEntry,
76 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
77 Box::pin(async move {
78 let id = id::ulid();
79 let metadata_json = entry
80 .metadata_value()
81 .map(|v| v.to_string())
82 .unwrap_or_else(|| "{}".to_string());
83
84 let (ip, user_agent, fingerprint) = match entry.client_info_value() {
85 Some(ci) => (
86 ci.ip_value().map(String::from),
87 ci.user_agent_value().map(String::from),
88 ci.fingerprint_value().map(String::from),
89 ),
90 None => (None, None, None),
91 };
92
93 self.db
94 .conn()
95 .execute_raw(
96 "INSERT INTO audit_log \
97 (id, actor, action, resource_type, resource_id, metadata, ip, user_agent, fingerprint, tenant_id) \
98 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
99 libsql::params![
100 id,
101 entry.actor(),
102 entry.action(),
103 entry.resource_type(),
104 entry.resource_id(),
105 metadata_json,
106 ip,
107 user_agent,
108 fingerprint,
109 entry.tenant_id_value(),
110 ],
111 )
112 .await
113 .map_err(crate::error::Error::from)?;
114
115 Ok(())
116 })
117 }
118}
119
120#[cfg(any(test, feature = "test-helpers"))]
122pub struct MemoryAuditBackend {
123 entries: std::sync::Mutex<Vec<AuditEntry>>,
124}
125
126#[cfg(any(test, feature = "test-helpers"))]
127impl MemoryAuditBackend {
128 pub fn entries(&self) -> Vec<AuditEntry> {
130 self.entries.lock().unwrap().clone()
131 }
132}
133
134#[cfg(any(test, feature = "test-helpers"))]
135impl AuditLogBackend for MemoryAuditBackend {
136 fn record<'a>(
137 &'a self,
138 entry: &'a AuditEntry,
139 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
140 self.entries.lock().unwrap().push(entry.clone());
141 Box::pin(async { Ok(()) })
142 }
143}