use axum::response::{IntoResponse, Response};
use http::StatusCode;
use std::fmt;
#[derive(Debug, Clone)]
pub struct AccessDenied {
pub roles: u32,
pub path: String,
pub id: String,
pub message: Option<String>,
pub auth_info: Option<String>,
}
impl AccessDenied {
pub fn new_with_roles(roles: u32, path: impl Into<String>, id: impl Into<String>) -> Self {
Self {
roles,
path: path.into(),
id: id.into(),
message: None,
auth_info: None,
}
}
pub fn new(path: impl Into<String>) -> Self {
Self {
roles: 0,
path: path.into(),
id: "*".to_string(),
message: None,
auth_info: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}
impl fmt::Display for AccessDenied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.message {
Some(msg) => write!(f, "{}", msg),
None => write!(f, "Access denied for roles 0x{:X} to path '{}'", self.roles, self.path),
}
}
}
impl std::error::Error for AccessDenied {}
impl IntoResponse for AccessDenied {
fn into_response(self) -> Response {
let body = match &self.message {
Some(msg) => msg.clone(),
None => "Access denied".to_string(),
};
(StatusCode::FORBIDDEN, body).into_response()
}
}
#[derive(Debug, thiserror::Error)]
pub enum AclError {
#[error("Access denied: {0}")]
AccessDenied(#[from] AccessDenied),
#[error("Failed to extract client IP address")]
IpExtractionFailed,
#[error("Failed to extract role: {0}")]
RoleExtractionFailed(String),
#[error("Invalid rule configuration: {0}")]
InvalidRule(String),
#[error("Rule provider error: {0}")]
ProviderError(String),
}
impl IntoResponse for AclError {
fn into_response(self) -> Response {
match self {
Self::AccessDenied(denied) => denied.into_response(),
Self::IpExtractionFailed => {
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to determine client IP").into_response()
}
Self::RoleExtractionFailed(_) => {
(StatusCode::UNAUTHORIZED, "Authentication required").into_response()
}
Self::InvalidRule(msg) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("Configuration error: {}", msg))
.into_response()
}
Self::ProviderError(msg) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("ACL error: {}", msg)).into_response()
}
}
}
}
pub trait AccessDeniedHandler: Send + Sync {
fn handle(&self, denied: &AccessDenied) -> Response;
}
#[derive(Debug, Clone, Default)]
pub struct DefaultDeniedHandler;
impl AccessDeniedHandler for DefaultDeniedHandler {
fn handle(&self, denied: &AccessDenied) -> Response {
denied.clone().into_response()
}
}
#[derive(Debug, Clone, Default)]
pub struct JsonDeniedHandler {
include_details: bool,
}
impl JsonDeniedHandler {
pub fn new() -> Self {
Self::default()
}
pub fn with_details(mut self) -> Self {
self.include_details = true;
self
}
}
impl AccessDeniedHandler for JsonDeniedHandler {
fn handle(&self, denied: &AccessDenied) -> Response {
use axum::Json;
let body = if self.include_details {
serde_json::json!({
"error": "access_denied",
"message": denied.message.as_deref().unwrap_or("Access denied"),
"roles": format!("0x{:X}", denied.roles),
"id": denied.id,
"path": denied.path,
})
} else {
serde_json::json!({
"error": "access_denied",
"message": denied.message.as_deref().unwrap_or("Access denied"),
})
};
(StatusCode::FORBIDDEN, Json(body)).into_response()
}
}