From fe766824b54d05a4923fb6a615f3c0dad10d7519 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Fri, 11 Aug 2023 16:50:08 -0700 Subject: [PATCH 01/12] Implement `SsoTokenProvider` --- aws/rust-runtime/aws-config/Cargo.toml | 13 +- aws/rust-runtime/aws-config/src/lib.rs | 2 +- .../src/profile/credentials/exec.rs | 2 +- aws/rust-runtime/aws-config/src/sso.rs | 454 +---- aws/rust-runtime/aws-config/src/sso/cache.rs | 594 ++++++ .../aws-config/src/sso/credentials.rs | 311 ++++ aws/rust-runtime/aws-config/src/sso/token.rs | 861 +++++++++ aws/rust-runtime/aws-runtime/src/identity.rs | 7 +- aws/rust-runtime/aws-types/Cargo.toml | 3 +- .../aws-types/src/os_shim_internal.rs | 74 +- aws/sdk/aws-models/sso-oidc.json | 1590 +++++++++++++++++ .../src/client/identity.rs | 15 +- .../src/client/identity/http.rs | 13 +- .../src/client/runtime_components.rs | 2 +- .../src/client/auth/http.rs | 26 + .../src/client/identity/no_auth.rs | 7 +- .../src/client/orchestrator/auth.rs | 12 +- 17 files changed, 3509 insertions(+), 477 deletions(-) create mode 100644 aws/rust-runtime/aws-config/src/sso/cache.rs create mode 100644 aws/rust-runtime/aws-config/src/sso/credentials.rs create mode 100644 aws/rust-runtime/aws-config/src/sso/token.rs create mode 100644 aws/sdk/aws-models/sso-oidc.json diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index 60fb0f5261..41dd43b472 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -14,19 +14,21 @@ rustls = ["aws-smithy-client/rustls", "client-hyper"] native-tls = [] allow-compilation = [] # our tests use `cargo test --all-features` and native-tls breaks CI rt-tokio = ["aws-smithy-async/rt-tokio", "tokio/rt"] -credentials-sso = ["dep:aws-sdk-sso", "dep:ring", "dep:hex", "dep:zeroize"] +sts = ["dep:aws-sdk-sts"] +sso = ["dep:aws-sdk-sso", "dep:aws-sdk-ssooidc", "dep:ring", "dep:hex", "dep:zeroize", "aws-smithy-runtime-api/http-auth"] -default = ["client-hyper", "rustls", "rt-tokio", "credentials-sso"] +default = ["client-hyper", "rustls", "rt-tokio", "sts"] [dependencies] aws-credential-types = { path = "../../sdk/build/aws-sdk/sdk/aws-credential-types" } aws-http = { path = "../../sdk/build/aws-sdk/sdk/aws-http" } -aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sdk/sts", default-features = false } +aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sdk/sts", default-features = false, optional = true } aws-smithy-async = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-async" } aws-smithy-client = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-client", default-features = false } aws-smithy-http = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-http" } aws-smithy-http-tower = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-http-tower" } aws-smithy-json = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-json" } +aws-smithy-runtime-api = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-runtime-api" } aws-smithy-types = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-types" } aws-types = { path = "../../sdk/build/aws-sdk/sdk/aws-types" } hyper = { version = "0.14.26", default-features = false } @@ -47,6 +49,9 @@ ring = { version = "0.16", optional = true } hex = { version = "0.4.3", optional = true } zeroize = { version = "1", optional = true } +# implementation detail of SSO OIDC `CreateToken` for SSO token providers +aws-sdk-ssooidc = { path = "../../sdk/build/aws-sdk/sdk/ssooidc", default-features = false, optional = true } + [dev-dependencies] futures-util = { version = "0.3.16", default-features = false } tracing-test = "0.2.1" @@ -63,6 +68,8 @@ serde_json = "1" aws-credential-types = { path = "../../sdk/build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } aws-smithy-client = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-client", features = ["test-util", "rt-tokio", "client-hyper"] } +aws-smithy-runtime-api = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-runtime-api", features = ["test-util"] } +aws-smithy-runtime = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-runtime", features = ["test-util"] } # used for a usage example hyper-rustls = { version = "0.24", features = ["webpki-tokio", "http2", "http1"] } diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index 1deffdbe16..113677a457 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -122,7 +122,7 @@ pub mod meta; pub mod profile; pub mod provider_config; pub mod retry; -#[cfg(feature = "credentials-sso")] +#[cfg(feature = "sso")] pub mod sso; pub(crate) mod standard_property; pub mod sts; diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs index 838007ad1e..99e2c98d49 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs @@ -8,7 +8,7 @@ use crate::credential_process::CredentialProcessProvider; use crate::profile::credentials::ProfileFileError; use crate::provider_config::ProviderConfig; #[cfg(feature = "credentials-sso")] -use crate::sso::{SsoCredentialsProvider, SsoProviderConfig}; +use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider}; use crate::sts; use crate::web_identity_token::{StaticConfiguration, WebIdentityTokenCredentialsProvider}; use aws_credential_types::provider::{self, error::CredentialsError, ProvideCredentials}; diff --git a/aws/rust-runtime/aws-config/src/sso.rs b/aws/rust-runtime/aws-config/src/sso.rs index 592ed5b6bc..69ed37ed0f 100644 --- a/aws/rust-runtime/aws-config/src/sso.rs +++ b/aws/rust-runtime/aws-config/src/sso.rs @@ -3,456 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! SSO Credentials Provider -//! -//! This credentials provider enables loading credentials from `~/.aws/sso/cache`. For more information, -//! see [Using AWS SSO Credentials](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/sso-credentials.html) -//! -//! This provider is included automatically when profiles are loaded. +//! SSO Credentials and Token providers -use crate::fs_util::{home_dir, Os}; -use crate::json_credentials::{json_parse_loop, InvalidJsonCredentials}; -use crate::provider_config::ProviderConfig; +pub mod credentials; -use aws_credential_types::cache::CredentialsCache; -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::{config::Builder as SsoConfigBuilder, Client as SsoClient, Config as SsoConfig}; -use aws_smithy_json::deserialize::Token; -use aws_smithy_types::date_time::Format; -use aws_smithy_types::DateTime; -use aws_types::os_shim_internal::{Env, Fs}; -use aws_types::region::Region; +pub use credentials::SsoCredentialsProvider; -use std::convert::TryInto; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::io; -use std::path::PathBuf; +pub mod token; -use crate::connector::expect_connector; -use aws_smithy_types::retry::RetryConfig; -use ring::digest; -use zeroize::Zeroizing; +pub use token::SsoTokenProvider; -/// SSO Credentials Provider -/// -/// _Note: This provider is part of the default credentials chain and is integrated with the profile-file provider._ -/// -/// This credentials provider will use cached SSO tokens stored in `~/.aws/sso/cache/.json`. -/// `` is computed based on the configured [`start_url`](Builder::start_url). -#[derive(Debug)] -pub struct SsoCredentialsProvider { - fs: Fs, - env: Env, - sso_provider_config: SsoProviderConfig, - sso_config: SsoConfigBuilder, -} - -impl SsoCredentialsProvider { - /// Creates a builder for [`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 mut sso_config = SsoConfig::builder() - .http_connector(expect_connector( - "The SSO credentials provider", - provider_config.connector(&Default::default()), - )) - .retry_config(RetryConfig::standard()); - sso_config.set_sleep_impl(provider_config.sleep()); - - SsoCredentialsProvider { - fs, - env, - sso_provider_config, - sso_config, - } - } - - async fn credentials(&self) -> provider::Result { - load_sso_credentials( - &self.sso_provider_config, - &self.sso_config, - &self.env, - &self.fs, - ) - .await - } -} - -impl ProvideCredentials for SsoCredentialsProvider { - fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> - where - Self: 'a, - { - future::ProvideCredentials::new(self.credentials()) - } -} - -/// Builder for [`SsoCredentialsProvider`] -#[derive(Default, Debug, Clone)] -pub struct Builder { - provider_config: Option, - account_id: Option, - role_name: Option, - start_url: Option, - region: Option, -} - -impl Builder { - /// Create a new builder for [`SsoCredentialsProvider`] - pub fn new() -> Self { - Self::default() - } - - /// Override the configuration used for this provider - pub fn configure(mut self, provider_config: &ProviderConfig) -> Self { - self.provider_config = Some(provider_config.clone()); - self - } - - /// Set the account id used for SSO - pub fn account_id(mut self, account_id: impl Into) -> Self { - self.account_id = Some(account_id.into()); - self - } - - /// Set the role name used for SSO - pub fn role_name(mut self, role_name: impl Into) -> Self { - self.role_name = Some(role_name.into()); - self - } - - /// Set the start URL used for SSO - pub fn start_url(mut self, start_url: impl Into) -> Self { - self.start_url = Some(start_url.into()); - self - } - - /// Set the region used for SSO - pub fn region(mut self, region: Region) -> Self { - self.region = Some(region); - self - } - - /// Construct an SsoCredentialsProvider from the builder - /// - /// # Panics - /// This method will panic if the any of the following required fields are unset: - /// - [`start_url`](Self::start_url) - /// - [`role_name`](Self::role_name) - /// - [`account_id`](Self::account_id) - /// - [`region`](Self::region) - 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"), - role_name: self.role_name.expect("role_name must be set"), - start_url: self.start_url.expect("start_url must be set"), - region: self.region.expect("region must be set"), - }; - SsoCredentialsProvider::new(&provider_config, sso_config) - } -} - -#[derive(Debug)] -pub(crate) enum LoadTokenError { - InvalidCredentials(InvalidJsonCredentials), - NoHomeDirectory, - IoError { err: io::Error, path: PathBuf }, -} - -impl Display for LoadTokenError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - LoadTokenError::InvalidCredentials(err) => { - write!(f, "SSO Token was invalid (expected JSON): {}", err) - } - LoadTokenError::NoHomeDirectory => write!(f, "Could not resolve a home directory"), - LoadTokenError::IoError { err, path } => { - write!(f, "failed to read `{}`: {}", path.display(), err) - } - } - } -} - -impl Error for LoadTokenError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - LoadTokenError::InvalidCredentials(err) => Some(err as _), - LoadTokenError::NoHomeDirectory => None, - LoadTokenError::IoError { err, .. } => Some(err as _), - } - } -} - -#[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, -} - -async fn load_sso_credentials( - sso_provider_config: &SsoProviderConfig, - sso_config: &SsoConfigBuilder, - env: &Env, - fs: &Fs, -) -> provider::Result { - let token = load_token(&sso_provider_config.start_url, env, fs) - .await - .map_err(CredentialsError::provider_error)?; - let config = sso_config - .clone() - .region(sso_provider_config.region.clone()) - .credentials_cache(CredentialsCache::no_caching()) - .build(); - // TODO(enableNewSmithyRuntimeCleanup): Use `customize().config_override()` to set the region instead of creating a new client once middleware is removed - let client = SsoClient::from_conf(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", - )) -} - -/// Load the token for `start_url` from `~/.aws/sso/cache/.json` -async fn load_token(start_url: &str, env: &Env, fs: &Fs) -> Result { - let home = home_dir(env, Os::real()).ok_or(LoadTokenError::NoHomeDirectory)?; - let path = sso_token_path(start_url, &home); - let data = - Zeroizing::new( - fs.read_to_end(&path) - .await - .map_err(|err| LoadTokenError::IoError { - err, - path: path.to_path_buf(), - })?, - ); - let token = parse_token_json(&data).map_err(LoadTokenError::InvalidCredentials)?; - Ok(token) -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct SsoToken { - access_token: Zeroizing, - expires_at: DateTime, - region: Option, -} - -/// Parse SSO token JSON from input -fn parse_token_json(input: &[u8]) -> Result { - /* - Example: - { - "accessToken": "base64string", - "expiresAt": "2019-11-14T04:05:45Z", - "region": "us-west-2", - "startUrl": "https://d-abc123.awsapps.com/start" - }*/ - let mut acccess_token = None; - let mut expires_at = None; - let mut region = None; - let mut start_url = None; - json_parse_loop(input, |key, value| { - match (key, value) { - (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("accessToken") => { - acccess_token = Some(value.to_unescaped()?.to_string()) - } - (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("expiresAt") => { - expires_at = Some(value.to_unescaped()?) - } - (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("region") => { - region = Some(value.to_unescaped()?.to_string()) - } - (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("startUrl") => { - start_url = Some(value.to_unescaped()?.to_string()) - } - _other => {} // ignored - }; - Ok(()) - })?; - let access_token = - Zeroizing::new(acccess_token.ok_or(InvalidJsonCredentials::MissingField("accessToken"))?); - let expires_at = expires_at.ok_or(InvalidJsonCredentials::MissingField("expiresAt"))?; - let expires_at = DateTime::from_str(expires_at.as_ref(), Format::DateTime).map_err(|e| { - InvalidJsonCredentials::InvalidField { - field: "expiresAt", - err: e.into(), - } - })?; - let region = region.map(Region::new); - Ok(SsoToken { - access_token, - expires_at, - region, - }) -} - -/// Determine the SSO token path for a given start_url -fn sso_token_path(start_url: &str, home: &str) -> PathBuf { - // hex::encode returns a lowercase string - let mut out = PathBuf::with_capacity(home.len() + "/.aws/sso/cache".len() + ".json".len() + 40); - out.push(home); - out.push(".aws/sso/cache"); - out.push(&hex::encode(digest::digest( - &digest::SHA1_FOR_LEGACY_USE_ONLY, - start_url.as_bytes(), - ))); - out.set_extension("json"); - out -} - -#[cfg(test)] -mod test { - use crate::json_credentials::InvalidJsonCredentials; - use crate::sso::{load_token, parse_token_json, sso_token_path, LoadTokenError, SsoToken}; - use aws_smithy_types::DateTime; - use aws_types::os_shim_internal::{Env, Fs}; - use aws_types::region::Region; - use zeroize::Zeroizing; - - #[test] - fn deserialize_valid_tokens() { - let token = br#" - { - "accessToken": "base64string", - "expiresAt": "2009-02-13T23:31:30Z", - "region": "us-west-2", - "startUrl": "https://d-abc123.awsapps.com/start" - }"#; - assert_eq!( - parse_token_json(token).expect("valid"), - SsoToken { - access_token: Zeroizing::new("base64string".into()), - expires_at: DateTime::from_secs(1234567890), - region: Some(Region::from_static("us-west-2")) - } - ); - - let no_region = br#"{ - "accessToken": "base64string", - "expiresAt": "2009-02-13T23:31:30Z" - }"#; - assert_eq!( - parse_token_json(no_region).expect("valid"), - SsoToken { - access_token: Zeroizing::new("base64string".into()), - expires_at: DateTime::from_secs(1234567890), - region: None - } - ); - } - - #[test] - fn invalid_timestamp() { - let token = br#" - { - "accessToken": "base64string", - "expiresAt": "notatimestamp", - "region": "us-west-2", - "startUrl": "https://d-abc123.awsapps.com/start" - }"#; - let err = parse_token_json(token).expect_err("invalid timestamp"); - assert!( - format!("{}", err).contains("Invalid field in response: `expiresAt`."), - "{}", - err - ); - } - - #[test] - fn missing_fields() { - let token = br#" - { - "expiresAt": "notatimestamp", - "region": "us-west-2", - "startUrl": "https://d-abc123.awsapps.com/start" - }"#; - let err = parse_token_json(token).expect_err("missing akid"); - assert!( - matches!(err, InvalidJsonCredentials::MissingField("accessToken")), - "incorrect error: {:?}", - err - ); - - let token = br#" - { - "accessToken": "akid", - "region": "us-west-2", - "startUrl": "https://d-abc123.awsapps.com/start" - }"#; - let err = parse_token_json(token).expect_err("missing expiry"); - assert!( - matches!(err, InvalidJsonCredentials::MissingField("expiresAt")), - "incorrect error: {:?}", - err - ); - } - - #[test] - fn determine_correct_cache_filenames() { - assert_eq!( - sso_token_path("https://d-92671207e4.awsapps.com/start", "/home/me").as_os_str(), - "/home/me/.aws/sso/cache/13f9d35043871d073ab260e020f0ffde092cb14b.json" - ); - assert_eq!( - sso_token_path("https://d-92671207e4.awsapps.com/start", "/home/me/").as_os_str(), - "/home/me/.aws/sso/cache/13f9d35043871d073ab260e020f0ffde092cb14b.json" - ); - } - - #[tokio::test] - async fn gracefully_handle_missing_files() { - let err = load_token( - "asdf", - &Env::from_slice(&[("HOME", "/home")]), - &Fs::from_slice(&[]), - ) - .await - .expect_err("should fail, file is missing"); - assert!( - matches!(err, LoadTokenError::IoError { .. }), - "should be io error, got {}", - err - ); - } -} +mod cache; diff --git a/aws/rust-runtime/aws-config/src/sso/cache.rs b/aws/rust-runtime/aws-config/src/sso/cache.rs new file mode 100644 index 0000000000..17a3424ae6 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/sso/cache.rs @@ -0,0 +1,594 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::fs_util::{home_dir, Os}; +use aws_smithy_json::deserialize::token::skip_value; +use aws_smithy_json::deserialize::Token; +use aws_smithy_json::deserialize::{json_token_iter, EscapeError}; +use aws_smithy_json::serialize::JsonObjectWriter; +use aws_smithy_types::date_time::{DateTimeFormatError, Format}; +use aws_smithy_types::DateTime; +use aws_types::os_shim_internal::{Env, Fs}; +use ring::digest; +use std::borrow::Cow; +use std::error::Error as StdError; +use std::fmt; +use std::path::PathBuf; +use std::time::SystemTime; +use zeroize::Zeroizing; + +#[cfg_attr(test, derive(Eq, PartialEq))] +#[derive(Clone)] +pub(super) struct CachedSsoToken { + pub(super) access_token: Zeroizing, + pub(super) client_id: Option, + pub(super) client_secret: Option>, + pub(super) expires_at: SystemTime, + pub(super) refresh_token: Option>, + pub(super) region: Option, + pub(super) registration_expires_at: Option, + pub(super) start_url: Option, +} + +impl CachedSsoToken { + /// True if the information required to refresh this token is present. + /// + /// The expiration times are not considered by this function. + pub(super) fn refreshable(&self) -> bool { + self.client_id.is_some() + && self.client_secret.is_some() + && self.refresh_token.is_some() + && self.registration_expires_at.is_some() + } +} + +impl fmt::Debug for CachedSsoToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CachedSsoToken") + .field("access_token", &"** redacted **") + .field("client_id", &self.client_id) + .field("client_secret", &"** redacted **") + .field("expires_at", &self.expires_at) + .field("refresh_token", &"** redacted **") + .field("region", &self.region) + .field("registration_expires_at", &self.registration_expires_at) + .field("start_url", &self.start_url) + .finish() + } +} + +#[derive(Debug)] +pub(super) enum CachedSsoTokenError { + FailedToFormatDateTime { + source: Box, + }, + InvalidField { + field: &'static str, + source: Box, + }, + IoError { + what: &'static str, + path: PathBuf, + source: std::io::Error, + }, + JsonError(Box), + MissingField(&'static str), + NoHomeDirectory, + Other(Cow<'static, str>), +} + +impl fmt::Display for CachedSsoTokenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FailedToFormatDateTime { .. } => write!(f, "failed to format date time"), + Self::InvalidField { field, .. } => write!( + f, + "invalid value for the `{field}` field in the cached SSO token file" + ), + Self::IoError { what, path, .. } => write!(f, "failed to {what} `{}`", path.display()), + Self::JsonError(_) => write!(f, "invalid JSON in cached SSO token file"), + Self::MissingField(field) => { + write!(f, "missing field `{field}` in cached SSO token file") + } + Self::NoHomeDirectory => write!(f, "couldn't resolve a home directory"), + Self::Other(message) => f.write_str(message), + } + } +} + +impl StdError for CachedSsoTokenError { + fn cause(&self) -> Option<&dyn StdError> { + match self { + Self::FailedToFormatDateTime { source } => Some(source.as_ref()), + Self::InvalidField { source, .. } => Some(source.as_ref()), + Self::IoError { source, .. } => Some(source), + Self::JsonError(source) => Some(source.as_ref()), + Self::MissingField(_) => None, + Self::NoHomeDirectory => None, + Self::Other(_) => None, + } + } +} + +impl From for CachedSsoTokenError { + fn from(err: EscapeError) -> Self { + Self::JsonError(err.into()) + } +} + +impl From for CachedSsoTokenError { + fn from(err: aws_smithy_json::deserialize::error::DeserializeError) -> Self { + Self::JsonError(err.into()) + } +} + +impl From for CachedSsoTokenError { + fn from(value: DateTimeFormatError) -> Self { + Self::FailedToFormatDateTime { + source: value.into(), + } + } +} + +/// Determine the SSO cached token path for a given identifier. +/// +/// The `identifier` is the `sso_start_url` for credentials providers, and `sso_session_name` for token providers. +fn cached_token_path(identifier: &str, home: &str) -> PathBuf { + // hex::encode returns a lowercase string + let mut out = PathBuf::with_capacity(home.len() + "/.aws/sso/cache".len() + ".json".len() + 40); + out.push(home); + out.push(".aws/sso/cache"); + out.push(&hex::encode(digest::digest( + &digest::SHA1_FOR_LEGACY_USE_ONLY, + identifier.as_bytes(), + ))); + out.set_extension("json"); + out +} + +/// Load the token for `identifier` from `~/.aws/sso/cache/.json` +/// +/// The `identifier` is the `sso_start_url` for credentials providers, and `sso_session_name` for token providers. +pub(super) async fn load_cached_token( + env: &Env, + fs: &Fs, + identifier: &str, +) -> Result { + let home = home_dir(env, Os::real()).ok_or(CachedSsoTokenError::NoHomeDirectory)?; + let path = cached_token_path(identifier, &home); + let data = Zeroizing::new(fs.read_to_end(&path).await.map_err(|source| { + CachedSsoTokenError::IoError { + what: "read", + path, + source, + } + })?); + parse_cached_token(&data) +} + +/// Parse SSO token JSON from input +fn parse_cached_token( + cached_token_file_contents: &[u8], +) -> Result { + use CachedSsoTokenError as Error; + + let mut access_token = None; + let mut expires_at = None; + let mut client_id = None; + let mut client_secret = None; + let mut refresh_token = None; + let mut region = None; + let mut registration_expires_at = None; + let mut start_url = None; + json_parse_loop(cached_token_file_contents, |key, value| { + match (key, value) { + /* + // Required fields: + "accessToken": "string", + "expiresAt": "2019-11-14T04:05:45Z", + + // Optional fields: + "refreshToken": "string", + "clientId": "ABCDEFG323242423121312312312312312", + "clientSecret": "ABCDE123", + "registrationExpiresAt": "2022-03-06T19:53:17Z", + "region": "us-west-2", + "startUrl": "https://d-abc123.awsapps.com/start" + */ + (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("accessToken") => { + access_token = Some(Zeroizing::new(value.to_unescaped()?.into_owned())); + } + (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("expiresAt") => { + expires_at = Some(value.to_unescaped()?); + } + (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("clientId") => { + client_id = Some(value.to_unescaped()?); + } + (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("clientSecret") => { + client_secret = Some(Zeroizing::new(value.to_unescaped()?.into_owned())); + } + (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("refreshToken") => { + refresh_token = Some(Zeroizing::new(value.to_unescaped()?.into_owned())); + } + (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("region") => { + region = Some(value.to_unescaped()?.into_owned()); + } + (key, Token::ValueString { value, .. }) + if key.eq_ignore_ascii_case("registrationExpiresAt") => + { + registration_expires_at = Some(value.to_unescaped()?); + } + (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("startUrl") => { + start_url = Some(value.to_unescaped()?.into_owned()); + } + _ => {} + }; + Ok(()) + })?; + + Ok(CachedSsoToken { + access_token: access_token.ok_or(Error::MissingField("accessToken"))?, + expires_at: expires_at + .ok_or(Error::MissingField("expiresAt")) + .and_then(|expires_at| { + DateTime::from_str(expires_at.as_ref(), Format::DateTime) + .map_err(|err| Error::InvalidField { field: "expiresAt", source: err.into() }) + .and_then(|date_time| { + SystemTime::try_from(date_time).map_err(|_| { + Error::Other( + "SSO token expiration time cannot be represented by a SystemTime" + .into(), + ) + }) + }) + })?, + client_id: client_id.map(Cow::into_owned), + client_secret, + refresh_token, + region, + registration_expires_at: Ok(registration_expires_at).and_then(|maybe_expires_at| { + if let Some(expires_at) = maybe_expires_at { + Some( + DateTime::from_str(expires_at.as_ref(), Format::DateTime) + .map_err(|err| Error::InvalidField { field: "registrationExpiresAt", source: err.into()}) + .and_then(|date_time| { + SystemTime::try_from(date_time).map_err(|_| { + Error::Other( + "SSO registration expiration time cannot be represented by a SystemTime" + .into(), + ) + }) + }), + ) + .transpose() + } else { + Ok(None) + } + })?, + start_url, + }) +} + +fn json_parse_loop<'a>( + input: &'a [u8], + mut f: impl FnMut(Cow<'a, str>, &Token<'a>) -> Result<(), CachedSsoTokenError>, +) -> Result<(), CachedSsoTokenError> { + use CachedSsoTokenError as Error; + let mut tokens = json_token_iter(input).peekable(); + if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) { + return Err(Error::Other( + "expected a JSON document starting with `{`".into(), + )); + } + loop { + match tokens.next().transpose()? { + Some(Token::EndObject { .. }) => break, + Some(Token::ObjectKey { key, .. }) => { + if let Some(Ok(token)) = tokens.peek() { + let key = key.to_unescaped()?; + f(key, token)? + } + skip_value(&mut tokens)?; + } + other => { + return Err(Error::Other( + format!("expected object key, found: {:?}", other).into(), + )); + } + } + } + if tokens.next().is_some() { + return Err(Error::Other( + "found more JSON tokens after completing parsing".into(), + )); + } + Ok(()) +} + +pub(super) async fn save_cached_token( + env: &Env, + fs: &Fs, + identifier: &str, + token: &CachedSsoToken, +) -> Result<(), CachedSsoTokenError> { + let expires_at = DateTime::from(token.expires_at).fmt(Format::DateTime)?; + let registration_expires_at = token + .registration_expires_at + .map(|time| DateTime::from(time).fmt(Format::DateTime)) + .transpose()?; + + let mut out = Zeroizing::new(String::new()); + let mut writer = JsonObjectWriter::new(&mut out); + writer.key("accessToken").string(&token.access_token); + writer.key("expiresAt").string(&expires_at); + if let Some(refresh_token) = &token.refresh_token { + writer.key("refreshToken").string(refresh_token); + } + if let Some(client_id) = &token.client_id { + writer.key("clientId").string(client_id); + } + if let Some(client_secret) = &token.client_secret { + writer.key("clientSecret").string(client_secret); + } + if let Some(registration_expires_at) = registration_expires_at { + writer + .key("registrationExpiresAt") + .string(®istration_expires_at); + } + if let Some(region) = &token.region { + writer.key("region").string(region); + } + if let Some(start_url) = &token.start_url { + writer.key("startUrl").string(start_url); + } + writer.finish(); + + let home = home_dir(env, Os::real()).ok_or(CachedSsoTokenError::NoHomeDirectory)?; + let path = cached_token_path(identifier, &home); + fs.write(&path, out.as_bytes()) + .await + .map_err(|err| CachedSsoTokenError::IoError { + what: "write", + path, + source: err, + })?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::time::Duration; + + #[test] + fn redact_fields_in_token_debug() { + let token = CachedSsoToken { + access_token: Zeroizing::new("!!SENSITIVE!!".into()), + client_id: Some("clientid".into()), + client_secret: Some(Zeroizing::new("!!SENSITIVE!!".into())), + expires_at: SystemTime::now(), + refresh_token: Some(Zeroizing::new("!!SENSITIVE!!".into())), + region: Some("region".into()), + registration_expires_at: Some(SystemTime::now()), + start_url: Some("starturl".into()), + }; + let debug_str = format!("{:?}", token); + assert!(!debug_str.contains("!!SENSITIVE!!"), "The `Debug` impl for `CachedSsoToken` isn't properly redacting sensitive fields: {debug_str}"); + } + + // Valid token with all fields + #[test] + fn parse_valid_token() { + let file_contents = r#" + { + "startUrl": "https://d-123.awsapps.com/start", + "region": "us-west-2", + "accessToken": "cachedtoken", + "expiresAt": "2021-12-25T21:30:00Z", + "clientId": "clientid", + "clientSecret": "YSBzZWNyZXQ=", + "registrationExpiresAt": "2022-12-25T13:30:00Z", + "refreshToken": "cachedrefreshtoken" + } + "#; + let cached = parse_cached_token(file_contents.as_bytes()).expect("success"); + assert_eq!("cachedtoken", cached.access_token.as_str()); + assert_eq!( + SystemTime::UNIX_EPOCH + Duration::from_secs(1640467800), + cached.expires_at + ); + assert_eq!("clientid", cached.client_id.expect("client id is present")); + assert_eq!( + "YSBzZWNyZXQ=", + cached + .client_secret + .expect("client secret is present") + .as_str() + ); + assert_eq!( + "cachedrefreshtoken", + cached + .refresh_token + .expect("refresh token is present") + .as_str() + ); + assert_eq!( + SystemTime::UNIX_EPOCH + Duration::from_secs(1671975000), + cached + .registration_expires_at + .expect("registration expiration is present") + ); + assert_eq!("us-west-2", cached.region.expect("region is present")); + assert_eq!( + "https://d-123.awsapps.com/start", + cached.start_url.expect("startUrl is present") + ); + } + + // Minimal valid cached token + #[test] + fn parse_valid_token_with_optional_fields_absent() { + let file_contents = r#" + { + "accessToken": "cachedtoken", + "expiresAt": "2021-12-25T21:30:00Z" + } + "#; + let cached = parse_cached_token(file_contents.as_bytes()).expect("success"); + assert_eq!("cachedtoken", cached.access_token.as_str()); + assert_eq!( + SystemTime::UNIX_EPOCH + Duration::from_secs(1640467800), + cached.expires_at + ); + assert!(cached.client_id.is_none()); + assert!(cached.client_secret.is_none()); + assert!(cached.refresh_token.is_none()); + assert!(cached.registration_expires_at.is_none()); + } + + #[test] + fn parse_invalid_timestamp() { + let token = br#" + { + "accessToken": "base64string", + "expiresAt": "notatimestamp", + "region": "us-west-2", + "startUrl": "https://d-abc123.awsapps.com/start" + }"#; + let err = parse_cached_token(token).expect_err("invalid timestamp"); + let expected = "invalid value for the `expiresAt` field in the cached SSO token file"; + let actual = format!("{err}"); + assert!( + actual.contains(expected), + "expected error to contain `{expected}`, but was `{actual}`", + ); + } + + #[test] + fn parse_missing_fields() { + // Token missing accessToken field + let token = br#" + { + "expiresAt": "notatimestamp", + "region": "us-west-2", + "startUrl": "https://d-abc123.awsapps.com/start" + }"#; + let err = parse_cached_token(token).expect_err("missing akid"); + assert!( + matches!(err, CachedSsoTokenError::MissingField("accessToken")), + "incorrect error: {:?}", + err + ); + + // Token missing expiresAt field + let token = br#" + { + "accessToken": "akid", + "region": "us-west-2", + "startUrl": "https://d-abc123.awsapps.com/start" + }"#; + let err = parse_cached_token(token).expect_err("missing expiry"); + assert!( + matches!(err, CachedSsoTokenError::MissingField("expiresAt")), + "incorrect error: {:?}", + err + ); + } + + #[tokio::test] + async fn gracefully_handle_missing_files() { + let err = load_cached_token( + &Env::from_slice(&[("HOME", "/home")]), + &Fs::from_slice(&[]), + "asdf", + ) + .await + .expect_err("should fail, file is missing"); + assert!( + matches!(err, CachedSsoTokenError::IoError { .. }), + "should be io error, got {}", + err + ); + } + + #[test] + fn determine_correct_cache_filenames() { + assert_eq!( + "/home/someuser/.aws/sso/cache/d033e22ae348aeb5660fc2140aec35850c4da997.json", + cached_token_path("admin", "/home/someuser").as_os_str() + ); + assert_eq!( + "/home/someuser/.aws/sso/cache/75e4d41276d8bd17f85986fc6cccef29fd725ce3.json", + cached_token_path("dev-scopes", "/home/someuser").as_os_str() + ); + assert_eq!( + "/home/me/.aws/sso/cache/13f9d35043871d073ab260e020f0ffde092cb14b.json", + cached_token_path("https://d-92671207e4.awsapps.com/start", "/home/me").as_os_str(), + ); + assert_eq!( + "/home/me/.aws/sso/cache/13f9d35043871d073ab260e020f0ffde092cb14b.json", + cached_token_path("https://d-92671207e4.awsapps.com/start", "/home/me/").as_os_str(), + ); + } + + #[tokio::test] + async fn save_cached_token() { + let expires_at = SystemTime::UNIX_EPOCH + Duration::from_secs(50_000_000); + let reg_expires_at = SystemTime::UNIX_EPOCH + Duration::from_secs(100_000_000); + let token = CachedSsoToken { + access_token: Zeroizing::new("access-token".into()), + client_id: Some("client-id".into()), + client_secret: Some(Zeroizing::new("client-secret".into())), + expires_at, + refresh_token: Some(Zeroizing::new("refresh-token".into())), + region: Some("region".into()), + registration_expires_at: Some(reg_expires_at), + start_url: Some("start-url".into()), + }; + + let env = Env::from_slice(&[("HOME", "/home/user")]); + let fs = Fs::from_map(HashMap::<_, Vec>::new()); + super::save_cached_token(&env, &fs, "test", &token) + .await + .expect("success"); + + let contents = fs + .read_to_end("/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json") + .await + .expect("correct file written"); + let contents_str = String::from_utf8(contents).expect("valid utf8"); + assert_eq!( + r#"{"accessToken":"access-token","expiresAt":"1971-08-02T16:53:20Z","refreshToken":"refresh-token","clientId":"client-id","clientSecret":"client-secret","registrationExpiresAt":"1973-03-03T09:46:40Z","region":"region","startUrl":"start-url"}"#, + contents_str, + ); + } + + #[tokio::test] + async fn round_trip_token() { + let expires_at = SystemTime::UNIX_EPOCH + Duration::from_secs(50_000_000); + let reg_expires_at = SystemTime::UNIX_EPOCH + Duration::from_secs(100_000_000); + let original = CachedSsoToken { + access_token: Zeroizing::new("access-token".into()), + client_id: Some("client-id".into()), + client_secret: Some(Zeroizing::new("client-secret".into())), + expires_at, + refresh_token: Some(Zeroizing::new("refresh-token".into())), + region: Some("region".into()), + registration_expires_at: Some(reg_expires_at), + start_url: Some("start-url".into()), + }; + + let env = Env::from_slice(&[("HOME", "/home/user")]); + let fs = Fs::from_map(HashMap::<_, Vec>::new()); + + super::save_cached_token(&env, &fs, "test", &original) + .await + .unwrap(); + + let roundtripped = load_cached_token(&env, &fs, "test").await.unwrap(); + assert_eq!(original, roundtripped) + } +} diff --git a/aws/rust-runtime/aws-config/src/sso/credentials.rs b/aws/rust-runtime/aws-config/src/sso/credentials.rs new file mode 100644 index 0000000000..40b7a36811 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/sso/credentials.rs @@ -0,0 +1,311 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! SSO Credentials Provider +//! +//! This credentials provider enables loading credentials from `~/.aws/sso/cache`. For more information, +//! see [Using AWS SSO Credentials](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/sso-credentials.html) +//! +//! This provider is included automatically when profiles are loaded. + +use super::cache::load_cached_token; +use crate::connector::expect_connector; +use crate::provider_config::ProviderConfig; +use crate::sso::SsoTokenProvider; +use aws_credential_types::cache::CredentialsCache; +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::{config::Builder as SsoConfigBuilder, Client as SsoClient, Config as SsoConfig}; +use aws_sdk_ssooidc::Config as SsoOidcConfig; +use aws_smithy_async::time::SharedTimeSource; +use aws_smithy_types::retry::RetryConfig; +use aws_smithy_types::DateTime; +use aws_types::os_shim_internal::{Env, Fs}; +use aws_types::region::Region; +use std::convert::TryInto; + +/// SSO Credentials Provider +/// +/// _Note: This provider is part of the default credentials chain and is integrated with the profile-file provider._ +/// +/// This credentials provider will use cached SSO tokens stored in `~/.aws/sso/cache/.json`. +/// Two different values will be tried for `` in order: +/// 1. The configured [`session_name`](Builder::session_name). +/// 2. The configured [`start_url`](Builder::start_url). +#[derive(Debug)] +pub struct SsoCredentialsProvider { + fs: Fs, + env: Env, + sso_provider_config: SsoProviderConfig, + sso_config: SsoConfigBuilder, + token_provider: Option, + time_source: SharedTimeSource, +} + +impl SsoCredentialsProvider { + /// Creates a builder for [`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 mut sso_config = SsoConfig::builder() + .http_connector(expect_connector( + "The SSO credentials provider", + provider_config.connector(&Default::default()), + )) + .retry_config(RetryConfig::standard()) + .time_source(provider_config.time_source()); + sso_config.set_sleep_impl(provider_config.sleep()); + + let token_provider = if let Some(session_name) = &sso_provider_config.session_name { + let mut sso_oidc_config = SsoOidcConfig::builder() + .http_connector(expect_connector( + "The SSO credentials provider", + provider_config.connector(&Default::default()), + )) + .retry_config(RetryConfig::standard()) + .time_source(provider_config.time_source()); + sso_oidc_config.set_sleep_impl(provider_config.sleep()); + + Some( + SsoTokenProvider::builder() + .start_url(&sso_provider_config.start_url) + .session_name(session_name) + .region(sso_provider_config.region.clone()) + .build(), + ) + } else { + None + }; + + SsoCredentialsProvider { + fs, + env, + sso_provider_config, + sso_config, + token_provider, + time_source: provider_config.time_source(), + } + } + + async fn credentials(&self) -> provider::Result { + load_sso_credentials( + &self.sso_provider_config, + &self.sso_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()) + } +} + +/// Builder for [`SsoCredentialsProvider`] +#[derive(Default, Debug, Clone)] +pub struct Builder { + provider_config: Option, + account_id: Option, + region: Option, + role_name: Option, + start_url: Option, + session_name: Option, +} + +impl Builder { + /// Create a new builder for [`SsoCredentialsProvider`] + pub fn new() -> Self { + Self::default() + } + + /// Override the configuration used for this provider + pub fn configure(mut self, provider_config: &ProviderConfig) -> Self { + self.provider_config = Some(provider_config.clone()); + self + } + + /// Set the account id used for SSO + /// + /// This is a required field. + pub fn account_id(mut self, account_id: impl Into) -> Self { + self.account_id = Some(account_id.into()); + self + } + + /// Set the account id used for SSO + /// + /// This is a required field. + pub fn set_account_id(&mut self, account_id: Option) -> &mut Self { + self.account_id = account_id; + self + } + + /// Set the region used for SSO + /// + /// This is a required field. + pub fn region(mut self, region: Region) -> Self { + self.region = Some(region); + self + } + + /// Set the region used for SSO + /// + /// This is a required field. + pub fn set_region(&mut self, region: Option) -> &mut Self { + self.region = region; + self + } + + /// Set the role name used for SSO + /// + /// This is a required field. + pub fn role_name(mut self, role_name: impl Into) -> Self { + self.role_name = Some(role_name.into()); + self + } + + /// Set the role name used for SSO + /// + /// This is a required field. + pub fn set_role_name(&mut self, role_name: Option) -> &mut Self { + self.role_name = role_name; + self + } + + /// Set the start URL used for SSO + /// + /// This is a required field. + pub fn start_url(mut self, start_url: impl Into) -> Self { + self.start_url = Some(start_url.into()); + self + } + + /// Set the start URL used for SSO + /// + /// This is a required field. + pub fn set_start_url(&mut self, start_url: Option) -> &mut Self { + self.start_url = start_url; + self + } + + /// Set the session name used for SSO + pub fn session_name(mut self, session_name: impl Into) -> Self { + self.session_name = Some(session_name.into()); + self + } + + /// Set the session name used for SSO + pub fn set_session_name(&mut self, session_name: Option) -> &mut Self { + self.session_name = session_name; + self + } + + /// Construct an SsoCredentialsProvider from the builder + /// + /// # Panics + /// This method will panic if the any of the following required fields are unset: + /// - [`start_url`](Self::start_url) + /// - [`role_name`](Self::role_name) + /// - [`account_id`](Self::account_id) + /// - [`region`](Self::region) + 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, +} + +async fn load_sso_credentials( + sso_provider_config: &SsoProviderConfig, + sso_config: &SsoConfigBuilder, + 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 { + // Backwards compatible token loading that uses `start_url` instead of `session_name` + load_cached_token(env, fs, &sso_provider_config.start_url) + .await + .map_err(CredentialsError::provider_error)? + }; + + let config = sso_config + .clone() + .region(sso_provider_config.region.clone()) + .credentials_cache(CredentialsCache::no_caching()) + .build(); + // TODO(enableNewSmithyRuntimeCleanup): Use `customize().config_override()` to set the region instead of creating a new client once middleware is removed + let client = SsoClient::from_conf(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", + )) +} diff --git a/aws/rust-runtime/aws-config/src/sso/token.rs b/aws/rust-runtime/aws-config/src/sso/token.rs new file mode 100644 index 0000000000..026510872e --- /dev/null +++ b/aws/rust-runtime/aws-config/src/sso/token.rs @@ -0,0 +1,861 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! SSO Token Provider +//! +//! This token provider enables loading an access token from `~/.aws/sso/cache`. For more information, +//! see [AWS Builder ID for developers](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/builder-id.html). +//! +//! This provider is included automatically when profiles are loaded. + +use crate::sso::cache::{ + load_cached_token, save_cached_token, CachedSsoToken, CachedSsoTokenError, +}; +use aws_credential_types::cache::{CredentialsCache, ExpiringCache}; +use aws_sdk_ssooidc::config::Builder as SsoOidcConfigBuilder; +use aws_sdk_ssooidc::error::DisplayErrorContext; +use aws_sdk_ssooidc::operation::create_token::CreateTokenOutput; +use aws_sdk_ssooidc::Client as SsoOidcClient; +use aws_smithy_async::time::SharedTimeSource; +use aws_smithy_runtime_api::client::identity::http::Token; +use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver}; +use aws_smithy_runtime_api::client::orchestrator::Future; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; +use aws_smithy_types::config_bag::ConfigBag; +use aws_types::os_shim_internal::{Env, Fs}; +use aws_types::region::Region; +use std::error::Error as StdError; +use std::fmt; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; +use zeroize::Zeroizing; + +const REFRESH_BUFFER_TIME: Duration = Duration::from_secs(5 * 60 /* 5 minutes */); +const MIN_TIME_BETWEEN_REFRESH: Duration = Duration::from_secs(30); + +/// SSO Token Provider +/// +/// This token provider will use cached SSO tokens stored in `~/.aws/sso/cache/.json`. +/// `` is computed based on the configured [`session_namej`](Builder::session_name). +/// +/// If possible, the cached token will be refreshed when it gets close to expiring. +#[derive(Debug)] +pub struct SsoTokenProvider { + inner: Arc, + token_cache: ExpiringCache, +} + +#[derive(Debug)] +struct Inner { + env: Env, + fs: Fs, + region: Region, + session_name: String, + start_url: String, + sso_oidc_config: SsoOidcConfigBuilder, + last_refresh_attempt: Mutex>, +} + +impl SsoTokenProvider { + /// Creates a `SsoTokenProvider` builder. + pub fn builder() -> Builder { + Default::default() + } + + async fn refresh_cached_token( + inner: &Inner, + cached_token: &CachedSsoToken, + identifier: &str, + now: SystemTime, + ) -> Result, SsoTokenProviderError> { + // TODO(enableNewSmithyRuntimeCleanup): Use `customize().config_override()` to set the region instead of creating a new client once middleware is removed + let config = inner + .sso_oidc_config + .clone() + .region(Some(inner.region.clone())) + .credentials_cache(CredentialsCache::no_caching()) + .build(); + let client = SsoOidcClient::from_conf(config); + let resp = client + .create_token() + .grant_type("refresh_token") + .client_id( + cached_token + .client_id + .as_ref() + .expect("required for token refresh") + .clone(), + ) + .client_secret( + cached_token + .client_secret + .as_ref() + .expect("required for token refresh") + .as_str(), + ) + .refresh_token( + cached_token + .refresh_token + .as_ref() + .expect("required for token refresh") + .as_str(), + ) + .send() + .await; + match resp { + Ok(CreateTokenOutput { + access_token: Some(access_token), + refresh_token, + expires_in, + .. + }) => { + let refreshed_token = CachedSsoToken { + access_token: Zeroizing::new(access_token), + client_id: cached_token.client_id.clone(), + client_secret: cached_token.client_secret.clone(), + expires_at: now + + Duration::from_secs( + u64::try_from(expires_in) + .map_err(|_| SsoTokenProviderError::BadExpirationTimeFromSsoOidc)?, + ), + refresh_token: refresh_token + .map(Zeroizing::new) + .or_else(|| cached_token.refresh_token.clone()), + region: Some(inner.region.to_string()), + registration_expires_at: cached_token.registration_expires_at, + start_url: Some(inner.start_url.clone()), + }; + save_cached_token(&inner.env, &inner.fs, identifier, &refreshed_token).await?; + tracing::debug!("saved refreshed SSO token"); + Ok(Some(refreshed_token)) + } + Ok(_) => { + tracing::debug!("SSO OIDC CreateToken responded without an access token"); + Ok(None) + } + Err(err) => { + tracing::debug!( + "call to SSO OIDC CreateToken for SSO token refresh failed: {}", + DisplayErrorContext(&err) + ); + Ok(None) + } + } + } + + pub(super) fn resolve_token( + &self, + time_source: SharedTimeSource, + ) -> impl std::future::Future> + 'static + { + let token_cache = self.token_cache.clone(); + let inner = self.inner.clone(); + + async move { + if let Some(token) = token_cache + .yield_or_clear_if_expired(time_source.now()) + .await + { + tracing::debug!("using cached SSO token"); + return Ok(token); + } + let token = token_cache + .get_or_load(|| async move { + tracing::debug!("expiring cache asked for an updated SSO token"); + let mut token = + load_cached_token(&inner.env, &inner.fs, &inner.session_name).await?; + tracing::debug!("loaded cached SSO token"); + + let now = time_source.now(); + let expired = token.expires_at <= now; + let expires_soon = token.expires_at - REFRESH_BUFFER_TIME <= now; + let last_refresh = *inner.last_refresh_attempt.lock().unwrap(); + let min_time_passed = last_refresh + .map(|lr| { + now.duration_since(lr).expect("last_refresh is in the past") + >= MIN_TIME_BETWEEN_REFRESH + }) + .unwrap_or(true); + let registration_expired = token + .registration_expires_at + .map(|t| t <= now) + .unwrap_or(true); + let refreshable = + token.refreshable() && min_time_passed && !registration_expired; + + tracing::debug!( + expired = ?expired, + expires_soon = ?expires_soon, + min_time_passed = ?min_time_passed, + registration_expired = ?registration_expired, + refreshable = ?refreshable, + will_refresh = ?(expires_soon && refreshable), + "cached SSO token refresh decision" + ); + + // Fail fast if the token has expired and we can't refresh it + if expired && !refreshable { + tracing::debug!("cached SSO token is expired and cannot be refreshed"); + return Err(SsoTokenProviderError::ExpiredToken); + } + + // Refresh the token if it is going to expire soon + if expires_soon && refreshable { + tracing::debug!("attempting to refresh SSO token"); + if let Some(refreshed_token) = + Self::refresh_cached_token(&inner, &token, &inner.session_name, now) + .await? + { + token = refreshed_token; + } + *inner.last_refresh_attempt.lock().unwrap() = Some(now); + } + + let expires_at = token.expires_at; + Ok((token, expires_at)) + }) + .await?; + + Ok(token) + } + } +} + +impl IdentityResolver for SsoTokenProvider { + fn resolve_identity( + &self, + runtime_components: &RuntimeComponents, + _config_bag: &ConfigBag, + ) -> Future { + let time_source = runtime_components + .time_source() + .expect("a time source required by SsoTokenProvider"); + let token_future = self.resolve_token(time_source); + Future::new(Box::pin(async move { + let token = token_future.await?; + Ok(Identity::new( + Token::new(token.access_token.as_str(), Some(token.expires_at)), + Some(token.expires_at), + )) + })) + } +} + +/// Builder for [`SsoTokenProvider`]. +#[derive(Debug, Default)] +pub struct Builder { + region: Option, + session_name: Option, + start_url: Option, + sso_oidc_config: Option, +} + +impl Builder { + /// Creates a new builder for [`SsoTokenProvider`]. + pub fn new() -> Self { + Default::default() + } + + /// Sets the SSO region. + /// + /// This is a required field. + pub fn region(mut self, region: impl Into) -> Self { + self.region = Some(region.into()); + self + } + + /// Sets the SSO region. + /// + /// This is a required field. + pub fn set_region(&mut self, region: Option) -> &mut Self { + self.region = region; + self + } + + /// Sets the SSO session name. + /// + /// This is a required field. + pub fn session_name(mut self, session_name: impl Into) -> Self { + self.session_name = Some(session_name.into()); + self + } + + /// Sets the SSO session name. + /// + /// This is a required field. + pub fn set_session_name(&mut self, session_name: Option) -> &mut Self { + self.session_name = session_name; + self + } + + /// Sets the SSO start URL. + /// + /// This is a required field. + pub fn start_url(mut self, start_url: impl Into) -> Self { + self.start_url = Some(start_url.into()); + self + } + + /// Sets the SSO start URL. + /// + /// This is a required field. + pub fn set_start_url(&mut self, start_url: Option) -> &mut Self { + self.start_url = start_url; + self + } + + /// Sets the SSO OIDC client config. + pub fn sso_oidc_config(mut self, config: SsoOidcConfigBuilder) -> Self { + self.sso_oidc_config = Some(config); + self + } + + /// Sets the SSO OIDC client config. + pub fn set_sso_oidc_config(&mut self, config: Option) -> &mut Self { + self.sso_oidc_config = config; + self + } + + /// Builds the [`SsoTokenProvider`]. + /// + /// # Panics + /// + /// This will panic if any of the required fields are not given. + pub fn build(self) -> SsoTokenProvider { + self.build_with(Env::real(), Fs::real()) + } + + fn build_with(self, env: Env, fs: Fs) -> SsoTokenProvider { + SsoTokenProvider { + inner: Arc::new(Inner { + env, + fs, + region: self.region.expect("region is required"), + session_name: self.session_name.expect("session_name is required"), + start_url: self.start_url.expect("start_url is required"), + sso_oidc_config: self.sso_oidc_config.unwrap_or_default(), + last_refresh_attempt: Mutex::new(None), + }), + token_cache: ExpiringCache::new(REFRESH_BUFFER_TIME), + } + } +} + +#[derive(Debug)] +pub(super) enum SsoTokenProviderError { + BadExpirationTimeFromSsoOidc, + FailedToLoadToken { + source: Box, + }, + ExpiredToken, +} + +impl fmt::Display for SsoTokenProviderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadExpirationTimeFromSsoOidc => { + f.write_str("SSO OIDC responded with a negative expiration duration") + } + Self::ExpiredToken => f.write_str("the SSO token has expired and cannot be refreshed"), + Self::FailedToLoadToken { .. } => f.write_str("failed to load the cached SSO token"), + } + } +} + +impl StdError for SsoTokenProviderError { + fn cause(&self) -> Option<&dyn StdError> { + match self { + Self::BadExpirationTimeFromSsoOidc => None, + Self::ExpiredToken => None, + Self::FailedToLoadToken { source } => Some(source.as_ref()), + } + } +} + +impl From for SsoTokenProviderError { + fn from(source: CachedSsoTokenError) -> Self { + Self::FailedToLoadToken { + source: source.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_sdk_sso::config::{AsyncSleep, SharedAsyncSleep}; + use aws_smithy_async::rt::sleep::TokioSleep; + use aws_smithy_async::test_util::instant_time_and_sleep; + use aws_smithy_async::time::{StaticTimeSource, TimeSource}; + use aws_smithy_client::test_connection::{capture_request, TestConnection}; + use aws_smithy_http::body::SdkBody; + use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; + use aws_smithy_types::date_time::Format; + use aws_smithy_types::retry::RetryConfig; + use aws_smithy_types::DateTime; + + fn time(s: &str) -> SystemTime { + SystemTime::try_from(DateTime::parse(Format::DateTime, s).unwrap()).unwrap() + } + + struct TestHarness { + time_source: SharedTimeSource, + token_provider: SsoTokenProvider, + env: Env, + fs: Fs, + } + + impl TestHarness { + fn new( + time_source: impl TimeSource + 'static, + sleep_impl: impl AsyncSleep + 'static, + conn: impl Into<::aws_smithy_client::http_connector::HttpConnector>, + fs: Fs, + ) -> Self { + let env = Env::from_slice(&[("HOME", "/home/user")]); + let time_source = SharedTimeSource::new(time_source); + let sso_oidc_config = aws_sdk_ssooidc::config::Config::builder() + .http_connector(conn) + .time_source(time_source.clone()) + .sleep_impl(SharedAsyncSleep::new(sleep_impl)) + // disable retry to simplify testing + .retry_config(RetryConfig::disabled()); + Self { + time_source, + token_provider: SsoTokenProvider::builder() + .session_name("test") + .region(Region::new("us-west-2")) + .start_url("https://d-123.awsapps.com/start") + .sso_oidc_config(sso_oidc_config) + .build_with(env.clone(), fs.clone()), + env, + fs, + } + } + + async fn expect_sso_token(&self, value: &str, expires_at: &str) -> CachedSsoToken { + let token = self + .token_provider + .resolve_token(self.time_source.clone()) + .await + .unwrap(); + assert_eq!(value, token.access_token.as_str()); + assert_eq!(time(expires_at), token.expires_at); + token + } + + async fn expect_token(&self, value: &str, expires_at: &str) { + let runtime_components = RuntimeComponentsBuilder::for_tests() + .with_time_source(Some(self.time_source.clone())) + .build() + .unwrap(); + let config_bag = ConfigBag::base(); + let identity = self + .token_provider + .resolve_identity(&runtime_components, &config_bag) + .await + .unwrap(); + let token = identity.data::().unwrap().clone(); + assert_eq!(value, token.token()); + assert_eq!(time(expires_at), *identity.expiration().unwrap()); + } + + async fn expect_expired_token_err(&self) { + let err = self + .token_provider + .resolve_token(self.time_source.clone()) + .await + .expect_err("expected failure"); + assert!( + matches!(err, SsoTokenProviderError::ExpiredToken), + "expected {err:?} to be `ExpiredToken`" + ); + } + + fn last_refresh_attempt_time(&self) -> Option { + self.token_provider + .inner + .last_refresh_attempt + .lock() + .unwrap() + .map(|time| { + DateTime::try_from(time) + .unwrap() + .fmt(Format::DateTime) + .unwrap() + }) + } + } + + #[tokio::test] + async fn use_unexpired_cached_token() { + let fs = Fs::from_slice(&[( + "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json", + r#" + { "accessToken": "some-token", + "expiresAt": "1975-01-01T00:00:00Z" } + "#, + )]); + + let now = time("1974-12-25T00:00:00Z"); + let time_source = SharedTimeSource::new(StaticTimeSource::new(now)); + + let (conn, req_rx) = capture_request(None); + let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs); + + harness + .expect_token("some-token", "1975-01-01T00:00:00Z") + .await; + // it can't refresh this token + req_rx.expect_no_request(); + } + + #[tokio::test] + async fn expired_cached_token() { + let fs = Fs::from_slice(&[( + "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json", + r#" + { "accessToken": "some-token", + "expiresAt": "1999-12-15T00:00:00Z" } + "#, + )]); + + let now = time("2023-01-01T00:00:00Z"); + let time_source = SharedTimeSource::new(StaticTimeSource::new(now)); + + let (conn, req_rx) = capture_request(None); + let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs); + + harness.expect_expired_token_err().await; + // it can't refresh this token + req_rx.expect_no_request(); + } + + #[tokio::test] + async fn expired_token_and_expired_client_registration() { + let fs = Fs::from_slice(&[( + "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json", + r#" + { "startUrl": "https://d-123.awsapps.com/start", + "region": "us-west-2", + "accessToken": "cachedtoken", + "expiresAt": "2021-10-25T13:00:00Z", + "clientId": "clientid", + "clientSecret": "YSBzZWNyZXQ=", + "registrationExpiresAt": "2021-11-25T13:30:00Z", + "refreshToken": "cachedrefreshtoken" } + "#, + )]); + + let now = time("2023-08-11T04:11:17Z"); + let time_source = SharedTimeSource::new(StaticTimeSource::new(now)); + + let (conn, req_rx) = capture_request(None); + let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs); + + // the registration has expired, so the token can't be refreshed + harness.expect_expired_token_err().await; + req_rx.expect_no_request(); + } + + #[tokio::test] + async fn expired_token_refresh_with_refresh_token() { + let fs = Fs::from_slice(&[( + "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json", + r#" + { "startUrl": "https://d-123.awsapps.com/start", + "region": "us-west-2", + "accessToken": "cachedtoken", + "expiresAt": "2021-12-25T13:00:00Z", + "clientId": "clientid", + "clientSecret": "YSBzZWNyZXQ=", + "registrationExpiresAt": "2022-12-25T13:30:00Z", + "refreshToken": "cachedrefreshtoken" } + "#, + )]); + + let now = time("2021-12-25T13:30:00Z"); + let time_source = SharedTimeSource::new(StaticTimeSource::new(now)); + + let (conn, req_rx) = capture_request(Some( + http::Response::builder() + .status(200) + .body(SdkBody::from( + r#" + { "tokenType": "Bearer", + "accessToken": "newtoken", + "expiresIn": 28800, + "refreshToken": "newrefreshtoken" } + "#, + )) + .unwrap(), + )); + let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs); + + let returned_token = harness + .expect_sso_token("newtoken", "2021-12-25T21:30:00Z") + .await; + let cached_token = load_cached_token(&harness.env, &harness.fs, "test") + .await + .unwrap(); + assert_eq!(returned_token, cached_token); + assert_eq!( + "newrefreshtoken", + returned_token.refresh_token.unwrap().as_str() + ); + assert_eq!( + "https://d-123.awsapps.com/start", + returned_token.start_url.unwrap() + ); + assert_eq!("us-west-2", returned_token.region.unwrap().to_string()); + assert_eq!("clientid", returned_token.client_id.unwrap()); + assert_eq!( + "YSBzZWNyZXQ=", + returned_token.client_secret.unwrap().as_str() + ); + assert_eq!( + SystemTime::UNIX_EPOCH + Duration::from_secs(1_671_975_000), + returned_token.registration_expires_at.unwrap() + ); + + let refresh_req = req_rx.expect_request(); + let parsed_req: serde_json::Value = + serde_json::from_slice(refresh_req.body().bytes().unwrap()).unwrap(); + let parsed_req = parsed_req.as_object().unwrap(); + assert_eq!( + "clientid", + parsed_req.get("clientId").unwrap().as_str().unwrap() + ); + assert_eq!( + "YSBzZWNyZXQ=", + parsed_req.get("clientSecret").unwrap().as_str().unwrap() + ); + assert_eq!( + "refresh_token", + parsed_req.get("grantType").unwrap().as_str().unwrap() + ); + assert_eq!( + "cachedrefreshtoken", + parsed_req.get("refreshToken").unwrap().as_str().unwrap() + ); + } + + #[tokio::test] + async fn expired_token_refresh_fails() { + let fs = Fs::from_slice(&[( + "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json", + r#" + { "startUrl": "https://d-123.awsapps.com/start", + "region": "us-west-2", + "accessToken": "cachedtoken", + "expiresAt": "2021-12-25T13:00:00Z", + "clientId": "clientid", + "clientSecret": "YSBzZWNyZXQ=", + "registrationExpiresAt": "2022-12-25T13:30:00Z", + "refreshToken": "cachedrefreshtoken" } + "#, + )]); + + let now = time("2021-12-25T13:30:00Z"); + let time_source = SharedTimeSource::new(StaticTimeSource::new(now)); + + let (conn, req_rx) = capture_request(Some( + http::Response::builder() + .status(500) + .body(SdkBody::from("")) + .unwrap(), + )); + let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs); + + // it should return the previous token since refresh failed and it hasn't expired yet + let returned_token = harness + .expect_sso_token("cachedtoken", "2021-12-25T13:00:00Z") + .await; + let cached_token = load_cached_token(&harness.env, &harness.fs, "test") + .await + .unwrap(); + assert_eq!(returned_token, cached_token); + + let _ = req_rx.expect_request(); + } + + // Expired token refresh without new refresh token + #[tokio::test] + async fn expired_token_refresh_without_new_refresh_token() { + let fs = Fs::from_slice(&[( + "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json", + r#" + { "startUrl": "https://d-123.awsapps.com/start", + "region": "us-west-2", + "accessToken": "cachedtoken", + "expiresAt": "2021-12-25T13:00:00Z", + "clientId": "clientid", + "clientSecret": "YSBzZWNyZXQ=", + "registrationExpiresAt": "2022-12-25T13:30:00Z", + "refreshToken": "cachedrefreshtoken" } + "#, + )]); + + let now = time("2021-12-25T13:30:00Z"); + let time_source = SharedTimeSource::new(StaticTimeSource::new(now)); + + let (conn, req_rx) = capture_request(Some( + http::Response::builder() + .status(200) + .body(SdkBody::from( + r#" + { "tokenType": "Bearer", + "accessToken": "newtoken", + "expiresIn": 28800 } + "#, + )) + .unwrap(), + )); + let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs); + + let returned_token = harness + .expect_sso_token("newtoken", "2021-12-25T21:30:00Z") + .await; + let cached_token = load_cached_token(&harness.env, &harness.fs, "test") + .await + .unwrap(); + assert_eq!(returned_token, cached_token); + assert_eq!( + "cachedrefreshtoken", + returned_token.refresh_token.unwrap().as_str(), + "it should have kept the old refresh token" + ); + + let _ = req_rx.expect_request(); + } + + #[tokio::test] + async fn refresh_timings() { + let _logs = capture_test_logs(); + + let start_time = DateTime::parse(Format::DateTime, "2023-01-01T00:00:00Z").unwrap(); + let (time_source, sleep_impl) = instant_time_and_sleep(start_time.try_into().unwrap()); + let shared_time_source = SharedTimeSource::new(time_source.clone()); + + let fs = Fs::from_slice(&[( + "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json", + r#" + { "startUrl": "https://d-123.awsapps.com/start", + "region": "us-west-2", + "accessToken": "first_token", + "_comment_expiresAt": "-------- Ten minutes after the start time: ------", + "expiresAt": "2023-01-01T00:10:00Z", + "clientId": "clientid", + "clientSecret": "YSBzZWNyZXQ=", + "registrationExpiresAt": "2023-01-02T12:00:00Z", + "refreshToken": "cachedrefreshtoken" } + "#, + )]); + + let events = vec![ + // First refresh attempt should fail + ( + http::Request::new(SdkBody::from("")), // don't really care what the request looks like + http::Response::builder() + .status(500) + .body(SdkBody::from("")) + .unwrap(), + ), + // Second refresh attempt should also fail + ( + http::Request::new(SdkBody::from("")), // don't really care what the request looks like + http::Response::builder() + .status(500) + .body(SdkBody::from("")) + .unwrap(), + ), + // Third refresh attempt will succeed + ( + http::Request::new(SdkBody::from("")), // don't really care what the request looks like + http::Response::builder() + .status(200) + .body(SdkBody::from( + r#" + { "tokenType": "Bearer", + "accessToken": "second_token", + "expiresIn": 28800 } + "#, + )) + .unwrap(), + ), + ]; + let conn = TestConnection::new(events); + let harness = TestHarness::new(shared_time_source, sleep_impl, conn, fs); + + tracing::info!("test: first token retrieval should return the cached token"); + assert!( + harness.last_refresh_attempt_time().is_none(), + "the last attempt time should start empty" + ); + harness + .expect_token("first_token", "2023-01-01T00:10:00Z") + .await; + assert!( + harness.last_refresh_attempt_time().is_none(), + "it shouldn't have tried to refresh, so the last refresh attempt time shouldn't be set" + ); + + tracing::info!("test: advance 3 minutes"); + time_source.advance(Duration::from_secs(3 * 60)); + + tracing::info!("test: the token shouldn't get refreshed since it's not in the 5 minute buffer time yet"); + harness + .expect_token("first_token", "2023-01-01T00:10:00Z") + .await; + assert!( + harness.last_refresh_attempt_time().is_none(), + "it shouldn't have tried to refresh since the token isn't expiring soon" + ); + + tracing::info!("test: advance 2 minutes"); + time_source.advance(Duration::from_secs(2 * 60)); + + tracing::info!( + "test: the token will fail to refresh, and the old cached token will be returned" + ); + harness + .expect_token("first_token", "2023-01-01T00:10:00Z") + .await; + assert_eq!( + Some("2023-01-01T00:05:00Z"), + harness.last_refresh_attempt_time().as_deref(), + "it should update the last refresh attempt time since the expiration time is soon" + ); + + tracing::info!("test: advance 15 seconds"); + time_source.advance(Duration::from_secs(15)); + + tracing::info!( + "test: the token will not refresh because the minimum time hasn't passed between attempts" + ); + harness + .expect_token("first_token", "2023-01-01T00:10:00Z") + .await; + + tracing::info!("test: advance 15 seconds"); + time_source.advance(Duration::from_secs(15)); + + tracing::info!( + "test: the token will fail to refresh, and the old cached token will be returned" + ); + harness + .expect_token("first_token", "2023-01-01T00:10:00Z") + .await; + + tracing::info!("test: advance 30 seconds"); + time_source.advance(Duration::from_secs(30)); + + tracing::info!("test: the token will refresh successfully"); + harness + .expect_token("second_token", "2023-01-01T08:06:00Z") + .await; + } +} diff --git a/aws/rust-runtime/aws-runtime/src/identity.rs b/aws/rust-runtime/aws-runtime/src/identity.rs index 84e20d2afe..43a3224a8b 100644 --- a/aws/rust-runtime/aws-runtime/src/identity.rs +++ b/aws/rust-runtime/aws-runtime/src/identity.rs @@ -9,6 +9,7 @@ pub mod credentials { use aws_smithy_runtime_api::box_error::BoxError; use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver}; use aws_smithy_runtime_api::client::orchestrator::Future; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::ConfigBag; /// Smithy identity resolver for AWS credentials. @@ -25,7 +26,11 @@ pub mod credentials { } impl IdentityResolver for CredentialsIdentityResolver { - fn resolve_identity(&self, _config_bag: &ConfigBag) -> Future { + fn resolve_identity( + &self, + _runtime_components: &RuntimeComponents, + _config_bag: &ConfigBag, + ) -> Future { let cache = self.credentials_cache.clone(); Future::new(Box::pin(async move { let credentials = cache.as_ref().provide_cached_credentials().await?; diff --git a/aws/rust-runtime/aws-types/Cargo.toml b/aws/rust-runtime/aws-types/Cargo.toml index eb0b527f42..084d3f8d0c 100644 --- a/aws/rust-runtime/aws-types/Cargo.toml +++ b/aws/rust-runtime/aws-types/Cargo.toml @@ -25,9 +25,10 @@ http = "0.2.6" hyper-rustls = { version = "0.24", optional = true, features = ["rustls-native-certs", "http2", "webpki-roots"] } [dev-dependencies] -futures-util = { version = "0.3.16", default-features = false } http = "0.2.4" +tempfile = "3" tracing-test = "0.2.4" +tokio = { version = "1", features = ["rt", "macros"] } [build-dependencies] rustc_version = "0.4.0" diff --git a/aws/rust-runtime/aws-types/src/os_shim_internal.rs b/aws/rust-runtime/aws-types/src/os_shim_internal.rs index 85f67cd4c2..53b26706f7 100644 --- a/aws/rust-runtime/aws-types/src/os_shim_internal.rs +++ b/aws/rust-runtime/aws-types/src/os_shim_internal.rs @@ -12,7 +12,7 @@ use std::env::VarError; use std::ffi::OsString; use std::fmt::Debug; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use crate::os_shim_internal::fs::Fake; @@ -50,7 +50,7 @@ impl Fs { } pub fn from_raw_map(fs: HashMap>) -> Self { - Fs(fs::Inner::Fake(Arc::new(Fake::MapFs(fs)))) + Fs(fs::Inner::Fake(Arc::new(Fake::MapFs(Mutex::new(fs))))) } pub fn from_map(data: HashMap>>) -> Self { @@ -125,9 +125,12 @@ impl Fs { use fs::Inner; let path = path.as_ref(); match &self.0 { + // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below Inner::Real => std::fs::read(path), Inner::Fake(fake) => match fake.as_ref() { Fake::MapFs(fs) => fs + .lock() + .unwrap() .get(path.as_os_str()) .cloned() .ok_or_else(|| std::io::ErrorKind::NotFound.into()), @@ -143,13 +146,48 @@ impl Fs { }, } } + + /// Write a slice as the entire contents of a file. + /// + /// This is equivalent to `std::fs::write`. + pub async fn write( + &self, + path: impl AsRef, + contents: impl AsRef<[u8]>, + ) -> std::io::Result<()> { + use fs::Inner; + match &self.0 { + // TODO(https://github.com/awslabs/aws-sdk-rust/issues/867): Use async IO below + Inner::Real => { + std::fs::write(path, contents)?; + } + Inner::Fake(fake) => match fake.as_ref() { + Fake::MapFs(fs) => { + fs.lock() + .unwrap() + .insert(path.as_ref().as_os_str().into(), contents.as_ref().to_vec()); + } + Fake::NamespacedFs { + real_path, + namespaced_to, + } => { + let actual_path = path + .as_ref() + .strip_prefix(namespaced_to) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound))?; + std::fs::write(real_path.join(actual_path), contents)?; + } + }, + } + Ok(()) + } } mod fs { use std::collections::HashMap; use std::ffi::OsString; use std::path::PathBuf; - use std::sync::Arc; + use std::sync::{Arc, Mutex}; #[derive(Clone, Debug)] pub(super) enum Inner { @@ -159,7 +197,7 @@ mod fs { #[derive(Debug)] pub(super) enum Fake { - MapFs(HashMap>), + MapFs(Mutex>>), NamespacedFs { real_path: PathBuf, namespaced_to: PathBuf, @@ -242,8 +280,6 @@ mod env { mod test { use std::env::VarError; - use futures_util::FutureExt; - use crate::os_shim_internal::{Env, Fs}; #[test] @@ -256,19 +292,33 @@ mod test { ) } - #[test] - fn fs_works() { + #[tokio::test] + async fn fs_from_test_dir_works() { let fs = Fs::from_test_dir(".", "/users/test-data"); let _ = fs .read_to_end("/users/test-data/Cargo.toml") - .now_or_never() - .expect("future should not poll") + .await .expect("file exists"); let _ = fs .read_to_end("doesntexist") - .now_or_never() - .expect("future should not poll") + .await .expect_err("file doesnt exists"); } + + #[tokio::test] + async fn fs_round_trip_file_with_real() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("test-file"); + + let fs = Fs::real(); + fs.read_to_end(&path) + .await + .expect_err("file doesn't exist yet"); + + fs.write(&path, b"test").await.expect("success"); + + let result = fs.read_to_end(&path).await.expect("success"); + assert_eq!(b"test", &result[..]); + } } diff --git a/aws/sdk/aws-models/sso-oidc.json b/aws/sdk/aws-models/sso-oidc.json new file mode 100644 index 0000000000..21fa139b31 --- /dev/null +++ b/aws/sdk/aws-models/sso-oidc.json @@ -0,0 +1,1590 @@ +{ + "smithy": "2.0", + "metadata": { + "suppressions": [ + { + "id": "HttpMethodSemantics", + "namespace": "*" + }, + { + "id": "HttpResponseCodeSemantics", + "namespace": "*" + }, + { + "id": "PaginatedTrait", + "namespace": "*" + }, + { + "id": "HttpHeaderTrait", + "namespace": "*" + }, + { + "id": "HttpUriConflict", + "namespace": "*" + }, + { + "id": "Service", + "namespace": "*" + } + ] + }, + "shapes": { + "com.amazonaws.ssooidc#AWSSSOOIDCService": { + "type": "service", + "version": "2019-06-10", + "operations": [ + { + "target": "com.amazonaws.ssooidc#CreateToken" + }, + { + "target": "com.amazonaws.ssooidc#RegisterClient" + }, + { + "target": "com.amazonaws.ssooidc#StartDeviceAuthorization" + } + ], + "traits": { + "aws.api#service": { + "sdkId": "SSO OIDC", + "arnNamespace": "awsssooidc", + "cloudFormationName": "SSOOIDC", + "cloudTrailEventSource": "ssooidc.amazonaws.com", + "endpointPrefix": "oidc" + }, + "aws.auth#sigv4": { + "name": "awsssooidc" + }, + "aws.protocols#restJson1": {}, + "smithy.api#documentation": "

