ricecoder_permissions/audit/
storage.rs

1//! Audit log persistence to JSON files
2
3use super::models::AuditLogEntry;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Audit log storage for persisting logs to disk
8pub struct AuditStorage {
9    path: PathBuf,
10}
11
12impl AuditStorage {
13    /// Create a new audit storage with the given path
14    pub fn new<P: AsRef<Path>>(path: P) -> Self {
15        Self {
16            path: path.as_ref().to_path_buf(),
17        }
18    }
19
20    /// Save audit logs to a JSON file
21    pub fn save_logs(&self, entries: &[AuditLogEntry]) -> Result<(), String> {
22        // Ensure parent directory exists
23        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        // Serialize entries to JSON
28        let json = serde_json::to_string_pretty(entries)
29            .map_err(|e| format!("Failed to serialize logs: {}", e))?;
30
31        // Write to file
32        fs::write(&self.path, json).map_err(|e| format!("Failed to write logs to file: {}", e))?;
33
34        Ok(())
35    }
36
37    /// Load audit logs from a JSON file
38    pub fn load_logs(&self) -> Result<Vec<AuditLogEntry>, String> {
39        // Check if file exists
40        if !self.path.exists() {
41            return Ok(Vec::new());
42        }
43
44        // Read file
45        let content = fs::read_to_string(&self.path)
46            .map_err(|e| format!("Failed to read logs from file: {}", e))?;
47
48        // Deserialize from JSON
49        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    /// Append audit logs to the existing file
56    pub fn append_logs(&self, new_entries: &[AuditLogEntry]) -> Result<(), String> {
57        // Load existing logs
58        let mut entries = self.load_logs()?;
59
60        // Append new entries
61        entries.extend_from_slice(new_entries);
62
63        // Save all logs
64        self.save_logs(&entries)?;
65
66        Ok(())
67    }
68
69    /// Clear all logs from the file
70    pub fn clear_logs(&self) -> Result<(), String> {
71        self.save_logs(&[])?;
72        Ok(())
73    }
74
75    /// Get the path to the storage file
76    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        // Create test entries
94        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        // Save logs
108        let result = storage.save_logs(&entries);
109        assert!(result.is_ok());
110
111        // Load logs
112        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        // Load from nonexistent file should return empty vec
125        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        // Save initial logs
136        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        // Append new logs
144        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        // Load and verify
152        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        // Save logs
165        let entries = vec![AuditLogEntry::new(
166            "tool1".to_string(),
167            AuditAction::Allowed,
168            AuditResult::Success,
169        )];
170        storage.save_logs(&entries).unwrap();
171
172        // Verify logs exist
173        let loaded = storage.load_logs().unwrap();
174        assert_eq!(loaded.len(), 1);
175
176        // Clear logs
177        storage.clear_logs().unwrap();
178
179        // Verify logs are cleared
180        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        // Save should create the directory
206        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        // Create entry with all fields
218        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        // Save and load
229        storage.save_logs(&entries).unwrap();
230        let loaded = storage.load_logs().unwrap();
231
232        // Verify all fields are preserved
233        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}