Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 133 additions & 9 deletions src/handlers/http/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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<String> {
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.
Expand Down Expand Up @@ -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");
}
}
Loading