Skip to main content

stateset_authz/
error.rs

1//! Error types for the authorization system.
2
3use serde::{Deserialize, Serialize};
4
5/// Errors that can occur during authorization operations.
6///
7/// ```rust
8/// use stateset_authz::AuthzError;
9///
10/// let err = AuthzError::unauthorized("missing API key");
11/// assert!(err.to_string().contains("missing API key"));
12/// ```
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
14#[non_exhaustive]
15pub enum AuthzError {
16    /// The actor is not authenticated (401).
17    #[error("unauthorized: {reason}")]
18    Unauthorized {
19        /// Why authentication failed.
20        reason: String,
21    },
22
23    /// The actor is authenticated but lacks permission (403).
24    #[error("forbidden: {reason}")]
25    Forbidden {
26        /// Why the operation was denied.
27        reason: String,
28    },
29
30    /// The actor has exceeded their rate limit (429).
31    #[error("rate limited: {reason}")]
32    RateLimited {
33        /// Details about the rate limit violation.
34        reason: String,
35    },
36
37    /// An invalid role was referenced.
38    #[error("invalid role: {name}")]
39    InvalidRole {
40        /// The role name that was not found.
41        name: String,
42    },
43
44    /// An invalid resource was referenced.
45    #[error("invalid resource: {name}")]
46    InvalidResource {
47        /// The resource name that was not found.
48        name: String,
49    },
50}
51
52impl AuthzError {
53    /// Creates an [`Unauthorized`](Self::Unauthorized) error.
54    #[must_use]
55    pub fn unauthorized(reason: impl Into<String>) -> Self {
56        Self::Unauthorized { reason: reason.into() }
57    }
58
59    /// Creates a [`Forbidden`](Self::Forbidden) error.
60    #[must_use]
61    pub fn forbidden(reason: impl Into<String>) -> Self {
62        Self::Forbidden { reason: reason.into() }
63    }
64
65    /// Creates a [`RateLimited`](Self::RateLimited) error.
66    #[must_use]
67    pub fn rate_limited(reason: impl Into<String>) -> Self {
68        Self::RateLimited { reason: reason.into() }
69    }
70
71    /// Creates an [`InvalidRole`](Self::InvalidRole) error.
72    #[must_use]
73    pub fn invalid_role(name: impl Into<String>) -> Self {
74        Self::InvalidRole { name: name.into() }
75    }
76
77    /// Creates an [`InvalidResource`](Self::InvalidResource) error.
78    #[must_use]
79    pub fn invalid_resource(name: impl Into<String>) -> Self {
80        Self::InvalidResource { name: name.into() }
81    }
82
83    /// Returns `true` if this is an unauthorized error.
84    #[must_use]
85    pub const fn is_unauthorized(&self) -> bool {
86        matches!(self, Self::Unauthorized { .. })
87    }
88
89    /// Returns `true` if this is a forbidden error.
90    #[must_use]
91    pub const fn is_forbidden(&self) -> bool {
92        matches!(self, Self::Forbidden { .. })
93    }
94
95    /// Returns `true` if this is a rate limited error.
96    #[must_use]
97    pub const fn is_rate_limited(&self) -> bool {
98        matches!(self, Self::RateLimited { .. })
99    }
100}
101
102/// Type alias for results using [`AuthzError`].
103pub type AuthzResult<T> = Result<T, AuthzError>;
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn unauthorized_display() {
111        let err = AuthzError::unauthorized("missing token");
112        assert_eq!(err.to_string(), "unauthorized: missing token");
113    }
114
115    #[test]
116    fn forbidden_display() {
117        let err = AuthzError::forbidden("insufficient permission");
118        assert_eq!(err.to_string(), "forbidden: insufficient permission");
119    }
120
121    #[test]
122    fn rate_limited_display() {
123        let err = AuthzError::rate_limited("too many requests");
124        assert_eq!(err.to_string(), "rate limited: too many requests");
125    }
126
127    #[test]
128    fn invalid_role_display() {
129        let err = AuthzError::invalid_role("superuser");
130        assert_eq!(err.to_string(), "invalid role: superuser");
131    }
132
133    #[test]
134    fn invalid_resource_display() {
135        let err = AuthzError::invalid_resource("widgets");
136        assert_eq!(err.to_string(), "invalid resource: widgets");
137    }
138
139    #[test]
140    fn is_helpers() {
141        assert!(AuthzError::unauthorized("x").is_unauthorized());
142        assert!(!AuthzError::unauthorized("x").is_forbidden());
143        assert!(!AuthzError::unauthorized("x").is_rate_limited());
144
145        assert!(AuthzError::forbidden("x").is_forbidden());
146        assert!(AuthzError::rate_limited("x").is_rate_limited());
147    }
148
149    #[test]
150    fn error_is_std_error() {
151        let err: Box<dyn std::error::Error> = Box::new(AuthzError::unauthorized("test"));
152        assert!(err.to_string().contains("test"));
153    }
154
155    #[test]
156    fn serde_roundtrip() {
157        let errors = vec![
158            AuthzError::unauthorized("a"),
159            AuthzError::forbidden("b"),
160            AuthzError::rate_limited("c"),
161            AuthzError::invalid_role("d"),
162            AuthzError::invalid_resource("e"),
163        ];
164
165        for err in errors {
166            let json = serde_json::to_string(&err).unwrap();
167            let parsed: AuthzError = serde_json::from_str(&json).unwrap();
168            assert_eq!(parsed, err);
169        }
170    }
171
172    #[test]
173    fn equality() {
174        assert_eq!(AuthzError::unauthorized("x"), AuthzError::unauthorized("x"));
175        assert_ne!(AuthzError::unauthorized("x"), AuthzError::unauthorized("y"));
176        assert_ne!(AuthzError::unauthorized("x"), AuthzError::forbidden("x"));
177    }
178}