google_cloud_auth/credentials/
api_key_credentials.rs1use crate::credentials::dynamic::CredentialsProvider;
24use crate::credentials::{CacheableResource, Credentials, Result};
25use crate::headers_util::build_cacheable_api_key_headers;
26use crate::token::{CachedTokenProvider, Token, TokenProvider};
27use crate::token_cache::TokenCache;
28use http::{Extensions, HeaderMap};
29use std::sync::Arc;
30
31struct ApiKeyTokenProvider {
32 api_key: String,
33}
34
35impl std::fmt::Debug for ApiKeyTokenProvider {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("ApiKeyCredentials")
38 .field("api_key", &"[censored]")
39 .finish()
40 }
41}
42
43#[async_trait::async_trait]
44impl TokenProvider for ApiKeyTokenProvider {
45 async fn token(&self) -> Result<Token> {
46 Ok(Token {
47 token: self.api_key.clone(),
48 token_type: String::new(),
49 expires_at: None,
50 metadata: None,
51 })
52 }
53}
54
55#[derive(Debug)]
56struct ApiKeyCredentials<T>
57where
58 T: CachedTokenProvider,
59{
60 token_provider: T,
61}
62
63#[derive(Debug)]
75pub struct Builder {
76 api_key: String,
77}
78
79impl Builder {
80 pub fn new<T: Into<String>>(api_key: T) -> Self {
91 Self {
92 api_key: api_key.into(),
93 }
94 }
95
96 fn build_token_provider(self) -> ApiKeyTokenProvider {
97 ApiKeyTokenProvider {
98 api_key: self.api_key,
99 }
100 }
101
102 pub fn build(self) -> Credentials {
104 Credentials {
105 inner: Arc::new(ApiKeyCredentials {
106 token_provider: TokenCache::new(self.build_token_provider()),
107 }),
108 }
109 }
110}
111
112#[async_trait::async_trait]
113impl<T> CredentialsProvider for ApiKeyCredentials<T>
114where
115 T: CachedTokenProvider,
116{
117 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
118 let cached_token = self.token_provider.token(extensions).await?;
119 build_cacheable_api_key_headers(&cached_token)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::credentials::tests::get_headers_from_cache;
127 use http::HeaderValue;
128 use scoped_env::ScopedEnv;
129 use serial_test::{parallel, serial};
130
131 const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
132 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
133
134 #[test]
135 #[parallel]
136 fn debug_token_provider() {
137 let expected = Builder::new("test-api-key").build_token_provider();
138
139 let fmt = format!("{expected:?}");
140 assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
141 }
142
143 #[tokio::test]
144 #[parallel]
145 async fn api_key_credentials_token_provider() {
146 let tp = Builder::new("test-api-key").build_token_provider();
147 assert_eq!(
148 tp.token().await.unwrap(),
149 Token {
150 token: "test-api-key".to_string(),
151 token_type: String::new(),
152 expires_at: None,
153 metadata: None,
154 }
155 );
156 }
157
158 #[tokio::test]
159 #[serial]
160 async fn create_api_key_credentials_basic() -> TestResult {
161 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
162
163 let creds = Builder::new("test-api-key").build();
164 let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
165 let value = headers.get(API_KEY_HEADER_KEY).unwrap();
166
167 assert_eq!(headers.len(), 1, "{headers:?}");
168 assert_eq!(value, HeaderValue::from_static("test-api-key"));
169 assert!(value.is_sensitive());
170 Ok(())
171 }
172
173 #[tokio::test]
174 #[serial]
175 async fn create_api_key_credentials_basic_with_extensions() -> TestResult {
176 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
177
178 let creds = Builder::new("test-api-key").build();
179 let mut extensions = Extensions::new();
180 let cached_headers = creds.headers(extensions.clone()).await?;
181 let (headers, entity_tag) = match cached_headers {
182 CacheableResource::New { entity_tag, data } => (data, entity_tag),
183 CacheableResource::NotModified => unreachable!("expecting new headers"),
184 };
185 let value = headers.get(API_KEY_HEADER_KEY).unwrap();
186
187 assert_eq!(headers.len(), 1, "{headers:?}");
188 assert_eq!(value, HeaderValue::from_static("test-api-key"));
189 assert!(value.is_sensitive());
190 extensions.insert(entity_tag);
191
192 let cached_headers = creds.headers(extensions).await?;
193
194 match cached_headers {
195 CacheableResource::New { .. } => unreachable!("expecting new headers"),
196 CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
197 };
198
199 Ok(())
200 }
201}