mixtape_core/permission/
authorizer.rs1use super::grant::{hash_params, Grant};
4use super::store::{GrantStore, GrantStoreError, MemoryGrantStore};
5use serde_json::Value;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum ToolAuthorizationPolicy {
20 #[default]
25 AutoDeny,
26
27 Interactive,
35}
36
37pub struct ToolCallAuthorizer {
62 store: Box<dyn GrantStore>,
63 policy: ToolAuthorizationPolicy,
64}
65
66impl ToolCallAuthorizer {
67 pub fn new() -> Self {
69 Self {
70 store: Box::new(MemoryGrantStore::new()),
71 policy: ToolAuthorizationPolicy::default(),
72 }
73 }
74
75 pub fn interactive() -> Self {
80 Self::new().with_authorization_policy(ToolAuthorizationPolicy::Interactive)
81 }
82
83 pub fn with_store(store: impl GrantStore + 'static) -> Self {
85 Self {
86 store: Box::new(store),
87 policy: ToolAuthorizationPolicy::default(),
88 }
89 }
90
91 pub fn with_boxed_store(store: Box<dyn GrantStore>) -> Self {
93 Self {
94 store,
95 policy: ToolAuthorizationPolicy::default(),
96 }
97 }
98
99 pub fn with_authorization_policy(mut self, policy: ToolAuthorizationPolicy) -> Self {
115 self.policy = policy;
116 self
117 }
118
119 pub fn policy(&self) -> ToolAuthorizationPolicy {
121 self.policy
122 }
123
124 pub async fn grant_tool(&self, tool: &str) -> Result<(), GrantStoreError> {
126 self.store.save(Grant::tool(tool)).await
127 }
128
129 pub async fn grant_params(&self, tool: &str, params: &Value) -> Result<(), GrantStoreError> {
133 let hash = hash_params(params);
134 self.store.save(Grant::exact(tool, hash)).await
135 }
136
137 pub async fn grant_params_hash(
139 &self,
140 tool: &str,
141 params_hash: &str,
142 ) -> Result<(), GrantStoreError> {
143 self.store.save(Grant::exact(tool, params_hash)).await
144 }
145
146 pub async fn check(&self, tool_name: &str, params: &Value) -> Authorization {
153 let params_hash = hash_params(params);
154
155 match self.store.load(tool_name).await {
157 Ok(grants) => {
158 for grant in grants {
159 if grant.matches(¶ms_hash) {
160 return Authorization::Granted { grant };
161 }
162 }
163 }
164 Err(e) => {
165 eprintln!("Warning: Failed to load grants for {}: {}", tool_name, e);
166 }
167 }
168
169 match self.policy {
171 ToolAuthorizationPolicy::AutoDeny => Authorization::Denied {
172 reason: format!("No grant configured for tool '{}'", tool_name),
173 },
174 ToolAuthorizationPolicy::Interactive => Authorization::PendingApproval { params_hash },
175 }
176 }
177
178 pub async fn revoke(
182 &self,
183 tool: &str,
184 params_hash: Option<&str>,
185 ) -> Result<bool, GrantStoreError> {
186 self.store.delete(tool, params_hash).await
187 }
188
189 pub async fn grants(&self) -> Result<Vec<Grant>, GrantStoreError> {
191 self.store.load_all().await
192 }
193
194 pub async fn clear(&self) -> Result<(), GrantStoreError> {
196 self.store.clear().await
197 }
198}
199
200impl Default for ToolCallAuthorizer {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206#[derive(Debug, Clone)]
208pub enum Authorization {
209 Granted {
211 grant: Grant,
213 },
214 Denied {
216 reason: String,
218 },
219 PendingApproval {
221 params_hash: String,
223 },
224}
225
226impl Authorization {
227 pub fn is_authorized(&self) -> bool {
229 matches!(self, Authorization::Granted { .. })
230 }
231
232 pub fn is_denied(&self) -> bool {
234 matches!(self, Authorization::Denied { .. })
235 }
236
237 pub fn is_pending(&self) -> bool {
239 matches!(self, Authorization::PendingApproval { .. })
240 }
241}
242
243#[derive(Debug, Clone)]
245pub enum AuthorizationResponse {
246 Once,
248
249 Trust {
251 grant: Grant,
253 },
254
255 Deny {
257 reason: Option<String>,
259 },
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
269 fn test_default_policy_is_auto_deny() {
270 let auth = ToolCallAuthorizer::new();
271 assert_eq!(auth.policy(), ToolAuthorizationPolicy::AutoDeny);
272 }
273
274 #[test]
275 fn test_interactive_constructor_sets_interactive_policy() {
276 let auth = ToolCallAuthorizer::interactive();
277 assert_eq!(auth.policy(), ToolAuthorizationPolicy::Interactive);
278 }
279
280 #[test]
281 fn test_with_authorization_policy() {
282 let auth = ToolCallAuthorizer::new()
283 .with_authorization_policy(ToolAuthorizationPolicy::Interactive);
284 assert_eq!(auth.policy(), ToolAuthorizationPolicy::Interactive);
285 }
286
287 #[tokio::test]
288 async fn test_auto_deny_policy_returns_denied() {
289 let auth = ToolCallAuthorizer::new(); let params = serde_json::json!({"key": "value"});
292 let result = auth.check("test", ¶ms).await;
293
294 assert!(result.is_denied());
295 assert!(!result.is_authorized());
296 assert!(!result.is_pending());
297 }
298
299 #[tokio::test]
300 async fn test_interactive_policy_returns_pending() {
301 let auth = ToolCallAuthorizer::interactive(); let params = serde_json::json!({"key": "value"});
304 let result = auth.check("test", ¶ms).await;
305
306 assert!(result.is_pending());
307 assert!(!result.is_authorized());
308 assert!(!result.is_denied());
309 }
310
311 #[tokio::test]
312 async fn test_grant_overrides_policy() {
313 let auth = ToolCallAuthorizer::new();
315 auth.grant_tool("test").await.unwrap();
316
317 let result = auth.check("test", &serde_json::json!({})).await;
318 assert!(result.is_authorized());
319 }
320
321 #[tokio::test]
324 async fn test_authorizer_tool_wide_grant() {
325 let auth = ToolCallAuthorizer::new();
326 auth.grant_tool("test").await.unwrap();
327
328 let result = auth.check("test", &serde_json::json!({"a": 1})).await;
330 assert!(result.is_authorized());
331
332 let result = auth.check("test", &serde_json::json!({"b": 2})).await;
333 assert!(result.is_authorized());
334 }
335
336 #[tokio::test]
337 async fn test_authorizer_params_grant() {
338 let auth = ToolCallAuthorizer::new();
339
340 let params = serde_json::json!({"key": "value"});
341 auth.grant_params("test", ¶ms).await.unwrap();
342
343 let result = auth.check("test", ¶ms).await;
345 assert!(result.is_authorized());
346
347 let other = serde_json::json!({"key": "other"});
349 let result = auth.check("test", &other).await;
350 assert!(result.is_denied());
351 }
352
353 #[tokio::test]
354 async fn test_authorizer_wrong_tool() {
355 let auth = ToolCallAuthorizer::new();
356 auth.grant_tool("tool_a").await.unwrap();
357
358 let result = auth.check("tool_b", &serde_json::json!({})).await;
359 assert!(result.is_denied());
360 }
361
362 #[tokio::test]
363 async fn test_authorizer_revoke() {
364 let auth = ToolCallAuthorizer::new();
365 auth.grant_tool("test").await.unwrap();
366
367 assert!(auth
368 .check("test", &serde_json::json!({}))
369 .await
370 .is_authorized());
371
372 auth.revoke("test", None).await.unwrap();
373
374 assert!(auth.check("test", &serde_json::json!({})).await.is_denied());
375 }
376
377 #[tokio::test]
378 async fn test_authorizer_grants() {
379 let auth = ToolCallAuthorizer::new();
380 auth.grant_tool("a").await.unwrap();
381 auth.grant_tool("b").await.unwrap();
382
383 let grants = auth.grants().await.unwrap();
384 assert_eq!(grants.len(), 2);
385 }
386
387 #[tokio::test]
388 async fn test_authorizer_clear() {
389 let auth = ToolCallAuthorizer::new();
390 auth.grant_tool("test").await.unwrap();
391
392 auth.clear().await.unwrap();
393
394 assert!(auth.grants().await.unwrap().is_empty());
395 }
396
397 #[test]
400 fn test_authorization_methods() {
401 let granted = Authorization::Granted {
402 grant: Grant::tool("test"),
403 };
404 assert!(granted.is_authorized());
405 assert!(!granted.is_denied());
406 assert!(!granted.is_pending());
407
408 let denied = Authorization::Denied {
409 reason: "test".to_string(),
410 };
411 assert!(!denied.is_authorized());
412 assert!(denied.is_denied());
413 assert!(!denied.is_pending());
414
415 let pending = Authorization::PendingApproval {
416 params_hash: "abc".to_string(),
417 };
418 assert!(!pending.is_authorized());
419 assert!(!pending.is_denied());
420 assert!(pending.is_pending());
421 }
422}