google_cloud_auth/credentials/
api_key_credentials.rs1use crate::credentials::dynamic::CredentialsProvider;
16use crate::credentials::{Credentials, Result};
17use crate::headers_util::build_api_key_headers;
18use crate::token::{Token, TokenProvider};
19use http::{Extensions, HeaderMap};
20use std::sync::Arc;
21
22struct ApiKeyTokenProvider {
23 api_key: String,
24}
25
26impl std::fmt::Debug for ApiKeyTokenProvider {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("ApiKeyCredentials")
29 .field("api_key", &"[censored]")
30 .finish()
31 }
32}
33
34#[async_trait::async_trait]
35impl TokenProvider for ApiKeyTokenProvider {
36 async fn token(&self) -> Result<Token> {
37 Ok(Token {
38 token: self.api_key.clone(),
39 token_type: String::new(),
40 expires_at: None,
41 metadata: None,
42 })
43 }
44}
45
46#[derive(Debug)]
47struct ApiKeyCredentials<T>
48where
49 T: TokenProvider,
50{
51 token_provider: T,
52 quota_project_id: Option<String>,
53}
54
55#[derive(Debug)]
67pub struct Builder {
68 api_key: String,
69 quota_project_id: Option<String>,
70}
71
72impl Builder {
73 pub fn new<T: Into<String>>(api_key: T) -> Self {
84 Self {
85 api_key: api_key.into(),
86 quota_project_id: None,
87 }
88 }
89
90 pub fn with_quota_project_id<T: Into<String>>(mut self, quota_project_id: T) -> Self {
108 self.quota_project_id = Some(quota_project_id.into());
109 self
110 }
111
112 fn build_token_provider(self) -> ApiKeyTokenProvider {
113 ApiKeyTokenProvider {
114 api_key: self.api_key,
115 }
116 }
117
118 pub fn build(self) -> Credentials {
120 let quota_project_id = std::env::var("GOOGLE_CLOUD_QUOTA_PROJECT")
121 .ok()
122 .or(self.quota_project_id.clone());
123
124 Credentials {
125 inner: Arc::new(ApiKeyCredentials {
126 token_provider: self.build_token_provider(),
127 quota_project_id,
128 }),
129 }
130 }
131}
132
133#[async_trait::async_trait]
134impl<T> CredentialsProvider for ApiKeyCredentials<T>
135where
136 T: TokenProvider,
137{
138 async fn headers(&self, _extensions: Extensions) -> Result<HeaderMap> {
139 let token = self.token_provider.token().await?;
140 build_api_key_headers(&token, &self.quota_project_id)
141 }
142}
143
144#[cfg(test)]
145mod test {
146 use super::*;
147 use crate::credentials::QUOTA_PROJECT_KEY;
148 use http::HeaderValue;
149 use scoped_env::ScopedEnv;
150
151 const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
152
153 #[test]
154 fn debug_token_provider() {
155 let expected = Builder::new("test-api-key").build_token_provider();
156
157 let fmt = format!("{expected:?}");
158 assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
159 }
160
161 #[tokio::test]
162 async fn api_key_credentials_token_provider() {
163 let tp = Builder::new("test-api-key").build_token_provider();
164 assert_eq!(
165 tp.token().await.unwrap(),
166 Token {
167 token: "test-api-key".to_string(),
168 token_type: String::new(),
169 expires_at: None,
170 metadata: None,
171 }
172 );
173 }
174
175 #[tokio::test]
176 #[serial_test::serial]
177 async fn create_api_key_credentials_basic() {
178 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
179
180 let creds = Builder::new("test-api-key").build();
181 let headers = creds.headers(Extensions::new()).await.unwrap();
182 let value = headers.get(API_KEY_HEADER_KEY).unwrap();
183
184 assert_eq!(headers.len(), 1, "{headers:?}");
185 assert_eq!(value, HeaderValue::from_static("test-api-key"));
186 assert!(value.is_sensitive());
187 }
188
189 #[tokio::test]
190 #[serial_test::serial]
191 async fn create_api_key_credentials_with_options() {
192 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
193
194 let creds = Builder::new("test-api-key")
195 .with_quota_project_id("qp-option")
196 .build();
197 let headers = creds.headers(Extensions::new()).await.unwrap();
198 let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
199 let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
200
201 assert_eq!(headers.len(), 2, "{headers:?}");
202 assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
203 assert!(api_key.is_sensitive());
204 assert_eq!(quota_project, HeaderValue::from_static("qp-option"));
205 assert!(!quota_project.is_sensitive());
206 }
207
208 #[tokio::test]
209 #[serial_test::serial]
210 async fn create_api_key_credentials_with_env() {
211 let _e = ScopedEnv::set("GOOGLE_CLOUD_QUOTA_PROJECT", "qp-env");
212
213 let creds = Builder::new("test-api-key")
214 .with_quota_project_id("qp-option")
215 .build();
216 let headers = creds.headers(Extensions::new()).await.unwrap();
217 let api_key = headers.get(API_KEY_HEADER_KEY).unwrap();
218 let quota_project = headers.get(QUOTA_PROJECT_KEY).unwrap();
219
220 assert_eq!(headers.len(), 2, "{headers:?}");
221 assert_eq!(api_key, HeaderValue::from_static("test-api-key"));
222 assert!(api_key.is_sensitive());
223 assert_eq!(quota_project, HeaderValue::from_static("qp-env"));
224 assert!(!quota_project.is_sensitive());
225 }
226}