google_cloud_auth/credentials/
api_key_credentials.rs1use crate::credentials::dynamic::CredentialsTrait;
16use crate::credentials::{Credentials, Result};
17use crate::headers_util::build_api_key_headers;
18use crate::token::{Token, TokenProvider};
19use http::header::{HeaderName, HeaderValue};
20use std::sync::Arc;
21
22#[derive(Default)]
24pub struct ApiKeyOptions {
25 quota_project: Option<String>,
26}
27
28impl ApiKeyOptions {
29 pub fn set_quota_project<T: Into<String>>(mut self, v: T) -> Self {
40 self.quota_project = Some(v.into());
41 self
42 }
43}
44
45pub async fn create_api_key_credentials<T: Into<String>>(
57 api_key: T,
58 o: ApiKeyOptions,
59) -> Result<Credentials> {
60 let token_provider = ApiKeyTokenProvider {
61 api_key: api_key.into(),
62 };
63
64 let quota_project_id = std::env::var("GOOGLE_CLOUD_QUOTA_PROJECT")
65 .ok()
66 .or(o.quota_project);
67
68 Ok(Credentials {
69 inner: Arc::new(ApiKeyCredentials {
70 token_provider,
71 quota_project_id,
72 }),
73 })
74}
75
76struct ApiKeyTokenProvider {
77 api_key: String,
78}
79
80impl std::fmt::Debug for ApiKeyTokenProvider {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("ApiKeyCredentials")
83 .field("api_key", &"[censored]")
84 .finish()
85 }
86}
87
88#[async_trait::async_trait]
89impl TokenProvider for ApiKeyTokenProvider {
90 async fn token(&self) -> Result<Token> {
91 Ok(Token {
92 token: self.api_key.clone(),
93 token_type: String::new(),
94 expires_at: None,
95 metadata: None,
96 })
97 }
98}
99
100#[derive(Debug)]
101struct ApiKeyCredentials<T>
102where
103 T: TokenProvider,
104{
105 token_provider: T,
106 quota_project_id: Option<String>,
107}
108
109#[async_trait::async_trait]
110impl<T> CredentialsTrait for ApiKeyCredentials<T>
111where
112 T: TokenProvider,
113{
114 async fn token(&self) -> Result<Token> {
115 self.token_provider.token().await
116 }
117
118 async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>> {
119 let token = self.token().await?;
120 build_api_key_headers(&token, &self.quota_project_id)
121 }
122}
123
124#[cfg(test)]
125mod test {
126 use super::*;
127 use crate::credentials::QUOTA_PROJECT_KEY;
128 use crate::credentials::test::HV;
129 use scoped_env::ScopedEnv;
130
131 const API_KEY_HEADER_KEY: &str = "x-goog-api-key";
132
133 #[test]
134 fn debug_token_provider() {
135 let expected = ApiKeyTokenProvider {
136 api_key: "super-secret-api-key".to_string(),
137 };
138 let fmt = format!("{expected:?}");
139 assert!(!fmt.contains("super-secret-api-key"), "{fmt}");
140 }
141
142 #[tokio::test]
143 #[serial_test::serial]
144 async fn create_api_key_credentials_basic() {
145 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
146
147 let creds = create_api_key_credentials("test-api-key", ApiKeyOptions::default())
148 .await
149 .unwrap();
150 let token = creds.token().await.unwrap();
151 assert_eq!(
152 token,
153 Token {
154 token: "test-api-key".to_string(),
155 token_type: String::new(),
156 expires_at: None,
157 metadata: None,
158 }
159 );
160 let headers: Vec<HV> = HV::from(creds.headers().await.unwrap());
161
162 assert_eq!(
163 headers,
164 vec![HV {
165 header: API_KEY_HEADER_KEY.to_string(),
166 value: "test-api-key".to_string(),
167 is_sensitive: true,
168 }]
169 );
170 }
171
172 #[tokio::test]
173 #[serial_test::serial]
174 async fn create_api_key_credentials_with_options() {
175 let _e = ScopedEnv::remove("GOOGLE_CLOUD_QUOTA_PROJECT");
176
177 let options = ApiKeyOptions::default().set_quota_project("qp-option");
178 let creds = create_api_key_credentials("test-api-key", options)
179 .await
180 .unwrap();
181 let headers: Vec<HV> = HV::from(creds.headers().await.unwrap());
182
183 assert_eq!(
184 headers,
185 vec![
186 HV {
187 header: API_KEY_HEADER_KEY.to_string(),
188 value: "test-api-key".to_string(),
189 is_sensitive: true,
190 },
191 HV {
192 header: QUOTA_PROJECT_KEY.to_string(),
193 value: "qp-option".to_string(),
194 is_sensitive: false,
195 }
196 ]
197 );
198 }
199
200 #[tokio::test]
201 #[serial_test::serial]
202 async fn create_api_key_credentials_with_env() {
203 let _e = ScopedEnv::set("GOOGLE_CLOUD_QUOTA_PROJECT", "qp-env");
204 let options = ApiKeyOptions::default().set_quota_project("qp-option");
205 let creds = create_api_key_credentials("test-api-key", options)
206 .await
207 .unwrap();
208 let headers: Vec<HV> = HV::from(creds.headers().await.unwrap());
209
210 assert_eq!(
211 headers,
212 vec![
213 HV {
214 header: API_KEY_HEADER_KEY.to_string(),
215 value: "test-api-key".to_string(),
216 is_sensitive: true,
217 },
218 HV {
219 header: QUOTA_PROJECT_KEY.to_string(),
220 value: "qp-env".to_string(),
221 is_sensitive: false,
222 }
223 ]
224 );
225 }
226}