Skip to main content

sh_layer2/permission/
manager.rs

1//! # Permission Manager
2//!
3//! Central manager for the permission system.
4
5use crate::permission::policy::{PermissionPolicy, SecurityLevel};
6use crate::permission::types::{
7    AuditEntry, CachedPermission, PermissionAction, PermissionDecision, PermissionRequest,
8    PermissionResponse,
9};
10use parking_lot::RwLock;
11use sh_layer1::generate_short_id;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15/// Callback type for interactive permission prompts
16pub type PermissionPromptCallback =
17    Box<dyn Fn(&PermissionRequest) -> PermissionResponse + Send + Sync>;
18
19/// Result type for permission operations
20pub type PermissionResult<T> = Result<T, PermissionError>;
21
22/// Errors that can occur in the permission system
23#[derive(Debug, thiserror::Error)]
24pub enum PermissionError {
25    /// Action was denied
26    #[error("Permission denied: {0}")]
27    Denied(String),
28
29    /// Action is blocked by policy
30    #[error("Action blocked by security policy: {0}")]
31    BlockedByPolicy(String),
32
33    /// No prompt callback configured
34    #[error("No permission prompt callback configured")]
35    NoCallback,
36
37    /// Cache error
38    #[error("Permission cache error: {0}")]
39    CacheError(String),
40
41    /// Invalid request
42    #[error("Invalid permission request: {0}")]
43    InvalidRequest(String),
44}
45
46/// The permission manager
47pub struct PermissionManager {
48    /// Security policy
49    policy: RwLock<PermissionPolicy>,
50    /// Permission cache
51    cache: RwLock<HashMap<String, CachedPermission>>,
52    /// Audit log
53    audit_log: RwLock<Vec<AuditEntry>>,
54    /// Optional prompt callback for interactive mode
55    prompt_callback: RwLock<Option<Arc<PermissionPromptCallback>>>,
56}
57
58impl Default for PermissionManager {
59    fn default() -> Self {
60        Self::new(PermissionPolicy::default())
61    }
62}
63
64impl PermissionManager {
65    /// Create a new permission manager with the given policy
66    pub fn new(policy: PermissionPolicy) -> Self {
67        Self {
68            policy: RwLock::new(policy),
69            cache: RwLock::new(HashMap::new()),
70            audit_log: RwLock::new(Vec::new()),
71            prompt_callback: RwLock::new(None),
72        }
73    }
74
75    /// Set the prompt callback for interactive mode
76    pub fn set_prompt_callback(&self, callback: PermissionPromptCallback) {
77        *self.prompt_callback.write() = Some(Arc::new(callback));
78    }
79
80    /// Clear the prompt callback
81    pub fn clear_prompt_callback(&self) {
82        *self.prompt_callback.write() = None;
83    }
84
85    /// Update the security policy
86    pub fn set_policy(&self, policy: PermissionPolicy) {
87        *self.policy.write() = policy;
88    }
89
90    /// Get the current security level
91    pub fn security_level(&self) -> SecurityLevel {
92        self.policy.read().level
93    }
94
95    /// Check if an action is allowed, prompting if necessary
96    pub fn check_permission(
97        &self,
98        request: PermissionRequest,
99    ) -> PermissionResult<PermissionResponse> {
100        let category = request.action.category();
101
102        // Check if category is blocked by policy
103        if self.policy.read().is_category_blocked(category) {
104            return Err(PermissionError::BlockedByPolicy(format!(
105                "Category '{}' is blocked by security policy",
106                category
107            )));
108        }
109
110        // Check for hard-coded blocks
111        if let Some(block_reason) = self.check_blocked(&request.action) {
112            return Err(PermissionError::BlockedByPolicy(block_reason));
113        }
114
115        // Check for auto-approval
116        if self.policy.read().should_auto_approve(category) {
117            let response = PermissionResponse::allow(request.id.clone());
118            self.log_audit(&request, &response, false);
119            return Ok(response);
120        }
121
122        // Check cache
123        if self.policy.read().enable_cache {
124            if let Some(cached) = self.check_cache(&request) {
125                let response = PermissionResponse {
126                    request_id: request.id.clone(),
127                    decision: cached.decision,
128                    reason: Some("From cache".to_string()),
129                    timestamp: chrono::Utc::now(),
130                };
131                self.log_audit(&request, &response, true);
132                return Ok(response);
133            }
134        }
135
136        // Prompt user if callback is set
137        if let Some(callback) = self.prompt_callback.read().as_ref() {
138            let response = callback(&request);
139            let description = request.action.description();
140
141            // Cache if allowed to remember
142            if response.decision.should_remember() && self.policy.read().enable_cache {
143                self.cache_permission(&request.action, response.decision);
144            }
145
146            // Log audit
147            self.log_audit(&request, &response, false);
148
149            // Return error if denied
150            if !response.decision.is_allowed() {
151                return Err(PermissionError::Denied(format!(
152                    "User denied: {}",
153                    description
154                )));
155            }
156
157            Ok(response)
158        } else {
159            // No callback - deny by default in non-trusted mode
160            if self.policy.read().level == SecurityLevel::Trusted {
161                let response = PermissionResponse::allow(request.id.clone());
162                self.log_audit(&request, &response, false);
163                Ok(response)
164            } else {
165                Err(PermissionError::NoCallback)
166            }
167        }
168    }
169
170    /// Check if an action is blocked by policy
171    fn check_blocked(&self, action: &PermissionAction) -> Option<String> {
172        let policy = self.policy.read();
173
174        match action {
175            PermissionAction::CommandExecute { command, .. } => {
176                for blocked in &policy.blocked_commands {
177                    if command.starts_with(blocked) || command.contains(blocked) {
178                        return Some(format!(
179                            "Command '{}' is blocked by security policy",
180                            command
181                        ));
182                    }
183                }
184                None
185            }
186            PermissionAction::FileRead { path }
187            | PermissionAction::FileWrite { path, .. }
188            | PermissionAction::FileDelete { path } => {
189                if policy.is_path_blocked(path) {
190                    return Some(format!("Path '{}' is blocked by security policy", path));
191                }
192                None
193            }
194            PermissionAction::NetworkRequest { url, .. } => {
195                if policy.blocked_urls.iter().any(|u| url.contains(u)) {
196                    return Some(format!("URL '{}' is blocked by security policy", url));
197                }
198                None
199            }
200            _ => None,
201        }
202    }
203
204    /// Check cache for a cached decision
205    fn check_cache(&self, request: &PermissionRequest) -> Option<CachedPermission> {
206        let cache = self.cache.read();
207        let key = self.cache_key(&request.action);
208
209        if let Some(cached) = cache.get(&key) {
210            if cached.is_valid() {
211                let mut cached = cached.clone();
212                cached.use_once();
213                self.cache.write().insert(key, cached.clone());
214                return Some(cached);
215            }
216        }
217
218        None
219    }
220
221    /// Cache a permission decision
222    fn cache_permission(&self, action: &PermissionAction, decision: PermissionDecision) {
223        let key = self.cache_key(action);
224        let expire_seconds = self.policy.read().cache_expire_seconds;
225
226        let cached = CachedPermission {
227            action_pattern: key.clone(),
228            decision,
229            cached_at: chrono::Utc::now(),
230            expires_at: if expire_seconds > 0 {
231                Some(chrono::Utc::now() + chrono::Duration::seconds(expire_seconds as i64))
232            } else {
233                None
234            },
235            use_count: 0,
236        };
237
238        self.cache.write().insert(key, cached);
239    }
240
241    /// Generate cache key for an action
242    fn cache_key(&self, action: &PermissionAction) -> String {
243        match action {
244            PermissionAction::CommandExecute { command, args } => {
245                format!("cmd:{}:{}", command, args.join(" "))
246            }
247            PermissionAction::FileRead { path } => format!("read:{}", path),
248            PermissionAction::FileWrite { path, .. } => format!("write:{}", path),
249            PermissionAction::FileDelete { path } => format!("delete:{}", path),
250            PermissionAction::NetworkRequest { url, method } => format!("net:{}:{}", method, url),
251            PermissionAction::EnvAccess { names } => format!("env:{}", names.join(",")),
252            PermissionAction::PackageInstall { packages } => format!("pkg:{}", packages.join(",")),
253            PermissionAction::SystemAccess { resource } => format!("sys:{}", resource),
254            PermissionAction::Custom { description } => format!("custom:{}", description),
255        }
256    }
257
258    /// Log an audit entry
259    fn log_audit(
260        &self,
261        request: &PermissionRequest,
262        response: &PermissionResponse,
263        from_cache: bool,
264    ) {
265        if !self.policy.read().audit_enabled {
266            return;
267        }
268
269        let entry = AuditEntry {
270            id: generate_short_id(),
271            request: request.clone(),
272            response: response.clone(),
273            from_cache,
274            timestamp: chrono::Utc::now(),
275        };
276
277        let max = self.policy.read().max_audit_entries;
278        let mut log = self.audit_log.write();
279        log.push(entry);
280
281        // Trim if exceeds max
282        if log.len() > max {
283            let excess = log.len() - max;
284            log.drain(0..excess);
285        }
286    }
287
288    /// Get audit log entries
289    pub fn get_audit_log(&self) -> Vec<AuditEntry> {
290        self.audit_log.read().clone()
291    }
292
293    /// Clear audit log
294    pub fn clear_audit_log(&self) {
295        self.audit_log.write().clear();
296    }
297
298    /// Get cache statistics
299    pub fn cache_stats(&self) -> (usize, usize) {
300        let cache = self.cache.read();
301        let total = cache.len();
302        let valid = cache.values().filter(|c| c.is_valid()).count();
303        (total, valid)
304    }
305
306    /// Clear permission cache
307    pub fn clear_cache(&self) {
308        self.cache.write().clear();
309    }
310
311    /// Create a permission request for command execution
312    pub fn request_command(&self, command: &str, args: Vec<String>) -> PermissionRequest {
313        PermissionRequest::new(PermissionAction::CommandExecute {
314            command: command.to_string(),
315            args,
316        })
317    }
318
319    /// Create a permission request for file read
320    pub fn request_file_read(&self, path: &str) -> PermissionRequest {
321        PermissionRequest::new(PermissionAction::FileRead {
322            path: path.to_string(),
323        })
324    }
325
326    /// Create a permission request for file write
327    pub fn request_file_write(
328        &self,
329        path: &str,
330        content_preview: Option<&str>,
331    ) -> PermissionRequest {
332        PermissionRequest::new(PermissionAction::FileWrite {
333            path: path.to_string(),
334            content_preview: content_preview.map(|s| s.to_string()),
335        })
336    }
337
338    /// Create a permission request for file delete
339    pub fn request_file_delete(&self, path: &str) -> PermissionRequest {
340        PermissionRequest::new(PermissionAction::FileDelete {
341            path: path.to_string(),
342        })
343    }
344
345    /// Create a permission request for network request
346    pub fn request_network(&self, url: &str, method: &str) -> PermissionRequest {
347        PermissionRequest::new(PermissionAction::NetworkRequest {
348            url: url.to_string(),
349            method: method.to_string(),
350        })
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn test_permission_manager_creation() {
360        let manager = PermissionManager::default();
361        assert_eq!(manager.security_level(), SecurityLevel::Standard);
362    }
363
364    #[test]
365    fn test_trusted_policy_auto_approves() {
366        let manager = PermissionManager::new(PermissionPolicy::trusted());
367        let request = PermissionRequest::new(PermissionAction::FileRead {
368            path: "/test/file.txt".to_string(),
369        });
370
371        let result = manager.check_permission(request);
372        assert!(result.is_ok());
373    }
374
375    #[test]
376    fn test_blocked_path_denied() {
377        let manager = PermissionManager::default();
378        let request = PermissionRequest::new(PermissionAction::FileRead {
379            path: ".env".to_string(),
380        });
381
382        let result = manager.check_permission(request);
383        assert!(result.is_err());
384        assert!(matches!(
385            result.unwrap_err(),
386            PermissionError::BlockedByPolicy(_)
387        ));
388    }
389
390    #[test]
391    fn test_cache_stats() {
392        let manager = PermissionManager::default();
393        let (total, valid) = manager.cache_stats();
394        assert_eq!(total, 0);
395        assert_eq!(valid, 0);
396    }
397}