mixtape_core/permission/
store.rs1use super::grant::Grant;
4use async_trait::async_trait;
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::RwLock;
8
9#[derive(Debug, thiserror::Error)]
11pub enum GrantStoreError {
12 #[error("Failed to read grants: {0}")]
14 Read(String),
15
16 #[error("Failed to write grants: {0}")]
18 Write(String),
19
20 #[error("IO error: {0}")]
22 Io(#[from] std::io::Error),
23
24 #[error("JSON error: {0}")]
26 Json(#[from] serde_json::Error),
27}
28
29#[async_trait]
34pub trait GrantStore: Send + Sync {
35 async fn save(&self, grant: Grant) -> Result<(), GrantStoreError>;
37
38 async fn load(&self, tool: &str) -> Result<Vec<Grant>, GrantStoreError>;
40
41 async fn load_all(&self) -> Result<Vec<Grant>, GrantStoreError>;
43
44 async fn delete(&self, tool: &str, params_hash: Option<&str>) -> Result<bool, GrantStoreError>;
48
49 async fn clear(&self) -> Result<(), GrantStoreError>;
51}
52
53pub struct MemoryGrantStore {
58 grants: RwLock<HashMap<String, Vec<Grant>>>,
59}
60
61impl MemoryGrantStore {
62 pub fn new() -> Self {
64 Self {
65 grants: RwLock::new(HashMap::new()),
66 }
67 }
68
69 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
131pub struct FileGrantStore {
136 path: PathBuf,
137 cache: RwLock<Option<HashMap<String, Vec<Grant>>>>,
138}
139
140impl FileGrantStore {
141 pub fn new(path: impl Into<PathBuf>) -> Self {
146 Self {
147 path: path.into(),
148 cache: RwLock::new(None),
149 }
150 }
151
152 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 fn flush(&self) -> Result<(), GrantStoreError> {
176 let cache = self.cache.read().expect("RwLock poisoned");
177 if let Some(ref grants) = *cache {
178 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 assert!(store.load("test").await.unwrap().is_empty());
266 assert!(store.load_all().await.unwrap().is_empty());
267
268 store.save(Grant::tool("test")).await.unwrap();
270
271 let grants = store.load("test").await.unwrap();
273 assert_eq!(grants.len(), 1);
274 assert_eq!(grants[0].tool, "test");
275
276 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 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 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 assert!(store.load("test").await.unwrap().is_empty());
357
358 store.save(Grant::tool("test")).await.unwrap();
360
361 assert!(path.exists());
363
364 let grants = store.load("test").await.unwrap();
366 assert_eq!(grants.len(), 1);
367
368 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 std::fs::write(&path, "").unwrap();
392
393 let store = FileGrantStore::new(&path);
394 assert!(store.load("test").await.unwrap().is_empty());
395
396 store.save(Grant::tool("test")).await.unwrap();
398 assert_eq!(store.load("test").await.unwrap().len(), 1);
399 }
400}