claude_code_acp/settings/
permission_checker.rs1use std::path::{Path, PathBuf};
6
7use super::manager::Settings;
8use super::rule::{ParsedRule, PermissionCheckResult};
9
10#[derive(Debug)]
12pub struct PermissionChecker {
13 settings: Settings,
15 cwd: PathBuf,
17 allow_rules: Vec<(String, ParsedRule)>,
19 deny_rules: Vec<(String, ParsedRule)>,
21 ask_rules: Vec<(String, ParsedRule)>,
23}
24
25impl PermissionChecker {
26 pub fn new(settings: Settings, cwd: impl AsRef<Path>) -> Self {
28 let cwd = cwd.as_ref().to_path_buf();
29
30 let allow_rules = Self::parse_rules(
32 settings.permissions.as_ref().and_then(|p| p.allow.as_ref()),
33 &cwd,
34 );
35 let deny_rules = Self::parse_rules(
36 settings.permissions.as_ref().and_then(|p| p.deny.as_ref()),
37 &cwd,
38 );
39 let ask_rules = Self::parse_rules(
40 settings.permissions.as_ref().and_then(|p| p.ask.as_ref()),
41 &cwd,
42 );
43
44 Self {
45 settings,
46 cwd,
47 allow_rules,
48 deny_rules,
49 ask_rules,
50 }
51 }
52
53 fn parse_rules(rules: Option<&Vec<String>>, cwd: &Path) -> Vec<(String, ParsedRule)> {
55 rules
56 .map(|rules| {
57 rules
58 .iter()
59 .map(|rule| (rule.clone(), ParsedRule::parse_with_glob(rule, cwd)))
60 .collect()
61 })
62 .unwrap_or_default()
63 }
64
65 pub fn check_permission(
71 &self,
72 tool_name: &str,
73 tool_input: &serde_json::Value,
74 ) -> PermissionCheckResult {
75 for (rule_str, parsed) in &self.deny_rules {
77 if parsed.matches(tool_name, tool_input, &self.cwd) {
78 tracing::debug!("Tool {} denied by rule: {}", tool_name, rule_str);
79 return PermissionCheckResult::deny(rule_str);
80 }
81 }
82
83 for (rule_str, parsed) in &self.allow_rules {
85 if parsed.matches(tool_name, tool_input, &self.cwd) {
86 tracing::debug!("Tool {} allowed by rule: {}", tool_name, rule_str);
87 return PermissionCheckResult::allow(rule_str);
88 }
89 }
90
91 for (rule_str, parsed) in &self.ask_rules {
93 if parsed.matches(tool_name, tool_input, &self.cwd) {
94 tracing::debug!(
95 "Tool {} requires permission (ask rule): {}",
96 tool_name,
97 rule_str
98 );
99 return PermissionCheckResult::ask_with_rule(rule_str);
100 }
101 }
102
103 tracing::debug!("Tool {} has no matching rule, defaulting to ask", tool_name);
105 PermissionCheckResult::ask()
106 }
107
108 pub fn settings(&self) -> &Settings {
110 &self.settings
111 }
112
113 pub fn cwd(&self) -> &Path {
115 &self.cwd
116 }
117
118 pub fn has_rules(&self) -> bool {
120 !self.allow_rules.is_empty() || !self.deny_rules.is_empty() || !self.ask_rules.is_empty()
121 }
122
123 pub fn add_allow_rule(&mut self, rule: &str) {
125 let parsed = ParsedRule::parse_with_glob(rule, &self.cwd);
126 self.allow_rules.push((rule.to_string(), parsed));
127 }
128
129 pub fn add_deny_rule(&mut self, rule: &str) {
131 let parsed = ParsedRule::parse_with_glob(rule, &self.cwd);
132 self.deny_rules.push((rule.to_string(), parsed));
133 }
134
135 pub fn default_mode(&self) -> Option<&str> {
137 self.settings
138 .permissions
139 .as_ref()
140 .and_then(|p| p.default_mode.as_deref())
141 }
142
143 pub fn additional_directories(&self) -> Option<&Vec<String>> {
145 self.settings
146 .permissions
147 .as_ref()
148 .and_then(|p| p.additional_directories.as_ref())
149 }
150}
151
152impl Default for PermissionChecker {
153 fn default() -> Self {
154 Self::new(Settings::default(), PathBuf::from("."))
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::settings::{PermissionDecision, PermissionSettings};
162 use serde_json::json;
163
164 fn settings_with_permissions(permissions: PermissionSettings) -> Settings {
165 Settings {
166 permissions: Some(permissions),
167 ..Default::default()
168 }
169 }
170
171 #[test]
172 fn test_empty_rules_default_to_ask() {
173 let checker = PermissionChecker::default();
174 let result = checker.check_permission("Read", &json!({"file_path": "/tmp/test.txt"}));
175
176 assert_eq!(result.decision, PermissionDecision::Ask);
177 assert!(result.rule.is_none());
178 }
179
180 #[test]
181 fn test_allow_rule() {
182 let permissions = PermissionSettings {
183 allow: Some(vec!["Read".to_string()]),
184 ..Default::default()
185 };
186 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
187
188 let result = checker.check_permission("Read", &json!({"file_path": "/tmp/test.txt"}));
189 assert_eq!(result.decision, PermissionDecision::Allow);
190 assert_eq!(result.rule, Some("Read".to_string()));
191 }
192
193 #[test]
194 fn test_deny_rule() {
195 let permissions = PermissionSettings {
196 deny: Some(vec!["Bash".to_string()]),
197 ..Default::default()
198 };
199 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
200
201 let result = checker.check_permission("Bash", &json!({"command": "rm -rf /"}));
202 assert_eq!(result.decision, PermissionDecision::Deny);
203 }
204
205 #[test]
206 fn test_deny_takes_priority_over_allow() {
207 let permissions = PermissionSettings {
208 allow: Some(vec!["Bash".to_string()]),
209 deny: Some(vec!["Bash".to_string()]),
210 ..Default::default()
211 };
212 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
213
214 let result = checker.check_permission("Bash", &json!({"command": "ls"}));
215 assert_eq!(result.decision, PermissionDecision::Deny);
216 }
217
218 #[test]
219 fn test_allow_takes_priority_over_ask() {
220 let permissions = PermissionSettings {
221 allow: Some(vec!["Read".to_string()]),
222 ask: Some(vec!["Read".to_string()]),
223 ..Default::default()
224 };
225 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
226
227 let result = checker.check_permission("Read", &json!({}));
228 assert_eq!(result.decision, PermissionDecision::Allow);
229 }
230
231 #[test]
232 fn test_bash_wildcard_rule() {
233 let permissions = PermissionSettings {
234 allow: Some(vec!["Bash(npm run:*)".to_string()]),
235 ..Default::default()
236 };
237 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
238
239 assert_eq!(
241 checker
242 .check_permission("Bash", &json!({"command": "npm run build"}))
243 .decision,
244 PermissionDecision::Allow
245 );
246
247 assert_eq!(
249 checker
250 .check_permission("Bash", &json!({"command": "npm install"}))
251 .decision,
252 PermissionDecision::Ask
253 );
254
255 assert_eq!(
257 checker
258 .check_permission("Bash", &json!({"command": "npm run build && rm -rf /"}))
259 .decision,
260 PermissionDecision::Ask
261 );
262 }
263
264 #[test]
265 fn test_read_group_matching() {
266 let permissions = PermissionSettings {
267 allow: Some(vec!["Read".to_string()]),
268 ..Default::default()
269 };
270 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
271
272 assert_eq!(
274 checker.check_permission("Read", &json!({})).decision,
275 PermissionDecision::Allow
276 );
277 assert_eq!(
278 checker.check_permission("Grep", &json!({})).decision,
279 PermissionDecision::Allow
280 );
281 assert_eq!(
282 checker.check_permission("Glob", &json!({})).decision,
283 PermissionDecision::Allow
284 );
285 assert_eq!(
286 checker.check_permission("LS", &json!({})).decision,
287 PermissionDecision::Allow
288 );
289
290 assert_eq!(
292 checker.check_permission("Write", &json!({})).decision,
293 PermissionDecision::Ask
294 );
295 }
296
297 #[test]
298 fn test_add_runtime_rule() {
299 let mut checker = PermissionChecker::default();
300
301 assert_eq!(
303 checker.check_permission("Read", &json!({})).decision,
304 PermissionDecision::Ask
305 );
306
307 checker.add_allow_rule("Read");
309
310 assert_eq!(
312 checker.check_permission("Read", &json!({})).decision,
313 PermissionDecision::Allow
314 );
315 }
316
317 #[test]
318 fn test_acp_prefix_stripped() {
319 let permissions = PermissionSettings {
320 allow: Some(vec!["Read".to_string()]),
321 ..Default::default()
322 };
323 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
324
325 assert_eq!(
327 checker.check_permission("Read", &json!({})).decision,
328 PermissionDecision::Allow
329 );
330 assert_eq!(
331 checker
332 .check_permission("mcp__acp__Read", &json!({}))
333 .decision,
334 PermissionDecision::Allow
335 );
336 }
337
338 #[test]
339 fn test_has_rules() {
340 let checker = PermissionChecker::default();
341 assert!(!checker.has_rules());
342
343 let permissions = PermissionSettings {
344 allow: Some(vec!["Read".to_string()]),
345 ..Default::default()
346 };
347 let checker = PermissionChecker::new(settings_with_permissions(permissions), "/tmp");
348 assert!(checker.has_rules());
349 }
350}