1use std::collections::{HashMap, HashSet};
33use std::sync::Arc;
34
35use base64::Engine;
36use base64::engine::general_purpose::STANDARD as BASE64;
37use tonic::Status;
38
39#[derive(Debug, Clone)]
41pub struct BasicCredential {
42 pub username: String,
43 pub password: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub enum Identity {
59 Token(String),
61 Basic(String),
63 Anonymous,
65}
66
67impl Identity {
68 pub fn name(&self) -> &str {
71 match self {
72 Identity::Token(name) | Identity::Basic(name) => name,
73 Identity::Anonymous => "*",
74 }
75 }
76}
77
78#[derive(Clone, Default)]
86pub enum Authorizer {
87 #[default]
89 AllowAll,
90 AllowList(HashMap<String, HashSet<String>>),
94 Custom(Arc<dyn Fn(&Identity, &str) -> bool + Send + Sync>),
96}
97
98impl std::fmt::Debug for Authorizer {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Authorizer::AllowAll => f.write_str("Authorizer::AllowAll"),
102 Authorizer::AllowList(m) => f.debug_tuple("Authorizer::AllowList").field(m).finish(),
103 Authorizer::Custom(_) => f.write_str("Authorizer::Custom(<fn>)"),
104 }
105 }
106}
107
108impl Authorizer {
109 pub fn allow_list<I, S1, S2>(entries: I) -> Self
113 where
114 I: IntoIterator<Item = (S1, Vec<S2>)>,
115 S1: Into<String>,
116 S2: Into<String>,
117 {
118 let map = entries
119 .into_iter()
120 .map(|(id, queries)| (id.into(), queries.into_iter().map(Into::into).collect()))
121 .collect();
122 Authorizer::AllowList(map)
123 }
124
125 pub fn custom<F>(f: F) -> Self
127 where
128 F: Fn(&Identity, &str) -> bool + Send + Sync + 'static,
129 {
130 Authorizer::Custom(Arc::new(f))
131 }
132
133 pub fn is_allowed(&self, identity: &Identity, query_name: &str) -> bool {
135 match self {
136 Authorizer::AllowAll => true,
137 Authorizer::AllowList(map) => {
138 let allowed = |key: &str| map.get(key).is_some_and(|qs| qs.contains(query_name));
139 allowed(identity.name()) || allowed("*")
141 }
142 Authorizer::Custom(f) => f(identity, query_name),
143 }
144 }
145
146 pub fn authorize(&self, identity: &Identity, query_name: &str) -> Result<(), Status> {
148 if self.is_allowed(identity, query_name) {
149 Ok(())
150 } else {
151 Err(Status::permission_denied(format!(
152 "identity '{}' is not permitted to run query '{query_name}'",
153 identity.name()
154 )))
155 }
156 }
157}
158
159#[derive(Debug, Clone, Default)]
162pub struct AuthConfig {
163 bearer_tokens: Vec<(String, String)>,
167 pub basic: Option<BasicCredential>,
169 pub authorizer: Authorizer,
172}
173
174impl AuthConfig {
175 pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
180 self.add_bearer_token(token);
181 self
182 }
183
184 pub fn with_bearer_tokens<I, S>(mut self, tokens: I) -> Self
191 where
192 I: IntoIterator<Item = S>,
193 S: Into<String>,
194 {
195 for t in tokens {
196 self.add_bearer_token(t);
197 }
198 self
199 }
200
201 pub fn with_named_bearer_tokens<I, S1, S2>(mut self, tokens: I) -> Self
206 where
207 I: IntoIterator<Item = (S1, S2)>,
208 S1: Into<String>,
209 S2: Into<String>,
210 {
211 for (token, name) in tokens {
212 self.add_named_bearer_token(token, name);
213 }
214 self
215 }
216
217 pub fn add_bearer_token(&mut self, token: impl Into<String>) -> &mut Self {
220 let token = token.into();
221 let name = token.clone();
222 self.add_named_bearer_token(token, name)
223 }
224
225 pub fn add_named_bearer_token(
227 &mut self,
228 token: impl Into<String>,
229 name: impl Into<String>,
230 ) -> &mut Self {
231 self.bearer_tokens.push((token.into(), name.into()));
232 self
233 }
234
235 pub fn with_basic(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
237 self.basic = Some(BasicCredential {
238 username: username.into(),
239 password: password.into(),
240 });
241 self
242 }
243
244 pub fn with_authorizer(mut self, authorizer: Authorizer) -> Self {
246 self.authorizer = authorizer;
247 self
248 }
249
250 pub fn authorize(&self, identity: &Identity, query_name: &str) -> Result<(), Status> {
253 self.authorizer.authorize(identity, query_name)
254 }
255
256 pub fn bearer_tokens(&self) -> impl Iterator<Item = &str> {
258 self.bearer_tokens.iter().map(|(t, _)| t.as_str())
259 }
260
261 pub fn is_enabled(&self) -> bool {
264 !self.bearer_tokens.is_empty() || self.basic.is_some()
265 }
266
267 pub fn issued_token(&self) -> Option<&str> {
270 self.bearer_tokens.first().map(|(t, _)| t.as_str())
271 }
272
273 pub fn check_header(&self, header: Option<&str>) -> Result<(), Status> {
279 self.authenticate(header).map(|_| ())
280 }
281
282 pub fn authenticate(&self, header: Option<&str>) -> Result<Identity, Status> {
289 if !self.is_enabled() {
290 return Ok(Identity::Anonymous);
291 }
292 let value = header.ok_or_else(|| {
293 Status::unauthenticated(
294 "missing 'authorization' header (Bearer token or Basic credentials)",
295 )
296 })?;
297
298 if let Some(token) = value.strip_prefix("Bearer ") {
299 for (expected, name) in &self.bearer_tokens {
301 if constant_time_eq(token.as_bytes(), expected.as_bytes()) {
302 return Ok(Identity::Token(name.clone()));
303 }
304 }
305 } else if let Some(user) = value
306 .strip_prefix("Basic ")
307 .and_then(|b64| self.check_basic(b64))
308 {
309 return Ok(Identity::Basic(user));
310 }
311 Err(Status::unauthenticated("invalid credentials"))
312 }
313
314 pub fn check_basic(&self, b64: &str) -> Option<String> {
317 let expected = self.basic.as_ref()?;
318 let decoded = BASE64.decode(b64).ok()?;
319 let text = std::str::from_utf8(&decoded).ok()?;
320 let (user, pass) = text.split_once(':')?;
321 let ok = constant_time_eq(user.as_bytes(), expected.username.as_bytes())
324 & constant_time_eq(pass.as_bytes(), expected.password.as_bytes());
325 ok.then(|| user.to_string())
326 }
327}
328
329pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
331 if a.len() != b.len() {
332 return false;
333 }
334 a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn basic_header(user: &str, pass: &str) -> String {
342 format!("Basic {}", BASE64.encode(format!("{user}:{pass}")))
343 }
344
345 #[test]
346 fn open_server_accepts_anything() {
347 let auth = AuthConfig::default();
348 assert!(!auth.is_enabled());
349 assert!(auth.check_header(None).is_ok());
350 assert!(auth.check_header(Some("garbage")).is_ok());
351 assert_eq!(auth.authenticate(None).unwrap(), Identity::Anonymous);
352 }
353
354 #[test]
355 fn bearer_token_accepted_and_rejected() {
356 let auth = AuthConfig::default().with_bearer_token("s3cret");
357 assert!(auth.check_header(Some("Bearer s3cret")).is_ok());
358 assert!(auth.check_header(Some("Bearer nope")).is_err());
359 assert!(auth.check_header(None).is_err());
360 assert_eq!(
361 auth.authenticate(Some("Bearer s3cret")).unwrap(),
362 Identity::Token("s3cret".into())
363 );
364 }
365
366 #[test]
367 fn multiple_tokens_all_accepted_for_rotation() {
368 let auth = AuthConfig::default().with_bearer_tokens(["old-tok", "new-tok"]);
370 assert!(auth.check_header(Some("Bearer old-tok")).is_ok());
371 assert!(auth.check_header(Some("Bearer new-tok")).is_ok());
372 assert!(auth.check_header(Some("Bearer retired-tok")).is_err());
374 assert_eq!(auth.issued_token(), Some("old-tok"));
376 }
377
378 #[test]
379 fn add_bearer_token_grows_the_set() {
380 let mut auth = AuthConfig::default().with_bearer_token("t1");
381 auth.add_bearer_token("t2");
382 assert!(auth.check_header(Some("Bearer t1")).is_ok());
383 assert!(auth.check_header(Some("Bearer t2")).is_ok());
384 assert_eq!(auth.bearer_tokens().collect::<Vec<_>>(), vec!["t1", "t2"]);
385 }
386
387 #[test]
388 fn named_tokens_share_an_identity() {
389 let auth = AuthConfig::default()
391 .with_named_bearer_tokens([("tok-a", "analyst"), ("tok-b", "analyst")]);
392 assert_eq!(
393 auth.authenticate(Some("Bearer tok-a")).unwrap(),
394 Identity::Token("analyst".into())
395 );
396 assert_eq!(
397 auth.authenticate(Some("Bearer tok-b")).unwrap(),
398 Identity::Token("analyst".into())
399 );
400 }
401
402 #[test]
403 fn basic_credentials_accepted_and_rejected() {
404 let auth = AuthConfig::default().with_basic("admin", "pw");
405 assert!(
406 auth.check_header(Some(&basic_header("admin", "pw")))
407 .is_ok()
408 );
409 assert!(
410 auth.check_header(Some(&basic_header("admin", "wrong")))
411 .is_err()
412 );
413 assert!(auth.check_header(Some(&basic_header("eve", "pw"))).is_err());
414 assert!(auth.check_header(Some("Basic !!!notbase64")).is_err());
415 assert_eq!(
416 auth.authenticate(Some(&basic_header("admin", "pw")))
417 .unwrap(),
418 Identity::Basic("admin".into())
419 );
420 }
421
422 #[test]
423 fn both_methods_accepted_when_both_configured() {
424 let auth = AuthConfig::default()
425 .with_bearer_token("tok")
426 .with_basic("u", "p");
427 assert!(auth.check_header(Some("Bearer tok")).is_ok());
428 assert!(auth.check_header(Some(&basic_header("u", "p"))).is_ok());
429 assert!(auth.check_header(Some("Bearer bad")).is_err());
430 }
431
432 #[test]
433 fn issued_token_is_the_first_bearer_token() {
434 let auth = AuthConfig::default().with_bearer_tokens(["tok", "tok2"]);
435 assert_eq!(auth.issued_token(), Some("tok"));
436 assert_eq!(AuthConfig::default().issued_token(), None);
437 }
438
439 #[test]
440 fn authorizer_allow_all_permits_everything() {
441 let authz = Authorizer::AllowAll;
442 assert!(authz.is_allowed(&Identity::Token("anyone".into()), "q1"));
443 assert!(authz.is_allowed(&Identity::Anonymous, "q2"));
444 assert!(
445 authz
446 .authorize(&Identity::Basic("u".into()), "anything")
447 .is_ok()
448 );
449 }
450
451 #[test]
452 fn authorizer_allow_list_allows_and_denies_per_identity() {
453 let authz = Authorizer::allow_list([("analyst", vec!["q1"])]);
455 let analyst = Identity::Token("analyst".into());
456 assert!(authz.is_allowed(&analyst, "q1"));
457 assert!(!authz.is_allowed(&analyst, "q2"));
458 assert!(authz.authorize(&analyst, "q1").is_ok());
459 let denied = authz.authorize(&analyst, "q2").unwrap_err();
460 assert_eq!(denied.code(), tonic::Code::PermissionDenied);
461 assert!(!authz.is_allowed(&Identity::Token("stranger".into()), "q1"));
463 }
464
465 #[test]
466 fn authorizer_allow_list_wildcard_fallback() {
467 let authz = Authorizer::allow_list([("analyst", vec!["q1"]), ("*", vec!["q_public"])]);
469 let analyst = Identity::Token("analyst".into());
470 assert!(authz.is_allowed(&analyst, "q1"));
471 assert!(authz.is_allowed(&analyst, "q_public")); let other = Identity::Token("other".into());
473 assert!(authz.is_allowed(&other, "q_public"));
474 assert!(!authz.is_allowed(&other, "q1"));
475 }
476
477 #[test]
478 fn authorizer_custom_closure() {
479 let authz = Authorizer::custom(|id, q| id.name() == "root" || q == "q_open");
480 assert!(authz.is_allowed(&Identity::Token("root".into()), "anything"));
481 assert!(authz.is_allowed(&Identity::Token("nobody".into()), "q_open"));
482 assert!(!authz.is_allowed(&Identity::Token("nobody".into()), "q_secret"));
483 }
484}