use anyhow::{Context, Result};
use holys3_sigv4::{encode_query_component, read_sso_token, Credentials, SsoProfile};
use std::time::Duration;
use time::OffsetDateTime;
pub(crate) fn role_credentials(profile: &SsoProfile) -> Result<(Credentials, OffsetDateTime)> {
let token = read_sso_token(profile)?;
let url = format!(
"https://portal.sso.{}.amazonaws.com/federation/credentials?account_id={}&role_name={}",
profile.sso_region,
encode_query_component(&profile.account_id),
encode_query_component(&profile.role_name),
);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(3))
.timeout(Duration::from_secs(30))
.build()?;
let body: serde_json::Value = rt.block_on(async {
let response = client
.get(url)
.header("x-amz-sso_bearer_token", &token)
.send()
.await
.context("SSO portal request failed")?;
anyhow::ensure!(
response.status().is_success(),
"SSO GetRoleCredentials returned HTTP {} for profile `{}`; try `aws sso login --profile {}`",
response.status(),
profile.profile,
profile.profile
);
let bytes = response.bytes().await?;
Ok(serde_json::from_slice(&bytes)?)
})?;
let role = &body["roleCredentials"];
let field = |name: &str| -> Result<String> {
role[name]
.as_str()
.map(str::to_owned)
.with_context(|| format!("SSO roleCredentials missing {name}"))
};
let expiration_ms = role["expiration"]
.as_i64()
.context("SSO roleCredentials missing expiration")?;
let expires_at = OffsetDateTime::from_unix_timestamp(expiration_ms / 1000)
.context("SSO expiration out of range")?;
Ok((
Credentials {
access_key: field("accessKeyId")?,
secret_key: field("secretAccessKey")?,
session_token: Some(field("sessionToken")?),
},
expires_at,
))
}