llm_config_audit/
storage.rs

1//! Audit log storage backends
2
3use crate::{events::AuditEvent, AuditError, Result};
4use chrono::{DateTime, Utc};
5use std::fs::{File, OpenOptions};
6use std::io::{BufRead, BufReader, Write};
7use std::path::{Path, PathBuf};
8
9/// Trait for audit log storage backends
10pub trait AuditStorage: Send + Sync {
11    /// Store an audit event
12    fn store(&self, event: &AuditEvent) -> Result<()>;
13
14    /// Query audit events within a time range
15    fn query(
16        &self,
17        start: DateTime<Utc>,
18        end: DateTime<Utc>,
19        limit: Option<usize>,
20    ) -> Result<Vec<AuditEvent>>;
21
22    /// Query events for a specific user
23    fn query_by_user(&self, user: &str, limit: Option<usize>) -> Result<Vec<AuditEvent>>;
24
25    /// Get total event count
26    fn count(&self) -> Result<usize>;
27}
28
29/// File-based audit log storage
30pub struct FileAuditStorage {
31    log_path: PathBuf,
32}
33
34impl FileAuditStorage {
35    /// Create a new file-based audit storage
36    pub fn new(log_dir: impl AsRef<Path>) -> Result<Self> {
37        let log_dir = log_dir.as_ref();
38        std::fs::create_dir_all(log_dir)?;
39
40        let log_path = log_dir.join("audit.log");
41
42        Ok(Self { log_path })
43    }
44
45    /// Get the current log file path
46    #[allow(dead_code)]
47    fn log_file_path(&self) -> &Path {
48        &self.log_path
49    }
50
51    /// Rotate log files (for future implementation)
52    #[allow(dead_code)]
53    fn rotate_logs(&self) -> Result<()> {
54        // TODO: Implement log rotation
55        // - Check file size
56        // - Compress old logs
57        // - Keep N most recent log files
58        Ok(())
59    }
60}
61
62impl AuditStorage for FileAuditStorage {
63    fn store(&self, event: &AuditEvent) -> Result<()> {
64        let mut file = OpenOptions::new()
65            .create(true)
66            .append(true)
67            .open(&self.log_path)?;
68
69        let json = serde_json::to_string(event)
70            .map_err(|e| AuditError::Serialization(e.to_string()))?;
71
72        writeln!(file, "{}", json)?;
73        file.sync_all()?;
74
75        Ok(())
76    }
77
78    fn query(
79        &self,
80        start: DateTime<Utc>,
81        end: DateTime<Utc>,
82        limit: Option<usize>,
83    ) -> Result<Vec<AuditEvent>> {
84        if !self.log_path.exists() {
85            return Ok(Vec::new());
86        }
87
88        let file = File::open(&self.log_path)?;
89        let reader = BufReader::new(file);
90
91        let mut events = Vec::new();
92
93        for line in reader.lines() {
94            let line = line?;
95            if line.trim().is_empty() {
96                continue;
97            }
98
99            let event: AuditEvent = serde_json::from_str(&line)
100                .map_err(|e| AuditError::Serialization(e.to_string()))?;
101
102            if event.timestamp >= start && event.timestamp <= end {
103                events.push(event);
104
105                if let Some(limit) = limit {
106                    if events.len() >= limit {
107                        break;
108                    }
109                }
110            }
111        }
112
113        Ok(events)
114    }
115
116    fn query_by_user(&self, user: &str, limit: Option<usize>) -> Result<Vec<AuditEvent>> {
117        if !self.log_path.exists() {
118            return Ok(Vec::new());
119        }
120
121        let file = File::open(&self.log_path)?;
122        let reader = BufReader::new(file);
123
124        let mut events = Vec::new();
125
126        for line in reader.lines() {
127            let line = line?;
128            if line.trim().is_empty() {
129                continue;
130            }
131
132            let event: AuditEvent = serde_json::from_str(&line)
133                .map_err(|e| AuditError::Serialization(e.to_string()))?;
134
135            if event.user == user {
136                events.push(event);
137
138                if let Some(limit) = limit {
139                    if events.len() >= limit {
140                        break;
141                    }
142                }
143            }
144        }
145
146        Ok(events)
147    }
148
149    fn count(&self) -> Result<usize> {
150        if !self.log_path.exists() {
151            return Ok(0);
152        }
153
154        let file = File::open(&self.log_path)?;
155        let reader = BufReader::new(file);
156
157        let count = reader.lines().filter(|l| l.is_ok()).count();
158
159        Ok(count)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::events::AuditEventType;
167    use tempfile::TempDir;
168
169    #[test]
170    fn test_file_storage_creation() {
171        let temp_dir = TempDir::new().unwrap();
172        let storage = FileAuditStorage::new(temp_dir.path()).unwrap();
173
174        // Storage directory should exist (file is created on first write)
175        assert!(temp_dir.path().exists());
176        assert_eq!(storage.count().unwrap(), 0);
177    }
178
179    #[test]
180    fn test_store_and_query() {
181        let temp_dir = TempDir::new().unwrap();
182        let storage = FileAuditStorage::new(temp_dir.path()).unwrap();
183
184        let event = AuditEvent::new(
185            AuditEventType::ConfigCreated {
186                namespace: "test".to_string(),
187                key: "key1".to_string(),
188                environment: "dev".to_string(),
189            },
190            "test-user",
191        );
192
193        storage.store(&event).unwrap();
194
195        let start = Utc::now() - chrono::Duration::hours(1);
196        let end = Utc::now() + chrono::Duration::hours(1);
197
198        let events = storage.query(start, end, None).unwrap();
199        assert_eq!(events.len(), 1);
200        assert_eq!(events[0].id, event.id);
201    }
202
203    #[test]
204    fn test_query_by_user() {
205        let temp_dir = TempDir::new().unwrap();
206        let storage = FileAuditStorage::new(temp_dir.path()).unwrap();
207
208        // Store events for different users
209        for i in 0..5 {
210            let user = if i % 2 == 0 { "user1" } else { "user2" };
211            let event = AuditEvent::new(
212                AuditEventType::ConfigAccessed {
213                    namespace: "test".to_string(),
214                    key: format!("key{}", i),
215                    environment: "dev".to_string(),
216                },
217                user,
218            );
219            storage.store(&event).unwrap();
220        }
221
222        let user1_events = storage.query_by_user("user1", None).unwrap();
223        let user2_events = storage.query_by_user("user2", None).unwrap();
224
225        assert_eq!(user1_events.len(), 3); // Events 0, 2, 4
226        assert_eq!(user2_events.len(), 2); // Events 1, 3
227    }
228
229    #[test]
230    fn test_count() {
231        let temp_dir = TempDir::new().unwrap();
232        let storage = FileAuditStorage::new(temp_dir.path()).unwrap();
233
234        assert_eq!(storage.count().unwrap(), 0);
235
236        for i in 0..10 {
237            let event = AuditEvent::new(
238                AuditEventType::ConfigAccessed {
239                    namespace: "test".to_string(),
240                    key: format!("key{}", i),
241                    environment: "dev".to_string(),
242                },
243                "user",
244            );
245            storage.store(&event).unwrap();
246        }
247
248        assert_eq!(storage.count().unwrap(), 10);
249    }
250
251    #[test]
252    fn test_query_with_limit() {
253        let temp_dir = TempDir::new().unwrap();
254        let storage = FileAuditStorage::new(temp_dir.path()).unwrap();
255
256        for i in 0..20 {
257            let event = AuditEvent::new(
258                AuditEventType::ConfigAccessed {
259                    namespace: "test".to_string(),
260                    key: format!("key{}", i),
261                    environment: "dev".to_string(),
262                },
263                "user",
264            );
265            storage.store(&event).unwrap();
266        }
267
268        let start = Utc::now() - chrono::Duration::hours(1);
269        let end = Utc::now() + chrono::Duration::hours(1);
270
271        let events = storage.query(start, end, Some(10)).unwrap();
272        assert_eq!(events.len(), 10);
273    }
274}