Skip to content

Commit debe62b

Browse files
accept api-key in Authorization header (#1774)
* accept api-key in Authorization header Authorization: Bearer <api-key> uses existing API-key RBAC flow X-API-KEY still supported and takes precedence if both supplied * extract tenant header insertion to helper * resolve coderabbit comment
1 parent 27c4e35 commit debe62b

1 file changed

Lines changed: 133 additions & 9 deletions

File tree

‎src/handlers/http/middleware.rs‎

Lines changed: 133 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ where
168168
let mut header_error = None;
169169
let user_and_tenant_id = get_user_and_tenant(&self.action, &mut req, &mut header_error);
170170

171-
// If an X-API-KEY header is present, resolve it to the backing
171+
// If an API key is present in X-API-KEY or as a Bearer token, resolve it to the backing
172172
// `UserType::ApiKey` user and register an ephemeral session whose
173173
// permissions are derived from the user's assigned roles (same flow
174174
// as native/OAuth users). The request then falls through to the
@@ -188,12 +188,11 @@ where
188188
permissions,
189189
&user.tenant,
190190
);
191-
if PARSEABLE.options.is_multi_tenant() {
192-
req.headers_mut().insert(
193-
HeaderName::from_static(TENANT_ID),
194-
HeaderValue::from_str(tenant).unwrap(),
195-
);
196-
}
191+
insert_api_key_tenant_header(
192+
&mut req,
193+
&user,
194+
PARSEABLE.options.is_multi_tenant(),
195+
);
197196
req.extensions_mut().insert(session_key);
198197
Some(session_id)
199198
}
@@ -326,12 +325,44 @@ fn extract_kinesis_headers(req: &mut ServiceRequest) {
326325
}
327326
}
328327

329-
/// Extract X-API-KEY header value if present (independent of action).
328+
/// Extract an API key from X-API-KEY or an Authorization Bearer token.
329+
/// X-API-KEY takes precedence when both headers are present.
330330
fn extract_api_key(req: &ServiceRequest) -> Option<String> {
331-
req.headers()
331+
if let Some(api_key) = req
332+
.headers()
332333
.get("x-api-key")
333334
.and_then(|v| v.to_str().ok())
334335
.map(String::from)
336+
{
337+
return Some(api_key);
338+
}
339+
340+
req.headers()
341+
.get(header::AUTHORIZATION)
342+
.and_then(|value| value.to_str().ok())
343+
.and_then(|value| value.split_once(' '))
344+
.and_then(|(scheme, api_key)| {
345+
scheme
346+
.eq_ignore_ascii_case("Bearer")
347+
.then_some(api_key.trim_start_matches(' '))
348+
})
349+
.filter(|api_key| !api_key.is_empty())
350+
.map(String::from)
351+
}
352+
353+
/// Add the tenant resolved from an API-key user to a multi-tenant request.
354+
fn insert_api_key_tenant_header(
355+
req: &mut ServiceRequest,
356+
user: &user::User,
357+
is_multi_tenant: bool,
358+
) {
359+
if is_multi_tenant {
360+
let tenant = user.tenant.as_deref().unwrap_or(DEFAULT_TENANT);
361+
req.headers_mut().insert(
362+
HeaderName::from_static(TENANT_ID),
363+
HeaderValue::from_str(tenant).unwrap(),
364+
);
365+
}
335366
}
336367

337368
/// Resolve an incoming API key value to its backing `UserType::ApiKey` user.
@@ -856,3 +887,96 @@ where
856887
}
857888
}
858889
}
890+
891+
#[cfg(test)]
892+
mod api_key_header_tests {
893+
use std::collections::HashSet;
894+
895+
use actix_web::test as actix_test;
896+
897+
use super::*;
898+
899+
#[test]
900+
fn extracts_x_api_key() {
901+
let req = actix_test::TestRequest::default()
902+
.insert_header(("x-api-key", "x-api-key-value"))
903+
.to_srv_request();
904+
905+
assert_eq!(extract_api_key(&req).as_deref(), Some("x-api-key-value"));
906+
}
907+
908+
#[test]
909+
fn extracts_bearer_api_key() {
910+
let req = actix_test::TestRequest::default()
911+
.insert_header((header::AUTHORIZATION, "Bearer bearer-api-key-value"))
912+
.to_srv_request();
913+
914+
assert_eq!(
915+
extract_api_key(&req).as_deref(),
916+
Some("bearer-api-key-value")
917+
);
918+
}
919+
920+
#[test]
921+
fn bearer_scheme_is_case_insensitive() {
922+
let req = actix_test::TestRequest::default()
923+
.insert_header((header::AUTHORIZATION, "bearer api-key-value"))
924+
.to_srv_request();
925+
926+
assert_eq!(extract_api_key(&req).as_deref(), Some("api-key-value"));
927+
}
928+
929+
#[test]
930+
fn bearer_allows_repeated_spaces_before_api_key() {
931+
let req = actix_test::TestRequest::default()
932+
.insert_header((header::AUTHORIZATION, "Bearer api-key-value"))
933+
.to_srv_request();
934+
935+
assert_eq!(extract_api_key(&req).as_deref(), Some("api-key-value"));
936+
}
937+
938+
#[test]
939+
fn bearer_requires_api_key() {
940+
let req = actix_test::TestRequest::default()
941+
.insert_header((header::AUTHORIZATION, "Bearer "))
942+
.to_srv_request();
943+
944+
assert_eq!(extract_api_key(&req), None);
945+
}
946+
947+
#[test]
948+
fn ignores_non_bearer_authorization() {
949+
let req = actix_test::TestRequest::default()
950+
.insert_header((header::AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA=="))
951+
.to_srv_request();
952+
953+
assert_eq!(extract_api_key(&req), None);
954+
}
955+
956+
#[test]
957+
fn x_api_key_takes_precedence_over_bearer() {
958+
let req = actix_test::TestRequest::default()
959+
.insert_header(("x-api-key", "x-api-key-value"))
960+
.insert_header((header::AUTHORIZATION, "Bearer bearer-api-key-value"))
961+
.to_srv_request();
962+
963+
assert_eq!(extract_api_key(&req).as_deref(), Some("x-api-key-value"));
964+
}
965+
966+
#[test]
967+
fn resolved_api_key_user_sets_multi_tenant_header() {
968+
let user = user::User::new_api_key(
969+
Ulid::new(),
970+
"valid-api-key".to_owned(),
971+
"test-key".to_owned(),
972+
HashSet::new(),
973+
"admin".to_owned(),
974+
Some("acme".to_owned()),
975+
);
976+
let mut req = actix_test::TestRequest::default().to_srv_request();
977+
978+
insert_api_key_tenant_header(&mut req, &user, true);
979+
980+
assert_eq!(req.headers().get(TENANT_ID).unwrap(), "acme");
981+
}
982+
}

0 commit comments

Comments
 (0)