Skip to main content

mixtape_core/permission/
store.rs

1//! Grant storage trait and implementations.
2
3use super::grant::Grant;
4use async_trait::async_trait;
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::RwLock;
8
9/// Errors that can occur in grant store operations.
10#[derive(Debug, thiserror::Error)]
11pub enum GrantStoreError {
12    /// Failed to read grants from storage.
13    #[error("Failed to read grants: {0}")]
14    Read(String),
15
16    /// Failed to write grants to storage.
17    #[error("Failed to write grants: {0}")]
18    Write(String),
19
20    /// IO error during storage operations.
21    #[error("IO error: {0}")]
22    Io(#[from] std::io::Error),
23
24    /// JSON serialization/deserialization error.
25    #[error("JSON error: {0}")]
26    Json(#[from] serde_json::Error),
27}
28
29/// Trait for grant storage implementations.
30///
31/// Stores persist grants for later retrieval. Implementations handle
32/// the mechanics of saving and loading grants.
33#[async_trait]
34pub trait GrantStore: Send + Sync {
35    /// Save a grant to storage.
36    async fn save(&self, grant: Grant) -> Result<(), GrantStoreError>;
37
38    /// Load all grants for a specific tool.
39    async fn load(&self, tool: &str) -> Result<Vec<Grant>, GrantStoreError>;
40
41    /// Load all grants across all tools.
42    async fn load_all(&self) -> Result<Vec<Grant>, GrantStoreError>;
43
44    /// Remove a specific grant.
45    ///
46    /// Returns `true` if a grant was removed, `false` if not found.
47    async fn delete(&self, tool: &str, params_hash: Option<&str>) -> Result<bool, GrantStoreError>;
48
49    /// Clear all grants.
50    async fn clear(&self) -> Result<(), GrantStoreError>;
51}
52
53/// In-memory grant store.
54///
55/// Grants are cleared when the process exits. This is the default store
56/// used by the agent.
57pub struct MemoryGrantStore {
58    grants: RwLock<HashMap<String, Vec<Grant>>>,
59}
60
61impl MemoryGrantStore {
62    /// Create a new empty memory store.
63    pub fn new() -> Self {
64        Self {
65            grants: RwLock::new(HashMap::new()),
66        }
67    }
68
69    /// Grant permission to use a tool (any parameters).
70    ///
71    /// Convenience method for `save(Grant::tool(name))`.
72    pub async fn grant_tool(&self, tool: &str) -> Result<(), GrantStoreError> {
73        self.save(Grant::tool(tool)).await
74    }
75}
76
77impl Default for MemoryGrantStore {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83#[async_trait]
84impl GrantStore for MemoryGrantStore {
85    async fn save(&self, grant: Grant) -> Result<(), GrantStoreError> {
86        let mut grants = self.grants.write().expect("RwLock poisoned");
87        grants.entry(grant.tool.clone()).or_default().push(grant);
88        Ok(())
89    }
90
91    async fn load(&self, tool: &str) -> Result<Vec<Grant>, GrantStoreError> {
92        Ok(self
93            .grants
94            .read()
95            .expect("RwLock poisoned")
96            .get(tool)
97            .cloned()
98            .unwrap_or_default())
99    }
100
101    async fn load_all(&self) -> Result<Vec<Grant>, GrantStoreError> {
102        Ok(self
103            .grants
104            .read()
105            .expect("RwLock poisoned")
106            .values()
107            .flatten()
108            .cloned()
109            .collect())
110    }
111
112    async fn delete(&self, tool: &str, params_hash: Option<&str>) -> Result<bool, GrantStoreError> {
113        let mut grants = self.grants.write().expect("RwLock poisoned");
114
115        if let Some(tool_grants) = grants.get_mut(tool) {
116            let original_len = tool_grants.len();
117            tool_grants.retain(|g| g.params_hash.as_deref() != params_hash);
118            Ok(tool_grants.len() < original_len)
119        } else {
120            Ok(false)
121        }
122    }
123
124    async fn clear(&self) -> Result<(), GrantStoreError> {
125        let mut grants = self.grants.write().expect("RwLock poisoned");
126        grants.clear();
127        Ok(())
128    }
129}
130
131/// File-based grant store.
132///
133/// Grants are persisted to a JSON file. The file is created automatically
134/// when the first grant is stored.
135pub struct FileGrantStore {
136    path: PathBuf,
137    cache: RwLock<Option<HashMap<String, Vec<Grant>>>>,
138}
139
140impl FileGrantStore {
141    /// Create a new file-based store at the given path.
142    ///
143    /// The file does not need to exist - it will be created when
144    /// the first grant is saved.
145    pub fn new(path: impl Into<PathBuf>) -> Self {
146        Self {
147            path: path.into(),
148            cache: RwLock::new(None),
149        }
150    }
151
152    /// Load grants from file into cache if not already loaded.
153    fn ensure_loaded(&self) -> Result<(), GrantStoreError> {
154        let mut cache = self.cache.write().expect("RwLock poisoned");
155        if cache.is_some() {
156            return Ok(());
157        }
158
159        let grants = if self.path.exists() {
160            let contents = std::fs::read_to_string(&self.path)?;
161            if contents.trim().is_empty() {
162                HashMap::new()
163            } else {
164                serde_json::from_str(&contents)?
165            }
166        } else {
167            HashMap::new()
168        };
169
170        *cache = Some(grants);
171        Ok(())
172    }
173
174    /// Write cache to file.
175    fn flush(&self) -> Result<(), GrantStoreError> {
176        let cache = self.cache.read().expect("RwLock poisoned");
177        if let Some(ref grants) = *cache {
178            // Create parent directories if needed
179            if let Some(parent) = self.path.parent() {
180                if !parent.exists() {
181                    std::fs::create_dir_all(parent)?;
182                }
183            }
184            let json = serde_json::to_string_pretty(grants)?;
185            std::fs::write(&self.path, json)?;
186        }
187        Ok(())
188    }
189}
190
191#[async_trait]
192impl GrantStore for FileGrantStore {
193    async fn save(&self, grant: Grant) -> Result<(), GrantStoreError> {
194        self.ensure_loaded()?;
195        {
196            let mut cache = self.cache.write().expect("RwLock poisoned");
197            if let Some(ref mut grants) = *cache {
198                grants.entry(grant.tool.clone()).or_default().push(grant);
199            }
200        }
201        self.flush()
202    }
203
204    async fn load(&self, tool: &str) -> Result<Vec<Grant>, GrantStoreError> {
205        self.ensure_loaded()?;
206        let cache = self.cache.read().expect("RwLock poisoned");
207        Ok(cache
208            .as_ref()
209            .and_then(|g| g.get(tool).cloned())
210            .unwrap_or_default())
211    }
212
213    async fn load_all(&self) -> Result<Vec<Grant>, GrantStoreError> {
214        self.ensure_loaded()?;
215        let cache = self.cache.read().expect("RwLock poisoned");
216        Ok(cache
217            .as_ref()
218            .map(|g| g.values().flatten().cloned().collect())
219            .unwrap_or_default())
220    }
221
222    async fn delete(&self, tool: &str, params_hash: Option<&str>) -> Result<bool, GrantStoreError> {
223        self.ensure_loaded()?;
224        let removed = {
225            let mut cache = self.cache.write().expect("RwLock poisoned");
226            if let Some(ref mut grants) = *cache {
227                if let Some(tool_grants) = grants.get_mut(tool) {
228                    let original_len = tool_grants.len();
229                    tool_grants.retain(|g| g.params_hash.as_deref() != params_hash);
230                    tool_grants.len() < original_len
231                } else {
232                    false
233                }
234            } else {
235                false
236            }
237        };
238        if removed {
239            self.flush()?;
240        }
241        Ok(removed)
242    }
243
244    async fn clear(&self) -> Result<(), GrantStoreError> {
245        self.ensure_loaded()?;
246        {
247            let mut cache = self.cache.write().expect("RwLock poisoned");
248            if let Some(ref mut grants) = *cache {
249                grants.clear();
250            }
251        }
252        self.flush()
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[tokio::test]
261    async fn test_memory_store_basic() {
262        let store = MemoryGrantStore::new();
263
264        // Initially empty
265        assert!(store.load("test").await.unwrap().is_empty());
266        assert!(store.load_all().await.unwrap().is_empty());
267
268        // Save a grant
269        store.save(Grant::tool("test")).await.unwrap();
270
271        // Should be retrievable
272        let grants = store.load("test").await.unwrap();
273        assert_eq!(grants.len(), 1);
274        assert_eq!(grants[0].tool, "test");
275
276        // load_all should include it
277        assert_eq!(store.load_all().await.unwrap().len(), 1);
278    }
279
280    #[tokio::test]
281    async fn test_memory_store_multiple_grants() {
282        let store = MemoryGrantStore::new();
283
284        store.save(Grant::tool("test")).await.unwrap();
285        store.save(Grant::exact("test", "hash1")).await.unwrap();
286        store.save(Grant::exact("test", "hash2")).await.unwrap();
287
288        let grants = store.load("test").await.unwrap();
289        assert_eq!(grants.len(), 3);
290    }
291
292    #[tokio::test]
293    async fn test_memory_store_multiple_tools() {
294        let store = MemoryGrantStore::new();
295
296        store.save(Grant::tool("tool_a")).await.unwrap();
297        store.save(Grant::tool("tool_b")).await.unwrap();
298
299        assert_eq!(store.load("tool_a").await.unwrap().len(), 1);
300        assert_eq!(store.load("tool_b").await.unwrap().len(), 1);
301        assert_eq!(store.load_all().await.unwrap().len(), 2);
302    }
303
304    #[tokio::test]
305    async fn test_memory_store_delete() {
306        let store = MemoryGrantStore::new();
307
308        store.save(Grant::tool("test")).await.unwrap();
309        store.save(Grant::exact("test", "hash1")).await.unwrap();
310
311        assert_eq!(store.load("test").await.unwrap().len(), 2);
312
313        // Delete the exact grant
314        let removed = store.delete("test", Some("hash1")).await.unwrap();
315        assert!(removed);
316
317        let grants = store.load("test").await.unwrap();
318        assert_eq!(grants.len(), 1);
319        assert!(grants[0].is_tool_wide());
320
321        // Delete the tool-wide grant
322        let removed = store.delete("test", None).await.unwrap();
323        assert!(removed);
324        assert!(store.load("test").await.unwrap().is_empty());
325    }
326
327    #[tokio::test]
328    async fn test_memory_store_delete_nonexistent() {
329        let store = MemoryGrantStore::new();
330
331        let removed = store.delete("test", None).await.unwrap();
332        assert!(!removed);
333    }
334
335    #[tokio::test]
336    async fn test_memory_store_clear() {
337        let store = MemoryGrantStore::new();
338
339        store.save(Grant::tool("a")).await.unwrap();
340        store.save(Grant::tool("b")).await.unwrap();
341
342        assert_eq!(store.load_all().await.unwrap().len(), 2);
343
344        store.clear().await.unwrap();
345        assert!(store.load_all().await.unwrap().is_empty());
346    }
347
348    #[tokio::test]
349    async fn test_file_store_basic() {
350        let temp_dir = tempfile::tempdir().unwrap();
351        let path = temp_dir.path().join("grants.json");
352
353        let store = FileGrantStore::new(&path);
354
355        // Initially empty (file doesn't exist)
356        assert!(store.load("test").await.unwrap().is_empty());
357
358        // Save a grant
359        store.save(Grant::tool("test")).await.unwrap();
360
361        // File should exist now
362        assert!(path.exists());
363
364        // Should be retrievable
365        let grants = store.load("test").await.unwrap();
366        assert_eq!(grants.len(), 1);
367
368        // Create new store instance to verify persistence
369        let store2 = FileGrantStore::new(&path);
370        let grants = store2.load("test").await.unwrap();
371        assert_eq!(grants.len(), 1);
372    }
373
374    #[tokio::test]
375    async fn test_file_store_creates_parent_dirs() {
376        let temp_dir = tempfile::tempdir().unwrap();
377        let path = temp_dir.path().join("nested/dir/grants.json");
378
379        let store = FileGrantStore::new(&path);
380        store.save(Grant::tool("test")).await.unwrap();
381
382        assert!(path.exists());
383    }
384
385    #[tokio::test]
386    async fn test_file_store_handles_empty_file() {
387        let temp_dir = tempfile::tempdir().unwrap();
388        let path = temp_dir.path().join("grants.json");
389
390        // Create empty file
391        std::fs::write(&path, "").unwrap();
392
393        let store = FileGrantStore::new(&path);
394        assert!(store.load("test").await.unwrap().is_empty());
395
396        // Can still save
397        store.save(Grant::tool("test")).await.unwrap();
398        assert_eq!(store.load("test").await.unwrap().len(), 1);
399    }
400}