reqsign_google/provide_credential/
default.rs1use log::debug;
19
20use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain, Result};
21
22use crate::constants::GOOGLE_APPLICATION_CREDENTIALS;
23use crate::credential::Credential;
24
25use super::{parse::parse_credential_bytes, vm_metadata::VmMetadataCredentialProvider};
26
27#[derive(Debug)]
34pub struct DefaultCredentialProvider {
35 chain: ProvideCredentialChain<Credential>,
36}
37
38impl Default for DefaultCredentialProvider {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl DefaultCredentialProvider {
45 pub fn builder() -> DefaultCredentialProviderBuilder {
47 DefaultCredentialProviderBuilder::default()
48 }
49
50 pub fn new() -> Self {
53 Self::builder().build()
54 }
55
56 pub fn with_chain(chain: ProvideCredentialChain<Credential>) -> Self {
58 Self { chain }
59 }
60
61 pub fn push_front(
63 mut self,
64 provider: impl ProvideCredential<Credential = Credential> + 'static,
65 ) -> Self {
66 self.chain = self.chain.push_front(provider);
67 self
68 }
69}
70impl ProvideCredential for DefaultCredentialProvider {
71 type Credential = Credential;
72
73 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
74 self.chain.provide_credential(ctx).await
75 }
76}
77
78#[derive(Default, Clone, Debug)]
79pub struct EnvCredentialProvider {
80 scope: Option<String>,
81}
82
83impl EnvCredentialProvider {
84 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
91 self.scope = Some(scope.into());
92 self
93 }
94}
95impl ProvideCredential for EnvCredentialProvider {
96 type Credential = Credential;
97
98 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
99 let path = match ctx.env_var(GOOGLE_APPLICATION_CREDENTIALS) {
100 Some(path) if !path.is_empty() => path,
101 _ => return Ok(None),
102 };
103
104 debug!("trying to load credential from env GOOGLE_APPLICATION_CREDENTIALS: {path}");
105
106 let content = ctx.file_read(&path).await?;
107 parse_credential_bytes(ctx, &content, self.scope.clone()).await
108 }
109}
110
111#[derive(Default, Clone, Debug)]
112pub struct WellKnownCredentialProvider {
113 scope: Option<String>,
114}
115
116impl WellKnownCredentialProvider {
117 pub fn new() -> Self {
119 Self::default()
120 }
121
122 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
124 self.scope = Some(scope.into());
125 self
126 }
127}
128impl ProvideCredential for WellKnownCredentialProvider {
129 type Credential = Credential;
130
131 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
132 let config_dir = if let Some(v) = ctx.env_var("APPDATA") {
133 v
134 } else if let Some(v) = ctx.env_var("XDG_CONFIG_HOME") {
135 v
136 } else if let Some(v) = ctx.env_var("HOME") {
137 format!("{v}/.config")
138 } else {
139 return Ok(None);
140 };
141
142 let path = format!("{config_dir}/gcloud/application_default_credentials.json");
143 debug!("trying to load credential from well-known location: {path}");
144
145 let content = match ctx.file_read(&path).await {
146 Ok(v) => v,
147 Err(_) => return Ok(None),
148 };
149
150 match parse_credential_bytes(ctx, &content, self.scope.clone()).await {
151 Ok(v) => Ok(v),
152 Err(_) => Ok(None),
153 }
154 }
155}
156
157pub struct DefaultCredentialProviderBuilder {
159 env: Option<EnvCredentialProvider>,
160 well_known: Option<WellKnownCredentialProvider>,
161 vm_metadata: Option<VmMetadataCredentialProvider>,
162}
163
164impl Default for DefaultCredentialProviderBuilder {
165 fn default() -> Self {
166 Self {
167 env: Some(EnvCredentialProvider::new()),
168 well_known: Some(WellKnownCredentialProvider::new()),
169 vm_metadata: Some(VmMetadataCredentialProvider::new()),
170 }
171 }
172}
173
174impl DefaultCredentialProviderBuilder {
175 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn env(mut self, provider: EnvCredentialProvider) -> Self {
182 self.env = Some(provider);
183 self
184 }
185
186 pub fn no_env(mut self) -> Self {
188 self.env = None;
189 self
190 }
191
192 pub fn well_known(mut self, provider: WellKnownCredentialProvider) -> Self {
194 self.well_known = Some(provider);
195 self
196 }
197
198 pub fn no_well_known(mut self) -> Self {
200 self.well_known = None;
201 self
202 }
203
204 pub fn vm_metadata(mut self, provider: VmMetadataCredentialProvider) -> Self {
206 self.vm_metadata = Some(provider);
207 self
208 }
209
210 pub fn no_vm_metadata(mut self) -> Self {
212 self.vm_metadata = None;
213 self
214 }
215
216 pub fn build(self) -> DefaultCredentialProvider {
218 let mut chain = ProvideCredentialChain::new();
219
220 if let Some(p) = self.env {
221 chain = chain.push(p);
222 }
223
224 if let Some(p) = self.well_known {
225 chain = chain.push(p);
226 }
227
228 if let Some(p) = self.vm_metadata {
229 chain = chain.push(p);
230 }
231
232 DefaultCredentialProvider::with_chain(chain)
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use bytes::Bytes;
240 use reqsign_core::{Context, FileRead, HttpSend, StaticEnv};
241 use std::collections::HashMap;
242 use std::env;
243 use std::sync::{Arc, Mutex};
244
245 #[derive(Clone, Debug, Default)]
246 struct MockHttpSend {
247 uris: Arc<Mutex<Vec<String>>>,
248 }
249
250 impl HttpSend for MockHttpSend {
251 async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
252 self.uris.lock().unwrap().push(req.uri().to_string());
253
254 Ok(http::Response::builder()
255 .status(http::StatusCode::OK)
256 .body(
257 br#"{"access_token":"test-access-token","expires_in":3600}"#
258 .as_slice()
259 .into(),
260 )
261 .expect("response must build"))
262 }
263 }
264
265 #[derive(Clone, Debug, Default)]
266 struct MockFileRead {
267 files: Arc<HashMap<String, Vec<u8>>>,
268 paths: Arc<Mutex<Vec<String>>>,
269 }
270
271 impl MockFileRead {
272 fn new(files: HashMap<String, Vec<u8>>) -> Self {
273 Self {
274 files: Arc::new(files),
275 paths: Arc::new(Mutex::new(Vec::new())),
276 }
277 }
278 }
279
280 impl FileRead for MockFileRead {
281 async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
282 self.paths.lock().unwrap().push(path.to_string());
283 self.files.get(path).cloned().ok_or_else(|| {
284 reqsign_core::Error::config_invalid(format!("file not found: {path}"))
285 })
286 }
287 }
288
289 #[tokio::test]
290 async fn test_default_provider_env() {
291 let envs = HashMap::from([(
292 GOOGLE_APPLICATION_CREDENTIALS.to_string(),
293 format!(
294 "{}/testdata/test_credential.json",
295 env::current_dir()
296 .expect("current_dir must exist")
297 .to_string_lossy()
298 ),
299 )]);
300
301 let ctx = Context::new()
302 .with_file_read(reqsign_file_read_tokio::TokioFileRead)
303 .with_http_send(reqsign_http_send_reqwest::ReqwestHttpSend::default())
304 .with_env(StaticEnv {
305 home_dir: None,
306 envs,
307 });
308
309 let provider = DefaultCredentialProvider::new();
310 let cred = provider
311 .provide_credential(&ctx)
312 .await
313 .expect("load must succeed");
314 assert!(cred.is_some());
315
316 let cred = cred.unwrap();
317 assert!(cred.has_service_account());
318 let sa = cred.service_account.as_ref().unwrap();
319 assert_eq!("test-234@test.iam.gserviceaccount.com", &sa.client_email);
320 }
321
322 #[tokio::test]
323 async fn test_default_provider_builder_default_chain() {
324 let provider = DefaultCredentialProvider::builder().build();
325
326 let ctx = Context::new()
328 .with_file_read(reqsign_file_read_tokio::TokioFileRead)
329 .with_http_send(reqsign_http_send_reqwest::ReqwestHttpSend::default());
330 let _ = provider.provide_credential(&ctx).await;
331 }
332
333 #[tokio::test]
334 async fn test_default_provider_no_env_removes_provider() -> Result<()> {
335 let env_path = "/tmp/google-env-adc.json";
336 let file_read = MockFileRead::new(HashMap::from([(
337 env_path.to_string(),
338 include_bytes!(concat!(
339 env!("CARGO_MANIFEST_DIR"),
340 "/testdata/test_credential.json"
341 ))
342 .to_vec(),
343 )]));
344 let ctx = Context::new()
345 .with_file_read(file_read.clone())
346 .with_env(StaticEnv {
347 home_dir: None,
348 envs: HashMap::from([(
349 GOOGLE_APPLICATION_CREDENTIALS.to_string(),
350 env_path.to_string(),
351 )]),
352 });
353
354 let provider = DefaultCredentialProvider::builder()
355 .no_env()
356 .no_well_known()
357 .no_vm_metadata()
358 .build();
359
360 assert!(provider.provide_credential(&ctx).await?.is_none());
361 assert!(file_read.paths.lock().unwrap().is_empty());
362
363 Ok(())
364 }
365
366 #[tokio::test]
367 async fn test_default_provider_no_well_known_removes_provider() -> Result<()> {
368 let home = "/tmp/google-home";
369 let well_known_path = format!("{home}/.config/gcloud/application_default_credentials.json");
370 let file_read = MockFileRead::new(HashMap::from([(
371 well_known_path.clone(),
372 include_bytes!(concat!(
373 env!("CARGO_MANIFEST_DIR"),
374 "/testdata/test_credential.json"
375 ))
376 .to_vec(),
377 )]));
378 let ctx = Context::new()
379 .with_file_read(file_read.clone())
380 .with_env(StaticEnv {
381 home_dir: None,
382 envs: HashMap::from([("HOME".to_string(), home.to_string())]),
383 });
384
385 let provider = DefaultCredentialProvider::builder()
386 .no_env()
387 .no_well_known()
388 .no_vm_metadata()
389 .build();
390
391 assert!(provider.provide_credential(&ctx).await?.is_none());
392 assert!(file_read.paths.lock().unwrap().is_empty());
393
394 Ok(())
395 }
396
397 #[tokio::test]
398 async fn test_default_provider_no_vm_metadata_removes_provider() -> Result<()> {
399 let http = MockHttpSend::default();
400 let ctx = Context::new().with_http_send(http.clone());
401
402 let provider = DefaultCredentialProvider::builder()
403 .no_env()
404 .no_well_known()
405 .no_vm_metadata()
406 .build();
407
408 assert!(provider.provide_credential(&ctx).await?.is_none());
409 assert!(http.uris.lock().unwrap().is_empty());
410
411 Ok(())
412 }
413
414 #[tokio::test]
415 async fn test_default_provider_custom_vm_metadata_service_account() -> Result<()> {
416 let http = MockHttpSend::default();
417 let ctx = Context::new().with_http_send(http.clone());
418
419 let provider = DefaultCredentialProvider::builder()
420 .no_env()
421 .no_well_known()
422 .vm_metadata(
423 VmMetadataCredentialProvider::new()
424 .with_endpoint("127.0.0.1:8080")
425 .with_service_account("custom@test-project.iam.gserviceaccount.com"),
426 )
427 .build();
428
429 let cred = provider
430 .provide_credential(&ctx)
431 .await?
432 .expect("credential must exist");
433
434 assert!(cred.has_token());
435 assert_eq!(
436 http.uris.lock().unwrap().as_slice(),
437 &["http://127.0.0.1:8080/computeMetadata/v1/instance/service-accounts/custom@test-project.iam.gserviceaccount.com/token?scopes=https://www.googleapis.com/auth/cloud-platform".to_string()]
438 );
439
440 Ok(())
441 }
442}