use crate::Result;
use crate::credentials::{CacheableResource, QUOTA_PROJECT_KEY};
use crate::errors;
use crate::token::Token;
use http::HeaderMap;
use http::header::{AUTHORIZATION, HeaderName, HeaderValue};
mod build_info {
include!(concat!(env!("OUT_DIR"), "/build_env.rs"));
}
pub(crate) const X_GOOG_API_CLIENT: &str = "x-goog-api-client";
pub(crate) const ACCESS_TOKEN_REQUEST_TYPE: &str = "at";
#[cfg(feature = "idtoken")]
pub(crate) const ID_TOKEN_REQUEST_TYPE: &str = "it";
pub(crate) fn metrics_header_value(request_type: &str, cred_type: &str) -> String {
let rustc_version = build_info::RUSTC_VERSION;
let auth_version = build_info::PKG_VERSION;
format!(
"gl-rust/{rustc_version} auth/{auth_version} auth-request-type/{request_type} cred-type/{cred_type}"
)
}
const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
#[derive(Debug)]
pub(crate) struct AuthHeadersBuilder<'a> {
token: &'a CacheableResource<Token>,
quota_project_id: Option<&'a str>,
header_type: HeaderType,
}
#[derive(Debug, Clone)]
enum HeaderType {
Bearer,
ApiKey,
}
impl<'a> AuthHeadersBuilder<'a> {
pub(crate) fn new(token: &'a CacheableResource<Token>) -> AuthHeadersBuilder<'a> {
AuthHeadersBuilder {
token,
header_type: HeaderType::Bearer,
quota_project_id: None,
}
}
pub(crate) fn for_api_key(token: &'a CacheableResource<Token>) -> AuthHeadersBuilder<'a> {
AuthHeadersBuilder {
token,
header_type: HeaderType::ApiKey,
quota_project_id: None,
}
}
pub(crate) fn maybe_quota_project_id(mut self, quota_project_id: Option<&'a str>) -> Self {
self.quota_project_id = quota_project_id;
self
}
pub(crate) fn build(&self) -> Result<CacheableResource<HeaderMap>> {
match self.token {
CacheableResource::NotModified => Ok(CacheableResource::NotModified),
CacheableResource::New { entity_tag, data } => Ok(CacheableResource::New {
entity_tag: entity_tag.clone(),
data: self.as_headers(data)?,
}),
}
}
fn header_name(&self) -> HeaderName {
match self.header_type {
HeaderType::Bearer => AUTHORIZATION,
HeaderType::ApiKey => HeaderName::from_static(API_KEY_HEADER_KEY),
}
}
fn header_value(&self, token: &Token) -> Result<HeaderValue> {
match self.header_type {
HeaderType::Bearer => {
HeaderValue::from_str(&format!("{} {}", token.token_type, token.token))
.map_err(errors::non_retryable)
}
HeaderType::ApiKey => {
HeaderValue::from_str(&token.token).map_err(errors::non_retryable)
}
}
}
fn as_headers(&self, token: &Token) -> Result<HeaderMap> {
let mut value = self.header_value(token)?;
value.set_sensitive(true);
let header_name = self.header_name();
let mut header_map = HeaderMap::new();
header_map.insert(header_name, value);
if let Some(project) = self.quota_project_id {
header_map.insert(
HeaderName::from_static(QUOTA_PROJECT_KEY),
HeaderValue::from_str(project).map_err(errors::non_retryable)?,
);
}
Ok(header_map)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{credentials::EntityTag, token::Token};
use std::error::Error as _;
fn create_test_token(token: &str, token_type: &str) -> Token {
Token {
token: token.to_string(),
token_type: token_type.to_string(),
expires_at: None,
metadata: None,
}
}
#[test]
fn build_cacheable_headers_basic_success() -> anyhow::Result<()> {
let token = create_test_token("test_token", "Bearer");
let cacheable_token = CacheableResource::New {
entity_tag: EntityTag::default(),
data: token,
};
let cached_headers = AuthHeadersBuilder::new(&cacheable_token).build()?;
let headers = match cached_headers {
CacheableResource::New { data, .. } => data,
CacheableResource::NotModified => unreachable!("expecting new headers"),
};
assert_eq!(headers.len(), 1, "{headers:?}");
let value = headers
.get(HeaderName::from_static("authorization"))
.unwrap();
assert_eq!(value, HeaderValue::from_static("Bearer test_token"));
assert!(value.is_sensitive());
Ok(())
}
#[test]
fn build_cacheable_headers_basic_not_modified() -> anyhow::Result<()> {
let cacheable_token = CacheableResource::NotModified;
let cached_headers = AuthHeadersBuilder::new(&cacheable_token).build()?;
assert!(
matches!(&cached_headers, CacheableResource::NotModified),
"{cached_headers:?}"
);
Ok(())
}
#[test]
fn build_cacheable_headers_with_quota_project_success() -> anyhow::Result<()> {
let token = create_test_token("test_token", "Bearer");
let cacheable_token = CacheableResource::New {
entity_tag: EntityTag::default(),
data: token,
};
let quota_project_id = Some("test-project-123");
let cached_headers = AuthHeadersBuilder::new(&cacheable_token)
.maybe_quota_project_id(quota_project_id)
.build()?;
let headers = match cached_headers {
CacheableResource::New { data, .. } => data,
CacheableResource::NotModified => unreachable!("expecting new headers"),
};
assert_eq!(headers.len(), 2, "{headers:?}");
let token = headers
.get(HeaderName::from_static("authorization"))
.unwrap();
assert_eq!(token, HeaderValue::from_static("Bearer test_token"));
assert!(token.is_sensitive());
let quota_project = headers
.get(HeaderName::from_static(QUOTA_PROJECT_KEY))
.unwrap();
assert_eq!(quota_project, HeaderValue::from_static("test-project-123"));
Ok(())
}
#[test]
fn build_bearer_headers_different_token_type() -> anyhow::Result<()> {
let token = create_test_token("special_token", "MAC");
let headers =
AuthHeadersBuilder::new(&CacheableResource::NotModified).as_headers(&token)?;
assert_eq!(headers.len(), 1, "{headers:?}");
let token = headers
.get(HeaderName::from_static("authorization"))
.unwrap_or_else(|| {
panic!("headers should contain authorization header, got={headers:?}")
});
assert_eq!(token, HeaderValue::from_static("MAC special_token"));
assert!(token.is_sensitive());
Ok(())
}
#[test]
fn build_bearer_headers_invalid_token() {
let token = create_test_token("token with \n invalid chars", "Bearer");
let result = AuthHeadersBuilder::new(&CacheableResource::NotModified).as_headers(&token);
assert!(result.is_err(), "{result:?}");
let error = result.unwrap_err();
assert!(!error.is_transient(), "{error:?}");
let source = error
.source()
.and_then(|e| e.downcast_ref::<http::header::InvalidHeaderValue>());
assert!(
matches!(source, Some(http::header::InvalidHeaderValue { .. })),
"{error:?}"
);
}
#[test]
fn build_cacheable_api_key_headers_basic_success() -> anyhow::Result<()> {
let token = create_test_token("api_key_12345", "Bearer");
let cacheable_token = CacheableResource::New {
entity_tag: EntityTag::default(),
data: token,
};
let cached_headers = AuthHeadersBuilder::for_api_key(&cacheable_token).build()?;
let headers = match cached_headers {
CacheableResource::New { data, .. } => data,
CacheableResource::NotModified => unreachable!("expecting new headers"),
};
assert_eq!(headers.len(), 1, "{headers:?}");
let api_key = headers
.get(HeaderName::from_static(API_KEY_HEADER_KEY))
.unwrap_or_else(|| panic!("headers contains {API_KEY_HEADER_KEY}, got={headers:?}"));
assert_eq!(api_key, HeaderValue::from_static("api_key_12345"));
assert!(api_key.is_sensitive());
Ok(())
}
#[test]
fn build_cacheable_api_key_headers_basic_not_modified() -> anyhow::Result<()> {
let cacheable_token = CacheableResource::NotModified;
let cached_headers = AuthHeadersBuilder::for_api_key(&cacheable_token).build()?;
assert!(
matches!(&cached_headers, CacheableResource::NotModified),
"{cached_headers:?}"
);
Ok(())
}
#[test]
fn build_api_key_headers_invalid_token() {
let token = create_test_token("api_key with \n invalid chars", "Bearer");
let result = AuthHeadersBuilder::new(&CacheableResource::NotModified).as_headers(&token);
let error = result.unwrap_err();
assert!(!error.is_transient(), "{error:?}");
let source = error
.source()
.and_then(|e| e.downcast_ref::<http::header::InvalidHeaderValue>());
assert!(
matches!(source, Some(http::header::InvalidHeaderValue { .. })),
"{error:?}"
);
}
#[test]
fn test_metrics_header_value() {
let header = metrics_header_value("at", "u");
let rustc_version = build_info::RUSTC_VERSION;
let expected = format!(
"gl-rust/{} auth/{} auth-request-type/at cred-type/u",
rustc_version,
build_info::PKG_VERSION
);
assert_eq!(header, expected);
}
}