ricecoder_permissions/audit/
storage.rs1use super::models::AuditLogEntry;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7pub struct AuditStorage {
9 path: PathBuf,
10}
11
12impl AuditStorage {
13 pub fn new<P: AsRef<Path>>(path: P) -> Self {
15 Self {
16 path: path.as_ref().to_path_buf(),
17 }
18 }
19
20 pub fn save_logs(&self, entries: &[AuditLogEntry]) -> Result<(), String> {
22 if let Some(parent) = self.path.parent() {
24 fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {}", e))?;
25 }
26
27 let json = serde_json::to_string_pretty(entries)
29 .map_err(|e| format!("Failed to serialize logs: {}", e))?;
30
31 fs::write(&self.path, json).map_err(|e| format!("Failed to write logs to file: {}", e))?;
33
34 Ok(())
35 }
36
37 pub fn load_logs(&self) -> Result<Vec<AuditLogEntry>, String> {
39 if !self.path.exists() {
41 return Ok(Vec::new());
42 }
43
44 let content = fs::read_to_string(&self.path)
46 .map_err(|e| format!("Failed to read logs from file: {}", e))?;
47
48 let entries: Vec<AuditLogEntry> = serde_json::from_str(&content)
50 .map_err(|e| format!("Failed to deserialize logs: {}", e))?;
51
52 Ok(entries)
53 }
54
55 pub fn append_logs(&self, new_entries: &[AuditLogEntry]) -> Result<(), String> {
57 let mut entries = self.load_logs()?;
59
60 entries.extend_from_slice(new_entries);
62
63 self.save_logs(&entries)?;
65
66 Ok(())
67 }
68
69 pub fn clear_logs(&self) -> Result<(), String> {
71 self.save_logs(&[])?;
72 Ok(())
73 }
74
75 pub fn path(&self) -> &Path {
77 &self.path
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::audit::models::{AuditAction, AuditResult};
85 use tempfile::TempDir;
86
87 #[test]
88 fn test_save_and_load_logs() {
89 let temp_dir = TempDir::new().unwrap();
90 let log_path = temp_dir.path().join("audit.json");
91 let storage = AuditStorage::new(&log_path);
92
93 let entries = vec![
95 AuditLogEntry::new(
96 "tool1".to_string(),
97 AuditAction::Allowed,
98 AuditResult::Success,
99 ),
100 AuditLogEntry::new(
101 "tool2".to_string(),
102 AuditAction::Denied,
103 AuditResult::Blocked,
104 ),
105 ];
106
107 let result = storage.save_logs(&entries);
109 assert!(result.is_ok());
110
111 let loaded = storage.load_logs().unwrap();
113 assert_eq!(loaded.len(), 2);
114 assert_eq!(loaded[0].tool, "tool1");
115 assert_eq!(loaded[1].tool, "tool2");
116 }
117
118 #[test]
119 fn test_load_nonexistent_file() {
120 let temp_dir = TempDir::new().unwrap();
121 let log_path = temp_dir.path().join("nonexistent.json");
122 let storage = AuditStorage::new(&log_path);
123
124 let loaded = storage.load_logs().unwrap();
126 assert_eq!(loaded.len(), 0);
127 }
128
129 #[test]
130 fn test_append_logs() {
131 let temp_dir = TempDir::new().unwrap();
132 let log_path = temp_dir.path().join("audit.json");
133 let storage = AuditStorage::new(&log_path);
134
135 let initial_entries = vec![AuditLogEntry::new(
137 "tool1".to_string(),
138 AuditAction::Allowed,
139 AuditResult::Success,
140 )];
141 storage.save_logs(&initial_entries).unwrap();
142
143 let new_entries = vec![AuditLogEntry::new(
145 "tool2".to_string(),
146 AuditAction::Denied,
147 AuditResult::Blocked,
148 )];
149 storage.append_logs(&new_entries).unwrap();
150
151 let loaded = storage.load_logs().unwrap();
153 assert_eq!(loaded.len(), 2);
154 assert_eq!(loaded[0].tool, "tool1");
155 assert_eq!(loaded[1].tool, "tool2");
156 }
157
158 #[test]
159 fn test_clear_logs() {
160 let temp_dir = TempDir::new().unwrap();
161 let log_path = temp_dir.path().join("audit.json");
162 let storage = AuditStorage::new(&log_path);
163
164 let entries = vec![AuditLogEntry::new(
166 "tool1".to_string(),
167 AuditAction::Allowed,
168 AuditResult::Success,
169 )];
170 storage.save_logs(&entries).unwrap();
171
172 let loaded = storage.load_logs().unwrap();
174 assert_eq!(loaded.len(), 1);
175
176 storage.clear_logs().unwrap();
178
179 let loaded = storage.load_logs().unwrap();
181 assert_eq!(loaded.len(), 0);
182 }
183
184 #[test]
185 fn test_storage_path() {
186 let temp_dir = TempDir::new().unwrap();
187 let log_path = temp_dir.path().join("audit.json");
188 let storage = AuditStorage::new(&log_path);
189
190 assert_eq!(storage.path(), log_path.as_path());
191 }
192
193 #[test]
194 fn test_save_creates_directory() {
195 let temp_dir = TempDir::new().unwrap();
196 let log_path = temp_dir.path().join("subdir").join("audit.json");
197 let storage = AuditStorage::new(&log_path);
198
199 let entries = vec![AuditLogEntry::new(
200 "tool1".to_string(),
201 AuditAction::Allowed,
202 AuditResult::Success,
203 )];
204
205 let result = storage.save_logs(&entries);
207 assert!(result.is_ok());
208 assert!(log_path.exists());
209 }
210
211 #[test]
212 fn test_serialization_roundtrip() {
213 let temp_dir = TempDir::new().unwrap();
214 let log_path = temp_dir.path().join("audit.json");
215 let storage = AuditStorage::new(&log_path);
216
217 let mut entry = AuditLogEntry::new(
219 "test_tool".to_string(),
220 AuditAction::Prompted,
221 AuditResult::Success,
222 );
223 entry.agent = Some("agent1".to_string());
224 entry.context = Some("User approved".to_string());
225
226 let entries = vec![entry.clone()];
227
228 storage.save_logs(&entries).unwrap();
230 let loaded = storage.load_logs().unwrap();
231
232 assert_eq!(loaded[0].tool, entry.tool);
234 assert_eq!(loaded[0].action, entry.action);
235 assert_eq!(loaded[0].result, entry.result);
236 assert_eq!(loaded[0].agent, entry.agent);
237 assert_eq!(loaded[0].context, entry.context);
238 }
239}