aws_config/sso/
credentials.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! SSO Credentials Provider
7//!
8//! This credentials provider enables loading credentials from `~/.aws/sso/cache`. For more information,
9//! see [Using AWS SSO Credentials](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/sso-credentials.html)
10//!
11//! This provider is included automatically when profiles are loaded.
12
13use super::cache::load_cached_token;
14use crate::identity::IdentityCache;
15use crate::provider_config::ProviderConfig;
16use crate::sso::SsoTokenProvider;
17use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
18use aws_credential_types::Credentials;
19use aws_sdk_sso::types::RoleCredentials;
20use aws_sdk_sso::Client as SsoClient;
21use aws_smithy_async::time::SharedTimeSource;
22use aws_smithy_types::DateTime;
23use aws_types::os_shim_internal::{Env, Fs};
24use aws_types::region::Region;
25use aws_types::SdkConfig;
26use std::convert::TryInto;
27
28/// SSO Credentials Provider
29///
30/// _Note: This provider is part of the default credentials chain and is integrated with the profile-file provider._
31///
32/// This credentials provider will use cached SSO tokens stored in `~/.aws/sso/cache/<hash>.json`.
33/// Two different values will be tried for `<hash>` in order:
34/// 1. The configured [`session_name`](Builder::session_name).
35/// 2. The configured [`start_url`](Builder::start_url).
36#[derive(Debug)]
37pub struct SsoCredentialsProvider {
38    fs: Fs,
39    env: Env,
40    sso_provider_config: SsoProviderConfig,
41    sdk_config: SdkConfig,
42    token_provider: Option<SsoTokenProvider>,
43    time_source: SharedTimeSource,
44}
45
46impl SsoCredentialsProvider {
47    /// Creates a builder for [`SsoCredentialsProvider`]
48    pub fn builder() -> Builder {
49        Builder::new()
50    }
51
52    pub(crate) fn new(
53        provider_config: &ProviderConfig,
54        sso_provider_config: SsoProviderConfig,
55    ) -> Self {
56        let fs = provider_config.fs();
57        let env = provider_config.env();
58
59        let token_provider = if let Some(session_name) = &sso_provider_config.session_name {
60            Some(
61                SsoTokenProvider::builder()
62                    .configure(&provider_config.client_config())
63                    .start_url(&sso_provider_config.start_url)
64                    .session_name(session_name)
65                    .region(sso_provider_config.region.clone())
66                    .build_with(env.clone(), fs.clone()),
67            )
68        } else {
69            None
70        };
71
72        SsoCredentialsProvider {
73            fs,
74            env,
75            sso_provider_config,
76            sdk_config: provider_config.client_config(),
77            token_provider,
78            time_source: provider_config.time_source(),
79        }
80    }
81
82    async fn credentials(&self) -> provider::Result {
83        load_sso_credentials(
84            &self.sso_provider_config,
85            &self.sdk_config,
86            self.token_provider.as_ref(),
87            &self.env,
88            &self.fs,
89            self.time_source.clone(),
90        )
91        .await
92    }
93}
94
95impl ProvideCredentials for SsoCredentialsProvider {
96    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
97    where
98        Self: 'a,
99    {
100        future::ProvideCredentials::new(self.credentials())
101    }
102}
103
104/// Builder for [`SsoCredentialsProvider`]
105#[derive(Default, Debug, Clone)]
106pub struct Builder {
107    provider_config: Option<ProviderConfig>,
108    account_id: Option<String>,
109    region: Option<Region>,
110    role_name: Option<String>,
111    start_url: Option<String>,
112    session_name: Option<String>,
113}
114
115impl Builder {
116    /// Create a new builder for [`SsoCredentialsProvider`]
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    /// Override the configuration used for this provider
122    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
123        self.provider_config = Some(provider_config.clone());
124        self
125    }
126
127    /// Set the account id used for SSO
128    ///
129    /// This is a required field.
130    pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
131        self.account_id = Some(account_id.into());
132        self
133    }
134
135    /// Set the account id used for SSO
136    ///
137    /// This is a required field.
138    pub fn set_account_id(&mut self, account_id: Option<String>) -> &mut Self {
139        self.account_id = account_id;
140        self
141    }
142
143    /// Set the region used for SSO
144    ///
145    /// This is a required field.
146    pub fn region(mut self, region: Region) -> Self {
147        self.region = Some(region);
148        self
149    }
150
151    /// Set the region used for SSO
152    ///
153    /// This is a required field.
154    pub fn set_region(&mut self, region: Option<Region>) -> &mut Self {
155        self.region = region;
156        self
157    }
158
159    /// Set the role name used for SSO
160    ///
161    /// This is a required field.
162    pub fn role_name(mut self, role_name: impl Into<String>) -> Self {
163        self.role_name = Some(role_name.into());
164        self
165    }
166
167    /// Set the role name used for SSO
168    ///
169    /// This is a required field.
170    pub fn set_role_name(&mut self, role_name: Option<String>) -> &mut Self {
171        self.role_name = role_name;
172        self
173    }
174
175    /// Set the start URL used for SSO
176    ///
177    /// This is a required field.
178    pub fn start_url(mut self, start_url: impl Into<String>) -> Self {
179        self.start_url = Some(start_url.into());
180        self
181    }
182
183    /// Set the start URL used for SSO
184    ///
185    /// This is a required field.
186    pub fn set_start_url(&mut self, start_url: Option<String>) -> &mut Self {
187        self.start_url = start_url;
188        self
189    }
190
191    /// Set the session name used for SSO
192    pub fn session_name(mut self, session_name: impl Into<String>) -> Self {
193        self.session_name = Some(session_name.into());
194        self
195    }
196
197    /// Set the session name used for SSO
198    pub fn set_session_name(&mut self, session_name: Option<String>) -> &mut Self {
199        self.session_name = session_name;
200        self
201    }
202
203    /// Construct an SsoCredentialsProvider from the builder
204    ///
205    /// # Panics
206    /// This method will panic if the any of the following required fields are unset:
207    /// - [`start_url`](Self::start_url)
208    /// - [`role_name`](Self::role_name)
209    /// - [`account_id`](Self::account_id)
210    /// - [`region`](Self::region)
211    pub fn build(self) -> SsoCredentialsProvider {
212        let provider_config = self.provider_config.unwrap_or_default();
213        let sso_config = SsoProviderConfig {
214            account_id: self.account_id.expect("account_id must be set"),
215            region: self.region.expect("region must be set"),
216            role_name: self.role_name.expect("role_name must be set"),
217            start_url: self.start_url.expect("start_url must be set"),
218            session_name: self.session_name,
219        };
220        SsoCredentialsProvider::new(&provider_config, sso_config)
221    }
222}
223
224#[derive(Debug)]
225pub(crate) struct SsoProviderConfig {
226    pub(crate) account_id: String,
227    pub(crate) role_name: String,
228    pub(crate) start_url: String,
229    pub(crate) region: Region,
230    pub(crate) session_name: Option<String>,
231}
232
233async fn load_sso_credentials(
234    sso_provider_config: &SsoProviderConfig,
235    sdk_config: &SdkConfig,
236    token_provider: Option<&SsoTokenProvider>,
237    env: &Env,
238    fs: &Fs,
239    time_source: SharedTimeSource,
240) -> provider::Result {
241    let token = if let Some(token_provider) = token_provider {
242        token_provider
243            .resolve_token(time_source)
244            .await
245            .map_err(CredentialsError::provider_error)?
246    } else {
247        // Backwards compatible token loading that uses `start_url` instead of `session_name`
248        load_cached_token(env, fs, &sso_provider_config.start_url)
249            .await
250            .map_err(CredentialsError::provider_error)?
251    };
252
253    let config = sdk_config
254        .to_builder()
255        .region(sso_provider_config.region.clone())
256        .identity_cache(IdentityCache::no_cache())
257        .build();
258    // TODO(enableNewSmithyRuntimeCleanup): Use `customize().config_override()` to set the region instead of creating a new client once middleware is removed
259    let client = SsoClient::new(&config);
260    let resp = client
261        .get_role_credentials()
262        .role_name(&sso_provider_config.role_name)
263        .access_token(&*token.access_token)
264        .account_id(&sso_provider_config.account_id)
265        .send()
266        .await
267        .map_err(CredentialsError::provider_error)?;
268    let credentials: RoleCredentials = resp
269        .role_credentials
270        .ok_or_else(|| CredentialsError::unhandled("SSO did not return credentials"))?;
271    let akid = credentials
272        .access_key_id
273        .ok_or_else(|| CredentialsError::unhandled("no access key id in response"))?;
274    let secret_key = credentials
275        .secret_access_key
276        .ok_or_else(|| CredentialsError::unhandled("no secret key in response"))?;
277    let expiration = DateTime::from_millis(credentials.expiration)
278        .try_into()
279        .map_err(|err| {
280            CredentialsError::unhandled(format!(
281                "expiration could not be converted into a system time: {}",
282                err
283            ))
284        })?;
285    Ok(Credentials::new(
286        akid,
287        secret_key,
288        credentials.session_token,
289        Some(expiration),
290        "SSO",
291    ))
292}