use super::cache::load_cached_token;
use crate::identity::IdentityCache;
use crate::provider_config::ProviderConfig;
use crate::sso::SsoTokenProvider;
use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
use aws_credential_types::Credentials;
use aws_sdk_sso::types::RoleCredentials;
use aws_sdk_sso::Client as SsoClient;
use aws_smithy_async::time::SharedTimeSource;
use aws_smithy_types::DateTime;
use aws_types::os_shim_internal::{Env, Fs};
use aws_types::region::Region;
use aws_types::SdkConfig;
#[derive(Debug)]
pub struct SsoCredentialsProvider {
fs: Fs,
env: Env,
sso_provider_config: SsoProviderConfig,
sdk_config: SdkConfig,
token_provider: Option<SsoTokenProvider>,
time_source: SharedTimeSource,
}
impl SsoCredentialsProvider {
pub fn builder() -> Builder {
Builder::new()
}
pub(crate) fn new(
provider_config: &ProviderConfig,
sso_provider_config: SsoProviderConfig,
) -> Self {
let fs = provider_config.fs();
let env = provider_config.env();
let token_provider = if let Some(session_name) = &sso_provider_config.session_name {
Some(
SsoTokenProvider::builder()
.configure(&provider_config.client_config())
.start_url(&sso_provider_config.start_url)
.session_name(session_name)
.region(sso_provider_config.region.clone())
.build_with(env.clone(), fs.clone()),
)
} else {
None
};
SsoCredentialsProvider {
fs,
env,
sso_provider_config,
sdk_config: provider_config.client_config(),
token_provider,
time_source: provider_config.time_source(),
}
}
async fn credentials(&self) -> provider::Result {
load_sso_credentials(
&self.sso_provider_config,
&self.sdk_config,
self.token_provider.as_ref(),
&self.env,
&self.fs,
self.time_source.clone(),
)
.await
}
}
impl ProvideCredentials for SsoCredentialsProvider {
fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
where
Self: 'a,
{
future::ProvideCredentials::new(self.credentials())
}
}
#[derive(Default, Debug, Clone)]
pub struct Builder {
provider_config: Option<ProviderConfig>,
account_id: Option<String>,
region: Option<Region>,
role_name: Option<String>,
start_url: Option<String>,
session_name: Option<String>,
}
impl Builder {
pub fn new() -> Self {
Self::default()
}
pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
self.provider_config = Some(provider_config.clone());
self
}
pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
self.account_id = Some(account_id.into());
self
}
pub fn set_account_id(&mut self, account_id: Option<String>) -> &mut Self {
self.account_id = account_id;
self
}
pub fn region(mut self, region: Region) -> Self {
self.region = Some(region);
self
}
pub fn set_region(&mut self, region: Option<Region>) -> &mut Self {
self.region = region;
self
}
pub fn role_name(mut self, role_name: impl Into<String>) -> Self {
self.role_name = Some(role_name.into());
self
}
pub fn set_role_name(&mut self, role_name: Option<String>) -> &mut Self {
self.role_name = role_name;
self
}
pub fn start_url(mut self, start_url: impl Into<String>) -> Self {
self.start_url = Some(start_url.into());
self
}
pub fn set_start_url(&mut self, start_url: Option<String>) -> &mut Self {
self.start_url = start_url;
self
}
pub fn session_name(mut self, session_name: impl Into<String>) -> Self {
self.session_name = Some(session_name.into());
self
}
pub fn set_session_name(&mut self, session_name: Option<String>) -> &mut Self {
self.session_name = session_name;
self
}
pub fn build(self) -> SsoCredentialsProvider {
let provider_config = self.provider_config.unwrap_or_default();
let sso_config = SsoProviderConfig {
account_id: self.account_id.expect("account_id must be set"),
region: self.region.expect("region must be set"),
role_name: self.role_name.expect("role_name must be set"),
start_url: self.start_url.expect("start_url must be set"),
session_name: self.session_name,
};
SsoCredentialsProvider::new(&provider_config, sso_config)
}
}
#[derive(Debug)]
pub(crate) struct SsoProviderConfig {
pub(crate) account_id: String,
pub(crate) role_name: String,
pub(crate) start_url: String,
pub(crate) region: Region,
pub(crate) session_name: Option<String>,
}
async fn load_sso_credentials(
sso_provider_config: &SsoProviderConfig,
sdk_config: &SdkConfig,
token_provider: Option<&SsoTokenProvider>,
env: &Env,
fs: &Fs,
time_source: SharedTimeSource,
) -> provider::Result {
let token = if let Some(token_provider) = token_provider {
token_provider
.resolve_token(time_source)
.await
.map_err(CredentialsError::provider_error)?
} else {
load_cached_token(env, fs, &sso_provider_config.start_url)
.await
.map_err(CredentialsError::provider_error)?
};
let config = sdk_config
.to_builder()
.region(sso_provider_config.region.clone())
.identity_cache(IdentityCache::no_cache())
.build();
let client = SsoClient::new(&config);
let resp = client
.get_role_credentials()
.role_name(&sso_provider_config.role_name)
.access_token(&*token.access_token)
.account_id(&sso_provider_config.account_id)
.send()
.await
.map_err(CredentialsError::provider_error)?;
let credentials: RoleCredentials = resp
.role_credentials
.ok_or_else(|| CredentialsError::unhandled("SSO did not return credentials"))?;
let akid = credentials
.access_key_id
.ok_or_else(|| CredentialsError::unhandled("no access key id in response"))?;
let secret_key = credentials
.secret_access_key
.ok_or_else(|| CredentialsError::unhandled("no secret key in response"))?;
let expiration = DateTime::from_millis(credentials.expiration)
.try_into()
.map_err(|err| {
CredentialsError::unhandled(format!(
"expiration could not be converted into a system time: {}",
err
))
})?;
Ok(Credentials::new(
akid,
secret_key,
credentials.session_token,
Some(expiration),
"SSO",
))
}