use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[non_exhaustive]
pub enum AuthzError {
#[error("unauthorized: {reason}")]
Unauthorized {
reason: String,
},
#[error("forbidden: {reason}")]
Forbidden {
reason: String,
},
#[error("rate limited: {reason}")]
RateLimited {
reason: String,
},
#[error("invalid role: {name}")]
InvalidRole {
name: String,
},
#[error("invalid resource: {name}")]
InvalidResource {
name: String,
},
}
impl AuthzError {
#[must_use]
pub fn unauthorized(reason: impl Into<String>) -> Self {
Self::Unauthorized { reason: reason.into() }
}
#[must_use]
pub fn forbidden(reason: impl Into<String>) -> Self {
Self::Forbidden { reason: reason.into() }
}
#[must_use]
pub fn rate_limited(reason: impl Into<String>) -> Self {
Self::RateLimited { reason: reason.into() }
}
#[must_use]
pub fn invalid_role(name: impl Into<String>) -> Self {
Self::InvalidRole { name: name.into() }
}
#[must_use]
pub fn invalid_resource(name: impl Into<String>) -> Self {
Self::InvalidResource { name: name.into() }
}
#[must_use]
pub const fn is_unauthorized(&self) -> bool {
matches!(self, Self::Unauthorized { .. })
}
#[must_use]
pub const fn is_forbidden(&self) -> bool {
matches!(self, Self::Forbidden { .. })
}
#[must_use]
pub const fn is_rate_limited(&self) -> bool {
matches!(self, Self::RateLimited { .. })
}
}
pub type AuthzResult<T> = Result<T, AuthzError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unauthorized_display() {
let err = AuthzError::unauthorized("missing token");
assert_eq!(err.to_string(), "unauthorized: missing token");
}
#[test]
fn forbidden_display() {
let err = AuthzError::forbidden("insufficient permission");
assert_eq!(err.to_string(), "forbidden: insufficient permission");
}
#[test]
fn rate_limited_display() {
let err = AuthzError::rate_limited("too many requests");
assert_eq!(err.to_string(), "rate limited: too many requests");
}
#[test]
fn invalid_role_display() {
let err = AuthzError::invalid_role("superuser");
assert_eq!(err.to_string(), "invalid role: superuser");
}
#[test]
fn invalid_resource_display() {
let err = AuthzError::invalid_resource("widgets");
assert_eq!(err.to_string(), "invalid resource: widgets");
}
#[test]
fn is_helpers() {
assert!(AuthzError::unauthorized("x").is_unauthorized());
assert!(!AuthzError::unauthorized("x").is_forbidden());
assert!(!AuthzError::unauthorized("x").is_rate_limited());
assert!(AuthzError::forbidden("x").is_forbidden());
assert!(AuthzError::rate_limited("x").is_rate_limited());
}
#[test]
fn error_is_std_error() {
let err: Box<dyn std::error::Error> = Box::new(AuthzError::unauthorized("test"));
assert!(err.to_string().contains("test"));
}
#[test]
fn serde_roundtrip() {
let errors = vec![
AuthzError::unauthorized("a"),
AuthzError::forbidden("b"),
AuthzError::rate_limited("c"),
AuthzError::invalid_role("d"),
AuthzError::invalid_resource("e"),
];
for err in errors {
let json = serde_json::to_string(&err).unwrap();
let parsed: AuthzError = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, err);
}
}
#[test]
fn equality() {
assert_eq!(AuthzError::unauthorized("x"), AuthzError::unauthorized("x"));
assert_ne!(AuthzError::unauthorized("x"), AuthzError::unauthorized("y"));
assert_ne!(AuthzError::unauthorized("x"), AuthzError::forbidden("x"));
}
}