1use axum::{
8 Router,
9 extract::Request,
10 http::{StatusCode, header},
11 middleware::{self, Next},
12 response::Response,
13};
14
15#[derive(Clone)]
17pub struct Admin {
18 pub address: String,
19 token: String,
20}
21
22impl std::fmt::Debug for Admin {
24 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
25 f.debug_struct("Admin")
26 .field("address", &self.address)
27 .finish_non_exhaustive()
28 }
29}
30
31const MIN_TOKEN: usize = 32;
33
34impl Admin {
35 pub fn from_env() -> Result<Option<Self>, String> {
38 let var = |name| std::env::var(name).ok().filter(|v: &String| !v.is_empty());
39 Self::from(var("ADMIN_ADDR"), var("ADMIN_TOKEN"))
40 }
41
42 fn from(address: Option<String>, token: Option<String>) -> Result<Option<Self>, String> {
43 match (address, token) {
44 (None, None) => Ok(None),
45 (Some(_), None) => Err("ADMIN_ADDR needs ADMIN_TOKEN".into()),
46 (None, Some(_)) => Err("ADMIN_TOKEN needs ADMIN_ADDR".into()),
47 (Some(address), Some(token)) => Self::new(address, token).map(Some),
48 }
49 }
50
51 pub fn new(address: impl Into<String>, token: impl Into<String>) -> Result<Self, String> {
54 let token = token.into();
55 if token.len() < MIN_TOKEN || !token.bytes().all(|b| b.is_ascii_graphic()) {
56 return Err(format!(
57 "ADMIN_TOKEN must be at least {MIN_TOKEN} printable ASCII characters"
58 ));
59 }
60 Ok(Self {
61 address: address.into(),
62 token,
63 })
64 }
65
66 pub fn guard(&self, router: Router) -> Router {
68 let token = self.token.clone();
69 router.layer(middleware::from_fn(move |request, next| {
70 let token = token.clone();
71 async move { check(&token, request, next).await }
72 }))
73 }
74}
75
76async fn check(token: &str, request: Request, next: Next) -> Response {
77 let bearer = request
78 .headers()
79 .get(header::AUTHORIZATION)
80 .and_then(|v| v.to_str().ok())
81 .and_then(|v| v.strip_prefix("Bearer "));
82 match bearer {
83 Some(given) if same(given.trim().as_bytes(), token.as_bytes()) => next.run(request).await,
84 _ => crate::error_response(StatusCode::UNAUTHORIZED, &"the admin token is required"),
85 }
86}
87
88fn same(a: &[u8], b: &[u8]) -> bool {
91 a.len() == b.len() && a.iter().zip(b).fold(0, |differ, (x, y)| differ | (x ^ y)) == 0
92}
93
94#[cfg(test)]
95mod tests {
96 use axum::{body::Body, routing::get};
97 use tower::ServiceExt;
98
99 use super::*;
100
101 const TOKEN: &str = "a-long-enough-admin-token-for-the-tests";
102
103 #[test]
104 fn it_takes_both_settings_or_neither() {
105 let some = |s: &str| Some(s.to_owned());
106 assert!(Admin::from(None, None).unwrap().is_none());
107 assert!(Admin::from(some("0.0.0.0:9200"), None).is_err());
108 assert!(Admin::from(None, some(TOKEN)).is_err());
109 assert!(Admin::from(some("0.0.0.0:9200"), some("short")).is_err());
110 let spaced = format!("{TOKEN} with spaces");
111 assert!(
112 Admin::new("0.0.0.0:9200", spaced).is_err(),
113 "it goes in a header"
114 );
115 let admin = Admin::from(some("0.0.0.0:9200"), some(TOKEN))
116 .unwrap()
117 .unwrap();
118 assert_eq!(admin.address, "0.0.0.0:9200");
119 assert!(
120 !format!("{admin:?}").contains(TOKEN),
121 "the token stays out of logs"
122 );
123 }
124
125 #[tokio::test]
126 async fn only_requests_bearing_the_token_get_through() {
127 let admin = Admin::from(Some("unused".into()), Some(TOKEN.into()))
128 .unwrap()
129 .unwrap();
130 let app = admin.guard(Router::new().route("/admin/x", get(|| async { "done" })));
131 let status = |authorization: Option<String>| {
132 let app = app.clone();
133 async move {
134 let mut request = Request::get("/admin/x");
135 if let Some(value) = authorization {
136 request = request.header(header::AUTHORIZATION, value);
137 }
138 app.oneshot(request.body(Body::empty()).unwrap())
139 .await
140 .unwrap()
141 .status()
142 }
143 };
144 assert_eq!(status(None).await, StatusCode::UNAUTHORIZED);
145 assert_eq!(
146 status(Some("Bearer wrong".into())).await,
147 StatusCode::UNAUTHORIZED
148 );
149 assert_eq!(
150 status(Some(format!("Bearer {TOKEN}x"))).await,
151 StatusCode::UNAUTHORIZED
152 );
153 assert_eq!(
154 status(Some(format!("Bearer {TOKEN}"))).await,
155 StatusCode::OK
156 );
157 }
158
159 #[test]
160 fn comparing_is_exact() {
161 assert!(same(b"abc", b"abc"));
162 assert!(!same(b"abc", b"abd"));
163 assert!(!same(b"abc", b"ab"));
164 }
165}