Skip to main content

ferro_rs/authorization/
error.rs

1//! Authorization error types.
2
3use crate::http::{HttpResponse, Response};
4use std::fmt;
5
6/// Error returned when authorization fails.
7#[derive(Debug, Clone)]
8pub struct AuthorizationError {
9    /// The ability that was being checked.
10    pub ability: String,
11    /// Optional message explaining the denial.
12    pub message: Option<String>,
13    /// HTTP status code to return.
14    pub status: u16,
15}
16
17impl AuthorizationError {
18    /// Create a new authorization error.
19    pub fn new(ability: impl Into<String>) -> Self {
20        Self {
21            ability: ability.into(),
22            message: None,
23            status: 403,
24        }
25    }
26
27    /// Create an authorization error with a message.
28    pub fn with_message(ability: impl Into<String>, message: impl Into<String>) -> Self {
29        Self {
30            ability: ability.into(),
31            message: Some(message.into()),
32            status: 403,
33        }
34    }
35
36    /// Create an authorization error that appears as 404.
37    pub fn not_found(ability: impl Into<String>) -> Self {
38        Self {
39            ability: ability.into(),
40            message: None,
41            status: 404,
42        }
43    }
44
45    /// Set a custom HTTP status code.
46    pub fn with_status(mut self, status: u16) -> Self {
47        self.status = status;
48        self
49    }
50
51    /// Get the error message or a default.
52    pub fn message_or_default(&self) -> String {
53        self.message
54            .clone()
55            .unwrap_or_else(|| "This action is unauthorized.".to_string())
56    }
57}
58
59impl fmt::Display for AuthorizationError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match &self.message {
62            Some(msg) => write!(f, "Authorization failed for '{}': {}", self.ability, msg),
63            None => write!(f, "Authorization failed for '{}'", self.ability),
64        }
65    }
66}
67
68impl std::error::Error for AuthorizationError {}
69
70impl From<AuthorizationError> for HttpResponse {
71    fn from(err: AuthorizationError) -> HttpResponse {
72        let message = err.message_or_default();
73        let body = serde_json::json!({
74            "message": message
75        });
76        HttpResponse::json(body).status(err.status)
77    }
78}
79
80impl From<AuthorizationError> for Response {
81    fn from(err: AuthorizationError) -> Response {
82        Err(HttpResponse::from(err))
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_new_error() {
92        let error = AuthorizationError::new("update");
93        assert_eq!(error.ability, "update");
94        assert_eq!(error.status, 403);
95        assert!(error.message.is_none());
96    }
97
98    #[test]
99    fn test_error_with_message() {
100        let error = AuthorizationError::with_message("delete", "You do not own this resource");
101        assert_eq!(error.ability, "delete");
102        assert_eq!(
103            error.message,
104            Some("You do not own this resource".to_string())
105        );
106    }
107
108    #[test]
109    fn test_not_found_error() {
110        let error = AuthorizationError::not_found("view");
111        assert_eq!(error.status, 404);
112    }
113
114    #[test]
115    fn test_display() {
116        let error = AuthorizationError::with_message("update", "Forbidden");
117        assert_eq!(
118            error.to_string(),
119            "Authorization failed for 'update': Forbidden"
120        );
121    }
122
123    #[test]
124    fn test_message_or_default() {
125        let error = AuthorizationError::new("test");
126        assert_eq!(error.message_or_default(), "This action is unauthorized.");
127
128        let error = AuthorizationError::with_message("test", "Custom message");
129        assert_eq!(error.message_or_default(), "Custom message");
130    }
131}