infrastore_server/
auth.rs1use std::sync::Arc;
4
5use tonic::Status;
6use tonic::service::Interceptor;
7
8const HEADER: &str = "x-api-key";
9
10#[derive(Clone)]
11pub struct ApiKeyInterceptor {
12 keys: Arc<Vec<String>>,
13}
14
15impl ApiKeyInterceptor {
16 pub fn new(keys: Vec<String>) -> Self {
17 Self {
18 keys: Arc::new(keys),
19 }
20 }
21}
22
23impl Interceptor for ApiKeyInterceptor {
24 fn call(&mut self, req: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
25 let header = req
26 .metadata()
27 .get(HEADER)
28 .ok_or_else(|| Status::unauthenticated("missing x-api-key header"))?;
29 let supplied = header
30 .to_str()
31 .map_err(|_| Status::unauthenticated("x-api-key is not valid ASCII"))?;
32 if !any_match(&self.keys, supplied) {
33 return Err(Status::unauthenticated("invalid x-api-key"));
34 }
35 Ok(req)
36 }
37}
38
39fn any_match(keys: &[String], supplied: &str) -> bool {
44 let supplied = supplied.as_bytes();
45 let mut found = false;
46 for k in keys {
47 let k = k.as_bytes();
48 if k.len() != supplied.len() {
49 continue;
51 }
52 let mut diff: u8 = 0;
53 for (a, b) in k.iter().zip(supplied.iter()) {
54 diff |= a ^ b;
55 }
56 found |= diff == 0;
57 }
58 found
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn matches_valid_key() {
67 let keys = vec!["secret-1".to_string(), "secret-2".to_string()];
68 assert!(any_match(&keys, "secret-1"));
69 assert!(any_match(&keys, "secret-2"));
70 }
71
72 #[test]
73 fn rejects_unknown_key() {
74 let keys = vec!["secret-1".to_string()];
75 assert!(!any_match(&keys, "nope"));
76 assert!(!any_match(&keys, ""));
77 assert!(!any_match(&keys, "secret-12"));
78 }
79}