ricecoder_mcp/
permissions.rs

1//! Permission Manager integration for MCP tools
2
3use crate::error::{Error, Result};
4use ricecoder_permissions::{GlobMatcher, PermissionLevel};
5use std::collections::HashMap;
6use tracing::{debug, warn};
7
8/// Permission rule for tool access control
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct PermissionRule {
11    pub pattern: String,
12    pub level: PermissionLevelConfig,
13    pub agent_id: Option<String>,
14}
15
16/// Permission level configuration
17#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum PermissionLevelConfig {
20    Allow,
21    Ask,
22    Deny,
23}
24
25impl From<PermissionLevelConfig> for PermissionLevel {
26    fn from(level: PermissionLevelConfig) -> Self {
27        match level {
28            PermissionLevelConfig::Allow => PermissionLevel::Allow,
29            PermissionLevelConfig::Ask => PermissionLevel::Ask,
30            PermissionLevelConfig::Deny => PermissionLevel::Deny,
31        }
32    }
33}
34
35/// MCP Permission Manager for controlling tool access
36#[derive(Debug, Clone)]
37pub struct MCPPermissionManager {
38    global_rules: Vec<PermissionRule>,
39    agent_rules: HashMap<String, Vec<PermissionRule>>,
40    glob_matcher: GlobMatcher,
41}
42
43impl MCPPermissionManager {
44    /// Creates a new MCP Permission Manager
45    pub fn new() -> Self {
46        Self {
47            global_rules: Vec::new(),
48            agent_rules: HashMap::new(),
49            glob_matcher: GlobMatcher::new(),
50        }
51    }
52
53    /// Adds a global permission rule
54    pub fn add_global_rule(&mut self, rule: PermissionRule) -> Result<()> {
55        // Validate the pattern
56        self.glob_matcher
57            .validate_pattern(&rule.pattern)
58            .map_err(|e| Error::ValidationError(format!("Invalid pattern: {}", e)))?;
59
60        self.global_rules.push(rule);
61        Ok(())
62    }
63
64    /// Adds a per-agent permission rule
65    pub fn add_agent_rule(&mut self, agent_id: String, rule: PermissionRule) -> Result<()> {
66        // Validate the pattern
67        self.glob_matcher
68            .validate_pattern(&rule.pattern)
69            .map_err(|e| Error::ValidationError(format!("Invalid pattern: {}", e)))?;
70
71        self.agent_rules
72            .entry(agent_id)
73            .or_insert_with(Vec::new)
74            .push(rule);
75        Ok(())
76    }
77
78    /// Checks permission for a tool execution
79    pub fn check_permission(
80        &self,
81        tool_id: &str,
82        agent_id: Option<&str>,
83    ) -> Result<PermissionLevel> {
84        // Check per-agent rules first (higher priority)
85        if let Some(agent_id) = agent_id {
86            if let Some(rules) = self.agent_rules.get(agent_id) {
87                if let Some(level) = self.match_rules(tool_id, rules)? {
88                    debug!(
89                        "Tool '{}' permission for agent '{}': {:?}",
90                        tool_id, agent_id, level
91                    );
92                    return Ok(level);
93                }
94            }
95        }
96
97        // Check global rules
98        if let Some(level) = self.match_rules(tool_id, &self.global_rules)? {
99            debug!("Tool '{}' global permission: {:?}", tool_id, level);
100            return Ok(level);
101        }
102
103        // Default to deny if no rule matches
104        warn!("No permission rule found for tool '{}', defaulting to deny", tool_id);
105        Ok(PermissionLevel::Deny)
106    }
107
108    /// Matches a tool ID against a set of rules
109    fn match_rules(&self, tool_id: &str, rules: &[PermissionRule]) -> Result<Option<PermissionLevel>> {
110        for rule in rules {
111            if self.glob_matcher.match_pattern(&rule.pattern, tool_id) {
112                let level = PermissionLevelConfig::clone(&rule.level).into();
113                return Ok(Some(level));
114            }
115        }
116        Ok(None)
117    }
118
119    /// Gets all global rules
120    pub fn get_global_rules(&self) -> &[PermissionRule] {
121        &self.global_rules
122    }
123
124    /// Gets all agent rules for a specific agent
125    pub fn get_agent_rules(&self, agent_id: &str) -> Option<&[PermissionRule]> {
126        self.agent_rules.get(agent_id).map(|v| v.as_slice())
127    }
128
129    /// Clears all rules
130    pub fn clear_rules(&mut self) {
131        self.global_rules.clear();
132        self.agent_rules.clear();
133    }
134}
135
136impl Default for MCPPermissionManager {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_create_permission_manager() {
148        let manager = MCPPermissionManager::new();
149        assert!(manager.get_global_rules().is_empty());
150    }
151
152    #[test]
153    fn test_add_global_rule() {
154        let mut manager = MCPPermissionManager::new();
155        let rule = PermissionRule {
156            pattern: "database-*".to_string(),
157            level: PermissionLevelConfig::Allow,
158            agent_id: None,
159        };
160
161        let result = manager.add_global_rule(rule);
162        assert!(result.is_ok());
163        assert_eq!(manager.get_global_rules().len(), 1);
164    }
165
166    #[test]
167    fn test_add_agent_rule() {
168        let mut manager = MCPPermissionManager::new();
169        let rule = PermissionRule {
170            pattern: "code-*".to_string(),
171            level: PermissionLevelConfig::Allow,
172            agent_id: Some("code-analyzer".to_string()),
173        };
174
175        let result = manager.add_agent_rule("code-analyzer".to_string(), rule);
176        assert!(result.is_ok());
177        assert!(manager.get_agent_rules("code-analyzer").is_some());
178    }
179
180    #[test]
181    fn test_check_permission_allow() {
182        let mut manager = MCPPermissionManager::new();
183        let rule = PermissionRule {
184            pattern: "database-*".to_string(),
185            level: PermissionLevelConfig::Allow,
186            agent_id: None,
187        };
188
189        manager.add_global_rule(rule).unwrap();
190
191        let result = manager.check_permission("database-query", None);
192        assert!(result.is_ok());
193        assert_eq!(result.unwrap(), PermissionLevel::Allow);
194    }
195
196    #[test]
197    fn test_check_permission_deny() {
198        let mut manager = MCPPermissionManager::new();
199        let rule = PermissionRule {
200            pattern: "dangerous-*".to_string(),
201            level: PermissionLevelConfig::Deny,
202            agent_id: None,
203        };
204
205        manager.add_global_rule(rule).unwrap();
206
207        let result = manager.check_permission("dangerous-operation", None);
208        assert!(result.is_ok());
209        assert_eq!(result.unwrap(), PermissionLevel::Deny);
210    }
211
212    #[test]
213    fn test_check_permission_ask() {
214        let mut manager = MCPPermissionManager::new();
215        let rule = PermissionRule {
216            pattern: "api-*".to_string(),
217            level: PermissionLevelConfig::Ask,
218            agent_id: None,
219        };
220
221        manager.add_global_rule(rule).unwrap();
222
223        let result = manager.check_permission("api-call", None);
224        assert!(result.is_ok());
225        assert_eq!(result.unwrap(), PermissionLevel::Ask);
226    }
227
228    #[test]
229    fn test_per_agent_override() {
230        let mut manager = MCPPermissionManager::new();
231
232        // Global rule: deny
233        let global_rule = PermissionRule {
234            pattern: "file-*".to_string(),
235            level: PermissionLevelConfig::Deny,
236            agent_id: None,
237        };
238        manager.add_global_rule(global_rule).unwrap();
239
240        // Per-agent rule: allow
241        let agent_rule = PermissionRule {
242            pattern: "file-*".to_string(),
243            level: PermissionLevelConfig::Allow,
244            agent_id: Some("file-manager".to_string()),
245        };
246        manager
247            .add_agent_rule("file-manager".to_string(), agent_rule)
248            .unwrap();
249
250        // Check permission for agent - should use per-agent rule
251        let result = manager.check_permission("file-read", Some("file-manager"));
252        assert!(result.is_ok());
253        assert_eq!(result.unwrap(), PermissionLevel::Allow);
254
255        // Check permission for other agent - should use global rule
256        let result = manager.check_permission("file-read", Some("other-agent"));
257        assert!(result.is_ok());
258        assert_eq!(result.unwrap(), PermissionLevel::Deny);
259    }
260
261    #[test]
262    fn test_default_deny() {
263        let manager = MCPPermissionManager::new();
264
265        // No rules defined - should default to deny
266        let result = manager.check_permission("unknown-tool", None);
267        assert!(result.is_ok());
268        assert_eq!(result.unwrap(), PermissionLevel::Deny);
269    }
270
271    #[test]
272    fn test_clear_rules() {
273        let mut manager = MCPPermissionManager::new();
274        let rule = PermissionRule {
275            pattern: "test-*".to_string(),
276            level: PermissionLevelConfig::Allow,
277            agent_id: None,
278        };
279
280        manager.add_global_rule(rule).unwrap();
281        assert_eq!(manager.get_global_rules().len(), 1);
282
283        manager.clear_rules();
284        assert!(manager.get_global_rules().is_empty());
285    }
286}