1use axum::response::{IntoResponse, Response};
4use http::StatusCode;
5use std::fmt;
6
7#[derive(Debug, Clone)]
9pub struct AccessDenied {
10 pub roles: u32,
12 pub path: String,
14 pub id: String,
16 pub message: Option<String>,
18}
19
20impl AccessDenied {
21 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 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 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#[derive(Debug, thiserror::Error)]
71pub enum AclError {
72 #[error("Access denied: {0}")]
74 AccessDenied(#[from] AccessDenied),
75
76 #[error("Failed to extract client IP address")]
78 IpExtractionFailed,
79
80 #[error("Failed to extract role: {0}")]
82 RoleExtractionFailed(String),
83
84 #[error("Invalid rule configuration: {0}")]
86 InvalidRule(String),
87
88 #[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
114pub trait AccessDeniedHandler: Send + Sync {
137 fn handle(&self, denied: &AccessDenied) -> Response;
139}
140
141#[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#[derive(Debug, Clone, Default)]
153pub struct JsonDeniedHandler {
154 include_details: bool,
155}
156
157impl JsonDeniedHandler {
158 pub fn new() -> Self {
160 Self::default()
161 }
162
163 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}