1use 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
20pub 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
51fn 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
65pub 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 _ 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 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 assert!(matches!(
135 monitoring_auth_decision(Some("secret"), Some("wrong"), true),
136 Err((StatusCode::UNAUTHORIZED, _))
137 ));
138 }
139}