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 quota_project_id: Option<String>,
62}
63
64#[derive(Debug)]
76pub struct Builder {
77 api_key: String,
78 quota_project_id: Option<String>,
79}
80
81impl Builder {
82 pub fn new<T: Into<String>>(api_key: T) -> Self {
93 Self {
94 api_key: api_key.into(),
95 quota_project_id: None,
96 }
97 }
98
99 pub fn with_quota_project_id<T: Into<String>>(mut self, quota_project_id: T) -> Self {
117 self.quota_project_id = Some(quota_project_id.into());
118 self
119 }
120
121 fn build_token_provider(self) -> ApiKeyTokenProvider {
122 ApiKeyTokenProvider {
123 api_key: self.api_key,
124 }
125 }
126
127 pub fn build(self) -> Credentials {
129 let quota_project_id = std::env::var("GOOGLE_CLOUD_QUOTA_PROJECT")
130 .ok()
131 .or(self.quota_project_id.clone());
132
133 Credentials {
134 inner: Arc::new(ApiKeyCredentials {
135 token_provider: TokenCache::new(self.build_token_provider()),
136 quota_project_id,
137 }),
138 }
139 }
140}
141
142#[async_trait::async_trait]
143impl<T> CredentialsProvider for ApiKeyCredentials<T>
144where
145 T: CachedTokenProvider,
146{
147 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
148 let cached_token = self.token_provider.token(extensions).await?;
149 build_cacheable_api_key_headers(&cached_token, &self.quota_project_id)
150 }
151}
152
153#[cfg(test)]
154mod test {
155 use super::*;
156 use crate::credentials::QUOTA_PROJECT_KEY;
157 use crate::credentials::test::get_headers_from_cache;
158 use http::HeaderValue;
159 use scoped_env::ScopedEnv;
160
161 const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
162 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
163
164 #[test]
165 fn debug_token_provider() {
166 let expected = Builder::new("test-api-key").build_token_provider();
167
168 let fmt = format!("{expected:?}");
169 assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
170 }
171
172 #[tokio::test]
173 async fn api_key_credentials_token_provider() {
174 let tp = Builder::new("test-api-key").build_token_provider();
175 assert_eq!(
176 tp.token().await.unwrap(),
177 Token {
178 token: "test-api-key".to_string(),
179 token_type: String::new(),
180 expires_at: None,
181 metadata: None,
182 }
183 );
184 }
185
186 #[tokio::test]
187 #[serial_test::serial]
188 async fn create_api_key_credentials_basic() -> TestResult {
189 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
190
191 let creds = Builder::new("test-api-key").build();
192 let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
193 let value = headers.get(API_KEY_HEADER_KEY).unwrap();
194
195 assert_eq!(headers.len(), 1, "{headers:?}");
196 assert_eq!(value, HeaderValue::from_static("test-api-key"));
197 assert!(value.is_sensitive());
198 Ok(())
199 }
200
201 #[tokio::test]
202 #[serial_test::serial]
203 async fn create_api_key_credentials_with_options() -> TestResult {
204 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
205
206 let creds = Builder::new("test-api-key")
207 .with_quota_project_id("qp-option")
208 .build();
209 let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
210 let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
211 let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
212
213 assert_eq!(headers.len(), 2, "{headers:?}");
214 assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
215 assert!(api_key.is_sensitive());
216 assert_eq!(quota_project, HeaderValue::from_static("qp-option"));
217 assert!(!quota_project.is_sensitive());
218 Ok(())
219 }
220
221 #[tokio::test]
222 #[serial_test::serial]
223 async fn create_api_key_credentials_with_env() -> TestResult {
224 let _e = ScopedEnv::set("GOOGLE_CLOUD_QUOTA_PROJECT", "qp-env");
225
226 let creds = Builder::new("test-api-key")
227 .with_quota_project_id("qp-option")
228 .build();
229 let headers = get_headers_from_cache(creds.headers(Extensions::new()).await.unwrap())?;
230 let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
231 let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
232
233 assert_eq!(headers.len(), 2, "{headers:?}");
234 assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
235 assert!(api_key.is_sensitive());
236 assert_eq!(quota_project, HeaderValue::from_static("qp-env"));
237 assert!(!quota_project.is_sensitive());
238 Ok(())
239 }
240
241 #[tokio::test]
242 #[serial_test::serial]
243 async fn create_api_key_credentials_basic_with_extensions() -> TestResult {
244 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
245
246 let creds = Builder::new("test-api-key").build();
247 let mut extensions = Extensions::new();
248 let cached_headers = creds.headers(extensions.clone()).await?;
249 let (headers, entity_tag) = match cached_headers {
250 CacheableResource::New { entity_tag, data } => (data, entity_tag),
251 CacheableResource::NotModified => unreachable!("expecting new headers"),
252 };
253 let value = headers.get(API_KEY_HEADER_KEY).unwrap();
254
255 assert_eq!(headers.len(), 1, "{headers:?}");
256 assert_eq!(value, HeaderValue::from_static("test-api-key"));
257 assert!(value.is_sensitive());
258 extensions.insert(entity_tag);
259
260 let cached_headers = creds.headers(extensions).await?;
261
262 match cached_headers {
263 CacheableResource::New { .. } => unreachable!("expecting new headers"),
264 CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
265 };
266
267 Ok(())
268 }
269}