1use ::auth::UserProviderRef;
16use api::v1::Basic;
17use axum::extract::{Request, State};
18use axum::http::{self, StatusCode};
19use axum::middleware::Next;
20use axum::response::{IntoResponse, Response};
21use base64::Engine;
22use base64::prelude::BASE64_STANDARD;
23use common_base::secrets::{ExposeSecret, SecretString};
24use common_catalog::consts::DEFAULT_SCHEMA_NAME;
25use common_catalog::parse_catalog_and_schema_from_db_string;
26use common_error::ext::ErrorExt;
27use common_telemetry::warn;
28use common_time::Timezone;
29use common_time::timezone::parse_timezone;
30use headers::Header;
31use session::context::QueryContextBuilder;
32use snafu::{OptionExt, ResultExt, ensure};
33
34use crate::error::{
35 self, InvalidAuthHeaderInvisibleASCIISnafu, InvalidAuthHeaderSnafu, InvalidParameterSnafu,
36 NotFoundAuthHeaderSnafu, NotFoundInfluxAuthSnafu, Result, UnsupportedAuthSchemeSnafu,
37 UrlDecodeSnafu,
38};
39use crate::http::header::{GREPTIME_TIMEZONE_HEADER_NAME, GreptimeDbName};
40use crate::http::result::error_result::ErrorResponse;
41use crate::http::splunk::is_splunk_request;
42use crate::http::{AUTHORIZATION_HEADER, HTTP_API_PREFIX, PUBLIC_API_PREFIX};
43use crate::influxdb::{is_influxdb_request, is_influxdb_v2_request};
44
45#[derive(Clone)]
48pub struct AuthState {
49 user_provider: Option<UserProviderRef>,
50}
51
52impl AuthState {
53 pub fn new(user_provider: Option<UserProviderRef>) -> Self {
54 Self { user_provider }
55 }
56}
57
58pub async fn inner_auth<B>(
59 user_provider: Option<UserProviderRef>,
60 mut req: Request<B>,
61) -> std::result::Result<Request<B>, Response> {
62 let (catalog, schema) = extract_catalog_and_schema(&req);
64 let timezone = extract_timezone(&req);
66 let query_ctx_builder = QueryContextBuilder::default()
67 .current_catalog(catalog.clone())
68 .current_schema(schema.clone())
69 .timezone(timezone);
70
71 let query_ctx = query_ctx_builder.build();
72 let need_auth = need_auth(&req);
73
74 let user_provider = if let Some(user_provider) = user_provider.filter(|_| need_auth) {
76 user_provider
77 } else {
78 query_ctx.set_current_user(auth::userinfo_by_name(None));
79 let _ = req.extensions_mut().insert(query_ctx);
80 return Ok(req);
81 };
82
83 if let Some(token) = extract_bearer_token(&req) {
88 match user_provider
89 .auth_bearer_token(token, &catalog, &schema)
90 .await
91 {
92 Ok(userinfo) => {
93 query_ctx.set_current_user(userinfo);
94 let _ = req.extensions_mut().insert(query_ctx);
95 return Ok(req);
96 }
97 Err(e) => {
98 warn!(e; "bearer token authentication failed");
99 crate::metrics::METRIC_AUTH_FAILURE
100 .with_label_values(&[e.status_code().as_ref()])
101 .inc();
102 if is_splunk_request(&req) {
105 return Err(splunk_hec_err(StatusCode::FORBIDDEN, 4));
106 }
107 return Err(err_response(e));
108 }
109 }
110 }
111
112 let (username, password) = match extract_username_and_password(&req) {
114 Ok((username, password)) => (username, password),
115 Err(e) => {
116 warn!(e; "extract username and password failed");
117 crate::metrics::METRIC_AUTH_FAILURE
118 .with_label_values(&[e.status_code().as_ref()])
119 .inc();
120 if is_splunk_request(&req) {
121 let (status, code) = match &e {
123 error::Error::NotFoundAuthHeader { .. } => (StatusCode::UNAUTHORIZED, 2),
124 _ => (StatusCode::FORBIDDEN, 4),
125 };
126 return Err(splunk_hec_err(status, code));
127 }
128 return Err(err_response(e));
129 }
130 };
131
132 match user_provider
134 .auth(
135 auth::Identity::UserId(&username, None),
136 auth::Password::PlainText(password),
137 &catalog,
138 &schema,
139 )
140 .await
141 {
142 Ok(userinfo) => {
143 query_ctx.set_current_user(userinfo);
144 let _ = req.extensions_mut().insert(query_ctx);
145 Ok(req)
146 }
147 Err(e) => {
148 warn!(e; "authenticate failed");
149 crate::metrics::METRIC_AUTH_FAILURE
150 .with_label_values(&[e.status_code().as_ref()])
151 .inc();
152 if is_splunk_request(&req) {
154 return Err(splunk_hec_err(StatusCode::FORBIDDEN, 4));
155 }
156 Err(err_response(e))
157 }
158 }
159}
160
161pub async fn check_http_auth(
162 State(auth_state): State<AuthState>,
163 req: Request,
164 next: Next,
165) -> Response {
166 match inner_auth(auth_state.user_provider, req).await {
167 Ok(req) => next.run(req).await,
168 Err(resp) => resp,
169 }
170}
171
172fn splunk_hec_err(status: StatusCode, code: u32) -> Response {
174 let text = match code {
175 2 => "Token is required",
176 4 => "Invalid token",
177 _ => "Unauthorized",
178 };
179 (
180 status,
181 axum::Json(serde_json::json!({ "text": text, "code": code })),
182 )
183 .into_response()
184}
185
186fn err_response(err: impl ErrorExt) -> Response {
187 (StatusCode::UNAUTHORIZED, ErrorResponse::from_error(err)).into_response()
188}
189
190pub fn extract_catalog_and_schema<B>(request: &Request<B>) -> (String, String) {
191 let dbname = request
193 .headers()
194 .get(GreptimeDbName::name())
195 .and_then(|header| header.to_str().ok())
197 .or_else(|| {
198 let query = request.uri().query().unwrap_or_default();
199 if is_influxdb_v2_request(request) {
200 extract_db_from_query(query).or_else(|| extract_bucket_from_query(query))
201 } else {
202 extract_db_from_query(query)
203 }
204 })
205 .unwrap_or(DEFAULT_SCHEMA_NAME);
206
207 parse_catalog_and_schema_from_db_string(dbname)
208}
209
210fn extract_timezone<B>(request: &Request<B>) -> Timezone {
211 let timezone = request
213 .headers()
214 .get(&GREPTIME_TIMEZONE_HEADER_NAME)
215 .and_then(|header| header.to_str().ok())
217 .unwrap_or("");
218 parse_timezone(Some(timezone))
219}
220
221fn get_influxdb_credentials<B>(request: &Request<B>) -> Result<Option<(Username, Password)>> {
222 if let Some(header) = request.headers().get(http::header::AUTHORIZATION) {
224 let (auth_scheme, credential) = header
226 .to_str()
227 .context(InvalidAuthHeaderInvisibleASCIISnafu)?
228 .split_once(' ')
229 .context(InvalidAuthHeaderSnafu)?;
230
231 let (username, password) = match auth_scheme.to_lowercase().as_str() {
232 "token" => {
233 let (u, p) = credential.split_once(':').context(InvalidAuthHeaderSnafu)?;
234 (u.to_string(), p.to_string().into())
235 }
236 "basic" => decode_basic(credential)?,
237 _ => UnsupportedAuthSchemeSnafu { name: auth_scheme }.fail()?,
238 };
239
240 Ok(Some((username, password)))
241 } else {
242 let Some(query_str) = request.uri().query() else {
244 return Ok(None);
245 };
246
247 let query_str = urlencoding::decode(query_str).context(UrlDecodeSnafu)?;
248
249 match extract_influxdb_user_from_query(&query_str) {
250 (None, None) => Ok(None),
251 (Some(username), Some(password)) => {
252 Ok(Some((username.to_string(), password.to_string().into())))
253 }
254 _ => InvalidParameterSnafu {
255 reason: "influxdb auth: username and password must be provided together"
256 .to_string(),
257 }
258 .fail(),
259 }
260 }
261}
262
263fn get_splunk_credentials<B>(request: &Request<B>) -> Result<Option<(Username, Password)>> {
264 let Some(header) = request.headers().get(http::header::AUTHORIZATION) else {
265 return Ok(None);
266 };
267 let (auth_scheme, credential) = header
268 .to_str()
269 .context(InvalidAuthHeaderInvisibleASCIISnafu)?
270 .split_once(' ')
271 .context(InvalidAuthHeaderSnafu)?;
272
273 let (username, password) = match auth_scheme.to_lowercase().as_str() {
274 "splunk" => {
275 let (u, p) = credential.split_once(':').context(InvalidAuthHeaderSnafu)?;
276 (u.to_string(), p.to_string().into())
277 }
278 "basic" => decode_basic(credential)?,
279 _ => UnsupportedAuthSchemeSnafu { name: auth_scheme }.fail()?,
280 };
281 Ok(Some((username, password)))
282}
283
284pub fn extract_username_and_password<B>(request: &Request<B>) -> Result<(Username, Password)> {
285 Ok(if is_influxdb_request(request) {
286 get_influxdb_credentials(request)?.context(NotFoundInfluxAuthSnafu)?
288 } else if is_splunk_request(request) {
289 get_splunk_credentials(request)?.context(NotFoundAuthHeaderSnafu)?
290 } else {
291 let scheme = auth_header(request)?;
293 match scheme {
294 AuthScheme::Basic(username, password) => (username, password),
295 }
296 })
297}
298
299#[derive(Debug)]
300pub enum AuthScheme {
301 Basic(Username, Password),
302}
303
304type Username = String;
305type Password = SecretString;
306
307impl TryFrom<&str> for AuthScheme {
308 type Error = error::Error;
309
310 fn try_from(value: &str) -> Result<Self> {
311 let (scheme, encoded_credentials) =
312 value.split_once(' ').context(InvalidAuthHeaderSnafu)?;
313
314 ensure!(!encoded_credentials.contains(' '), InvalidAuthHeaderSnafu);
315
316 match scheme.to_lowercase().as_str() {
317 "basic" => decode_basic(encoded_credentials)
318 .map(|(username, password)| AuthScheme::Basic(username, password)),
319 other => UnsupportedAuthSchemeSnafu { name: other }.fail(),
320 }
321 }
322}
323
324impl From<AuthScheme> for api::v1::auth_header::AuthScheme {
325 fn from(value: AuthScheme) -> Self {
326 match value {
327 AuthScheme::Basic(username, password) => {
328 api::v1::auth_header::AuthScheme::Basic(Basic {
329 username,
330 password: password.expose_secret().clone(),
331 })
332 }
333 }
334 }
335}
336
337type Credential<'a> = &'a str;
338
339fn extract_bearer_token<B>(req: &Request<B>) -> Option<&str> {
345 let header = req
346 .headers()
347 .get(AUTHORIZATION_HEADER)
348 .or_else(|| req.headers().get(http::header::AUTHORIZATION))?;
349 let value = header.to_str().ok()?;
350 let (scheme, token) = value.split_once(' ')?;
355 if !scheme.eq_ignore_ascii_case("bearer") {
356 return None;
357 }
358 let token = token.trim_start();
359 (!token.is_empty()).then_some(token)
360}
361
362fn auth_header<B>(req: &Request<B>) -> Result<AuthScheme> {
363 let auth_header = req
364 .headers()
365 .get(AUTHORIZATION_HEADER)
366 .or_else(|| req.headers().get(http::header::AUTHORIZATION))
367 .context(error::NotFoundAuthHeaderSnafu)?
368 .to_str()
369 .context(InvalidAuthHeaderInvisibleASCIISnafu)?;
370
371 auth_header.try_into()
372}
373
374fn decode_basic(credential: Credential) -> Result<(Username, Password)> {
375 let decoded = BASE64_STANDARD
376 .decode(credential)
377 .context(error::InvalidBase64ValueSnafu)?;
378 let as_utf8 =
379 String::from_utf8(decoded).context(error::InvalidAuthHeaderInvalidUtf8ValueSnafu)?;
380
381 if let Some((user_id, password)) = as_utf8.split_once(':') {
382 return Ok((user_id.to_string(), password.to_string().into()));
383 }
384
385 InvalidAuthHeaderSnafu {}.fail()
386}
387
388fn need_auth<B>(req: &Request<B>) -> bool {
389 let path = req.uri().path();
390
391 for api in PUBLIC_API_PREFIX {
392 if path.starts_with(api) {
393 return false;
394 }
395 }
396
397 path.starts_with(HTTP_API_PREFIX)
398}
399
400fn extract_param_from_query<'a>(query: &'a str, param: &'a str) -> Option<&'a str> {
401 let prefix = format!("{}=", param);
402 for pair in query.split('&') {
403 if let Some(param) = pair.strip_prefix(&prefix) {
404 return if param.is_empty() { None } else { Some(param) };
405 }
406 }
407 None
408}
409
410fn extract_db_from_query(query: &str) -> Option<&str> {
411 extract_param_from_query(query, "db")
412}
413
414fn extract_bucket_from_query(query: &str) -> Option<&str> {
417 extract_param_from_query(query, "bucket")
418}
419
420fn extract_influxdb_user_from_query(query: &str) -> (Option<&str>, Option<&str>) {
421 let mut username = None;
422 let mut password = None;
423
424 for pair in query.split('&') {
425 if pair.starts_with("u=") && pair.len() > 2 {
426 username = Some(&pair[2..]);
427 } else if pair.starts_with("p=") && pair.len() > 2 {
428 password = Some(&pair[2..]);
429 }
430 }
431 (username, password)
432}
433
434#[cfg(test)]
435mod tests {
436 use std::assert_matches;
437
438 use common_base::secrets::ExposeSecret;
439
440 use super::*;
441
442 #[test]
443 fn test_need_auth() {
444 let req = Request::builder()
445 .uri("http://127.0.0.1/v1/influxdb/ping")
446 .body(())
447 .unwrap();
448
449 assert!(!need_auth(&req));
450
451 let req = Request::builder()
452 .uri("http://127.0.0.1/v1/influxdb/health")
453 .body(())
454 .unwrap();
455
456 assert!(!need_auth(&req));
457
458 let req = Request::builder()
459 .uri("http://127.0.0.1/v1/influxdb/write")
460 .body(())
461 .unwrap();
462
463 assert!(need_auth(&req));
464 }
465
466 #[test]
467 fn test_splunk_auth() {
468 let splunk_uri = "http://127.0.0.1/v1/splunk/services/collector/event";
469 let splunk_req = |auth: Option<&str>| {
470 let mut req = Request::builder().uri(splunk_uri);
471 if let Some(auth) = auth {
472 req = req.header(http::header::AUTHORIZATION, auth);
473 }
474 req.body(()).unwrap()
475 };
476
477 assert!(is_splunk_request(&splunk_req(None)));
479 assert!(!is_splunk_request(
480 &Request::builder()
481 .uri("http://127.0.0.1/v1/influxdb/write")
482 .body(())
483 .unwrap()
484 ));
485 assert!(!is_splunk_request(
486 &Request::builder()
487 .uri("http://127.0.0.1/v1/sql")
488 .body(())
489 .unwrap()
490 ));
491
492 let (username, password) =
494 get_splunk_credentials(&splunk_req(Some("Splunk teamA:secretA")))
495 .unwrap()
496 .unwrap();
497 assert_eq!(username, "teamA");
498 assert_eq!(password.expose_secret(), "secretA");
499
500 let basic = basic_auth("u", "p");
502 let (username, password) = get_splunk_credentials(&splunk_req(Some(&basic)))
503 .unwrap()
504 .unwrap();
505 assert_eq!(username, "u");
506 assert_eq!(password.expose_secret(), "p");
507
508 assert!(get_splunk_credentials(&splunk_req(None)).unwrap().is_none());
510 assert!(get_splunk_credentials(&splunk_req(Some("Splunk no_colon_token"))).is_err());
511
512 let (username, password) =
514 extract_username_and_password(&splunk_req(Some("Splunk teamA:secretA"))).unwrap();
515 assert_eq!(username, "teamA");
516 assert_eq!(password.expose_secret(), "secretA");
517 }
518
519 #[test]
520 fn test_decode_basic() {
521 let credential = basic_auth_credentials("username", "password");
522 let (username, pwd) = decode_basic(&credential).unwrap();
523 assert_eq!("username", username);
524 assert_eq!("password", pwd.expose_secret());
525
526 let wrong_credential = credential.replacen('c', "c ", 1);
527 let result = decode_basic(&wrong_credential);
528 assert_matches!(result.err(), Some(error::Error::InvalidBase64Value { .. }));
529 }
530
531 #[test]
532 fn test_try_into_auth_scheme() {
533 let auth_scheme_str = "basic";
534 let re: Result<AuthScheme> = auth_scheme_str.try_into();
535 assert!(re.is_err());
536
537 let auth_scheme_str = basic_auth("test", "test");
538 let scheme: AuthScheme = auth_scheme_str.as_str().try_into().unwrap();
539 assert_matches!(scheme, AuthScheme::Basic(username, pwd) if username == "test" && pwd.expose_secret() == "test");
540
541 let unsupported = "digest";
542 let auth_scheme: Result<AuthScheme> = unsupported.try_into();
543 assert!(auth_scheme.is_err());
544 }
545
546 #[test]
547 fn test_inner_auth_assigns_remote_query_id() {
548 let req =
549 mock_http_request(None, Some("http://127.0.0.1/v1/sql?db=greptime-public")).unwrap();
550 let req = futures::executor::block_on(inner_auth::<()>(None, req)).unwrap();
551 let query_ctx = req
552 .extensions()
553 .get::<session::context::QueryContext>()
554 .unwrap();
555
556 assert!(query_ctx.remote_query_id().is_some());
557 }
558
559 #[test]
560 fn test_auth_header() {
561 let header_value = basic_auth("username", "password");
562 let req = mock_http_request(Some(&header_value), None).unwrap();
563
564 let auth_scheme = auth_header(&req).unwrap();
565 assert_matches!(auth_scheme, AuthScheme::Basic(username, pwd) if username == "username" && pwd.expose_secret() == "password");
566
567 let wrong_auth_header = header_value.replacen('c', "c ", 1);
568 let wrong_req = mock_http_request(Some(&wrong_auth_header), None).unwrap();
569 let res = auth_header(&wrong_req);
570 assert_matches!(res.err(), Some(error::Error::InvalidAuthHeader { .. }));
571
572 let wrong_req = mock_http_request(
573 Some(&format!(
574 "Digest {}",
575 basic_auth_credentials("username", "password")
576 )),
577 None,
578 )
579 .unwrap();
580 let res = auth_header(&wrong_req);
581 assert_matches!(res.err(), Some(error::Error::UnsupportedAuthScheme { .. }));
582 }
583
584 fn basic_auth(username: &str, password: &str) -> String {
585 format!("Basic {}", basic_auth_credentials(username, password))
586 }
587
588 fn basic_auth_credentials(username: &str, password: &str) -> String {
589 BASE64_STANDARD.encode(format!("{username}:{password}"))
590 }
591
592 fn mock_http_request(auth_header: Option<&str>, uri: Option<&str>) -> Result<Request<()>> {
593 let http_api_version = crate::http::HTTP_API_VERSION;
594 let mut req = Request::builder()
595 .uri(uri.unwrap_or(format!("http://localhost/{http_api_version}/sql").as_str()));
596 if let Some(auth_header) = auth_header {
597 req = req.header(http::header::AUTHORIZATION, auth_header);
598 }
599
600 Ok(req.body(()).unwrap())
601 }
602
603 #[test]
604 fn test_db_name_header() {
605 let http_api_version = crate::http::HTTP_API_VERSION;
606 let req = Request::builder()
607 .uri(format!("http://localhost/{http_api_version}/sql").as_str())
608 .header(GreptimeDbName::name(), "greptime-tomcat")
609 .body(())
610 .unwrap();
611
612 let db = extract_catalog_and_schema(&req);
613 assert_eq!(db, ("greptime".to_string(), "tomcat".to_string()));
614 }
615
616 #[test]
617 fn test_extract_db() {
618 assert_matches!(extract_db_from_query(""), None);
619 assert_matches!(extract_db_from_query("&"), None);
620 assert_matches!(extract_db_from_query("db="), None);
621 assert_matches!(extract_bucket_from_query("bucket="), None);
622 assert_matches!(extract_bucket_from_query("db=foo"), None);
623 assert_matches!(extract_db_from_query("db=foo"), Some("foo"));
624 assert_matches!(extract_bucket_from_query("bucket=foo"), Some("foo"));
625 assert_matches!(extract_db_from_query("name=bar"), None);
626 assert_matches!(extract_db_from_query("db=&name=bar"), None);
627 assert_matches!(extract_db_from_query("db=foo&name=bar"), Some("foo"));
628 assert_matches!(extract_bucket_from_query("db=foo&bucket=bar"), Some("bar"));
629 assert_matches!(extract_db_from_query("name=bar&db="), None);
630 assert_matches!(extract_db_from_query("name=bar&db=foo"), Some("foo"));
631 assert_matches!(extract_db_from_query("name=bar&db=&name=bar"), None);
632 assert_matches!(
633 extract_db_from_query("name=bar&db=foo&name=bar"),
634 Some("foo")
635 );
636 }
637
638 #[test]
639 fn test_extract_user() {
640 assert_matches!(extract_influxdb_user_from_query(""), (None, None));
641 assert_matches!(extract_influxdb_user_from_query("u="), (None, None));
642 assert_matches!(
643 extract_influxdb_user_from_query("u=123"),
644 (Some("123"), None)
645 );
646 assert_matches!(
647 extract_influxdb_user_from_query("u=123&p="),
648 (Some("123"), None)
649 );
650 assert_matches!(
651 extract_influxdb_user_from_query("u=123&p=4"),
652 (Some("123"), Some("4"))
653 );
654 assert_matches!(extract_influxdb_user_from_query("p="), (None, None));
655 assert_matches!(extract_influxdb_user_from_query("p=4"), (None, Some("4")));
656 assert_matches!(
657 extract_influxdb_user_from_query("p=4&u="),
658 (None, Some("4"))
659 );
660 assert_matches!(
661 extract_influxdb_user_from_query("p=4&u=123"),
662 (Some("123"), Some("4"))
663 );
664 }
665
666 #[test]
667 fn test_extract_bearer_token() {
668 let bearer = |scheme: &str, val: &str| {
669 mock_http_request(Some(&format!("{scheme} {val}")), None).unwrap()
670 };
671
672 assert_eq!(
674 extract_bearer_token(&bearer("Bearer", "abc.def.ghi")),
675 Some("abc.def.ghi")
676 );
677 assert_eq!(extract_bearer_token(&bearer("bearer", "tok")), Some("tok"));
678 assert_eq!(
681 extract_bearer_token(&bearer("BEARER", "ABC.DEF.GHI")),
682 Some("ABC.DEF.GHI")
683 );
684 assert_eq!(extract_bearer_token(&bearer("BeArEr", "tok")), Some("tok"));
685 let mut req = mock_http_request(Some("Bearer xyz"), None).unwrap();
686 req.headers_mut().insert(
687 AUTHORIZATION_HEADER,
688 "Bearer from-x-greptime".parse().unwrap(),
689 );
690 assert_eq!(extract_bearer_token(&req), Some("from-x-greptime"));
691
692 assert_eq!(extract_bearer_token(&bearer("Basic", "dXNlcjpwYXNz")), None);
694 assert_eq!(extract_bearer_token(&bearer("Token", "u:p")), None);
695 assert_eq!(extract_bearer_token(&bearer("Splunk", "u:p")), None);
696
697 assert_eq!(
699 extract_bearer_token(&mock_http_request(None, None).unwrap()),
700 None
701 );
702 assert_eq!(extract_bearer_token(&bearer("Bearer", "")), None);
703 }
704
705 struct TokenUserProvider {
708 token: String,
709 user: auth::UserInfoRef,
710 }
711
712 #[async_trait::async_trait]
713 impl auth::UserProvider for TokenUserProvider {
714 fn name(&self) -> &str {
715 "token-test"
716 }
717
718 async fn authenticate(
719 &self,
720 _: auth::Identity<'_>,
721 _: auth::Password<'_>,
722 ) -> auth::error::Result<auth::UserInfoRef> {
723 unreachable!("password auth should not be reached for a bearer request")
724 }
725
726 async fn authorize(
727 &self,
728 _: &str,
729 _: &str,
730 _: &auth::UserInfoRef,
731 ) -> auth::error::Result<()> {
732 Ok(())
733 }
734
735 async fn auth_bearer_token(
736 &self,
737 token: &str,
738 _: &str,
739 _: &str,
740 ) -> auth::error::Result<auth::UserInfoRef> {
741 if token == self.token {
742 Ok(self.user.clone())
743 } else {
744 auth::error::UnsupportedAuthMethodSnafu {
745 method: "bearer token",
746 }
747 .fail()
748 }
749 }
750 }
751
752 #[tokio::test]
753 async fn test_bearer_token_dispatches_to_auth_bearer_token() {
754 let provider = TokenUserProvider {
755 token: "good-token".to_string(),
756 user: auth::userinfo_by_name(Some("alice".into())),
757 };
758 let req = mock_http_request(Some("Bearer good-token"), None).unwrap();
759
760 let req = inner_auth::<()>(
761 Some(std::sync::Arc::new(provider) as auth::UserProviderRef),
762 req,
763 )
764 .await
765 .expect("valid bearer token authenticates");
766
767 let user = req
768 .extensions()
769 .get::<session::context::QueryContext>()
770 .expect("query context is populated")
771 .current_user();
772 assert_eq!(user.username(), "alice");
773 }
774
775 #[tokio::test]
776 async fn test_bearer_token_failure_rejects() {
777 let provider = TokenUserProvider {
778 token: "good-token".to_string(),
779 user: auth::userinfo_by_name(Some("alice".into())),
780 };
781 let req = mock_http_request(Some("Bearer bad-token"), None).unwrap();
782
783 let result = inner_auth::<()>(
784 Some(std::sync::Arc::new(provider) as auth::UserProviderRef),
785 req,
786 )
787 .await;
788 assert!(result.is_err(), "an invalid bearer token is rejected");
789 }
790
791 #[tokio::test]
792 async fn test_default_provider_rejects_bearer() {
793 let provider =
795 auth::user_provider_from_option("static_user_provider:cmd:alice=s3cret").unwrap();
796 let req = mock_http_request(Some("Bearer some-jwt"), None).unwrap();
797 let result = inner_auth::<()>(Some(provider), req).await;
798 assert!(
799 result.is_err(),
800 "password-only providers reject bearer tokens"
801 );
802 }
803
804 #[tokio::test]
809 async fn test_bearer_failure_on_splunk_keeps_hec_contract() {
810 let provider = TokenUserProvider {
811 token: "good-token".to_string(),
812 user: auth::userinfo_by_name(Some("alice".into())),
813 };
814 let req = mock_http_request(
815 Some("Bearer bad-token"),
816 Some("http://127.0.0.1/v1/splunk/services/collector/event"),
817 )
818 .unwrap();
819
820 let resp = inner_auth::<()>(
821 Some(std::sync::Arc::new(provider) as auth::UserProviderRef),
822 req,
823 )
824 .await
825 .expect_err("an invalid bearer token is rejected");
826
827 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
828 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
829 .await
830 .unwrap();
831 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
832 assert_eq!(payload["code"], 4);
833 assert_eq!(payload["text"], "Invalid token");
834 }
835}