ggen_cli_validation/
security.rs1use crate::error::{Result, ValidationError};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Permission {
12 Read,
14 Write,
16 Execute,
18}
19
20#[derive(Debug, Clone)]
22pub struct PermissionModel {
23 allowed_read_paths: Vec<PathBuf>,
25 allowed_write_paths: Vec<PathBuf>,
27 sandbox_root: Option<PathBuf>,
29 restricted_env_vars: Vec<String>,
31}
32
33impl Default for PermissionModel {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl PermissionModel {
40 #[must_use]
43 pub fn new() -> Self {
44 Self {
45 allowed_read_paths: vec![], allowed_write_paths: vec![], sandbox_root: None,
48 restricted_env_vars: vec!["PATH".to_string(), "HOME".to_string(), "USER".to_string()],
49 }
50 }
51
52 #[must_use]
54 pub fn with_sandbox(mut self, root: PathBuf) -> Self {
55 self.sandbox_root = Some(root);
56 self
57 }
58
59 #[must_use]
61 pub fn allow_read(mut self, path: PathBuf) -> Self {
62 self.allowed_read_paths.push(path);
63 self
64 }
65
66 #[must_use]
68 pub fn allow_write(mut self, path: PathBuf) -> Self {
69 self.allowed_write_paths.push(path);
70 self
71 }
72
73 pub fn check_permission(&self, path: &Path, permission: Permission) -> Result<()> {
75 self.check_path_traversal(path)?;
77
78 if let Some(sandbox_root) = &self.sandbox_root {
80 self.check_sandbox(path, sandbox_root)?;
81 }
82
83 match permission {
85 Permission::Read => self.check_read_permission(path),
86 Permission::Write => self.check_write_permission(path),
87 Permission::Execute => Ok(()), }
89 }
90
91 fn check_path_traversal(&self, path: &Path) -> Result<()> {
93 let path_str = path.to_string_lossy();
94
95 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 fn check_sandbox(&self, path: &Path, sandbox_root: &Path) -> Result<()> {
107 let canonical_path = path.canonicalize().or_else(|_| {
108 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 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 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 fn is_path_allowed(&self, path: &Path, allowed_paths: &[PathBuf]) -> bool {
165 if allowed_paths.is_empty() {
166 return true; }
168 allowed_paths.iter().any(|allowed| {
169 path.starts_with(allowed)
171 })
172 }
173
174 #[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 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 let inside_path = temp_dir.join("test.txt");
210 assert!(model.check_sandbox(&inside_path, &temp_dir).is_ok());
211
212 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 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 assert!(model
226 .check_permission(Path::new("./src/lib.rs"), Permission::Read)
227 .is_ok());
228
229 assert!(model
231 .check_permission(Path::new("./target/output"), Permission::Write)
232 .is_ok());
233
234 assert!(model
236 .check_permission(Path::new("./target/lib.rs"), Permission::Read)
237 .is_err());
238
239 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}