Skip to main content

orcs_auth/
policy.rs

1//! Permission policy trait.
2//!
3//! Defines [`PermissionPolicy`] — the abstract policy for permission checking.
4//! This trait lives in `orcs-auth` so that both `orcs-component` and `orcs-runtime`
5//! can reference it without circular dependencies.
6//!
7//! # Architecture
8//!
9//! ```text
10//! PermissionPolicy trait (orcs-auth)   <- abstract, no runtime deps
11//!          │
12//!          ├── PermissionChecker (orcs-runtime) <- extends with check_command(CommandCheckResult)
13//!          │         │
14//!          │         └── DefaultPolicy (orcs-runtime) <- concrete impl
15//!          │
16//!          └── (future) WasmPolicy, DockerPolicy, ...
17//! ```
18//!
19//! # Three-Layer Model
20//!
21//! ```text
22//! Effective Permission = Capability(WHAT) ∩ SandboxPolicy(WHERE) ∩ PermissionPolicy(WHO+WHEN)
23//! ```
24//!
25//! | Layer | Type | Controls |
26//! |-------|------|----------|
27//! | [`crate::Capability`] | Bitflags | What operations are allowed |
28//! | [`crate::SandboxPolicy`] | Trait | Where operations can target |
29//! | [`PermissionPolicy`] | Trait (THIS) | Who can act, with what privilege |
30
31use crate::{CommandPermission, Session};
32use orcs_types::SignalScope;
33
34/// Abstract permission policy for session-based access control.
35///
36/// Implement this trait to define custom permission policies.
37/// The trait is runtime-independent — it doesn't depend on
38/// `ApprovalRequest` or other runtime-specific types.
39///
40/// # Implementors
41///
42/// - `DefaultPolicy` (in `orcs-runtime`) — standard policy with blocked/elevated patterns
43/// - Custom impls for testing or restricted environments
44///
45/// # Example
46///
47/// ```
48/// use orcs_auth::{PermissionPolicy, Session, CommandPermission};
49/// use orcs_types::{SignalScope, Principal, PrincipalId};
50///
51/// struct PermissivePolicy;
52///
53/// impl PermissionPolicy for PermissivePolicy {
54///     fn can_signal(&self, _session: &Session, _scope: &SignalScope) -> bool {
55///         true
56///     }
57///
58///     fn can_destructive(&self, _session: &Session, _action: &str) -> bool {
59///         true
60///     }
61///
62///     fn can_execute_command(&self, _session: &Session, _cmd: &str) -> bool {
63///         true
64///     }
65///
66///     fn can_spawn_child(&self, _session: &Session) -> bool {
67///         true
68///     }
69///
70///     fn can_spawn_runner(&self, _session: &Session) -> bool {
71///         true
72///     }
73/// }
74///
75/// let policy = PermissivePolicy;
76/// let session = Session::new(Principal::User(PrincipalId::new()));
77/// assert!(policy.can_execute_command(&session, "ls -la"));
78/// assert!(policy.check_command_permission(&session, "ls -la").is_allowed());
79/// ```
80pub trait PermissionPolicy: Send + Sync {
81    /// Check if session can send a signal with the given scope.
82    fn can_signal(&self, session: &Session, scope: &SignalScope) -> bool;
83
84    /// Check if session can perform a destructive operation.
85    ///
86    /// Destructive operations include `git reset --hard`, `git push --force`,
87    /// `rm -rf`, file overwrite without backup, etc.
88    fn can_destructive(&self, session: &Session, action: &str) -> bool;
89
90    /// Check if session can execute a shell command.
91    fn can_execute_command(&self, session: &Session, cmd: &str) -> bool;
92
93    /// Check if session can spawn a child entity.
94    fn can_spawn_child(&self, session: &Session) -> bool;
95
96    /// Check if session can spawn a runner (parallel execution).
97    fn can_spawn_runner(&self, session: &Session) -> bool;
98
99    /// Check command with granular permission result.
100    ///
101    /// Returns [`CommandPermission`] with three possible states:
102    /// - `Allowed`: Execute immediately
103    /// - `Denied`: Block with reason
104    /// - `RequiresApproval`: Needs user approval
105    ///
106    /// # Default Implementation
107    ///
108    /// Wraps `can_execute_command`:
109    /// - Returns `Allowed` if `can_execute_command` returns true
110    /// - Returns `Denied` otherwise
111    ///
112    /// Override this for more granular control (e.g., HIL integration).
113    fn check_command_permission(&self, session: &Session, cmd: &str) -> CommandPermission {
114        if self.can_execute_command(session, cmd) {
115            CommandPermission::Allowed
116        } else {
117            CommandPermission::Denied("permission denied".to_string())
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use orcs_types::{ChannelId, Principal, PrincipalId};
126
127    struct PermissivePolicy;
128
129    impl PermissionPolicy for PermissivePolicy {
130        fn can_signal(&self, _session: &Session, _scope: &SignalScope) -> bool {
131            true
132        }
133        fn can_destructive(&self, _session: &Session, _action: &str) -> bool {
134            true
135        }
136        fn can_execute_command(&self, _session: &Session, _cmd: &str) -> bool {
137            true
138        }
139        fn can_spawn_child(&self, _session: &Session) -> bool {
140            true
141        }
142        fn can_spawn_runner(&self, _session: &Session) -> bool {
143            true
144        }
145    }
146
147    struct StrictPolicy;
148
149    impl PermissionPolicy for StrictPolicy {
150        fn can_signal(&self, session: &Session, scope: &SignalScope) -> bool {
151            match scope {
152                SignalScope::Global => session.is_elevated(),
153                _ => true,
154            }
155        }
156        fn can_destructive(&self, session: &Session, _action: &str) -> bool {
157            session.is_elevated()
158        }
159        fn can_execute_command(&self, session: &Session, _cmd: &str) -> bool {
160            session.is_elevated()
161        }
162        fn can_spawn_child(&self, session: &Session) -> bool {
163            session.is_elevated()
164        }
165        fn can_spawn_runner(&self, session: &Session) -> bool {
166            session.is_elevated()
167        }
168    }
169
170    fn standard_session() -> Session {
171        Session::new(Principal::User(PrincipalId::new()))
172    }
173
174    fn elevated_session() -> Session {
175        standard_session().elevate(std::time::Duration::from_secs(60))
176    }
177
178    #[test]
179    fn permissive_allows_everything() {
180        let policy = PermissivePolicy;
181        let session = standard_session();
182
183        assert!(policy.can_signal(&session, &SignalScope::Global));
184        assert!(policy.can_destructive(&session, "rm -rf"));
185        assert!(policy.can_execute_command(&session, "ls"));
186        assert!(policy.can_spawn_child(&session));
187        assert!(policy.can_spawn_runner(&session));
188    }
189
190    #[test]
191    fn strict_denies_standard_session() {
192        let policy = StrictPolicy;
193        let session = standard_session();
194
195        assert!(!policy.can_signal(&session, &SignalScope::Global));
196        assert!(!policy.can_destructive(&session, "rm -rf"));
197        assert!(!policy.can_execute_command(&session, "ls"));
198        assert!(!policy.can_spawn_child(&session));
199        assert!(!policy.can_spawn_runner(&session));
200    }
201
202    #[test]
203    fn strict_allows_elevated_session() {
204        let policy = StrictPolicy;
205        let session = elevated_session();
206
207        assert!(policy.can_signal(&session, &SignalScope::Global));
208        assert!(policy.can_destructive(&session, "rm -rf"));
209        assert!(policy.can_execute_command(&session, "ls"));
210        assert!(policy.can_spawn_child(&session));
211        assert!(policy.can_spawn_runner(&session));
212    }
213
214    #[test]
215    fn strict_allows_channel_signal_for_standard() {
216        let policy = StrictPolicy;
217        let session = standard_session();
218        let channel = ChannelId::new();
219
220        assert!(policy.can_signal(&session, &SignalScope::Channel(channel)));
221    }
222
223    #[test]
224    fn default_check_command_permission_wraps_can_execute() {
225        let policy = PermissivePolicy;
226        let session = standard_session();
227
228        let result = policy.check_command_permission(&session, "ls -la");
229        assert!(result.is_allowed());
230    }
231
232    #[test]
233    fn default_check_command_permission_denied_when_not_allowed() {
234        let policy = StrictPolicy;
235        let session = standard_session();
236
237        let result = policy.check_command_permission(&session, "ls -la");
238        assert!(result.is_denied());
239    }
240
241    #[test]
242    fn trait_object_works() {
243        let policy: Box<dyn PermissionPolicy> = Box::new(PermissivePolicy);
244        let session = standard_session();
245
246        assert!(policy.can_execute_command(&session, "ls"));
247    }
248}