reqsign_aws_core/provide_credential/
default.rs1use crate::Credential;
19use crate::provide_credential::{
20 AssumeRoleWithWebIdentityCredentialProvider, ECSCredentialProvider, EnvCredentialProvider,
21 IMDSv2CredentialProvider, ProfileCredentialProvider,
22};
23#[cfg(not(target_arch = "wasm32"))]
24use crate::provide_credential::{ProcessCredentialProvider, SSOCredentialProvider};
25use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain, Result};
26
27#[derive(Debug)]
39pub struct DefaultCredentialProvider {
40 chain: ProvideCredentialChain<Credential>,
41}
42
43impl Default for DefaultCredentialProvider {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl DefaultCredentialProvider {
50 pub fn builder() -> DefaultCredentialProviderBuilder {
52 DefaultCredentialProviderBuilder::default()
53 }
54
55 pub fn new() -> Self {
57 Self::builder().build()
58 }
59
60 pub fn with_chain(chain: ProvideCredentialChain<Credential>) -> Self {
62 Self { chain }
63 }
64
65 pub fn push_front(
79 mut self,
80 provider: impl ProvideCredential<Credential = Credential> + 'static,
81 ) -> Self {
82 self.chain = self.chain.push_front(provider);
83 self
84 }
85}
86
87pub struct DefaultCredentialProviderBuilder {
92 env: Option<EnvCredentialProvider>,
93 profile: Option<ProfileCredentialProvider>,
94 #[cfg(not(target_arch = "wasm32"))]
95 sso: Option<SSOCredentialProvider>,
96 web_identity: Option<AssumeRoleWithWebIdentityCredentialProvider>,
97 #[cfg(not(target_arch = "wasm32"))]
98 process: Option<ProcessCredentialProvider>,
99 ecs: Option<ECSCredentialProvider>,
100 imds: Option<IMDSv2CredentialProvider>,
101}
102
103impl Default for DefaultCredentialProviderBuilder {
104 fn default() -> Self {
105 Self {
106 env: Some(EnvCredentialProvider::new()),
107 profile: Some(ProfileCredentialProvider::default()),
108 #[cfg(not(target_arch = "wasm32"))]
109 sso: Some(SSOCredentialProvider::default()),
110 web_identity: Some(AssumeRoleWithWebIdentityCredentialProvider::default()),
111 #[cfg(not(target_arch = "wasm32"))]
112 process: Some(ProcessCredentialProvider::default()),
113 ecs: Some(ECSCredentialProvider::default()),
114 imds: Some(IMDSv2CredentialProvider::default()),
115 }
116 }
117}
118
119impl DefaultCredentialProviderBuilder {
120 pub fn new() -> Self {
122 Self::default()
123 }
124
125 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
136 let profile = profile.into();
137
138 self.profile = self
139 .profile
140 .map(|provider| provider.with_profile(profile.clone()));
141
142 #[cfg(not(target_arch = "wasm32"))]
143 {
144 self.sso = self
145 .sso
146 .map(|provider| provider.with_profile(profile.clone()));
147 self.process = self.process.map(|provider| provider.with_profile(profile));
148 }
149
150 self
151 }
152
153 pub fn env(mut self, provider: EnvCredentialProvider) -> Self {
155 self.env = Some(provider);
156 self
157 }
158
159 pub fn no_env(mut self) -> Self {
161 self.env = None;
162 self
163 }
164
165 pub fn profile(mut self, provider: ProfileCredentialProvider) -> Self {
167 self.profile = Some(provider);
168 self
169 }
170
171 pub fn no_profile(mut self) -> Self {
173 self.profile = None;
174 self
175 }
176
177 #[cfg(not(target_arch = "wasm32"))]
179 pub fn sso(mut self, provider: SSOCredentialProvider) -> Self {
180 self.sso = Some(provider);
181 self
182 }
183
184 #[cfg(not(target_arch = "wasm32"))]
186 pub fn no_sso(mut self) -> Self {
187 self.sso = None;
188 self
189 }
190
191 pub fn web_identity(mut self, provider: AssumeRoleWithWebIdentityCredentialProvider) -> Self {
193 self.web_identity = Some(provider);
194 self
195 }
196
197 pub fn no_web_identity(mut self) -> Self {
199 self.web_identity = None;
200 self
201 }
202
203 #[cfg(not(target_arch = "wasm32"))]
205 pub fn process(mut self, provider: ProcessCredentialProvider) -> Self {
206 self.process = Some(provider);
207 self
208 }
209
210 #[cfg(not(target_arch = "wasm32"))]
212 pub fn no_process(mut self) -> Self {
213 self.process = None;
214 self
215 }
216
217 pub fn ecs(mut self, provider: ECSCredentialProvider) -> Self {
219 self.ecs = Some(provider);
220 self
221 }
222
223 pub fn no_ecs(mut self) -> Self {
225 self.ecs = None;
226 self
227 }
228
229 pub fn imds(mut self, provider: IMDSv2CredentialProvider) -> Self {
231 self.imds = Some(provider);
232 self
233 }
234
235 pub fn no_imds(mut self) -> Self {
237 self.imds = None;
238 self
239 }
240
241 pub fn build(self) -> DefaultCredentialProvider {
243 let mut chain = ProvideCredentialChain::new();
244
245 if let Some(p) = self.env {
246 chain = chain.push(p);
247 }
248
249 if let Some(p) = self.profile {
250 chain = chain.push(p);
251 }
252
253 #[cfg(not(target_arch = "wasm32"))]
254 {
255 if let Some(p) = self.sso {
256 chain = chain.push(p);
257 }
258 }
259
260 if let Some(p) = self.web_identity {
261 chain = chain.push(p);
262 }
263
264 #[cfg(not(target_arch = "wasm32"))]
265 {
266 if let Some(p) = self.process {
267 chain = chain.push(p);
268 }
269 }
270
271 if let Some(p) = self.ecs {
272 chain = chain.push(p);
273 }
274
275 if let Some(p) = self.imds {
276 chain = chain.push(p);
277 }
278
279 DefaultCredentialProvider::with_chain(chain)
280 }
281}
282impl ProvideCredential for DefaultCredentialProvider {
283 type Credential = Credential;
284
285 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
286 self.chain.provide_credential(ctx).await
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::constants::{
294 AWS_ACCESS_KEY_ID, AWS_CONFIG_FILE, AWS_PROFILE, AWS_SECRET_ACCESS_KEY,
295 AWS_SHARED_CREDENTIALS_FILE,
296 };
297 #[cfg(not(target_arch = "wasm32"))]
298 use reqsign_command_execute_tokio::TokioCommandExecute;
299 #[cfg(not(target_arch = "wasm32"))]
300 use reqsign_core::ErrorKind;
301 use reqsign_core::{OsEnv, StaticEnv};
302 use reqsign_file_read_tokio::TokioFileRead;
303 use reqsign_http_send_reqwest::ReqwestHttpSend;
304 use std::collections::HashMap;
305 use std::fs::File;
306 use std::io::Write;
307 use std::path::Path;
308 use tempfile::tempdir;
309
310 fn test_path(path: &str) -> String {
311 Path::new(env!("CARGO_MANIFEST_DIR"))
312 .join(path)
313 .to_string_lossy()
314 .into_owned()
315 }
316
317 #[tokio::test]
318 async fn test_credential_env_loader_without_env() {
319 let _ = env_logger::builder().is_test(true).try_init();
320
321 let ctx = Context::new()
322 .with_file_read(TokioFileRead)
323 .with_http_send(ReqwestHttpSend::default())
324 .with_env(OsEnv);
325 let ctx = ctx.with_env(StaticEnv {
326 home_dir: None,
327 envs: HashMap::new(),
328 });
329
330 let builder = DefaultCredentialProvider::builder()
331 .no_profile()
332 .no_web_identity()
333 .no_ecs()
334 .no_imds();
335 #[cfg(not(target_arch = "wasm32"))]
336 let builder = builder.no_sso().no_process();
337 #[cfg(target_arch = "wasm32")]
338 let builder = builder;
339
340 let l = builder.build();
341 let x = l.provide_credential(&ctx).await.expect("load must succeed");
342 assert!(x.is_none());
343 }
344
345 #[tokio::test]
346 async fn test_credential_env_loader_with_env() {
347 let _ = env_logger::builder().is_test(true).try_init();
348
349 let ctx = Context::new()
350 .with_file_read(TokioFileRead)
351 .with_http_send(ReqwestHttpSend::default())
352 .with_env(OsEnv);
353 let ctx = ctx.with_env(StaticEnv {
354 home_dir: None,
355 envs: HashMap::from_iter([
356 (AWS_ACCESS_KEY_ID.to_string(), "access_key_id".to_string()),
357 (
358 AWS_SECRET_ACCESS_KEY.to_string(),
359 "secret_access_key".to_string(),
360 ),
361 ]),
362 });
363
364 let l = DefaultCredentialProvider::new();
365 let x = l.provide_credential(&ctx).await.expect("load must succeed");
366
367 let x = x.expect("must load succeed");
368 assert_eq!("access_key_id", x.access_key_id);
369 assert_eq!("secret_access_key", x.secret_access_key);
370 }
371
372 #[tokio::test]
373 async fn test_default_credential_provider_no_env_removes_slot() {
374 let _ = env_logger::builder().is_test(true).try_init();
375
376 let ctx = Context::new()
377 .with_file_read(TokioFileRead)
378 .with_http_send(ReqwestHttpSend::default())
379 .with_env(OsEnv);
380 let ctx = ctx.with_env(StaticEnv {
381 home_dir: None,
382 envs: HashMap::from_iter([
383 (AWS_ACCESS_KEY_ID.to_string(), "access_key_id".to_string()),
384 (
385 AWS_SECRET_ACCESS_KEY.to_string(),
386 "secret_access_key".to_string(),
387 ),
388 ]),
389 });
390
391 let builder = DefaultCredentialProvider::builder()
392 .no_env()
393 .no_profile()
394 .no_imds()
395 .no_ecs()
396 .no_web_identity();
397 #[cfg(not(target_arch = "wasm32"))]
398 let builder = builder.no_sso().no_process();
399 #[cfg(target_arch = "wasm32")]
400 let builder = builder;
401
402 let provider = builder.build();
403
404 let cred = provider
405 .provide_credential(&ctx)
406 .await
407 .expect("load must succeed");
408 assert!(cred.is_none());
409 }
410
411 #[tokio::test]
412 async fn test_credential_profile_loader_from_config() {
413 let _ = env_logger::builder().is_test(true).try_init();
414
415 let ctx = Context::new()
416 .with_file_read(TokioFileRead)
417 .with_http_send(ReqwestHttpSend::default())
418 .with_env(OsEnv);
419 let ctx = ctx.with_env(StaticEnv {
420 home_dir: None,
421 envs: HashMap::from_iter([
422 (
423 AWS_CONFIG_FILE.to_string(),
424 test_path("testdata/default_config"),
425 ),
426 (
427 AWS_SHARED_CREDENTIALS_FILE.to_string(),
428 test_path("testdata/not_exist"),
429 ),
430 ]),
431 });
432
433 let l = DefaultCredentialProvider::new();
434 let x = l.provide_credential(&ctx).await.unwrap().unwrap();
435 assert_eq!("config_access_key_id", x.access_key_id);
436 assert_eq!("config_secret_access_key", x.secret_access_key);
437 }
438
439 #[tokio::test]
440 async fn test_credential_profile_loader_from_shared() {
441 let _ = env_logger::builder().is_test(true).try_init();
442
443 let ctx = Context::new()
444 .with_file_read(TokioFileRead)
445 .with_http_send(ReqwestHttpSend::default())
446 .with_env(OsEnv);
447 let ctx = ctx.with_env(StaticEnv {
448 home_dir: None,
449 envs: HashMap::from_iter([
450 (AWS_CONFIG_FILE.to_string(), test_path("testdata/not_exist")),
451 (
452 AWS_SHARED_CREDENTIALS_FILE.to_string(),
453 test_path("testdata/default_credential"),
454 ),
455 ]),
456 });
457
458 let l = DefaultCredentialProvider::new();
459 let x = l.provide_credential(&ctx).await.unwrap().unwrap();
460 assert_eq!("shared_access_key_id", x.access_key_id);
461 assert_eq!("shared_secret_access_key", x.secret_access_key);
462 }
463
464 #[tokio::test]
465 async fn test_default_credential_provider_prepend() {
466 let _ = env_logger::builder().is_test(true).try_init();
467
468 let ctx = Context::new()
469 .with_file_read(TokioFileRead)
470 .with_http_send(ReqwestHttpSend::default())
471 .with_env(OsEnv);
472 let ctx = ctx.with_env(StaticEnv {
473 home_dir: None,
474 envs: HashMap::from_iter([
475 (AWS_ACCESS_KEY_ID.to_string(), "env_access_key".to_string()),
477 (
478 AWS_SECRET_ACCESS_KEY.to_string(),
479 "env_secret_key".to_string(),
480 ),
481 ]),
482 });
483
484 let static_provider =
486 crate::StaticCredentialProvider::new("static_access_key", "static_secret_key");
487
488 let provider = DefaultCredentialProvider::new().push_front(static_provider);
490
491 let cred = provider
493 .provide_credential(&ctx)
494 .await
495 .expect("load must succeed")
496 .expect("credential must exist");
497
498 assert_eq!("static_access_key", cred.access_key_id);
499 assert_eq!("static_secret_key", cred.secret_access_key);
500 }
501
502 #[tokio::test]
503 async fn test_default_credential_provider_no_profile_removes_slot() {
504 let _ = env_logger::builder().is_test(true).try_init();
505
506 let ctx = Context::new()
507 .with_file_read(TokioFileRead)
508 .with_http_send(ReqwestHttpSend::default())
509 .with_env(OsEnv);
510 let ctx = ctx.with_env(StaticEnv {
511 home_dir: None,
512 envs: HashMap::from_iter([
513 (
514 AWS_CONFIG_FILE.to_string(),
515 test_path("testdata/default_config"),
516 ),
517 (
518 AWS_SHARED_CREDENTIALS_FILE.to_string(),
519 test_path("testdata/not_exist"),
520 ),
521 ]),
522 });
523
524 let builder = DefaultCredentialProvider::builder()
525 .no_profile()
526 .no_imds()
527 .no_ecs()
528 .no_web_identity();
529 #[cfg(not(target_arch = "wasm32"))]
530 let builder = builder.no_sso().no_process();
531 #[cfg(target_arch = "wasm32")]
532 let builder = builder;
533
534 let provider = builder.build();
535
536 let cred = provider
537 .provide_credential(&ctx)
538 .await
539 .expect("load must succeed");
540 assert!(cred.is_none());
541 }
542
543 #[tokio::test]
544 async fn test_default_credential_provider_custom_profile_slot() {
545 let _ = env_logger::builder().is_test(true).try_init();
546
547 let ctx = Context::new()
548 .with_file_read(TokioFileRead)
549 .with_http_send(ReqwestHttpSend::default())
550 .with_env(OsEnv);
551 let ctx = ctx.with_env(StaticEnv {
552 home_dir: None,
553 envs: HashMap::new(),
554 });
555
556 let custom_config = test_path("testdata/default_config");
558
559 let provider = DefaultCredentialProvider::builder()
560 .profile(ProfileCredentialProvider::new().with_config_file(custom_config))
561 .build();
562
563 let cred = provider
564 .provide_credential(&ctx)
565 .await
566 .expect("load must succeed");
567 let cred = cred.expect("credential should exist");
568 assert_eq!("config_access_key_id", cred.access_key_id);
569 assert_eq!("config_secret_access_key", cred.secret_access_key);
570 }
571
572 #[tokio::test]
573 async fn test_with_profile_configures_profile_slot() -> anyhow::Result<()> {
574 let tmp_dir = tempdir()?;
575 let credentials_file = tmp_dir.path().join("credentials");
576 let mut file = File::create(&credentials_file)?;
577 writeln!(file, "[ambient]")?;
578 writeln!(file, "aws_access_key_id = AMBIENT")?;
579 writeln!(file, "aws_secret_access_key = ambient-secret")?;
580 writeln!(file)?;
581 writeln!(file, "[selected]")?;
582 writeln!(file, "aws_access_key_id = SELECTED")?;
583 writeln!(file, "aws_secret_access_key = selected-secret")?;
584
585 let ctx = Context::new()
586 .with_file_read(TokioFileRead)
587 .with_env(StaticEnv {
588 home_dir: None,
589 envs: HashMap::from([(AWS_PROFILE.to_string(), "ambient".to_string())]),
590 });
591
592 let builder = DefaultCredentialProvider::builder()
593 .profile(
594 ProfileCredentialProvider::new()
595 .with_credentials_file(credentials_file.to_string_lossy()),
596 )
597 .with_profile("selected");
598 let provider = builder.profile.expect("profile slot must remain enabled");
599
600 let credential = provider
601 .provide_credential(&ctx)
602 .await?
603 .expect("selected profile must provide credentials");
604 assert_eq!("SELECTED", credential.access_key_id);
605 assert_eq!("selected-secret", credential.secret_access_key);
606
607 Ok(())
608 }
609
610 #[cfg(not(target_arch = "wasm32"))]
611 #[tokio::test]
612 async fn test_with_profile_configures_sso_slot() -> anyhow::Result<()> {
613 let tmp_dir = tempdir()?;
614 let config_file = tmp_dir.path().join("config");
615 let mut file = File::create(&config_file)?;
616 writeln!(file, "[profile selected]")?;
617 writeln!(file, "sso_account_id = 123456789012")?;
618 writeln!(file, "sso_region = us-east-1")?;
619 writeln!(file, "sso_role_name = Developer")?;
620 writeln!(file, "sso_start_url = https://example.awsapps.com/start")?;
621
622 let ctx = Context::new()
623 .with_file_read(TokioFileRead)
624 .with_env(StaticEnv {
625 home_dir: Some(tmp_dir.path().to_path_buf()),
626 envs: HashMap::from([
627 (
628 AWS_CONFIG_FILE.to_string(),
629 config_file.to_string_lossy().into(),
630 ),
631 (AWS_PROFILE.to_string(), "ambient".to_string()),
632 ]),
633 });
634
635 let builder = DefaultCredentialProvider::builder().with_profile("selected");
636 let provider = builder.sso.expect("SSO slot must remain enabled");
637
638 let error = provider
639 .provide_credential(&ctx)
640 .await
641 .expect_err("selected SSO profile must be loaded before cache lookup");
642 assert_eq!(ErrorKind::ConfigInvalid, error.kind());
643 assert_eq!(
644 "No valid SSO token found. Please run 'aws sso login' first",
645 error.to_string()
646 );
647
648 Ok(())
649 }
650
651 #[cfg(not(target_arch = "wasm32"))]
652 #[tokio::test]
653 async fn test_with_profile_configures_process_slot() -> anyhow::Result<()> {
654 let tmp_dir = tempdir()?;
655 let config_file = tmp_dir.path().join("config");
656 let helper = test_path("tests/mocks/credential_process_helper.py");
657 let mut file = File::create(&config_file)?;
658 writeln!(file, "[profile ambient]")?;
659 writeln!(file, "credential_process = python3 {helper}")?;
660 writeln!(file)?;
661 writeln!(file, "[profile selected]")?;
662 writeln!(file, "credential_process = python3 {helper} --profile test")?;
663
664 let ctx = Context::new()
665 .with_file_read(TokioFileRead)
666 .with_command_execute(TokioCommandExecute)
667 .with_env(StaticEnv {
668 home_dir: None,
669 envs: HashMap::from([
670 (
671 AWS_CONFIG_FILE.to_string(),
672 config_file.to_string_lossy().into(),
673 ),
674 (AWS_PROFILE.to_string(), "ambient".to_string()),
675 ]),
676 });
677
678 let builder = DefaultCredentialProvider::builder().with_profile("selected");
679 let provider = builder.process.expect("process slot must remain enabled");
680
681 let credential = provider
682 .provide_credential(&ctx)
683 .await?
684 .expect("selected process profile must provide credentials");
685 assert_eq!("ASIAPROCESSTEST", credential.access_key_id);
686 assert_eq!(
687 "process/test/secret/key/EXAMPLE",
688 credential.secret_access_key
689 );
690
691 Ok(())
692 }
693
694 #[test]
695 fn test_with_profile_preserves_removed_slots() {
696 let builder = DefaultCredentialProvider::builder().no_profile();
697 #[cfg(not(target_arch = "wasm32"))]
698 let builder = builder.no_sso().no_process();
699
700 let builder = builder.with_profile("selected");
701
702 assert!(builder.profile.is_none());
703 #[cfg(not(target_arch = "wasm32"))]
704 {
705 assert!(builder.sso.is_none());
706 assert!(builder.process.is_none());
707 }
708 }
709
710 #[cfg(not(target_arch = "wasm32"))]
711 #[tokio::test]
712 async fn test_default_credential_provider_custom_process_slot() {
713 let _ = env_logger::builder().is_test(true).try_init();
714
715 let ctx = Context::new()
716 .with_file_read(TokioFileRead)
717 .with_http_send(ReqwestHttpSend::default())
718 .with_command_execute(TokioCommandExecute)
719 .with_env(OsEnv);
720 let ctx = ctx.with_env(StaticEnv {
721 home_dir: None,
722 envs: HashMap::new(),
723 });
724
725 let helper = test_path("tests/mocks/credential_process_helper.py");
726
727 let provider = DefaultCredentialProvider::builder()
728 .no_env()
729 .no_profile()
730 .no_sso()
731 .no_web_identity()
732 .no_ecs()
733 .no_imds()
734 .process(ProcessCredentialProvider::new().with_command(format!("python3 {helper}")))
735 .build();
736
737 let cred = provider
738 .provide_credential(&ctx)
739 .await
740 .expect("load must succeed")
741 .expect("credential should exist");
742 assert_eq!("ASIAPROCESSEXAMPLE", cred.access_key_id);
743 assert_eq!("process/secret/key/EXAMPLE", cred.secret_access_key);
744 }
745
746 #[cfg(not(target_arch = "wasm32"))]
747 #[tokio::test]
748 async fn test_default_credential_provider_no_process_removes_slot() {
749 let _ = env_logger::builder().is_test(true).try_init();
750
751 let ctx = Context::new()
752 .with_file_read(TokioFileRead)
753 .with_http_send(ReqwestHttpSend::default())
754 .with_command_execute(TokioCommandExecute)
755 .with_env(OsEnv);
756 let ctx = ctx.with_env(StaticEnv {
757 home_dir: None,
758 envs: HashMap::new(),
759 });
760
761 let helper = test_path("tests/mocks/credential_process_helper.py");
762
763 let provider = DefaultCredentialProvider::builder()
764 .no_env()
765 .no_profile()
766 .no_sso()
767 .no_web_identity()
768 .no_ecs()
769 .no_imds()
770 .process(ProcessCredentialProvider::new().with_command(format!("python3 {helper}")))
771 .no_process()
772 .build();
773
774 let cred = provider
775 .provide_credential(&ctx)
776 .await
777 .expect("load must succeed");
778 assert!(cred.is_none());
779 }
780
781 #[tokio::test]
783 async fn test_credential_profile_loader_from_both() {
784 let _ = env_logger::builder().is_test(true).try_init();
785
786 let ctx = Context::new()
787 .with_file_read(TokioFileRead)
788 .with_http_send(ReqwestHttpSend::default())
789 .with_env(OsEnv);
790 let ctx = ctx.with_env(StaticEnv {
791 home_dir: None,
792 envs: HashMap::from_iter([
793 (
794 AWS_CONFIG_FILE.to_string(),
795 test_path("testdata/default_config"),
796 ),
797 (
798 AWS_SHARED_CREDENTIALS_FILE.to_string(),
799 test_path("testdata/default_credential"),
800 ),
801 ]),
802 });
803
804 let l = DefaultCredentialProvider::new();
805 let x = l
806 .provide_credential(&ctx)
807 .await
808 .expect("load must success")
809 .unwrap();
810 assert_eq!("shared_access_key_id", x.access_key_id);
811 assert_eq!("shared_secret_access_key", x.secret_access_key);
812 }
813}