Skip to main content

orcs_auth/
error.rs

1//! Unified access denied error type.
2//!
3//! [`AccessDenied`] unifies the three permission layers into a single error:
4//!
5//! ```text
6//! Effective Permission = Capability(WHAT) ∩ SandboxPolicy(WHERE) ∩ Session(WHO+WHEN)
7//!                            │                    │                      │
8//!                   CapabilityDenied      ResourceDenied         SessionDenied
9//! ```
10
11use crate::{Capability, SandboxError};
12use thiserror::Error;
13
14/// Unified error for access denied across all permission layers.
15///
16/// Callers can match on the variant to determine which layer denied access
17/// and provide appropriate user feedback.
18///
19/// # Example
20///
21/// ```
22/// use orcs_auth::{AccessDenied, Capability};
23///
24/// let err = AccessDenied::CapabilityDenied {
25///     operation: "write".to_string(),
26///     required: Capability::WRITE,
27///     available: Capability::READ,
28/// };
29///
30/// assert!(err.to_string().contains("write"));
31/// ```
32#[derive(Debug, Error)]
33pub enum AccessDenied {
34    /// Operation requires a capability the entity does not have.
35    #[error("capability denied: '{operation}' requires {required}, available: {available}")]
36    CapabilityDenied {
37        /// The operation that was attempted.
38        operation: String,
39        /// The capability required for the operation.
40        required: Capability,
41        /// The capabilities actually available to the entity.
42        available: Capability,
43    },
44
45    /// Resource access denied by sandbox policy.
46    #[error(transparent)]
47    ResourceDenied(#[from] SandboxError),
48
49    /// Session does not have sufficient privilege for the operation.
50    #[error("session denied: {0}")]
51    SessionDenied(String),
52}
53
54impl AccessDenied {
55    /// Returns the permission layer that denied access.
56    #[must_use]
57    pub fn layer(&self) -> &'static str {
58        match self {
59            Self::CapabilityDenied { .. } => "capability",
60            Self::ResourceDenied(_) => "resource",
61            Self::SessionDenied(_) => "session",
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn capability_denied_display() {
72        let err = AccessDenied::CapabilityDenied {
73            operation: "write_file".to_string(),
74            required: Capability::WRITE,
75            available: Capability::READ,
76        };
77
78        let msg = err.to_string();
79        assert!(msg.contains("write_file"), "got: {msg}");
80        assert!(msg.contains("capability denied"), "got: {msg}");
81        assert_eq!(err.layer(), "capability");
82    }
83
84    #[test]
85    fn resource_denied_from_sandbox_error() {
86        let sandbox_err = SandboxError::OutsideBoundary {
87            path: "/etc/passwd".to_string(),
88            root: "/home/user/project".to_string(),
89        };
90        let err = AccessDenied::from(sandbox_err);
91
92        let msg = err.to_string();
93        assert!(msg.contains("access denied"), "got: {msg}");
94        assert_eq!(err.layer(), "resource");
95    }
96
97    #[test]
98    fn session_denied_display() {
99        let err = AccessDenied::SessionDenied("requires elevation".to_string());
100
101        let msg = err.to_string();
102        assert!(msg.contains("requires elevation"), "got: {msg}");
103        assert_eq!(err.layer(), "session");
104    }
105}