mockforge_http/auth/
admin_auth.rs

1//! Admin UI authentication
2//!
3//! This module provides authentication for admin UI endpoints
4
5use axum::body::Body;
6use axum::http::{Request, StatusCode};
7use axum::response::Response;
8use base64::{engine::general_purpose, Engine as _};
9use tracing::{debug, warn};
10
11/// Check if admin authentication is required and valid
12pub fn check_admin_auth(
13    req: &Request<Body>,
14    admin_auth_required: bool,
15    admin_username: &Option<String>,
16    admin_password: &Option<String>,
17) -> Result<(), Response> {
18    // If auth not required, allow through
19    if !admin_auth_required {
20        debug!("Admin auth not required, allowing access");
21        return Ok(());
22    }
23
24    // Get authorization header
25    let auth_header = req.headers().get("authorization").and_then(|h| h.to_str().ok());
26
27    if let Some(auth_value) = auth_header {
28        // Check if it's Basic auth
29        if let Some(basic_creds) = auth_value.strip_prefix("Basic ") {
30            // Decode base64 credentials
31            match general_purpose::STANDARD.decode(basic_creds) {
32                Ok(decoded) => {
33                    if let Ok(creds_str) = String::from_utf8(decoded) {
34                        // Split on first colon
35                        if let Some((username, password)) = creds_str.split_once(':') {
36                            // Compare with configured credentials
37                            if let (Some(expected_user), Some(expected_pass)) =
38                                (admin_username, admin_password)
39                            {
40                                if username == expected_user && password == expected_pass {
41                                    debug!("Admin authentication successful");
42                                    return Ok(());
43                                }
44                            }
45                        }
46                    }
47                }
48                Err(e) => {
49                    warn!("Failed to decode admin credentials: {}", e);
50                }
51            }
52        }
53    }
54
55    // Authentication failed
56    warn!("Admin authentication failed or missing");
57    let mut res = Response::new(Body::from(
58        serde_json::json!({
59            "error": "Authentication required",
60            "message": "Admin UI requires authentication"
61        })
62        .to_string(),
63    ));
64    *res.status_mut() = StatusCode::UNAUTHORIZED;
65    res.headers_mut()
66        .insert("www-authenticate", "Basic realm=\"MockForge Admin\"".parse().unwrap());
67
68    Err(res)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use axum::http::Request;
75
76    #[test]
77    fn test_admin_auth_not_required() {
78        let req = Request::builder().body(Body::empty()).unwrap();
79        assert!(check_admin_auth(&req, false, &None, &None).is_ok());
80    }
81
82    #[test]
83    fn test_admin_auth_missing() {
84        let req = Request::builder().body(Body::empty()).unwrap();
85        let username = Some("admin".to_string());
86        let password = Some("secret".to_string());
87        assert!(check_admin_auth(&req, true, &username, &password).is_err());
88    }
89
90    #[test]
91    fn test_admin_auth_valid() {
92        let username = Some("admin".to_string());
93        let password = Some("secret".to_string());
94
95        // Create Basic auth header: admin:secret
96        let credentials = general_purpose::STANDARD.encode("admin:secret");
97        let auth_value = format!("Basic {}", credentials);
98
99        let req = Request::builder()
100            .header("authorization", auth_value)
101            .body(Body::empty())
102            .unwrap();
103
104        assert!(check_admin_auth(&req, true, &username, &password).is_ok());
105    }
106
107    #[test]
108    fn test_admin_auth_invalid_password() {
109        let username = Some("admin".to_string());
110        let password = Some("secret".to_string());
111
112        // Wrong password
113        let credentials = general_purpose::STANDARD.encode("admin:wrong");
114        let auth_value = format!("Basic {}", credentials);
115
116        let req = Request::builder()
117            .header("authorization", auth_value)
118            .body(Body::empty())
119            .unwrap();
120
121        assert!(check_admin_auth(&req, true, &username, &password).is_err());
122    }
123}