aws_config/profile/credentials/
exec.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use super::repr::{self, BaseProvider};
7#[cfg(feature = "credentials-process")]
8use crate::credential_process::CredentialProcessProvider;
9use crate::profile::credentials::ProfileFileError;
10use crate::provider_config::ProviderConfig;
11use crate::sts;
12use crate::web_identity_token::{StaticConfiguration, WebIdentityTokenCredentialsProvider};
13use aws_credential_types::provider::{
14    self, error::CredentialsError, ProvideCredentials, SharedCredentialsProvider,
15};
16use aws_sdk_sts::config::Credentials;
17use aws_sdk_sts::Client as StsClient;
18use aws_smithy_async::time::SharedTimeSource;
19use aws_types::SdkConfig;
20use std::fmt::Debug;
21use std::sync::Arc;
22
23#[derive(Debug)]
24pub(super) struct AssumeRoleProvider {
25    role_arn: String,
26    external_id: Option<String>,
27    session_name: Option<String>,
28    time_source: SharedTimeSource,
29}
30
31impl AssumeRoleProvider {
32    pub(super) async fn credentials(
33        &self,
34        input_credentials: Credentials,
35        sdk_config: &SdkConfig,
36    ) -> provider::Result {
37        let config = sdk_config
38            .to_builder()
39            .credentials_provider(SharedCredentialsProvider::new(input_credentials))
40            .build();
41        let client = StsClient::new(&config);
42        let session_name = &self.session_name.as_ref().cloned().unwrap_or_else(|| {
43            sts::util::default_session_name("assume-role-from-profile", self.time_source.now())
44        });
45        let assume_role_creds = client
46            .assume_role()
47            .role_arn(&self.role_arn)
48            .set_external_id(self.external_id.clone())
49            .role_session_name(session_name)
50            .send()
51            .await
52            .map_err(CredentialsError::provider_error)?
53            .credentials;
54        sts::util::into_credentials(assume_role_creds, "AssumeRoleProvider")
55    }
56}
57
58#[derive(Debug)]
59pub(super) struct ProviderChain {
60    base: Arc<dyn ProvideCredentials>,
61    chain: Vec<AssumeRoleProvider>,
62}
63
64impl ProviderChain {
65    pub(crate) fn base(&self) -> &dyn ProvideCredentials {
66        self.base.as_ref()
67    }
68
69    pub(crate) fn chain(&self) -> &[AssumeRoleProvider] {
70        self.chain.as_slice()
71    }
72}
73
74impl ProviderChain {
75    pub(super) fn from_repr(
76        provider_config: &ProviderConfig,
77        repr: repr::ProfileChain<'_>,
78        factory: &named::NamedProviderFactory,
79    ) -> Result<Self, ProfileFileError> {
80        let base = match repr.base() {
81            BaseProvider::NamedSource(name) => {
82                factory
83                    .provider(name)
84                    .ok_or(ProfileFileError::UnknownProvider {
85                        name: name.to_string(),
86                    })?
87            }
88            BaseProvider::AccessKey(key) => Arc::new(key.clone()),
89            BaseProvider::CredentialProcess(_credential_process) => {
90                #[cfg(feature = "credentials-process")]
91                {
92                    Arc::new(CredentialProcessProvider::from_command(_credential_process))
93                }
94                #[cfg(not(feature = "credentials-process"))]
95                {
96                    Err(ProfileFileError::FeatureNotEnabled {
97                        feature: "credentials-process".into(),
98                        message: Some(
99                            "In order to spawn a subprocess, the `credentials-process` feature must be enabled."
100                                .into(),
101                        ),
102                    })?
103                }
104            }
105            BaseProvider::WebIdentityTokenRole {
106                role_arn,
107                web_identity_token_file,
108                session_name,
109            } => {
110                let provider = WebIdentityTokenCredentialsProvider::builder()
111                    .static_configuration(StaticConfiguration {
112                        web_identity_token_file: web_identity_token_file.into(),
113                        role_arn: role_arn.to_string(),
114                        session_name: session_name.map(|sess| sess.to_string()).unwrap_or_else(
115                            || {
116                                sts::util::default_session_name(
117                                    "web-identity-token-profile",
118                                    provider_config.time_source().now(),
119                                )
120                            },
121                        ),
122                    })
123                    .configure(provider_config)
124                    .build();
125                Arc::new(provider)
126            }
127            #[allow(unused_variables)]
128            BaseProvider::Sso {
129                sso_account_id,
130                sso_region,
131                sso_role_name,
132                sso_start_url,
133                sso_session_name,
134            } => {
135                #[cfg(feature = "sso")]
136                {
137                    use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider};
138                    use aws_types::region::Region;
139
140                    let (Some(sso_account_id), Some(sso_role_name)) =
141                        (sso_account_id, sso_role_name)
142                    else {
143                        return Err(ProfileFileError::TokenProviderConfig {});
144                    };
145                    let sso_config = SsoProviderConfig {
146                        account_id: sso_account_id.to_string(),
147                        role_name: sso_role_name.to_string(),
148                        start_url: sso_start_url.to_string(),
149                        region: Region::new(sso_region.to_string()),
150                        session_name: sso_session_name.map(|s| s.to_string()),
151                    };
152                    Arc::new(SsoCredentialsProvider::new(provider_config, sso_config))
153                }
154                #[cfg(not(feature = "sso"))]
155                {
156                    Err(ProfileFileError::FeatureNotEnabled {
157                        feature: "sso".into(),
158                        message: None,
159                    })?
160                }
161            }
162        };
163        tracing::debug!(base = ?repr.base(), "first credentials will be loaded from {:?}", repr.base());
164        let chain = repr
165            .chain()
166            .iter()
167            .map(|role_arn| {
168                tracing::debug!(role_arn = ?role_arn, "which will be used to assume a role");
169                AssumeRoleProvider {
170                    role_arn: role_arn.role_arn.into(),
171                    external_id: role_arn.external_id.map(Into::into),
172                    session_name: role_arn.session_name.map(Into::into),
173                    time_source: provider_config.time_source(),
174                }
175            })
176            .collect();
177        Ok(ProviderChain { base, chain })
178    }
179}
180
181pub(super) mod named {
182    use std::collections::HashMap;
183    use std::sync::Arc;
184
185    use aws_credential_types::provider::ProvideCredentials;
186    use std::borrow::Cow;
187
188    #[derive(Debug)]
189    pub(crate) struct NamedProviderFactory {
190        providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
191    }
192
193    fn lower_cow(mut input: Cow<'_, str>) -> Cow<'_, str> {
194        if !input.chars().all(|c| c.is_ascii_lowercase()) {
195            input.to_mut().make_ascii_lowercase();
196        }
197        input
198    }
199
200    impl NamedProviderFactory {
201        pub(crate) fn new(
202            providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
203        ) -> Self {
204            let providers = providers
205                .into_iter()
206                .map(|(k, v)| (lower_cow(k), v))
207                .collect();
208            Self { providers }
209        }
210
211        pub(crate) fn provider(&self, name: &str) -> Option<Arc<dyn ProvideCredentials>> {
212            self.providers.get(&lower_cow(Cow::Borrowed(name))).cloned()
213        }
214    }
215}
216
217#[cfg(test)]
218mod test {
219    use crate::profile::credentials::exec::named::NamedProviderFactory;
220    use crate::profile::credentials::exec::ProviderChain;
221    use crate::profile::credentials::repr::{BaseProvider, ProfileChain};
222    use crate::provider_config::ProviderConfig;
223    use crate::test_case::no_traffic_client;
224
225    use aws_credential_types::Credentials;
226    use std::collections::HashMap;
227    use std::sync::Arc;
228
229    #[test]
230    fn providers_case_insensitive() {
231        let mut base = HashMap::new();
232        base.insert(
233            "Environment".into(),
234            Arc::new(Credentials::for_tests()) as _,
235        );
236        let provider = NamedProviderFactory::new(base);
237        assert!(provider.provider("environment").is_some());
238        assert!(provider.provider("envIROnment").is_some());
239        assert!(provider.provider(" envIROnment").is_none());
240        assert!(provider.provider("Environment").is_some());
241    }
242
243    #[test]
244    fn error_on_unknown_provider() {
245        let factory = NamedProviderFactory::new(HashMap::new());
246        let chain = ProviderChain::from_repr(
247            &ProviderConfig::empty().with_http_client(no_traffic_client()),
248            ProfileChain {
249                base: BaseProvider::NamedSource("floozle"),
250                chain: vec![],
251            },
252            &factory,
253        );
254        let err = chain.expect_err("no source by that name");
255        assert!(
256            format!("{}", err).contains(
257                "profile referenced `floozle` provider but that provider is not supported"
258            ),
259            "`{}` did not match expected error",
260            err
261        );
262    }
263}