Skip to main content

ferro_rs/authorization/
response.rs

1//! Authorization response types.
2
3use std::fmt;
4
5/// The result of an authorization check.
6///
7/// This enum represents whether an action is allowed or denied,
8/// with optional messages and HTTP status codes for denied responses.
9#[derive(Debug, Clone)]
10pub enum AuthResponse {
11    /// The action is allowed.
12    Allow,
13    /// The action is denied.
14    Deny {
15        /// Optional message explaining why the action was denied.
16        message: Option<String>,
17        /// HTTP status code to return (default: 403).
18        status: u16,
19    },
20}
21
22impl AuthResponse {
23    /// Create an allow response.
24    pub fn allow() -> Self {
25        Self::Allow
26    }
27
28    /// Create a deny response with a message.
29    pub fn deny(message: impl Into<String>) -> Self {
30        Self::Deny {
31            message: Some(message.into()),
32            status: 403,
33        }
34    }
35
36    /// Create a deny response without a message.
37    pub fn deny_silent() -> Self {
38        Self::Deny {
39            message: None,
40            status: 403,
41        }
42    }
43
44    /// Create a deny response that appears as a 404 Not Found.
45    ///
46    /// This is useful when you want to hide the existence of a resource
47    /// from unauthorized users.
48    pub fn deny_as_not_found() -> Self {
49        Self::Deny {
50            message: None,
51            status: 404,
52        }
53    }
54
55    /// Create a deny response with a custom status code.
56    pub fn deny_with_status(message: impl Into<String>, status: u16) -> Self {
57        Self::Deny {
58            message: Some(message.into()),
59            status,
60        }
61    }
62
63    /// Check if the response allows the action.
64    pub fn allowed(&self) -> bool {
65        matches!(self, Self::Allow)
66    }
67
68    /// Check if the response denies the action.
69    pub fn denied(&self) -> bool {
70        matches!(self, Self::Deny { .. })
71    }
72
73    /// Get the denial message if present.
74    pub fn message(&self) -> Option<&str> {
75        match self {
76            Self::Deny { message, .. } => message.as_deref(),
77            Self::Allow => None,
78        }
79    }
80
81    /// Get the HTTP status code for denied responses.
82    pub fn status(&self) -> u16 {
83        match self {
84            Self::Allow => 200,
85            Self::Deny { status, .. } => *status,
86        }
87    }
88}
89
90impl From<bool> for AuthResponse {
91    fn from(allowed: bool) -> Self {
92        if allowed {
93            Self::Allow
94        } else {
95            Self::Deny {
96                message: None,
97                status: 403,
98            }
99        }
100    }
101}
102
103impl fmt::Display for AuthResponse {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            Self::Allow => write!(f, "Allowed"),
107            Self::Deny {
108                message: Some(msg), ..
109            } => write!(f, "Denied: {msg}"),
110            Self::Deny { message: None, .. } => write!(f, "Denied"),
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_allow() {
121        let response = AuthResponse::allow();
122        assert!(response.allowed());
123        assert!(!response.denied());
124        assert_eq!(response.status(), 200);
125    }
126
127    #[test]
128    fn test_deny_with_message() {
129        let response = AuthResponse::deny("Not authorized");
130        assert!(!response.allowed());
131        assert!(response.denied());
132        assert_eq!(response.message(), Some("Not authorized"));
133        assert_eq!(response.status(), 403);
134    }
135
136    #[test]
137    fn test_deny_silent() {
138        let response = AuthResponse::deny_silent();
139        assert!(response.denied());
140        assert_eq!(response.message(), None);
141        assert_eq!(response.status(), 403);
142    }
143
144    #[test]
145    fn test_deny_as_not_found() {
146        let response = AuthResponse::deny_as_not_found();
147        assert!(response.denied());
148        assert_eq!(response.status(), 404);
149    }
150
151    #[test]
152    fn test_from_bool_true() {
153        let response: AuthResponse = true.into();
154        assert!(response.allowed());
155    }
156
157    #[test]
158    fn test_from_bool_false() {
159        let response: AuthResponse = false.into();
160        assert!(response.denied());
161    }
162
163    #[test]
164    fn test_display() {
165        assert_eq!(AuthResponse::allow().to_string(), "Allowed");
166        assert_eq!(
167            AuthResponse::deny("Forbidden").to_string(),
168            "Denied: Forbidden"
169        );
170        assert_eq!(AuthResponse::deny_silent().to_string(), "Denied");
171    }
172}