Skip to main content

hypercall_admin/
auth.rs

1//! Authentication middleware for the admin/operator HTTP surface.
2//!
3//! These endpoints are protected by the `ADMIN_API_KEY` secret for
4//! performance reasons - they execute synchronous database queries that could
5//! impact system performance under load. Provide the `X-Admin-Key` header to access.
6//!
7//! In development mode (no `ADMIN_API_KEY` set), these endpoints are open.
8
9use axum::{
10    body::Body,
11    extract::State,
12    http::{Request, StatusCode},
13    middleware::Next,
14    response::{IntoResponse, Response},
15};
16use tracing::warn;
17
18use crate::state::AdminState;
19
20/// Middleware to protect monitoring endpoints.
21///
22/// If `ADMIN_API_KEY` is set, requires `X-Admin-Key` header to match.
23/// If `ADMIN_API_KEY` is not set, fails closed unless the runtime explicitly
24/// allows unauthenticated monitoring (development environment only).
25///
26/// These endpoints are authenticated for performance reasons - they execute
27/// synchronous database queries that could impact system performance under load.
28pub async fn monitoring_auth_middleware(
29    State(state): State<AdminState>,
30    request: Request<Body>,
31    next: Next,
32) -> Response {
33    let provided_key = request
34        .headers()
35        .get("X-Admin-Key")
36        .and_then(|v| v.to_str().ok());
37
38    match monitoring_auth_decision(
39        state.admin_api_key.as_deref(),
40        provided_key,
41        state.allow_unauthenticated_monitoring,
42    ) {
43        Ok(()) => next.run(request).await,
44        Err(rejection) => {
45            warn!("Monitoring endpoint access denied - {}", rejection.1);
46            rejection.into_response()
47        }
48    }
49}
50
51/// Constant-time byte comparison so admin-key validation does not leak the key
52/// via early-exit timing. Length difference is allowed to short-circuit (key
53/// length is not the secret); equal-length inputs are compared in full.
54fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
55    if a.len() != b.len() {
56        return false;
57    }
58    let mut diff: u8 = 0;
59    for (x, y) in a.iter().zip(b.iter()) {
60        diff |= x ^ y;
61    }
62    std::hint::black_box(diff) == 0
63}
64
65/// Pure auth decision for monitoring endpoints.
66///
67/// Fails closed when no admin key is configured, unless the runtime
68/// explicitly allows unauthenticated monitoring (development only).
69pub fn monitoring_auth_decision(
70    expected_key: Option<&str>,
71    provided_key: Option<&str>,
72    allow_unauthenticated: bool,
73) -> Result<(), (StatusCode, &'static str)> {
74    match expected_key {
75        Some(expected) if !expected.is_empty() => match provided_key {
76            Some(provided) if constant_time_eq(provided.as_bytes(), expected.as_bytes()) => Ok(()),
77            _ => Err((
78                StatusCode::UNAUTHORIZED,
79                "Invalid or missing X-Admin-Key header",
80            )),
81        },
82        // No admin key configured. These endpoints expose full account,
83        // position, and engine state, so fail closed everywhere except
84        // the development environment.
85        _ if allow_unauthenticated => Ok(()),
86        _ => Err((
87            StatusCode::SERVICE_UNAVAILABLE,
88            "Monitoring auth is not configured; set ADMIN_API_KEY",
89        )),
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn monitoring_auth_fails_closed_without_configured_key() {
99        let result = monitoring_auth_decision(None, None, false);
100        assert_eq!(
101            result,
102            Err((
103                StatusCode::SERVICE_UNAVAILABLE,
104                "Monitoring auth is not configured; set ADMIN_API_KEY"
105            ))
106        );
107
108        // Empty configured key is treated the same as no key.
109        let result = monitoring_auth_decision(Some(""), Some("anything"), false);
110        assert!(matches!(result, Err((StatusCode::SERVICE_UNAVAILABLE, _))));
111    }
112
113    #[test]
114    fn monitoring_auth_allows_unauthenticated_only_when_opted_in() {
115        assert_eq!(monitoring_auth_decision(None, None, true), Ok(()));
116        assert_eq!(monitoring_auth_decision(Some(""), None, true), Ok(()));
117    }
118
119    #[test]
120    fn monitoring_auth_requires_matching_key_when_configured() {
121        assert_eq!(
122            monitoring_auth_decision(Some("secret"), Some("secret"), false),
123            Ok(())
124        );
125        assert!(matches!(
126            monitoring_auth_decision(Some("secret"), Some("wrong"), false),
127            Err((StatusCode::UNAUTHORIZED, _))
128        ));
129        assert!(matches!(
130            monitoring_auth_decision(Some("secret"), None, false),
131            Err((StatusCode::UNAUTHORIZED, _))
132        ));
133        // The dev opt-out must not bypass a configured key.
134        assert!(matches!(
135            monitoring_auth_decision(Some("secret"), Some("wrong"), true),
136            Err((StatusCode::UNAUTHORIZED, _))
137        ));
138    }
139}