ggen_cli_validation/
security.rs

1//! Security and permission model for CLI operations
2//!
3//! Implements a permission-based security model for validating
4//! read, write, and execute operations.
5
6use crate::error::{Result, ValidationError};
7use std::path::{Path, PathBuf};
8
9/// Permission types for operations
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Permission {
12    /// Read permission
13    Read,
14    /// Write permission
15    Write,
16    /// Execute permission (for running commands)
17    Execute,
18}
19
20/// Permission model for validating operations
21#[derive(Debug, Clone)]
22pub struct PermissionModel {
23    /// Allowed read paths (patterns)
24    allowed_read_paths: Vec<PathBuf>,
25    /// Allowed write paths (patterns)
26    allowed_write_paths: Vec<PathBuf>,
27    /// Sandbox root (if enabled)
28    sandbox_root: Option<PathBuf>,
29    /// Environment variable restrictions
30    restricted_env_vars: Vec<String>,
31}
32
33impl Default for PermissionModel {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl PermissionModel {
40    /// Create a new permission model with permissive default settings
41    /// Allows all operations unless explicitly restricted by sandbox
42    #[must_use]
43    pub fn new() -> Self {
44        Self {
45            allowed_read_paths: vec![],  // Empty means allow all
46            allowed_write_paths: vec![], // Empty means allow all
47            sandbox_root: None,
48            restricted_env_vars: vec!["PATH".to_string(), "HOME".to_string(), "USER".to_string()],
49        }
50    }
51
52    /// Enable sandbox mode with a root directory
53    #[must_use]
54    pub fn with_sandbox(mut self, root: PathBuf) -> Self {
55        self.sandbox_root = Some(root);
56        self
57    }
58
59    /// Add allowed read path
60    #[must_use]
61    pub fn allow_read(mut self, path: PathBuf) -> Self {
62        self.allowed_read_paths.push(path);
63        self
64    }
65
66    /// Add allowed write path
67    #[must_use]
68    pub fn allow_write(mut self, path: PathBuf) -> Self {
69        self.allowed_write_paths.push(path);
70        self
71    }
72
73    /// Check if a path is allowed for the given permission
74    pub fn check_permission(&self, path: &Path, permission: Permission) -> Result<()> {
75        // First check path traversal
76        self.check_path_traversal(path)?;
77
78        // Check sandbox constraints
79        if let Some(sandbox_root) = &self.sandbox_root {
80            self.check_sandbox(path, sandbox_root)?;
81        }
82
83        // Check specific permission
84        match permission {
85            Permission::Read => self.check_read_permission(path),
86            Permission::Write => self.check_write_permission(path),
87            Permission::Execute => Ok(()), // Execute permissions handled separately
88        }
89    }
90
91    /// Check for path traversal attempts
92    fn check_path_traversal(&self, path: &Path) -> Result<()> {
93        let path_str = path.to_string_lossy();
94
95        // Check for dangerous path traversal patterns (../ going up directories)
96        if path_str.contains("../") || path_str.starts_with("..") {
97            return Err(ValidationError::PathTraversal {
98                path: path_str.to_string(),
99            });
100        }
101
102        Ok(())
103    }
104
105    /// Check sandbox constraints
106    fn check_sandbox(&self, path: &Path, sandbox_root: &Path) -> Result<()> {
107        let canonical_path = path.canonicalize().or_else(|_| {
108            // If path doesn't exist yet, check parent
109            path.parent()
110                .and_then(|p| p.canonicalize().ok())
111                .ok_or(ValidationError::InvalidPath {
112                    path: path.display().to_string(),
113                    reason: "Cannot canonicalize path".to_string(),
114                })
115        })?;
116
117        let canonical_root =
118            sandbox_root
119                .canonicalize()
120                .map_err(|e| ValidationError::InvalidPath {
121                    path: sandbox_root.display().to_string(),
122                    reason: format!("Cannot canonicalize sandbox root: {e}"),
123                })?;
124
125        if !canonical_path.starts_with(&canonical_root) {
126            return Err(ValidationError::SandboxViolation {
127                reason: format!(
128                    "Path {} is outside sandbox {}",
129                    canonical_path.display(),
130                    canonical_root.display()
131                ),
132            });
133        }
134
135        Ok(())
136    }
137
138    /// Check read permission for path
139    fn check_read_permission(&self, path: &Path) -> Result<()> {
140        if self.is_path_allowed(path, &self.allowed_read_paths) {
141            Ok(())
142        } else {
143            Err(ValidationError::PermissionDenied {
144                operation: "read".to_string(),
145                path: path.display().to_string(),
146            })
147        }
148    }
149
150    /// Check write permission for path
151    fn check_write_permission(&self, path: &Path) -> Result<()> {
152        if self.is_path_allowed(path, &self.allowed_write_paths) {
153            Ok(())
154        } else {
155            Err(ValidationError::PermissionDenied {
156                operation: "write".to_string(),
157                path: path.display().to_string(),
158            })
159        }
160    }
161
162    /// Check if path matches any allowed patterns
163    /// Empty allowed_paths means allow all (permissive default)
164    fn is_path_allowed(&self, path: &Path, allowed_paths: &[PathBuf]) -> bool {
165        if allowed_paths.is_empty() {
166            return true; // Permissive default
167        }
168        allowed_paths.iter().any(|allowed| {
169            // Simple prefix matching for now
170            path.starts_with(allowed)
171        })
172    }
173
174    /// Check if environment variable is restricted
175    #[must_use]
176    pub fn is_env_var_restricted(&self, var: &str) -> bool {
177        self.restricted_env_vars.contains(&var.to_string())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use std::env;
185
186    #[test]
187    fn test_default_permission_model() {
188        let model = PermissionModel::new();
189        assert!(model.sandbox_root.is_none());
190        // Permissive default - empty paths mean allow all
191        assert!(model.allowed_read_paths.is_empty());
192    }
193
194    #[test]
195    fn test_path_traversal_detection() {
196        let model = PermissionModel::new();
197        let traversal_path = Path::new("../../../etc/passwd");
198
199        let result = model.check_path_traversal(traversal_path);
200        assert!(result.is_err());
201    }
202
203    #[test]
204    fn test_sandbox_enforcement() {
205        let temp_dir = env::temp_dir();
206        let model = PermissionModel::new().with_sandbox(temp_dir.clone());
207
208        // Path inside sandbox should be allowed
209        let inside_path = temp_dir.join("test.txt");
210        assert!(model.check_sandbox(&inside_path, &temp_dir).is_ok());
211
212        // Path outside sandbox should be denied
213        let outside_path = Path::new("/etc/passwd");
214        assert!(model.check_sandbox(outside_path, &temp_dir).is_err());
215    }
216
217    #[test]
218    fn test_read_write_permissions() {
219        // Create restrictive model with specific allowed paths
220        let mut model = PermissionModel::new();
221        model.allowed_read_paths = vec![PathBuf::from("./src")];
222        model.allowed_write_paths = vec![PathBuf::from("./target")];
223
224        // Read permission - allowed
225        assert!(model
226            .check_permission(Path::new("./src/lib.rs"), Permission::Read)
227            .is_ok());
228
229        // Write permission - allowed
230        assert!(model
231            .check_permission(Path::new("./target/output"), Permission::Write)
232            .is_ok());
233
234        // Read permission - denied (not in allowed paths)
235        assert!(model
236            .check_permission(Path::new("./target/lib.rs"), Permission::Read)
237            .is_err());
238
239        // Write permission - denied (not in allowed paths)
240        assert!(model
241            .check_permission(Path::new("./src/output"), Permission::Write)
242            .is_err());
243    }
244
245    #[test]
246    fn test_env_var_restrictions() {
247        let model = PermissionModel::new();
248        assert!(model.is_env_var_restricted("PATH"));
249        assert!(model.is_env_var_restricted("HOME"));
250        assert!(!model.is_env_var_restricted("MY_CUSTOM_VAR"));
251    }
252}