1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13pub trait PermissionChecker: Send + Sync {
19 fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision;
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum PermissionDecision {
27 Allow,
29 Deny,
31 Ask,
33}
34
35#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
53pub struct PermissionRule {
54 pub rule: String,
56 #[serde(skip)]
58 tool_name: Option<String>,
59 #[serde(skip)]
61 arg_pattern: Option<String>,
62}
63
64impl<'de> Deserialize<'de> for PermissionRule {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: serde::Deserializer<'de>,
68 {
69 #[derive(Deserialize)]
71 #[serde(untagged)]
72 enum RuleRepr {
73 Plain(String),
74 Struct { rule: String },
75 }
76
77 let rule_str = match RuleRepr::deserialize(deserializer)? {
78 RuleRepr::Plain(s) => s,
79 RuleRepr::Struct { rule } => rule,
80 };
81 Ok(PermissionRule::new(&rule_str))
83 }
84}
85
86impl PermissionRule {
87 pub fn new(rule: &str) -> Self {
89 let (tool_name, arg_pattern) = Self::parse_rule(rule);
90 Self {
91 rule: rule.to_string(),
92 tool_name,
93 arg_pattern,
94 }
95 }
96
97 fn parse_rule(rule: &str) -> (Option<String>, Option<String>) {
99 if let Some(paren_start) = rule.find('(') {
101 if rule.ends_with(')') {
102 let tool_name = rule[..paren_start].to_string();
103 let pattern = rule[paren_start + 1..rule.len() - 1].to_string();
104 return (Some(tool_name), Some(pattern));
105 }
106 }
107 (Some(rule.to_string()), None)
109 }
110
111 pub fn matches(&self, tool_name: &str, args: &serde_json::Value) -> bool {
113 let rule_tool = match &self.tool_name {
115 Some(t) => t,
116 None => return false,
117 };
118
119 if !self.matches_tool_name(rule_tool, tool_name) {
120 return false;
121 }
122
123 let pattern = match &self.arg_pattern {
125 Some(p) => p,
126 None => return true,
127 };
128
129 self.matches_args(pattern, tool_name, args)
131 }
132
133 fn matches_tool_name(&self, rule_tool: &str, actual_tool: &str) -> bool {
135 if rule_tool.starts_with("mcp__") && actual_tool.starts_with("mcp__") {
137 if actual_tool.starts_with(rule_tool) {
139 return true;
140 }
141 }
142 rule_tool.eq_ignore_ascii_case(actual_tool)
143 }
144
145 fn matches_args(&self, pattern: &str, tool_name: &str, args: &serde_json::Value) -> bool {
147 if pattern == "*" {
149 return true;
150 }
151
152 let arg_string = self.build_arg_string(tool_name, args);
154
155 self.glob_match(pattern, &arg_string)
157 }
158
159 fn build_arg_string(&self, tool_name: &str, args: &serde_json::Value) -> String {
161 match tool_name.to_lowercase().as_str() {
162 "bash" => {
163 args.get("command")
165 .and_then(|v| v.as_str())
166 .unwrap_or("")
167 .to_string()
168 }
169 "read" | "write" | "edit" => {
170 args.get("file_path")
172 .and_then(|v| v.as_str())
173 .unwrap_or("")
174 .to_string()
175 }
176 "glob" => {
177 args.get("pattern")
179 .and_then(|v| v.as_str())
180 .unwrap_or("")
181 .to_string()
182 }
183 "grep" => {
184 let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
186 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
187 format!("{} {}", pattern, path)
188 }
189 "ls" => {
190 args.get("path")
192 .and_then(|v| v.as_str())
193 .unwrap_or("")
194 .to_string()
195 }
196 _ => {
197 serde_json::to_string(args).unwrap_or_default()
199 }
200 }
201 }
202
203 fn glob_match(&self, pattern: &str, text: &str) -> bool {
210 if let Some(prefix) = pattern.strip_suffix(":*") {
212 return text.starts_with(prefix);
213 }
214
215 let text = text.replace('\\', "/");
217
218 let regex_pattern = Self::glob_to_regex(pattern);
220 if let Ok(re) = regex::Regex::new(®ex_pattern) {
221 re.is_match(&text)
222 } else {
223 text.starts_with(pattern)
225 }
226 }
227
228 fn glob_to_regex(pattern: &str) -> String {
230 let mut regex = String::from("^");
231 let chars: Vec<char> = pattern.chars().collect();
232 let mut i = 0;
233
234 while i < chars.len() {
235 let c = chars[i];
236 match c {
237 '*' => {
238 if i + 1 < chars.len() && chars[i + 1] == '*' {
240 if i + 2 < chars.len() && chars[i + 2] == '/' {
243 regex.push_str(".*");
244 i += 3;
245 } else {
246 regex.push_str(".*");
247 i += 2;
248 }
249 } else {
250 regex.push_str("[^/\\\\]*");
252 i += 1;
253 }
254 }
255 '?' => {
256 regex.push_str("[^/\\\\]");
258 i += 1;
259 }
260 '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
261 regex.push('\\');
263 regex.push(c);
264 i += 1;
265 }
266 _ => {
267 regex.push(c);
268 i += 1;
269 }
270 }
271 }
272
273 regex.push('$');
274 regex
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct PermissionPolicy {
287 #[serde(default)]
289 pub deny: Vec<PermissionRule>,
290
291 #[serde(default)]
293 pub allow: Vec<PermissionRule>,
294
295 #[serde(default)]
297 pub ask: Vec<PermissionRule>,
298
299 #[serde(default = "default_decision")]
301 pub default_decision: PermissionDecision,
302
303 #[serde(default = "default_enabled")]
305 pub enabled: bool,
306}
307
308fn default_decision() -> PermissionDecision {
309 PermissionDecision::Ask
310}
311
312fn default_enabled() -> bool {
313 true
314}
315
316impl Default for PermissionPolicy {
317 fn default() -> Self {
318 Self {
319 deny: Vec::new(),
320 allow: Vec::new(),
321 ask: Vec::new(),
322 default_decision: PermissionDecision::Ask,
323 enabled: true,
324 }
325 }
326}
327
328impl PermissionPolicy {
329 pub fn new() -> Self {
331 Self::default()
332 }
333
334 pub fn permissive() -> Self {
336 Self {
337 deny: Vec::new(),
338 allow: Vec::new(),
339 ask: Vec::new(),
340 default_decision: PermissionDecision::Allow,
341 enabled: true,
342 }
343 }
344
345 pub fn strict() -> Self {
347 Self {
348 deny: Vec::new(),
349 allow: Vec::new(),
350 ask: Vec::new(),
351 default_decision: PermissionDecision::Ask,
352 enabled: true,
353 }
354 }
355
356 pub fn deny(mut self, rule: &str) -> Self {
358 self.deny.push(PermissionRule::new(rule));
359 self
360 }
361
362 pub fn allow(mut self, rule: &str) -> Self {
364 self.allow.push(PermissionRule::new(rule));
365 self
366 }
367
368 pub fn ask(mut self, rule: &str) -> Self {
370 self.ask.push(PermissionRule::new(rule));
371 self
372 }
373
374 pub fn deny_all(mut self, rules: &[&str]) -> Self {
376 for rule in rules {
377 self.deny.push(PermissionRule::new(rule));
378 }
379 self
380 }
381
382 pub fn allow_all(mut self, rules: &[&str]) -> Self {
384 for rule in rules {
385 self.allow.push(PermissionRule::new(rule));
386 }
387 self
388 }
389
390 pub fn ask_all(mut self, rules: &[&str]) -> Self {
392 for rule in rules {
393 self.ask.push(PermissionRule::new(rule));
394 }
395 self
396 }
397
398 pub fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision {
406 if !self.enabled {
407 return PermissionDecision::Allow;
408 }
409
410 for rule in &self.deny {
412 if rule.matches(tool_name, args) {
413 return PermissionDecision::Deny;
414 }
415 }
416
417 for rule in &self.allow {
419 if rule.matches(tool_name, args) {
420 return PermissionDecision::Allow;
421 }
422 }
423
424 for rule in &self.ask {
426 if rule.matches(tool_name, args) {
427 return PermissionDecision::Ask;
428 }
429 }
430
431 self.default_decision
433 }
434
435 pub fn is_allowed(&self, tool_name: &str, args: &serde_json::Value) -> bool {
437 matches!(self.check(tool_name, args), PermissionDecision::Allow)
438 }
439
440 pub fn is_denied(&self, tool_name: &str, args: &serde_json::Value) -> bool {
442 matches!(self.check(tool_name, args), PermissionDecision::Deny)
443 }
444
445 pub fn requires_confirmation(&self, tool_name: &str, args: &serde_json::Value) -> bool {
447 matches!(self.check(tool_name, args), PermissionDecision::Ask)
448 }
449
450 pub fn get_matching_rules(&self, tool_name: &str, args: &serde_json::Value) -> MatchingRules {
452 let mut result = MatchingRules::default();
453
454 for rule in &self.deny {
455 if rule.matches(tool_name, args) {
456 result.deny.push(rule.rule.clone());
457 }
458 }
459
460 for rule in &self.allow {
461 if rule.matches(tool_name, args) {
462 result.allow.push(rule.rule.clone());
463 }
464 }
465
466 for rule in &self.ask {
467 if rule.matches(tool_name, args) {
468 result.ask.push(rule.rule.clone());
469 }
470 }
471
472 result
473 }
474}
475
476impl PermissionChecker for PermissionPolicy {
477 fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision {
478 self.check(tool_name, args)
479 }
480}
481
482#[derive(Debug, Default, Clone)]
484pub struct MatchingRules {
485 pub deny: Vec<String>,
486 pub allow: Vec<String>,
487 pub ask: Vec<String>,
488}
489
490impl MatchingRules {
491 pub fn is_empty(&self) -> bool {
492 self.deny.is_empty() && self.allow.is_empty() && self.ask.is_empty()
493 }
494}
495
496#[derive(Debug)]
498pub struct PermissionManager {
499 global_policy: PermissionPolicy,
501 session_policies: HashMap<String, PermissionPolicy>,
503}
504
505impl Default for PermissionManager {
506 fn default() -> Self {
507 Self::new()
508 }
509}
510
511impl PermissionManager {
512 pub fn new() -> Self {
514 Self {
515 global_policy: PermissionPolicy::default(),
516 session_policies: HashMap::new(),
517 }
518 }
519
520 pub fn with_global_policy(policy: PermissionPolicy) -> Self {
522 Self {
523 global_policy: policy,
524 session_policies: HashMap::new(),
525 }
526 }
527
528 pub fn set_global_policy(&mut self, policy: PermissionPolicy) {
530 self.global_policy = policy;
531 }
532
533 pub fn global_policy(&self) -> &PermissionPolicy {
535 &self.global_policy
536 }
537
538 pub fn set_session_policy(&mut self, session_id: &str, policy: PermissionPolicy) {
540 self.session_policies.insert(session_id.to_string(), policy);
541 }
542
543 pub fn remove_session_policy(&mut self, session_id: &str) {
545 self.session_policies.remove(session_id);
546 }
547
548 pub fn get_effective_policy(&self, session_id: &str) -> &PermissionPolicy {
553 self.session_policies
554 .get(session_id)
555 .unwrap_or(&self.global_policy)
556 }
557
558 pub fn check(
560 &self,
561 session_id: &str,
562 tool_name: &str,
563 args: &serde_json::Value,
564 ) -> PermissionDecision {
565 let policy = self.get_effective_policy(session_id);
567
568 for rule in &policy.deny {
570 if rule.matches(tool_name, args) {
571 return PermissionDecision::Deny;
572 }
573 }
574
575 if !self.session_policies.contains_key(session_id) {
577 } else {
579 for rule in &self.global_policy.deny {
580 if rule.matches(tool_name, args) {
581 return PermissionDecision::Deny;
582 }
583 }
584 }
585
586 for rule in &policy.allow {
588 if rule.matches(tool_name, args) {
589 return PermissionDecision::Allow;
590 }
591 }
592
593 for rule in &policy.ask {
595 if rule.matches(tool_name, args) {
596 return PermissionDecision::Ask;
597 }
598 }
599
600 policy.default_decision
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use serde_json::json;
609
610 #[test]
615 fn test_rule_parse_simple() {
616 let rule = PermissionRule::new("Bash");
617 assert_eq!(rule.tool_name, Some("Bash".to_string()));
618 assert_eq!(rule.arg_pattern, None);
619 }
620
621 #[test]
622 fn test_rule_parse_with_pattern() {
623 let rule = PermissionRule::new("Bash(cargo:*)");
624 assert_eq!(rule.tool_name, Some("Bash".to_string()));
625 assert_eq!(rule.arg_pattern, Some("cargo:*".to_string()));
626 }
627
628 #[test]
629 fn test_rule_parse_wildcard() {
630 let rule = PermissionRule::new("Grep(*)");
631 assert_eq!(rule.tool_name, Some("Grep".to_string()));
632 assert_eq!(rule.arg_pattern, Some("*".to_string()));
633 }
634
635 #[test]
636 fn test_rule_match_tool_only() {
637 let rule = PermissionRule::new("Bash");
638 assert!(rule.matches("Bash", &json!({"command": "ls -la"})));
639 assert!(rule.matches("bash", &json!({"command": "echo hello"})));
640 assert!(!rule.matches("Read", &json!({})));
641 }
642
643 #[test]
644 fn test_rule_match_wildcard() {
645 let rule = PermissionRule::new("Grep(*)");
646 assert!(rule.matches("Grep", &json!({"pattern": "foo", "path": "/tmp"})));
647 assert!(rule.matches("grep", &json!({"pattern": "bar"})));
648 }
649
650 #[test]
651 fn test_rule_match_prefix_wildcard() {
652 let rule = PermissionRule::new("Bash(cargo:*)");
653 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
654 assert!(rule.matches("Bash", &json!({"command": "cargo test --lib"})));
655 assert!(rule.matches("Bash", &json!({"command": "cargo"})));
656 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
657 }
658
659 #[test]
660 fn test_rule_match_npm_commands() {
661 let rule = PermissionRule::new("Bash(npm run:*)");
662 assert!(rule.matches("Bash", &json!({"command": "npm run test"})));
663 assert!(rule.matches("Bash", &json!({"command": "npm run build"})));
664 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
665 }
666
667 #[test]
668 fn test_rule_match_file_path() {
669 let rule = PermissionRule::new("Read(src/*.rs)");
670 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
671 assert!(rule.matches("Read", &json!({"file_path": "src/lib.rs"})));
672 assert!(!rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
673 }
674
675 #[test]
676 fn test_rule_match_recursive_glob() {
677 let rule = PermissionRule::new("Read(src/**/*.rs)");
678 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
679 assert!(rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
680 assert!(rule.matches("Read", &json!({"file_path": "src/a/b/c.rs"})));
681 }
682
683 #[test]
684 fn test_rule_match_mcp_tool() {
685 let rule = PermissionRule::new("mcp__pencil");
686 assert!(rule.matches("mcp__pencil", &json!({})));
687 assert!(rule.matches("mcp__pencil__batch_design", &json!({})));
688 assert!(rule.matches("mcp__pencil__batch_get", &json!({})));
689 assert!(!rule.matches("mcp__other", &json!({})));
690 }
691
692 #[test]
693 fn test_rule_case_insensitive() {
694 let rule = PermissionRule::new("BASH(cargo:*)");
695 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
696 assert!(rule.matches("bash", &json!({"command": "cargo test"})));
697 assert!(rule.matches("BASH", &json!({"command": "cargo check"})));
698 }
699
700 #[test]
705 fn test_policy_default() {
706 let policy = PermissionPolicy::default();
707 assert!(policy.enabled);
708 assert_eq!(policy.default_decision, PermissionDecision::Ask);
709 assert!(policy.allow.is_empty());
710 assert!(policy.deny.is_empty());
711 assert!(policy.ask.is_empty());
712 }
713
714 #[test]
715 fn test_policy_permissive() {
716 let policy = PermissionPolicy::permissive();
717 assert_eq!(policy.default_decision, PermissionDecision::Allow);
718 }
719
720 #[test]
721 fn test_policy_strict() {
722 let policy = PermissionPolicy::strict();
723 assert_eq!(policy.default_decision, PermissionDecision::Ask);
724 }
725
726 #[test]
727 fn test_policy_builder() {
728 let policy = PermissionPolicy::new()
729 .allow("Bash(cargo:*)")
730 .allow("Grep(*)")
731 .deny("Bash(rm -rf:*)")
732 .ask("Write(*)");
733
734 assert_eq!(policy.allow.len(), 2);
735 assert_eq!(policy.deny.len(), 1);
736 assert_eq!(policy.ask.len(), 1);
737 }
738
739 #[test]
740 fn test_policy_check_allow() {
741 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
742
743 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
744 assert_eq!(decision, PermissionDecision::Allow);
745 }
746
747 #[test]
748 fn test_policy_check_deny() {
749 let policy = PermissionPolicy::new().deny("Bash(rm -rf:*)");
750
751 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
752 assert_eq!(decision, PermissionDecision::Deny);
753 }
754
755 #[test]
756 fn test_policy_check_ask() {
757 let policy = PermissionPolicy::new().ask("Write(*)");
758
759 let decision = policy.check("Write", &json!({"file_path": "/tmp/test.txt"}));
760 assert_eq!(decision, PermissionDecision::Ask);
761 }
762
763 #[test]
764 fn test_policy_check_default() {
765 let policy = PermissionPolicy::new();
766
767 let decision = policy.check("Unknown", &json!({}));
768 assert_eq!(decision, PermissionDecision::Ask);
769 }
770
771 #[test]
772 fn test_policy_deny_wins_over_allow() {
773 let policy = PermissionPolicy::new().allow("Bash(*)").deny("Bash(rm:*)");
774
775 let decision = policy.check("Bash", &json!({"command": "rm -rf /tmp"}));
777 assert_eq!(decision, PermissionDecision::Deny);
778
779 let decision = policy.check("Bash", &json!({"command": "ls -la"}));
781 assert_eq!(decision, PermissionDecision::Allow);
782 }
783
784 #[test]
785 fn test_policy_allow_wins_over_ask() {
786 let policy = PermissionPolicy::new()
787 .allow("Bash(cargo:*)")
788 .ask("Bash(*)");
789
790 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
792 assert_eq!(decision, PermissionDecision::Allow);
793
794 let decision = policy.check("Bash", &json!({"command": "npm install"}));
796 assert_eq!(decision, PermissionDecision::Ask);
797 }
798
799 #[test]
800 fn test_policy_disabled() {
801 let mut policy = PermissionPolicy::new().deny("Bash(rm:*)").ask("Bash(*)");
802 policy.enabled = false;
803
804 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
806 assert_eq!(decision, PermissionDecision::Allow);
807 }
808
809 #[test]
810 fn test_policy_is_allowed() {
811 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
812
813 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
814 assert!(!policy.is_allowed("Bash", &json!({"command": "npm install"})));
815 }
816
817 #[test]
818 fn test_policy_is_denied() {
819 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
820
821 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
822 assert!(!policy.is_denied("Bash", &json!({"command": "ls -la"})));
823 }
824
825 #[test]
826 fn test_policy_requires_confirmation() {
827 let mut policy = PermissionPolicy::new().allow("Read(*)").ask("Write(*)");
829 policy.default_decision = PermissionDecision::Deny; assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test"})));
832 assert!(!policy.requires_confirmation("Read", &json!({"file_path": "/tmp/test"})));
833 }
834
835 #[test]
836 fn test_policy_matching_rules() {
837 let policy = PermissionPolicy::new()
838 .allow("Bash(cargo:*)")
839 .deny("Bash(cargo fmt:*)")
840 .ask("Bash(*)");
841
842 let matching = policy.get_matching_rules("Bash", &json!({"command": "cargo fmt"}));
843 assert_eq!(matching.deny.len(), 1);
844 assert_eq!(matching.allow.len(), 1);
845 assert_eq!(matching.ask.len(), 1);
846 }
847
848 #[test]
849 fn test_policy_allow_all() {
850 let policy =
851 PermissionPolicy::new().allow_all(&["Bash(cargo:*)", "Bash(npm:*)", "Grep(*)"]);
852
853 assert_eq!(policy.allow.len(), 3);
854 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
855 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
856 assert!(policy.is_allowed("Grep", &json!({"pattern": "foo"})));
857 }
858
859 #[test]
864 fn test_rule_deserialize_plain_string() {
865 let rule: PermissionRule = serde_yaml::from_str("read").unwrap();
867 assert_eq!(rule.rule, "read");
868 assert!(rule.matches("read", &json!({})));
869 assert!(!rule.matches("write", &json!({})));
870 }
871
872 #[test]
873 fn test_rule_deserialize_plain_string_with_pattern() {
874 let rule: PermissionRule = serde_yaml::from_str("\"Bash(cargo:*)\"").unwrap();
876 assert_eq!(rule.rule, "Bash(cargo:*)");
877 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
878 }
879
880 #[test]
881 fn test_rule_deserialize_struct_form() {
882 let rule: PermissionRule = serde_yaml::from_str("rule: read").unwrap();
884 assert_eq!(rule.rule, "read");
885 assert!(rule.matches("read", &json!({})));
886 }
887
888 #[test]
889 fn test_rule_deserialize_in_policy() {
890 let yaml = r#"
892allow:
893 - read
894 - "Bash(cargo:*)"
895 - rule: grep
896deny:
897 - write
898"#;
899 let policy: PermissionPolicy = serde_yaml::from_str(yaml).unwrap();
900 assert_eq!(policy.allow.len(), 3);
901 assert_eq!(policy.deny.len(), 1);
902 assert!(policy.is_allowed("read", &json!({})));
903 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
904 assert!(policy.is_allowed("grep", &json!({})));
905 assert!(policy.is_denied("write", &json!({})));
906 }
907
908 #[test]
913 fn test_manager_default() {
914 let manager = PermissionManager::new();
915 assert_eq!(
916 manager.global_policy().default_decision,
917 PermissionDecision::Ask
918 );
919 }
920
921 #[test]
922 fn test_manager_with_global_policy() {
923 let policy = PermissionPolicy::permissive();
924 let manager = PermissionManager::with_global_policy(policy);
925 assert_eq!(
926 manager.global_policy().default_decision,
927 PermissionDecision::Allow
928 );
929 }
930
931 #[test]
932 fn test_manager_session_policy() {
933 let mut manager = PermissionManager::new();
934
935 let session_policy = PermissionPolicy::new().allow("Bash(cargo:*)");
936 manager.set_session_policy("session-1", session_policy);
937
938 let decision = manager.check("session-1", "Bash", &json!({"command": "cargo build"}));
940 assert_eq!(decision, PermissionDecision::Allow);
941
942 let decision = manager.check("session-2", "Bash", &json!({"command": "cargo build"}));
944 assert_eq!(decision, PermissionDecision::Ask);
945 }
946
947 #[test]
948 fn test_manager_remove_session_policy() {
949 let mut manager = PermissionManager::new();
950
951 let session_policy = PermissionPolicy::permissive();
952 manager.set_session_policy("session-1", session_policy);
953
954 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
956 assert_eq!(decision, PermissionDecision::Allow);
957
958 manager.remove_session_policy("session-1");
959
960 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
962 assert_eq!(decision, PermissionDecision::Ask);
963 }
964
965 #[test]
966 fn test_manager_global_deny_overrides_session_allow() {
967 let mut manager =
968 PermissionManager::with_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
969
970 let session_policy = PermissionPolicy::new().allow("Bash(*)");
971 manager.set_session_policy("session-1", session_policy);
972
973 let decision = manager.check("session-1", "Bash", &json!({"command": "rm -rf /"}));
975 assert_eq!(decision, PermissionDecision::Deny);
976
977 let decision = manager.check("session-1", "Bash", &json!({"command": "ls -la"}));
979 assert_eq!(decision, PermissionDecision::Allow);
980 }
981
982 #[test]
987 fn test_realistic_dev_policy() {
988 let policy = PermissionPolicy::new()
989 .allow_all(&[
991 "Bash(cargo:*)",
992 "Bash(npm:*)",
993 "Bash(pnpm:*)",
994 "Bash(just:*)",
995 "Bash(git status:*)",
996 "Bash(git diff:*)",
997 "Bash(echo:*)",
998 "Grep(*)",
999 "Glob(*)",
1000 "Ls(*)",
1001 ])
1002 .deny_all(&["Bash(rm -rf:*)", "Bash(sudo:*)", "Bash(curl | sh:*)"])
1004 .ask_all(&["Write(*)", "Edit(*)"]);
1006
1007 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
1009 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
1010 assert!(policy.is_allowed("Grep", &json!({"pattern": "TODO"})));
1011
1012 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
1014 assert!(policy.is_denied("Bash", &json!({"command": "sudo apt install"})));
1015
1016 assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test.rs"})));
1018 assert!(policy.requires_confirmation("Edit", &json!({"file_path": "src/main.rs"})));
1019 }
1020
1021 #[test]
1022 fn test_mcp_tool_permissions() {
1023 let policy = PermissionPolicy::new()
1024 .allow("mcp__pencil")
1025 .deny("mcp__dangerous");
1026
1027 assert!(policy.is_allowed("mcp__pencil__batch_design", &json!({})));
1028 assert!(policy.is_allowed("mcp__pencil__batch_get", &json!({})));
1029 assert!(policy.is_denied("mcp__dangerous__execute", &json!({})));
1030 }
1031
1032 #[test]
1033 fn test_serialization() {
1034 let policy = PermissionPolicy::new()
1035 .allow("Bash(cargo:*)")
1036 .deny("Bash(rm:*)");
1037
1038 let json = serde_json::to_string(&policy).unwrap();
1039 let deserialized: PermissionPolicy = serde_json::from_str(&json).unwrap();
1040
1041 assert_eq!(deserialized.allow.len(), 1);
1042 assert_eq!(deserialized.deny.len(), 1);
1043 }
1044
1045 #[test]
1046 fn test_matching_rules_is_empty() {
1047 let rules = MatchingRules {
1048 deny: vec![],
1049 allow: vec![],
1050 ask: vec![],
1051 };
1052 assert!(rules.is_empty());
1053
1054 let rules = MatchingRules {
1055 deny: vec!["Bash".to_string()],
1056 allow: vec![],
1057 ask: vec![],
1058 };
1059 assert!(!rules.is_empty());
1060
1061 let rules = MatchingRules {
1062 deny: vec![],
1063 allow: vec!["Read".to_string()],
1064 ask: vec![],
1065 };
1066 assert!(!rules.is_empty());
1067
1068 let rules = MatchingRules {
1069 deny: vec![],
1070 allow: vec![],
1071 ask: vec!["Write".to_string()],
1072 };
1073 assert!(!rules.is_empty());
1074 }
1075
1076 #[test]
1077 fn test_permission_manager_default() {
1078 let pm = PermissionManager::default();
1079 let policy = pm.global_policy();
1080 assert!(policy.allow.is_empty());
1081 assert!(policy.deny.is_empty());
1082 assert!(policy.ask.is_empty());
1083 }
1084
1085 #[test]
1086 fn test_permission_manager_set_global_policy() {
1087 let mut pm = PermissionManager::new();
1088 let policy = PermissionPolicy::new().allow("Bash(*)");
1089 pm.set_global_policy(policy);
1090 assert_eq!(pm.global_policy().allow.len(), 1);
1091 }
1092
1093 #[test]
1094 fn test_permission_manager_session_policy() {
1095 let mut pm = PermissionManager::new();
1096 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
1097 pm.set_session_policy("s1", policy);
1098
1099 let effective = pm.get_effective_policy("s1");
1100 assert_eq!(effective.deny.len(), 1);
1101
1102 let global = pm.get_effective_policy("s2");
1104 assert!(global.deny.is_empty());
1105 }
1106
1107 #[test]
1108 fn test_permission_manager_remove_session_policy() {
1109 let mut pm = PermissionManager::new();
1110 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(*)"));
1111 assert_eq!(pm.get_effective_policy("s1").deny.len(), 1);
1112
1113 pm.remove_session_policy("s1");
1114 assert!(pm.get_effective_policy("s1").deny.is_empty());
1115 }
1116
1117 #[test]
1118 fn test_permission_manager_check_deny() {
1119 let mut pm = PermissionManager::new();
1120 pm.set_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
1121
1122 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1123 assert_eq!(decision, PermissionDecision::Deny);
1124 }
1125
1126 #[test]
1127 fn test_permission_manager_check_allow() {
1128 let mut pm = PermissionManager::new();
1129 pm.set_global_policy(PermissionPolicy::new().allow("Bash(cargo:*)"));
1130
1131 let decision = pm.check("s1", "Bash", &json!({"command": "cargo build"}));
1132 assert_eq!(decision, PermissionDecision::Allow);
1133 }
1134
1135 #[test]
1136 fn test_permission_manager_check_session_override() {
1137 let mut pm = PermissionManager::new();
1138 pm.set_global_policy(PermissionPolicy::new().allow("Bash(*)"));
1139 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(rm:*)"));
1140
1141 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1143 assert_eq!(decision, PermissionDecision::Deny);
1144
1145 let decision = pm.check("s2", "Bash", &json!({"command": "rm -rf /"}));
1147 assert_eq!(decision, PermissionDecision::Allow);
1148 }
1149
1150 #[test]
1151 fn test_permission_manager_with_global_policy() {
1152 let policy = PermissionPolicy::new().allow("Read(*)").deny("Write(*)");
1153 let pm = PermissionManager::with_global_policy(policy);
1154 assert_eq!(pm.global_policy().allow.len(), 1);
1155 assert_eq!(pm.global_policy().deny.len(), 1);
1156 }
1157}