use std::sync::Arc;
use chrono::{DateTime, Utc};
use super::{AuditEvent, AuditStorage};
pub struct DbAuditStorage {
pool: Arc<crate::database::DbPool>,
}
impl DbAuditStorage {
pub fn new(pool: Arc<crate::database::DbPool>) -> Self {
Self { pool }
}
pub async fn init(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let session = self.pool.get_session("admin").await?;
session
.execute_raw_ddl(
"CREATE TABLE IF NOT EXISTS audit_events (\
id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, \
user_id TEXT NOT NULL, entity_type TEXT NOT NULL, \
operation TEXT NOT NULL, severity TEXT NOT NULL, \
result TEXT NOT NULL, event TEXT NOT NULL)",
)
.await?;
Ok(())
}
}
#[async_trait::async_trait]
impl AuditStorage for DbAuditStorage {
async fn store(
&self,
event: &AuditEvent,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.init().await?;
let event_text = event.to_json()?;
let sql = format!(
"INSERT INTO audit_events (id, timestamp, user_id, entity_type, \
operation, severity, result, event) \
VALUES ('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}') \
ON CONFLICT(id) DO UPDATE SET timestamp = excluded.timestamp, \
user_id = excluded.user_id, entity_type = excluded.entity_type, \
operation = excluded.operation, severity = excluded.severity, \
result = excluded.result, event = excluded.event",
sql_escape(&event.id),
sql_escape(&event.timestamp.to_rfc3339()),
sql_escape(&event.user_id),
sql_escape(&event.entity_type),
sql_escape(&serde_json::to_string(&event.operation)?),
sql_escape(&serde_json::to_string(&event.severity)?),
sql_escape(&serde_json::to_string(&event.result)?),
sql_escape(&event_text),
);
let session = self.pool.get_session("admin").await?;
session.execute_raw(&sql).await?;
Ok(())
}
async fn query(
&self,
filters: &super::AuditQueryFilters,
) -> Result<Vec<AuditEvent>, Box<dyn std::error::Error + Send + Sync>> {
let mut conditions: Vec<String> = Vec::new();
if let Some(user_id) = &filters.user_id {
conditions.push(format!("user_id = '{}'", sql_escape(user_id)));
}
if let Some(entity_type) = &filters.entity_type {
conditions.push(format!("entity_type = '{}'", sql_escape(entity_type)));
}
if let Some(operation) = &filters.operation {
conditions.push(format!(
"operation = '{}'",
sql_escape(&serde_json::to_string(operation)?)
));
}
if let Some(severity) = &filters.severity {
conditions.push(format!(
"severity = '{}'",
sql_escape(&serde_json::to_string(severity)?)
));
}
if let Some(result) = &filters.result {
conditions.push(format!(
"result = '{}'",
sql_escape(&serde_json::to_string(result)?)
));
}
if let Some(start) = &filters.start_time {
conditions.push(format!(
"timestamp >= '{}'",
sql_escape(&start.to_rfc3339())
));
}
if let Some(end) = &filters.end_time {
conditions.push(format!("timestamp <= '{}'", sql_escape(&end.to_rfc3339())));
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!(" WHERE {}", conditions.join(" AND "))
};
let rows = self
.pool
.query_rows(
&format!(
"SELECT event FROM audit_events{} ORDER BY timestamp, id",
where_clause
),
"admin",
)
.await?;
let mut events = Vec::with_capacity(rows.len());
for row in &rows {
let text = match row.get("event").and_then(|v| v.as_str()) {
Some(t) => t,
None => continue,
};
events.push(AuditEvent::from_json(text)?);
}
Ok(events)
}
async fn cleanup(
&self,
before: &DateTime<Utc>,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
let session = self.pool.get_session("admin").await?;
let sql = format!(
"DELETE FROM audit_events WHERE timestamp < '{}'",
sql_escape(&before.to_rfc3339())
);
let result = session.execute_raw(&sql).await?;
Ok(result.rows_affected())
}
}
fn sql_escape(s: &str) -> String {
s.replace('\'', "''")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sql_escape_quotes() {
assert_eq!(sql_escape("it's"), "it''s");
assert_eq!(sql_escape("plain"), "plain");
}
}