axum_acl/
error.rs

1//! Error types for the ACL middleware.
2
3use axum::response::{IntoResponse, Response};
4use http::StatusCode;
5use std::fmt;
6
7/// Error returned when access is denied by the ACL middleware.
8#[derive(Debug, Clone)]
9pub struct AccessDenied {
10    /// The roles bitmask that was denied.
11    pub roles: u32,
12    /// The path that was requested.
13    pub path: String,
14    /// The user/session ID.
15    pub id: String,
16    /// Optional custom message.
17    pub message: Option<String>,
18}
19
20impl AccessDenied {
21    /// Create a new access denied error with roles bitmask.
22    pub fn new_with_roles(roles: u32, path: impl Into<String>, id: impl Into<String>) -> Self {
23        Self {
24            roles,
25            path: path.into(),
26            id: id.into(),
27            message: None,
28        }
29    }
30
31    /// Create a new access denied error (legacy, uses 0 for roles).
32    pub fn new(path: impl Into<String>) -> Self {
33        Self {
34            roles: 0,
35            path: path.into(),
36            id: "*".to_string(),
37            message: None,
38        }
39    }
40
41    /// Add a custom message to the error.
42    pub fn with_message(mut self, message: impl Into<String>) -> Self {
43        self.message = Some(message.into());
44        self
45    }
46}
47
48impl fmt::Display for AccessDenied {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match &self.message {
51            Some(msg) => write!(f, "{}", msg),
52            None => write!(f, "Access denied for roles 0x{:X} to path '{}'", self.roles, self.path),
53        }
54    }
55}
56
57impl std::error::Error for AccessDenied {}
58
59impl IntoResponse for AccessDenied {
60    fn into_response(self) -> Response {
61        let body = match &self.message {
62            Some(msg) => msg.clone(),
63            None => "Access denied".to_string(),
64        };
65        (StatusCode::FORBIDDEN, body).into_response()
66    }
67}
68
69/// Error type for ACL operations.
70#[derive(Debug, thiserror::Error)]
71pub enum AclError {
72    /// Access was denied by an ACL rule.
73    #[error("Access denied: {0}")]
74    AccessDenied(#[from] AccessDenied),
75
76    /// Failed to extract the client IP address.
77    #[error("Failed to extract client IP address")]
78    IpExtractionFailed,
79
80    /// Failed to extract role from the request.
81    #[error("Failed to extract role: {0}")]
82    RoleExtractionFailed(String),
83
84    /// Invalid rule configuration.
85    #[error("Invalid rule configuration: {0}")]
86    InvalidRule(String),
87
88    /// Rule provider error.
89    #[error("Rule provider error: {0}")]
90    ProviderError(String),
91}
92
93impl IntoResponse for AclError {
94    fn into_response(self) -> Response {
95        match self {
96            Self::AccessDenied(denied) => denied.into_response(),
97            Self::IpExtractionFailed => {
98                (StatusCode::INTERNAL_SERVER_ERROR, "Failed to determine client IP").into_response()
99            }
100            Self::RoleExtractionFailed(_) => {
101                (StatusCode::UNAUTHORIZED, "Authentication required").into_response()
102            }
103            Self::InvalidRule(msg) => {
104                (StatusCode::INTERNAL_SERVER_ERROR, format!("Configuration error: {}", msg))
105                    .into_response()
106            }
107            Self::ProviderError(msg) => {
108                (StatusCode::INTERNAL_SERVER_ERROR, format!("ACL error: {}", msg)).into_response()
109            }
110        }
111    }
112}
113
114/// Custom response handler for access denied errors.
115///
116/// Implement this trait to customize the response when access is denied.
117///
118/// # Example
119/// ```
120/// use axum_acl::{AccessDeniedHandler, AccessDenied};
121/// use axum::response::{Response, IntoResponse};
122/// use http::StatusCode;
123///
124/// struct JsonDeniedHandler;
125///
126/// impl AccessDeniedHandler for JsonDeniedHandler {
127///     fn handle(&self, denied: &AccessDenied) -> Response {
128///         let body = serde_json::json!({
129///             "error": "access_denied",
130///             "message": denied.to_string(),
131///         });
132///         (StatusCode::FORBIDDEN, axum::Json(body)).into_response()
133///     }
134/// }
135/// ```
136pub trait AccessDeniedHandler: Send + Sync {
137    /// Handle an access denied error and return a response.
138    fn handle(&self, denied: &AccessDenied) -> Response;
139}
140
141/// Default handler that returns a plain text 403 response.
142#[derive(Debug, Clone, Default)]
143pub struct DefaultDeniedHandler;
144
145impl AccessDeniedHandler for DefaultDeniedHandler {
146    fn handle(&self, denied: &AccessDenied) -> Response {
147        denied.clone().into_response()
148    }
149}
150
151/// Handler that returns a JSON error response.
152#[derive(Debug, Clone, Default)]
153pub struct JsonDeniedHandler {
154    include_details: bool,
155}
156
157impl JsonDeniedHandler {
158    /// Create a new JSON denied handler.
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// Include detailed information in the response.
164    ///
165    /// When enabled, includes the role and path in the error response.
166    /// This may be a security risk in production.
167    pub fn with_details(mut self) -> Self {
168        self.include_details = true;
169        self
170    }
171}
172
173impl AccessDeniedHandler for JsonDeniedHandler {
174    fn handle(&self, denied: &AccessDenied) -> Response {
175        use axum::Json;
176
177        let body = if self.include_details {
178            serde_json::json!({
179                "error": "access_denied",
180                "message": denied.message.as_deref().unwrap_or("Access denied"),
181                "roles": format!("0x{:X}", denied.roles),
182                "id": denied.id,
183                "path": denied.path,
184            })
185        } else {
186            serde_json::json!({
187                "error": "access_denied",
188                "message": denied.message.as_deref().unwrap_or("Access denied"),
189            })
190        };
191
192        (StatusCode::FORBIDDEN, Json(body)).into_response()
193    }
194}