AWS IAM Identity Center (successor to AWS Single Sign-On) OpenID Connect (OIDC) is a web service that enables a client (such as AWS CLI\n or a native application) to register with IAM Identity Center. The service also enables the client to\n fetch the user’s access token upon successful authentication and authorization with\n IAM Identity Center.

\n \n

Although AWS Single Sign-On was renamed, the sso and\n identitystore API namespaces will continue to retain their original name for\n backward compatibility purposes. For more information, see IAM Identity Center rename.

\n
\n

\n Considerations for Using This Guide\n

\n

Before you begin using this guide, we recommend that you first review the following\n important information about how the IAM Identity Center OIDC service works.

\n
    \n
  • \n

    The IAM Identity Center OIDC service currently implements only the portions of the OAuth 2.0\n Device Authorization Grant standard (https://tools.ietf.org/html/rfc8628) that are necessary to enable single\n sign-on authentication with the AWS CLI. Support for other OIDC flows frequently needed\n for native applications, such as Authorization Code Flow (+ PKCE), will be addressed in\n future releases.

    \n
  • \n
  • \n

    The service emits only OIDC access tokens, such that obtaining a new token (For\n example, token refresh) requires explicit user re-authentication.

    \n
  • \n
  • \n

    The access tokens provided by this service grant access to all AWS account\n entitlements assigned to an IAM Identity Center user, not just a particular application.

    \n
  • \n
  • \n

    The documentation in this guide does not describe the mechanism to convert the access\n token into AWS Auth (“sigv4”) credentials for use with IAM-protected AWS service\n endpoints. For more information, see GetRoleCredentials in the IAM Identity Center Portal API Reference\n Guide.

    \n
  • \n
\n\n

For general information about IAM Identity Center, see What is\n IAM Identity Center? in the IAM Identity Center User Guide.

", + "smithy.api#title": "AWS SSO OIDC", + "smithy.rules#endpointRuleSet": { + "version": "1.0", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "String" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "Boolean" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "Boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "String" + } + }, + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "error": "Invalid Configuration: FIPS and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": { + "ref": "Endpoint" + }, + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ] + } + ] + }, + { + "conditions": [], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + } + ], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [], + "type": "tree", + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://oidc-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ] + } + ] + }, + { + "conditions": [], + "error": "FIPS and DualStack are enabled, but this partition does not support one or both", + "type": "error" + } + ] + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + } + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [], + "type": "tree", + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://oidc-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ] + } + ] + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ] + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "type": "tree", + "rules": [ + { + "conditions": [], + "type": "tree", + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://oidc.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ] + } + ] + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ] + }, + { + "conditions": [], + "type": "tree", + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://oidc.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ] + } + ] + } + ] + }, + { + "conditions": [], + "error": "Invalid Configuration: Missing Region", + "type": "error" + } + ] + } + ] + }, + "smithy.rules#endpointTests": { + "testCases": [ + { + "documentation": "For region ap-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ap-east-1.amazonaws.com" + } + }, + "params": { + "Region": "ap-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-northeast-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ap-northeast-1.amazonaws.com" + } + }, + "params": { + "Region": "ap-northeast-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-northeast-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ap-northeast-2.amazonaws.com" + } + }, + "params": { + "Region": "ap-northeast-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-northeast-3 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ap-northeast-3.amazonaws.com" + } + }, + "params": { + "Region": "ap-northeast-3", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-south-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ap-south-1.amazonaws.com" + } + }, + "params": { + "Region": "ap-south-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-southeast-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ap-southeast-1.amazonaws.com" + } + }, + "params": { + "Region": "ap-southeast-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-southeast-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ap-southeast-2.amazonaws.com" + } + }, + "params": { + "Region": "ap-southeast-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ca-central-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.ca-central-1.amazonaws.com" + } + }, + "params": { + "Region": "ca-central-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-central-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.eu-central-1.amazonaws.com" + } + }, + "params": { + "Region": "eu-central-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-north-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.eu-north-1.amazonaws.com" + } + }, + "params": { + "Region": "eu-north-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-south-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.eu-south-1.amazonaws.com" + } + }, + "params": { + "Region": "eu-south-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-west-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.eu-west-1.amazonaws.com" + } + }, + "params": { + "Region": "eu-west-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-west-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.eu-west-2.amazonaws.com" + } + }, + "params": { + "Region": "eu-west-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-west-3 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.eu-west-3.amazonaws.com" + } + }, + "params": { + "Region": "eu-west-3", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region me-south-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.me-south-1.amazonaws.com" + } + }, + "params": { + "Region": "me-south-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region sa-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.sa-east-1.amazonaws.com" + } + }, + "params": { + "Region": "sa-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-east-2.amazonaws.com" + } + }, + "params": { + "Region": "us-east-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-west-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.cn-north-1.api.amazonwebservices.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.cn-north-1.amazonaws.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region cn-north-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://oidc.cn-north-1.api.amazonwebservices.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.cn-north-1.amazonaws.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-gov-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-west-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-gov-west-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-west-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.us-gov-east-1.api.aws" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.us-gov-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-gov-east-1.api.aws" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "error": "FIPS and DualStack are enabled, but this partition does not support one or both" + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "error": "DualStack is enabled but this partition does not support DualStack" + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "error": "FIPS and DualStack are enabled, but this partition does not support one or both" + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc-fips.us-isob-east-1.sc2s.sgov.gov" + } + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "error": "DualStack is enabled but this partition does not support DualStack" + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://oidc.us-isob-east-1.sc2s.sgov.gov" + } + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For custom endpoint with region set and fips disabled and dualstack disabled", + "expect": { + "endpoint": { + "url": "https://example.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with region not set and fips disabled and dualstack disabled", + "expect": { + "endpoint": { + "url": "https://example.com" + } + }, + "params": { + "UseFIPS": false, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with fips enabled and dualstack disabled", + "expect": { + "error": "Invalid Configuration: FIPS and custom endpoint are not supported" + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with fips disabled and dualstack enabled", + "expect": { + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported" + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "Missing region", + "expect": { + "error": "Invalid Configuration: Missing Region" + } + } + ], + "version": "1.0" + } + } + }, + "com.amazonaws.ssooidc#AccessDeniedException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

You do not have sufficient access to perform this action.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#AccessToken": { + "type": "string" + }, + "com.amazonaws.ssooidc#AuthCode": { + "type": "string" + }, + "com.amazonaws.ssooidc#AuthorizationPendingException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that a request to authorize a client with an access user session token is\n pending.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#ClientId": { + "type": "string" + }, + "com.amazonaws.ssooidc#ClientName": { + "type": "string" + }, + "com.amazonaws.ssooidc#ClientSecret": { + "type": "string" + }, + "com.amazonaws.ssooidc#ClientType": { + "type": "string" + }, + "com.amazonaws.ssooidc#CreateToken": { + "type": "operation", + "input": { + "target": "com.amazonaws.ssooidc#CreateTokenRequest" + }, + "output": { + "target": "com.amazonaws.ssooidc#CreateTokenResponse" + }, + "errors": [ + { + "target": "com.amazonaws.ssooidc#AccessDeniedException" + }, + { + "target": "com.amazonaws.ssooidc#AuthorizationPendingException" + }, + { + "target": "com.amazonaws.ssooidc#ExpiredTokenException" + }, + { + "target": "com.amazonaws.ssooidc#InternalServerException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidClientException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidGrantException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidRequestException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidScopeException" + }, + { + "target": "com.amazonaws.ssooidc#SlowDownException" + }, + { + "target": "com.amazonaws.ssooidc#UnauthorizedClientException" + }, + { + "target": "com.amazonaws.ssooidc#UnsupportedGrantTypeException" + } + ], + "traits": { + "smithy.api#auth": [], + "smithy.api#documentation": "

Creates and returns an access token for the authorized client. The access token issued\n will be used to fetch short-term credentials for the assigned roles in the AWS\n account.

", + "smithy.api#http": { + "method": "POST", + "uri": "/token", + "code": 200 + }, + "smithy.api#optionalAuth": {} + } + }, + "com.amazonaws.ssooidc#CreateTokenRequest": { + "type": "structure", + "members": { + "clientId": { + "target": "com.amazonaws.ssooidc#ClientId", + "traits": { + "smithy.api#documentation": "

The unique identifier string for each client. This value should come from the persisted\n result of the RegisterClient API.

", + "smithy.api#required": {} + } + }, + "clientSecret": { + "target": "com.amazonaws.ssooidc#ClientSecret", + "traits": { + "smithy.api#documentation": "

A secret string generated for the client. This value should come from the persisted result\n of the RegisterClient API.

", + "smithy.api#required": {} + } + }, + "grantType": { + "target": "com.amazonaws.ssooidc#GrantType", + "traits": { + "smithy.api#documentation": "

Supports grant types for the authorization code, refresh token, and device code request.\n For device code requests, specify the following value:

\n\n

\n urn:ietf:params:oauth:grant-type:device_code\n \n

\n\n

For information about how to obtain the device code, see the StartDeviceAuthorization topic.

", + "smithy.api#required": {} + } + }, + "deviceCode": { + "target": "com.amazonaws.ssooidc#DeviceCode", + "traits": { + "smithy.api#documentation": "

Used only when calling this API for the device code grant type. This short-term code is\n used to identify this authentication attempt. This should come from an in-memory reference to\n the result of the StartDeviceAuthorization API.

" + } + }, + "code": { + "target": "com.amazonaws.ssooidc#AuthCode", + "traits": { + "smithy.api#documentation": "

The authorization code received from the authorization service. This parameter is required\n to perform an authorization grant request to get access to a token.

" + } + }, + "refreshToken": { + "target": "com.amazonaws.ssooidc#RefreshToken", + "traits": { + "smithy.api#documentation": "

Currently, refreshToken is not yet implemented and is not supported. For more\n information about the features and limitations of the current IAM Identity Center OIDC implementation,\n see Considerations for Using this Guide in the IAM Identity Center\n OIDC API Reference.

\n

The token used to obtain an access token in the event that the access token is invalid or\n expired.

" + } + }, + "scope": { + "target": "com.amazonaws.ssooidc#Scopes", + "traits": { + "smithy.api#documentation": "

The list of scopes that is defined by the client. Upon authorization, this list is used to\n restrict permissions when granting an access token.

" + } + }, + "redirectUri": { + "target": "com.amazonaws.ssooidc#URI", + "traits": { + "smithy.api#documentation": "

The location of the application that will receive the authorization code. Users authorize\n the service to send the request to this location.

" + } + } + } + }, + "com.amazonaws.ssooidc#CreateTokenResponse": { + "type": "structure", + "members": { + "accessToken": { + "target": "com.amazonaws.ssooidc#AccessToken", + "traits": { + "smithy.api#documentation": "

An opaque token to access IAM Identity Center resources assigned to a user.

" + } + }, + "tokenType": { + "target": "com.amazonaws.ssooidc#TokenType", + "traits": { + "smithy.api#documentation": "

Used to notify the client that the returned token is an access token. The supported type\n is BearerToken.

" + } + }, + "expiresIn": { + "target": "com.amazonaws.ssooidc#ExpirationInSeconds", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

Indicates the time in seconds when an access token will expire.

" + } + }, + "refreshToken": { + "target": "com.amazonaws.ssooidc#RefreshToken", + "traits": { + "smithy.api#documentation": "

Currently, refreshToken is not yet implemented and is not supported. For more\n information about the features and limitations of the current IAM Identity Center OIDC implementation,\n see Considerations for Using this Guide in the IAM Identity Center\n OIDC API Reference.

\n

A token that, if present, can be used to refresh a previously issued access token that\n might have expired.

" + } + }, + "idToken": { + "target": "com.amazonaws.ssooidc#IdToken", + "traits": { + "smithy.api#documentation": "

Currently, idToken is not yet implemented and is not supported. For more\n information about the features and limitations of the current IAM Identity Center OIDC implementation,\n see Considerations for Using this Guide in the IAM Identity Center\n OIDC API Reference.

\n

The identifier of the user that associated with the access token, if present.

" + } + } + } + }, + "com.amazonaws.ssooidc#DeviceCode": { + "type": "string" + }, + "com.amazonaws.ssooidc#Error": { + "type": "string" + }, + "com.amazonaws.ssooidc#ErrorDescription": { + "type": "string" + }, + "com.amazonaws.ssooidc#ExpirationInSeconds": { + "type": "integer", + "traits": { + "smithy.api#default": 0 + } + }, + "com.amazonaws.ssooidc#ExpiredTokenException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the token issued by the service is expired and is no longer valid.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#GrantType": { + "type": "string" + }, + "com.amazonaws.ssooidc#IdToken": { + "type": "string" + }, + "com.amazonaws.ssooidc#InternalServerException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that an error from the service occurred while trying to process a\n request.

", + "smithy.api#error": "server", + "smithy.api#httpError": 500 + } + }, + "com.amazonaws.ssooidc#IntervalInSeconds": { + "type": "integer", + "traits": { + "smithy.api#default": 0 + } + }, + "com.amazonaws.ssooidc#InvalidClientException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the clientId or clientSecret in the request is\n invalid. For example, this can occur when a client sends an incorrect clientId or\n an expired clientSecret.

", + "smithy.api#error": "client", + "smithy.api#httpError": 401 + } + }, + "com.amazonaws.ssooidc#InvalidClientMetadataException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the client information sent in the request during registration is\n invalid.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#InvalidGrantException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that a request contains an invalid grant. This can occur if a client makes a\n CreateToken request with an invalid grant type.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#InvalidRequestException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that something is wrong with the input to the request. For example, a required\n parameter might be missing or out of range.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#InvalidScopeException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the scope provided in the request is invalid.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#LongTimeStampType": { + "type": "long", + "traits": { + "smithy.api#default": 0 + } + }, + "com.amazonaws.ssooidc#RefreshToken": { + "type": "string" + }, + "com.amazonaws.ssooidc#RegisterClient": { + "type": "operation", + "input": { + "target": "com.amazonaws.ssooidc#RegisterClientRequest" + }, + "output": { + "target": "com.amazonaws.ssooidc#RegisterClientResponse" + }, + "errors": [ + { + "target": "com.amazonaws.ssooidc#InternalServerException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidClientMetadataException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidRequestException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidScopeException" + } + ], + "traits": { + "smithy.api#auth": [], + "smithy.api#documentation": "

Registers a client with IAM Identity Center. This allows clients to initiate device authorization.\n The output should be persisted for reuse through many authentication requests.

", + "smithy.api#http": { + "method": "POST", + "uri": "/client/register", + "code": 200 + }, + "smithy.api#optionalAuth": {} + } + }, + "com.amazonaws.ssooidc#RegisterClientRequest": { + "type": "structure", + "members": { + "clientName": { + "target": "com.amazonaws.ssooidc#ClientName", + "traits": { + "smithy.api#documentation": "

The friendly name of the client.

", + "smithy.api#required": {} + } + }, + "clientType": { + "target": "com.amazonaws.ssooidc#ClientType", + "traits": { + "smithy.api#documentation": "

The type of client. The service supports only public as a client type.\n Anything other than public will be rejected by the service.

", + "smithy.api#required": {} + } + }, + "scopes": { + "target": "com.amazonaws.ssooidc#Scopes", + "traits": { + "smithy.api#documentation": "

The list of scopes that are defined by the client. Upon authorization, this list is used\n to restrict permissions when granting an access token.

" + } + } + } + }, + "com.amazonaws.ssooidc#RegisterClientResponse": { + "type": "structure", + "members": { + "clientId": { + "target": "com.amazonaws.ssooidc#ClientId", + "traits": { + "smithy.api#documentation": "

The unique identifier string for each client. This client uses this identifier to get\n authenticated by the service in subsequent calls.

" + } + }, + "clientSecret": { + "target": "com.amazonaws.ssooidc#ClientSecret", + "traits": { + "smithy.api#documentation": "

A secret string generated for the client. The client will use this string to get\n authenticated by the service in subsequent calls.

" + } + }, + "clientIdIssuedAt": { + "target": "com.amazonaws.ssooidc#LongTimeStampType", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

Indicates the time at which the clientId and clientSecret were\n issued.

" + } + }, + "clientSecretExpiresAt": { + "target": "com.amazonaws.ssooidc#LongTimeStampType", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

Indicates the time at which the clientId and clientSecret will\n become invalid.

" + } + }, + "authorizationEndpoint": { + "target": "com.amazonaws.ssooidc#URI", + "traits": { + "smithy.api#documentation": "

The endpoint where the client can request authorization.

" + } + }, + "tokenEndpoint": { + "target": "com.amazonaws.ssooidc#URI", + "traits": { + "smithy.api#documentation": "

The endpoint where the client can get an access token.

" + } + } + } + }, + "com.amazonaws.ssooidc#Scope": { + "type": "string" + }, + "com.amazonaws.ssooidc#Scopes": { + "type": "list", + "member": { + "target": "com.amazonaws.ssooidc#Scope" + } + }, + "com.amazonaws.ssooidc#SlowDownException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the client is making the request too frequently and is more than the\n service can handle.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#StartDeviceAuthorization": { + "type": "operation", + "input": { + "target": "com.amazonaws.ssooidc#StartDeviceAuthorizationRequest" + }, + "output": { + "target": "com.amazonaws.ssooidc#StartDeviceAuthorizationResponse" + }, + "errors": [ + { + "target": "com.amazonaws.ssooidc#InternalServerException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidClientException" + }, + { + "target": "com.amazonaws.ssooidc#InvalidRequestException" + }, + { + "target": "com.amazonaws.ssooidc#SlowDownException" + }, + { + "target": "com.amazonaws.ssooidc#UnauthorizedClientException" + } + ], + "traits": { + "smithy.api#auth": [], + "smithy.api#documentation": "

Initiates device authorization by requesting a pair of verification codes from the\n authorization service.

", + "smithy.api#http": { + "method": "POST", + "uri": "/device_authorization", + "code": 200 + }, + "smithy.api#optionalAuth": {} + } + }, + "com.amazonaws.ssooidc#StartDeviceAuthorizationRequest": { + "type": "structure", + "members": { + "clientId": { + "target": "com.amazonaws.ssooidc#ClientId", + "traits": { + "smithy.api#documentation": "

The unique identifier string for the client that is registered with IAM Identity Center. This value\n should come from the persisted result of the RegisterClient API\n operation.

", + "smithy.api#required": {} + } + }, + "clientSecret": { + "target": "com.amazonaws.ssooidc#ClientSecret", + "traits": { + "smithy.api#documentation": "

A secret string that is generated for the client. This value should come from the\n persisted result of the RegisterClient API operation.

", + "smithy.api#required": {} + } + }, + "startUrl": { + "target": "com.amazonaws.ssooidc#URI", + "traits": { + "smithy.api#documentation": "

The URL for the AWS access portal. For more information, see Using\n the AWS access portal in the IAM Identity Center User Guide.

", + "smithy.api#required": {} + } + } + } + }, + "com.amazonaws.ssooidc#StartDeviceAuthorizationResponse": { + "type": "structure", + "members": { + "deviceCode": { + "target": "com.amazonaws.ssooidc#DeviceCode", + "traits": { + "smithy.api#documentation": "

The short-lived code that is used by the device when polling for a session token.

" + } + }, + "userCode": { + "target": "com.amazonaws.ssooidc#UserCode", + "traits": { + "smithy.api#documentation": "

A one-time user verification code. This is needed to authorize an in-use device.

" + } + }, + "verificationUri": { + "target": "com.amazonaws.ssooidc#URI", + "traits": { + "smithy.api#documentation": "

The URI of the verification page that takes the userCode to authorize the\n device.

" + } + }, + "verificationUriComplete": { + "target": "com.amazonaws.ssooidc#URI", + "traits": { + "smithy.api#documentation": "

An alternate URL that the client can use to automatically launch a browser. This process\n skips the manual step in which the user visits the verification page and enters their\n code.

" + } + }, + "expiresIn": { + "target": "com.amazonaws.ssooidc#ExpirationInSeconds", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

Indicates the number of seconds in which the verification code will become invalid.

" + } + }, + "interval": { + "target": "com.amazonaws.ssooidc#IntervalInSeconds", + "traits": { + "smithy.api#default": 0, + "smithy.api#documentation": "

Indicates the number of seconds the client must wait between attempts when polling for a\n session.

" + } + } + } + }, + "com.amazonaws.ssooidc#TokenType": { + "type": "string" + }, + "com.amazonaws.ssooidc#URI": { + "type": "string" + }, + "com.amazonaws.ssooidc#UnauthorizedClientException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the client is not currently authorized to make the request. This can happen\n when a clientId is not issued for a public client.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#UnsupportedGrantTypeException": { + "type": "structure", + "members": { + "error": { + "target": "com.amazonaws.ssooidc#Error" + }, + "error_description": { + "target": "com.amazonaws.ssooidc#ErrorDescription" + } + }, + "traits": { + "smithy.api#documentation": "

Indicates that the grant type in the request is not supported by the service.

", + "smithy.api#error": "client", + "smithy.api#httpError": 400 + } + }, + "com.amazonaws.ssooidc#UserCode": { + "type": "string" + } + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs index 27c4accf4e..61d46a3c92 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs @@ -5,6 +5,7 @@ use crate::client::auth::AuthSchemeId; use crate::client::orchestrator::Future; +use crate::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::ConfigBag; use std::any::Any; use std::fmt; @@ -28,7 +29,11 @@ pub mod http; /// There is no fallback to other auth schemes in the absense of an identity. pub trait IdentityResolver: Send + Sync + Debug { /// Asynchronously resolves an identity for a request using the given config. - fn resolve_identity(&self, config_bag: &ConfigBag) -> Future; + fn resolve_identity( + &self, + runtime_components: &RuntimeComponents, + config_bag: &ConfigBag, + ) -> Future; } /// Container for a shared identity resolver. @@ -43,8 +48,12 @@ impl SharedIdentityResolver { } impl IdentityResolver for SharedIdentityResolver { - fn resolve_identity(&self, config_bag: &ConfigBag) -> Future { - self.0.resolve_identity(config_bag) + fn resolve_identity( + &self, + runtime_components: &RuntimeComponents, + config_bag: &ConfigBag, + ) -> Future { + self.0.resolve_identity(runtime_components, config_bag) } } diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/identity/http.rs b/rust-runtime/aws-smithy-runtime-api/src/client/identity/http.rs index 878943c545..96c809d2af 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/identity/http.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/identity/http.rs @@ -7,6 +7,7 @@ use crate::client::identity::{Identity, IdentityResolver}; use crate::client::orchestrator::Future; +use crate::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::ConfigBag; use std::fmt::Debug; use std::sync::Arc; @@ -65,7 +66,11 @@ impl From for Token { } impl IdentityResolver for Token { - fn resolve_identity(&self, _config_bag: &ConfigBag) -> Future { + fn resolve_identity( + &self, + _runtime_components: &RuntimeComponents, + _config_bag: &ConfigBag, + ) -> Future { Future::ready(Ok(Identity::new(self.clone(), self.0.expiration))) } } @@ -124,7 +129,11 @@ impl Login { } impl IdentityResolver for Login { - fn resolve_identity(&self, _config_bag: &ConfigBag) -> Future { + fn resolve_identity( + &self, + _runtime_components: &RuntimeComponents, + _config_bag: &ConfigBag, + ) -> Future { Future::ready(Ok(Identity::new(self.clone(), self.0.expiration))) } } diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs b/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs index 520c164e74..9c612fd437 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs @@ -576,7 +576,7 @@ impl RuntimeComponentsBuilder { #[derive(Debug)] struct FakeIdentityResolver; impl IdentityResolver for FakeIdentityResolver { - fn resolve_identity(&self, _: &ConfigBag) -> Future { + fn resolve_identity(&self, _: &RuntimeComponents, _: &ConfigBag) -> Future { unreachable!("fake identity resolver must be overridden for this test") } } diff --git a/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs b/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs index 1e0efcd1c4..bb7c073e02 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs @@ -389,4 +389,30 @@ mod tests { request.headers().get("Authorization").unwrap() ); } + + #[test] + fn test_bearer_auth_overwrite_existing_header() { + let signer = BearerAuthSigner; + + let config_bag = ConfigBag::base(); + let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let identity = Identity::new(Token::new("some-token", None), None); + let mut request = http::Request::builder() + .header("Authorization", "wrong") + .body(SdkBody::empty()) + .unwrap(); + signer + .sign_http_request( + &mut request, + &identity, + AuthSchemeEndpointConfig::empty(), + &runtime_components, + &config_bag, + ) + .expect("success"); + assert_eq!( + "Bearer some-token", + request.headers().get("Authorization").unwrap() + ); + } } diff --git a/rust-runtime/aws-smithy-runtime/src/client/identity/no_auth.rs b/rust-runtime/aws-smithy-runtime/src/client/identity/no_auth.rs index 01121eab90..48d11d41fa 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/identity/no_auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/identity/no_auth.rs @@ -5,6 +5,7 @@ use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver}; use aws_smithy_runtime_api::client::orchestrator::Future; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::ConfigBag; /// Identity for the [`NoAuthScheme`](crate::client::auth::no_auth::NoAuthScheme) auth scheme. @@ -30,7 +31,11 @@ impl NoAuthIdentityResolver { } impl IdentityResolver for NoAuthIdentityResolver { - fn resolve_identity(&self, _: &ConfigBag) -> Future { + fn resolve_identity( + &self, + _runtime_components: &RuntimeComponents, + _: &ConfigBag, + ) -> Future { Future::ready(Ok(Identity::new(NoAuthIdentity::new(), None))) } } diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs index ae17780979..56b36b3c8f 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs @@ -101,7 +101,9 @@ pub(super) async fn orchestrate_auth( extract_endpoint_auth_scheme_config(endpoint, scheme_id)?; trace!(auth_scheme_endpoint_config = ?auth_scheme_endpoint_config, "extracted auth scheme endpoint config"); - let identity = identity_resolver.resolve_identity(cfg).await?; + let identity = identity_resolver + .resolve_identity(runtime_components, cfg) + .await?; trace!(identity = ?identity, "resolved identity"); trace!("signing request"); @@ -170,7 +172,7 @@ mod tests { use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext}; use aws_smithy_runtime_api::client::orchestrator::{Future, HttpRequest}; use aws_smithy_runtime_api::client::runtime_components::{ - GetIdentityResolver, RuntimeComponentsBuilder, + GetIdentityResolver, RuntimeComponents, RuntimeComponentsBuilder, }; use aws_smithy_types::config_bag::Layer; use std::collections::HashMap; @@ -180,7 +182,11 @@ mod tests { #[derive(Debug)] struct TestIdentityResolver; impl IdentityResolver for TestIdentityResolver { - fn resolve_identity(&self, _config_bag: &ConfigBag) -> Future { + fn resolve_identity( + &self, + _runtime_components: &RuntimeComponents, + _config_bag: &ConfigBag, + ) -> Future { Future::ready(Ok(Identity::new("doesntmatter", None))) } } From 04a5d728345fd5143618e46281b87fc159b0b4ec Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Fri, 11 Aug 2023 16:56:43 -0700 Subject: [PATCH 02/12] Update changelog --- CHANGELOG.next.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 8cf6a7fb2a..a262bc217c 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -11,6 +11,18 @@ # meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"} # author = "rcoh" +[[aws-sdk-rust]] +message = "(Behavior Break!) The SSO credentials provider is no longer enabled by default in `aws-config`, and so SSO profile config will no longer work out of box. The `credentials-sso` feature in `aws-config` was removed from the default features, and renamed to `sso`. If you need credentials from SSO, then enable the `sso` feature in `aws-config`." +references = ["smithy-rs#2917"] +meta = { "breaking" = true, "tada" = false, "bug" = false } +author = "jdisanti" + +[[aws-sdk-rust]] +message = "The `SsoCredentialsProvider` now supports token refresh and is compatible with the token cache file paths the latest AWS CLI uses." +references = ["smithy-rs#2917", "aws-sdk-rust#703", "aws-sdk-rust#699"] +meta = { "breaking" = false, "tada" = true, "bug" = false } +author = "jdisanti" + [[aws-sdk-rust]] message = "`RuntimeComponents` are now re-exported so that implementing a custom interceptor doens't require directly depending on `aws-smithy-runtime-api`." references = ["smithy-rs#2904", "aws-sdk-rust#862"] @@ -28,3 +40,9 @@ message = "Fix requests to S3 with `no_credentials` set." references = ["smithy-rs#2907", "aws-sdk-rust#864"] meta = { "breaking" = false, "tada" = false, "bug" = true } author = "jdisanti" + +[[smithy-rs]] +message = "`RuntimeComponents` have been added as an argument to the `IdentityResolver::resolve_identity` trait function." +references = ["smithy-rs#2917"] +meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client"} +author = "jdisanti" From 149ac97543acb1e11031acefaf46e34e0fa61a9f Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 14 Aug 2023 10:15:44 -0700 Subject: [PATCH 03/12] Fix dependency cycle issue --- .../src/main/kotlin/software/amazon/smithy/rustsdk/AwsDocs.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsDocs.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsDocs.kt index 7166ce1144..5411fddff2 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsDocs.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsDocs.kt @@ -22,8 +22,9 @@ object AwsDocs { fun canRelyOnAwsConfig(codegenContext: ClientCodegenContext): Boolean = SdkSettings.from(codegenContext.settings).awsConfigVersion != null && !setOf( - ShapeId.from("com.amazonaws.sts#AWSSecurityTokenServiceV20110615"), ShapeId.from("com.amazonaws.sso#SWBPortalService"), + ShapeId.from("com.amazonaws.ssooidc#AWSSSOOIDCService"), + ShapeId.from("com.amazonaws.sts#AWSSecurityTokenServiceV20110615"), ).contains(codegenContext.serviceShape.id) fun constructClient(codegenContext: ClientCodegenContext, indent: String): Writable { From 8174f002db6ed8e9c0aa853db606456267aabeed Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 14 Aug 2023 10:45:54 -0700 Subject: [PATCH 04/12] Fix TLS test --- .github/workflows/ci-tls.yml | 118 +++++++++++++++++------------------ 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/.github/workflows/ci-tls.yml b/.github/workflows/ci-tls.yml index 324b23383c..68a8867405 100644 --- a/.github/workflows/ci-tls.yml +++ b/.github/workflows/ci-tls.yml @@ -20,62 +20,62 @@ jobs: name: Verify TLS configuration runs-on: ubuntu-latest steps: - - name: Install packages - shell: bash - run: | - sudo apt-get update - sudo apt-get -y install gcc make python3-pip nginx git ruby openjdk-17-jre pkg-config libssl-dev faketime - pip3 install certbuilder crlbuilder - - name: Stop nginx - run: sudo systemctl stop nginx - - name: Checkout smithy-rs - uses: actions/checkout@v3 - with: - path: ./smithy-rs - - name: Checkout trytls - uses: actions/checkout@v3 - with: - repository: ouspg/trytls - path: ./trytls - - name: Checkout badtls - uses: actions/checkout@v3 - with: - repository: wbond/badtls.io - path: ./badtls.io - - name: Checkout badssl - uses: actions/checkout@v3 - with: - repository: chromium/badssl.com - path: ./badssl.com - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.rust_version }} - - name: Build badssl.com - shell: bash - working-directory: badssl.com - env: - DOCKER_BUILDKIT: 1 - run: ../smithy-rs/tools/ci-scripts/configure-tls/configure-badssl - - name: Build SDK - working-directory: smithy-rs - run: ./gradlew :aws:sdk:assemble -Paws.services=+sts,+sso - - name: Build trytls - shell: bash - working-directory: trytls - run: ../smithy-rs/tools/ci-scripts/configure-tls/configure-trytls - - name: Build badtls.io - working-directory: badtls.io - shell: bash - run: ../smithy-rs/tools/ci-scripts/configure-tls/configure-badtls - - name: Update TLS configuration - shell: bash - run: smithy-rs/tools/ci-scripts/configure-tls/update-certs - - name: Build TLS stub - working-directory: smithy-rs/tools/ci-resources/tls-stub - shell: bash - run: cargo build - - name: Test TLS configuration - working-directory: smithy-rs/tools - shell: bash - run: trytls https target/debug/stub + - name: Install packages + shell: bash + run: | + sudo apt-get update + sudo apt-get -y install gcc make python3-pip nginx git ruby openjdk-17-jre pkg-config libssl-dev faketime + pip3 install certbuilder crlbuilder + - name: Stop nginx + run: sudo systemctl stop nginx + - name: Checkout smithy-rs + uses: actions/checkout@v3 + with: + path: ./smithy-rs + - name: Checkout trytls + uses: actions/checkout@v3 + with: + repository: ouspg/trytls + path: ./trytls + - name: Checkout badtls + uses: actions/checkout@v3 + with: + repository: wbond/badtls.io + path: ./badtls.io + - name: Checkout badssl + uses: actions/checkout@v3 + with: + repository: chromium/badssl.com + path: ./badssl.com + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.rust_version }} + - name: Build badssl.com + shell: bash + working-directory: badssl.com + env: + DOCKER_BUILDKIT: 1 + run: ../smithy-rs/tools/ci-scripts/configure-tls/configure-badssl + - name: Build SDK + working-directory: smithy-rs + run: ./gradlew :aws:sdk:assemble -Paws.services=+sts,+sso,+ssooidc + - name: Build trytls + shell: bash + working-directory: trytls + run: ../smithy-rs/tools/ci-scripts/configure-tls/configure-trytls + - name: Build badtls.io + working-directory: badtls.io + shell: bash + run: ../smithy-rs/tools/ci-scripts/configure-tls/configure-badtls + - name: Update TLS configuration + shell: bash + run: smithy-rs/tools/ci-scripts/configure-tls/update-certs + - name: Build TLS stub + working-directory: smithy-rs/tools/ci-resources/tls-stub + shell: bash + run: cargo build + - name: Test TLS configuration + working-directory: smithy-rs/tools + shell: bash + run: trytls https target/debug/stub From cfca1d74fd227d6f585cf8b9d6affcbf21a331f7 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 14 Aug 2023 11:04:08 -0700 Subject: [PATCH 05/12] Fix compilation issue --- aws/rust-runtime/aws-config/src/sso/token.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws/rust-runtime/aws-config/src/sso/token.rs b/aws/rust-runtime/aws-config/src/sso/token.rs index 026510872e..db5e9f5c14 100644 --- a/aws/rust-runtime/aws-config/src/sso/token.rs +++ b/aws/rust-runtime/aws-config/src/sso/token.rs @@ -398,7 +398,7 @@ mod tests { use aws_smithy_types::DateTime; fn time(s: &str) -> SystemTime { - SystemTime::try_from(DateTime::parse(Format::DateTime, s).unwrap()).unwrap() + SystemTime::try_from(DateTime::from_str(s, Format::DateTime).unwrap()).unwrap() } struct TestHarness { @@ -736,7 +736,7 @@ mod tests { async fn refresh_timings() { let _logs = capture_test_logs(); - let start_time = DateTime::parse(Format::DateTime, "2023-01-01T00:00:00Z").unwrap(); + let start_time = DateTime::from_str("2023-01-01T00:00:00Z", Format::DateTime).unwrap(); let (time_source, sleep_impl) = instant_time_and_sleep(start_time.try_into().unwrap()); let shared_time_source = SharedTimeSource::new(time_source.clone()); From b91242efeb86c48f545449eeb575a48ab255957d Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 14 Aug 2023 13:37:08 -0700 Subject: [PATCH 06/12] Fix external types check --- .../aws-config/external-types.toml | 1 + aws/rust-runtime/aws-config/src/sso/token.rs | 34 ++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/aws/rust-runtime/aws-config/external-types.toml b/aws/rust-runtime/aws-config/external-types.toml index b90a84885c..242896968d 100644 --- a/aws/rust-runtime/aws-config/external-types.toml +++ b/aws/rust-runtime/aws-config/external-types.toml @@ -20,6 +20,7 @@ allowed_external_types = [ "aws_smithy_http::endpoint", "aws_smithy_http::endpoint::error::InvalidEndpointError", "aws_smithy_http::result::SdkError", + "aws_smithy_runtime_api::*", "aws_smithy_types::retry", "aws_smithy_types::retry::*", "aws_smithy_types::timeout", diff --git a/aws/rust-runtime/aws-config/src/sso/token.rs b/aws/rust-runtime/aws-config/src/sso/token.rs index db5e9f5c14..a08728fcb7 100644 --- a/aws/rust-runtime/aws-config/src/sso/token.rs +++ b/aws/rust-runtime/aws-config/src/sso/token.rs @@ -10,6 +10,8 @@ //! //! This provider is included automatically when profiles are loaded. +use crate::connector::expect_connector; +use crate::provider_config::ProviderConfig; use crate::sso::cache::{ load_cached_token, save_cached_token, CachedSsoToken, CachedSsoTokenError, }; @@ -24,6 +26,7 @@ use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver}; use aws_smithy_runtime_api::client::orchestrator::Future; use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::ConfigBag; +use aws_smithy_types::retry::RetryConfig; use aws_types::os_shim_internal::{Env, Fs}; use aws_types::region::Region; use std::error::Error as StdError; @@ -246,6 +249,7 @@ impl IdentityResolver for SsoTokenProvider { /// Builder for [`SsoTokenProvider`]. #[derive(Debug, Default)] pub struct Builder { + provider_config: Option, region: Option, session_name: Option, start_url: Option, @@ -258,6 +262,12 @@ impl Builder { Default::default() } + /// Override the configuration used for this provider + pub fn configure(mut self, provider_config: &ProviderConfig) -> Self { + self.provider_config = Some(provider_config.clone()); + self + } + /// Sets the SSO region. /// /// This is a required field. @@ -307,17 +317,12 @@ impl Builder { } /// Sets the SSO OIDC client config. - pub fn sso_oidc_config(mut self, config: SsoOidcConfigBuilder) -> Self { + #[cfg(test)] + pub(crate) fn sso_oidc_config(mut self, config: SsoOidcConfigBuilder) -> Self { self.sso_oidc_config = Some(config); self } - /// Sets the SSO OIDC client config. - pub fn set_sso_oidc_config(&mut self, config: Option) -> &mut Self { - self.sso_oidc_config = config; - self - } - /// Builds the [`SsoTokenProvider`]. /// /// # Panics @@ -328,6 +333,19 @@ impl Builder { } fn build_with(self, env: Env, fs: Fs) -> SsoTokenProvider { + let sso_oidc_config = self.sso_oidc_config.unwrap_or_else(|| { + let provider_config = self.provider_config.unwrap_or_default(); + let mut sso_oidc_config = aws_sdk_ssooidc::Config::builder() + .http_connector(expect_connector( + "The SSO token provider", + provider_config.connector(&Default::default()), + )) + .retry_config(RetryConfig::standard()) + .time_source(provider_config.time_source()); + sso_oidc_config.set_sleep_impl(provider_config.sleep()); + sso_oidc_config + }); + SsoTokenProvider { inner: Arc::new(Inner { env, @@ -335,7 +353,7 @@ impl Builder { region: self.region.expect("region is required"), session_name: self.session_name.expect("session_name is required"), start_url: self.start_url.expect("start_url is required"), - sso_oidc_config: self.sso_oidc_config.unwrap_or_default(), + sso_oidc_config, last_refresh_attempt: Mutex::new(None), }), token_cache: ExpiringCache::new(REFRESH_BUFFER_TIME), From 77e092adf70803b066c317ae2594e9afff525b01 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 14 Aug 2023 16:11:00 -0700 Subject: [PATCH 07/12] Fix aws-config feature problems --- .../src/default_provider/credentials.rs | 35 ++++++--- .../aws-config/src/imds/region.rs | 2 +- aws/rust-runtime/aws-config/src/lib.rs | 2 + .../aws-config/src/profile/credentials.rs | 46 ++++++++---- .../src/profile/credentials/exec.rs | 72 ++++++++++++------- .../aws-config/src/profile/region.rs | 2 +- .../aws-config/src/provider_config.rs | 52 ++++++++------ aws/rust-runtime/aws-config/src/sso/cache.rs | 2 +- 8 files changed, 139 insertions(+), 74 deletions(-) diff --git a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs index a5e1fb0dc5..182066cd56 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs @@ -94,6 +94,7 @@ impl ProvideCredentials for DefaultCredentialsChain { #[derive(Debug, Default)] pub struct Builder { profile_file_builder: crate::profile::credentials::Builder, + #[cfg(feature = "sts")] web_identity_builder: crate::web_identity_token::Builder, imds_builder: crate::imds::credentials::Builder, ecs_builder: crate::ecs::Builder, @@ -182,13 +183,19 @@ impl Builder { let env_provider = EnvironmentVariableCredentialsProvider::new_with_env(conf.env()); let profile_provider = self.profile_file_builder.configure(&conf).build(); + #[cfg(feature = "sts")] let web_identity_token_provider = self.web_identity_builder.configure(&conf).build(); let imds_provider = self.imds_builder.configure(&conf).build(); let ecs_provider = self.ecs_builder.configure(&conf).build(); - let provider_chain = CredentialsProviderChain::first_try("Environment", env_provider) - .or_else("Profile", profile_provider) - .or_else("WebIdentityToken", web_identity_token_provider) + let mut provider_chain = CredentialsProviderChain::first_try("Environment", env_provider) + .or_else("Profile", profile_provider); + #[cfg(feature = "sts")] + { + provider_chain = + provider_chain.or_else("WebIdentityToken", web_identity_token_provider); + } + provider_chain = provider_chain .or_else("EcsContainer", ecs_provider) .or_else("Ec2InstanceMetadata", imds_provider); @@ -268,21 +275,30 @@ mod test { make_test!(prefer_environment); make_test!(profile_static_keys); + #[cfg(feature = "sts")] make_test!(web_identity_token_env); + #[cfg(feature = "sts")] make_test!(web_identity_source_profile_no_env); + #[cfg(feature = "sts")] make_test!(web_identity_token_invalid_jwt); + #[cfg(feature = "sts")] make_test!(web_identity_token_source_profile); + #[cfg(feature = "sts")] make_test!(web_identity_token_profile); make_test!(profile_name); + #[cfg(feature = "sts")] make_test!(profile_overrides_web_identity); make_test!(environment_variables_blank); + #[cfg(feature = "sts")] make_test!(imds_token_fail); + #[cfg(feature = "sts")] make_test!(imds_no_iam_role); make_test!(imds_default_chain_error); make_test!(imds_default_chain_success, builder: |config| { config.with_time_source(StaticTimeSource::new(UNIX_EPOCH)) }); + #[cfg(feature = "sts")] make_test!(imds_assume_role); make_test!(imds_config_with_no_creds, builder: |config| { config.with_time_source(StaticTimeSource::new(UNIX_EPOCH)) @@ -291,19 +307,20 @@ mod test { make_test!(imds_default_chain_retries, builder: |config| { config.with_time_source(StaticTimeSource::new(UNIX_EPOCH)) }); + #[cfg(feature = "sts")] make_test!(ecs_assume_role); make_test!(ecs_credentials); make_test!(ecs_credentials_invalid_profile); - #[cfg(not(feature = "credentials-sso"))] - make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: credentials-sso")]); - #[cfg(not(feature = "credentials-sso"))] - make_test!(sso_no_token_file #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: credentials-sso")]); + #[cfg(not(feature = "sso"))] + make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); + #[cfg(not(feature = "sso"))] + make_test!(sso_no_token_file #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); - #[cfg(feature = "credentials-sso")] + #[cfg(feature = "sso")] make_test!(sso_assume_role); - #[cfg(feature = "credentials-sso")] + #[cfg(feature = "sso")] make_test!(sso_no_token_file); #[tokio::test] diff --git a/aws/rust-runtime/aws-config/src/imds/region.rs b/aws/rust-runtime/aws-config/src/imds/region.rs index bc784f8d4f..2563862480 100644 --- a/aws/rust-runtime/aws-config/src/imds/region.rs +++ b/aws/rust-runtime/aws-config/src/imds/region.rs @@ -117,11 +117,11 @@ mod test { use crate::imds::client::test::{imds_request, imds_response, token_request, token_response}; use crate::imds::region::ImdsRegionProvider; use crate::provider_config::ProviderConfig; - use aws_sdk_sts::config::Region; use aws_smithy_async::rt::sleep::TokioSleep; use aws_smithy_client::erase::DynConnector; use aws_smithy_client::test_connection::TestConnection; use aws_smithy_http::body::SdkBody; + use aws_types::region::Region; use tracing_test::traced_test; #[tokio::test] diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index 113677a457..0b8f214f69 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -125,8 +125,10 @@ pub mod retry; #[cfg(feature = "sso")] pub mod sso; pub(crate) mod standard_property; +#[cfg(feature = "sts")] pub mod sts; pub mod timeout; +#[cfg(feature = "sts")] pub mod web_identity_token; /// Create an environment loader for AWS Configuration diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index c3d08e58d5..2f32ad883f 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -29,6 +29,7 @@ use crate::profile::profile_file::ProfileFiles; use crate::profile::Profile; use crate::provider_config::ProviderConfig; use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials}; +#[cfg(feature = "sts")] use aws_sdk_sts::config::Builder as StsConfigBuilder; use aws_smithy_types::error::display::DisplayErrorContext; use std::borrow::Cow; @@ -142,6 +143,7 @@ impl ProvideCredentials for ProfileFileCredentialsProvider { #[derive(Debug)] pub struct ProfileFileCredentialsProvider { factory: NamedProviderFactory, + #[cfg(feature = "sts")] sts_config: StsConfigBuilder, provider_config: ProviderConfig, } @@ -165,6 +167,7 @@ impl ProfileFileCredentialsProvider { &err )), })?; + #[allow(unused_mut)] let mut creds = match inner_provider .base() .provide_credentials() @@ -180,19 +183,23 @@ impl ProfileFileCredentialsProvider { return Err(CredentialsError::provider_error(e)); } }; - for provider in inner_provider.chain().iter() { - let next_creds = provider - .credentials(creds, &self.sts_config) - .instrument(tracing::debug_span!("load_assume_role", provider = ?provider)) - .await; - match next_creds { - Ok(next_creds) => { - tracing::info!(creds = ?next_creds, "loaded assume role credentials"); - creds = next_creds - } - Err(e) => { - tracing::warn!(provider = ?provider, "failed to load assume role credentials"); - return Err(CredentialsError::provider_error(e)); + // Note: the chain is checked against the `sts` feature in the `build_provider_chain` + #[cfg(feature = "sts")] + { + for provider in inner_provider.chain().iter() { + let next_creds = provider + .credentials(creds, &self.sts_config) + .instrument(tracing::debug_span!("load_assume_role", provider = ?provider)) + .await; + match next_creds { + Ok(next_creds) => { + tracing::info!(creds = ?next_creds, "loaded assume role credentials"); + creds = next_creds + } + Err(e) => { + tracing::warn!(provider = ?provider, "failed to load assume role credentials"); + return Err(CredentialsError::provider_error(e)); + } } } } @@ -444,6 +451,7 @@ impl Builder { ProfileFileCredentialsProvider { factory, + #[cfg(feature = "sts")] sts_config: conf.sts_client_config(), provider_config: conf, } @@ -460,7 +468,14 @@ async fn build_provider_chain( .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?; let repr = repr::resolve_chain(profile_set)?; tracing::info!(chain = ?repr, "constructed abstract provider from config file"); - exec::ProviderChain::from_repr(provider_config, repr, factory) + let provider = exec::ProviderChain::from_repr(provider_config, repr, factory)?; + #[cfg(not(feature = "sts"))] + if !provider.chain().is_empty() { + return Err(ProfileFileError::FeatureNotEnabled { + feature: "sts".into(), + }); + } + Ok(provider) } #[cfg(test)] @@ -484,10 +499,13 @@ mod test { }; } + #[cfg(feature = "sts")] make_test!(e2e_assume_role); make_test!(empty_config); + #[cfg(feature = "sts")] make_test!(retry_on_error); make_test!(invalid_config); + #[cfg(feature = "sts")] make_test!(region_override); make_test!(credential_process); make_test!(credential_process_failure); diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs index 99e2c98d49..354c5a79b1 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs @@ -7,17 +7,24 @@ use super::repr::{self, BaseProvider}; use crate::credential_process::CredentialProcessProvider; use crate::profile::credentials::ProfileFileError; use crate::provider_config::ProviderConfig; -#[cfg(feature = "credentials-sso")] +use aws_credential_types::provider::ProvideCredentials; +use aws_smithy_async::time::SharedTimeSource; +use std::fmt::Debug; +use std::sync::Arc; + +#[cfg(feature = "sso")] use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider}; + +#[cfg(feature = "sts")] use crate::sts; +#[cfg(feature = "sts")] use crate::web_identity_token::{StaticConfiguration, WebIdentityTokenCredentialsProvider}; -use aws_credential_types::provider::{self, error::CredentialsError, ProvideCredentials}; +#[cfg(feature = "sts")] use aws_sdk_sts::config::{Builder as StsConfigBuilder, Credentials}; +#[cfg(feature = "sts")] use aws_sdk_sts::Client as StsClient; -use aws_smithy_async::time::SharedTimeSource; -use std::fmt::Debug; -use std::sync::Arc; +#[cfg_attr(not(feature = "sts"), allow(dead_code))] #[derive(Debug)] pub(super) struct AssumeRoleProvider { role_arn: String, @@ -26,12 +33,15 @@ pub(super) struct AssumeRoleProvider { time_source: SharedTimeSource, } +#[cfg(feature = "sts")] impl AssumeRoleProvider { pub(super) async fn credentials( &self, input_credentials: Credentials, sts_config: &StsConfigBuilder, - ) -> provider::Result { + ) -> aws_credential_types::provider::Result { + use aws_credential_types::provider::error::CredentialsError; + let config = sts_config .clone() .credentials_provider(input_credentials) @@ -92,22 +102,32 @@ impl ProviderChain { web_identity_token_file, session_name, } => { - let provider = WebIdentityTokenCredentialsProvider::builder() - .static_configuration(StaticConfiguration { - web_identity_token_file: web_identity_token_file.into(), - role_arn: role_arn.to_string(), - session_name: session_name.map(|sess| sess.to_string()).unwrap_or_else( - || { - sts::util::default_session_name( - "web-identity-token-profile", - provider_config.time_source().now(), - ) - }, - ), - }) - .configure(provider_config) - .build(); - Arc::new(provider) + #[cfg(feature = "sts")] + { + let provider = WebIdentityTokenCredentialsProvider::builder() + .static_configuration(StaticConfiguration { + web_identity_token_file: web_identity_token_file.into(), + role_arn: role_arn.to_string(), + session_name: session_name.map(|sess| sess.to_string()).unwrap_or_else( + || { + sts::util::default_session_name( + "web-identity-token-profile", + provider_config.time_source().now(), + ) + }, + ), + }) + .configure(provider_config) + .build(); + Arc::new(provider) + } + #[cfg(not(feature = "sts"))] + { + let _ = (role_arn, web_identity_token_file, session_name); + Err(ProfileFileError::FeatureNotEnabled { + feature: "sts".into(), + })? + } } #[allow(unused_variables)] BaseProvider::Sso { @@ -116,7 +136,7 @@ impl ProviderChain { sso_role_name, sso_start_url, } => { - #[cfg(feature = "credentials-sso")] + #[cfg(feature = "sso")] { use aws_types::region::Region; let sso_config = SsoProviderConfig { @@ -124,13 +144,15 @@ impl ProviderChain { role_name: sso_role_name.to_string(), start_url: sso_start_url.to_string(), region: Region::new(sso_region.to_string()), + // TODO(https://github.com/awslabs/aws-sdk-rust/issues/703): Implement sso_session_name profile property + session_name: None, }; Arc::new(SsoCredentialsProvider::new(provider_config, sso_config)) } - #[cfg(not(feature = "credentials-sso"))] + #[cfg(not(feature = "sso"))] { Err(ProfileFileError::FeatureNotEnabled { - feature: "credentials-sso".into(), + feature: "sso".into(), })? } } diff --git a/aws/rust-runtime/aws-config/src/profile/region.rs b/aws/rust-runtime/aws-config/src/profile/region.rs index 3cdcf8f7e4..5f8043220a 100644 --- a/aws/rust-runtime/aws-config/src/profile/region.rs +++ b/aws/rust-runtime/aws-config/src/profile/region.rs @@ -158,8 +158,8 @@ mod test { use crate::profile::ProfileFileRegionProvider; use crate::provider_config::ProviderConfig; use crate::test_case::no_traffic_connector; - use aws_sdk_sts::config::Region; use aws_types::os_shim_internal::{Env, Fs}; + use aws_types::region::Region; use futures_util::FutureExt; use tracing_test::traced_test; diff --git a/aws/rust-runtime/aws-config/src/provider_config.rs b/aws/rust-runtime/aws-config/src/provider_config.rs index f1caa75e51..a91be12184 100644 --- a/aws/rust-runtime/aws-config/src/provider_config.rs +++ b/aws/rust-runtime/aws-config/src/provider_config.rs @@ -118,19 +118,20 @@ impl ProviderConfig { /// when they are enabled as crate features which is usually the correct option. To construct /// a `ProviderConfig` without these fields set, use [`ProviderConfig::empty`]. /// - /// - /// # Examples - /// ```no_run - /// # #[cfg(feature = "rustls")] - /// # fn example() { - /// use aws_config::provider_config::ProviderConfig; - /// use aws_sdk_sts::config::Region; - /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; - /// let conf = ProviderConfig::without_region().with_region(Some(Region::new("us-east-1"))); - /// - /// let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); - /// # } - /// ``` + #[cfg_attr( + all(feature = "rustls", feature = "sts"), + doc = " +# Examples +```no_run +use aws_config::provider_config::ProviderConfig; +use aws_sdk_sts::config::Region; +use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; +let conf = ProviderConfig::without_region().with_region(Some(Region::new(\"us-east-1\"))); + +let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); +``` + " + )] pub fn without_region() -> Self { Self::default() } @@ -167,16 +168,21 @@ impl ProviderConfig { /// Create a default provider config with the region region automatically loaded from the default chain. /// - /// # Examples - /// ```no_run - /// # async fn test() { - /// use aws_config::provider_config::ProviderConfig; - /// use aws_sdk_sts::config::Region; - /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; - /// let conf = ProviderConfig::with_default_region().await; - /// let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); - /// } - /// ``` + #[cfg_attr( + feature = "sts", + doc = " +# Examples +```no_run +# async fn test() { +use aws_config::provider_config::ProviderConfig; +use aws_sdk_sts::config::Region; +use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; +let conf = ProviderConfig::with_default_region().await; +let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); +} +``` + " + )] pub async fn with_default_region() -> Self { Self::without_region().load_default_region().await } diff --git a/aws/rust-runtime/aws-config/src/sso/cache.rs b/aws/rust-runtime/aws-config/src/sso/cache.rs index 17a3424ae6..5d78798f87 100644 --- a/aws/rust-runtime/aws-config/src/sso/cache.rs +++ b/aws/rust-runtime/aws-config/src/sso/cache.rs @@ -99,7 +99,7 @@ impl fmt::Display for CachedSsoTokenError { } impl StdError for CachedSsoTokenError { - fn cause(&self) -> Option<&dyn StdError> { + fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { Self::FailedToFormatDateTime { source } => Some(source.as_ref()), Self::InvalidField { source, .. } => Some(source.as_ref()), From 09004ea92f5f176089a1cdbfa7bb765441b8e023 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 15 Aug 2023 16:51:13 -0700 Subject: [PATCH 08/12] Revert "Fix aws-config feature problems" This reverts commit 77e092adf70803b066c317ae2594e9afff525b01. --- .../src/default_provider/credentials.rs | 35 +++------ .../aws-config/src/imds/region.rs | 2 +- aws/rust-runtime/aws-config/src/lib.rs | 2 - .../aws-config/src/profile/credentials.rs | 46 ++++-------- .../src/profile/credentials/exec.rs | 72 +++++++------------ .../aws-config/src/profile/region.rs | 2 +- .../aws-config/src/provider_config.rs | 52 ++++++-------- aws/rust-runtime/aws-config/src/sso/cache.rs | 2 +- 8 files changed, 74 insertions(+), 139 deletions(-) diff --git a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs index 182066cd56..a5e1fb0dc5 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs @@ -94,7 +94,6 @@ impl ProvideCredentials for DefaultCredentialsChain { #[derive(Debug, Default)] pub struct Builder { profile_file_builder: crate::profile::credentials::Builder, - #[cfg(feature = "sts")] web_identity_builder: crate::web_identity_token::Builder, imds_builder: crate::imds::credentials::Builder, ecs_builder: crate::ecs::Builder, @@ -183,19 +182,13 @@ impl Builder { let env_provider = EnvironmentVariableCredentialsProvider::new_with_env(conf.env()); let profile_provider = self.profile_file_builder.configure(&conf).build(); - #[cfg(feature = "sts")] let web_identity_token_provider = self.web_identity_builder.configure(&conf).build(); let imds_provider = self.imds_builder.configure(&conf).build(); let ecs_provider = self.ecs_builder.configure(&conf).build(); - let mut provider_chain = CredentialsProviderChain::first_try("Environment", env_provider) - .or_else("Profile", profile_provider); - #[cfg(feature = "sts")] - { - provider_chain = - provider_chain.or_else("WebIdentityToken", web_identity_token_provider); - } - provider_chain = provider_chain + let provider_chain = CredentialsProviderChain::first_try("Environment", env_provider) + .or_else("Profile", profile_provider) + .or_else("WebIdentityToken", web_identity_token_provider) .or_else("EcsContainer", ecs_provider) .or_else("Ec2InstanceMetadata", imds_provider); @@ -275,30 +268,21 @@ mod test { make_test!(prefer_environment); make_test!(profile_static_keys); - #[cfg(feature = "sts")] make_test!(web_identity_token_env); - #[cfg(feature = "sts")] make_test!(web_identity_source_profile_no_env); - #[cfg(feature = "sts")] make_test!(web_identity_token_invalid_jwt); - #[cfg(feature = "sts")] make_test!(web_identity_token_source_profile); - #[cfg(feature = "sts")] make_test!(web_identity_token_profile); make_test!(profile_name); - #[cfg(feature = "sts")] make_test!(profile_overrides_web_identity); make_test!(environment_variables_blank); - #[cfg(feature = "sts")] make_test!(imds_token_fail); - #[cfg(feature = "sts")] make_test!(imds_no_iam_role); make_test!(imds_default_chain_error); make_test!(imds_default_chain_success, builder: |config| { config.with_time_source(StaticTimeSource::new(UNIX_EPOCH)) }); - #[cfg(feature = "sts")] make_test!(imds_assume_role); make_test!(imds_config_with_no_creds, builder: |config| { config.with_time_source(StaticTimeSource::new(UNIX_EPOCH)) @@ -307,20 +291,19 @@ mod test { make_test!(imds_default_chain_retries, builder: |config| { config.with_time_source(StaticTimeSource::new(UNIX_EPOCH)) }); - #[cfg(feature = "sts")] make_test!(ecs_assume_role); make_test!(ecs_credentials); make_test!(ecs_credentials_invalid_profile); - #[cfg(not(feature = "sso"))] - make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); - #[cfg(not(feature = "sso"))] - make_test!(sso_no_token_file #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); + #[cfg(not(feature = "credentials-sso"))] + make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: credentials-sso")]); + #[cfg(not(feature = "credentials-sso"))] + make_test!(sso_no_token_file #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: credentials-sso")]); - #[cfg(feature = "sso")] + #[cfg(feature = "credentials-sso")] make_test!(sso_assume_role); - #[cfg(feature = "sso")] + #[cfg(feature = "credentials-sso")] make_test!(sso_no_token_file); #[tokio::test] diff --git a/aws/rust-runtime/aws-config/src/imds/region.rs b/aws/rust-runtime/aws-config/src/imds/region.rs index 2563862480..bc784f8d4f 100644 --- a/aws/rust-runtime/aws-config/src/imds/region.rs +++ b/aws/rust-runtime/aws-config/src/imds/region.rs @@ -117,11 +117,11 @@ mod test { use crate::imds::client::test::{imds_request, imds_response, token_request, token_response}; use crate::imds::region::ImdsRegionProvider; use crate::provider_config::ProviderConfig; + use aws_sdk_sts::config::Region; use aws_smithy_async::rt::sleep::TokioSleep; use aws_smithy_client::erase::DynConnector; use aws_smithy_client::test_connection::TestConnection; use aws_smithy_http::body::SdkBody; - use aws_types::region::Region; use tracing_test::traced_test; #[tokio::test] diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index 0b8f214f69..113677a457 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -125,10 +125,8 @@ pub mod retry; #[cfg(feature = "sso")] pub mod sso; pub(crate) mod standard_property; -#[cfg(feature = "sts")] pub mod sts; pub mod timeout; -#[cfg(feature = "sts")] pub mod web_identity_token; /// Create an environment loader for AWS Configuration diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index 2f32ad883f..c3d08e58d5 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -29,7 +29,6 @@ use crate::profile::profile_file::ProfileFiles; use crate::profile::Profile; use crate::provider_config::ProviderConfig; use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials}; -#[cfg(feature = "sts")] use aws_sdk_sts::config::Builder as StsConfigBuilder; use aws_smithy_types::error::display::DisplayErrorContext; use std::borrow::Cow; @@ -143,7 +142,6 @@ impl ProvideCredentials for ProfileFileCredentialsProvider { #[derive(Debug)] pub struct ProfileFileCredentialsProvider { factory: NamedProviderFactory, - #[cfg(feature = "sts")] sts_config: StsConfigBuilder, provider_config: ProviderConfig, } @@ -167,7 +165,6 @@ impl ProfileFileCredentialsProvider { &err )), })?; - #[allow(unused_mut)] let mut creds = match inner_provider .base() .provide_credentials() @@ -183,23 +180,19 @@ impl ProfileFileCredentialsProvider { return Err(CredentialsError::provider_error(e)); } }; - // Note: the chain is checked against the `sts` feature in the `build_provider_chain` - #[cfg(feature = "sts")] - { - for provider in inner_provider.chain().iter() { - let next_creds = provider - .credentials(creds, &self.sts_config) - .instrument(tracing::debug_span!("load_assume_role", provider = ?provider)) - .await; - match next_creds { - Ok(next_creds) => { - tracing::info!(creds = ?next_creds, "loaded assume role credentials"); - creds = next_creds - } - Err(e) => { - tracing::warn!(provider = ?provider, "failed to load assume role credentials"); - return Err(CredentialsError::provider_error(e)); - } + for provider in inner_provider.chain().iter() { + let next_creds = provider + .credentials(creds, &self.sts_config) + .instrument(tracing::debug_span!("load_assume_role", provider = ?provider)) + .await; + match next_creds { + Ok(next_creds) => { + tracing::info!(creds = ?next_creds, "loaded assume role credentials"); + creds = next_creds + } + Err(e) => { + tracing::warn!(provider = ?provider, "failed to load assume role credentials"); + return Err(CredentialsError::provider_error(e)); } } } @@ -451,7 +444,6 @@ impl Builder { ProfileFileCredentialsProvider { factory, - #[cfg(feature = "sts")] sts_config: conf.sts_client_config(), provider_config: conf, } @@ -468,14 +460,7 @@ async fn build_provider_chain( .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?; let repr = repr::resolve_chain(profile_set)?; tracing::info!(chain = ?repr, "constructed abstract provider from config file"); - let provider = exec::ProviderChain::from_repr(provider_config, repr, factory)?; - #[cfg(not(feature = "sts"))] - if !provider.chain().is_empty() { - return Err(ProfileFileError::FeatureNotEnabled { - feature: "sts".into(), - }); - } - Ok(provider) + exec::ProviderChain::from_repr(provider_config, repr, factory) } #[cfg(test)] @@ -499,13 +484,10 @@ mod test { }; } - #[cfg(feature = "sts")] make_test!(e2e_assume_role); make_test!(empty_config); - #[cfg(feature = "sts")] make_test!(retry_on_error); make_test!(invalid_config); - #[cfg(feature = "sts")] make_test!(region_override); make_test!(credential_process); make_test!(credential_process_failure); diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs index 354c5a79b1..99e2c98d49 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs @@ -7,24 +7,17 @@ use super::repr::{self, BaseProvider}; use crate::credential_process::CredentialProcessProvider; use crate::profile::credentials::ProfileFileError; use crate::provider_config::ProviderConfig; -use aws_credential_types::provider::ProvideCredentials; -use aws_smithy_async::time::SharedTimeSource; -use std::fmt::Debug; -use std::sync::Arc; - -#[cfg(feature = "sso")] +#[cfg(feature = "credentials-sso")] use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider}; - -#[cfg(feature = "sts")] use crate::sts; -#[cfg(feature = "sts")] use crate::web_identity_token::{StaticConfiguration, WebIdentityTokenCredentialsProvider}; -#[cfg(feature = "sts")] +use aws_credential_types::provider::{self, error::CredentialsError, ProvideCredentials}; use aws_sdk_sts::config::{Builder as StsConfigBuilder, Credentials}; -#[cfg(feature = "sts")] use aws_sdk_sts::Client as StsClient; +use aws_smithy_async::time::SharedTimeSource; +use std::fmt::Debug; +use std::sync::Arc; -#[cfg_attr(not(feature = "sts"), allow(dead_code))] #[derive(Debug)] pub(super) struct AssumeRoleProvider { role_arn: String, @@ -33,15 +26,12 @@ pub(super) struct AssumeRoleProvider { time_source: SharedTimeSource, } -#[cfg(feature = "sts")] impl AssumeRoleProvider { pub(super) async fn credentials( &self, input_credentials: Credentials, sts_config: &StsConfigBuilder, - ) -> aws_credential_types::provider::Result { - use aws_credential_types::provider::error::CredentialsError; - + ) -> provider::Result { let config = sts_config .clone() .credentials_provider(input_credentials) @@ -102,32 +92,22 @@ impl ProviderChain { web_identity_token_file, session_name, } => { - #[cfg(feature = "sts")] - { - let provider = WebIdentityTokenCredentialsProvider::builder() - .static_configuration(StaticConfiguration { - web_identity_token_file: web_identity_token_file.into(), - role_arn: role_arn.to_string(), - session_name: session_name.map(|sess| sess.to_string()).unwrap_or_else( - || { - sts::util::default_session_name( - "web-identity-token-profile", - provider_config.time_source().now(), - ) - }, - ), - }) - .configure(provider_config) - .build(); - Arc::new(provider) - } - #[cfg(not(feature = "sts"))] - { - let _ = (role_arn, web_identity_token_file, session_name); - Err(ProfileFileError::FeatureNotEnabled { - feature: "sts".into(), - })? - } + let provider = WebIdentityTokenCredentialsProvider::builder() + .static_configuration(StaticConfiguration { + web_identity_token_file: web_identity_token_file.into(), + role_arn: role_arn.to_string(), + session_name: session_name.map(|sess| sess.to_string()).unwrap_or_else( + || { + sts::util::default_session_name( + "web-identity-token-profile", + provider_config.time_source().now(), + ) + }, + ), + }) + .configure(provider_config) + .build(); + Arc::new(provider) } #[allow(unused_variables)] BaseProvider::Sso { @@ -136,7 +116,7 @@ impl ProviderChain { sso_role_name, sso_start_url, } => { - #[cfg(feature = "sso")] + #[cfg(feature = "credentials-sso")] { use aws_types::region::Region; let sso_config = SsoProviderConfig { @@ -144,15 +124,13 @@ impl ProviderChain { role_name: sso_role_name.to_string(), start_url: sso_start_url.to_string(), region: Region::new(sso_region.to_string()), - // TODO(https://github.com/awslabs/aws-sdk-rust/issues/703): Implement sso_session_name profile property - session_name: None, }; Arc::new(SsoCredentialsProvider::new(provider_config, sso_config)) } - #[cfg(not(feature = "sso"))] + #[cfg(not(feature = "credentials-sso"))] { Err(ProfileFileError::FeatureNotEnabled { - feature: "sso".into(), + feature: "credentials-sso".into(), })? } } diff --git a/aws/rust-runtime/aws-config/src/profile/region.rs b/aws/rust-runtime/aws-config/src/profile/region.rs index 5f8043220a..3cdcf8f7e4 100644 --- a/aws/rust-runtime/aws-config/src/profile/region.rs +++ b/aws/rust-runtime/aws-config/src/profile/region.rs @@ -158,8 +158,8 @@ mod test { use crate::profile::ProfileFileRegionProvider; use crate::provider_config::ProviderConfig; use crate::test_case::no_traffic_connector; + use aws_sdk_sts::config::Region; use aws_types::os_shim_internal::{Env, Fs}; - use aws_types::region::Region; use futures_util::FutureExt; use tracing_test::traced_test; diff --git a/aws/rust-runtime/aws-config/src/provider_config.rs b/aws/rust-runtime/aws-config/src/provider_config.rs index a91be12184..f1caa75e51 100644 --- a/aws/rust-runtime/aws-config/src/provider_config.rs +++ b/aws/rust-runtime/aws-config/src/provider_config.rs @@ -118,20 +118,19 @@ impl ProviderConfig { /// when they are enabled as crate features which is usually the correct option. To construct /// a `ProviderConfig` without these fields set, use [`ProviderConfig::empty`]. /// - #[cfg_attr( - all(feature = "rustls", feature = "sts"), - doc = " -# Examples -```no_run -use aws_config::provider_config::ProviderConfig; -use aws_sdk_sts::config::Region; -use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; -let conf = ProviderConfig::without_region().with_region(Some(Region::new(\"us-east-1\"))); - -let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); -``` - " - )] + /// + /// # Examples + /// ```no_run + /// # #[cfg(feature = "rustls")] + /// # fn example() { + /// use aws_config::provider_config::ProviderConfig; + /// use aws_sdk_sts::config::Region; + /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; + /// let conf = ProviderConfig::without_region().with_region(Some(Region::new("us-east-1"))); + /// + /// let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); + /// # } + /// ``` pub fn without_region() -> Self { Self::default() } @@ -168,21 +167,16 @@ let credential_provider = WebIdentityTokenCredentialsProvider::builder().configu /// Create a default provider config with the region region automatically loaded from the default chain. /// - #[cfg_attr( - feature = "sts", - doc = " -# Examples -```no_run -# async fn test() { -use aws_config::provider_config::ProviderConfig; -use aws_sdk_sts::config::Region; -use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; -let conf = ProviderConfig::with_default_region().await; -let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); -} -``` - " - )] + /// # Examples + /// ```no_run + /// # async fn test() { + /// use aws_config::provider_config::ProviderConfig; + /// use aws_sdk_sts::config::Region; + /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider; + /// let conf = ProviderConfig::with_default_region().await; + /// let credential_provider = WebIdentityTokenCredentialsProvider::builder().configure(&conf).build(); + /// } + /// ``` pub async fn with_default_region() -> Self { Self::without_region().load_default_region().await } diff --git a/aws/rust-runtime/aws-config/src/sso/cache.rs b/aws/rust-runtime/aws-config/src/sso/cache.rs index 5d78798f87..17a3424ae6 100644 --- a/aws/rust-runtime/aws-config/src/sso/cache.rs +++ b/aws/rust-runtime/aws-config/src/sso/cache.rs @@ -99,7 +99,7 @@ impl fmt::Display for CachedSsoTokenError { } impl StdError for CachedSsoTokenError { - fn source(&self) -> Option<&(dyn StdError + 'static)> { + fn cause(&self) -> Option<&dyn StdError> { match self { Self::FailedToFormatDateTime { source } => Some(source.as_ref()), Self::InvalidField { source, .. } => Some(source.as_ref()), From 1c61b46396b3fb7269ffde32e3089b77ba23f2e2 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 15 Aug 2023 17:09:54 -0700 Subject: [PATCH 09/12] Fix feature issues --- aws/rust-runtime/aws-config/Cargo.toml | 5 ++--- .../aws-config/src/default_provider/credentials.rs | 12 ++++++------ .../aws-config/src/profile/credentials/exec.rs | 12 +++++++----- aws/rust-runtime/aws-config/src/sso/cache.rs | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index 41dd43b472..63ad3f5c84 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -14,15 +14,14 @@ rustls = ["aws-smithy-client/rustls", "client-hyper"] native-tls = [] allow-compilation = [] # our tests use `cargo test --all-features` and native-tls breaks CI rt-tokio = ["aws-smithy-async/rt-tokio", "tokio/rt"] -sts = ["dep:aws-sdk-sts"] sso = ["dep:aws-sdk-sso", "dep:aws-sdk-ssooidc", "dep:ring", "dep:hex", "dep:zeroize", "aws-smithy-runtime-api/http-auth"] -default = ["client-hyper", "rustls", "rt-tokio", "sts"] +default = ["client-hyper", "rustls", "rt-tokio"] [dependencies] aws-credential-types = { path = "../../sdk/build/aws-sdk/sdk/aws-credential-types" } aws-http = { path = "../../sdk/build/aws-sdk/sdk/aws-http" } -aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sdk/sts", default-features = false, optional = true } +aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sdk/sts", default-features = false } aws-smithy-async = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-async" } aws-smithy-client = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-client", default-features = false } aws-smithy-http = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-http" } diff --git a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs index a5e1fb0dc5..eb3b8855a7 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs @@ -295,15 +295,15 @@ mod test { make_test!(ecs_credentials); make_test!(ecs_credentials_invalid_profile); - #[cfg(not(feature = "credentials-sso"))] - make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: credentials-sso")]); - #[cfg(not(feature = "credentials-sso"))] - make_test!(sso_no_token_file #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: credentials-sso")]); + #[cfg(not(feature = "sso"))] + make_test!(sso_assume_role #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); + #[cfg(not(feature = "sso"))] + make_test!(sso_no_token_file #[should_panic(expected = "This behavior requires following cargo feature(s) enabled: sso")]); - #[cfg(feature = "credentials-sso")] + #[cfg(feature = "sso")] make_test!(sso_assume_role); - #[cfg(feature = "credentials-sso")] + #[cfg(feature = "sso")] make_test!(sso_no_token_file); #[tokio::test] diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs index 99e2c98d49..1b8c5bb6e8 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs @@ -7,8 +7,6 @@ use super::repr::{self, BaseProvider}; use crate::credential_process::CredentialProcessProvider; use crate::profile::credentials::ProfileFileError; use crate::provider_config::ProviderConfig; -#[cfg(feature = "credentials-sso")] -use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider}; use crate::sts; use crate::web_identity_token::{StaticConfiguration, WebIdentityTokenCredentialsProvider}; use aws_credential_types::provider::{self, error::CredentialsError, ProvideCredentials}; @@ -116,21 +114,25 @@ impl ProviderChain { sso_role_name, sso_start_url, } => { - #[cfg(feature = "credentials-sso")] + #[cfg(feature = "sso")] { + use crate::sso::{credentials::SsoProviderConfig, SsoCredentialsProvider}; use aws_types::region::Region; + let sso_config = SsoProviderConfig { account_id: sso_account_id.to_string(), role_name: sso_role_name.to_string(), start_url: sso_start_url.to_string(), region: Region::new(sso_region.to_string()), + // TODO(https://github.com/awslabs/aws-sdk-rust/issues/703): Implement sso_session_name profile property + session_name: None, }; Arc::new(SsoCredentialsProvider::new(provider_config, sso_config)) } - #[cfg(not(feature = "credentials-sso"))] + #[cfg(not(feature = "sso"))] { Err(ProfileFileError::FeatureNotEnabled { - feature: "credentials-sso".into(), + feature: "sso".into(), })? } } diff --git a/aws/rust-runtime/aws-config/src/sso/cache.rs b/aws/rust-runtime/aws-config/src/sso/cache.rs index 17a3424ae6..5d78798f87 100644 --- a/aws/rust-runtime/aws-config/src/sso/cache.rs +++ b/aws/rust-runtime/aws-config/src/sso/cache.rs @@ -99,7 +99,7 @@ impl fmt::Display for CachedSsoTokenError { } impl StdError for CachedSsoTokenError { - fn cause(&self) -> Option<&dyn StdError> { + fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { Self::FailedToFormatDateTime { source } => Some(source.as_ref()), Self::InvalidField { source, .. } => Some(source.as_ref()), From 27cee6dbadda5910903e04db103eb88818e9323e Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 11 Oct 2023 10:51:38 -0700 Subject: [PATCH 10/12] Fix external types check --- aws/rust-runtime/aws-config/external-types.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/aws/rust-runtime/aws-config/external-types.toml b/aws/rust-runtime/aws-config/external-types.toml index 450f0b5c9d..69a7c58975 100644 --- a/aws/rust-runtime/aws-config/external-types.toml +++ b/aws/rust-runtime/aws-config/external-types.toml @@ -20,6 +20,7 @@ allowed_external_types = [ "aws_smithy_runtime_api::client::dns::SharedDnsResolver", "aws_smithy_runtime_api::client::http::HttpClient", "aws_smithy_runtime_api::client::http::SharedHttpClient", + "aws_smithy_runtime_api::client::identity::IdentityResolver", "aws_smithy_types::retry", "aws_smithy_types::retry::*", "aws_smithy_types::timeout", From 819fd75b150094400a160383c76a538d179950bc Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 16 Oct 2023 15:38:44 -0700 Subject: [PATCH 11/12] Fix external types check --- aws/rust-runtime/aws-config/external-types.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-config/external-types.toml b/aws/rust-runtime/aws-config/external-types.toml index 4c0e99bcd0..94002d550d 100644 --- a/aws/rust-runtime/aws-config/external-types.toml +++ b/aws/rust-runtime/aws-config/external-types.toml @@ -20,7 +20,7 @@ allowed_external_types = [ "aws_smithy_runtime_api::client::dns::SharedDnsResolver", "aws_smithy_runtime_api::client::http::HttpClient", "aws_smithy_runtime_api::client::http::SharedHttpClient", - "aws_smithy_runtime_api::client::identity::IdentityResolver", + "aws_smithy_runtime_api::client::identity::ResolveIdentity", "aws_smithy_types::retry", "aws_smithy_types::retry::*", "aws_smithy_types::timeout", From 9090e8804cabb5b2d5bc758be009145ac76748cb Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Mon, 16 Oct 2023 16:11:12 -0700 Subject: [PATCH 12/12] Fix doc preview --- .github/workflows/pull-request-bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-bot.yml b/.github/workflows/pull-request-bot.yml index 1997859064..2f0bf5c33c 100644 --- a/.github/workflows/pull-request-bot.yml +++ b/.github/workflows/pull-request-bot.yml @@ -106,7 +106,7 @@ jobs: # included since aws-config depends on them. Transcribe Streaming and DynamoDB (paginators/waiters) were chosen # below to stay small while still representing most features. Combined, they are about ~20MB at time of writing. run: | - ./gradlew -Paws.services=+sts,+sso,+transcribestreaming,+dynamodb :aws:sdk:assemble + ./gradlew -Paws.services=+sts,+sso,+ssooidc,+transcribestreaming,+dynamodb :aws:sdk:assemble # Copy the Server runtime crate(s) in cp -r rust-runtime/aws-smithy-http-server rust-runtime/aws-smithy-http-server-python rust-runtime/aws-smithy-http-server-typescript aws/sdk/build/aws-sdk/sdk