Skip to main content

alopex_server/
audit.rs

1use std::fmt;
2use std::fs::OpenOptions;
3use std::io::{BufWriter, Write};
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Deserializer, Serialize};
9
10use crate::error::{Result, ServerError};
11
12/// Audit log sink trait.
13pub trait AuditLogSink: Send + Sync + 'static {
14    /// Emit a log entry.
15    fn log(&self, entry: &AuditLogEntry);
16    /// Flush buffered output.
17    fn flush(&self) -> Result<()>;
18}
19
20/// Output configuration for audit logs.
21#[derive(Clone, Default)]
22pub enum AuditLogOutput {
23    #[default]
24    Stdout,
25    File {
26        path: PathBuf,
27    },
28    Custom(Arc<dyn AuditLogSink>),
29}
30
31impl fmt::Debug for AuditLogOutput {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            AuditLogOutput::Stdout => f.debug_tuple("Stdout").finish(),
35            AuditLogOutput::File { path } => f.debug_struct("File").field("path", path).finish(),
36            AuditLogOutput::Custom(_) => f.debug_tuple("Custom").finish(),
37        }
38    }
39}
40
41#[derive(Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43enum AuditLogOutputConfig {
44    Stdout,
45    File { path: PathBuf },
46}
47
48impl<'de> Deserialize<'de> for AuditLogOutput {
49    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
50    where
51        D: Deserializer<'de>,
52    {
53        let config = AuditLogOutputConfig::deserialize(deserializer)?;
54        Ok(match config {
55            AuditLogOutputConfig::Stdout => Self::Stdout,
56            AuditLogOutputConfig::File { path } => Self::File { path },
57        })
58    }
59}
60
61impl AuditLogOutput {
62    fn into_sink(self) -> Result<Arc<dyn AuditLogSink>> {
63        match self {
64            AuditLogOutput::Stdout => Ok(Arc::new(StdoutAuditSink::default())),
65            AuditLogOutput::File { path } => Ok(Arc::new(FileAuditSink::new(path)?)),
66            AuditLogOutput::Custom(sink) => Ok(sink),
67        }
68    }
69}
70
71/// Audit log entry structure.
72#[derive(Debug, Serialize)]
73pub struct AuditLogEntry {
74    pub event_type: AuditEventType,
75    pub actor: Option<String>,
76    pub target: String,
77    pub correlation_id: String,
78    pub timestamp: DateTime<Utc>,
79    pub details: serde_json::Value,
80}
81
82#[derive(Debug, Serialize)]
83#[serde(rename_all = "snake_case")]
84pub enum AuditEventType {
85    DdlExecute,
86    ConfigChange,
87    AuthFailure,
88}
89
90/// Audit logger with output routing.
91#[derive(Clone)]
92pub struct AuditLogger {
93    sink: Arc<dyn AuditLogSink>,
94}
95
96impl AuditLogger {
97    /// Create a new audit logger.
98    pub fn new(output: AuditLogOutput) -> Result<Self> {
99        Ok(Self {
100            sink: output.into_sink()?,
101        })
102    }
103
104    /// Log a raw entry.
105    pub fn log(&self, entry: AuditLogEntry) {
106        self.sink.log(&entry);
107    }
108
109    /// Log DDL execution.
110    pub fn log_ddl(&self, sql: &str, actor: Option<&str>, correlation_id: &str) {
111        let entry = AuditLogEntry {
112            event_type: AuditEventType::DdlExecute,
113            actor: actor.map(|v| v.to_string()),
114            target: "ddl".to_string(),
115            correlation_id: correlation_id.to_string(),
116            timestamp: Utc::now(),
117            details: serde_json::json!({ "sql": sql }),
118        };
119        self.log(entry);
120    }
121
122    /// Log a config change.
123    pub fn log_config_change(&self, key: &str, old: &str, new: &str, correlation_id: &str) {
124        let entry = AuditLogEntry {
125            event_type: AuditEventType::ConfigChange,
126            actor: None,
127            target: key.to_string(),
128            correlation_id: correlation_id.to_string(),
129            timestamp: Utc::now(),
130            details: serde_json::json!({ "old": old, "new": new }),
131        };
132        self.log(entry);
133    }
134
135    /// Flush output.
136    pub fn flush(&self) -> Result<()> {
137        self.sink.flush()
138    }
139}
140
141#[derive(Default)]
142struct StdoutAuditSink {
143    buffer: Mutex<()>,
144}
145
146impl AuditLogSink for StdoutAuditSink {
147    fn log(&self, entry: &AuditLogEntry) {
148        let _guard = self.buffer.lock().ok();
149        if let Ok(line) = serde_json::to_string(entry) {
150            println!("{line}");
151        }
152    }
153
154    fn flush(&self) -> Result<()> {
155        Ok(())
156    }
157}
158
159struct FileAuditSink {
160    writer: Mutex<BufWriter<std::fs::File>>,
161}
162
163impl FileAuditSink {
164    fn new(path: PathBuf) -> Result<Self> {
165        let file = OpenOptions::new()
166            .create(true)
167            .append(true)
168            .open(&path)
169            .map_err(ServerError::Io)?;
170        Ok(Self {
171            writer: Mutex::new(BufWriter::new(file)),
172        })
173    }
174}
175
176impl AuditLogSink for FileAuditSink {
177    fn log(&self, entry: &AuditLogEntry) {
178        let Ok(mut writer) = self.writer.lock() else {
179            return;
180        };
181        if let Ok(line) = serde_json::to_string(entry) {
182            let _ = writeln!(writer, "{line}");
183        }
184    }
185
186    fn flush(&self) -> Result<()> {
187        let mut writer = self
188            .writer
189            .lock()
190            .map_err(|_| ServerError::Internal("audit log lock poisoned".into()))?;
191        writer.flush().map_err(ServerError::Io)
192    }
193}