From bda4e9ef9beefad7467509b7956f2b6dbb8030fa Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Sun, 30 Aug 2026 08:14:41 +0530 Subject: [PATCH 1/3] accept api-key in Authorization header Authorization: Bearer uses existing API-key RBAC flow X-API-KEY still supported and takes precedence if both supplied --- src/handlers/http/middleware.rs | 74 +++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/src/handlers/http/middleware.rs b/src/handlers/http/middleware.rs index 212b0f263..e4ec0814c 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 @@ -326,12 +326,80 @@ 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)) + .map(String::from) +} + +#[cfg(test)] +mod api_key_header_tests { + 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 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")); + } } /// Resolve an incoming API key value to its backing `UserType::ApiKey` user. From 958b8a88a7382ca60331e357973ec73b60f42145 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Sun, 30 Aug 2026 10:07:32 +0530 Subject: [PATCH 2/3] extract tenant header insertion to helper --- src/handlers/http/middleware.rs | 149 +++++++++++++++++++------------- 1 file changed, 91 insertions(+), 58 deletions(-) diff --git a/src/handlers/http/middleware.rs b/src/handlers/http/middleware.rs index e4ec0814c..6a9b9b3e7 100644 --- a/src/handlers/http/middleware.rs +++ b/src/handlers/http/middleware.rs @@ -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) } @@ -346,60 +345,19 @@ fn extract_api_key(req: &ServiceRequest) -> Option { .map(String::from) } -#[cfg(test)] -mod api_key_header_tests { - 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") +/// 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(), ); } - - #[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 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")); - } } /// Resolve an incoming API key value to its backing `UserType::ApiKey` user. @@ -924,3 +882,78 @@ 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 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"); + } +} From 18cd063cbca057696fcefb520f8f358bd3cf2944 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Sun, 30 Aug 2026 10:40:08 +0530 Subject: [PATCH 3/3] resolve coderabbit comment --- src/handlers/http/middleware.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/handlers/http/middleware.rs b/src/handlers/http/middleware.rs index 6a9b9b3e7..cd3797bfb 100644 --- a/src/handlers/http/middleware.rs +++ b/src/handlers/http/middleware.rs @@ -341,7 +341,12 @@ fn extract_api_key(req: &ServiceRequest) -> Option { .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)) + .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) } @@ -921,6 +926,24 @@ mod api_key_header_tests { 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()