sen_plugin_host/permission/
store.rs1use 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum StoredTrustLevel {
36 Session,
38 Permanent,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct StoredPermission {
45 pub granted_at: u64,
47 pub capabilities_hash: String,
49 pub capabilities: Capabilities,
51 pub trust_level: StoredTrustLevel,
53}
54
55impl StoredPermission {
56 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 pub fn has_escalated(&self, new_caps: &Capabilities) -> bool {
74 self.capabilities_hash != new_caps.compute_hash()
75 }
76}
77
78pub trait PermissionStore: Send + Sync {
82 fn get(&self, key: &str) -> Result<Option<StoredPermission>, StoreError>;
84
85 fn set(&self, key: &str, permission: StoredPermission) -> Result<(), StoreError>;
87
88 fn remove(&self, key: &str) -> Result<(), StoreError>;
90
91 fn list(&self) -> Result<Vec<(String, StoredPermission)>, StoreError>;
93
94 fn clear(&self) -> Result<(), StoreError>;
96
97 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 format!("{}:execution", plugin)
113 }
114 }
115 }
116}
117
118#[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
138pub struct FilePermissionStore {
143 path: PathBuf,
144 data: RwLock<PermissionFileData>,
145}
146
147impl FilePermissionStore {
148 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 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 pub fn path(&self) -> &Path {
175 &self.path
176 }
177
178 fn save(&self) -> Result<(), StoreError> {
180 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
259pub struct MemoryPermissionStore {
265 data: RwLock<HashMap<String, StoredPermission>>,
266}
267
268impl MemoryPermissionStore {
269 pub fn new() -> Self {
271 Self {
272 data: RwLock::new(HashMap::new()),
273 }
274 }
275
276 pub fn len(&self) -> usize {
278 self.data
279 .read()
280 .expect("MemoryPermissionStore RwLock poisoned")
281 .len()
282 }
283
284 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
352pub struct ReadOnlyPermissionStore<S: PermissionStore> {
361 inner: S,
362}
363
364impl<S: PermissionStore> ReadOnlyPermissionStore<S> {
365 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 assert!(path.exists());
435
436 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 assert!(store.get("pre-existing").unwrap().is_some());
453
454 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 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 let caps2 = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
475 assert!(!perm.has_escalated(&caps2));
476
477 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}