Skip to main content

orcs_auth/
permission.rs

1//! Command permission types.
2//!
3//! Provides [`CommandPermission`] for trait-level permission results
4//! that don't depend on runtime-specific types like `ApprovalRequest`.
5//!
6//! # Architecture
7//!
8//! ```text
9//! CommandPermission (orcs-auth)   <- trait-level, runtime-independent
10//!         │
11//!         └── CommandCheckResult (orcs-runtime)  <- runtime, includes ApprovalRequest
12//! ```
13//!
14//! `CommandPermission` is used by:
15//! - `ChildContext::check_command_permission()` (in `orcs-component`) — trait boundary
16//! - [`crate::PermissionPolicy::check_command_permission()`] — abstract policy
17//!
18//! `CommandCheckResult` extends this with HIL-specific fields and stays in `orcs-runtime`.
19
20/// Result of a command permission check (trait-level type).
21///
22/// This is a simplified, runtime-independent version suitable for trait boundaries.
23/// Runtime implementations that need HIL integration use `CommandCheckResult`
24/// (in `orcs-runtime`) which includes `ApprovalRequest`.
25///
26/// # Variants
27///
28/// - `Allowed`: Command can execute immediately
29/// - `Denied`: Command is permanently blocked (e.g., denylist)
30/// - `RequiresApproval`: Command needs user approval before execution
31///
32/// # Example
33///
34/// ```
35/// use orcs_auth::CommandPermission;
36///
37/// let perm = CommandPermission::Allowed;
38/// assert!(perm.is_allowed());
39///
40/// let perm = CommandPermission::Denied("blocked pattern".to_string());
41/// assert!(perm.is_denied());
42/// assert_eq!(perm.status_str(), "denied");
43///
44/// let perm = CommandPermission::RequiresApproval {
45///     grant_pattern: "rm -rf".to_string(),
46///     description: "destructive operation".to_string(),
47/// };
48/// assert!(perm.requires_approval());
49/// ```
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum CommandPermission {
52    /// Command is allowed to execute.
53    Allowed,
54    /// Command is denied with a reason.
55    Denied(String),
56    /// Command requires user approval via HIL.
57    RequiresApproval {
58        /// The pattern to grant if approved.
59        grant_pattern: String,
60        /// Human-readable description of why approval is needed.
61        description: String,
62    },
63}
64
65impl CommandPermission {
66    /// Returns `true` if the command is allowed.
67    #[must_use]
68    pub fn is_allowed(&self) -> bool {
69        matches!(self, Self::Allowed)
70    }
71
72    /// Returns `true` if the command is denied.
73    #[must_use]
74    pub fn is_denied(&self) -> bool {
75        matches!(self, Self::Denied(_))
76    }
77
78    /// Returns `true` if the command requires approval.
79    #[must_use]
80    pub fn requires_approval(&self) -> bool {
81        matches!(self, Self::RequiresApproval { .. })
82    }
83
84    /// Returns the status as a string ("allowed", "denied", "requires_approval").
85    #[must_use]
86    pub fn status_str(&self) -> &'static str {
87        match self {
88            Self::Allowed => "allowed",
89            Self::Denied(_) => "denied",
90            Self::RequiresApproval { .. } => "requires_approval",
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn allowed_helpers() {
101        let p = CommandPermission::Allowed;
102        assert!(p.is_allowed());
103        assert!(!p.is_denied());
104        assert!(!p.requires_approval());
105        assert_eq!(p.status_str(), "allowed");
106    }
107
108    #[test]
109    fn denied_helpers() {
110        let p = CommandPermission::Denied("blocked".to_string());
111        assert!(!p.is_allowed());
112        assert!(p.is_denied());
113        assert!(!p.requires_approval());
114        assert_eq!(p.status_str(), "denied");
115    }
116
117    #[test]
118    fn requires_approval_helpers() {
119        let p = CommandPermission::RequiresApproval {
120            grant_pattern: "rm -rf".to_string(),
121            description: "destructive operation".to_string(),
122        };
123        assert!(!p.is_allowed());
124        assert!(!p.is_denied());
125        assert!(p.requires_approval());
126        assert_eq!(p.status_str(), "requires_approval");
127    }
128
129    #[test]
130    fn equality() {
131        assert_eq!(CommandPermission::Allowed, CommandPermission::Allowed);
132        assert_eq!(
133            CommandPermission::Denied("x".into()),
134            CommandPermission::Denied("x".into())
135        );
136        assert_ne!(
137            CommandPermission::Allowed,
138            CommandPermission::Denied("x".into())
139        );
140    }
141}