use crate::credentials::dynamic::CredentialsProvider;
use crate::credentials::{CacheableResource, Credentials, Result};
use crate::headers_util::AuthHeadersBuilder;
use crate::token::{CachedTokenProvider, Token, TokenProvider};
use crate::token_cache::TokenCache;
use http::{Extensions, HeaderMap};
use std::sync::Arc;
struct ApiKeyTokenProvider {
api_key: String,
}
impl std::fmt::Debug for ApiKeyTokenProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiKeyCredentials")
.field("api_key", &"[censored]")
.finish()
}
}
#[async_trait::async_trait]
impl TokenProvider for ApiKeyTokenProvider {
async fn token(&self) -> Result<Token> {
Ok(Token {
token: self.api_key.clone(),
token_type: String::new(),
expires_at: None,
metadata: None,
})
}
}
#[derive(Debug)]
struct ApiKeyCredentials<T>
where
T: CachedTokenProvider,
{
token_provider: T,
}
#[derive(Debug)]
pub struct Builder {
api_key: String,
}
impl Builder {
pub fn new<T: Into<String>>(api_key: T) -> Self {
Self {
api_key: api_key.into(),
}
}
fn build_token_provider(self) -> ApiKeyTokenProvider {
ApiKeyTokenProvider {
api_key: self.api_key,
}
}
pub fn build(self) -> Credentials {
Credentials {
inner: Arc::new(ApiKeyCredentials {
token_provider: TokenCache::new(self.build_token_provider()),
}),
}
}
}
#[async_trait::async_trait]
impl<T> CredentialsProvider for ApiKeyCredentials<T>
where
T: CachedTokenProvider,
{
async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
let cached_token = self.token_provider.token(extensions).await?;
AuthHeadersBuilder::for_api_key(&cached_token).build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::credentials::tests::get_headers_from_cache;
use http::HeaderValue;
use scoped_env::ScopedEnv;
use serial_test::{parallel, serial};
const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
#[test]
#[parallel]
fn debug_token_provider() {
let expected = Builder::new("test-api-key").build_token_provider();
let fmt = format!("{expected:?}");
assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
}
#[tokio::test]
#[parallel]
async fn api_key_credentials_token_provider() {
let tp = Builder::new("test-api-key").build_token_provider();
assert_eq!(
tp.token().await.unwrap(),
Token {
token: "test-api-key".to_string(),
token_type: String::new(),
expires_at: None,
metadata: None,
}
);
}
#[tokio::test]
#[serial]
async fn create_api_key_credentials_basic() -> TestResult {
let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
let creds = Builder::new("test-api-key").build();
let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
let value = headers.get(API_KEY_HEADER_KEY).unwrap();
assert_eq!(headers.len(), 1, "{headers:?}");
assert_eq!(value, HeaderValue::from_static("test-api-key"));
assert!(value.is_sensitive());
Ok(())
}
#[tokio::test]
#[serial]
async fn create_api_key_credentials_basic_with_extensions() -> TestResult {
let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
let creds = Builder::new("test-api-key").build();
let mut extensions = Extensions::new();
let cached_headers = creds.headers(extensions.clone()).await?;
let (headers, entity_tag) = match cached_headers {
CacheableResource::New { entity_tag, data } => (data, entity_tag),
CacheableResource::NotModified => unreachable!("expecting new headers"),
};
let value = headers.get(API_KEY_HEADER_KEY).unwrap();
assert_eq!(headers.len(), 1, "{headers:?}");
assert_eq!(value, HeaderValue::from_static("test-api-key"));
assert!(value.is_sensitive());
extensions.insert(entity_tag);
let cached_headers = creds.headers(extensions).await?;
match cached_headers {
CacheableResource::New { .. } => unreachable!("expecting new headers"),
CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
};
Ok(())
}
}