1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
14#[non_exhaustive]
15pub enum AuthzError {
16 #[error("unauthorized: {reason}")]
18 Unauthorized {
19 reason: String,
21 },
22
23 #[error("forbidden: {reason}")]
25 Forbidden {
26 reason: String,
28 },
29
30 #[error("rate limited: {reason}")]
32 RateLimited {
33 reason: String,
35 },
36
37 #[error("invalid role: {name}")]
39 InvalidRole {
40 name: String,
42 },
43
44 #[error("invalid resource: {name}")]
46 InvalidResource {
47 name: String,
49 },
50}
51
52impl AuthzError {
53 #[must_use]
55 pub fn unauthorized(reason: impl Into<String>) -> Self {
56 Self::Unauthorized { reason: reason.into() }
57 }
58
59 #[must_use]
61 pub fn forbidden(reason: impl Into<String>) -> Self {
62 Self::Forbidden { reason: reason.into() }
63 }
64
65 #[must_use]
67 pub fn rate_limited(reason: impl Into<String>) -> Self {
68 Self::RateLimited { reason: reason.into() }
69 }
70
71 #[must_use]
73 pub fn invalid_role(name: impl Into<String>) -> Self {
74 Self::InvalidRole { name: name.into() }
75 }
76
77 #[must_use]
79 pub fn invalid_resource(name: impl Into<String>) -> Self {
80 Self::InvalidResource { name: name.into() }
81 }
82
83 #[must_use]
85 pub const fn is_unauthorized(&self) -> bool {
86 matches!(self, Self::Unauthorized { .. })
87 }
88
89 #[must_use]
91 pub const fn is_forbidden(&self) -> bool {
92 matches!(self, Self::Forbidden { .. })
93 }
94
95 #[must_use]
97 pub const fn is_rate_limited(&self) -> bool {
98 matches!(self, Self::RateLimited { .. })
99 }
100}
101
102pub 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}