ricecoder_permissions/
storage.rs

1//! Storage integration for permissions system
2//!
3//! This module provides persistence for permissions configuration and audit logs.
4
5use crate::audit::AuditLogEntry;
6use crate::error::Result;
7use crate::permission::PermissionConfig;
8use std::path::Path;
9
10/// Repository trait for storing and retrieving permissions
11pub trait PermissionRepository: Send + Sync {
12    /// Load permission configuration from storage
13    fn load_config(&self) -> Result<PermissionConfig>;
14
15    /// Save permission configuration to storage
16    fn save_config(&self, config: &PermissionConfig) -> Result<()>;
17
18    /// Load audit logs from storage
19    fn load_audit_logs(&self) -> Result<Vec<AuditLogEntry>>;
20
21    /// Save audit logs to storage
22    fn save_audit_logs(&self, logs: &[AuditLogEntry]) -> Result<()>;
23
24    /// Append a single audit log entry to storage
25    fn append_audit_log(&self, entry: &AuditLogEntry) -> Result<()>;
26}
27
28/// File-based permission repository
29pub struct FilePermissionRepository {
30    /// Path to permissions configuration file
31    config_path: std::path::PathBuf,
32    /// Path to audit logs file
33    audit_path: std::path::PathBuf,
34}
35
36impl FilePermissionRepository {
37    /// Create a new file-based permission repository
38    pub fn new<P: AsRef<Path>>(config_path: P, audit_path: P) -> Self {
39        Self {
40            config_path: config_path.as_ref().to_path_buf(),
41            audit_path: audit_path.as_ref().to_path_buf(),
42        }
43    }
44
45    /// Create a new file-based permission repository with default paths
46    pub fn with_defaults<P: AsRef<Path>>(base_path: P) -> Self {
47        let base = base_path.as_ref();
48        Self {
49            config_path: base.join("permissions.json"),
50            audit_path: base.join("audit_logs.json"),
51        }
52    }
53}
54
55impl PermissionRepository for FilePermissionRepository {
56    fn load_config(&self) -> Result<PermissionConfig> {
57        if !self.config_path.exists() {
58            // Return default config if file doesn't exist
59            return Ok(PermissionConfig::new());
60        }
61
62        let content = std::fs::read_to_string(&self.config_path)?;
63        let config = serde_json::from_str(&content)?;
64        Ok(config)
65    }
66
67    fn save_config(&self, config: &PermissionConfig) -> Result<()> {
68        // Create parent directories if they don't exist
69        if let Some(parent) = self.config_path.parent() {
70            std::fs::create_dir_all(parent)?;
71        }
72
73        let content = serde_json::to_string_pretty(config)?;
74        std::fs::write(&self.config_path, content)?;
75        Ok(())
76    }
77
78    fn load_audit_logs(&self) -> Result<Vec<AuditLogEntry>> {
79        if !self.audit_path.exists() {
80            // Return empty logs if file doesn't exist
81            return Ok(Vec::new());
82        }
83
84        let content = std::fs::read_to_string(&self.audit_path)?;
85        let logs = serde_json::from_str(&content)?;
86        Ok(logs)
87    }
88
89    fn save_audit_logs(&self, logs: &[AuditLogEntry]) -> Result<()> {
90        // Create parent directories if they don't exist
91        if let Some(parent) = self.audit_path.parent() {
92            std::fs::create_dir_all(parent)?;
93        }
94
95        let content = serde_json::to_string_pretty(logs)?;
96        std::fs::write(&self.audit_path, content)?;
97        Ok(())
98    }
99
100    fn append_audit_log(&self, entry: &AuditLogEntry) -> Result<()> {
101        // Load existing logs
102        let mut logs = self.load_audit_logs()?;
103
104        // Append new entry
105        logs.push(entry.clone());
106
107        // Save all logs
108        self.save_audit_logs(&logs)?;
109        Ok(())
110    }
111}
112
113/// In-memory permission repository (for testing)
114pub struct InMemoryPermissionRepository {
115    config: std::sync::Arc<std::sync::RwLock<PermissionConfig>>,
116    logs: std::sync::Arc<std::sync::RwLock<Vec<AuditLogEntry>>>,
117}
118
119impl InMemoryPermissionRepository {
120    /// Create a new in-memory permission repository
121    pub fn new() -> Self {
122        Self {
123            config: std::sync::Arc::new(std::sync::RwLock::new(PermissionConfig::new())),
124            logs: std::sync::Arc::new(std::sync::RwLock::new(Vec::new())),
125        }
126    }
127}
128
129impl Default for InMemoryPermissionRepository {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135impl PermissionRepository for InMemoryPermissionRepository {
136    fn load_config(&self) -> Result<PermissionConfig> {
137        let config = self
138            .config
139            .read()
140            .map_err(|e| crate::error::Error::Internal(format!("Failed to read config: {}", e)))?;
141        Ok(config.clone())
142    }
143
144    fn save_config(&self, config: &PermissionConfig) -> Result<()> {
145        let mut stored_config = self
146            .config
147            .write()
148            .map_err(|e| crate::error::Error::Internal(format!("Failed to write config: {}", e)))?;
149        *stored_config = config.clone();
150        Ok(())
151    }
152
153    fn load_audit_logs(&self) -> Result<Vec<AuditLogEntry>> {
154        let logs = self
155            .logs
156            .read()
157            .map_err(|e| crate::error::Error::Internal(format!("Failed to read logs: {}", e)))?;
158        Ok(logs.clone())
159    }
160
161    fn save_audit_logs(&self, logs: &[AuditLogEntry]) -> Result<()> {
162        let mut stored_logs = self
163            .logs
164            .write()
165            .map_err(|e| crate::error::Error::Internal(format!("Failed to write logs: {}", e)))?;
166        *stored_logs = logs.to_vec();
167        Ok(())
168    }
169
170    fn append_audit_log(&self, entry: &AuditLogEntry) -> Result<()> {
171        let mut logs = self
172            .logs
173            .write()
174            .map_err(|e| crate::error::Error::Internal(format!("Failed to write logs: {}", e)))?;
175        logs.push(entry.clone());
176        Ok(())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::audit::AuditAction;
184    use crate::audit::AuditResult;
185    use crate::permission::PermissionLevel;
186    use crate::permission::ToolPermission;
187
188    #[test]
189    fn test_in_memory_repository_save_and_load_config() {
190        let repo = InMemoryPermissionRepository::new();
191
192        let mut config = PermissionConfig::new();
193        config.add_permission(ToolPermission::new(
194            "test_tool".to_string(),
195            PermissionLevel::Allow,
196        ));
197
198        repo.save_config(&config).unwrap();
199
200        let loaded = repo.load_config().unwrap();
201        assert_eq!(loaded.get_permissions().len(), 1);
202        assert_eq!(loaded.get_permissions()[0].tool_pattern, "test_tool");
203    }
204
205    #[test]
206    fn test_in_memory_repository_save_and_load_logs() {
207        let repo = InMemoryPermissionRepository::new();
208
209        let entry = AuditLogEntry::new(
210            "test_tool".to_string(),
211            AuditAction::Allowed,
212            AuditResult::Success,
213        );
214
215        repo.append_audit_log(&entry).unwrap();
216
217        let logs = repo.load_audit_logs().unwrap();
218        assert_eq!(logs.len(), 1);
219        assert_eq!(logs[0].tool, "test_tool");
220    }
221
222    #[test]
223    fn test_in_memory_repository_append_multiple_logs() {
224        let repo = InMemoryPermissionRepository::new();
225
226        let entry1 = AuditLogEntry::new(
227            "tool1".to_string(),
228            AuditAction::Allowed,
229            AuditResult::Success,
230        );
231        let entry2 = AuditLogEntry::new(
232            "tool2".to_string(),
233            AuditAction::Denied,
234            AuditResult::Blocked,
235        );
236
237        repo.append_audit_log(&entry1).unwrap();
238        repo.append_audit_log(&entry2).unwrap();
239
240        let logs = repo.load_audit_logs().unwrap();
241        assert_eq!(logs.len(), 2);
242        assert_eq!(logs[0].tool, "tool1");
243        assert_eq!(logs[1].tool, "tool2");
244    }
245
246    #[test]
247    fn test_in_memory_repository_default() {
248        let repo = InMemoryPermissionRepository::default();
249        let config = repo.load_config().unwrap();
250        assert_eq!(config.get_permissions().len(), 0);
251    }
252}