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