Skip to main content

reqsign_aws_core/provide_credential/
default.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// DefaultCredentialProvider is a loader that will try to load credential via default chains.
28///
29/// Resolution order:
30///
31/// 1. Environment variables
32/// 2. Shared config (`~/.aws/config`, `~/.aws/credentials`)
33/// 3. SSO credentials
34/// 4. Web Identity Tokens
35/// 5. Process credentials
36/// 6. ECS (IAM Roles for Tasks) & Container credentials
37/// 7. EC2 IMDSv2
38#[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    /// Create a builder to configure the default credential chain.
51    pub fn builder() -> DefaultCredentialProviderBuilder {
52        DefaultCredentialProviderBuilder::default()
53    }
54
55    /// Create a new `DefaultCredentialProvider` instance using the default chain.
56    pub fn new() -> Self {
57        Self::builder().build()
58    }
59
60    /// Create with a custom credential chain.
61    pub fn with_chain(chain: ProvideCredentialChain<Credential>) -> Self {
62        Self { chain }
63    }
64
65    /// Add a credential provider to the front of the default chain.
66    ///
67    /// This allows adding a high-priority credential source that will be tried
68    /// before all other providers in the default chain.
69    ///
70    /// # Example
71    ///
72    /// ```no_run
73    /// use reqsign_aws_core::{DefaultCredentialProvider, StaticCredentialProvider};
74    ///
75    /// let provider = DefaultCredentialProvider::new()
76    ///     .push_front(StaticCredentialProvider::new("access_key_id", "secret_access_key"));
77    /// ```
78    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
87/// Builder for `DefaultCredentialProvider`.
88///
89/// Use `slot(provider)` to override a default slot and `no_slot()` to remove it
90/// from the default chain. Call `build()` to construct the provider.
91pub 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    /// Create a new builder with default state.
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Select the AWS profile used by all profile-aware provider slots.
126    ///
127    /// The explicit profile is applied to the shared profile, SSO, and process
128    /// providers that are still enabled. Other settings on those providers are
129    /// preserved, and slots removed with `no_profile()`, `no_sso()`, or
130    /// `no_process()` remain removed.
131    ///
132    /// An explicitly selected profile takes precedence over `AWS_PROFILE`.
133    /// Slot-level methods called after this method can still replace or remove
134    /// individual providers.
135    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    /// Override the environment credential provider slot.
154    pub fn env(mut self, provider: EnvCredentialProvider) -> Self {
155        self.env = Some(provider);
156        self
157    }
158
159    /// Remove the environment credential provider from the chain.
160    pub fn no_env(mut self) -> Self {
161        self.env = None;
162        self
163    }
164
165    /// Override the profile credential provider slot.
166    pub fn profile(mut self, provider: ProfileCredentialProvider) -> Self {
167        self.profile = Some(provider);
168        self
169    }
170
171    /// Remove the profile credential provider from the chain.
172    pub fn no_profile(mut self) -> Self {
173        self.profile = None;
174        self
175    }
176
177    /// Override the SSO credential provider slot.
178    #[cfg(not(target_arch = "wasm32"))]
179    pub fn sso(mut self, provider: SSOCredentialProvider) -> Self {
180        self.sso = Some(provider);
181        self
182    }
183
184    /// Remove the SSO credential provider from the chain.
185    #[cfg(not(target_arch = "wasm32"))]
186    pub fn no_sso(mut self) -> Self {
187        self.sso = None;
188        self
189    }
190
191    /// Override the web-identity credential provider slot.
192    pub fn web_identity(mut self, provider: AssumeRoleWithWebIdentityCredentialProvider) -> Self {
193        self.web_identity = Some(provider);
194        self
195    }
196
197    /// Remove the web-identity credential provider from the chain.
198    pub fn no_web_identity(mut self) -> Self {
199        self.web_identity = None;
200        self
201    }
202
203    /// Override the external process credential provider slot.
204    #[cfg(not(target_arch = "wasm32"))]
205    pub fn process(mut self, provider: ProcessCredentialProvider) -> Self {
206        self.process = Some(provider);
207        self
208    }
209
210    /// Remove the external process credential provider from the chain.
211    #[cfg(not(target_arch = "wasm32"))]
212    pub fn no_process(mut self) -> Self {
213        self.process = None;
214        self
215    }
216
217    /// Override the ECS credential provider slot.
218    pub fn ecs(mut self, provider: ECSCredentialProvider) -> Self {
219        self.ecs = Some(provider);
220        self
221    }
222
223    /// Remove the ECS credential provider from the chain.
224    pub fn no_ecs(mut self) -> Self {
225        self.ecs = None;
226        self
227    }
228
229    /// Override the EC2 IMDSv2 credential provider slot.
230    pub fn imds(mut self, provider: IMDSv2CredentialProvider) -> Self {
231        self.imds = Some(provider);
232        self
233    }
234
235    /// Remove the EC2 IMDSv2 credential provider from the chain.
236    pub fn no_imds(mut self) -> Self {
237        self.imds = None;
238        self
239    }
240
241    /// Build the `DefaultCredentialProvider` with the configured options.
242    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                // Set environment variables that would normally be loaded
476                (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        // Create a static provider with different credentials
485        let static_provider =
486            crate::StaticCredentialProvider::new("static_access_key", "static_secret_key");
487
488        // Create default provider and push_front the static provider
489        let provider = DefaultCredentialProvider::new().push_front(static_provider);
490
491        // The static provider should take precedence over environment variables
492        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        // Build a custom chain with Profile provider using a custom config file
557        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    /// AWS_SHARED_CREDENTIALS_FILE should be taken first.
782    #[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}