// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Keystone authentication middleware //! //! This middleware intercepts HTTP requests and checks for OpenStack Keystone //! authentication headers (X-Auth-Token). If found, it validates the token //! with Keystone and stores the authenticated credentials in task-local storage //! for use by downstream authentication handlers. //! //! ## Authentication Flow //! //! 1. Check if Keystone is enabled (via global provider) //! 2. Extract X-Auth-Token header from request //! 3. If token present: //! - Validate with Keystone service //! - On success: Store credentials in task-local, continue processing //! - On failure: Return 401 Unauthorized immediately //! 4. If no token: Pass through to standard S3 authentication //! //! ## Task-Local Storage //! //! Uses tokio task-local storage to pass credentials from middleware to //! auth handlers without modifying request/response types. This is async-safe //! and properly scoped to the request lifetime. use bytes::Bytes; use futures::Future; use http::{HeaderMap, Request, Response, StatusCode}; use http_body::Body; use http_body_util::{BodyExt, Full}; use hyper::body::Incoming; use rustfs_credentials::Credentials; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use tower::{Layer, Service}; use tracing::{debug, info, warn}; use crate::KeystoneAuthProvider; // Task-local storage for Keystone credentials // This allows passing credentials from middleware to auth handlers // without modifying the request/response types tokio::task_local! { pub static KEYSTONE_CREDENTIALS: Option; } /// Tower Layer for Keystone authentication /// /// This layer wraps services with Keystone authentication middleware. /// It checks for X-Auth-Token headers and validates them with OpenStack Keystone. #[derive(Clone)] pub struct KeystoneAuthLayer { keystone_auth: Option>, } impl KeystoneAuthLayer { /// Create a new Keystone authentication layer /// /// # Arguments /// /// * `keystone_auth` - Optional Keystone auth provider. If None, middleware is disabled. pub fn new(keystone_auth: Option>) -> Self { if keystone_auth.is_some() { info!("Keystone authentication middleware enabled"); } else { debug!("Keystone authentication middleware disabled (no provider)"); } Self { keystone_auth } } } impl Layer for KeystoneAuthLayer { type Service = KeystoneAuthMiddleware; fn layer(&self, inner: S) -> Self::Service { KeystoneAuthMiddleware { inner, keystone_auth: self.keystone_auth.clone(), } } } /// Keystone authentication middleware service /// /// This service intercepts requests, validates Keystone tokens if present, /// and stores authenticated credentials in task-local storage. #[derive(Clone)] pub struct KeystoneAuthMiddleware { inner: S, keystone_auth: Option>, } type BoxError = Box; type BoxBody = http_body_util::combinators::UnsyncBoxBody; impl Service> for KeystoneAuthMiddleware where S: Service, Response = Response> + Clone + Send + 'static, S::Future: Send + 'static, S::Error: Send + 'static, B: Body + Send + 'static, B::Error: Into + Send + 'static, { type Response = Response; type Error = S::Error; type Future = Pin> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request) -> Self::Future { let keystone_auth = self.keystone_auth.clone(); let mut inner = self.inner.clone(); Box::pin(async move { // Check if Keystone is enabled let keystone_auth = match keystone_auth { Some(auth) => auth, None => { // No Keystone configured, pass through to normal authentication debug!("Keystone middleware: No provider configured, passing through"); let resp = inner.call(req).await?; let (parts, body) = resp.into_parts(); let body: BoxBody = body.map_err(Into::into).boxed_unsync(); return Ok(Response::from_parts(parts, body)); } }; // Extract X-Auth-Token header let token = extract_keystone_token(req.headers()); if let Some(token) = token { debug!("Keystone middleware: Found X-Auth-Token header, validating"); // Validate token with Keystone match keystone_auth.authenticate_with_token(token).await { Ok(credentials) => { // Authentication successful! info!("Keystone middleware: Authentication successful for user: {}", credentials.parent_user); // Store credentials in task-local storage and continue processing // The auth handlers will retrieve these credentials when needed let resp = KEYSTONE_CREDENTIALS.scope(Some(credentials), inner.call(req)).await?; let (parts, body) = resp.into_parts(); let body: BoxBody = body.map_err(Into::into).boxed_unsync(); return Ok(Response::from_parts(parts, body)); } Err(e) => { // Authentication failed - return 401 Unauthorized immediately // Per Q5.A: Return 401 immediately, no fallback to local auth warn!("Keystone middleware: Authentication failed: {}", e); let error_xml = format!( r#" InvalidToken Invalid Keystone token
{}
"#, xml_escape(&e.to_string()) ); let body: BoxBody = Full::new(Bytes::from(error_xml)) .map_err(|e| -> BoxError { Box::new(e) }) .boxed_unsync(); let response = Response::builder() .status(StatusCode::UNAUTHORIZED) .header("Content-Type", "application/xml") .header("WWW-Authenticate", "Keystone") .body(body) .unwrap(); return Ok(response); } } } // No Keystone token header present, pass through to normal S3 authentication debug!("Keystone middleware: No X-Auth-Token header, passing through to S3 auth"); let resp = inner.call(req).await?; let (parts, body) = resp.into_parts(); let body: BoxBody = body.map_err(Into::into).boxed_unsync(); Ok(Response::from_parts(parts, body)) }) } } /// Extract Keystone token from request headers /// /// Checks for X-Auth-Token header (Keystone v3 standard). /// Note: X-Storage-Token (Swift) support deferred to future PR per Q4.C fn extract_keystone_token(headers: &HeaderMap) -> Option<&str> { headers.get("X-Auth-Token").and_then(|v| v.to_str().ok()) // TODO: Add X-Storage-Token support in Phase 2 (Swift API) // .or_else(|| headers.get("X-Storage-Token").and_then(|v| v.to_str().ok())) } /// Escape XML special characters to prevent injection fn xml_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } #[cfg(test)] mod tests { use super::*; use crate::{KeystoneClient, KeystoneVersion}; use std::time::Duration; #[test] fn test_layer_creation_no_keystone() { // Test that layer can be created without Keystone provider let layer = KeystoneAuthLayer::new(None); assert!(layer.keystone_auth.is_none()); } #[test] fn test_layer_creation_with_keystone() { // Test that layer can be created with Keystone provider let client = KeystoneClient::new( "http://localhost:5000".to_string(), KeystoneVersion::V3, None, None, None, "Default".to_string(), true, ); let provider = KeystoneAuthProvider::new(client, 100, Duration::from_secs(60), true); let layer = KeystoneAuthLayer::new(Some(Arc::new(provider))); assert!(layer.keystone_auth.is_some()); } #[tokio::test] async fn test_extract_keystone_token() { let mut headers = HeaderMap::new(); assert!(extract_keystone_token(&headers).is_none()); headers.insert("X-Auth-Token", "test-token-123".parse().unwrap()); assert_eq!(extract_keystone_token(&headers), Some("test-token-123")); } #[tokio::test] async fn test_xml_escape() { assert_eq!(xml_escape("normal text"), "normal text"); assert_eq!(xml_escape(""), "<tag>"); assert_eq!(xml_escape("a&b"), "a&b"); assert_eq!(xml_escape("it's \"quoted\""), "it's "quoted""); } #[tokio::test] async fn test_task_local_scope() { // Verify that task-local storage works correctly use rustfs_credentials::Credentials; let creds = Credentials { access_key: "test-key".to_string(), parent_user: "test-user".to_string(), ..Default::default() }; // Should be None outside of scope assert!(KEYSTONE_CREDENTIALS.try_with(|c| c.clone()).is_err()); // Should be Some inside scope KEYSTONE_CREDENTIALS .scope(Some(creds.clone()), async { let stored = KEYSTONE_CREDENTIALS.try_with(|c| c.clone()).unwrap(); assert!(stored.is_some()); assert_eq!(stored.unwrap().access_key, "test-key"); }) .await; // Should be None again after scope assert!(KEYSTONE_CREDENTIALS.try_with(|c| c.clone()).is_err()); } // Note: test_valid_token and test_invalid_token require mock Keystone server // These will be added in Task 3.3 (Integration Testing) }