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_output = 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        sts::util::into_credentials(
54            assume_role_output.credentials,
55            assume_role_output.assumed_role_user,
56            "AssumeRoleProvider",
57        )
58    }
59}
60
61#[derive(Debug)]
62pub(super) struct ProviderChain {
63    base: Arc<dyn ProvideCredentials>,
64    chain: Vec<AssumeRoleProvider>,
65}
66
67impl ProviderChain {
68    pub(crate) fn base(&self) -> &dyn ProvideCredentials {
69        self.base.as_ref()
70    }
71
72    pub(crate) fn chain(&self) -> &[AssumeRoleProvider] {
73        self.chain.as_slice()
74    }
75}
76
77impl ProviderChain {
78    pub(super) fn from_repr(
79        provider_config: &ProviderConfig,
80        repr: repr::ProfileChain<'_>,
81        factory: &named::NamedProviderFactory,
82    ) -> Result<Self, ProfileFileError> {
83        let base = match repr.base() {
84            BaseProvider::NamedSource(name) => {
85                factory
86                    .provider(name)
87                    .ok_or(ProfileFileError::UnknownProvider {
88                        name: name.to_string(),
89                    })?
90            }
91            BaseProvider::AccessKey(key) => Arc::new(key.clone()),
92            BaseProvider::CredentialProcess {
93                command_with_sensitive_args,
94                account_id,
95            } => {
96                #[cfg(feature = "credentials-process")]
97                {
98                    Arc::new({
99                        let mut builder = CredentialProcessProvider::builder()
100                            .command(command_with_sensitive_args.to_owned_string());
101                        builder.set_account_id(
102                            account_id.map(aws_credential_types::attributes::AccountId::from),
103                        );
104                        builder.build()
105                    })
106                }
107                #[cfg(not(feature = "credentials-process"))]
108                {
109                    let _ = (command_with_sensitive_args, account_id);
110                    Err(ProfileFileError::FeatureNotEnabled {
111                        feature: "credentials-process".into(),
112                        message: Some(
113                            "In order to spawn a subprocess, the `credentials-process` feature must be enabled."
114                                .into(),
115                        ),
116                    })?
117                }
118            }
119            BaseProvider::WebIdentityTokenRole {
120                role_arn,
121                web_identity_token_file,
122                session_name,
123            } => {
124                let provider = WebIdentityTokenCredentialsProvider::builder()
125                    .static_configuration(StaticConfiguration {
126                        web_identity_token_file: web_identity_token_file.into(),
127                        role_arn: role_arn.to_string(),
128                        session_name: session_name.map(|sess| sess.to_string()).unwrap_or_else(
129                            || {
130                                sts::util::default_session_name(
131                                    "web-identity-token-profile",
132                                    provider_config.time_source().now(),
133                                )
134                            },
135                        ),
136                    })
137                    .configure(provider_config)
138                    .build();
139                Arc::new(provider)
140            }
141            #[allow(unused_variables)]
142            BaseProvider::Sso {
143                sso_account_id,
144                sso_region,
145                sso_role_name,
146                sso_start_url,
147                sso_session_name,
148            } => {
149                #[cfg(feature = "sso")]
150                {
151                    use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider};
152                    use aws_types::region::Region;
153
154                    let (Some(sso_account_id), Some(sso_role_name)) =
155                        (sso_account_id, sso_role_name)
156                    else {
157                        return Err(ProfileFileError::TokenProviderConfig {});
158                    };
159                    let sso_config = SsoProviderConfig {
160                        account_id: sso_account_id.to_string(),
161                        role_name: sso_role_name.to_string(),
162                        start_url: sso_start_url.to_string(),
163                        region: Region::new(sso_region.to_string()),
164                        session_name: sso_session_name.map(|s| s.to_string()),
165                    };
166                    Arc::new(SsoCredentialsProvider::new(provider_config, sso_config))
167                }
168                #[cfg(not(feature = "sso"))]
169                {
170                    Err(ProfileFileError::FeatureNotEnabled {
171                        feature: "sso".into(),
172                        message: None,
173                    })?
174                }
175            }
176        };
177        tracing::debug!(base = ?repr.base(), "first credentials will be loaded from {:?}", repr.base());
178        let chain = repr
179            .chain()
180            .iter()
181            .map(|role_arn| {
182                tracing::debug!(role_arn = ?role_arn, "which will be used to assume a role");
183                AssumeRoleProvider {
184                    role_arn: role_arn.role_arn.into(),
185                    external_id: role_arn.external_id.map(Into::into),
186                    session_name: role_arn.session_name.map(Into::into),
187                    time_source: provider_config.time_source(),
188                }
189            })
190            .collect();
191        Ok(ProviderChain { base, chain })
192    }
193}
194
195pub(super) mod named {
196    use std::collections::HashMap;
197    use std::sync::Arc;
198
199    use aws_credential_types::provider::ProvideCredentials;
200    use std::borrow::Cow;
201
202    #[derive(Debug)]
203    pub(crate) struct NamedProviderFactory {
204        providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
205    }
206
207    fn lower_cow(mut input: Cow<'_, str>) -> Cow<'_, str> {
208        if !input.chars().all(|c| c.is_ascii_lowercase()) {
209            input.to_mut().make_ascii_lowercase();
210        }
211        input
212    }
213
214    impl NamedProviderFactory {
215        pub(crate) fn new(
216            providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
217        ) -> Self {
218            let providers = providers
219                .into_iter()
220                .map(|(k, v)| (lower_cow(k), v))
221                .collect();
222            Self { providers }
223        }
224
225        pub(crate) fn provider(&self, name: &str) -> Option<Arc<dyn ProvideCredentials>> {
226            self.providers.get(&lower_cow(Cow::Borrowed(name))).cloned()
227        }
228    }
229}
230
231#[cfg(test)]
232mod test {
233    use crate::profile::credentials::exec::named::NamedProviderFactory;
234    use crate::profile::credentials::exec::ProviderChain;
235    use crate::profile::credentials::repr::{BaseProvider, ProfileChain};
236    use crate::provider_config::ProviderConfig;
237    use crate::test_case::no_traffic_client;
238
239    use aws_credential_types::Credentials;
240    use std::collections::HashMap;
241    use std::sync::Arc;
242
243    #[test]
244    fn providers_case_insensitive() {
245        let mut base = HashMap::new();
246        base.insert(
247            "Environment".into(),
248            Arc::new(Credentials::for_tests()) as _,
249        );
250        let provider = NamedProviderFactory::new(base);
251        assert!(provider.provider("environment").is_some());
252        assert!(provider.provider("envIROnment").is_some());
253        assert!(provider.provider(" envIROnment").is_none());
254        assert!(provider.provider("Environment").is_some());
255    }
256
257    #[test]
258    fn error_on_unknown_provider() {
259        let factory = NamedProviderFactory::new(HashMap::new());
260        let chain = ProviderChain::from_repr(
261            &ProviderConfig::empty().with_http_client(no_traffic_client()),
262            ProfileChain {
263                base: BaseProvider::NamedSource("floozle"),
264                chain: vec![],
265            },
266            &factory,
267        );
268        let err = chain.expect_err("no source by that name");
269        assert!(
270            format!("{}", err).contains(
271                "profile referenced `floozle` provider but that provider is not supported"
272            ),
273            "`{}` did not match expected error",
274            err
275        );
276    }
277}