Skip to main content

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