1use car_ir::{Action, ActionType, ToolSchema};
8use car_policy::PolicyEngine;
9use car_state::StateStore;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum AuthzStage {
17 ToolExists,
19 Capability,
21 Permission,
23 Restriction,
25 Policy,
27 Validation,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum AuthzDecision {
35 Allow,
37 AskUser,
39 Deny,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct AuthzResult {
46 pub decision: AuthzDecision,
48 pub stage: AuthzStage,
50 pub reason_code: String,
52 pub explanation: String,
54 pub stage_results: Vec<StageResult>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct StageResult {
61 pub stage: AuthzStage,
62 pub decision: AuthzDecision,
63 pub reason: String,
64}
65
66impl AuthzResult {
67 pub fn allowed(stage: AuthzStage) -> Self {
68 Self {
69 decision: AuthzDecision::Allow,
70 stage,
71 reason_code: "allowed".to_string(),
72 explanation: "All authorization checks passed".to_string(),
73 stage_results: Vec::new(),
74 }
75 }
76
77 pub fn denied(stage: AuthzStage, reason_code: &str, explanation: &str) -> Self {
78 Self {
79 decision: AuthzDecision::Deny,
80 stage,
81 reason_code: reason_code.to_string(),
82 explanation: explanation.to_string(),
83 stage_results: Vec::new(),
84 }
85 }
86
87 pub fn ask_user(stage: AuthzStage, reason_code: &str, explanation: &str) -> Self {
88 Self {
89 decision: AuthzDecision::AskUser,
90 stage,
91 reason_code: reason_code.to_string(),
92 explanation: explanation.to_string(),
93 stage_results: Vec::new(),
94 }
95 }
96
97 fn with_stages(mut self, stages: Vec<StageResult>) -> Self {
98 self.stage_results = stages;
99 self
100 }
101}
102
103pub struct Restriction {
105 pub name: String,
106 pub description: String,
107 check: Box<dyn Fn(&Action) -> Option<String> + Send + Sync>,
108}
109
110impl Restriction {
111 pub fn new<F>(name: &str, description: &str, check: F) -> Self
112 where
113 F: Fn(&Action) -> Option<String> + Send + Sync + 'static,
114 {
115 Self {
116 name: name.to_string(),
117 description: description.to_string(),
118 check: Box::new(check),
119 }
120 }
121
122 fn check(&self, action: &Action) -> Option<String> {
123 (self.check)(action)
124 }
125}
126
127#[async_trait::async_trait]
130pub trait PermissionHandler: Send + Sync {
131 async fn check(&self, tool_name: &str, action: &Action) -> AuthzDecision;
133}
134
135pub struct AllowAllPermissions;
137
138#[async_trait::async_trait]
139impl PermissionHandler for AllowAllPermissions {
140 async fn check(&self, _tool_name: &str, _action: &Action) -> AuthzDecision {
141 AuthzDecision::Allow
142 }
143}
144
145pub struct AuthzPipeline {
147 restrictions: Vec<Restriction>,
148 permission_handler: Box<dyn PermissionHandler>,
149}
150
151impl AuthzPipeline {
152 pub fn new() -> Self {
153 Self {
154 restrictions: Vec::new(),
155 permission_handler: Box::new(AllowAllPermissions),
156 }
157 }
158
159 pub fn add_restriction(&mut self, restriction: Restriction) {
161 self.restrictions.push(restriction);
162 }
163
164 pub fn set_permission_handler(&mut self, handler: Box<dyn PermissionHandler>) {
166 self.permission_handler = handler;
167 }
168
169 pub async fn authorize(
179 &self,
180 action: &Action,
181 tools: &HashMap<String, ToolSchema>,
182 capabilities: Option<&crate::capabilities::CapabilitySet>,
183 policies: &PolicyEngine,
184 state: &StateStore,
185 ) -> AuthzResult {
186 let mut stages = Vec::new();
187
188 if let Some(tool_name) = &action.tool {
190 if action.action_type == ActionType::ToolCall && !tools.contains_key(tool_name) {
191 stages.push(StageResult {
192 stage: AuthzStage::ToolExists,
193 decision: AuthzDecision::Deny,
194 reason: format!("tool '{}' not registered", tool_name),
195 });
196 return AuthzResult::denied(
197 AuthzStage::ToolExists,
198 "tool_not_found",
199 &format!("Tool '{}' is not registered", tool_name),
200 )
201 .with_stages(stages);
202 }
203 }
204 stages.push(StageResult {
205 stage: AuthzStage::ToolExists,
206 decision: AuthzDecision::Allow,
207 reason: "tool registered".to_string(),
208 });
209
210 if let Some(caps) = capabilities {
212 if let Some(tool_name) = &action.tool {
213 if !caps.tool_allowed(tool_name) {
214 stages.push(StageResult {
215 stage: AuthzStage::Capability,
216 decision: AuthzDecision::Deny,
217 reason: format!("tool '{}' not in capability set", tool_name),
218 });
219 return AuthzResult::denied(
220 AuthzStage::Capability,
221 "capability_denied",
222 &format!("Tool '{}' denied by capability set", tool_name),
223 )
224 .with_stages(stages);
225 }
226 }
227 }
228 stages.push(StageResult {
229 stage: AuthzStage::Capability,
230 decision: AuthzDecision::Allow,
231 reason: "capability check passed".to_string(),
232 });
233
234 if let Some(tool_name) = &action.tool {
236 let perm = self.permission_handler.check(tool_name, action).await;
237 stages.push(StageResult {
238 stage: AuthzStage::Permission,
239 decision: perm,
240 reason: format!("permission handler returned {:?}", perm),
241 });
242 if perm == AuthzDecision::Deny {
243 return AuthzResult::denied(
244 AuthzStage::Permission,
245 "permission_denied",
246 &format!("Permission denied for tool '{}'", tool_name),
247 )
248 .with_stages(stages);
249 }
250 if perm == AuthzDecision::AskUser {
251 return AuthzResult::ask_user(
252 AuthzStage::Permission,
253 "approval_required",
254 &format!("Tool '{}' requires user approval", tool_name),
255 )
256 .with_stages(stages);
257 }
258 } else {
259 stages.push(StageResult {
260 stage: AuthzStage::Permission,
261 decision: AuthzDecision::Allow,
262 reason: "no tool name, skipped".to_string(),
263 });
264 }
265
266 for restriction in &self.restrictions {
268 if let Some(reason) = restriction.check(action) {
269 stages.push(StageResult {
270 stage: AuthzStage::Restriction,
271 decision: AuthzDecision::Deny,
272 reason: reason.clone(),
273 });
274 return AuthzResult::denied(
275 AuthzStage::Restriction,
276 &format!("restriction_{}", restriction.name),
277 &format!("Permanent restriction '{}': {}", restriction.name, reason),
278 )
279 .with_stages(stages);
280 }
281 }
282 stages.push(StageResult {
283 stage: AuthzStage::Restriction,
284 decision: AuthzDecision::Allow,
285 reason: "all restrictions passed".to_string(),
286 });
287
288 let violations = policies.check(action, state);
290 if !violations.is_empty() {
291 let reasons: Vec<String> = violations
292 .iter()
293 .map(|v| format!("{}: {}", v.policy_name, v.reason))
294 .collect();
295 stages.push(StageResult {
296 stage: AuthzStage::Policy,
297 decision: AuthzDecision::Deny,
298 reason: reasons.join("; "),
299 });
300 return AuthzResult::denied(
301 AuthzStage::Policy,
302 "policy_violation",
303 &format!("Policy violations: {}", reasons.join("; ")),
304 )
305 .with_stages(stages);
306 }
307 stages.push(StageResult {
308 stage: AuthzStage::Policy,
309 decision: AuthzDecision::Allow,
310 reason: "all policies passed".to_string(),
311 });
312
313 stages.push(StageResult {
315 stage: AuthzStage::Validation,
316 decision: AuthzDecision::Allow,
317 reason: "validation deferred".to_string(),
318 });
319
320 AuthzResult::allowed(AuthzStage::Validation).with_stages(stages)
321 }
322}
323
324impl Default for AuthzPipeline {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330pub struct TierPermissionHandler {
357 gate: std::sync::Arc<tokio::sync::RwLock<car_policy::PermissionGate>>,
358 log: Option<std::sync::Arc<tokio::sync::Mutex<car_eventlog::EventLog>>>,
359}
360
361impl TierPermissionHandler {
362 pub fn new(gate: std::sync::Arc<tokio::sync::RwLock<car_policy::PermissionGate>>) -> Self {
363 Self { gate, log: None }
364 }
365
366 pub fn with_event_log(
368 mut self,
369 log: std::sync::Arc<tokio::sync::Mutex<car_eventlog::EventLog>>,
370 ) -> Self {
371 self.log = Some(log);
372 self
373 }
374
375 pub async fn record_approval(
382 &self,
383 action: &Action,
384 approve: bool,
385 reviewer: &str,
386 reason: &str,
387 evidence: Option<String>,
388 ) -> std::io::Result<car_policy::ApprovalRecord> {
389 let record = {
390 let mut gate = self.gate.write().await;
391 if approve {
392 gate.approve(action, reviewer, reason, evidence)?
393 } else {
394 gate.reject(action, reviewer, reason, evidence)?
395 }
396 };
397 if let Some(log) = &self.log {
398 let mut data = HashMap::new();
399 data.insert("fingerprint".into(), record.fingerprint.clone().into());
400 data.insert(
401 "approval".into(),
402 match record.decision {
403 car_policy::ApprovalDecision::Approved => "approved",
404 car_policy::ApprovalDecision::Rejected => "rejected",
405 }
406 .into(),
407 );
408 data.insert("required_tier".into(), record.required_tier.as_str().into());
409 data.insert("reviewer".into(), record.reviewer.clone().into());
410 data.insert("reason".into(), record.reason.clone().into());
411 if let Some(ev) = &record.evidence {
412 data.insert("evidence".into(), ev.clone().into());
413 }
414 log.lock().await.append(
415 car_eventlog::EventKind::ApprovalRecorded,
416 Some(&action.id),
417 None,
418 data,
419 );
420 }
421 Ok(record)
422 }
423
424 fn map_decision(decision: &car_policy::GateDecision) -> AuthzDecision {
426 match decision {
427 car_policy::GateDecision::Allow { .. } => AuthzDecision::Allow,
428 car_policy::GateDecision::NeedsApproval { .. } => AuthzDecision::AskUser,
429 car_policy::GateDecision::Deny { .. } => AuthzDecision::Deny,
430 }
431 }
432
433 fn decision_data(
444 reversibility: car_ir::Reversibility,
445 decision: &car_policy::GateDecision,
446 ) -> HashMap<String, serde_json::Value> {
447 use car_policy::GateDecision::*;
448 let mut data = HashMap::new();
449 data.insert("reversibility".into(), reversibility.as_str().into());
450 match decision {
451 Allow { required, granted } => {
452 data.insert("gate_decision".into(), "allow".into());
453 data.insert("required_tier".into(), required.as_str().into());
454 data.insert("granted_tier".into(), granted.as_str().into());
455 }
456 NeedsApproval {
457 required,
458 granted,
459 fingerprint,
460 reason,
461 } => {
462 data.insert("gate_decision".into(), "needs_approval".into());
463 data.insert("required_tier".into(), required.as_str().into());
464 data.insert("granted_tier".into(), granted.as_str().into());
465 data.insert("fingerprint".into(), fingerprint.clone().into());
466 data.insert("reason".into(), reason.clone().into());
467 }
468 Deny {
469 required,
470 fingerprint,
471 reason,
472 } => {
473 data.insert("gate_decision".into(), "deny".into());
474 data.insert("required_tier".into(), required.as_str().into());
475 data.insert("fingerprint".into(), fingerprint.clone().into());
476 data.insert("reason".into(), reason.clone().into());
477 }
478 }
479 data
480 }
481}
482
483#[async_trait::async_trait]
484impl PermissionHandler for TierPermissionHandler {
485 async fn check(&self, _tool_name: &str, action: &Action) -> AuthzDecision {
486 let axes = {
490 let gate = self.gate.read().await;
491 gate.evaluate_axes(action, None, None)
492 };
493 let decision = axes.decision;
494 if let Some(log) = &self.log {
495 let data = Self::decision_data(axes.reversibility, &decision);
496 log.lock().await.append(
497 car_eventlog::EventKind::PermissionDecision,
498 Some(&action.id),
499 None,
500 data,
501 );
502 }
503 Self::map_decision(&decision)
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use car_ir::{Action, ActionType, ToolSchema};
511
512 fn test_action(tool: &str) -> Action {
513 {
514 let mut a = Action::new(ActionType::ToolCall);
515 a.id = "test-1".to_string();
516 a.tool = Some(tool.to_string());
517 a
518 }
519 }
520
521 fn test_tools() -> HashMap<String, ToolSchema> {
522 let mut m = HashMap::new();
523 m.insert(
524 "read".to_string(),
525 ToolSchema {
526 name: "read".to_string(),
527 source: car_ir::ToolSourceKind::UserDefined,
528 description: "Read a file".to_string(),
529 parameters: serde_json::json!({"type": "object"}),
530 returns: None,
531 idempotent: true,
532 cache_ttl_secs: None,
533 rate_limit: None,
534 },
535 );
536 m
537 }
538
539 #[tokio::test]
540 async fn test_allow_registered_tool() {
541 let pipeline = AuthzPipeline::new();
542 let tools = test_tools();
543 let policies = PolicyEngine::new();
544 let state = StateStore::new();
545
546 let result = pipeline
547 .authorize(&test_action("read"), &tools, None, &policies, &state)
548 .await;
549 assert_eq!(result.decision, AuthzDecision::Allow);
550 assert_eq!(result.stage_results.len(), 6);
551 }
552
553 #[tokio::test]
554 async fn test_deny_unregistered_tool() {
555 let pipeline = AuthzPipeline::new();
556 let tools = test_tools();
557 let policies = PolicyEngine::new();
558 let state = StateStore::new();
559
560 let result = pipeline
561 .authorize(&test_action("delete"), &tools, None, &policies, &state)
562 .await;
563 assert_eq!(result.decision, AuthzDecision::Deny);
564 assert_eq!(result.stage, AuthzStage::ToolExists);
565 assert_eq!(result.reason_code, "tool_not_found");
566 }
567
568 #[tokio::test]
569 async fn test_capability_denial() {
570 let pipeline = AuthzPipeline::new();
571 let tools = test_tools();
572 let policies = PolicyEngine::new();
573 let state = StateStore::new();
574 let mut caps = crate::capabilities::CapabilitySet::default();
575 caps.denied_tools.insert("read".to_string());
576
577 let result = pipeline
578 .authorize(&test_action("read"), &tools, Some(&caps), &policies, &state)
579 .await;
580 assert_eq!(result.decision, AuthzDecision::Deny);
581 assert_eq!(result.stage, AuthzStage::Capability);
582 }
583
584 #[tokio::test]
585 async fn test_restriction() {
586 let mut pipeline = AuthzPipeline::new();
587 pipeline.add_restriction(Restriction::new("no_read", "Never allow read", |action| {
588 if action.tool.as_deref() == Some("read") {
589 Some("reads are restricted".to_string())
590 } else {
591 None
592 }
593 }));
594 let tools = test_tools();
595 let policies = PolicyEngine::new();
596 let state = StateStore::new();
597
598 let result = pipeline
599 .authorize(&test_action("read"), &tools, None, &policies, &state)
600 .await;
601 assert_eq!(result.decision, AuthzDecision::Deny);
602 assert_eq!(result.stage, AuthzStage::Restriction);
603 }
604
605 #[tokio::test]
606 async fn test_policy_violation() {
607 let pipeline = AuthzPipeline::new();
608 let tools = test_tools();
609 let state = StateStore::new();
610 let mut policies = PolicyEngine::new();
611 policies.register(
612 "deny_all",
613 Box::new(|_action: &Action, _state: &StateStore| Some("denied by test".to_string())),
614 "test policy",
615 );
616
617 let result = pipeline
618 .authorize(&test_action("read"), &tools, None, &policies, &state)
619 .await;
620 assert_eq!(result.decision, AuthzDecision::Deny);
621 assert_eq!(result.stage, AuthzStage::Policy);
622 }
623
624 #[tokio::test]
625 async fn test_ask_user_permission() {
626 struct AskPermissions;
627 #[async_trait::async_trait]
628 impl PermissionHandler for AskPermissions {
629 async fn check(&self, _tool_name: &str, _action: &Action) -> AuthzDecision {
630 AuthzDecision::AskUser
631 }
632 }
633
634 let mut pipeline = AuthzPipeline::new();
635 pipeline.set_permission_handler(Box::new(AskPermissions));
636 let tools = test_tools();
637 let policies = PolicyEngine::new();
638 let state = StateStore::new();
639
640 let result = pipeline
641 .authorize(&test_action("read"), &tools, None, &policies, &state)
642 .await;
643 assert_eq!(result.decision, AuthzDecision::AskUser);
644 assert_eq!(result.stage, AuthzStage::Permission);
645 assert_eq!(result.reason_code, "approval_required");
646 }
647
648 #[tokio::test]
649 async fn test_stage_results_trace() {
650 let pipeline = AuthzPipeline::new();
651 let tools = test_tools();
652 let policies = PolicyEngine::new();
653 let state = StateStore::new();
654
655 let result = pipeline
656 .authorize(&test_action("read"), &tools, None, &policies, &state)
657 .await;
658 let stage_names: Vec<AuthzStage> = result.stage_results.iter().map(|s| s.stage).collect();
660 assert_eq!(
661 stage_names,
662 vec![
663 AuthzStage::ToolExists,
664 AuthzStage::Capability,
665 AuthzStage::Permission,
666 AuthzStage::Restriction,
667 AuthzStage::Policy,
668 AuthzStage::Validation,
669 ]
670 );
671 }
672
673 #[tokio::test]
674 async fn test_short_circuit_on_deny() {
675 let pipeline = AuthzPipeline::new();
676 let tools = test_tools();
677 let policies = PolicyEngine::new();
678 let state = StateStore::new();
679
680 let result = pipeline
682 .authorize(&test_action("nonexistent"), &tools, None, &policies, &state)
683 .await;
684 assert_eq!(result.stage_results.len(), 1);
685 assert_eq!(result.stage_results[0].stage, AuthzStage::ToolExists);
686 }
687
688 #[tokio::test]
689 async fn test_serde_roundtrip() {
690 let result = AuthzResult::denied(AuthzStage::Policy, "policy_violation", "Test violation");
691 let json = serde_json::to_string(&result).unwrap();
692 let roundtripped: AuthzResult = serde_json::from_str(&json).unwrap();
693 assert_eq!(roundtripped.decision, AuthzDecision::Deny);
694 assert_eq!(roundtripped.stage, AuthzStage::Policy);
695 assert_eq!(roundtripped.reason_code, "policy_violation");
696 }
697
698 use std::sync::Arc;
701 use tokio::sync::RwLock;
702
703 fn deploy_action() -> Action {
704 let mut a = test_action("deploy_service");
705 a.id = "deploy-1".to_string();
706 a
707 }
708
709 #[tokio::test]
710 async fn tier_handler_asks_user_for_full_access() {
711 let gate = Arc::new(RwLock::new(car_policy::PermissionGate::new(
715 car_policy::PermissionTier::SandboxEdit,
716 )));
717 let handler = TierPermissionHandler::new(gate);
718 assert_eq!(
719 handler.check("deploy_service", &deploy_action()).await,
720 AuthzDecision::AskUser
721 );
722 }
723
724 #[tokio::test]
725 async fn tier_handler_allows_after_approval_and_audits() {
726 let gate = Arc::new(RwLock::new(car_policy::PermissionGate::new(
727 car_policy::PermissionTier::SandboxEdit,
728 )));
729 let log = Arc::new(tokio::sync::Mutex::new(car_eventlog::EventLog::new()));
730 let handler = TierPermissionHandler::new(gate.clone()).with_event_log(log.clone());
731 let action = deploy_action();
732
733 assert_eq!(
735 handler.check("deploy_service", &action).await,
736 AuthzDecision::AskUser
737 );
738 handler
741 .record_approval(&action, true, "matt", "reviewed", Some("diff".into()))
742 .await
743 .unwrap();
744 assert_eq!(
746 handler.check("deploy_service", &action).await,
747 AuthzDecision::Allow
748 );
749
750 let log = log.lock().await;
751 let decisions: Vec<_> = log
753 .events()
754 .iter()
755 .filter(|e| e.kind == car_eventlog::EventKind::PermissionDecision)
756 .collect();
757 assert_eq!(decisions.len(), 2);
758 assert_eq!(
759 decisions[0].data.get("gate_decision").unwrap(),
760 "needs_approval"
761 );
762 assert_eq!(decisions[1].data.get("gate_decision").unwrap(), "allow");
763 for d in &decisions {
767 assert_eq!(d.data.get("reversibility").unwrap(), "compensable");
768 }
769 let approvals: Vec<_> = log
771 .events()
772 .iter()
773 .filter(|e| e.kind == car_eventlog::EventKind::ApprovalRecorded)
774 .collect();
775 assert_eq!(approvals.len(), 1);
776 assert_eq!(approvals[0].data.get("approval").unwrap(), "approved");
777 assert_eq!(approvals[0].data.get("reviewer").unwrap(), "matt");
778 }
779
780 #[tokio::test]
781 async fn tier_handler_denies_after_rejection() {
782 let gate = Arc::new(RwLock::new(car_policy::PermissionGate::new(
783 car_policy::PermissionTier::FullAccess,
784 )));
785 let handler = TierPermissionHandler::new(gate.clone());
786 let action = deploy_action();
787 gate.write()
788 .await
789 .reject(&action, "matt", "not authorized", None)
790 .unwrap();
791 assert_eq!(
792 handler.check("deploy_service", &action).await,
793 AuthzDecision::Deny
794 );
795 }
796
797 #[tokio::test]
798 async fn permission_decision_events_carry_both_axes() {
799 let gate = Arc::new(RwLock::new(car_policy::PermissionGate::new(
805 car_policy::PermissionTier::FullAccess,
806 )));
807 let log = Arc::new(tokio::sync::Mutex::new(car_eventlog::EventLog::new()));
808 let handler = TierPermissionHandler::new(gate).with_event_log(log.clone());
809
810 let deploy = deploy_action();
811 let mut email = test_action("send_email");
812 email.id = "email-1".to_string();
813
814 assert_eq!(
815 handler.check("deploy_service", &deploy).await,
816 AuthzDecision::AskUser
817 );
818 assert_eq!(
819 handler.check("send_email", &email).await,
820 AuthzDecision::AskUser
821 );
822
823 let log = log.lock().await;
824 let rows: Vec<_> = log
825 .events()
826 .iter()
827 .filter(|e| e.kind == car_eventlog::EventKind::PermissionDecision)
828 .collect();
829 assert_eq!(rows.len(), 2);
830 for row in &rows {
831 assert_eq!(row.data.get("gate_decision").unwrap(), "needs_approval");
832 assert_eq!(row.data.get("required_tier").unwrap(), "full_access");
833 }
834 assert_eq!(rows[0].data.get("reversibility").unwrap(), "compensable");
836 assert_eq!(rows[1].data.get("reversibility").unwrap(), "irreversible");
837 }
838}