Skip to main content

sen_plugin_host/permission/
store.rs

1//! Permission storage for persisting granted permissions
2//!
3//! Provides trait-based permission storage that framework users can customize.
4
5use sen_plugin_api::Capabilities;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs::{self, File};
9use std::io::{BufReader, BufWriter};
10use std::path::{Path, PathBuf};
11use std::sync::RwLock;
12use thiserror::Error;
13
14use super::strategy::PermissionGranularity;
15
16/// Error type for permission store operations
17#[derive(Debug, Error)]
18pub enum StoreError {
19    #[error("Failed to read permission store: {0}")]
20    ReadError(#[from] std::io::Error),
21
22    #[error("Failed to parse permission store: {0}")]
23    ParseError(#[from] serde_json::Error),
24
25    #[error("Permission not found for: {0}")]
26    NotFound(String),
27
28    #[error("Store is read-only")]
29    ReadOnly,
30}
31
32/// Trust level for stored permissions
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum StoredTrustLevel {
36    /// Trust for this session only (cleared on restart)
37    Session,
38    /// Trust permanently
39    Permanent,
40}
41
42/// Stored permission entry
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct StoredPermission {
45    /// When permission was granted (Unix timestamp)
46    pub granted_at: u64,
47    /// Hash of capabilities at grant time
48    pub capabilities_hash: String,
49    /// Granted capabilities
50    pub capabilities: Capabilities,
51    /// Trust level
52    pub trust_level: StoredTrustLevel,
53}
54
55impl StoredPermission {
56    /// Create a new stored permission
57    pub fn new(capabilities: Capabilities, trust_level: StoredTrustLevel) -> Self {
58        use std::time::{SystemTime, UNIX_EPOCH};
59        let granted_at = SystemTime::now()
60            .duration_since(UNIX_EPOCH)
61            .unwrap_or_default()
62            .as_secs();
63
64        Self {
65            granted_at,
66            capabilities_hash: capabilities.compute_hash(),
67            capabilities,
68            trust_level,
69        }
70    }
71
72    /// Check if this permission has escalated (capabilities changed)
73    pub fn has_escalated(&self, new_caps: &Capabilities) -> bool {
74        self.capabilities_hash != new_caps.compute_hash()
75    }
76}
77
78/// Trait for permission storage
79///
80/// Framework users implement this trait to customize permission persistence.
81pub trait PermissionStore: Send + Sync {
82    /// Get stored permission for a plugin
83    fn get(&self, key: &str) -> Result<Option<StoredPermission>, StoreError>;
84
85    /// Store permission for a plugin
86    fn set(&self, key: &str, permission: StoredPermission) -> Result<(), StoreError>;
87
88    /// Remove permission for a plugin
89    fn remove(&self, key: &str) -> Result<(), StoreError>;
90
91    /// List all stored permissions
92    fn list(&self) -> Result<Vec<(String, StoredPermission)>, StoreError>;
93
94    /// Clear all permissions
95    fn clear(&self) -> Result<(), StoreError>;
96
97    /// Generate storage key from plugin/command info
98    fn make_key(
99        &self,
100        plugin: &str,
101        command: Option<&str>,
102        granularity: PermissionGranularity,
103    ) -> String {
104        match granularity {
105            PermissionGranularity::Plugin => plugin.to_string(),
106            PermissionGranularity::Command => match command {
107                Some(cmd) => format!("{}:{}", plugin, cmd),
108                None => plugin.to_string(),
109            },
110            PermissionGranularity::Execution => {
111                // Execution-level permissions are not stored
112                format!("{}:execution", plugin)
113            }
114        }
115    }
116}
117
118// ============================================================================
119// File-based Permission Store
120// ============================================================================
121
122/// Persistent file data structure
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124struct PermissionFileData {
125    version: u32,
126    plugins: HashMap<String, StoredPermission>,
127}
128
129impl PermissionFileData {
130    fn new() -> Self {
131        Self {
132            version: 1,
133            plugins: HashMap::new(),
134        }
135    }
136}
137
138/// File-based permission store
139///
140/// Stores permissions in a JSON file at a configurable location.
141/// Default: `~/.config/<app>/permissions.json`
142pub struct FilePermissionStore {
143    path: PathBuf,
144    data: RwLock<PermissionFileData>,
145}
146
147impl FilePermissionStore {
148    /// Create a new file-based store at the specified path
149    pub fn new(path: impl AsRef<Path>) -> Result<Self, StoreError> {
150        let path = path.as_ref().to_path_buf();
151
152        let data = if path.exists() {
153            let file = File::open(&path)?;
154            let reader = BufReader::new(file);
155            serde_json::from_reader(reader)?
156        } else {
157            PermissionFileData::new()
158        };
159
160        Ok(Self {
161            path,
162            data: RwLock::new(data),
163        })
164    }
165
166    /// Create a store in the default location for an application
167    pub fn default_for_app(app_name: &str) -> Result<Self, StoreError> {
168        let config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".config"));
169        let path = config_dir.join(app_name).join("permissions.json");
170        Self::new(path)
171    }
172
173    /// Get the store file path
174    pub fn path(&self) -> &Path {
175        &self.path
176    }
177
178    /// Save data to file
179    fn save(&self) -> Result<(), StoreError> {
180        // Create parent directory if needed
181        if let Some(parent) = self.path.parent() {
182            fs::create_dir_all(parent)?;
183        }
184
185        let data = self
186            .data
187            .read()
188            .expect("FilePermissionStore RwLock poisoned");
189        let file = File::create(&self.path)?;
190        let writer = BufWriter::new(file);
191        serde_json::to_writer_pretty(writer, &*data)?;
192        Ok(())
193    }
194}
195
196impl PermissionStore for FilePermissionStore {
197    fn get(&self, key: &str) -> Result<Option<StoredPermission>, StoreError> {
198        let data = self
199            .data
200            .read()
201            .expect("FilePermissionStore RwLock poisoned");
202        Ok(data.plugins.get(key).cloned())
203    }
204
205    fn set(&self, key: &str, permission: StoredPermission) -> Result<(), StoreError> {
206        {
207            let mut data = self
208                .data
209                .write()
210                .expect("FilePermissionStore RwLock poisoned");
211            data.plugins.insert(key.to_string(), permission);
212        }
213        self.save()
214    }
215
216    fn remove(&self, key: &str) -> Result<(), StoreError> {
217        {
218            let mut data = self
219                .data
220                .write()
221                .expect("FilePermissionStore RwLock poisoned");
222            data.plugins.remove(key);
223        }
224        self.save()
225    }
226
227    fn list(&self) -> Result<Vec<(String, StoredPermission)>, StoreError> {
228        let data = self
229            .data
230            .read()
231            .expect("FilePermissionStore RwLock poisoned");
232        Ok(data
233            .plugins
234            .iter()
235            .map(|(k, v)| (k.clone(), v.clone()))
236            .collect())
237    }
238
239    fn clear(&self) -> Result<(), StoreError> {
240        {
241            let mut data = self
242                .data
243                .write()
244                .expect("FilePermissionStore RwLock poisoned");
245            data.plugins.clear();
246        }
247        self.save()
248    }
249}
250
251impl std::fmt::Debug for FilePermissionStore {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        f.debug_struct("FilePermissionStore")
254            .field("path", &self.path)
255            .finish()
256    }
257}
258
259// ============================================================================
260// In-Memory Permission Store
261// ============================================================================
262
263/// In-memory permission store for testing or session-only permissions
264pub struct MemoryPermissionStore {
265    data: RwLock<HashMap<String, StoredPermission>>,
266}
267
268impl MemoryPermissionStore {
269    /// Create a new in-memory store
270    pub fn new() -> Self {
271        Self {
272            data: RwLock::new(HashMap::new()),
273        }
274    }
275
276    /// Get the number of stored permissions
277    pub fn len(&self) -> usize {
278        self.data
279            .read()
280            .expect("MemoryPermissionStore RwLock poisoned")
281            .len()
282    }
283
284    /// Check if the store is empty
285    pub fn is_empty(&self) -> bool {
286        self.data
287            .read()
288            .expect("MemoryPermissionStore RwLock poisoned")
289            .is_empty()
290    }
291}
292
293impl Default for MemoryPermissionStore {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299impl PermissionStore for MemoryPermissionStore {
300    fn get(&self, key: &str) -> Result<Option<StoredPermission>, StoreError> {
301        let data = self
302            .data
303            .read()
304            .expect("MemoryPermissionStore RwLock poisoned");
305        Ok(data.get(key).cloned())
306    }
307
308    fn set(&self, key: &str, permission: StoredPermission) -> Result<(), StoreError> {
309        let mut data = self
310            .data
311            .write()
312            .expect("MemoryPermissionStore RwLock poisoned");
313        data.insert(key.to_string(), permission);
314        Ok(())
315    }
316
317    fn remove(&self, key: &str) -> Result<(), StoreError> {
318        let mut data = self
319            .data
320            .write()
321            .expect("MemoryPermissionStore RwLock poisoned");
322        data.remove(key);
323        Ok(())
324    }
325
326    fn list(&self) -> Result<Vec<(String, StoredPermission)>, StoreError> {
327        let data = self
328            .data
329            .read()
330            .expect("MemoryPermissionStore RwLock poisoned");
331        Ok(data.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
332    }
333
334    fn clear(&self) -> Result<(), StoreError> {
335        let mut data = self
336            .data
337            .write()
338            .expect("MemoryPermissionStore RwLock poisoned");
339        data.clear();
340        Ok(())
341    }
342}
343
344impl std::fmt::Debug for MemoryPermissionStore {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        f.debug_struct("MemoryPermissionStore")
347            .field("count", &self.len())
348            .finish()
349    }
350}
351
352// ============================================================================
353// Read-Only Permission Store
354// ============================================================================
355
356/// Read-only wrapper for any permission store
357///
358/// Useful for CI environments where permissions should be pre-defined
359/// but not modified at runtime.
360pub struct ReadOnlyPermissionStore<S: PermissionStore> {
361    inner: S,
362}
363
364impl<S: PermissionStore> ReadOnlyPermissionStore<S> {
365    /// Create a read-only wrapper
366    pub fn new(inner: S) -> Self {
367        Self { inner }
368    }
369}
370
371impl<S: PermissionStore> PermissionStore for ReadOnlyPermissionStore<S> {
372    fn get(&self, key: &str) -> Result<Option<StoredPermission>, StoreError> {
373        self.inner.get(key)
374    }
375
376    fn set(&self, _key: &str, _permission: StoredPermission) -> Result<(), StoreError> {
377        Err(StoreError::ReadOnly)
378    }
379
380    fn remove(&self, _key: &str) -> Result<(), StoreError> {
381        Err(StoreError::ReadOnly)
382    }
383
384    fn list(&self) -> Result<Vec<(String, StoredPermission)>, StoreError> {
385        self.inner.list()
386    }
387
388    fn clear(&self) -> Result<(), StoreError> {
389        Err(StoreError::ReadOnly)
390    }
391}
392
393impl<S: PermissionStore + std::fmt::Debug> std::fmt::Debug for ReadOnlyPermissionStore<S> {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        f.debug_struct("ReadOnlyPermissionStore")
396            .field("inner", &self.inner)
397            .finish()
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use sen_plugin_api::PathPattern;
405
406    #[test]
407    fn test_memory_store() {
408        let store = MemoryPermissionStore::new();
409        let caps = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
410        let perm = StoredPermission::new(caps, StoredTrustLevel::Permanent);
411
412        store.set("test-plugin", perm.clone()).unwrap();
413
414        let retrieved = store.get("test-plugin").unwrap();
415        assert!(retrieved.is_some());
416        assert_eq!(retrieved.unwrap().capabilities_hash, perm.capabilities_hash);
417
418        store.remove("test-plugin").unwrap();
419        assert!(store.get("test-plugin").unwrap().is_none());
420    }
421
422    #[test]
423    fn test_file_store() {
424        let dir = tempfile::tempdir().unwrap();
425        let path = dir.path().join("permissions.json");
426
427        let store = FilePermissionStore::new(&path).unwrap();
428        let caps = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
429        let perm = StoredPermission::new(caps, StoredTrustLevel::Permanent);
430
431        store.set("test-plugin", perm).unwrap();
432
433        // Verify file was created
434        assert!(path.exists());
435
436        // Create new store from same file
437        let store2 = FilePermissionStore::new(&path).unwrap();
438        let retrieved = store2.get("test-plugin").unwrap();
439        assert!(retrieved.is_some());
440    }
441
442    #[test]
443    fn test_read_only_store() {
444        let inner = MemoryPermissionStore::new();
445        let caps = Capabilities::none();
446        let perm = StoredPermission::new(caps, StoredTrustLevel::Session);
447        inner.set("pre-existing", perm).unwrap();
448
449        let store = ReadOnlyPermissionStore::new(inner);
450
451        // Can read
452        assert!(store.get("pre-existing").unwrap().is_some());
453
454        // Cannot write
455        let new_perm = StoredPermission::new(Capabilities::none(), StoredTrustLevel::Session);
456        assert!(matches!(
457            store.set("new", new_perm),
458            Err(StoreError::ReadOnly)
459        ));
460
461        // Cannot remove
462        assert!(matches!(
463            store.remove("pre-existing"),
464            Err(StoreError::ReadOnly)
465        ));
466    }
467
468    #[test]
469    fn test_escalation_detection() {
470        let caps1 = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
471        let perm = StoredPermission::new(caps1, StoredTrustLevel::Permanent);
472
473        // Same capabilities
474        let caps2 = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
475        assert!(!perm.has_escalated(&caps2));
476
477        // Different capabilities (escalation)
478        let caps3 = Capabilities::default()
479            .with_fs_read(vec![PathPattern::new("./data")])
480            .with_fs_write(vec![PathPattern::new("./output")]);
481        assert!(perm.has_escalated(&caps3));
482    }
483
484    #[test]
485    fn test_make_key() {
486        let store = MemoryPermissionStore::new();
487
488        let key = store.make_key("hello", None, PermissionGranularity::Plugin);
489        assert_eq!(key, "hello");
490
491        let key = store.make_key("hello", Some("greet"), PermissionGranularity::Command);
492        assert_eq!(key, "hello:greet");
493
494        let key = store.make_key("hello", None, PermissionGranularity::Command);
495        assert_eq!(key, "hello");
496    }
497}