athena_rs 2.0.2

Database gateway API
Documentation
use actix_web::{HttpRequest, HttpResponse};
use serde_json::json;
use std::collections::HashMap;
use std::env;
use tracing::warn;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiKeySource {
    AuthorizationBearer,
    ApiKeyHeader,
    XApiKeyHeader,
    XAthenaKeyHeader,
    QueryParamApiKey,
    QueryParamApikey,
}

fn query_param_value(req: &HttpRequest, key: &str) -> Option<String> {
    req.uri().query().and_then(|query| {
        serde_urlencoded::from_str::<HashMap<String, String>>(query)
            .ok()
            .and_then(|params| params.get(key).cloned())
            .filter(|value| !value.trim().is_empty())
    })
}

pub fn extract_api_key_with_source(req: &HttpRequest) -> Option<(String, ApiKeySource)> {
    req.headers()
        .get("Authorization")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer ").map(str::to_string))
        .filter(|value| !value.trim().is_empty())
        .map(|value| (value, ApiKeySource::AuthorizationBearer))
        .or_else(|| {
            req.headers()
                .get("apikey")
                .and_then(|value| value.to_str().ok())
                .map(str::to_string)
                .filter(|value| !value.trim().is_empty())
                .map(|value| (value, ApiKeySource::ApiKeyHeader))
        })
        .or_else(|| {
            req.headers()
                .get("x-api-key")
                .and_then(|value| value.to_str().ok())
                .map(str::to_string)
                .filter(|value| !value.trim().is_empty())
                .map(|value| (value, ApiKeySource::XApiKeyHeader))
        })
        .or_else(|| {
            req.headers()
                .get("x-athena-key")
                .and_then(|value| value.to_str().ok())
                .map(str::to_string)
                .filter(|value| !value.trim().is_empty())
                .map(|value| (value, ApiKeySource::XAthenaKeyHeader))
        })
        .or_else(|| {
            query_param_value(req, "api_key").map(|value| {
                warn!("API key query parameter 'api_key' is deprecated; use headers instead");
                (value, ApiKeySource::QueryParamApiKey)
            })
        })
        .or_else(|| {
            query_param_value(req, "apikey").map(|value| {
                warn!("API key query parameter 'apikey' is deprecated; use headers instead");
                (value, ApiKeySource::QueryParamApikey)
            })
        })
}

pub fn api_key_query_param_used(req: &HttpRequest) -> bool {
    extract_api_key_with_source(req)
        .map(|(_, source)| {
            matches!(
                source,
                ApiKeySource::QueryParamApiKey | ApiKeySource::QueryParamApikey
            )
        })
        .unwrap_or(false)
}

/// Extract an API key from standard request locations.
///
/// The first non-empty value wins.
pub fn extract_api_key(req: &HttpRequest) -> Option<String> {
    extract_api_key_with_source(req).map(|(value, _)| value)
}

/// Validate the static admin key stored in `ATHENA_KEY_12`.
pub fn authorize_static_admin_key(req: &HttpRequest) -> Result<(), HttpResponse> {
    let expected: Option<String> = env::var("ATHENA_KEY_12")
        .ok()
        .filter(|value| !value.is_empty());
    let provided = extract_api_key(req);

    match (expected, provided) {
        (None, _) => Err(HttpResponse::InternalServerError().json(json!({
            "error": "ATHENA_KEY_12 is not configured"
        }))),
        (Some(expected), Some(provided)) if expected == provided => Ok(()),
        _ => Err(HttpResponse::Unauthorized().json(json!({
            "error": "Invalid or missing API key. Use Authorization: Bearer <key>, X-Athena-Key, apikey, or X-API-Key. Query-param API keys are deprecated."
        }))),
    }
}