1use crate::error::{Error, Result};
4use ricecoder_permissions::{GlobMatcher, PermissionLevel};
5use std::collections::HashMap;
6use tracing::{debug, warn};
7
8#[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#[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#[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 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 pub fn add_global_rule(&mut self, rule: PermissionRule) -> Result<()> {
55 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 pub fn add_agent_rule(&mut self, agent_id: String, rule: PermissionRule) -> Result<()> {
66 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 pub fn check_permission(
80 &self,
81 tool_id: &str,
82 agent_id: Option<&str>,
83 ) -> Result<PermissionLevel> {
84 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 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 warn!("No permission rule found for tool '{}', defaulting to deny", tool_id);
105 Ok(PermissionLevel::Deny)
106 }
107
108 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 pub fn get_global_rules(&self) -> &[PermissionRule] {
121 &self.global_rules
122 }
123
124 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 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 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 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 let result = manager.check_permission("file-read", Some("file-manager"));
252 assert!(result.is_ok());
253 assert_eq!(result.unwrap(), PermissionLevel::Allow);
254
255 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 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}