llm_config_audit/
storage.rs1use 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
9pub trait AuditStorage: Send + Sync {
11 fn store(&self, event: &AuditEvent) -> Result<()>;
13
14 fn query(
16 &self,
17 start: DateTime<Utc>,
18 end: DateTime<Utc>,
19 limit: Option<usize>,
20 ) -> Result<Vec<AuditEvent>>;
21
22 fn query_by_user(&self, user: &str, limit: Option<usize>) -> Result<Vec<AuditEvent>>;
24
25 fn count(&self) -> Result<usize>;
27}
28
29pub struct FileAuditStorage {
31 log_path: PathBuf,
32}
33
34impl FileAuditStorage {
35 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 #[allow(dead_code)]
47 fn log_file_path(&self) -> &Path {
48 &self.log_path
49 }
50
51 #[allow(dead_code)]
53 fn rotate_logs(&self) -> Result<()> {
54 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 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 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); assert_eq!(user2_events.len(), 2); }
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}