1use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
12pub enum AccessLevel {
13 None,
15 #[default]
17 Read,
18 Write,
20 Admin,
22 Owner,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Permission {
29 pub action: PermissionAction,
31 pub path_pattern: Option<String>,
33 pub effect: PermissionEffect,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub enum PermissionAction {
40 Read,
42 Write,
44 Execute,
46 Tool(String),
48 Admin,
50 All,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub enum PermissionEffect {
57 Allow,
59 Deny,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, Default)]
65pub struct ProjectAccess {
66 pub default_level: AccessLevel,
68
69 pub deny_rules: Vec<Permission>,
71
72 pub protected_paths: Vec<String>,
74
75 pub restricted_tools: Vec<String>,
77}
78
79impl ProjectAccess {
80 pub fn new() -> Self {
82 ProjectAccess {
83 default_level: AccessLevel::Write,
84 deny_rules: Vec::new(),
85 protected_paths: vec![
86 ".env".to_string(),
87 ".env.*".to_string(),
88 "**/secrets/**".to_string(),
89 "**/*.pem".to_string(),
90 "**/*.key".to_string(),
91 "**/credentials*".to_string(),
92 ],
93 restricted_tools: vec![
94 "execute_command".to_string(), ],
96 }
97 }
98
99 pub fn can_access_path(&self, path: &Path, level: &AccessLevel) -> bool {
101 if *level == AccessLevel::Owner {
103 return true;
104 }
105
106 let path_str = path.to_string_lossy();
107
108 for pattern in &self.protected_paths {
110 if Self::matches_glob(pattern, &path_str) {
111 return false;
112 }
113 }
114
115 for rule in &self.deny_rules {
117 if rule.effect == PermissionEffect::Deny {
118 if let Some(ref pattern) = rule.path_pattern {
119 if Self::matches_glob(pattern, &path_str) {
120 return false;
121 }
122 }
123 }
124 }
125
126 true
127 }
128
129 pub fn can_use_tool(&self, tool: &str, level: &AccessLevel) -> bool {
131 if *level >= AccessLevel::Admin {
133 return true;
134 }
135
136 !self.restricted_tools.contains(&tool.to_string())
138 }
139
140 fn matches_glob(pattern: &str, path: &str) -> bool {
142 if pattern.contains("**") {
144 let parts: Vec<&str> = pattern.split("**").collect();
146 if parts.len() == 2 {
147 let prefix = parts[0].trim_end_matches('/');
148 let suffix = parts[1].trim_start_matches('/');
149
150 let prefix_ok = prefix.is_empty() || path.starts_with(prefix);
152
153 let suffix_ok = if suffix.is_empty() {
155 true
156 } else if suffix.starts_with('*') {
157 let ext = suffix.trim_start_matches('*');
159 path.ends_with(ext)
160 } else {
161 path.ends_with(suffix)
162 };
163
164 return prefix_ok && suffix_ok;
165 }
166 }
167
168 if pattern.contains('*') && !pattern.contains("**") {
170 let parts: Vec<&str> = pattern.split('*').collect();
171 if parts.len() == 2 {
172 return path.starts_with(parts[0]) && path.ends_with(parts[1]);
173 }
174 }
175
176 pattern == path
178 }
179
180 pub fn deny(&mut self, action: PermissionAction, path_pattern: Option<&str>) {
182 self.deny_rules.push(Permission {
183 action,
184 path_pattern: path_pattern.map(String::from),
185 effect: PermissionEffect::Deny,
186 });
187 }
188
189 pub fn protect_path(&mut self, pattern: &str) {
191 self.protected_paths.push(pattern.to_string());
192 }
193
194 pub fn restrict_tool(&mut self, tool: &str) {
196 if !self.restricted_tools.contains(&tool.to_string()) {
197 self.restricted_tools.push(tool.to_string());
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_default_access() {
208 let access = ProjectAccess::new();
209 assert_eq!(access.default_level, AccessLevel::Write);
210 }
211
212 #[test]
213 fn test_protected_paths() {
214 let access = ProjectAccess::new();
215 let level = AccessLevel::Write;
216
217 assert!(!access.can_access_path(Path::new(".env"), &level));
219 assert!(!access.can_access_path(Path::new(".env.production"), &level));
220
221 assert!(access.can_access_path(Path::new("src/main.rs"), &level));
223 }
224
225 #[test]
226 fn test_owner_bypasses_all() {
227 let access = ProjectAccess::new();
228 let level = AccessLevel::Owner;
229
230 assert!(access.can_access_path(Path::new(".env"), &level));
232 assert!(access.can_access_path(Path::new("secrets/api.key"), &level));
233 }
234
235 #[test]
236 fn test_glob_matching() {
237 assert!(ProjectAccess::matches_glob("*.rs", "main.rs"));
238 assert!(ProjectAccess::matches_glob("**/*.key", "secrets/api.key"));
239 assert!(ProjectAccess::matches_glob(".env.*", ".env.production"));
240 assert!(!ProjectAccess::matches_glob("*.rs", "main.py"));
241 }
242}