use axum::{ extract::{Request, State}, http::{header, Method, StatusCode}, middleware::Next, response::{IntoResponse, Response}, }; use base64::Engine; use std::sync::Arc; use super::schema::RedfishError; use crate::state::AppState; pub async fn redfish_auth_middleware( State(state): State>, mut request: Request, next: Next, ) -> Response { if !state.config.is_initialized() { let body = RedfishError::service_unavailable("System not initialized"); return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(body)).into_response(); } let path = request.uri().path(); if is_redfish_public_endpoint(path, request.method()) { return next.run(request).await; } if let Some(token) = request.headers().get("X-Auth-Token") { if let Ok(token_str) = token.to_str() { if state.is_session_revoked(token_str).await { let body = RedfishError::invalid_credentials(); return (StatusCode::UNAUTHORIZED, axum::Json(body)).into_response(); } if let Ok(Some(session)) = state.sessions.get(token_str).await { request.extensions_mut().insert(session); return next.run(request).await; } } } if let Some(auth_header) = request.headers().get(header::AUTHORIZATION) { if let Ok(auth_str) = auth_header.to_str() { if let Some(credentials) = auth_str.strip_prefix("Basic ") { if let Some((username, password)) = decode_basic_auth(credentials) { match state.users.verify(&username, &password).await { Ok(Some(user)) => { request.extensions_mut().insert(user); return next.run(request).await; } _ => { let body = RedfishError::invalid_credentials(); return (StatusCode::UNAUTHORIZED, axum::Json(body)).into_response(); } } } } } } let body = RedfishError::authentication_required(); (StatusCode::UNAUTHORIZED, axum::Json(body)).into_response() } fn is_redfish_public_endpoint(path: &str, method: &Method) -> bool { matches!(path, "/" | "/v1" | "/v1/" | "/v1/odata") || path.starts_with("/v1/$metadata") || (path == "/v1/SessionService/Sessions" && *method == Method::POST) } fn decode_basic_auth(encoded: &str) -> Option<(String, String)> { let decoded = base64::engine::general_purpose::STANDARD .decode(encoded) .ok()?; let credentials = String::from_utf8(decoded).ok()?; let mut parts = credentials.splitn(2, ':'); let username = parts.next()?.to_string(); let password = parts.next()?.to_string(); if username.is_empty() { return None; } Some((username, password)) } #[cfg(test)] mod tests { use super::*; #[test] fn only_service_discovery_and_session_creation_are_public() { assert!(is_redfish_public_endpoint("/v1/", &Method::GET)); assert!(is_redfish_public_endpoint("/v1/$metadata", &Method::GET)); assert!(is_redfish_public_endpoint( "/v1/SessionService/Sessions", &Method::POST )); assert!(!is_redfish_public_endpoint( "/v1/Managers/1/VirtualMedia", &Method::GET )); assert!(!is_redfish_public_endpoint( "/v1/Managers/1/VirtualMedia/1/Actions/VirtualMedia.EjectMedia", &Method::POST )); } }