diff --git a/src/handlers/http/middleware.rs b/src/handlers/http/middleware.rs index 212b0f263..cd3797bfb 100644 --- a/src/handlers/http/middleware.rs +++ b/src/handlers/http/middleware.rs @@ -168,7 +168,7 @@ where let mut header_error = None; let user_and_tenant_id = get_user_and_tenant(&self.action, &mut req, &mut header_error); - // If an X-API-KEY header is present, resolve it to the backing + // If an API key is present in X-API-KEY or as a Bearer token, resolve it to the backing // `UserType::ApiKey` user and register an ephemeral session whose // permissions are derived from the user's assigned roles (same flow // as native/OAuth users). The request then falls through to the @@ -188,12 +188,11 @@ where permissions, &user.tenant, ); - if PARSEABLE.options.is_multi_tenant() { - req.headers_mut().insert( - HeaderName::from_static(TENANT_ID), - HeaderValue::from_str(tenant).unwrap(), - ); - } + insert_api_key_tenant_header( + &mut req, + &user, + PARSEABLE.options.is_multi_tenant(), + ); req.extensions_mut().insert(session_key); Some(session_id) } @@ -326,12 +325,44 @@ fn extract_kinesis_headers(req: &mut ServiceRequest) { } } -/// Extract X-API-KEY header value if present (independent of action). +/// Extract an API key from X-API-KEY or an Authorization Bearer token. +/// X-API-KEY takes precedence when both headers are present. fn extract_api_key(req: &ServiceRequest) -> Option { - req.headers() + if let Some(api_key) = req + .headers() .get("x-api-key") .and_then(|v| v.to_str().ok()) .map(String::from) + { + return Some(api_key); + } + + req.headers() + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.split_once(' ')) + .and_then(|(scheme, api_key)| { + scheme + .eq_ignore_ascii_case("Bearer") + .then_some(api_key.trim_start_matches(' ')) + }) + .filter(|api_key| !api_key.is_empty()) + .map(String::from) +} + +/// Add the tenant resolved from an API-key user to a multi-tenant request. +fn insert_api_key_tenant_header( + req: &mut ServiceRequest, + user: &user::User, + is_multi_tenant: bool, +) { + if is_multi_tenant { + let tenant = user.tenant.as_deref().unwrap_or(DEFAULT_TENANT); + req.headers_mut().insert( + HeaderName::from_static(TENANT_ID), + HeaderValue::from_str(tenant).unwrap(), + ); + } } /// Resolve an incoming API key value to its backing `UserType::ApiKey` user. @@ -856,3 +887,96 @@ where } } } + +#[cfg(test)] +mod api_key_header_tests { + use std::collections::HashSet; + + use actix_web::test as actix_test; + + use super::*; + + #[test] + fn extracts_x_api_key() { + let req = actix_test::TestRequest::default() + .insert_header(("x-api-key", "x-api-key-value")) + .to_srv_request(); + + assert_eq!(extract_api_key(&req).as_deref(), Some("x-api-key-value")); + } + + #[test] + fn extracts_bearer_api_key() { + let req = actix_test::TestRequest::default() + .insert_header((header::AUTHORIZATION, "Bearer bearer-api-key-value")) + .to_srv_request(); + + assert_eq!( + extract_api_key(&req).as_deref(), + Some("bearer-api-key-value") + ); + } + + #[test] + fn bearer_scheme_is_case_insensitive() { + let req = actix_test::TestRequest::default() + .insert_header((header::AUTHORIZATION, "bearer api-key-value")) + .to_srv_request(); + + assert_eq!(extract_api_key(&req).as_deref(), Some("api-key-value")); + } + + #[test] + fn bearer_allows_repeated_spaces_before_api_key() { + let req = actix_test::TestRequest::default() + .insert_header((header::AUTHORIZATION, "Bearer api-key-value")) + .to_srv_request(); + + assert_eq!(extract_api_key(&req).as_deref(), Some("api-key-value")); + } + + #[test] + fn bearer_requires_api_key() { + let req = actix_test::TestRequest::default() + .insert_header((header::AUTHORIZATION, "Bearer ")) + .to_srv_request(); + + assert_eq!(extract_api_key(&req), None); + } + + #[test] + fn ignores_non_bearer_authorization() { + let req = actix_test::TestRequest::default() + .insert_header((header::AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")) + .to_srv_request(); + + assert_eq!(extract_api_key(&req), None); + } + + #[test] + fn x_api_key_takes_precedence_over_bearer() { + let req = actix_test::TestRequest::default() + .insert_header(("x-api-key", "x-api-key-value")) + .insert_header((header::AUTHORIZATION, "Bearer bearer-api-key-value")) + .to_srv_request(); + + assert_eq!(extract_api_key(&req).as_deref(), Some("x-api-key-value")); + } + + #[test] + fn resolved_api_key_user_sets_multi_tenant_header() { + let user = user::User::new_api_key( + Ulid::new(), + "valid-api-key".to_owned(), + "test-key".to_owned(), + HashSet::new(), + "admin".to_owned(), + Some("acme".to_owned()), + ); + let mut req = actix_test::TestRequest::default().to_srv_request(); + + insert_api_key_tenant_header(&mut req, &user, true); + + assert_eq!(req.headers().get(TENANT_ID).unwrap(), "acme"); + } +}