Skip to main content

sh_layer2/permission/
policy.rs

1//! # Permission Policy
2//!
3//! Security policy configuration for the permission system.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8/// Security level for permission policies
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10pub enum SecurityLevel {
11    /// Trust everything - no permission prompts
12    Trusted,
13    /// Default - prompt for potentially dangerous actions
14    #[default]
15    Standard,
16    /// Strict - prompt for all actions
17    Strict,
18    /// Paranoid - prompt for all actions and log everything
19    Paranoid,
20}
21
22/// Rule for allowing or denying actions
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct PermissionRule {
25    /// Pattern to match (glob pattern)
26    pub pattern: String,
27    /// Whether to allow or deny
28    pub allow: bool,
29    /// Optional description
30    pub description: Option<String>,
31}
32
33impl PermissionRule {
34    /// Create an allow rule
35    pub fn allow(pattern: impl Into<String>) -> Self {
36        Self {
37            pattern: pattern.into(),
38            allow: true,
39            description: None,
40        }
41    }
42
43    /// Create a deny rule
44    pub fn deny(pattern: impl Into<String>) -> Self {
45        Self {
46            pattern: pattern.into(),
47            allow: false,
48            description: None,
49        }
50    }
51
52    /// Add description
53    pub fn with_description(mut self, description: impl Into<String>) -> Self {
54        self.description = Some(description.into());
55        self
56    }
57
58    /// Check if a value matches this rule's pattern
59    pub fn matches(&self, value: &str) -> bool {
60        // Simple glob matching: * matches anything, ? matches single char
61        let pattern_parts: Vec<&str> = self.pattern.split('*').collect();
62        if pattern_parts.len() == 1 {
63            return value == self.pattern;
64        }
65
66        // Check prefix and suffix
67        if !value.starts_with(pattern_parts[0]) {
68            return false;
69        }
70        if !value.ends_with(pattern_parts.last().unwrap()) {
71            return false;
72        }
73
74        // Check middle parts in order
75        let mut search_start = pattern_parts[0].len();
76        for part in pattern_parts.iter().skip(1).take(pattern_parts.len() - 2) {
77            if let Some(pos) = value[search_start..].find(part) {
78                search_start += pos + part.len();
79            } else {
80                return false;
81            }
82        }
83
84        true
85    }
86}
87
88/// Permission policy configuration
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct PermissionPolicy {
91    /// Security level
92    pub level: SecurityLevel,
93    /// List of allowed rules
94    pub allow_rules: Vec<PermissionRule>,
95    /// List of denied rules
96    pub deny_rules: Vec<PermissionRule>,
97    /// Categories that are always allowed
98    pub trusted_categories: HashSet<String>,
99    /// Categories that are always denied
100    pub blocked_categories: HashSet<String>,
101    /// Paths that are trusted (read/write without prompts)
102    pub trusted_paths: HashSet<String>,
103    /// Paths that are blocked (never allowed)
104    pub blocked_paths: HashSet<String>,
105    /// URLs that are trusted (network requests without prompts)
106    pub trusted_urls: HashSet<String>,
107    /// URLs that are blocked
108    pub blocked_urls: HashSet<String>,
109    /// Commands that are trusted
110    pub trusted_commands: HashSet<String>,
111    /// Commands that are blocked
112    pub blocked_commands: HashSet<String>,
113    /// Whether to cache permission decisions
114    pub enable_cache: bool,
115    /// Cache expiration time in seconds
116    pub cache_expire_seconds: u64,
117    /// Whether to audit log all decisions
118    pub audit_enabled: bool,
119    /// Maximum audit entries to keep
120    pub max_audit_entries: usize,
121}
122
123impl Default for PermissionPolicy {
124    fn default() -> Self {
125        Self {
126            level: SecurityLevel::Standard,
127            allow_rules: Vec::new(),
128            deny_rules: Vec::new(),
129            trusted_categories: HashSet::new(),
130            blocked_categories: HashSet::new(),
131            trusted_paths: HashSet::new(),
132            blocked_paths: Self::default_blocked_paths(),
133            trusted_urls: HashSet::new(),
134            blocked_urls: HashSet::new(),
135            trusted_commands: HashSet::new(),
136            blocked_commands: Self::default_blocked_commands(),
137            enable_cache: true,
138            cache_expire_seconds: 3600, // 1 hour
139            audit_enabled: true,
140            max_audit_entries: 10000,
141        }
142    }
143}
144
145impl PermissionPolicy {
146    /// Create a trusted policy (no prompts)
147    pub fn trusted() -> Self {
148        Self {
149            level: SecurityLevel::Trusted,
150            ..Self::default()
151        }
152    }
153
154    /// Create a strict policy (prompt for everything)
155    pub fn strict() -> Self {
156        Self {
157            level: SecurityLevel::Strict,
158            ..Self::default()
159        }
160    }
161
162    /// Create a paranoid policy (prompt for everything, log everything)
163    pub fn paranoid() -> Self {
164        Self {
165            level: SecurityLevel::Paranoid,
166            audit_enabled: true,
167            ..Self::strict()
168        }
169    }
170
171    /// Default blocked paths (sensitive files)
172    fn default_blocked_paths() -> HashSet<String> {
173        let mut paths = HashSet::new();
174        // Environment files
175        paths.insert(".env".to_string());
176        paths.insert(".env.local".to_string());
177        paths.insert(".env.*.local".to_string());
178        // Credentials
179        paths.insert("**/credentials.json".to_string());
180        paths.insert("**/secrets.json".to_string());
181        paths.insert("**/api_keys.json".to_string());
182        // SSH keys
183        paths.insert("~/.ssh/id_rsa".to_string());
184        paths.insert("~/.ssh/id_ed25519".to_string());
185        // System sensitive files
186        paths.insert("/etc/shadow".to_string());
187        paths.insert("/etc/passwd".to_string());
188        paths
189    }
190
191    /// Default blocked commands
192    fn default_blocked_commands() -> HashSet<String> {
193        let mut commands = HashSet::new();
194        // Dangerous commands
195        commands.insert("rm -rf /".to_string());
196        commands.insert("rm -rf ~".to_string());
197        commands.insert("mkfs".to_string());
198        commands.insert("dd if=/dev/zero".to_string());
199        commands.insert(":(){ :|:& };:".to_string()); // Fork bomb
200        commands.insert("chmod 777".to_string());
201        commands
202    }
203
204    /// Add a trusted path
205    pub fn add_trusted_path(mut self, path: impl Into<String>) -> Self {
206        self.trusted_paths.insert(path.into());
207        self
208    }
209
210    /// Add a blocked path
211    pub fn add_blocked_path(mut self, path: impl Into<String>) -> Self {
212        self.blocked_paths.insert(path.into());
213        self
214    }
215
216    /// Add a trusted URL
217    pub fn add_trusted_url(mut self, url: impl Into<String>) -> Self {
218        self.trusted_urls.insert(url.into());
219        self
220    }
221
222    /// Add a blocked URL
223    pub fn add_blocked_url(mut self, url: impl Into<String>) -> Self {
224        self.blocked_urls.insert(url.into());
225        self
226    }
227
228    /// Add a trusted command
229    pub fn add_trusted_command(mut self, command: impl Into<String>) -> Self {
230        self.trusted_commands.insert(command.into());
231        self
232    }
233
234    /// Add a blocked command
235    pub fn add_blocked_command(mut self, command: impl Into<String>) -> Self {
236        self.blocked_commands.insert(command.into());
237        self
238    }
239
240    /// Check if a path is trusted
241    pub fn is_path_trusted(&self, path: &str) -> bool {
242        self.trusted_paths
243            .iter()
244            .any(|p| path.starts_with(p) || PermissionRule::allow(p.clone()).matches(path))
245    }
246
247    /// Check if a path is blocked
248    pub fn is_path_blocked(&self, path: &str) -> bool {
249        self.blocked_paths
250            .iter()
251            .any(|p| path.starts_with(p) || PermissionRule::deny(p.clone()).matches(path))
252    }
253
254    /// Check if an action should be auto-approved based on security level
255    pub fn should_auto_approve(&self, action_category: &str) -> bool {
256        match self.level {
257            SecurityLevel::Trusted => true,
258            SecurityLevel::Standard => self.trusted_categories.contains(action_category),
259            SecurityLevel::Strict | SecurityLevel::Paranoid => false,
260        }
261    }
262
263    /// Check if a category is blocked
264    pub fn is_category_blocked(&self, category: &str) -> bool {
265        self.blocked_categories.contains(category)
266    }
267
268    /// Load policy from a TOML file
269    pub fn load_from_file(path: &std::path::Path) -> anyhow::Result<Self> {
270        let content = std::fs::read_to_string(path)?;
271        let policy: Self = toml::from_str(&content)?;
272        Ok(policy)
273    }
274
275    /// Save policy to a TOML file
276    pub fn save_to_file(&self, path: &std::path::Path) -> anyhow::Result<()> {
277        let content = toml::to_string_pretty(self)?;
278        std::fs::write(path, content)?;
279        Ok(())
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_permission_rule_matching() {
289        let rule = PermissionRule::allow("/home/user/*.txt");
290        assert!(rule.matches("/home/user/test.txt"));
291        assert!(rule.matches("/home/user/another.txt"));
292        assert!(!rule.matches("/home/other/test.txt"));
293    }
294
295    #[test]
296    fn test_default_blocked_paths() {
297        let policy = PermissionPolicy::default();
298        assert!(policy.is_path_blocked(".env"));
299        assert!(policy.is_path_blocked("/etc/shadow"));
300    }
301
302    #[test]
303    fn test_security_level_auto_approve() {
304        let trusted_policy = PermissionPolicy::trusted();
305        assert!(trusted_policy.should_auto_approve("file_read"));
306
307        let strict_policy = PermissionPolicy::strict();
308        assert!(!strict_policy.should_auto_approve("file_read"));
309    }
310}