1use std::sync::OnceLock;
12
13use actix_web::HttpRequest;
14
15use crate::errors::AppError;
16
17static EXPECTED: OnceLock<Option<String>> = OnceLock::new();
18
19fn expected() -> Option<&'static str> {
20 EXPECTED
21 .get_or_init(|| std::env::var("ADMIN_TOKEN").ok().filter(|s| !s.is_empty()))
22 .as_deref()
23}
24
25pub fn require_admin(req: &HttpRequest) -> Result<(), AppError> {
32 let expected = expected().ok_or_else(|| {
33 AppError::Forbidden(
34 "admin endpoints are disabled (set ADMIN_TOKEN env var to enable)".into(),
35 )
36 })?;
37
38 let presented = req
39 .headers()
40 .get("X-Admin-Token")
41 .and_then(|v| v.to_str().ok())
42 .unwrap_or("");
43
44 if constant_time_eq(presented.as_bytes(), expected.as_bytes()) {
45 Ok(())
46 } else {
47 Err(AppError::Forbidden(
48 "invalid or missing X-Admin-Token".into(),
49 ))
50 }
51}
52
53fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
57 if a.len() != b.len() {
58 return false;
59 }
60 let mut diff: u8 = 0;
61 for (x, y) in a.iter().zip(b.iter()) {
62 diff |= x ^ y;
63 }
64 diff == 0
65}
66
67#[cfg(test)]
68mod tests {
69 use super::constant_time_eq;
70
71 #[test]
72 fn ct_eq_equal() {
73 assert!(constant_time_eq(b"hunter2", b"hunter2"));
74 assert!(constant_time_eq(b"", b""));
75 }
76
77 #[test]
78 fn ct_eq_different_content() {
79 assert!(!constant_time_eq(b"hunter2", b"hunter3"));
80 }
81
82 #[test]
83 fn ct_eq_different_length() {
84 assert!(!constant_time_eq(b"abc", b"abcd"));
85 assert!(!constant_time_eq(b"abcd", b"abc"));
86 }
87}