From 6fd3f80b0598a1be3cc8d23e3cab060f8da6e95e Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 10:36:24 -0400 Subject: [PATCH 01/18] Add ProvideCredentials to aws-types --- aws/rust-runtime/aws-types/Cargo.toml | 4 +- aws/rust-runtime/aws-types/src/config.rs | 61 +++++++ .../src/credentials/credentials_impl.rs | 107 ++++++++++++ .../aws-types/src/credentials/mod.rs | 79 +++++++++ .../aws-types/src/credentials/provider.rs | 165 ++++++++++++++++++ aws/rust-runtime/aws-types/src/lib.rs | 3 + 6 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 aws/rust-runtime/aws-types/src/credentials/credentials_impl.rs create mode 100644 aws/rust-runtime/aws-types/src/credentials/mod.rs create mode 100644 aws/rust-runtime/aws-types/src/credentials/provider.rs diff --git a/aws/rust-runtime/aws-types/Cargo.toml b/aws/rust-runtime/aws-types/Cargo.toml index 05d1e65af4..983119b88b 100644 --- a/aws/rust-runtime/aws-types/Cargo.toml +++ b/aws/rust-runtime/aws-types/Cargo.toml @@ -10,10 +10,12 @@ license = "Apache-2.0" [dependencies] lazy_static = "1" tracing = "0.1" +smithy-async = { path = "../../../rust-runtime/smithy-async" } +zeroize = "1.4.1" [dev-dependencies] tracing-test = "0.1.0" -serde = { version = "1", features = ["derive"]} +serde = { version = "1", features = ["derive"] } serde_json = "1" arbitrary = "1" futures-util = "0.3.16" diff --git a/aws/rust-runtime/aws-types/src/config.rs b/aws/rust-runtime/aws-types/src/config.rs index 307a8ee7b9..3ee55a306d 100644 --- a/aws/rust-runtime/aws-types/src/config.rs +++ b/aws/rust-runtime/aws-types/src/config.rs @@ -9,17 +9,20 @@ //! //! This module contains an shared configuration representation that is agnostic from a specific service. +use crate::credentials::SharedCredentialsProvider; use crate::region::Region; /// AWS Shared Configuration pub struct Config { region: Option, + credentials_provider: Option, } /// Builder for AWS Shared Configuration #[derive(Default)] pub struct Builder { region: Option, + credentials_provider: Option, } impl Builder { @@ -57,10 +60,63 @@ impl Builder { self } + /// Set the credentials provider for the builder + /// + /// # Example + /// ```rust + /// use aws_types::credentials::{ProvideCredentials, SharedCredentialsProvider}; + /// use aws_types::config::Config; + /// fn make_provider() -> impl ProvideCredentials { + /// // ... + /// # use aws_types::Credentials; + /// # Credentials::from_keys("test", "test", None) + /// } + /// + /// let config = Config::builder() + /// .credentials_provider(SharedCredentialsProvider::new(make_provider())) + /// .build(); + /// ``` + pub fn credentials_provider(mut self, provider: SharedCredentialsProvider) -> Self { + self.set_credentials_provider(Some(provider)); + self + } + + /// Set the credentials provider for the builder + /// + /// # Example + /// ```rust + /// use aws_types::credentials::{ProvideCredentials, SharedCredentialsProvider}; + /// use aws_types::config::Config; + /// fn make_provider() -> impl ProvideCredentials { + /// // ... + /// # use aws_types::Credentials; + /// # Credentials::from_keys("test", "test", None) + /// } + /// + /// fn override_provider() -> bool { + /// // ... + /// # true + /// } + /// + /// let mut builder = Config::builder(); + /// if override_provider() { + /// builder.set_credentials_provider(Some(SharedCredentialsProvider::new(make_provider()))); + /// } + /// let config = builder.build(); + /// ``` + pub fn set_credentials_provider( + &mut self, + provider: Option, + ) -> &mut Self { + self.credentials_provider = provider; + self + } + /// Build a [`Config`](Config) from this builder pub fn build(self) -> Config { Config { region: self.region, + credentials_provider: self.credentials_provider, } } } @@ -71,6 +127,11 @@ impl Config { self.region.as_ref() } + /// Configured credentials provider + pub fn credentials_provider(&self) -> Option<&SharedCredentialsProvider> { + self.credentials_provider.as_ref() + } + /// Config builder pub fn builder() -> Builder { Builder::default() diff --git a/aws/rust-runtime/aws-types/src/credentials/credentials_impl.rs b/aws/rust-runtime/aws-types/src/credentials/credentials_impl.rs new file mode 100644 index 0000000000..b2db3d43d9 --- /dev/null +++ b/aws/rust-runtime/aws-types/src/credentials/credentials_impl.rs @@ -0,0 +1,107 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use std::fmt; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; +use std::time::SystemTime; +use zeroize::Zeroizing; + +/// AWS SDK Credentials +/// +/// An opaque struct representing credentials that may be used in an AWS SDK, modeled on +/// the [CRT credentials implementation](https://github.com/awslabs/aws-c-auth/blob/main/source/credentials.c). +/// +/// When `Credentials` is dropped, its contents are zeroed in memory. Credentials uses an interior Arc to ensure +/// that even when cloned, credentials don't exist in multiple memory locations. +#[derive(Clone, Eq, PartialEq)] +pub struct Credentials(Arc); + +#[derive(Clone, Eq, PartialEq)] +struct Inner { + access_key_id: Zeroizing, + secret_access_key: Zeroizing, + session_token: Zeroizing>, + + /// Credential Expiry + /// + /// A timepoint at which the credentials should no longer + /// be used because they have expired. The primary purpose of this value is to allow + /// credentials to communicate to the caching provider when they need to be refreshed. + /// + /// If these credentials never expire, this value will be set to `None` + expires_after: Option, + + provider_name: &'static str, +} + +impl Debug for Credentials { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut creds = f.debug_struct("Credentials"); + creds + .field("provider_name", &self.0.provider_name) + .field("access_key_id", &self.0.access_key_id.as_str()) + .field("secret_access_key", &"** redacted **"); + if let Some(expiry) = self.expiry() { + // TODO: format the expiry nicely + creds.field("expires_after", &expiry); + } + creds.finish() + } +} + +const STATIC_CREDENTIALS: &str = "Static"; + +impl Credentials { + pub fn new( + access_key_id: impl Into, + secret_access_key: impl Into, + session_token: Option, + expires_after: Option, + provider_name: &'static str, + ) -> Self { + Credentials(Arc::new(Inner { + access_key_id: Zeroizing::new(access_key_id.into()), + secret_access_key: Zeroizing::new(secret_access_key.into()), + session_token: Zeroizing::new(session_token), + expires_after, + provider_name, + })) + } + + pub fn from_keys( + access_key_id: impl Into, + secret_access_key: impl Into, + session_token: Option, + ) -> Self { + Self::new( + access_key_id, + secret_access_key, + session_token, + None, + STATIC_CREDENTIALS, + ) + } + + pub fn access_key_id(&self) -> &str { + &self.0.access_key_id + } + + pub fn secret_access_key(&self) -> &str { + &self.0.secret_access_key + } + + pub fn expiry(&self) -> Option { + self.0.expires_after + } + + pub fn expiry_mut(&mut self) -> &mut Option { + &mut Arc::make_mut(&mut self.0).expires_after + } + + pub fn session_token(&self) -> Option<&str> { + self.0.session_token.as_deref() + } +} diff --git a/aws/rust-runtime/aws-types/src/credentials/mod.rs b/aws/rust-runtime/aws-types/src/credentials/mod.rs new file mode 100644 index 0000000000..498af09f8b --- /dev/null +++ b/aws/rust-runtime/aws-types/src/credentials/mod.rs @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! AWS SDK Credentials +//! +//! ## Implementing your own credentials provider +//! +//! While for many use cases, using a built in credentials provider is sufficient, you may want to +//! implement your own credential provider. +//! +//! ### With static credentials +//! [`Credentials`](credentials::Credentials) implement +//! [`ProvideCredentials](provide_credentials::ProvideCredentials) directly, so no custom provider +//! implementation is required: +//! ```rust +//! use aws_types::Credentials; +//! # mod dynamodb { +//! # use aws_types::credentials::ProvideCredentials; +//! # pub struct Config; +//! # impl Config { +//! # pub fn builder() -> Self { +//! # Config +//! # } +//! # pub fn credentials_provider(self, provider: impl ProvideCredentials + 'static) -> Self { +//! # self +//! # } +//! # } +//! # } +//! +//! let my_creds = Credentials::from_keys("akid", "secret_key", None); +//! let conf = dynamodb::Config::builder().credentials_provider(my_creds); +//! ``` +//! ### With dynamically loaded credentials +//! If you are loading credentials dynamically, you can provide your own implementation of +//! [`ProvideCredentials`](provide_credentials::ProvideCredentials). Generally, this is best done by +//! defining an inherent `async fn` on your structure, then calling that method directly from +//! the trait implementation. +//! ```rust +//! use aws_types::credentials::{CredentialsError, Credentials, ProvideCredentials, future, self}; +//! struct SubprocessCredentialProvider; +//! +//! async fn invoke_command(command: &str) -> String { +//! // implementation elided... +//! # String::from("some credentials") +//! } +//! +//! /// Parse access key and secret from the first two lines of a string +//! fn parse_credentials(creds: &str) -> credentials::Result { +//! let mut lines = creds.lines(); +//! let akid = lines.next().ok_or(CredentialsError::ProviderError("invalid credentials".into()))?; +//! let secret = lines.next().ok_or(CredentialsError::ProviderError("invalid credentials".into()))?; +//! Ok(Credentials::new(akid, secret, None, None, "CustomCommand")) +//! } +//! +//! impl SubprocessCredentialProvider { +//! async fn load_credentials(&self) -> credentials::Result { +//! let creds = invoke_command("load-credentials.py").await; +//! parse_credentials(&creds) +//! } +//! } +//! +//! impl ProvideCredentials for SubprocessCredentialProvider { +//! fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> where Self: 'a { +//! future::ProvideCredentials::new(self.load_credentials()) +//! } +//! } +//! ``` + +mod credentials_impl; +mod provider; + +pub use credentials_impl::Credentials; +pub use provider::future; +pub use provider::CredentialsError; +pub use provider::ProvideCredentials; +pub use provider::Result; +pub use provider::SharedCredentialsProvider; diff --git a/aws/rust-runtime/aws-types/src/credentials/provider.rs b/aws/rust-runtime/aws-types/src/credentials/provider.rs new file mode 100644 index 0000000000..2045e60f81 --- /dev/null +++ b/aws/rust-runtime/aws-types/src/credentials/provider.rs @@ -0,0 +1,165 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use crate::Credentials; +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Debug)] +#[non_exhaustive] +pub enum CredentialsError { + /// No credentials were available for this provider + CredentialsNotLoaded, + + /// Loading credentials from this provider exceeded the maximum allowed duration + ProviderTimedOut(Duration), + + /// The provider was given an invalid configuration + /// + /// For example: + /// - syntax error in ~/.aws/config + /// - assume role profile that forms an infinite loop + InvalidConfiguration(Box), + + /// The provider experienced an error during credential resolution + /// + /// This may include errors like a 503 from STS or a file system error when attempting to + /// read a configuration file. + ProviderError(Box), + + /// An unexpected error occured during credential resolution + /// + /// If the error is something that can occur during expected usage of a provider, `ProviderError` + /// should be returned instead. Unhandled is reserved for exceptional cases, for example: + /// - Returned data not UTF-8 + /// - A provider returns data that is missing required fields + Unhandled(Box), +} + +impl Display for CredentialsError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + CredentialsError::CredentialsNotLoaded => { + write!(f, "The provider could not provide credentials or required configuration was not set") + } + CredentialsError::ProviderTimedOut(d) => write!( + f, + "Credentials provider timed out after {} seconds", + d.as_secs() + ), + CredentialsError::Unhandled(err) => write!(f, "Unexpected credentials error: {}", err), + CredentialsError::InvalidConfiguration(err) => { + write!(f, "The credentials provider was not properly: {}", err) + } + CredentialsError::ProviderError(err) => { + write!(f, "An error occured while loading credentials: {}", err) + } + } + } +} + +impl Error for CredentialsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + CredentialsError::Unhandled(e) => Some(e.as_ref() as _), + _ => None, + } + } +} + +pub type Result = std::result::Result; + +pub mod future { + use smithy_async::future::now_or_later::NowOrLater; + use std::future::Future; + use std::pin::Pin; + use std::task::{Context, Poll}; + + type BoxFuture<'a, T> = Pin + Send + 'a>>; + pub struct ProvideCredentials<'a>(NowOrLater>); + + impl<'a> ProvideCredentials<'a> { + pub fn new(future: impl Future + Send + 'a) -> Self { + ProvideCredentials(NowOrLater::new(Box::pin(future))) + } + + pub fn ready(credentials: super::Result) -> Self { + ProvideCredentials(NowOrLater::ready(credentials)) + } + } + + impl Future for ProvideCredentials<'_> { + type Output = super::Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx) + } + } +} + +/// Asynchronous Credentials Provider +pub trait ProvideCredentials: Send + Sync { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a; +} + +impl ProvideCredentials for Credentials { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::ready(Ok(self.clone())) + } +} + +impl ProvideCredentials for Arc { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + self.as_ref().provide_credentials() + } +} + +/// Credentials Provider wrapper that may be shared +/// +/// Newtype wrapper around ProvideCredentials that implements Clone using an internal +/// Arc. +#[derive(Clone)] +pub struct SharedCredentialsProvider(Arc); + +impl SharedCredentialsProvider { + /// Create a new SharedCredentials provider from `ProvideCredentials` + /// + /// The given provider will be wrapped in an internal `Arc`. If your + /// provider is already in an `Arc`, use `SharedCredentialsProvider::from(provider)` instead. + pub fn new(provider: impl ProvideCredentials + 'static) -> Self { + Self(Arc::new(provider)) + } +} + +impl AsRef for SharedCredentialsProvider { + fn as_ref(&self) -> &(dyn ProvideCredentials + 'static) { + self.0.as_ref() + } +} + +impl From> for SharedCredentialsProvider { + fn from(provider: Arc) -> Self { + SharedCredentialsProvider(provider) + } +} + +impl ProvideCredentials for SharedCredentialsProvider { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + self.0.provide_credentials() + } +} diff --git a/aws/rust-runtime/aws-types/src/lib.rs b/aws/rust-runtime/aws-types/src/lib.rs index 44f298b43b..b913cfb1c3 100644 --- a/aws/rust-runtime/aws-types/src/lib.rs +++ b/aws/rust-runtime/aws-types/src/lib.rs @@ -6,11 +6,14 @@ pub mod build_metadata; // internal APIs, may be unstable pub mod config; +pub mod credentials; #[doc(hidden)] pub mod os_shim_internal; pub mod profile; pub mod region; +pub use credentials::Credentials; + use std::borrow::Cow; /// The name of the service used to sign this request From a2c56552d9645f2d7165cc5261d0da381658f8ba Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 11:46:08 -0400 Subject: [PATCH 02/18] Integrate ProvideCredentials into SDK runtime and codegen --- aws/rust-runtime/aws-auth/src/credentials.rs | 106 ---------- aws/rust-runtime/aws-auth/src/lib.rs | 8 +- aws/rust-runtime/aws-auth/src/middleware.rs | 22 ++- aws/rust-runtime/aws-auth/src/provider.rs | 187 ++++-------------- .../aws-auth/src/provider/cache.rs | 12 +- .../aws-auth/src/provider/lazy_caching.rs | 45 ++--- .../aws-auth/src/provider/time.rs | 5 +- .../aws-config/src/default_provider.rs | 15 +- .../src/environment/credentials.rs} | 84 +++++--- .../aws-config/src/environment/mod.rs | 4 +- aws/rust-runtime/aws-config/src/lib.rs | 37 +++- aws/rust-runtime/aws-inlineable/Cargo.toml | 1 + aws/rust-runtime/aws-inlineable/src/lib.rs | 4 +- .../aws-inlineable/src/no_credentials.rs | 20 ++ .../aws-types/src/credentials/provider.rs | 6 +- .../smithy/rustsdk/CredentialProviders.kt | 49 +++-- .../smithy/rustsdk/SharedConfigDecorator.kt | 1 + 17 files changed, 253 insertions(+), 353 deletions(-) delete mode 100644 aws/rust-runtime/aws-auth/src/credentials.rs rename aws/rust-runtime/{aws-auth/src/provider/env.rs => aws-config/src/environment/credentials.rs} (69%) create mode 100644 aws/rust-runtime/aws-inlineable/src/no_credentials.rs diff --git a/aws/rust-runtime/aws-auth/src/credentials.rs b/aws/rust-runtime/aws-auth/src/credentials.rs deleted file mode 100644 index f10dfa3000..0000000000 --- a/aws/rust-runtime/aws-auth/src/credentials.rs +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use std::fmt; -use std::fmt::{Debug, Formatter}; -use std::sync::Arc; -use std::time::SystemTime; -use zeroize::Zeroizing; - -/// AWS SDK Credentials -/// -/// An opaque struct representing credentials that may be used in an AWS SDK, modeled on -/// the [CRT credentials implementation](https://github.com/awslabs/aws-c-auth/blob/main/source/credentials.c). -/// -/// When `Credentials` is dropped, its contents are zeroed in memory. Credentials uses an interior Arc to ensure -/// that even when cloned, credentials don't exist in multiple memory locations. -#[derive(Clone, Eq, PartialEq)] -pub struct Credentials(Arc); - -#[derive(Clone, Eq, PartialEq)] -struct Inner { - access_key_id: Zeroizing, - secret_access_key: Zeroizing, - session_token: Zeroizing>, - - /// Credential Expiry - /// - /// A timepoint at which the credentials should no longer - /// be used because they have expired. The primary purpose of this value is to allow - /// credentials to communicate to the caching provider when they need to be refreshed. - /// - /// If these credentials never expire, this value will be set to `None` - expires_after: Option, - - provider_name: &'static str, -} - -impl Debug for Credentials { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut creds = f.debug_struct("Credentials"); - creds - .field("provider_name", &self.0.provider_name) - .field("access_key_id", &self.0.access_key_id.as_str()) - .field("secret_access_key", &"** redacted **"); - if let Some(expiry) = self.expiry() { - // TODO: format the expiry nicely - creds.field("expires_after", &expiry); - } - creds.finish() - } -} - -const STATIC_CREDENTIALS: &str = "Static"; -impl Credentials { - pub fn new( - access_key_id: impl Into, - secret_access_key: impl Into, - session_token: Option, - expires_after: Option, - provider_name: &'static str, - ) -> Self { - Credentials(Arc::new(Inner { - access_key_id: Zeroizing::new(access_key_id.into()), - secret_access_key: Zeroizing::new(secret_access_key.into()), - session_token: Zeroizing::new(session_token), - expires_after, - provider_name, - })) - } - - pub fn from_keys( - access_key_id: impl Into, - secret_access_key: impl Into, - session_token: Option, - ) -> Self { - Self::new( - access_key_id, - secret_access_key, - session_token, - None, - STATIC_CREDENTIALS, - ) - } - - pub fn access_key_id(&self) -> &str { - &self.0.access_key_id - } - - pub fn secret_access_key(&self) -> &str { - &self.0.secret_access_key - } - - pub fn expiry(&self) -> Option { - self.0.expires_after - } - - pub fn expiry_mut(&mut self) -> &mut Option { - &mut Arc::make_mut(&mut self.0).expires_after - } - - pub fn session_token(&self) -> Option<&str> { - self.0.session_token.as_deref() - } -} diff --git a/aws/rust-runtime/aws-auth/src/lib.rs b/aws/rust-runtime/aws-auth/src/lib.rs index 0bd4a1bc05..7196fa6b1f 100644 --- a/aws/rust-runtime/aws-auth/src/lib.rs +++ b/aws/rust-runtime/aws-auth/src/lib.rs @@ -3,8 +3,12 @@ * SPDX-License-Identifier: Apache-2.0. */ -mod credentials; pub mod middleware; pub mod provider; -pub use credentials::Credentials; +use aws_types::credentials::SharedCredentialsProvider; +use smithy_http::property_bag::PropertyBag; + +pub fn set_provider(bag: &mut PropertyBag, provider: SharedCredentialsProvider) { + bag.insert(provider); +} diff --git a/aws/rust-runtime/aws-auth/src/middleware.rs b/aws/rust-runtime/aws-auth/src/middleware.rs index e66570c0b9..a11fc6153e 100644 --- a/aws/rust-runtime/aws-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-auth/src/middleware.rs @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -use crate::provider::{CredentialsError, CredentialsProvider}; use smithy_http::middleware::AsyncMapRequest; use smithy_http::operation::Request; use std::future::Future; @@ -26,7 +25,10 @@ impl CredentialsStage { } async fn load_creds(mut request: Request) -> Result { - let provider = request.properties().get::().cloned(); + let provider = request + .properties() + .get::() + .cloned(); let provider = match provider { Some(provider) => provider, None => { @@ -51,7 +53,7 @@ impl CredentialsStage { } mod error { - use crate::provider::CredentialsError; + use aws_types::credentials::CredentialsError; use std::error::Error as StdError; use std::fmt; @@ -86,6 +88,7 @@ mod error { } } +use aws_types::credentials::{CredentialsError, ProvideCredentials, SharedCredentialsProvider}; pub use error::*; type BoxFuture = Pin + Send>>; @@ -102,12 +105,13 @@ impl AsyncMapRequest for CredentialsStage { #[cfg(test)] mod tests { use super::CredentialsStage; - use crate::provider::{async_provide_credentials_fn, set_provider, CredentialsError}; - use crate::Credentials; + use crate::provider::async_provide_credentials_fn; + use crate::set_provider; + use aws_types::credentials::{CredentialsError, SharedCredentialsProvider}; + use aws_types::Credentials; use smithy_http::body::SdkBody; use smithy_http::middleware::AsyncMapRequest; use smithy_http::operation; - use std::sync::Arc; #[tokio::test] async fn no_cred_provider_is_ok() { @@ -123,7 +127,7 @@ mod tests { let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); set_provider( &mut req.properties_mut(), - Arc::new(async_provide_credentials_fn(|| async { + SharedCredentialsProvider::new(async_provide_credentials_fn(|| async { Err(CredentialsError::Unhandled("whoops".into())) })), ); @@ -138,7 +142,7 @@ mod tests { let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); set_provider( &mut req.properties_mut(), - Arc::new(async_provide_credentials_fn(|| async { + SharedCredentialsProvider::new(async_provide_credentials_fn(|| async { Err(CredentialsError::CredentialsNotLoaded) })), ); @@ -153,7 +157,7 @@ mod tests { let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); set_provider( &mut req.properties_mut(), - Arc::new(Credentials::from_keys("test", "test", None)), + SharedCredentialsProvider::new(Credentials::from_keys("test", "test", None)), ); let req = CredentialsStage::new() .apply(req) diff --git a/aws/rust-runtime/aws-auth/src/provider.rs b/aws/rust-runtime/aws-auth/src/provider.rs index 6c2a9620e6..00c17885aa 100644 --- a/aws/rust-runtime/aws-auth/src/provider.rs +++ b/aws/rust-runtime/aws-auth/src/provider.rs @@ -8,139 +8,59 @@ //! Credentials providers acquire AWS credentials from environment variables, files, //! or calls to AWS services such as STS. Custom credential provider implementations can //! be provided by implementing [`ProvideCredentials`] for synchronous use-cases, or -//! [`AsyncProvideCredentials`] for async use-cases. Generic credential caching implementations, +//! [`ProvideCredentials`] for async use-cases. Generic credential caching implementations, //! for example, //! [`LazyCachingCredentialsProvider`](crate::provider::lazy_caching::LazyCachingCredentialsProvider), //! are also provided as part of this module. mod cache; -pub mod env; pub mod lazy_caching; mod time; -use crate::Credentials; -use smithy_http::property_bag::PropertyBag; -use std::error::Error; +use aws_types::credentials; +use aws_types::credentials::ProvideCredentials; + use std::fmt; -use std::fmt::{Debug, Display, Formatter}; -use std::future::{self, Future}; +use std::fmt::{Debug, Formatter}; +use std::future::Future; use std::marker::PhantomData; -use std::pin::Pin; -use std::sync::Arc; -use std::time::Duration; - -#[derive(Debug)] -#[non_exhaustive] -pub enum CredentialsError { - /// No credentials were available for this provider - CredentialsNotLoaded, - - /// Loading credentials from this provider exceeded the maximum allowed duration - ProviderTimedOut(Duration), - - /// The provider was given an invalid configuration - /// - /// For example: - /// - syntax error in ~/.aws/config - /// - assume role profile that forms an infinite loop - InvalidConfiguration(Box), - - /// The provider experienced an error during credential resolution - /// - /// This may include errors like a 503 from STS or a file system error when attempting to - /// read a configuration file. - ProviderError(Box), - - /// An unexpected error occured during credential resolution - /// - /// If the error is something that can occur during expected usage of a provider, `ProviderError` - /// should be returned instead. Unhandled is reserved for exceptional cases, for example: - /// - Returned data not UTF-8 - /// - A provider returns data that is missing required fields - Unhandled(Box), -} -impl Display for CredentialsError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - CredentialsError::CredentialsNotLoaded => { - write!(f, "The provider could not provide credentials or required configuration was not set") - } - CredentialsError::ProviderTimedOut(d) => write!( - f, - "Credentials provider timed out after {} seconds", - d.as_secs() - ), - CredentialsError::Unhandled(err) => write!(f, "Unexpected credentials error: {}", err), - CredentialsError::InvalidConfiguration(err) => { - write!(f, "The credentials provider was not properly: {}", err) - } - CredentialsError::ProviderError(err) => { - write!(f, "An error occured while loading credentials: {}", err) - } - } - } -} - -impl Error for CredentialsError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - CredentialsError::Unhandled(e) => Some(e.as_ref() as _), - _ => None, - } - } -} - -pub type CredentialsResult = Result; -pub type BoxFuture<'a, T> = Pin + Send + 'a>>; - -/// An asynchronous credentials provider -/// -/// If your use-case is synchronous, you should implement [`ProvideCredentials`] instead. Otherwise, -/// consider using [`async_provide_credentials_fn`] with a closure rather than directly implementing -/// this trait. -pub trait AsyncProvideCredentials: Send + Sync { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> - where - Self: 'a; -} - -pub type CredentialsProvider = Arc; - -/// A [`AsyncProvideCredentials`] implemented by a closure. +/// A [`ProvideCredentials`] implemented by a closure. /// /// See [`async_provide_credentials_fn`] for more details. #[derive(Copy, Clone)] -pub struct AsyncProvideCredentialsFn<'c, T, F> -where - T: Fn() -> F + Send + Sync + 'c, - F: Future + Send + 'static, -{ +pub struct AsyncProvideCredentialsFn<'c, T> { f: T, phantom: PhantomData<&'c T>, } -impl<'c, T, F> AsyncProvideCredentials for AsyncProvideCredentialsFn<'c, T, F> +impl Debug for AsyncProvideCredentialsFn<'_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ProvideCredentialsFn") + } +} + +impl<'c, T, F> ProvideCredentials for AsyncProvideCredentialsFn<'c, T> where T: Fn() -> F + Send + Sync + 'c, - F: Future + Send + 'static, + F: Future + Send + 'static, { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> + fn provide_credentials<'a>(&'a self) -> credentials::future::ProvideCredentials<'a> where Self: 'a, { - Box::pin((self.f)()) + credentials::future::ProvideCredentials::new((self.f)()) } } /// Returns a new [`AsyncProvideCredentialsFn`] with the given closure. This allows you -/// to create an [`AsyncProvideCredentials`] implementation from an async block that returns -/// a [`CredentialsResult`]. +/// to create an [`ProvideCredentials`] implementation from an async block that returns +/// a [`credentials::Result`]. /// /// # Example /// /// ``` -/// use aws_auth::Credentials; +/// use aws_types::Credentials; /// use aws_auth::provider::async_provide_credentials_fn; /// /// async fn load_credentials() -> Credentials { @@ -153,10 +73,10 @@ where /// Ok(credentials) /// }); /// ``` -pub fn async_provide_credentials_fn<'c, T, F>(f: T) -> AsyncProvideCredentialsFn<'c, T, F> +pub fn async_provide_credentials_fn<'c, T, F>(f: T) -> AsyncProvideCredentialsFn<'c, T> where T: Fn() -> F + Send + Sync + 'c, - F: Future + Send + 'static, + F: Future + Send + 'static, { AsyncProvideCredentialsFn { f, @@ -164,49 +84,13 @@ where } } -/// A synchronous credentials provider -/// -/// This is offered as a convenience for credential provider implementations that don't -/// need to be async. Otherwise, implement [`AsyncProvideCredentials`]. -pub trait ProvideCredentials: Send + Sync { - fn provide_credentials(&self) -> Result; -} - -impl AsyncProvideCredentials for T -where - T: ProvideCredentials, -{ - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> - where - Self: 'a, - { - let result = self.provide_credentials(); - Box::pin(future::ready(result)) - } -} - -pub fn default_provider() -> impl AsyncProvideCredentials { - // TODO: this should be a chain based on the CRT - env::EnvironmentVariableCredentialsProvider::new() -} - -impl ProvideCredentials for Credentials { - fn provide_credentials(&self) -> Result { - Ok(self.clone()) - } -} - -pub fn set_provider(config: &mut PropertyBag, provider: Arc) { - config.insert(provider); -} - #[cfg(test)] mod test { - use crate::provider::{ - async_provide_credentials_fn, AsyncProvideCredentials, BoxFuture, CredentialsResult, - }; - use crate::Credentials; + use crate::provider::async_provide_credentials_fn; use async_trait::async_trait; + use aws_types::credentials::ProvideCredentials; + use aws_types::{credentials, Credentials}; + use std::fmt::{Debug, Formatter}; fn assert_send_sync() {} @@ -224,13 +108,20 @@ mod test { inner: T, } - impl AsyncProvideCredentials for AnotherTraitWrapper { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> + impl Debug for AnotherTraitWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "wrapper") + } + } + + impl ProvideCredentials for AnotherTraitWrapper { + fn provide_credentials<'a>(&'a self) -> credentials::future::ProvideCredentials<'a> where Self: 'a, { - let inner_fut = self.inner.creds(); - Box::pin(async move { Ok(inner_fut.await) }) + credentials::future::ProvideCredentials::new( + async move { Ok(self.inner.creds().await) }, + ) } } @@ -238,7 +129,7 @@ mod test { #[tokio::test] async fn async_provide_credentials_fn_closure_can_borrow() { fn check_is_str_ref(_input: &str) {} - async fn test_async_provider(input: String) -> CredentialsResult { + async fn test_async_provider(input: String) -> credentials::Result { Ok(Credentials::from_keys(&input, &input, None)) } diff --git a/aws/rust-runtime/aws-auth/src/provider/cache.rs b/aws/rust-runtime/aws-auth/src/provider/cache.rs index acad121b2e..5b4c560ac6 100644 --- a/aws/rust-runtime/aws-auth/src/provider/cache.rs +++ b/aws/rust-runtime/aws-auth/src/provider/cache.rs @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0. */ -use crate::provider::{CredentialsError, CredentialsResult}; -use crate::Credentials; +use aws_types::credentials::CredentialsError; +use aws_types::{credentials, Credentials}; use std::future::Future; use std::sync::Arc; use std::time::{Duration, SystemTime}; use tokio::sync::{OnceCell, RwLock}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub(super) struct Cache { /// Amount of time before the actual credential expiration time /// where credentials are considered expired. @@ -41,7 +41,7 @@ impl Cache { /// and the others will await that thread's result rather than multiple refreshes occurring. /// The function given to acquire a credentials future, `f`, will not be called /// if another thread is chosen to load the credentials. - pub async fn get_or_load(&self, f: F) -> CredentialsResult + pub async fn get_or_load(&self, f: F) -> credentials::Result where F: FnOnce() -> Fut, Fut: Future>, @@ -84,8 +84,8 @@ fn expired(expiration: SystemTime, buffer_time: Duration, now: SystemTime) -> bo #[cfg(test)] mod tests { use super::{expired, Cache}; - use crate::provider::CredentialsError; - use crate::Credentials; + use aws_types::credentials::CredentialsError; + use aws_types::Credentials; use std::time::{Duration, SystemTime}; fn credentials(expired_secs: u64) -> Result<(Credentials, SystemTime), CredentialsError> { diff --git a/aws/rust-runtime/aws-auth/src/provider/lazy_caching.rs b/aws/rust-runtime/aws-auth/src/provider/lazy_caching.rs index ff11dc7b21..d4b310d540 100644 --- a/aws/rust-runtime/aws-auth/src/provider/lazy_caching.rs +++ b/aws/rust-runtime/aws-auth/src/provider/lazy_caching.rs @@ -7,7 +7,7 @@ use crate::provider::cache::Cache; use crate::provider::time::TimeSource; -use crate::provider::{AsyncProvideCredentials, BoxFuture, CredentialsError, CredentialsResult}; +use aws_types::credentials::{future, CredentialsError, ProvideCredentials}; use smithy_async::future::timeout::Timeout; use smithy_async::rt::sleep::AsyncSleep; use std::sync::Arc; @@ -18,17 +18,18 @@ const DEFAULT_LOAD_TIMEOUT: Duration = Duration::from_secs(5); const DEFAULT_CREDENTIAL_EXPIRATION: Duration = Duration::from_secs(15 * 60); const DEFAULT_BUFFER_TIME: Duration = Duration::from_secs(10); -/// `LazyCachingCredentialsProvider` implements [`AsyncProvideCredentials`] by caching -/// credentials that it loads by calling a user-provided [`AsyncProvideCredentials`] implementation. +/// `LazyCachingCredentialsProvider` implements [`ProvideCredentials`] by caching +/// credentials that it loads by calling a user-provided [`ProvideCredentials`] implementation. /// -/// For example, you can provide an [`AsyncProvideCredentials`] implementation that calls +/// For example, you can provide an [`ProvideCredentials`] implementation that calls /// AWS STS's AssumeRole operation to get temporary credentials, and `LazyCachingCredentialsProvider` /// will cache those credentials until they expire. +#[derive(Debug)] pub struct LazyCachingCredentialsProvider { time: Box, sleeper: Box, cache: Cache, - loader: Arc, + loader: Arc, load_timeout: Duration, default_credential_expiration: Duration, } @@ -37,7 +38,7 @@ impl LazyCachingCredentialsProvider { fn new( time: impl TimeSource, sleeper: Box, - loader: Arc, + loader: Arc, load_timeout: Duration, default_credential_expiration: Duration, buffer_time: Duration, @@ -58,8 +59,8 @@ impl LazyCachingCredentialsProvider { } } -impl AsyncProvideCredentials for LazyCachingCredentialsProvider { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> +impl ProvideCredentials for LazyCachingCredentialsProvider { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a, { @@ -70,7 +71,7 @@ impl AsyncProvideCredentials for LazyCachingCredentialsProvider { let cache = self.cache.clone(); let default_credential_expiration = self.default_credential_expiration; - Box::pin(async move { + future::ProvideCredentials::new(async move { // Attempt to get cached credentials, or clear the cache if they're expired if let Some(credentials) = cache.yield_or_clear_if_expired(now).await { Ok(credentials) @@ -109,7 +110,7 @@ pub mod builder { DEFAULT_LOAD_TIMEOUT, }; use crate::provider::time::SystemTimeSource; - use crate::provider::AsyncProvideCredentials; + use aws_types::credentials::ProvideCredentials; use smithy_async::rt::sleep::{default_async_sleep, AsyncSleep}; use std::sync::Arc; use std::time::Duration; @@ -119,11 +120,9 @@ pub mod builder { /// # Example /// /// ``` - /// use aws_auth::Credentials; /// use aws_auth::provider::async_provide_credentials_fn; /// use aws_auth::provider::lazy_caching::LazyCachingCredentialsProvider; - /// use std::sync::Arc; - /// use std::time::Duration; + /// use aws_types::Credentials; /// /// let provider = LazyCachingCredentialsProvider::builder() /// .load(async_provide_credentials_fn(|| async { @@ -135,7 +134,7 @@ pub mod builder { #[derive(Default)] pub struct Builder { sleep: Option>, - load: Option>, + load: Option>, load_timeout: Option, buffer_time: Option, default_credential_expiration: Option, @@ -146,9 +145,9 @@ pub mod builder { Default::default() } - /// An implementation of [`AsyncProvideCredentials`] that will be used to load + /// An implementation of [`ProvideCredentials`] that will be used to load /// the cached credentials once they're expired. - pub fn load(mut self, loader: impl AsyncProvideCredentials + 'static) -> Self { + pub fn load(mut self, loader: impl ProvideCredentials + 'static) -> Self { self.load = Some(Arc::new(loader)); self } @@ -162,7 +161,7 @@ pub mod builder { self } - /// (Optional) Timeout for the given [`AsyncProvideCredentials`] implementation. + /// (Optional) Timeout for the given [`ProvideCredentials`] implementation. /// Defaults to 5 seconds. pub fn load_timeout(mut self, timeout: Duration) -> Self { self.load_timeout = Some(timeout); @@ -179,7 +178,7 @@ pub mod builder { } /// (Optional) Default expiration time to set on credentials if they don't - /// have an expiration time. This is only used if the given [`AsyncProvideCredentials`] + /// have an expiration time. This is only used if the given [`ProvideCredentials`] /// returns [`Credentials`](crate::Credentials) that don't have their `expiry` set. /// This must be at least 15 minutes. pub fn default_credential_expiration(mut self, duration: Duration) -> Self { @@ -217,20 +216,18 @@ pub mod builder { #[cfg(test)] mod tests { + use crate::provider::async_provide_credentials_fn; use crate::provider::lazy_caching::{ LazyCachingCredentialsProvider, TimeSource, DEFAULT_BUFFER_TIME, DEFAULT_CREDENTIAL_EXPIRATION, DEFAULT_LOAD_TIMEOUT, }; - use crate::provider::{ - async_provide_credentials_fn, AsyncProvideCredentials, CredentialsError, CredentialsResult, - }; - use crate::Credentials; + use aws_types::credentials::{self, Credentials, CredentialsError, ProvideCredentials}; use smithy_async::rt::sleep::TokioSleep; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use tracing::info; - #[derive(Clone)] + #[derive(Clone, Debug)] struct TestTime { time: Arc>, } @@ -255,7 +252,7 @@ mod tests { fn test_provider( time: T, - load_list: Vec, + load_list: Vec, ) -> LazyCachingCredentialsProvider { let load_list = Arc::new(Mutex::new(load_list)); LazyCachingCredentialsProvider::new( diff --git a/aws/rust-runtime/aws-auth/src/provider/time.rs b/aws/rust-runtime/aws-auth/src/provider/time.rs index 6ca7e9ec29..c4f120bf68 100644 --- a/aws/rust-runtime/aws-auth/src/provider/time.rs +++ b/aws/rust-runtime/aws-auth/src/provider/time.rs @@ -3,14 +3,15 @@ * SPDX-License-Identifier: Apache-2.0. */ +use std::fmt::Debug; use std::time::SystemTime; /// Allows us to abstract time for tests. -pub(super) trait TimeSource: Send + Sync + 'static { +pub(super) trait TimeSource: Send + Sync + Debug + 'static { fn now(&self) -> SystemTime; } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub(super) struct SystemTimeSource; impl TimeSource for SystemTimeSource { diff --git a/aws/rust-runtime/aws-config/src/default_provider.rs b/aws/rust-runtime/aws-config/src/default_provider.rs index 477d9d633f..b82c54faaa 100644 --- a/aws/rust-runtime/aws-config/src/default_provider.rs +++ b/aws/rust-runtime/aws-config/src/default_provider.rs @@ -5,8 +5,8 @@ //! Default Provider chains for [`region`](default_provider::region) and credentials (TODO) +/// Default region provider chain pub mod region { - //! Default region provider chain use crate::environment::region::EnvironmentVariableRegionProvider; use crate::meta::region::ProvideRegion; @@ -18,3 +18,16 @@ pub mod region { EnvironmentVariableRegionProvider::new() } } + +/// Default credentials provider chain +pub mod credentials { + use crate::environment::credentials::EnvironmentVariableCredentialsProvider; + use aws_types::credentials::ProvideCredentials; + + /// Default Region Provider chain + /// + /// This provider will load region from environment variables. + pub fn default_provider() -> impl ProvideCredentials { + EnvironmentVariableCredentialsProvider::new() + } +} diff --git a/aws/rust-runtime/aws-auth/src/provider/env.rs b/aws/rust-runtime/aws-config/src/environment/credentials.rs similarity index 69% rename from aws/rust-runtime/aws-auth/src/provider/env.rs rename to aws/rust-runtime/aws-config/src/environment/credentials.rs index d2741588a3..dd15ae1942 100644 --- a/aws/rust-runtime/aws-auth/src/provider/env.rs +++ b/aws/rust-runtime/aws-config/src/environment/credentials.rs @@ -3,23 +3,53 @@ * SPDX-License-Identifier: Apache-2.0. */ -//! Credential provider implementation that pulls from environment variables +use std::env::VarError; -use crate::provider::{CredentialsError, ProvideCredentials}; -use crate::Credentials; +use aws_types::credentials::future; +use aws_types::credentials::{CredentialsError, ProvideCredentials}; use aws_types::os_shim_internal::Env; -use std::env::VarError; +use aws_types::{credentials, Credentials}; /// Load Credentials from Environment Variables +/// +/// `EnvironmentVariableCredentialsProvider` uses the following variables: +/// - `AWS_ACCESS_KEY_ID` +/// - `AWS_SECRET_ACCESS_KEY` with fallback to `SECRET_ACCESS_KEY` +/// - `AWS_SESSION_TOKEN` +#[derive(Debug)] pub struct EnvironmentVariableCredentialsProvider { env: Env, } impl EnvironmentVariableCredentialsProvider { + fn credentials(&self) -> credentials::Result { + let access_key = self.env.get("AWS_ACCESS_KEY_ID").map_err(to_cred_error)?; + let secret_key = self + .env + .get("AWS_SECRET_ACCESS_KEY") + .or_else(|_| self.env.get("SECRET_ACCESS_KEY")) + .map_err(to_cred_error)?; + let session_token = self.env.get("AWS_SESSION_TOKEN").ok(); + Ok(Credentials::new( + access_key, + secret_key, + session_token, + None, + ENV_PROVIDER, + )) + } +} + +impl EnvironmentVariableCredentialsProvider { + /// Create a `EnvironmentVariableCredentialsProvider` pub fn new() -> Self { Self::new_with_env(Env::real()) } + #[doc(hidden)] + /// Create a new `EnvironmentVariableCredentialsProvider` with `Env` overriden + /// + /// This function is intended for tests that mock out the process environment. pub fn new_with_env(env: Env) -> Self { Self { env } } @@ -34,21 +64,11 @@ impl Default for EnvironmentVariableCredentialsProvider { const ENV_PROVIDER: &str = "EnvironmentVariable"; impl ProvideCredentials for EnvironmentVariableCredentialsProvider { - fn provide_credentials(&self) -> Result { - let access_key = self.env.get("AWS_ACCESS_KEY_ID").map_err(to_cred_error)?; - let secret_key = self - .env - .get("AWS_SECRET_ACCESS_KEY") - .or_else(|_| self.env.get("SECRET_ACCESS_KEY")) - .map_err(to_cred_error)?; - let session_token = self.env.get("AWS_SESSION_TOKEN").ok(); - Ok(Credentials::new( - access_key, - secret_key, - session_token, - None, - ENV_PROVIDER, - )) + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::ready(self.credentials()) } } @@ -61,9 +81,11 @@ fn to_cred_error(err: VarError) -> CredentialsError { #[cfg(test)] mod test { - use super::EnvironmentVariableCredentialsProvider; - use crate::provider::{CredentialsError, ProvideCredentials}; + use aws_types::credentials::{CredentialsError, ProvideCredentials}; use aws_types::os_shim_internal::Env; + use futures_util::FutureExt; + + use super::EnvironmentVariableCredentialsProvider; fn make_provider(vars: &[(&str, &str)]) -> EnvironmentVariableCredentialsProvider { EnvironmentVariableCredentialsProvider { @@ -77,7 +99,11 @@ mod test { ("AWS_ACCESS_KEY_ID", "access"), ("AWS_SECRET_ACCESS_KEY", "secret"), ]); - let creds = provider.provide_credentials().expect("valid credentials"); + let creds = provider + .provide_credentials() + .now_or_never() + .unwrap() + .expect("valid credentials"); assert_eq!(creds.session_token(), None); assert_eq!(creds.access_key_id(), "access"); assert_eq!(creds.secret_access_key(), "secret"); @@ -91,7 +117,11 @@ mod test { ("AWS_SESSION_TOKEN", "token"), ]); - let creds = provider.provide_credentials().expect("valid credentials"); + let creds = provider + .provide_credentials() + .now_or_never() + .unwrap() + .expect("valid credentials"); assert_eq!(creds.session_token().unwrap(), "token"); assert_eq!(creds.access_key_id(), "access"); assert_eq!(creds.secret_access_key(), "secret"); @@ -105,7 +135,11 @@ mod test { ("AWS_SESSION_TOKEN", "token"), ]); - let creds = provider.provide_credentials().expect("valid credentials"); + let creds = provider + .provide_credentials() + .now_or_never() + .unwrap() + .expect("valid credentials"); assert_eq!(creds.session_token().unwrap(), "token"); assert_eq!(creds.access_key_id(), "access"); assert_eq!(creds.secret_access_key(), "secret"); @@ -116,6 +150,8 @@ mod test { let provider = make_provider(&[]); let err = provider .provide_credentials() + .now_or_never() + .unwrap() .expect_err("no credentials defined"); if let CredentialsError::Unhandled(_) = err { panic!("wrong error type") diff --git a/aws/rust-runtime/aws-config/src/environment/mod.rs b/aws/rust-runtime/aws-config/src/environment/mod.rs index fd9ece3c57..39131f865a 100644 --- a/aws/rust-runtime/aws-config/src/environment/mod.rs +++ b/aws/rust-runtime/aws-config/src/environment/mod.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ -// TODO: -// pub mod credentials; +/// Load credentials from the environment +pub mod credentials; /// Load regions from the environment pub mod region; diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index acd6c948ad..58a187e4e2 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -76,9 +76,10 @@ pub async fn load_from_env() -> aws_types::config::Config { pub use loader::ConfigLoader; mod loader { - use crate::default_provider::region; + use crate::default_provider::{credentials, region}; use crate::meta::region::ProvideRegion; use aws_types::config::Config; + use aws_types::credentials::{ProvideCredentials, SharedCredentialsProvider}; /// Load a cross-service [`Config`](aws_types::config::Config) from the environment /// @@ -89,12 +90,13 @@ mod loader { #[derive(Default, Debug)] pub struct ConfigLoader { region: Option>, + credentials_provider: Option, } impl ConfigLoader { - /// Override the region used to construct the [`Config`](aws_types::config::Config). + /// Override the region used to build [`Config`](aws_types::config::Config). /// - /// ## Example + /// # Example /// ```rust /// # async fn create_config() { /// use aws_types::region::Region; @@ -108,6 +110,25 @@ mod loader { self } + /// Override the credentials provider used to build [`Config`](aws_types::config::Config). + /// # Example + /// Override the credentials provider but load the default value for region: + /// ```rust + /// # use aws_types::Credentials; + /// async fn create_config() { + /// let config = aws_config::from_env() + /// .credentials_provider(Credentials::from_keys("accesskey", "secretkey", None)) + /// .load().await; + /// # } + /// ``` + pub fn credentials_provider( + mut self, + credentials_provider: impl ProvideCredentials + 'static, + ) -> Self { + self.credentials_provider = Some(SharedCredentialsProvider::new(credentials_provider)); + self + } + /// Load the default configuration chain /// /// If fields have been overridden during builder construction, the override values will be used. @@ -123,7 +144,15 @@ mod loader { } else { region::default_provider().region().await }; - Config::builder().region(region).build() + let credentials_provider = if let Some(provider) = self.credentials_provider { + provider + } else { + SharedCredentialsProvider::new(credentials::default_provider()) + }; + Config::builder() + .region(region) + .credentials_provider(credentials_provider) + .build() } } } diff --git a/aws/rust-runtime/aws-inlineable/Cargo.toml b/aws/rust-runtime/aws-inlineable/Cargo.toml index 05de299c5c..fbc2b449e7 100644 --- a/aws/rust-runtime/aws-inlineable/Cargo.toml +++ b/aws/rust-runtime/aws-inlineable/Cargo.toml @@ -12,3 +12,4 @@ are to allow this crate to be compilable and testable in isolation, no client co smithy-xml = { path = "../../../rust-runtime/smithy-xml" } smithy-types = { path = "../../../rust-runtime/smithy-types" } http = "0.2.4" +aws-types = { path = "../../rust-runtime/aws-types" } diff --git a/aws/rust-runtime/aws-inlineable/src/lib.rs b/aws/rust-runtime/aws-inlineable/src/lib.rs index d19a648709..881e9dc470 100644 --- a/aws/rust-runtime/aws-inlineable/src/lib.rs +++ b/aws/rust-runtime/aws-inlineable/src/lib.rs @@ -3,5 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ -#[allow(dead_code)] +#![allow(dead_code)] + +mod no_credentials; mod s3_errors; diff --git a/aws/rust-runtime/aws-inlineable/src/no_credentials.rs b/aws/rust-runtime/aws-inlineable/src/no_credentials.rs new file mode 100644 index 0000000000..085dc571b9 --- /dev/null +++ b/aws/rust-runtime/aws-inlineable/src/no_credentials.rs @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use aws_types::credentials::future; +use aws_types::credentials::{CredentialsError, ProvideCredentials}; + +/// Stub Provider for use when no credentials provider is used +#[non_exhaustive] +pub struct NoCredentials; + +impl ProvideCredentials for NoCredentials { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::ready(Err(CredentialsError::CredentialsNotLoaded)) + } +} diff --git a/aws/rust-runtime/aws-types/src/credentials/provider.rs b/aws/rust-runtime/aws-types/src/credentials/provider.rs index 2045e60f81..5b9e7ca033 100644 --- a/aws/rust-runtime/aws-types/src/credentials/provider.rs +++ b/aws/rust-runtime/aws-types/src/credentials/provider.rs @@ -5,7 +5,7 @@ use crate::Credentials; use std::error::Error; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; use std::sync::Arc; use std::time::Duration; @@ -102,7 +102,7 @@ pub mod future { } /// Asynchronous Credentials Provider -pub trait ProvideCredentials: Send + Sync { +pub trait ProvideCredentials: Send + Sync + Debug { fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> where Self: 'a; @@ -130,7 +130,7 @@ impl ProvideCredentials for Arc { /// /// Newtype wrapper around ProvideCredentials that implements Clone using an internal /// Arc. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct SharedCredentialsProvider(Arc); impl SharedCredentialsProvider { diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt index 30d9536bf2..489b153afe 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt @@ -8,8 +8,8 @@ package software.amazon.smithy.rustsdk import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.rustlang.asType -import software.amazon.smithy.rust.codegen.rustlang.docs import software.amazon.smithy.rust.codegen.rustlang.rust +import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.rustlang.writable import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.smithy.RuntimeType @@ -53,32 +53,41 @@ class CredentialsProviderDecorator : RustCodegenDecorator { * Add a `.credentials_provider` field and builder to the `Config` for a given service */ class CredentialProviderConfig(runtimeConfig: RuntimeConfig) : ConfigCustomization() { - private val credentialsProvider = credentialsProvider(runtimeConfig) - private val defaultProvider = defaultProvider(runtimeConfig) + private val defaultProvider = defaultProvider() + private val codegenScope = arrayOf( + "credentials" to awsTypes(runtimeConfig).asType().member("credentials"), + "DefaultProvider" to defaultProvider + ) + override fun section(section: ServiceConfig) = writable { when (section) { - is ServiceConfig.ConfigStruct -> rust( - """pub(crate) credentials_provider: std::sync::Arc,""", - credentialsProvider + is ServiceConfig.ConfigStruct -> rustTemplate( + """pub(crate) credentials_provider: #{credentials}::SharedCredentialsProvider,""", + *codegenScope ) is ServiceConfig.ConfigImpl -> emptySection is ServiceConfig.BuilderStruct -> - rust("credentials_provider: Option>,", credentialsProvider) + rustTemplate("credentials_provider: Option<#{credentials}::SharedCredentialsProvider>,", *codegenScope) ServiceConfig.BuilderImpl -> { - docs("""Set the credentials provider for this service""") - rust( + rustTemplate( """ - pub fn credentials_provider(mut self, credentials_provider: impl #T + 'static) -> Self { - self.credentials_provider = Some(std::sync::Arc::new(credentials_provider)); + /// Set the credentials provider for this service + pub fn credentials_provider(mut self, credentials_provider: impl #{credentials}::ProvideCredentials + 'static) -> Self { + self.credentials_provider = Some(#{credentials}::SharedCredentialsProvider::new(credentials_provider)); + self + } + + pub fn set_credentials_provider(&mut self, credentials_provider: Option<#{credentials}::SharedCredentialsProvider>) -> &mut Self { + self.credentials_provider = credentials_provider; self } """, - credentialsProvider, + *codegenScope, ) } - ServiceConfig.BuilderBuild -> rust( - "credentials_provider: self.credentials_provider.unwrap_or_else(|| std::sync::Arc::new(#T())),", - defaultProvider + ServiceConfig.BuilderBuild -> rustTemplate( + "credentials_provider: self.credentials_provider.unwrap_or_else(|| #{credentials}::SharedCredentialsProvider::new(#{DefaultProvider})),", + *codegenScope ) } } @@ -103,18 +112,16 @@ class CredentialsProviderFeature(private val runtimeConfig: RuntimeConfig) : Ope class PubUseCredentials(private val runtimeConfig: RuntimeConfig) : LibRsCustomization() { override fun section(section: LibRsSection): Writable { return when (section) { - is LibRsSection.Body -> writable { rust("pub use #T::Credentials;", awsAuth(runtimeConfig).asType()) } + is LibRsSection.Body -> writable { rust("pub use #T::Credentials;", awsTypes(runtimeConfig).asType()) } else -> emptySection } } } fun awsAuth(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeDependency("aws-auth") -fun credentialsProvider(runtimeConfig: RuntimeConfig) = - RuntimeType("AsyncProvideCredentials", awsAuth(runtimeConfig), "aws_auth::provider") -fun defaultProvider(runtimeConfig: RuntimeConfig) = - RuntimeType("default_provider", awsAuth(runtimeConfig), "aws_auth::provider") +fun defaultProvider() = + RuntimeType.forInlineDependency(InlineAwsDependency.forRustFile("no_credentials")).member("NoCredentials") fun setProvider(runtimeConfig: RuntimeConfig) = - RuntimeType("set_provider", awsAuth(runtimeConfig), "aws_auth::provider") + RuntimeType("set_provider", awsAuth(runtimeConfig), "aws_auth") diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SharedConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SharedConfigDecorator.kt index 9ccf35ae4c..5f28beefc7 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SharedConfigDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SharedConfigDecorator.kt @@ -46,6 +46,7 @@ class SharedConfigDecorator : RustCodegenDecorator { fn from(input: &#{Config}) -> Self { let mut builder = Builder::default(); builder = builder.region(input.region().cloned()); + builder.set_credentials_provider(input.credentials_provider().cloned()); builder } } From 0249ffb0325190cc4f9ff644d7d4cb0c50bee54d Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 12:14:25 -0400 Subject: [PATCH 03/18] Add CredentialsChainProvider to aws-config --- .../aws-config/src/meta/credentials/chain.rs | 102 ++++++++++++++++++ .../aws-config/src/meta/credentials/mod.rs | 18 ++++ aws/rust-runtime/aws-config/src/meta/mod.rs | 3 + 3 files changed, 123 insertions(+) create mode 100644 aws/rust-runtime/aws-config/src/meta/credentials/chain.rs create mode 100644 aws/rust-runtime/aws-config/src/meta/credentials/mod.rs diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs b/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs new file mode 100644 index 0000000000..9f6986648a --- /dev/null +++ b/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use std::borrow::Cow; + +use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials}; +use tracing::Instrument; + +/// Credentials provider that checks a series of inner providers +/// +/// Each provider will be evaluated in order: +/// * If a provider returns valid [`Credentials`](aws_types::Credentials) they will be returned immediately. +/// No other credential providers will be used. +/// * Otherwise, if a provider returns +/// [`CredentialsError::CredentialsNotLoaded`](aws_types::credentials::CredentialsError::CredentialsNotLoaded), +/// the next provider will be checked. +/// * Finally, if a provider returns any other error condition, an error will be returned immediately. +/// +/// ## Example +/// ```rust +/// use aws_config::meta::credentials::CredentialsProviderChain; +/// use aws_types::Credentials; +/// use aws_config::environment; +/// use aws_config::environment::credentials::EnvironmentVariableCredentialsProvider; +/// let provider = CredentialsProviderChain::first_try("Environment", EnvironmentVariableCredentialsProvider::new()) +/// .or_else("Static", Credentials::from_keys("someacceskeyid", "somesecret", None)); +/// ``` +#[derive(Debug)] +pub struct CredentialsProviderChain { + providers: Vec<(Cow<'static, str>, Box)>, +} + +impl CredentialsProviderChain { + /// Create a `CredentialsProviderChain` that begins by evaluating this provider + pub fn first_try( + name: impl Into>, + provider: impl ProvideCredentials + 'static, + ) -> Self { + CredentialsProviderChain { + providers: vec![(name.into(), Box::new(provider))], + } + } + + /// Add a fallback provider to the credentials provider chain + pub fn or_else( + mut self, + name: impl Into>, + provider: impl ProvideCredentials + 'static, + ) -> Self { + self.providers.push((name.into(), Box::new(provider))); + self + } + + #[cfg(feature = "default-provider")] + /// Add a fallback to the default provider chain + pub fn or_default_provider(self) -> Self { + self.or_else( + "DefaultProviderChain", + crate::default_provider::credentials::default_provider(), + ) + } + + #[cfg(feature = "default-provider")] + /// Creates a credential provider chain that starts with the default provider + pub fn default_provider() -> Self { + Self::first_try( + "DefaultProviderChain", + crate::default_provider::credentials::default_provider(), + ) + } + + async fn credentials(&self) -> credentials::Result { + for (name, provider) in &self.providers { + let span = tracing::info_span!("load_credentials", provider = %name); + match provider.provide_credentials().instrument(span).await { + Ok(credentials) => { + tracing::info!(provider = %name, "loaded credentials"); + return Ok(credentials); + } + Err(CredentialsError::CredentialsNotLoaded) => { + tracing::info!(provider = %name, "provider in chain did not provide credentials"); + } + Err(e) => { + tracing::warn!(provider = %name, error = %e, "provider failed to provide credentials"); + return Err(e); + } + } + } + return Err(CredentialsError::CredentialsNotLoaded); + } +} + +impl ProvideCredentials for CredentialsProviderChain { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials + where + Self: 'a, + { + future::ProvideCredentials::new(self.credentials()) + } +} diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs new file mode 100644 index 0000000000..30968e3c33 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +mod chain; +pub use chain::CredentialsProviderChain; + +// pub mod credential_fn; +// pub mod lazy_caching; + +// mod cache; +// mod time; diff --git a/aws/rust-runtime/aws-config/src/meta/mod.rs b/aws/rust-runtime/aws-config/src/meta/mod.rs index 1444e9a24f..6704e1cb7d 100644 --- a/aws/rust-runtime/aws-config/src/meta/mod.rs +++ b/aws/rust-runtime/aws-config/src/meta/mod.rs @@ -6,6 +6,9 @@ /// Region Providers pub mod region; +/// Credential Providers +pub mod credentials; + // coming soon: // pub mod credentials: // - CredentialProviderChain From 6b1ca7b5dcf9ae425cb6f3130fdaeda6bd4183ff Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 13:12:54 -0400 Subject: [PATCH 04/18] Move lazy_caching and async_provide_credentials_fn to config --- .../aws-auth-providers/src/chain.rs | 2 +- .../src/default_provider_chain.rs | 2 +- .../aws-auth-providers/src/profile/exec.rs | 2 +- .../aws-auth-providers/src/profile/repr.rs | 2 +- .../aws-auth-providers/src/sts_util.rs | 4 +- .../aws-auth-providers/src/test_case.rs | 4 +- aws/rust-runtime/aws-auth/src/lib.rs | 3 +- aws/rust-runtime/aws-auth/src/middleware.rs | 38 +++++++++++--- aws/rust-runtime/aws-config/Cargo.toml | 6 +++ .../src/meta/credentials/credential_fn.rs} | 27 ++-------- .../src/meta/credentials}/lazy_caching.rs | 50 +++++++++++-------- .../meta/credentials/lazy_caching}/cache.rs | 0 .../meta/credentials/lazy_caching}/time.rs | 6 ++- .../aws-config/src/meta/credentials/mod.rs | 6 +++ aws/rust-runtime/aws-hyper/tests/e2e_test.rs | 2 +- .../aws-inlineable/src/no_credentials.rs | 1 + .../aws-sig-auth/src/event_stream.rs | 4 +- .../aws-sig-auth/src/middleware.rs | 4 +- aws/rust-runtime/aws-sig-auth/src/signer.rs | 2 +- .../kms/tests/integration.rs | 2 +- .../qldbsession/tests/integration.rs | 2 +- 21 files changed, 99 insertions(+), 70 deletions(-) rename aws/rust-runtime/{aws-auth/src/provider.rs => aws-config/src/meta/credentials/credential_fn.rs} (79%) rename aws/rust-runtime/{aws-auth/src/provider => aws-config/src/meta/credentials}/lazy_caching.rs (96%) rename aws/rust-runtime/{aws-auth/src/provider => aws-config/src/meta/credentials/lazy_caching}/cache.rs (100%) rename aws/rust-runtime/{aws-auth/src/provider => aws-config/src/meta/credentials/lazy_caching}/time.rs (70%) diff --git a/aws/rust-runtime/aws-auth-providers/src/chain.rs b/aws/rust-runtime/aws-auth-providers/src/chain.rs index b1157bc92c..f55988ccd6 100644 --- a/aws/rust-runtime/aws-auth-providers/src/chain.rs +++ b/aws/rust-runtime/aws-auth-providers/src/chain.rs @@ -17,7 +17,7 @@ use tracing::Instrument; /// ```rust /// use aws_auth_providers::chain::ChainProvider; /// use aws_auth::provider::env::EnvironmentVariableCredentialsProvider; -/// use aws_auth::Credentials; +/// use aws_types::Credentials; /// let provider = ChainProvider::first_try("Environment", EnvironmentVariableCredentialsProvider::new()) /// .or_else("Static", Credentials::from_keys("someacceskeyid", "somesecret", None)); /// ``` diff --git a/aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs b/aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs index a9cb4add81..2ac36568f8 100644 --- a/aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs +++ b/aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs @@ -112,7 +112,7 @@ impl Builder { /// also be used. Using custom sources must be registered: /// ```rust /// use aws_auth::provider::{ProvideCredentials, CredentialsError}; - /// use aws_auth::Credentials; + /// use aws_types::Credentials; /// use aws_auth_providers::DefaultProviderChain; /// struct MyCustomProvider; /// // there is a blanket implementation for `AsyncProvideCredentials` on ProvideCredentials diff --git a/aws/rust-runtime/aws-auth-providers/src/profile/exec.rs b/aws/rust-runtime/aws-auth-providers/src/profile/exec.rs index 249dac9221..f8d7a55184 100644 --- a/aws/rust-runtime/aws-auth-providers/src/profile/exec.rs +++ b/aws/rust-runtime/aws-auth-providers/src/profile/exec.rs @@ -6,11 +6,11 @@ use std::sync::Arc; use aws_auth::provider::{AsyncProvideCredentials, CredentialsError, CredentialsResult}; -use aws_auth::Credentials; use aws_hyper::{DynConnector, StandardClient}; use aws_sdk_sts::operation::AssumeRole; use aws_sdk_sts::Config; use aws_types::region::Region; +use aws_types::Credentials; use crate::profile::repr::BaseProvider; use crate::profile::ProfileFileError; diff --git a/aws/rust-runtime/aws-auth-providers/src/profile/repr.rs b/aws/rust-runtime/aws-auth-providers/src/profile/repr.rs index c596ceb6d5..187edb165a 100644 --- a/aws/rust-runtime/aws-auth-providers/src/profile/repr.rs +++ b/aws/rust-runtime/aws-auth-providers/src/profile/repr.rs @@ -13,8 +13,8 @@ //! multiple actions into the same profile). use crate::profile::ProfileFileError; -use aws_auth::Credentials; use aws_types::profile::{Profile, ProfileSet}; +use aws_types::Credentials; /// Chain of Profile Providers /// diff --git a/aws/rust-runtime/aws-auth-providers/src/sts_util.rs b/aws/rust-runtime/aws-auth-providers/src/sts_util.rs index 3d082c9d95..93b477053a 100644 --- a/aws/rust-runtime/aws-auth-providers/src/sts_util.rs +++ b/aws/rust-runtime/aws-auth-providers/src/sts_util.rs @@ -4,11 +4,11 @@ */ use aws_auth::provider::{CredentialsError, CredentialsResult}; -use aws_auth::Credentials as AwsCredentials; use aws_sdk_sts::model::Credentials as StsCredentials; +use aws_types::Credentials as AwsCredentials; use std::time::{SystemTime, UNIX_EPOCH}; -/// Convert STS credentials to aws_auth::Credentials +/// Convert STS credentials to aws_types::Credentials pub fn into_credentials( sts_credentials: Option, provider_name: &'static str, diff --git a/aws/rust-runtime/aws-auth-providers/src/test_case.rs b/aws/rust-runtime/aws-auth-providers/src/test_case.rs index a8c0bc9b45..47e08eb71d 100644 --- a/aws/rust-runtime/aws-auth-providers/src/test_case.rs +++ b/aws/rust-runtime/aws-auth-providers/src/test_case.rs @@ -28,8 +28,8 @@ struct Credentials { /// /// Comparing equality on real credentials works, but it's a pain because the Debug implementation /// hides the actual keys -impl From<&aws_auth::Credentials> for Credentials { - fn from(credentials: &aws_auth::Credentials) -> Self { +impl From<&aws_types::Credentials> for Credentials { + fn from(credentials: &aws_types::Credentials) -> Self { Self { access_key_id: credentials.access_key_id().into(), secret_access_key: credentials.secret_access_key().into(), diff --git a/aws/rust-runtime/aws-auth/src/lib.rs b/aws/rust-runtime/aws-auth/src/lib.rs index 7196fa6b1f..7a9c4ba545 100644 --- a/aws/rust-runtime/aws-auth/src/lib.rs +++ b/aws/rust-runtime/aws-auth/src/lib.rs @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0. */ +//! AWS authentication middleware used to store and retrieve credentials from the property bag + pub mod middleware; -pub mod provider; use aws_types::credentials::SharedCredentialsProvider; use smithy_http::property_bag::PropertyBag; diff --git a/aws/rust-runtime/aws-auth/src/middleware.rs b/aws/rust-runtime/aws-auth/src/middleware.rs index a11fc6153e..299ae5f260 100644 --- a/aws/rust-runtime/aws-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-auth/src/middleware.rs @@ -105,13 +105,39 @@ impl AsyncMapRequest for CredentialsStage { #[cfg(test)] mod tests { use super::CredentialsStage; - use crate::provider::async_provide_credentials_fn; use crate::set_provider; - use aws_types::credentials::{CredentialsError, SharedCredentialsProvider}; + use aws_types::credential::provide_credentials::future; + use aws_types::credential::{CredentialsError, ProvideCredentials}; + use aws_types::credentials::{ + future, CredentialsError, ProvideCredentials, SharedCredentialsProvider, + }; use aws_types::Credentials; use smithy_http::body::SdkBody; use smithy_http::middleware::AsyncMapRequest; use smithy_http::operation; + use std::sync::Arc; + + #[derive(Debug)] + struct Unhandled; + impl ProvideCredentials for Unhandled { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::ready(Err(CredentialsError::Unhandled("whoops".into()))) + } + } + + #[derive(Debug)] + struct NoCreds; + impl ProvideCredentials for NoCreds { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::ready(Err(CredentialsError::CredentialsNotLoaded)) + } + } #[tokio::test] async fn no_cred_provider_is_ok() { @@ -127,9 +153,7 @@ mod tests { let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); set_provider( &mut req.properties_mut(), - SharedCredentialsProvider::new(async_provide_credentials_fn(|| async { - Err(CredentialsError::Unhandled("whoops".into())) - })), + SharedCredentialsProvider::new(Unhandled), ); CredentialsStage::new() .apply(req) @@ -142,9 +166,7 @@ mod tests { let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); set_provider( &mut req.properties_mut(), - SharedCredentialsProvider::new(async_provide_credentials_fn(|| async { - Err(CredentialsError::CredentialsNotLoaded) - })), + SharedCredentialsProvider::new(NoCreds), ); CredentialsStage::new() .apply(req) diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index 8017d66f34..8fb4ad2872 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -22,6 +22,12 @@ default = ["default-provider"] aws-types = { path = "../../sdk/build/aws-sdk/aws-types" } smithy-async = { path = "../../sdk/build/aws-sdk/smithy-async" } tracing = { version = "0.1" } +tokio = { version = "1", features = ["sync"] } [dev-dependencies] futures-util = "0.3.16" +test-env-log = "0.2.7" +tokio = { version = "1", features = ["full"]} +# used to test compatibility +async-trait = "0.1.51" +env_logger = "0.9.0" diff --git a/aws/rust-runtime/aws-auth/src/provider.rs b/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs similarity index 79% rename from aws/rust-runtime/aws-auth/src/provider.rs rename to aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs index 00c17885aa..cef68d6bc0 100644 --- a/aws/rust-runtime/aws-auth/src/provider.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs @@ -1,27 +1,6 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -//! AWS credential providers, generic caching provider implementations, and traits to implement custom providers. -//! -//! Credentials providers acquire AWS credentials from environment variables, files, -//! or calls to AWS services such as STS. Custom credential provider implementations can -//! be provided by implementing [`ProvideCredentials`] for synchronous use-cases, or -//! [`ProvideCredentials`] for async use-cases. Generic credential caching implementations, -//! for example, -//! [`LazyCachingCredentialsProvider`](crate::provider::lazy_caching::LazyCachingCredentialsProvider), -//! are also provided as part of this module. - -mod cache; -pub mod lazy_caching; -mod time; - use aws_types::credentials; use aws_types::credentials::ProvideCredentials; - -use std::fmt; -use std::fmt::{Debug, Formatter}; +use std::fmt::{self, Debug, Formatter}; use std::future::Future; use std::marker::PhantomData; @@ -61,7 +40,7 @@ where /// /// ``` /// use aws_types::Credentials; -/// use aws_auth::provider::async_provide_credentials_fn; +/// use aws_config::meta::credentials::async_provide_credentials_fn; /// /// async fn load_credentials() -> Credentials { /// todo!() @@ -86,7 +65,7 @@ where #[cfg(test)] mod test { - use crate::provider::async_provide_credentials_fn; + use crate::meta::credentials::credential_fn::async_provide_credentials_fn; use async_trait::async_trait; use aws_types::credentials::ProvideCredentials; use aws_types::{credentials, Credentials}; diff --git a/aws/rust-runtime/aws-auth/src/provider/lazy_caching.rs b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs similarity index 96% rename from aws/rust-runtime/aws-auth/src/provider/lazy_caching.rs rename to aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs index d4b310d540..cf5593f0bb 100644 --- a/aws/rust-runtime/aws-auth/src/provider/lazy_caching.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs @@ -5,15 +5,19 @@ //! Lazy, caching, credentials provider implementation -use crate::provider::cache::Cache; -use crate::provider::time::TimeSource; -use aws_types::credentials::{future, CredentialsError, ProvideCredentials}; -use smithy_async::future::timeout::Timeout; -use smithy_async::rt::sleep::AsyncSleep; +mod cache; +mod time; + use std::sync::Arc; use std::time::Duration; + +use smithy_async::future::timeout::Timeout; +use smithy_async::rt::sleep::AsyncSleep; use tracing::{trace_span, Instrument}; +use self::{cache::Cache, time::TimeSource}; +use aws_types::credentials::{future, CredentialsError, ProvideCredentials}; + const DEFAULT_LOAD_TIMEOUT: Duration = Duration::from_secs(5); const DEFAULT_CREDENTIAL_EXPIRATION: Duration = Duration::from_secs(15 * 60); const DEFAULT_BUFFER_TIME: Duration = Duration::from_secs(10); @@ -105,24 +109,26 @@ impl ProvideCredentials for LazyCachingCredentialsProvider { } pub mod builder { - use crate::provider::lazy_caching::{ + use std::sync::Arc; + use std::time::Duration; + + use aws_types::credentials::ProvideCredentials; + use smithy_async::rt::sleep::{default_async_sleep, AsyncSleep}; + + use super::{ LazyCachingCredentialsProvider, DEFAULT_BUFFER_TIME, DEFAULT_CREDENTIAL_EXPIRATION, DEFAULT_LOAD_TIMEOUT, }; - use crate::provider::time::SystemTimeSource; - use aws_types::credentials::ProvideCredentials; - use smithy_async::rt::sleep::{default_async_sleep, AsyncSleep}; - use std::sync::Arc; - use std::time::Duration; + use crate::meta::credentials::lazy_caching::time::SystemTimeSource; /// Builder for constructing a [`LazyCachingCredentialsProvider`]. /// /// # Example /// /// ``` - /// use aws_auth::provider::async_provide_credentials_fn; - /// use aws_auth::provider::lazy_caching::LazyCachingCredentialsProvider; /// use aws_types::Credentials; + /// use aws_config::meta::credentials::async_provide_credentials_fn; + /// use aws_config::meta::credentials::LazyCachingCredentialsProvider; /// /// let provider = LazyCachingCredentialsProvider::builder() /// .load(async_provide_credentials_fn(|| async { @@ -216,17 +222,21 @@ pub mod builder { #[cfg(test)] mod tests { - use crate::provider::async_provide_credentials_fn; - use crate::provider::lazy_caching::{ - LazyCachingCredentialsProvider, TimeSource, DEFAULT_BUFFER_TIME, - DEFAULT_CREDENTIAL_EXPIRATION, DEFAULT_LOAD_TIMEOUT, - }; - use aws_types::credentials::{self, Credentials, CredentialsError, ProvideCredentials}; - use smithy_async::rt::sleep::TokioSleep; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; + + use aws_types::credentials::{self, CredentialsError, ProvideCredentials}; + use aws_types::Credentials; + use smithy_async::rt::sleep::TokioSleep; use tracing::info; + use crate::meta::credentials::credential_fn::async_provide_credentials_fn; + + use super::{ + LazyCachingCredentialsProvider, TimeSource, DEFAULT_BUFFER_TIME, + DEFAULT_CREDENTIAL_EXPIRATION, DEFAULT_LOAD_TIMEOUT, + }; + #[derive(Clone, Debug)] struct TestTime { time: Arc>, diff --git a/aws/rust-runtime/aws-auth/src/provider/cache.rs b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/cache.rs similarity index 100% rename from aws/rust-runtime/aws-auth/src/provider/cache.rs rename to aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/cache.rs diff --git a/aws/rust-runtime/aws-auth/src/provider/time.rs b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/time.rs similarity index 70% rename from aws/rust-runtime/aws-auth/src/provider/time.rs rename to aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/time.rs index c4f120bf68..44e6ccacd9 100644 --- a/aws/rust-runtime/aws-auth/src/provider/time.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/time.rs @@ -6,11 +6,15 @@ use std::fmt::Debug; use std::time::SystemTime; -/// Allows us to abstract time for tests. +/// Wall Clock Time Source +/// +/// By default, `SystemTime::now()` is used, however, this trait allows +/// tests to provide their own time source. pub(super) trait TimeSource: Send + Sync + Debug + 'static { fn now(&self) -> SystemTime; } +/// Load time from `SystemTime::now()` #[derive(Copy, Clone, Debug)] pub(super) struct SystemTimeSource; diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs index 30968e3c33..e17155603b 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs @@ -11,6 +11,12 @@ mod chain; pub use chain::CredentialsProviderChain; +mod credential_fn; +pub use credential_fn::async_provide_credentials_fn; + +mod lazy_caching; +pub use lazy_caching::LazyCachingCredentialsProvider; + // pub mod credential_fn; // pub mod lazy_caching; diff --git a/aws/rust-runtime/aws-hyper/tests/e2e_test.rs b/aws/rust-runtime/aws-hyper/tests/e2e_test.rs index f29cb347f1..dfea286d22 100644 --- a/aws/rust-runtime/aws-hyper/tests/e2e_test.rs +++ b/aws/rust-runtime/aws-hyper/tests/e2e_test.rs @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -use aws_auth::Credentials; use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion}; use aws_endpoint::set_endpoint_resolver; use aws_http::user_agent::AwsUserAgent; @@ -11,6 +10,7 @@ use aws_http::AwsErrorRetryPolicy; use aws_hyper::{Client, RetryConfig}; use aws_sig_auth::signer::OperationSigningConfig; use aws_types::region::Region; +use aws_types::Credentials; use aws_types::SigningService; use bytes::Bytes; use http::header::{AUTHORIZATION, USER_AGENT}; diff --git a/aws/rust-runtime/aws-inlineable/src/no_credentials.rs b/aws/rust-runtime/aws-inlineable/src/no_credentials.rs index 085dc571b9..585c2f0fe1 100644 --- a/aws/rust-runtime/aws-inlineable/src/no_credentials.rs +++ b/aws/rust-runtime/aws-inlineable/src/no_credentials.rs @@ -8,6 +8,7 @@ use aws_types::credentials::{CredentialsError, ProvideCredentials}; /// Stub Provider for use when no credentials provider is used #[non_exhaustive] +#[derive(Debug)] pub struct NoCredentials; impl ProvideCredentials for NoCredentials { diff --git a/aws/rust-runtime/aws-sig-auth/src/event_stream.rs b/aws/rust-runtime/aws-sig-auth/src/event_stream.rs index d8f9d76ed0..7cf35a9521 100644 --- a/aws/rust-runtime/aws-sig-auth/src/event_stream.rs +++ b/aws/rust-runtime/aws-sig-auth/src/event_stream.rs @@ -4,10 +4,10 @@ */ use crate::middleware::Signature; -use aws_auth::Credentials; use aws_sigv4::event_stream::{sign_empty_message, sign_message}; use aws_sigv4::SigningParams; use aws_types::region::SigningRegion; +use aws_types::Credentials; use aws_types::SigningService; use smithy_eventstream::frame::{Message, SignMessage, SignMessageError}; use smithy_http::property_bag::{PropertyBag, SharedPropertyBag}; @@ -91,9 +91,9 @@ impl SignMessage for SigV4Signer { mod tests { use crate::event_stream::SigV4Signer; use crate::middleware::Signature; - use aws_auth::Credentials; use aws_types::region::Region; use aws_types::region::SigningRegion; + use aws_types::Credentials; use aws_types::SigningService; use smithy_eventstream::frame::{HeaderValue, Message, SignMessage}; use smithy_http::property_bag::PropertyBag; diff --git a/aws/rust-runtime/aws-sig-auth/src/middleware.rs b/aws/rust-runtime/aws-sig-auth/src/middleware.rs index 37920f2d77..bc70472c94 100644 --- a/aws/rust-runtime/aws-sig-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-sig-auth/src/middleware.rs @@ -6,9 +6,9 @@ use crate::signer::{ OperationSigningConfig, RequestConfig, SigV4Signer, SigningError, SigningRequirements, }; -use aws_auth::Credentials; use aws_sigv4::http_request::SignableBody; use aws_types::region::SigningRegion; +use aws_types::Credentials; use aws_types::SigningService; use smithy_http::middleware::MapRequest; use smithy_http::operation::Request; @@ -137,10 +137,10 @@ impl MapRequest for SigV4SigningStage { mod test { use crate::middleware::{SigV4SigningStage, Signature, SigningStageError}; use crate::signer::{OperationSigningConfig, SigV4Signer}; - use aws_auth::Credentials; use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion}; use aws_endpoint::{set_endpoint_resolver, AwsEndpointStage}; use aws_types::region::{Region, SigningRegion}; + use aws_types::Credentials; use aws_types::SigningService; use http::header::AUTHORIZATION; use smithy_http::body::SdkBody; diff --git a/aws/rust-runtime/aws-sig-auth/src/signer.rs b/aws/rust-runtime/aws-sig-auth/src/signer.rs index 5a483b372c..6de640cfe0 100644 --- a/aws/rust-runtime/aws-sig-auth/src/signer.rs +++ b/aws/rust-runtime/aws-sig-auth/src/signer.rs @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0. */ -use aws_auth::Credentials; use aws_sigv4::http_request::{ calculate_signing_headers, PayloadChecksumKind, SigningSettings, UriEncoding, }; use aws_types::region::SigningRegion; +use aws_types::Credentials; use aws_types::SigningService; use http::header::HeaderName; use smithy_http::body::SdkBody; diff --git a/aws/sdk/integration-tests/kms/tests/integration.rs b/aws/sdk/integration-tests/kms/tests/integration.rs index d7658b156f..1d4740dd80 100644 --- a/aws/sdk/integration-tests/kms/tests/integration.rs +++ b/aws/sdk/integration-tests/kms/tests/integration.rs @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0. */ -use aws_auth::Credentials; use aws_http::user_agent::AwsUserAgent; use aws_hyper::{Client, SdkError}; use aws_sdk_kms as kms; +use aws_types::Credentials; use http::header::AUTHORIZATION; use http::Uri; use kms::operation::GenerateRandom; diff --git a/aws/sdk/integration-tests/qldbsession/tests/integration.rs b/aws/sdk/integration-tests/qldbsession/tests/integration.rs index 590e2363e7..d594a89ee1 100644 --- a/aws/sdk/integration-tests/qldbsession/tests/integration.rs +++ b/aws/sdk/integration-tests/qldbsession/tests/integration.rs @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0. */ -use aws_auth::Credentials; use aws_http::user_agent::AwsUserAgent; use aws_hyper::Client; use aws_sdk_qldbsession as qldbsession; +use aws_types::Credentials; use http::Uri; use qldbsession::model::StartSessionRequest; use qldbsession::operation::SendCommand; From 33aace7154a6579756799c526d2252cce5b9a8a5 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 14:04:38 -0400 Subject: [PATCH 05/18] Migrate profile & web identity token providers --- .../src/web_identity_token.rs | 2 +- aws/rust-runtime/aws-config/Cargo.toml | 32 +- aws/rust-runtime/aws-config/src/lib.rs | 44 ++ .../src/meta/credentials/credential_fn.rs | 2 +- .../aws-config/src/profile/credentials.rs | 418 ++++++++++ .../src/profile/credentials/exec.rs | 203 +++++ .../src/profile/credentials/repr.rs | 412 ++++++++++ .../aws-config/src/profile/mod.rs | 21 + .../aws-config/src/profile/parser.rs | 370 +++++++++ .../src/profile/parser/normalize.rs | 227 ++++++ .../aws-config/src/profile/parser/parse.rs | 351 +++++++++ .../aws-config/src/profile/parser/source.rs | 353 +++++++++ aws/rust-runtime/aws-config/src/sts.rs | 51 ++ aws/rust-runtime/aws-config/src/test_case.rs | 178 +++++ .../aws-config/src/web_identity_token.rs | 333 ++++++++ .../test-data/assume-role-tests.json | 0 .../prefer_environment/env.json | 0 .../prefer_environment/fs/home/.aws/config | 0 .../fs/home/.aws/credentials | 0 .../prefer_environment/http-traffic.json | 0 .../prefer_environment/test-case.json | 0 .../profile_overrides_web_identity/env.json | 0 .../fs/home/.aws/config | 0 .../fs/token.jwt | 0 .../http-traffic.json | 0 .../test-case.json | 0 .../profile_static_keys/env.json | 0 .../profile_static_keys/fs/home/.aws/config | 0 .../fs/home/.aws/credentials | 0 .../profile_static_keys/http-traffic.json | 0 .../profile_static_keys/test-case.json | 0 .../env.json | 0 .../fs/home/.aws/config | 0 .../fs/token.jwt | 0 .../http-traffic.json | 0 .../test-case.json | 0 .../web_identity_token_env/env.json | 0 .../web_identity_token_env/fs/token.jwt | 0 .../web_identity_token_env/http-traffic.json | 0 .../web_identity_token_env/test-case.json | 0 .../web_identity_token_invalid_jwt/env.json | 0 .../fs/home/.aws/config | 0 .../fs/token.jwt | 0 .../http-traffic.json | 0 .../test-case.json | 0 .../web_identity_token_profile/env.json | 0 .../fs/home/.aws/config | 0 .../web_identity_token_profile/fs/token.jwt | 0 .../http-traffic.json | 0 .../web_identity_token_profile/test-case.json | 0 .../env.json | 0 .../fs/home/.aws/config | 0 .../fs/token.jwt | 0 .../http-traffic.json | 0 .../test-case.json | 0 .../test-data/file-location-tests.json | 121 +++ .../test-data/profile-parser-tests.json | 713 ++++++++++++++++++ .../profile-provider/e2e_assume_role/env.json | 0 .../e2e_assume_role/fs/home/.aws/config | 0 .../e2e_assume_role/fs/home/.aws/credentials | 0 .../e2e_assume_role/http-traffic.json | 0 .../e2e_assume_role/test-case.json | 0 .../profile-provider/empty_config/env.json | 0 .../empty_config/http-traffic.json | 0 .../empty_config/test-case.json | 0 .../profile-provider/invalid_config/env.json | 0 .../invalid_config/fs/home/.aws/config | 0 .../invalid_config/http-traffic.json | 0 .../invalid_config/test-case.json | 0 .../profile-provider/region_override/env.json | 0 .../region_override/fs/home/.aws/config | 0 .../region_override/fs/home/.aws/credentials | 0 .../region_override/http-traffic.json | 0 .../region_override/test-case.json | 0 .../profile-provider/retry_on_error/env.json | 0 .../retry_on_error/fs/home/.aws/config | 0 .../retry_on_error/fs/home/.aws/credentials | 0 .../retry_on_error/http-traffic.json | 0 .../retry_on_error/test-case.json | 0 79 files changed, 3824 insertions(+), 7 deletions(-) create mode 100644 aws/rust-runtime/aws-config/src/profile/credentials.rs create mode 100644 aws/rust-runtime/aws-config/src/profile/credentials/exec.rs create mode 100644 aws/rust-runtime/aws-config/src/profile/credentials/repr.rs create mode 100644 aws/rust-runtime/aws-config/src/profile/mod.rs create mode 100644 aws/rust-runtime/aws-config/src/profile/parser.rs create mode 100644 aws/rust-runtime/aws-config/src/profile/parser/normalize.rs create mode 100644 aws/rust-runtime/aws-config/src/profile/parser/parse.rs create mode 100644 aws/rust-runtime/aws-config/src/profile/parser/source.rs create mode 100644 aws/rust-runtime/aws-config/src/sts.rs create mode 100644 aws/rust-runtime/aws-config/src/test_case.rs create mode 100644 aws/rust-runtime/aws-config/src/web_identity_token.rs rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/assume-role-tests.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/prefer_environment/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/prefer_environment/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/prefer_environment/fs/home/.aws/credentials (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/prefer_environment/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/prefer_environment/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_overrides_web_identity/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_overrides_web_identity/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_overrides_web_identity/fs/token.jwt (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_overrides_web_identity/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_overrides_web_identity/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_static_keys/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/credentials (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_static_keys/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/profile_static_keys/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_source_profile_no_env/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/token.jwt (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_source_profile_no_env/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_source_profile_no_env/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_env/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_env/fs/token.jwt (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_env/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_env/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_invalid_jwt/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/token.jwt (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_invalid_jwt/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_invalid_jwt/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_profile/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_profile/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_profile/fs/token.jwt (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_profile/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_profile/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_source_profile/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_source_profile/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_source_profile/fs/token.jwt (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_source_profile/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/default-provider-chain/web_identity_token_source_profile/test-case.json (100%) create mode 100644 aws/rust-runtime/aws-config/test-data/file-location-tests.json create mode 100644 aws/rust-runtime/aws-config/test-data/profile-parser-tests.json rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/e2e_assume_role/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/e2e_assume_role/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/e2e_assume_role/fs/home/.aws/credentials (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/e2e_assume_role/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/e2e_assume_role/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/empty_config/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/empty_config/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/empty_config/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/invalid_config/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/invalid_config/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/invalid_config/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/invalid_config/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/region_override/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/region_override/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/region_override/fs/home/.aws/credentials (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/region_override/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/region_override/test-case.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/retry_on_error/env.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/retry_on_error/fs/home/.aws/config (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/retry_on_error/fs/home/.aws/credentials (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/retry_on_error/http-traffic.json (100%) rename aws/rust-runtime/{aws-auth-providers => aws-config}/test-data/profile-provider/retry_on_error/test-case.json (100%) diff --git a/aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs b/aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs index 0d0f1e2444..601b208566 100644 --- a/aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs +++ b/aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs @@ -109,7 +109,7 @@ impl WebIdentityTokenCredentialProvider { Source::Static(conf) => Ok(Cow::Borrowed(conf)), } } - async fn credentials(&self) -> CredentialsResult { + async fn credentials(&self) -> credentials::Result { let conf = self.source()?; load_credentials( &self.fs, diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index 8fb4ad2872..121aaae56f 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -11,23 +11,45 @@ default-provider = ["profile", "imds", "meta", "sts"] profile = ["sts", "web-identity-token"] # note: IMDS currently unsupported imds = [] -meta = [] -sts = [] +meta = ["tokio/sync"] +sts = ["aws-sdk-sts", "aws-hyper"] web-identity-token = ["sts"] sso = [] -default = ["default-provider"] +rustls = ["smithy-client/rustls"] +native-tls = ["smithy-client/native-tls"] +rt-tokio = ["smithy-async/rt-tokio"] + +default = ["default-provider", "rustls", "rt-tokio"] [dependencies] aws-types = { path = "../../sdk/build/aws-sdk/aws-types" } smithy-async = { path = "../../sdk/build/aws-sdk/smithy-async" } +smithy-client = { path = "../../sdk/build/aws-sdk/smithy-client" } tracing = { version = "0.1" } -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["sync"], optional = true } +aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sts", optional = true } + +# TODO: remove when middleware stacks are moved inside of clients directly +aws-hyper = { path = "../../sdk/build/aws-sdk/aws-hyper", optional = true } [dev-dependencies] futures-util = "0.3.16" + +# TODO: unify usages of test-env-log and tracing-test test-env-log = "0.2.7" -tokio = { version = "1", features = ["full"]} +tracing-test = "0.1.0" + +tokio = { version = "1", features = ["full"] } # used to test compatibility async-trait = "0.1.51" env_logger = "0.9.0" + +# used for fuzzing profile parsing +arbitrary = "1.0.2" + +# used for test case deserialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +smithy-client = { path = "../../sdk/build/aws-sdk/smithy-client", features = ["test-util"] } diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index 58a187e4e2..c7a056129f 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -49,6 +49,18 @@ pub mod environment; #[cfg(feature = "meta")] pub mod meta; +#[cfg(feature = "profile")] +pub mod profile; + +#[cfg(feature = "sts")] +mod sts; + +#[cfg(test)] +mod test_case; + +#[cfg(feature = "web-identity-token")] +pub mod web_identity_token; + /// Create an environment loader for AWS Configuration /// /// ## Example @@ -156,3 +168,35 @@ mod loader { } } } + +mod connector { + + // create a default connector given the currently enabled cargo features. + // rustls | native tls | result + // ----------------------------- + // yes | yes | rustls + // yes | no | rustls + // no | yes | native_tls + // no | no | no default + + use smithy_client::erase::DynConnector; + + pub fn must_have_connector() -> DynConnector { + default_connector().expect("A connector was not available. Either set a custom connector or enable the `rustls` and `native-tls` crate features.") + } + + #[cfg(feature = "rustls")] + fn default_connector() -> Option { + Some(DynConnector::new(smithy_client::conns::https())) + } + + #[cfg(all(not(feature = "rustls"), feature = "native-tls"))] + fn default_connector() -> Option { + Some(DynConnector::new(smithy_client::conns::native_tls())) + } + + #[cfg(not(any(feature = "rustls", feature = "native-tls")))] + fn default_connector() -> Option { + None + } +} diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs b/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs index cef68d6bc0..79fbe7810e 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs @@ -32,7 +32,7 @@ where } } -/// Returns a new [`AsyncProvideCredentialsFn`] with the given closure. This allows you +/// Returns a new credentials provider built with the given closure. This allows you /// to create an [`ProvideCredentials`] implementation from an async block that returns /// a [`credentials::Result`]. /// diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs new file mode 100644 index 0000000000..2c945462d6 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -0,0 +1,418 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Profile File Based Credential Providers +//! +//! Profile file based providers combine two pieces: +//! +//! 1. Parsing and resolution of the assume role chain +//! 2. A user-modifiable hashmap of provider name to provider. +//! +//! Profile file based providers first determine the chain of providers that will be used to load +//! credentials. After determining and validating this chain, a `Vec` of providers will be created. +//! +//! Each subsequent provider will provide boostrap providers to the next provider in order to load +//! the final credentials. +//! +//! This module contains two sub modules: +//! - `repr` which contains an abstract representation of a provider chain and the logic to +//! build it from `~/.aws/credentials` and `~/.aws/config`. +//! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials +//! through a series of providers. +use std::borrow::Cow; +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::sync::Arc; + +use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials}; +use aws_types::os_shim_internal::{Env, Fs}; +use aws_types::region::Region; +use tracing::Instrument; + +use crate::connector::must_have_connector; +use crate::meta::region::ProvideRegion; +use crate::profile::credentials::exec::named::NamedProviderFactory; +use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain}; +use crate::profile::parser::ProfileParseError; +use smithy_client::erase::DynConnector; + +mod exec; +mod repr; + +impl ProvideCredentials for ProfileFileCredentialsProvider { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::new(self.load_credentials().instrument(tracing::info_span!( + "load_credentials", + provider = "Profile" + ))) + } +} + +/// AWS Profile based credentials provider +/// +/// This credentials provider will load credentials from `~/.aws/config` and `~/.aws/credentials`. +/// The locations of these files are configurable, see [`profile::load`](aws_types::profile::load). +/// +/// Generally, this will be constructed via the default provider chain, however, it can be manually +/// constructed with the builder: +/// ```rust,no_run +/// use aws_config::profile::ProfileFileCredentialsProvider; +/// let provider = ProfileFileCredentialsProvider::builder().build(); +/// ``` +/// +/// **Note:** Profile providers to not implement any caching. They will reload and reparse the profile +/// from the file system when called. See [lazy_caching](crate::meta::credentials::LazyCachingCredentialsProvider) for +/// more information about caching. +/// +/// This provider supports several different credentials formats: +/// ### Credentials defined explicitly within the file +/// ```ini +/// [default] +/// aws_access_key_id = 123 +/// aws_secret_access_key = 456 +/// ``` +/// +/// ### Assume Role Credentials loaded from a credential source +/// ```ini +/// [default] +/// role_arn = arn:aws:iam::123456789:role/RoleA +/// credential_source = Environment +/// ``` +/// +/// NOTE: Currently only the `Environment` credential source is supported although it is possible to +/// provide custom sources: +/// ```rust +/// use aws_types::credentials::{self, ProvideCredentials, future}; +/// use aws_config::profile::ProfileFileCredentialsProvider; +/// #[derive(Debug)] +/// struct MyCustomProvider; +/// impl MyCustomProvider { +/// async fn load_credentials(&self) -> credentials::Result { +/// todo!() +/// } +/// } +/// +/// impl ProvideCredentials for MyCustomProvider { +/// fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a { +/// future::ProvideCredentials::new(self.load_credentials()) +/// } +/// } +/// let provider = ProfileFileCredentialsProvider::builder() +/// .with_custom_provider("Custom", MyCustomProvider) +/// .build(); +/// ``` +/// +/// ### Assume role credentials from a source profile +/// ```ini +/// [default] +/// role_arn = arn:aws:iam::123456789:role/RoleA +/// source_profile = base +/// +/// [profile base] +/// aws_access_key_id = 123 +/// aws_secret_access_key = 456 +/// ``` +/// +/// Other more complex configurations are possible, consult `test-data/assume-role-tests.json`. +#[derive(Debug)] +pub struct ProfileFileCredentialsProvider { + factory: NamedProviderFactory, + client_config: ClientConfiguration, + fs: Fs, + env: Env, + region: Option, + connector: DynConnector, +} + +impl ProfileFileCredentialsProvider { + /// Builder for this credentials provider + pub fn builder() -> Builder { + Builder::default() + } + + async fn load_credentials(&self) -> credentials::Result { + // 1. grab a read lock, use it to see if the base profile has already been loaded + // 2. If it's loaded, great, lets use it. + // If not, upgrade to a write lock and use that to load the profile file. + // 3. Finally, downgrade to ensure no one swapped in the intervening time, then use try_load() + // to pull the new state. + let profile = build_provider_chain( + &self.fs, + &self.env, + &self.region, + &self.connector, + &self.factory, + ) + .await; + let inner_provider = profile.map_err(|err| match err { + ProfileFileError::NoProfilesDefined => CredentialsError::CredentialsNotLoaded, + _ => CredentialsError::InvalidConfiguration( + format!("ProfileFile provider could not be built: {}", &err).into(), + ), + })?; + let mut creds = match inner_provider + .base() + .provide_credentials() + .instrument(tracing::info_span!("load_base_credentials")) + .await + { + Ok(creds) => { + tracing::info!(creds = ?creds, "loaded base credentials"); + creds + } + Err(e) => { + tracing::warn!(error = %e, "failed to load base credentials"); + return Err(CredentialsError::ProviderError(e.into())); + } + }; + for provider in inner_provider.chain().iter() { + let next_creds = provider + .credentials(creds, &self.client_config) + .instrument(tracing::info_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::ProviderError(e.into())); + } + } + } + Ok(creds) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum ProfileFileError { + CouldNotParseProfile(ProfileParseError), + NoProfilesDefined, + CredentialLoop { + profiles: Vec, + next: String, + }, + MissingCredentialSource { + profile: String, + message: Cow<'static, str>, + }, + InvalidCredentialSource { + profile: String, + message: Cow<'static, str>, + }, + MissingProfile { + profile: String, + message: Cow<'static, str>, + }, + UnknownProvider { + name: String, + }, +} + +impl Display for ProfileFileError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ProfileFileError::CouldNotParseProfile(err) => { + write!(f, "could not parse profile file: {}", err) + } + ProfileFileError::CredentialLoop { profiles, next } => write!( + f, + "profile formed an infinite loop. first we loaded {:?}, \ + then attempted to reload {}", + profiles, next + ), + ProfileFileError::MissingCredentialSource { profile, message } => { + write!(f, "missing credential source in `{}`: {}", profile, message) + } + ProfileFileError::InvalidCredentialSource { profile, message } => { + write!(f, "invalid credential source in `{}`: {}", profile, message) + } + ProfileFileError::MissingProfile { profile, message } => { + write!(f, "profile `{}` was not defined: {}", profile, message) + } + ProfileFileError::UnknownProvider { name } => write!( + f, + "profile referenced `{}` provider but that provider is not supported", + name + ), + ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"), + } + } +} + +impl Error for ProfileFileError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + ProfileFileError::CouldNotParseProfile(err) => Some(err), + _ => None, + } + } +} + +#[derive(Default)] +pub struct Builder { + fs: Fs, + env: Env, + region: Option, + connector: Option, + custom_providers: HashMap, Arc>, +} + +impl Builder { + pub fn fs(mut self, fs: Fs) -> Self { + self.fs = fs; + self + } + + pub fn set_fs(&mut self, fs: Fs) -> &mut Self { + self.fs = fs; + self + } + + pub fn env(mut self, env: Env) -> Self { + self.env = env; + self + } + + pub fn set_env(&mut self, env: Env) -> &mut Self { + self.env = env; + self + } + + pub fn connector(mut self, connector: DynConnector) -> Self { + self.connector = Some(connector); + self + } + + pub fn set_connector(&mut self, connector: Option) -> &mut Self { + self.connector = connector; + self + } + + pub fn region(mut self, region: Region) -> Self { + self.region = Some(region); + self + } + + pub fn set_region(&mut self, region: Option) -> &mut Self { + self.region = region; + self + } + + pub fn with_custom_provider( + mut self, + name: impl Into>, + provider: impl ProvideCredentials + 'static, + ) -> Self { + self.custom_providers + .insert(name.into(), Arc::new(provider)); + self + } + + pub fn build(self) -> ProfileFileCredentialsProvider { + let build_span = tracing::info_span!("build_profile_provider"); + let _enter = build_span.enter(); + let env = self.env.clone(); + let fs = self.fs; + let mut named_providers = self.custom_providers.clone(); + named_providers + .entry("Environment".into()) + .or_insert_with(|| { + Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env( + env.clone(), + )) + }); + // TODO: ECS, IMDS, and other named providers + let factory = exec::named::NamedProviderFactory::new(named_providers); + let connector = self.connector.clone().unwrap_or_else(must_have_connector); + let core_client = aws_hyper::Client::new(connector.clone()); + + ProfileFileCredentialsProvider { + factory, + client_config: ClientConfiguration { + core_client, + region: self.region.clone(), + }, + fs, + env, + region: self.region.clone(), + connector, + } + } +} + +async fn build_provider_chain( + fs: &Fs, + env: &Env, + region: &dyn ProvideRegion, + connector: &DynConnector, + factory: &NamedProviderFactory, +) -> Result { + let profile_set = super::parser::load(&fs, &env).await.map_err(|err| { + tracing::warn!(err = %err, "failed to parse profile"); + ProfileFileError::CouldNotParseProfile(err) + })?; + let repr = repr::resolve_chain(&profile_set)?; + tracing::info!(chain = ?repr, "constructed abstract provider from config file"); + exec::ProviderChain::from_repr(fs.clone(), connector, region.region().await, repr, &factory) +} + +#[cfg(test)] +mod test { + use tracing_test::traced_test; + + use crate::profile::credentials::Builder; + use crate::test_case::TestEnvironment; + use aws_types::region::Region; + + macro_rules! make_test { + ($name: ident) => { + #[traced_test] + #[tokio::test] + async fn $name() { + TestEnvironment::from_dir(concat!( + "./test-data/profile-provider/", + stringify!($name) + )) + .unwrap() + .execute(|fs, env, conn| { + Builder::default() + .env(env) + .fs(fs) + .region(Region::from_static("us-east-1")) + .connector(conn) + .build() + }) + .await + } + }; + } + + make_test!(e2e_assume_role); + make_test!(empty_config); + make_test!(retry_on_error); + make_test!(invalid_config); + + #[tokio::test] + async fn region_override() { + TestEnvironment::from_dir("./test-data/profile-provider/region_override") + .unwrap() + .execute(|fs, env, conn| { + Builder::default() + .env(env) + .fs(fs) + .region(Region::from_static("us-east-2")) + .connector(conn) + .build() + }) + .await + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs new file mode 100644 index 0000000000..fb50e7ca52 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs @@ -0,0 +1,203 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use std::sync::Arc; + +use aws_sdk_sts::operation::AssumeRole; +use aws_sdk_sts::{Config, Credentials}; +use aws_types::region::Region; + +use super::repr::{self, BaseProvider}; + +use crate::profile::credentials::ProfileFileError; +use crate::sts; +use crate::web_identity_token::{StaticConfiguration, WebIdentityTokenCredentialsProvider}; +use aws_types::credentials::{self, CredentialsError, ProvideCredentials}; +use aws_types::os_shim_internal::Fs; +use smithy_client::erase::DynConnector; +use std::fmt::{Debug, Formatter}; + +#[derive(Debug)] +pub struct AssumeRoleProvider { + role_arn: String, + external_id: Option, + session_name: Option, +} + +#[derive(Debug)] +pub struct ClientConfiguration { + pub(crate) core_client: aws_hyper::StandardClient, + pub(crate) region: Option, +} + +impl AssumeRoleProvider { + pub async fn credentials( + &self, + input_credentials: Credentials, + client_config: &ClientConfiguration, + ) -> credentials::Result { + let config = Config::builder() + .credentials_provider(input_credentials) + .region(client_config.region.clone()) + .build(); + let session_name = &self + .session_name + .as_ref() + .cloned() + .unwrap_or_else(|| sts::util::default_session_name("assume-role-from-profile")); + let operation = AssumeRole::builder() + .role_arn(&self.role_arn) + .set_external_id(self.external_id.clone()) + .role_session_name(session_name) + .build() + .expect("operation is valid") + .make_operation(&config) + .expect("valid operation"); + let assume_role_creds = client_config + .core_client + .call(operation) + .await + .map_err(|err| CredentialsError::ProviderError(err.into()))? + .credentials; + sts::util::into_credentials(assume_role_creds, "AssumeRoleProvider") + } +} + +pub(crate) struct ProviderChain { + base: Arc, + chain: Vec, +} + +impl Debug for ProviderChain { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // TODO: ProvideCredentials should probably mandate debug + f.debug_struct("ProviderChain").finish() + } +} + +impl ProviderChain { + pub fn base(&self) -> &dyn ProvideCredentials { + self.base.as_ref() + } + + pub fn chain(&self) -> &[AssumeRoleProvider] { + &self.chain.as_slice() + } +} + +impl ProviderChain { + pub fn from_repr( + fs: Fs, + connector: &DynConnector, + region: Option, + repr: repr::ProfileChain, + factory: &named::NamedProviderFactory, + ) -> Result { + let base = match repr.base() { + BaseProvider::NamedSource(name) => { + factory + .provider(name) + .ok_or(ProfileFileError::UnknownProvider { + name: name.to_string(), + })? + } + BaseProvider::AccessKey(key) => Arc::new(key.clone()), + BaseProvider::WebIdentityTokenRole { + role_arn, + 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"), + ), + }) + .fs(fs) + .connector(connector.clone()) + .region(region) + .build(); + Arc::new(provider) + } + }; + tracing::info!(base = ?repr.base(), "first credentials will be loaded from {:?}", repr.base()); + let chain = repr + .chain() + .iter() + .map(|role_arn| { + tracing::info!(role_arn = ?role_arn, "which will be used to assume a role"); + AssumeRoleProvider { + role_arn: role_arn.role_arn.into(), + external_id: role_arn.external_id.map(|id| id.into()), + session_name: role_arn.session_name.map(|id| id.into()), + } + }) + .collect(); + Ok(ProviderChain { base, chain }) + } +} + +pub mod named { + use std::collections::HashMap; + use std::sync::Arc; + + use aws_types::credentials::ProvideCredentials; + use std::borrow::Cow; + + #[derive(Debug)] + pub struct NamedProviderFactory { + providers: HashMap, Arc>, + } + + impl NamedProviderFactory { + pub fn new(providers: HashMap, Arc>) -> Self { + Self { providers } + } + + pub fn provider(&self, name: &str) -> Option> { + self.providers.get(name).cloned() + } + } +} + +#[cfg(test)] +mod test { + use crate::profile::credentials::exec::named::NamedProviderFactory; + use crate::profile::credentials::exec::ProviderChain; + use crate::profile::credentials::repr::{BaseProvider, ProfileChain}; + use aws_sdk_sts::Region; + use smithy_client::dvr; + use smithy_client::erase::DynConnector; + use std::collections::HashMap; + + fn stub_connector() -> DynConnector { + DynConnector::new(dvr::ReplayingConnection::new(vec![])) + } + + #[test] + fn error_on_unknown_provider() { + let factory = NamedProviderFactory::new(HashMap::new()); + let chain = ProviderChain::from_repr( + Default::default(), + &stub_connector(), + Some(Region::new("us-east-1")), + ProfileChain { + base: BaseProvider::NamedSource("floozle"), + chain: vec![], + }, + &factory, + ); + let err = chain.expect_err("no source by that name"); + assert!( + format!("{}", err).contains( + "profile referenced `floozle` provider but that provider is not supported" + ), + "`{}` did not match expected error", + err + ); + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs new file mode 100644 index 0000000000..3cc96de5fe --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs @@ -0,0 +1,412 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Flattened Representation of an AssumeRole chain +//! +//! Assume Role credentials in profile files can chain together credentials from multiple +//! different providers with subsequent credentials being used to configure subsequent providers. +//! +//! This module can parse and resolve the profile chain into a flattened representation with +//! 1-credential-per row (as opposed to a direct profile file representation which can combine +//! multiple actions into the same profile). + +use crate::profile::credentials::ProfileFileError; +use crate::profile::{Profile, ProfileSet}; +use aws_types::Credentials; + +/// Chain of Profile Providers +/// +/// Within a profile file, a chain of providers is produced. Starting with a base provider, +/// subsequent providers use the credentials from previous providers to perform their task. +/// +/// ProfileChain is a direct representation of the Profile. It can contain named providers +/// that don't actually have implementations. +#[derive(Debug)] +pub struct ProfileChain<'a> { + pub(crate) base: BaseProvider<'a>, + pub(crate) chain: Vec>, +} + +impl<'a> ProfileChain<'a> { + pub fn base(&self) -> &BaseProvider<'a> { + &self.base + } + + pub fn chain(&self) -> &[RoleArn<'a>] { + &self.chain.as_slice() + } +} + +/// A base member of the profile chain +/// +/// Base providers do not require input credentials to provide their own credentials, +/// eg. IMDS, ECS, Environment variables +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum BaseProvider<'a> { + /// A profile that specifies a named credential source + /// Eg: `credential_source = Ec2InstanceMetadata` + /// + /// The following profile produces two separate `ProfileProvider` rows: + /// 1. `BaseProvider::NamedSource("Ec2InstanceMetadata")` + /// 2. `RoleArn { role_arn: "...", ... } + /// ```ini + /// [profile assume-role] + /// role_arn = arn:aws:iam::123456789:role/MyRole + /// credential_source = Ec2InstanceMetadata + /// ``` + NamedSource(&'a str), + + /// A profile with explicitly configured access keys + /// + /// Example + /// ```ini + /// [profile C] + /// aws_access_key_id = abc123 + /// aws_secret_access_key = def456 + /// ``` + AccessKey(Credentials), + + WebIdentityTokenRole { + role_arn: &'a str, + web_identity_token_file: &'a str, + session_name: Option<&'a str>, + }, // TODO: add SSO support + /* + /// An SSO Provider + Sso { + sso_account_id: &'a str, + sso_region: &'a str, + sso_role_name: &'a str, + sso_start_url: &'a str, + }, + */ +} + +/// A profile that specifies a role to assume +/// +/// A RoleArn can only be created from either a profile with `source_profile` +/// or one with `credential_source`. +#[derive(Debug)] +pub struct RoleArn<'a> { + /// Role to assume + pub role_arn: &'a str, + /// external_id parameter to pass to the assume role provider + pub external_id: Option<&'a str>, + + /// session name parameter to pass to the assume role provider + pub session_name: Option<&'a str>, +} + +/// Resolve a ProfileChain from a ProfileSet or return an error +pub fn resolve_chain(profile_set: &ProfileSet) -> Result { + if profile_set.is_empty() { + return Err(ProfileFileError::NoProfilesDefined); + } + let mut source_profile_name = profile_set.selected_profile(); + let mut visited_profiles = vec![]; + let mut chain = vec![]; + let base = loop { + let profile = profile_set.get_profile(source_profile_name).ok_or( + ProfileFileError::MissingProfile { + profile: source_profile_name.into(), + message: format!( + "could not find source profile {} referenced from {}", + source_profile_name, + visited_profiles.last().unwrap_or(&"the root profile") + ) + .into(), + }, + )?; + if visited_profiles.contains(&source_profile_name) { + return Err(ProfileFileError::CredentialLoop { + profiles: visited_profiles + .into_iter() + .map(|s| s.to_string()) + .collect(), + next: source_profile_name.to_string(), + }); + } + visited_profiles.push(&source_profile_name); + // After the first item in the chain, we will prioritize static credentials if they exist + if visited_profiles.len() > 1 { + let try_static = static_creds_from_profile(&profile); + if let Ok(static_credentials) = try_static { + break BaseProvider::AccessKey(static_credentials); + } + } + let next_profile = match chain_provider(&profile) { + // this provider wasn't a chain provider, reload it as a base provider + None => { + break base_provider(profile)?; + } + Some(result) => { + let (chain_profile, next) = result?; + chain.push(chain_profile); + next + } + }; + match next_profile { + NextProfile::SelfReference => { + // self referential profile, don't go through the loop because it will error + // on the infinite loop check. Instead, reload this profile as a base profile + // and exit. + break base_provider(profile)?; + } + NextProfile::Named(name) => source_profile_name = name, + } + }; + chain.reverse(); + Ok(ProfileChain { base, chain }) +} + +mod role { + pub const ROLE_ARN: &str = "role_arn"; + pub const EXTERNAL_ID: &str = "external_id"; + pub const SESSION_NAME: &str = "role_session_name"; + + pub const CREDENTIAL_SOURCE: &str = "credential_source"; + pub const SOURCE_PROFILE: &str = "source_profile"; +} + +mod web_identity_token { + pub const TOKEN_FILE: &str = "web_identity_token_file"; +} + +mod static_credentials { + pub const AWS_ACCESS_KEY_ID: &str = "aws_access_key_id"; + pub const AWS_SECRET_ACCESS_KEY: &str = "aws_secret_access_key"; + pub const AWS_SESSION_TOKEN: &str = "aws_session_token"; +} +const PROVIDER_NAME: &str = "ProfileFile"; + +fn base_provider(profile: &Profile) -> Result { + // the profile must define either a `CredentialsSource` or a concrete set of access keys + match profile.get(role::CREDENTIAL_SOURCE) { + Some(source) => Ok(BaseProvider::NamedSource(source)), + None => web_identity_token_from_profile(profile) + .unwrap_or_else(|| Ok(BaseProvider::AccessKey(static_creds_from_profile(profile)?))), + } +} + +enum NextProfile<'a> { + SelfReference, + Named(&'a str), +} + +fn chain_provider(profile: &Profile) -> Option> { + let role_provider = role_arn_from_profile(&profile)?; + let (source_profile, credential_source) = ( + profile.get(role::SOURCE_PROFILE), + profile.get(role::CREDENTIAL_SOURCE), + ); + let profile = match (source_profile, credential_source) { + (Some(_), Some(_)) => Err(ProfileFileError::InvalidCredentialSource { + profile: profile.name().to_string(), + message: "profile contained both source_profile and credential_source. \ + Only one or the other can be defined" + .into(), + }), + (None, None) => Err(ProfileFileError::InvalidCredentialSource { + profile: profile.name().to_string(), + message: + "profile must contain source_profile or credentials_source but neither were defined" + .into(), + }), + (Some(source_profile), None) if source_profile == profile.name() => { + Ok((role_provider, NextProfile::SelfReference)) + } + + (Some(source_profile), None) => Ok((role_provider, NextProfile::Named(source_profile))), + // we want to loop back into this profile and pick up the credential source + (None, Some(_credential_source)) => Ok((role_provider, NextProfile::SelfReference)), + }; + Some(profile) +} + +fn role_arn_from_profile(profile: &Profile) -> Option { + // Web Identity Tokens are root providers, not chained roles + if profile.get(web_identity_token::TOKEN_FILE).is_some() { + return None; + } + let role_arn = profile.get(role::ROLE_ARN)?; + let session_name = profile.get(role::SESSION_NAME); + let external_id = profile.get(role::EXTERNAL_ID); + Some(RoleArn { + role_arn, + external_id, + session_name, + }) +} + +fn web_identity_token_from_profile( + profile: &Profile, +) -> Option> { + let session_name = profile.get(role::SESSION_NAME); + match ( + profile.get(role::ROLE_ARN), + profile.get(web_identity_token::TOKEN_FILE), + ) { + (Some(role_arn), Some(token_file)) => Some(Ok(BaseProvider::WebIdentityTokenRole { + role_arn, + web_identity_token_file: token_file, + session_name, + })), + (None, None) => None, + (Some(_role_arn), None) => None, + (None, Some(_token_file)) => Some(Err(ProfileFileError::InvalidCredentialSource { + profile: profile.name().to_string(), + message: "`web_identity_token_file` was specified but `role_arn` was missing".into(), + })), + } +} + +/// Load static credentials from a profile +/// +/// Example: +/// ```ini +/// [profile B] +/// aws_access_key_id = abc123 +/// aws_secret_access_key = def456 +/// ``` +fn static_creds_from_profile(profile: &Profile) -> Result { + use static_credentials::*; + let access_key = profile.get(AWS_ACCESS_KEY_ID); + let secret_key = profile.get(AWS_SECRET_ACCESS_KEY); + let session_token = profile.get(AWS_SESSION_TOKEN); + if let (None, None, None) = (access_key, secret_key, session_token) { + return Err(ProfileFileError::MissingCredentialSource { + profile: profile.name().to_string(), + message: "expected `aws_access_key_id` and `aws_secret_access_key` to be defined" + .into(), + }); + } + let access_key = access_key.ok_or_else(|| ProfileFileError::InvalidCredentialSource { + profile: profile.name().to_string(), + message: "profile missing aws_access_key_id".into(), + })?; + let secret_key = secret_key.ok_or_else(|| ProfileFileError::InvalidCredentialSource { + profile: profile.name().to_string(), + message: "profile missing aws_secret_access_key".into(), + })?; + Ok(Credentials::new( + access_key, + secret_key, + session_token.map(|s| s.to_string()), + None, + PROVIDER_NAME, + )) +} + +#[cfg(test)] +mod tests { + use crate::profile::credentials::repr::{resolve_chain, BaseProvider, ProfileChain}; + use crate::profile::ProfileSet; + use serde::Deserialize; + use std::collections::HashMap; + use std::error::Error; + use std::fs; + + #[test] + fn run_test_cases() -> Result<(), Box> { + let test_cases: Vec = + serde_json::from_str(&fs::read_to_string("./test-data/assume-role-tests.json")?)?; + for test_case in test_cases { + print!("checking: {}...", test_case.docs); + check(test_case); + println!("ok") + } + Ok(()) + } + + fn check(test_case: TestCase) { + let source = ProfileSet::new(test_case.input.profile, test_case.input.selected_profile); + let actual = resolve_chain(&source); + let expected = test_case.output; + match (expected, actual) { + (TestOutput::Error(s), Err(e)) => assert!( + format!("{}", e).contains(&s), + "expected {} to contain `{}`", + e, + s + ), + (TestOutput::ProfileChain(expected), Ok(actual)) => { + assert_eq!(to_test_output(actual), expected) + } + (expected, actual) => panic!( + "error/success mismatch. Expected:\n {:?}\nActual:\n {:?}", + &expected, actual + ), + } + } + + #[derive(Deserialize)] + struct TestCase { + docs: String, + input: TestInput, + output: TestOutput, + } + + #[derive(Deserialize)] + struct TestInput { + profile: HashMap>, + selected_profile: String, + } + + fn to_test_output(profile_chain: ProfileChain) -> Vec { + let mut output = vec![]; + match profile_chain.base { + BaseProvider::NamedSource(name) => output.push(Provider::NamedSource(name.into())), + BaseProvider::AccessKey(creds) => output.push(Provider::AccessKey { + access_key_id: creds.access_key_id().into(), + secret_access_key: creds.secret_access_key().into(), + session_token: creds.session_token().map(|tok| tok.to_string()), + }), + BaseProvider::WebIdentityTokenRole { + role_arn, + web_identity_token_file, + session_name, + } => output.push(Provider::WebIdentityToken { + role_arn: role_arn.into(), + web_identity_token_file: web_identity_token_file.into(), + role_session_name: session_name.map(|sess| sess.to_string()), + }), + }; + for role in profile_chain.chain { + output.push(Provider::AssumeRole { + role_arn: role.role_arn.into(), + external_id: role.external_id.map(ToString::to_string), + role_session_name: role.session_name.map(ToString::to_string), + }) + } + output + } + + #[derive(Deserialize, Debug, PartialEq, Eq)] + enum TestOutput { + ProfileChain(Vec), + Error(String), + } + + #[derive(Deserialize, Debug, Eq, PartialEq)] + enum Provider { + AssumeRole { + role_arn: String, + external_id: Option, + role_session_name: Option, + }, + AccessKey { + access_key_id: String, + secret_access_key: String, + session_token: Option, + }, + NamedSource(String), + WebIdentityToken { + role_arn: String, + web_identity_token_file: String, + role_session_name: Option, + }, + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/mod.rs b/aws/rust-runtime/aws-config/src/profile/mod.rs new file mode 100644 index 0000000000..3c0e8ea7df --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/mod.rs @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Load configuration from AWS Profiles +//! +//! AWS profiles are typically stored in `~/.aws/config` and `~/.aws/credentials`. For more details +//! see + +mod parser; +pub use parser::{load, Profile, ProfileSet, Property}; + +mod credentials; +pub use credentials::ProfileFileCredentialsProvider; +/* +pub mod credential; + +pub mod region; + +pub use parser::{Profile, ProfileSet, Property};*/ diff --git a/aws/rust-runtime/aws-config/src/profile/parser.rs b/aws/rust-runtime/aws-config/src/profile/parser.rs new file mode 100644 index 0000000000..e201c303bb --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/parser.rs @@ -0,0 +1,370 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +mod normalize; +mod parse; +mod source; + +use crate::profile::parser::parse::parse_profile_file; +use crate::profile::parser::source::{FileKind, Source}; +use aws_types::os_shim_internal::{Env, Fs}; +use std::borrow::Cow; +use std::collections::HashMap; + +pub use self::parse::ProfileParseError; + +/// Read & parse AWS config files +/// +/// Loads and parses profile files according to the spec: +/// +/// ## Location of Profile Files +/// * The location of the config file will be loaded from `$AWS_CONFIG_FILE` with a fallback to +/// `~/.aws/config` +/// * The location of the credentials file will be loaded from `$AWS_SHARED_CREDENTIALS_FILE` with a +/// fallback to `~/.aws/credentials` +/// +/// ## Home directory resolution +/// Home directory resolution is implemented to match the behavior of the CLI & Python. `~` is only +/// used for home directory resolution when it: +/// - Starts the path +/// - Is followed immediately by `/` or a platform specific separator. (On windows, `~/` and `~\` both +/// resolve to the home directory. +/// +/// When determining the home directory, the following environment variables are checked: +/// - `$HOME` on all platforms +/// - `$USERPROFILE` on Windows +/// - `$HOMEDRIVE$HOMEPATH` on Windows +/// +/// ## Profile file syntax +/// +/// Profile files have a general form similar to INI but with a number of quirks and edge cases. These +/// behaviors are largely to match existing parser implementations and these cases are documented in `test-data/profile-parser-tests.json` +/// in this repo. +/// +/// ### The config file `~/.aws/config` +/// ```ini +/// # ~/.aws/config +/// [profile default] +/// key = value +/// +/// # profiles must begin with `profile` +/// [profile other] +/// key = value2 +/// ``` +/// +/// ### The credentials file `~/.aws/credentials` +/// The main difference is that in ~/.aws/credentials, profiles MUST NOT be prefixed with profile: +/// ```ini +/// [default] +/// aws_access_key_id = 123 +/// +/// [other] +/// aws_access_key_id = 456 +/// ``` +pub async fn load(fs: &Fs, env: &Env) -> Result { + let source = source::load(&env, &fs).await; + ProfileSet::parse(source) +} + +/// A top-level configuration source containing multiple named profiles +#[derive(Debug, Eq, Clone, PartialEq)] +pub struct ProfileSet { + profiles: HashMap, + selected_profile: Cow<'static, str>, +} + +impl ProfileSet { + /// Create a new Profile set directly from a HashMap + /// + /// This method creates a ProfileSet directly from a hashmap with no normalization. + /// + /// ## Note + /// + /// This is probably not what you want! In general, [`load`](load) should be used instead + /// because it will perform input normalization. However, for tests which operate on the + /// normalized profile, this method exists to facilitate easy construction of a ProfileSet + pub fn new( + profiles: HashMap>, + selected_profile: impl Into>, + ) -> Self { + let mut base = ProfileSet::empty(); + base.selected_profile = selected_profile.into(); + for (name, profile) in profiles { + base.profiles.insert( + name.clone(), + Profile::new( + name, + profile + .into_iter() + .map(|(k, v)| (k.clone(), Property::new(k, v))) + .collect(), + ), + ); + } + base + } + + /// Retrieves a key-value pair from the currently selected profile + pub fn get(&self, key: &str) -> Option<&str> { + self.profiles + .get(self.selected_profile.as_ref()) + .and_then(|profile| profile.get(key)) + } + + /// Retrieve a named profile from the profile set + pub fn get_profile(&self, profile_name: &str) -> Option<&Profile> { + self.profiles.get(profile_name) + } + + /// Returns the name of the currently selected profile + pub fn selected_profile(&self) -> &str { + self.selected_profile.as_ref() + } + + /// Returns true if no profiles are contained in this profile set + pub fn is_empty(&self) -> bool { + self.profiles.is_empty() + } + + fn parse(source: Source) -> Result { + let mut base = ProfileSet::empty(); + base.selected_profile = source.profile; + + normalize::merge_in( + &mut base, + parse_profile_file(&source.config_file)?, + FileKind::Config, + ); + normalize::merge_in( + &mut base, + parse_profile_file(&source.credentials_file)?, + FileKind::Credentials, + ); + Ok(base) + } + + fn empty() -> Self { + Self { + profiles: Default::default(), + selected_profile: "default".into(), + } + } +} + +/// An individual configuration profile +/// +/// An AWS config may be composed of a multiple named profiles within a [`ProfileSet`](ProfileSet) +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Profile { + name: String, + properties: HashMap, +} + +impl Profile { + /// Create a new profile + pub fn new(name: String, properties: HashMap) -> Self { + Self { name, properties } + } + + /// The name of this profile + pub fn name(&self) -> &str { + &self.name + } + + /// Returns a reference to the property named `name` + pub fn get(&self, name: &str) -> Option<&str> { + self.properties.get(name).map(|prop| prop.value()) + } +} + +/// Key-Value property pair +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Property { + key: String, + value: String, +} + +impl Property { + /// Value of this property + pub fn value(&self) -> &str { + &self.value + } + + /// Name of this property + pub fn key(&self) -> &str { + &self.key + } + + /// Creates a new property + pub fn new(key: String, value: String) -> Self { + Property { key, value } + } +} + +#[cfg(test)] +mod test { + use crate::profile::parser::source::{File, Source}; + use crate::profile::ProfileSet; + use arbitrary::{Arbitrary, Unstructured}; + use serde::Deserialize; + use std::collections::HashMap; + use std::error::Error; + use std::fs; + use tracing_test::traced_test; + + /// Run all tests from profile-parser-tests.json + /// + /// These represent the bulk of the test cases and reach effectively 100% coverage + #[test] + #[traced_test] + fn run_tests() -> Result<(), Box> { + let tests = fs::read_to_string("test-data/profile-parser-tests.json")?; + let tests: ParserTests = serde_json::from_str(&tests)?; + for (i, test) in tests.tests.into_iter().enumerate() { + eprintln!("test: {}", i); + check(test); + } + Ok(()) + } + + #[test] + fn empty_source_empty_profile() { + let source = Source { + config_file: File { + path: "~/.aws/config".to_string(), + contents: "".into(), + }, + credentials_file: File { + path: "~/.aws/credentials".to_string(), + contents: "".into(), + }, + profile: "default".into(), + }; + let profile_set = ProfileSet::parse(source).expect("empty profiles are valid"); + assert_eq!(profile_set.is_empty(), true); + } + + /// Run all tests from the fuzzing corpus to validate coverage + #[test] + #[ignore] + fn run_fuzz_tests() -> Result<(), Box> { + let fuzz_corpus = fs::read_dir("fuzz/corpus/profile-parser")? + .map(|res| res.map(|entry| entry.path())) + .collect::, _>>()?; + for file in fuzz_corpus { + let raw = fs::read(file)?; + let mut unstructured = Unstructured::new(&raw); + let (conf, creds): (Option<&str>, Option<&str>) = + Arbitrary::arbitrary(&mut unstructured)?; + let profile_source = Source { + config_file: File { + path: "~/.aws/config".to_string(), + contents: conf.unwrap_or_default().to_string(), + }, + credentials_file: File { + path: "~/.aws/config".to_string(), + contents: creds.unwrap_or_default().to_string(), + }, + profile: "default".into(), + }; + // don't care if parse fails, just don't panic + let _ = ProfileSet::parse(profile_source); + } + + Ok(()) + } + + // for test comparison purposes, flatten a profile into a hashmap + fn flatten(profile: ProfileSet) -> HashMap> { + profile + .profiles + .into_iter() + .map(|(_name, profile)| { + ( + profile.name, + profile + .properties + .into_iter() + .map(|(_, prop)| (prop.key, prop.value)) + .collect(), + ) + }) + .collect() + } + + fn make_source(input: ParserInput) -> Source { + Source { + config_file: File { + path: "~/.aws/config".to_string(), + contents: input.config_file.unwrap_or_default(), + }, + credentials_file: File { + path: "~/.aws/credentials".to_string(), + contents: input.credentials_file.unwrap_or_default(), + }, + profile: "default".into(), + } + } + + // wrapper to generate nicer errors during test failure + fn check(test_case: ParserTest) { + let copy = test_case.clone(); + let parsed = ProfileSet::parse(make_source(test_case.input)); + let res = match (parsed.map(flatten), &test_case.output) { + (Ok(actual), ParserOutput::Profiles(expected)) if &actual != expected => Err(format!( + "mismatch:\nExpected: {:#?}\nActual: {:#?}", + expected, actual + )), + (Ok(_), ParserOutput::Profiles(_)) => Ok(()), + (Err(msg), ParserOutput::ErrorContaining(substr)) => { + if format!("{}", msg).contains(substr) { + Ok(()) + } else { + Err(format!("Expected {} to contain {}", msg, substr)) + } + } + (Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!( + "expected an error: {} but parse succeeded:\n{:#?}", + err, output + )), + (Err(err), ParserOutput::Profiles(_expected)) => { + Err(format!("Expected to succeed but got: {}", err)) + } + }; + if let Err(e) = res { + eprintln!("Test case failed: {:#?}", copy); + eprintln!("failure: {}", e); + panic!("test failed") + } + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct ParserTests { + tests: Vec, + } + + #[derive(Deserialize, Debug, Clone)] + #[serde(rename_all = "camelCase")] + struct ParserTest { + name: String, + input: ParserInput, + output: ParserOutput, + } + + #[derive(Deserialize, Debug, Clone)] + #[serde(rename_all = "camelCase")] + enum ParserOutput { + Profiles(HashMap>), + ErrorContaining(String), + } + + #[derive(Deserialize, Debug, Clone)] + #[serde(rename_all = "camelCase")] + struct ParserInput { + config_file: Option, + credentials_file: Option, + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs b/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs new file mode 100644 index 0000000000..74033726ce --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs @@ -0,0 +1,227 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use std::borrow::Cow; +use std::collections::HashMap; + +use crate::profile::parser::parse::{RawProfileSet, WHITESPACE}; +use crate::profile::parser::source::FileKind; +use crate::profile::{Profile, ProfileSet, Property}; + +const DEFAULT: &str = "default"; +const PROFILE_PREFIX: &str = "profile"; + +#[derive(Eq, PartialEq, Hash, Debug)] +struct ProfileName<'a> { + name: &'a str, + has_profile_prefix: bool, +} + +impl ProfileName<'_> { + fn parse(input: &str) -> ProfileName { + let input = input.trim_matches(WHITESPACE); + let (name, has_profile_prefix) = match input.strip_prefix(PROFILE_PREFIX) { + // profilefoo isn't considered as having the profile prefix + Some(stripped) if stripped.starts_with(WHITESPACE) => (stripped.trim(), true), + _ => (input, false), + }; + ProfileName { + name, + has_profile_prefix, + } + } + + /// Validate a ProfileName for a given file key + /// + /// 1. `name` must ALWAYS be a valid identifier + /// 2. For Config files, the profile must either be `default` or it must have a profile prefix + /// 3. For credentials files, the profile name MUST NOT have a profile prefix + fn valid_for(self, kind: FileKind) -> Result { + if validate_identifier(self.name).is_err() { + return Err(format!( + "profile `{}` ignored because `{}` was not a valid identifier", + &self.name, &self.name + )); + } + match (self.name, kind, self.has_profile_prefix) { + (_, FileKind::Config, true) => Ok(self), + (DEFAULT, FileKind::Config, false) => Ok(self), + (_not_default, FileKind::Config, false) => Err(format!( + "profile `{}` ignored because config profiles must be of the form `[profile ]`", + self.name + )), + (_, FileKind::Credentials, true) => Err(format!( + "profile `{}` ignored because credential profiles must NOT begin with `profile`", + self.name + )), + (_, FileKind::Credentials, false) => Ok(self), + } + } +} + +/// Normalize a raw profile into a `MergedProfile` +/// +/// This function follows the following rules, codified in the tests & the reference Java implementation +/// - When the profile is a config file, strip `profile` and trim whitespace (`profile foo` => `foo`) +/// - Profile names are validated (see `validate_profile_name`) +/// - A profile named `profile default` takes priority over a profile named `default`. +/// - Profiles with identical names are merged +pub fn merge_in(base: &mut ProfileSet, raw_profile_set: RawProfileSet, kind: FileKind) { + // parse / validate profile names + let validated_profiles = raw_profile_set + .into_iter() + .map(|(name, profile)| (ProfileName::parse(name).valid_for(kind), profile)); + + // remove invalid profiles & emit warning + // valid_profiles contains only valid profiles but it may contain `[profile default]` and `[default]` + // which must be filtered later + let valid_profiles = validated_profiles + .filter_map(|(name, profile)| match name { + Ok(profile_name) => Some((profile_name, profile)), + Err(e) => { + tracing::warn!("{}", e); + None + } + }) + .collect::>(); + // if a `[profile default]` exists then we should ignore `[default]` + let ignore_unprefixed_default = valid_profiles + .iter() + .any(|(profile, _)| profile.name == DEFAULT && profile.has_profile_prefix); + + for (profile_name, raw_profile) in valid_profiles { + // When normalizing profiles, profiles should be merged. However, `[profile default]` and + // `[default]` are considered two separate profiles. Furthermore, `[profile default]` fully + // replaces any contents of `[default]`! + // for each profile in the raw set of profiles, normalize, then merge it into the base set + if ignore_unprefixed_default + && profile_name.name == DEFAULT + && !profile_name.has_profile_prefix + { + tracing::warn!("profile `default` ignored because `[profile default]` was found which takes priority"); + continue; + } + let profile = base + .profiles + .entry(profile_name.name.to_string()) + .or_insert_with(|| Profile::new(profile_name.name.to_string(), Default::default())); + merge_into_base(profile, raw_profile) + } +} + +fn merge_into_base<'a>(target: &mut Profile, profile: HashMap<&str, Cow<'a, str>>) { + for (k, v) in profile { + match validate_identifier(&k) { + Ok(k) => { + target + .properties + .insert(k.to_owned(), Property::new(k.to_owned(), v.into())); + } + Err(_) => { + tracing::warn!(profile = %&target.name, key = ?k, "key ignored because `{}` was not a valid identifier", k); + } + } + } +} + +/// Validate that a string is a valid identifier +/// +/// Identifiers must match `[A-Za-z0-9\-_]+` +fn validate_identifier(input: &str) -> Result<&str, ()> { + input + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '\\') + .then(|| input) + .ok_or(()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use tracing_test::traced_test; + + use crate::profile::parser::parse::RawProfileSet; + use crate::profile::parser::source::FileKind; + use crate::profile::ProfileSet; + + use super::{merge_in, ProfileName}; + + #[test] + fn profile_name_parsing() { + assert_eq!( + ProfileName::parse("profile name"), + ProfileName { + name: "name", + has_profile_prefix: true + } + ); + assert_eq!( + ProfileName::parse("name"), + ProfileName { + name: "name", + has_profile_prefix: false + } + ); + assert_eq!( + ProfileName::parse("profile\tname"), + ProfileName { + name: "name", + has_profile_prefix: true + } + ); + assert_eq!( + ProfileName::parse("profile name "), + ProfileName { + name: "name", + has_profile_prefix: true + } + ); + assert_eq!( + ProfileName::parse("profilename"), + ProfileName { + name: "profilename", + has_profile_prefix: false + } + ); + assert_eq!( + ProfileName::parse(" whitespace "), + ProfileName { + name: "whitespace", + has_profile_prefix: false + } + ); + } + + #[test] + #[traced_test] + fn ignored_key_generates_warning() { + let mut profile: RawProfileSet = HashMap::new(); + profile.insert("default", { + let mut out = HashMap::new(); + out.insert("invalid key", "value".into()); + out + }); + let mut base = ProfileSet::empty(); + merge_in(&mut base, profile, FileKind::Config); + assert!(base + .get_profile("default") + .expect("contains default profile") + .properties + .is_empty()); + assert!(logs_contain( + "key ignored because `invalid key` was not a valid identifier" + )); + } + + #[test] + #[traced_test] + fn invalid_profile_generates_warning() { + let mut profile: RawProfileSet = HashMap::new(); + profile.insert("foo", HashMap::new()); + merge_in(&mut ProfileSet::empty(), profile, FileKind::Config); + assert!(logs_contain("profile `foo` ignored")); + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs new file mode 100644 index 0000000000..5f921692b9 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs @@ -0,0 +1,351 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Profile file parsing +//! +//! This file implements profile file parsing at a very literal level. Prior to actually being used, +//! profiles must be normalized into a canonical form. Constructions that will eventually be +//! deemed invalid are accepted during parsing such as: +//! - keys that are invalid identifiers: `a b = c` +//! - profiles with invalid names +//! - profile name normalization (`profile foo` => `foo`) + +use crate::profile::parser::source::File; +use std::borrow::Cow; +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{self, Display, Formatter}; + +/// A set of profiles that still carries a reference to the underlying data +pub type RawProfileSet<'a> = HashMap<&'a str, HashMap<&'a str, Cow<'a, str>>>; + +/// Characters considered to be whitespace by the spec +/// +/// Profile parsing is actually quite strict about what is and is not whitespace, so use this instead +/// of `.is_whitespace()` / `.trim()` +pub const WHITESPACE: &[char] = &[' ', '\t']; +const COMMENT: &[char] = &['#', ';']; + +/// Location for use during error reporting +#[derive(Clone, Debug, Eq, PartialEq)] +struct Location { + line_number: usize, + path: String, +} + +/// An error encountered while parsing a profile +#[derive(Debug)] +pub struct ProfileParseError { + /// Location where this error occurred + location: Location, + + /// Error message + message: String, +} + +impl Display for ProfileParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "error parsing {} on line {}:\n {}", + self.location.path, self.location.line_number, self.message + ) + } +} + +impl Error for ProfileParseError {} + +/// Validate that a line represents a valid subproperty +/// +/// - Subproperties looks like regular properties (`k=v`) that are nested within an existing property. +/// - Subproperties must be validated for compatibility with other SDKs, but they are not actually +/// parsed into structured data. +fn validate_subproperty(value: &str, location: Location) -> Result<(), ProfileParseError> { + if value.trim_matches(WHITESPACE).is_empty() { + Ok(()) + } else { + parse_property_line(value) + .map_err(|err| err.into_error("sub-property", location)) + .map(|_| ()) + } +} + +fn is_empty_line(line: &str) -> bool { + line.trim_matches(WHITESPACE).is_empty() +} + +fn is_comment_line(line: &str) -> bool { + line.starts_with(COMMENT) +} + +/// Parser for profile files +struct Parser<'a> { + /// In-progress profile representation + data: RawProfileSet<'a>, + + /// Parser state + state: State<'a>, + + /// Parser source location + /// + /// Location is tracked to facilitate error reporting + location: Location, +} + +enum State<'a> { + Starting, + ReadingProfile { + profile: &'a str, + property: Option<&'a str>, + is_subproperty: bool, + }, +} + +/// Parse `file` into a `RawProfileSet` +pub fn parse_profile_file(file: &File) -> Result { + let mut parser = Parser { + data: HashMap::new(), + state: State::Starting, + location: Location { + line_number: 0, + path: file.path.to_string(), + }, + }; + parser.parse_profile(&file.contents)?; + Ok(parser.data) +} + +impl<'a> Parser<'a> { + /// Parse `file` containing profile data into `self.data`. + fn parse_profile(&mut self, file: &'a str) -> Result<(), ProfileParseError> { + for (line_number, line) in file.lines().enumerate() { + self.location.line_number = line_number + 1; // store a 1-indexed line number + if is_empty_line(line) || is_comment_line(line) { + continue; + } + if line.starts_with('[') { + self.read_profile_line(line)?; + } else if line.starts_with(WHITESPACE) { + self.read_property_continuation(line)?; + } else { + self.read_property_line(line)?; + } + } + Ok(()) + } + + /// Parse a property line like `a = b` + /// + /// A property line is only valid when we're within a profile definition, `[profile foo]` + fn read_property_line(&mut self, line: &'a str) -> Result<(), ProfileParseError> { + let location = &self.location; + let (current_profile, name) = match &self.state { + State::Starting => return Err(self.make_error("Expected a profile definition")), + State::ReadingProfile { profile, .. } => ( + self.data.get_mut(*profile).expect("profile must exist"), + profile, + ), + }; + let (k, v) = parse_property_line(line) + .map_err(|err| err.into_error("property", location.clone()))?; + self.state = State::ReadingProfile { + profile: name, + property: Some(k), + is_subproperty: v.is_empty(), + }; + current_profile.insert(k, v.into()); + Ok(()) + } + + /// Create a location-tagged error message + fn make_error(&self, message: &str) -> ProfileParseError { + ProfileParseError { + location: self.location.clone(), + message: message.into(), + } + } + + /// Parse the lines of a property after the first line. + /// + /// This is triggered by lines that start with whitespace. + fn read_property_continuation(&mut self, line: &'a str) -> Result<(), ProfileParseError> { + let current_property = match &self.state { + State::Starting => return Err(self.make_error("Expected a profile definition")), + State::ReadingProfile { + profile, + property: Some(property), + is_subproperty, + } => { + if *is_subproperty { + validate_subproperty(line, self.location.clone())?; + } + self.data + .get_mut(*profile) + .expect("profile must exist") + .get_mut(*property) + .expect("property must exist") + } + State::ReadingProfile { + profile: _, + property: None, + .. + } => return Err(self.make_error("Expected a property definition, found continuation")), + }; + let line = line.trim_matches(WHITESPACE); + let current_property = current_property.to_mut(); + current_property.push('\n'); + current_property.push_str(line); + Ok(()) + } + + fn read_profile_line(&mut self, line: &'a str) -> Result<(), ProfileParseError> { + let line = prepare_line(line, false); + let profile_name = line + .strip_prefix('[') + .ok_or_else(|| self.make_error("Profile definition must start with ]"))? + .strip_suffix(']') + .ok_or_else(|| self.make_error("Profile definition must end with ']'"))?; + if !self.data.contains_key(profile_name) { + self.data.insert(profile_name, Default::default()); + } + self.state = State::ReadingProfile { + profile: profile_name, + property: None, + is_subproperty: false, + }; + Ok(()) + } +} + +/// Error encountered while parsing a property +#[derive(Debug, Eq, PartialEq)] +enum PropertyError { + NoEquals, + NoName, +} + +impl PropertyError { + fn into_error(self, ctx: &str, location: Location) -> ProfileParseError { + let mut ctx = ctx.to_string(); + match self { + PropertyError::NoName => { + ctx.get_mut(0..1).unwrap().make_ascii_uppercase(); + ProfileParseError { + location, + message: format!("{} did not have a name", ctx), + } + } + PropertyError::NoEquals => ProfileParseError { + location, + message: format!("Expected an '=' sign defining a {}", ctx), + }, + } + } +} + +/// Parse a property line into a key-value pair +fn parse_property_line(line: &str) -> Result<(&str, &str), PropertyError> { + let line = prepare_line(line, true); + let (k, v) = line.split_once('=').ok_or(PropertyError::NoEquals)?; + let k = k.trim_matches(WHITESPACE); + let v = v.trim_matches(WHITESPACE); + if k.is_empty() { + return Err(PropertyError::NoName); + } + Ok((k, v)) +} + +/// Prepare a line for parsing +/// +/// Because leading whitespace is significant, this method should only be called after determining +/// whether a line represents a property (no whitespace) or a sub-property (whitespace). +/// This function preprocesses a line to simplify parsing: +/// 1. Strip leading and trailing whitespace +/// 2. Remove trailing comments +/// +/// Depending on context, comment characters may need to be preceded by whitespace to be considered +/// comments. +fn prepare_line(line: &str, comments_need_whitespace: bool) -> &str { + let line = line.trim_matches(WHITESPACE); + let mut prev_char_whitespace = false; + let mut comment_idx = None; + for (idx, chr) in line.char_indices() { + if (COMMENT.contains(&chr)) && (prev_char_whitespace || !comments_need_whitespace) { + comment_idx = Some(idx); + break; + } + prev_char_whitespace = chr.is_whitespace(); + } + comment_idx + .map(|idx| &line[..idx]) + .unwrap_or(&line) + // trimming the comment might result in more whitespace that needs to be handled + .trim_matches(WHITESPACE) +} + +#[cfg(test)] +mod test { + use super::{parse_profile_file, prepare_line, Location}; + use crate::profile::parser::parse::{parse_property_line, PropertyError}; + use crate::profile::parser::source::File; + + // most test cases covered by the JSON test suite + + #[test] + fn property_parsing() { + assert_eq!(parse_property_line("a = b"), Ok(("a", "b"))); + assert_eq!(parse_property_line("a=b"), Ok(("a", "b"))); + assert_eq!(parse_property_line("a = b "), Ok(("a", "b"))); + assert_eq!(parse_property_line(" a = b "), Ok(("a", "b"))); + assert_eq!(parse_property_line(" a = b 🐱 "), Ok(("a", "b 🐱"))); + assert_eq!(parse_property_line("a b"), Err(PropertyError::NoEquals)); + assert_eq!(parse_property_line("= b"), Err(PropertyError::NoName)); + assert_eq!(parse_property_line("a = "), Ok(("a", ""))); + assert_eq!( + parse_property_line("something_base64=aGVsbG8gZW50aHVzaWFzdGljIHJlYWRlcg=="), + Ok(("something_base64", "aGVsbG8gZW50aHVzaWFzdGljIHJlYWRlcg==")) + ); + } + + #[test] + fn prepare_line_strips_comments() { + assert_eq!( + prepare_line("name = value # Comment with # sign", true), + "name = value" + ); + + assert_eq!( + prepare_line("name = value#Comment # sign", true), + "name = value#Comment" + ); + + assert_eq!( + prepare_line("name = value#Comment # sign", false), + "name = value" + ); + } + + #[test] + fn error_line_numbers() { + let file = File { + path: "~/.aws/config".into(), + contents: "[default\nk=v".into(), + }; + let err = parse_profile_file(&file).expect_err("parsing should fail"); + assert_eq!(err.message, "Profile definition must end with ']'"); + assert_eq!( + err.location, + Location { + path: "~/.aws/config".into(), + line_number: 1 + } + ) + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/parser/source.rs b/aws/rust-runtime/aws-config/src/profile/parser/source.rs new file mode 100644 index 0000000000..75838d111d --- /dev/null +++ b/aws/rust-runtime/aws-config/src/profile/parser/source.rs @@ -0,0 +1,353 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use aws_types::os_shim_internal; +use std::borrow::Cow; +use std::io::ErrorKind; +use std::path::{Component, Path, PathBuf}; +use tracing::Instrument; + +/// In-memory source of profile data +pub struct Source { + /// Contents and path of ~/.aws/config + pub config_file: File, + + /// Contents and path of ~/.aws/credentials + pub credentials_file: File, + + /// Profile to use + /// + /// Overridden via `$AWS_PROFILE`, defaults to `default` + pub profile: Cow<'static, str>, +} + +/// In-memory configuration file +pub struct File { + pub path: String, + pub contents: String, +} + +#[derive(Clone, Copy)] +pub enum FileKind { + Config, + Credentials, +} + +impl FileKind { + fn default_path(&self) -> &'static str { + match &self { + FileKind::Credentials => "~/.aws/credentials", + FileKind::Config => "~/.aws/config", + } + } + + fn override_environment_variable(&self) -> &'static str { + match &self { + FileKind::Config => "AWS_CONFIG_FILE", + FileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE", + } + } +} + +/// Load a [Source](Source) from a given environment and filesystem. +pub async fn load(proc_env: &os_shim_internal::Env, fs: &os_shim_internal::Fs) -> Source { + let home = home_dir(&proc_env, Os::real()); + let config = load_config_file(FileKind::Config, &home, &fs, &proc_env) + .instrument(tracing::info_span!("load_config_file")) + .await; + let credentials = load_config_file(FileKind::Credentials, &home, &fs, &proc_env) + .instrument(tracing::info_span!("load_credentials_file")) + .await; + + Source { + config_file: config, + credentials_file: credentials, + profile: proc_env + .get("AWS_PROFILE") + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed("default")), + } +} + +/// Loads an AWS Config file +/// +/// Both the default & the overriding patterns may contain `~/` which MUST be expanded to the users +/// home directory in a platform-aware way (see [`expand_home`](expand_home)) +/// +/// Arguments: +/// * `kind`: The type of config file to load +/// * `home_directory`: Home directory to use during home directory expansion +/// * `fs`: Filesystem abstraction +/// * `environment`: Process environment abstraction +async fn load_config_file( + kind: FileKind, + home_directory: &Option, + fs: &os_shim_internal::Fs, + environment: &os_shim_internal::Env, +) -> File { + let path = environment + .get(kind.override_environment_variable()) + .map(Cow::Owned) + .ok() + .unwrap_or_else(|| kind.default_path().into()); + let expanded = expand_home(path.as_ref(), home_directory); + if path != expanded.to_string_lossy() { + tracing::debug!(before = ?path, after = ?expanded, "home directory expanded"); + } + // read the data at the specified path + // if the path does not exist, log a warning but pretend it was actually an empty file + let data = match fs.read_to_end(&expanded).await { + Ok(data) => data, + Err(e) => { + match e.kind() { + ErrorKind::NotFound if path == kind.default_path() => { + tracing::info!(path = %path, "config file not found") + } + ErrorKind::NotFound if path != kind.default_path() => { + // in the case where the user overrode the path with an environment variable, + // log more loudly than the case where the default path was missing + tracing::warn!(path = %path, env = %kind.override_environment_variable(), "config file overridden via environment variable not found") + } + _other => tracing::warn!(path = %path, error = %e, "failed to read config file"), + }; + Default::default() + } + }; + // if the file is not valid utf-8, log a warning and use an empty file instead + let data = match String::from_utf8(data) { + Ok(data) => data, + Err(e) => { + tracing::warn!(path = %path, error = %e, "config file did not contain utf-8 encoded data"); + Default::default() + } + }; + tracing::info!(path = %path, size = ?data.len(), "config file loaded"); + File { + // lossy is OK here, the name of this file is just for debugging purposes + path: expanded.to_string_lossy().into(), + contents: data, + } +} + +fn expand_home(path: impl AsRef, home_dir: &Option) -> PathBuf { + let path = path.as_ref(); + let mut components = path.components(); + let start = components.next(); + match start { + None => path.into(), // empty path, + Some(Component::Normal(s)) if s == "~" => { + // do homedir replacement + let path = match home_dir { + Some(dir) => { + tracing::debug!(home = ?dir, path = ?path, "performing home directory substitution"); + dir.clone() + } + None => { + tracing::warn!( + "could not determine home directory but home expansion was requested" + ); + // if we can't determine the home directory, just leave it as `~` + "~".into() + } + }; + let mut path: PathBuf = path.into(); + // rewrite the path using system-specific path separators + for component in components { + path.push(component); + } + path + } + // Finally, handle the case where it doesn't begin with some version of `~/`: + // NOTE: in this case we aren't performing path rewriting. This is correct because + // this path comes from an environment variable on the target + // platform, so in that case, the separators should already be correct. + _other => path.into(), + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Os { + Windows, + NotWindows, +} + +impl Os { + pub fn real() -> Self { + match std::env::consts::OS { + "windows" => Os::Windows, + _ => Os::NotWindows, + } + } +} + +/// Resolve a home directory given a set of environment variables +fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option { + if let Ok(home) = env_var.get("HOME") { + tracing::debug!(src = "HOME", "loaded home directory"); + return Some(home); + } + + if os == Os::Windows { + if let Ok(home) = env_var.get("USERPROFILE") { + tracing::debug!(src = "USERPROFILE", "loaded home directory"); + return Some(home); + } + + let home_drive = env_var.get("HOMEDRIVE"); + let home_path = env_var.get("HOMEPATH"); + tracing::debug!(src = "HOMEDRIVE/HOMEPATH", "loaded home directory"); + if let (Ok(mut drive), Ok(path)) = (home_drive, home_path) { + drive.push_str(&path); + return Some(drive); + } + } + None +} + +#[cfg(test)] +mod tests { + use crate::profile::parser::source::{expand_home, home_dir, load, Os}; + use aws_types::os_shim_internal::{Env, Fs}; + use serde::Deserialize; + use std::collections::HashMap; + use std::error::Error; + use std::fs; + + #[test] + fn only_expand_home_prefix() { + // ~ is only expanded as a single component (currently) + let path = "~aws/config"; + assert_eq!(expand_home(&path, &None).to_str().unwrap(), "~aws/config"); + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct SourceTests { + tests: Vec, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct TestCase { + name: String, + environment: HashMap, + platform: String, + profile: Option, + config_location: String, + credentials_location: String, + } + + /// Run all tests from file-location-tests.json + #[test] + fn run_tests() -> Result<(), Box> { + let tests = fs::read_to_string("test-data/file-location-tests.json")?; + let tests: SourceTests = serde_json::from_str(&tests)?; + for (i, test) in tests.tests.into_iter().enumerate() { + eprintln!("test: {}", i); + check(test) + .now_or_never() + .expect("these futures should never poll"); + } + Ok(()) + } + + use futures_util::FutureExt; + use tracing_test::traced_test; + + #[traced_test] + #[test] + fn logs_produced_default() { + let env = Env::from_slice(&[("HOME", "/user/name")]); + let mut fs = HashMap::new(); + fs.insert( + "/user/name/.aws/config".to_string(), + "[default]\nregion = us-east-1".into(), + ); + + let fs = Fs::from_map(fs); + + let _src = load(&env, &fs).now_or_never(); + assert!(logs_contain("config file loaded")); + assert!(logs_contain("performing home directory substitution")); + } + + async fn check(test_case: TestCase) { + let fs = Fs::real(); + let env = Env::from(test_case.environment); + let platform_matches = (cfg!(windows) && test_case.platform == "windows") + || (!cfg!(windows) && test_case.platform != "windows"); + if platform_matches { + let source = load(&env, &fs).await; + if let Some(expected_profile) = test_case.profile { + assert_eq!(source.profile, expected_profile, "{}", &test_case.name); + } + assert_eq!( + source.config_file.path, test_case.config_location, + "{}", + &test_case.name + ); + assert_eq!( + source.credentials_file.path, test_case.credentials_location, + "{}", + &test_case.name + ) + } else { + println!( + "NOTE: ignoring test case for {} which does not apply to our platform: \n {}", + &test_case.platform, &test_case.name + ) + } + } + + #[test] + #[cfg_attr(windows, ignore)] + fn test_expand_home() { + let path = "~/.aws/config"; + assert_eq!( + expand_home(&path, &Some("/user/foo".to_string())) + .to_str() + .unwrap(), + "/user/foo/.aws/config" + ); + } + + #[test] + fn homedir_profile_only_windows() { + // windows specific variables should only be considered when the platform is windows + let env = Env::from_slice(&[("USERPROFILE", "C:\\Users\\name")]); + assert_eq!( + home_dir(&env, Os::Windows), + Some("C:\\Users\\name".to_string()) + ); + assert_eq!(home_dir(&env, Os::NotWindows), None); + } + + #[test] + fn expand_home_no_home() { + // there is an edge case around expansion when no home directory exists + // if no home directory can be determined, leave the path as is + if !cfg!(windows) { + assert_eq!(expand_home("~/config", &None).to_str().unwrap(), "~/config") + } else { + assert_eq!( + expand_home("~/config", &None).to_str().unwrap(), + "~\\config" + ) + } + } + + /// Test that a linux oriented path expands on windows + #[test] + #[cfg_attr(not(windows), ignore)] + fn test_expand_home_windows() { + let path = "~/.aws/config"; + assert_eq!( + expand_home(&path, &Some("C:\\Users\\name".to_string())) + .to_str() + .unwrap(), + "C:\\Users\\name\\.aws\\config" + ); + } +} diff --git a/aws/rust-runtime/aws-config/src/sts.rs b/aws/rust-runtime/aws-config/src/sts.rs new file mode 100644 index 0000000000..4bf74f192c --- /dev/null +++ b/aws/rust-runtime/aws-config/src/sts.rs @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +pub mod util { + use aws_sdk_sts::model::Credentials as StsCredentials; + use aws_types::credentials::{self, CredentialsError}; + use aws_types::Credentials as AwsCredentials; + use std::time::{SystemTime, UNIX_EPOCH}; + + /// Convert STS credentials to aws_auth::Credentials + pub fn into_credentials( + sts_credentials: Option, + provider_name: &'static str, + ) -> credentials::Result { + let sts_credentials = sts_credentials + .ok_or_else(|| CredentialsError::Unhandled("STS credentials must be defined".into()))?; + let expiration = sts_credentials + .expiration + .ok_or_else(|| CredentialsError::Unhandled("missing expiration".into()))?; + let expiration = expiration.to_system_time().ok_or_else(|| { + CredentialsError::Unhandled( + format!("expiration is before unix epoch: {:?}", &expiration).into(), + ) + })?; + Ok(AwsCredentials::new( + sts_credentials.access_key_id.ok_or_else(|| { + CredentialsError::Unhandled("access key id missing from result".into()) + })?, + sts_credentials + .secret_access_key + .ok_or_else(|| CredentialsError::Unhandled("secret access token missing".into()))?, + sts_credentials.session_token, + Some(expiration), + provider_name, + )) + } + + /// Create a default STS session name + /// + /// STS Assume Role providers MUST assign a name to their generated session. When a user does not + /// provide a name for the session, the provider will choose a name composed of a base + a timestamp, + /// eg. `profile-file-provider-123456789` + pub fn default_session_name(base: &str) -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("post epoch"); + format!("{}-{}", base, now.as_millis()) + } +} diff --git a/aws/rust-runtime/aws-config/src/test_case.rs b/aws/rust-runtime/aws-config/src/test_case.rs new file mode 100644 index 0000000000..a18182e001 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/test_case.rs @@ -0,0 +1,178 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use std::collections::HashMap; +use std::error::Error; +use std::path::{Path, PathBuf}; +use std::time::UNIX_EPOCH; + +use aws_types::credentials::{self, ProvideCredentials}; +use aws_types::os_shim_internal::{Env, Fs}; +use serde::Deserialize; +use smithy_client::dvr::{NetworkTraffic, RecordingConnection, ReplayingConnection}; +use smithy_client::erase::DynConnector; + +/// Test case credentials +/// +/// Credentials for use in test cases. These implement Serialize/Deserialize and have a +/// non-hidden debug implementation. +#[derive(Deserialize, Debug, Eq, PartialEq)] +struct Credentials { + access_key_id: String, + secret_access_key: String, + session_token: Option, + expiry: Option, +} + +/// Convert real credentials to test credentials +/// +/// Comparing equality on real credentials works, but it's a pain because the Debug implementation +/// hides the actual keys +impl From<&aws_types::Credentials> for Credentials { + fn from(credentials: &aws_types::Credentials) -> Self { + Self { + access_key_id: credentials.access_key_id().into(), + secret_access_key: credentials.secret_access_key().into(), + session_token: credentials.session_token().map(ToString::to_string), + expiry: credentials + .expiry() + .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs()), + } + } +} + +/// Credentials test environment +/// +/// A credentials test environment is a directory containing: +/// - an `fs` directory. This is loaded into the test as if it was mounted at `/` +/// - an `env.json` file containing environment variables +/// - an `http-traffic.json` file containing an http traffic log from [`dvr`](smithy_client::dvr) +/// - a `test-case.json` file defining the expected output of the test +pub struct TestEnvironment { + env: Env, + fs: Fs, + network_traffic: NetworkTraffic, + metadata: Metadata, + base_dir: PathBuf, +} + +#[derive(Deserialize)] +enum TestResult { + Ok(Credentials), + ErrorContains(String), +} + +#[derive(Deserialize)] +pub struct Metadata { + result: TestResult, + docs: String, + name: String, +} + +impl TestEnvironment { + pub fn from_dir(dir: impl AsRef) -> Result> { + let dir = dir.as_ref(); + let env = std::fs::read_to_string(dir.join("env.json")) + .map_err(|e| format!("failed to load env: {}", e))?; + let env: HashMap = + serde_json::from_str(&env).map_err(|e| format!("failed to parse env: {}", e))?; + let env = Env::from(env); + let fs = Fs::from_test_dir(dir.join("fs"), "/"); + let network_traffic = std::fs::read_to_string(dir.join("http-traffic.json")) + .map_err(|e| format!("failed to load http traffic: {}", e))?; + let network_traffic: NetworkTraffic = serde_json::from_str(&network_traffic)?; + + let metadata: Metadata = serde_json::from_str( + &std::fs::read_to_string(dir.join("test-case.json")) + .map_err(|e| format!("failed to load test case: {}", e))?, + )?; + Ok(TestEnvironment { + base_dir: dir.into(), + env, + fs, + network_traffic, + metadata, + }) + } + + #[allow(dead_code)] + /// Execute the test suite & record a new traffic log + /// + /// A connector will be created with the factory, then request traffic will be recorded. + /// Response are generated from the existing http-traffic.json. + pub async fn execute_and_update

(&self, make_provider: impl Fn(Fs, Env, DynConnector) -> P) + where + P: ProvideCredentials, + { + let connector = RecordingConnection::new(ReplayingConnection::new( + self.network_traffic.events().clone(), + )); + let provider = make_provider( + self.fs.clone(), + self.env.clone(), + DynConnector::new(connector.clone()), + ); + let result = provider.provide_credentials().await; + std::fs::write( + self.base_dir.join("http-traffic-recorded.json"), + serde_json::to_string(&connector.network_traffic()).unwrap(), + ) + .unwrap(); + self.check_results(&result); + } + + fn log_info(&self) { + eprintln!("test case: {}. {}", self.metadata.name, self.metadata.docs); + } + + /// Execute a test case. Failures lead to panics. + pub async fn execute

(&self, make_provider: impl Fn(Fs, Env, DynConnector) -> P) + where + P: ProvideCredentials, + { + let connector = ReplayingConnection::new(self.network_traffic.events().clone()); + let provider = make_provider( + self.fs.clone(), + self.env.clone(), + DynConnector::new(connector.clone()), + ); + let result = provider.provide_credentials().await; + self.log_info(); + self.check_results(&result); + // todo: validate bodies + match connector.validate(&["CONTENT-TYPE"], |_expected, _actual| Ok(())) { + Ok(()) => {} + Err(e) => panic!("{}", e), + } + } + + fn check_results(&self, result: &credentials::Result) { + match (&result, &self.metadata.result) { + (Ok(actual), TestResult::Ok(expected)) => { + assert_eq!( + expected, + &Credentials::from(actual), + "incorrect credentials were returned" + ) + } + (Err(err), TestResult::ErrorContains(substr)) => { + assert!( + format!("{}", err).contains(substr), + "`{}` did not contain `{}`", + err, + substr + ) + } + (Err(actual_error), TestResult::Ok(expected_creds)) => panic!( + "expected credentials ({:?}) but an error was returned: {}", + expected_creds, actual_error + ), + (Ok(creds), TestResult::ErrorContains(substr)) => panic!( + "expected an error containing: `{}`, but credentials were returned: {:?}", + substr, creds + ), + } + } +} diff --git a/aws/rust-runtime/aws-config/src/web_identity_token.rs b/aws/rust-runtime/aws-config/src/web_identity_token.rs new file mode 100644 index 0000000000..5866ae5d39 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/web_identity_token.rs @@ -0,0 +1,333 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Load Credentials from Web Identity Tokens +//! +//! WebIdentity tokens can be loaded via environment variables, or via profiles: +//! +//! ## Via Environment Variables +//! WebIdentityTokenCredentialProvider will load the following environment variables: +//! - `AWS_WEB_IDENTITY_TOKEN_FILE`: **required**, location to find the token file containing a JWT token +//! - `AWS_ROLE_ARN`: **required**, role ARN to assume +//! - `AWS_IAM_ROLE_SESSION_NAME`: **optional**: Session name to use when assuming the role +//! +//! ## Via Shared Config Profiles +//! Web identity token credentials can be loaded from `~/.aws/config` in two ways: +//! 1. Directly: +//! ```ini +//! [profile default] +//! role_arn = arn:aws:iam::1234567890123:role/RoleA +//! web_identity_token_file = /token.jwt +//! ``` +//! +//! 2. As a source profile for another role: +//! +//! ```ini +//! [profile default] +//! role_arn = arn:aws:iam::123456789:role/RoleA +//! source_profile = base +//! +//! [profile base] +//! role_arn = arn:aws:iam::123456789012:role/s3-reader +//! web_identity_token_file = /token.jwt +//! ``` + +use aws_sdk_sts::Region; +use aws_types::os_shim_internal::{Env, Fs}; +use smithy_client::erase::DynConnector; + +use crate::connector::must_have_connector; +use crate::sts; +use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials}; +use std::borrow::Cow; +use std::path::{Path, PathBuf}; + +const ENV_VAR_TOKEN_FILE: &str = "AWS_WEB_IDENTITY_TOKEN_FILE"; +const ENV_VAR_ROLE_ARN: &str = "AWS_ROLE_ARN"; +const ENV_VAR_SESSION_NAME: &str = "AWS_ROLE_SESSION_NAME"; + +/// Credential provider to load credentials from Web Identity Tokens +/// +/// See Module documentation for more details +#[derive(Debug)] +pub struct WebIdentityTokenCredentialsProvider { + source: Source, + fs: Fs, + client: aws_hyper::StandardClient, + region: Option, +} + +impl WebIdentityTokenCredentialsProvider { + /// Builder for this credentials provider + pub fn builder() -> Builder { + Builder::default() + } +} + +#[derive(Debug)] +enum Source { + Env(Env), + Static(StaticConfiguration), +} + +/// Statically configured WebIdentityToken configuration +#[derive(Debug, Clone)] +pub struct StaticConfiguration { + /// Location of the file containing the web identity token + pub web_identity_token_file: PathBuf, + + /// RoleArn to assume + pub role_arn: String, + + /// Session name to use when assuming the role + pub session_name: String, +} + +impl ProvideCredentials for WebIdentityTokenCredentialsProvider { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::new(self.credentials()) + } +} + +impl WebIdentityTokenCredentialsProvider { + fn source(&self) -> Result, CredentialsError> { + match &self.source { + Source::Env(env) => { + let token_file = env + .get(ENV_VAR_TOKEN_FILE) + .map_err(|_| CredentialsError::CredentialsNotLoaded)?; + let role_arn = env.get(ENV_VAR_ROLE_ARN).map_err(|_| { + CredentialsError::InvalidConfiguration( + "AWS_ROLE_ARN environment variable must be set".into(), + ) + })?; + let session_name = env + .get(ENV_VAR_SESSION_NAME) + .unwrap_or_else(|_| sts::util::default_session_name("web-identity-token")); + Ok(Cow::Owned(StaticConfiguration { + web_identity_token_file: token_file.into(), + role_arn, + session_name, + })) + } + Source::Static(conf) => Ok(Cow::Borrowed(conf)), + } + } + async fn credentials(&self) -> credentials::Result { + let conf = self.source()?; + load_credentials( + &self.fs, + &self.client, + &self.region.as_ref().cloned().ok_or_else(|| { + CredentialsError::InvalidConfiguration( + "region is required for WebIdentityTokenProvider".into(), + ) + })?, + &conf.web_identity_token_file, + &conf.role_arn, + &conf.session_name, + ) + .await + } +} + +/// Builder for [`WebIdentityTokenCredentialsProvider`](WebIdentityTokenCredentialsProvider) +#[derive(Default)] +pub struct Builder { + source: Option, + fs: Fs, + connector: Option, + region: Option, +} + +impl Builder { + #[doc(hidden)] + /// Set the Fs used for this provider + pub fn fs(mut self, fs: Fs) -> Self { + self.fs = fs; + self + } + + #[doc(hidden)] + /// Set the Fs used for this provider + pub fn set_fs(&mut self, fs: Fs) -> &mut Self { + self.fs = fs; + self + } + + #[doc(hidden)] + /// Set the process environment used for this provider + pub fn env(mut self, env: Env) -> Self { + self.source = Some(Source::Env(env)); + self + } + + #[doc(hidden)] + /// Set the process environment used for this provider + pub fn set_env(&mut self, env: Env) -> &mut Self { + self.source = Some(Source::Env(env)); + self + } + + /// Configure this builder to use [`StaticConfiguration`](StaticConfiguration) + /// + /// WebIdentityToken providers load credentials from the file system. They may either determine + /// the path from environment variables (default), or via a statically configured path. + pub fn static_configuration(mut self, config: StaticConfiguration) -> Self { + self.source = Some(Source::Static(config)); + self + } + + /// Sets the HTTPS connector used for this provider + pub fn connector(mut self, connector: DynConnector) -> Self { + self.connector = Some(connector); + self + } + + /// Sets the HTTPS connector used for this provider + pub fn set_connector(&mut self, connector: Option) -> &mut Self { + self.connector = connector; + self + } + + /// Sets the region used for this provider + pub fn region(mut self, region: Option) -> Self { + self.region = region; + self + } + + /// Sets the region used for this provider + pub fn set_region(&mut self, region: Option) -> &mut Self { + self.region = region; + self + } + + /// Build a [`WebIdentityTokenCredentialsProvider`] + /// + /// ## Panics + /// If no connector has been enabled via crate features and no connector has been provided via the + /// builder, this function will panic. + pub fn build(self) -> WebIdentityTokenCredentialsProvider { + let connector = self.connector.unwrap_or_else(must_have_connector); + let client = aws_hyper::Client::new(connector); + let source = self.source.unwrap_or_else(|| Source::Env(Env::default())); + WebIdentityTokenCredentialsProvider { + source, + fs: self.fs, + client, + region: self.region, + } + } +} + +async fn load_credentials( + fs: &Fs, + client: &aws_hyper::StandardClient, + region: &Region, + token_file: impl AsRef, + role_arn: &str, + session_name: &str, +) -> credentials::Result { + let token = fs + .read_to_end(token_file) + .await + .map_err(|err| CredentialsError::ProviderError(err.into()))?; + let token = String::from_utf8(token).map_err(|_utf_8_error| { + CredentialsError::Unhandled("WebIdentityToken was not valid UTF-8".into()) + })?; + let conf = aws_sdk_sts::Config::builder() + .region(region.clone()) + .build(); + + let operation = aws_sdk_sts::operation::AssumeRoleWithWebIdentity::builder() + .role_arn(role_arn) + .role_session_name(session_name) + .web_identity_token(token) + .build() + .expect("valid operation") + .make_operation(&conf) + .expect("valid operation"); + let resp = client.call(operation).await.map_err(|sdk_error| { + tracing::warn!(error = ?sdk_error, "sts returned an error assuming web identity role"); + CredentialsError::ProviderError(sdk_error.into()) + })?; + sts::util::into_credentials(resp.credentials, "WebIdentityToken") +} + +#[cfg(test)] +mod test { + use crate::web_identity_token::{ + Builder, ENV_VAR_ROLE_ARN, ENV_VAR_SESSION_NAME, ENV_VAR_TOKEN_FILE, + }; + + use aws_sdk_sts::Region; + use aws_types::os_shim_internal::{Env, Fs}; + + use aws_types::credentials::CredentialsError; + use std::collections::HashMap; + + #[tokio::test] + async fn unloaded_provider() { + // empty environment + let env = Env::from_slice(&[]); + let provider = Builder::default() + .region(Some(Region::new("us-east-1"))) + .env(env) + .build(); + let err = provider + .credentials() + .await + .expect_err("should fail, provider not loaded"); + match err { + CredentialsError::CredentialsNotLoaded => { /* ok */ } + _ => panic!("incorrect error variant"), + } + } + + #[tokio::test] + async fn missing_env_var() { + let env = Env::from_slice(&[(ENV_VAR_TOKEN_FILE, "/token.jwt")]); + let provider = Builder::default() + .region(Some(Region::new("us-east-1"))) + .env(env) + .build(); + let err = provider + .credentials() + .await + .expect_err("should fail, provider not loaded"); + assert!( + format!("{}", err).contains("AWS_ROLE_ARN"), + "`{}` did not contain expected string", + err + ); + match err { + CredentialsError::InvalidConfiguration(_) => { /* ok */ } + _ => panic!("incorrect error variant"), + } + } + + #[tokio::test] + async fn fs_missing_file() { + let env = Env::from_slice(&[ + (ENV_VAR_TOKEN_FILE, "/token.jwt"), + (ENV_VAR_ROLE_ARN, "arn:aws:iam::123456789123:role/test-role"), + (ENV_VAR_SESSION_NAME, "test-session"), + ]); + let fs = Fs::from_map(HashMap::new()); + let provider = Builder::default() + .region(Some(Region::new("us-east-1"))) + .fs(fs) + .env(env) + .build(); + let err = provider.credentials().await.expect_err("no JWT token"); + match err { + CredentialsError::ProviderError(_) => { /* ok */ } + _ => panic!("incorrect error variant"), + } + } +} diff --git a/aws/rust-runtime/aws-auth-providers/test-data/assume-role-tests.json b/aws/rust-runtime/aws-config/test-data/assume-role-tests.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/assume-role-tests.json rename to aws/rust-runtime/aws-config/test-data/assume-role-tests.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/fs/home/.aws/credentials similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/fs/home/.aws/credentials rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/fs/home/.aws/credentials diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/prefer_environment/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/prefer_environment/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/fs/token.jwt b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/fs/token.jwt similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/fs/token.jwt rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/fs/token.jwt diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_overrides_web_identity/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_overrides_web_identity/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/credentials similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/credentials rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/fs/home/.aws/credentials diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/profile_static_keys/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/profile_static_keys/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/token.jwt b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/token.jwt similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/token.jwt rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/fs/token.jwt diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_source_profile_no_env/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_source_profile_no_env/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/fs/token.jwt b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/fs/token.jwt similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/fs/token.jwt rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/fs/token.jwt diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_env/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_env/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/token.jwt b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/token.jwt similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/token.jwt rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/fs/token.jwt diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_invalid_jwt/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_invalid_jwt/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/fs/token.jwt b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/fs/token.jwt similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/fs/token.jwt rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/fs/token.jwt diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_profile/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_profile/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/env.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/env.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/fs/token.jwt b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/fs/token.jwt similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/fs/token.jwt rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/fs/token.jwt diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/http-traffic.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/default-provider-chain/web_identity_token_source_profile/test-case.json rename to aws/rust-runtime/aws-config/test-data/default-provider-chain/web_identity_token_source_profile/test-case.json diff --git a/aws/rust-runtime/aws-config/test-data/file-location-tests.json b/aws/rust-runtime/aws-config/test-data/file-location-tests.json new file mode 100644 index 0000000000..81d32cc604 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/file-location-tests.json @@ -0,0 +1,121 @@ +{ + "description": [ + "These are test descriptions that specify which files and profiles should be loaded based on the specified environment ", + "variables.", + "See 'file-location-tests.schema.json' for a description of this file's structure." + ], + + "tests": [ + { + "name": "User home is loaded from $HOME with highest priority on non-windows platforms.", + "environment": { + "HOME": "/home/user", + "USERPROFILE": "ignored", + "HOMEDRIVE": "ignored", + "HOMEPATH": "ignored" + }, + "platform": "linux", + "profile": "default", + "configLocation": "/home/user/.aws/config", + "credentialsLocation": "/home/user/.aws/credentials" + }, + + { + "name": "User home is loaded from $HOME with highest priority on windows platforms.", + "environment": { + "HOME": "C:\\users\\user", + "USERPROFILE": "ignored", + "HOMEDRIVE": "ignored", + "HOMEPATH": "ignored" + }, + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "User home is loaded from $USERPROFILE on windows platforms when $HOME is not set.", + "environment": { + "USERPROFILE": "C:\\users\\user", + "HOMEDRIVE": "ignored", + "HOMEPATH": "ignored" + }, + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "User home is loaded from $HOMEDRIVE$HOMEPATH on windows platforms when $HOME and $USERPROFILE are not set.", + "environment": { + "HOMEDRIVE": "C:", + "HOMEPATH": "\\users\\user" + }, + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "The default config location can be overridden by the user on non-windows platforms.", + "environment": { + "AWS_CONFIG_FILE": "/other/path/config", + "HOME": "/home/user" + }, + "platform": "linux", + "configLocation": "/other/path/config", + "credentialsLocation": "/home/user/.aws/credentials" + }, + + { + "name": "The default credentials location can be overridden by the user on non-windows platforms.", + "environment": { + "AWS_SHARED_CREDENTIALS_FILE": "/other/path/credentials", + "HOME": "/home/user" + }, + "platform": "linux", + "profile": "default", + "configLocation": "/home/user/.aws/config", + "credentialsLocation": "/other/path/credentials" + }, + + { + "name": "The default credentials location can be overridden by the user on windows platforms.", + "environment": { + "AWS_CONFIG_FILE": "C:\\other\\path\\config", + "HOME": "C:\\users\\user" + }, + "platform": "windows", + "profile": "default", + "configLocation": "C:\\other\\path\\config", + "credentialsLocation": "C:\\users\\user\\.aws\\credentials" + }, + + { + "name": "The default credentials location can be overridden by the user on windows platforms.", + "environment": { + "AWS_SHARED_CREDENTIALS_FILE": "C:\\other\\path\\credentials", + "HOME": "C:\\users\\user" + }, + "platform": "windows", + "profile": "default", + "configLocation": "C:\\users\\user\\.aws\\config", + "credentialsLocation": "C:\\other\\path\\credentials" + }, + + { + "name": "The default profile can be overridden via environment variable.", + "environment": { + "AWS_PROFILE": "other", + "HOME": "/home/user" + }, + "platform": "linux", + "profile": "other", + "configLocation": "/home/user/.aws/config", + "credentialsLocation": "/home/user/.aws/credentials" + } + ] +} diff --git a/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json b/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json new file mode 100644 index 0000000000..cd0d980a32 --- /dev/null +++ b/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json @@ -0,0 +1,713 @@ +{ + "description": [ + "These are test descriptions that describe how to convert a raw configuration and credentials file into an ", + "in-memory representation of the profile file.", + "See 'parser-tests.schema.json' for a description of this file's structure." + ], + "tests": [ + { + "name": "Empty files have no profiles.", + "input": { + "configFile": "" + }, + "output": { + "profiles": {} + } + }, + { + "name": "Empty profiles have no properties.", + "input": { + "configFile": "[profile foo]" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + { + "name": "Profile definitions must end with brackets.", + "input": { + "configFile": "[profile foo" + }, + "output": { + "errorContaining": "Profile definition must end with ']'" + } + }, + { + "name": "Profile names should be trimmed.", + "input": { + "configFile": "[profile \tfoo \t]" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + { + "name": "Tabs can separate profile names from profile prefix.", + "input": { + "configFile": "[profile\tfoo]" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + { + "name": "Properties must be defined in a profile.", + "input": { + "configFile": "name = value" + }, + "output": { + "errorContaining": "Expected a profile definition" + } + }, + { + "name": "Profiles can contain properties.", + "input": { + "configFile": "[profile foo]\nname = value" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "Windows style line endings are supported.", + "input": { + "configFile": "[profile foo]\r\nname = value" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "Equals signs are supported in property values.", + "input": { + "configFile": "[profile foo]\nname = val=ue" + }, + "output": { + "profiles": { + "foo": { + "name": "val=ue" + } + } + } + }, + { + "name": "Unicode characters are supported in property values.", + "input": { + "configFile": "[profile foo]\nname = 😂" + }, + "output": { + "profiles": { + "foo": { + "name": "😂" + } + } + } + }, + { + "name": "Profiles can contain multiple properties.", + "input": { + "configFile": "[profile foo]\nname = value\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + { + "name": "Profiles can contain multiple properties.", + "input": { + "configFile": "[profile foo]\nname = value\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + { + "name": "Property keys and values are trimmed.", + "input": { + "configFile": "[profile foo]\nname \t= \tvalue \t" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "Property values can be empty.", + "input": { + "configFile": "[profile foo]\nname =" + }, + "output": { + "profiles": { + "foo": { + "name": "" + } + } + } + }, + { + "name": "Property key cannot be empty.", + "input": { + "configFile": "[profile foo]\n= value" + }, + "output": { + "errorContaining": "Property did not have a name" + } + }, + { + "name": "Property definitions must contain an equals sign.", + "input": { + "configFile": "[profile foo]\nkey : value" + }, + "output": { + "errorContaining": "Expected an '=' sign defining a property" + } + }, + { + "name": "Multiple profiles can be empty.", + "input": { + "configFile": "[profile foo]\n[profile bar]" + }, + "output": { + "profiles": { + "foo": {}, + "bar": {} + } + } + }, + { + "name": "Multiple profiles can have properties.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile bar]\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + }, + "bar": { + "name2": "value2" + } + } + } + }, + { + "name": "Blank lines are ignored.", + "input": { + "configFile": "\t \n[profile foo]\n\t\n \nname = value\n\t \n[profile bar]\n \t" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + }, + "bar": {} + } + } + }, + { + "name": "Pound sign comments are ignored.", + "input": { + "configFile": "# Comment\n[profile foo] # Comment\nname = value # Comment with # sign" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "Semicolon sign comments are ignored.", + "input": { + "configFile": "; Comment\n[profile foo] ; Comment\nname = value ; Comment with ; sign" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "All comment types can be used together.", + "input": { + "configFile": "# Comment\n[profile foo] ; Comment\nname = value # Comment with ; sign" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "Comments can be empty.", + "input": { + "configFile": ";\n[profile foo];\nname = value ;\n" + }, + "output": { + "profiles": { + "foo": { + "name": "value" + } + } + } + }, + { + "name": "Comments can be adjacent to profile names.", + "input": { + "configFile": "[profile foo]; Adjacent semicolons\n[profile bar]# Adjacent pound signs" + }, + "output": { + "profiles": { + "foo": {}, + "bar": {} + } + } + }, + { + "name": "Comments adjacent to values are included in the value.", + "input": { + "configFile": "[profile foo]\nname = value; Adjacent semicolons\nname2 = value# Adjacent pound signs" + }, + "output": { + "profiles": { + "foo": { + "name": "value; Adjacent semicolons", + "name2": "value# Adjacent pound signs" + } + } + } + }, + { + "name": "Property values can be continued on the next line.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued" + } + } + } + }, + { + "name": "Property values can be continued with multiple lines.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued\n -and-continued" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued\n-and-continued" + } + } + } + }, + { + "name": "Continuations are trimmed.", + "input": { + "configFile": "[profile foo]\nname = value\n \t -continued \t " + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued" + } + } + } + }, + { + "name": "Continuation values include pound comments.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued # Comment" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued # Comment" + } + } + } + }, + { + "name": "Continuation values include semicolon comments.", + "input": { + "configFile": "[profile foo]\nname = value\n -continued ; Comment" + }, + "output": { + "profiles": { + "foo": { + "name": "value\n-continued ; Comment" + } + } + } + }, + { + "name": "Continuations cannot be used outside of a profile.", + "input": { + "configFile": " -continued" + }, + "output": { + "errorContaining": "Expected a profile definition" + } + }, + { + "name": "Continuations cannot be used outside of a property.", + "input": { + "configFile": "[profile foo]\n -continued" + }, + "output": { + "errorContaining": "Expected a property definition" + } + }, + { + "name": "Continuations reset with profile definitions.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\n -continued" + }, + "output": { + "errorContaining": "Expected a property definition" + } + }, + { + "name": "Duplicate profiles in the same file merge properties.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + { + "name": "Duplicate properties in a profile use the last one defined.", + "input": { + "configFile": "[profile foo]\nname = value\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + }, + { + "name": "Duplicate properties in duplicate profiles use the last one defined.", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + }, + { + "name": "Default profile with profile prefix overrides default profile without prefix when profile prefix is first.", + "input": { + "configFile": "[profile default]\nname = value\n[default]\nname2 = value2" + }, + "output": { + "profiles": { + "default": { + "name": "value" + } + } + } + }, + { + "name": "Default profile with profile prefix overrides default profile without prefix when profile prefix is last.", + "input": { + "configFile": "[default]\nname2 = value2\n[profile default]\nname = value" + }, + "output": { + "profiles": { + "default": { + "name": "value" + } + } + } + }, + { + "name": "Invalid profile names are ignored.", + "input": { + "configFile": "[profile in valid]\nname = value", + "credentialsFile": "[in valid 2]\nname2 = value2" + }, + "output": { + "profiles": {} + } + }, + { + "name": "Invalid property names are ignored.", + "input": { + "configFile": "[profile foo]\nin valid = value" + }, + "output": { + "profiles": { + "foo": {} + } + } + }, + { + "name": "All valid profile name characters are supported.", + "input": { + "configFile": "[profile ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_]" + }, + "output": { + "profiles": { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_": {} + } + } + }, + { + "name": "All valid property name characters are supported.", + "input": { + "configFile": "[profile foo]\nABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ = value" + }, + "output": { + "profiles": { + "foo": { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_": "value" + } + } + } + }, + { + "name": "Properties can have sub-properties.", + "input": { + "configFile": "[profile foo]\ns3 =\n name = value" + }, + "output": { + "profiles": { + "foo": { + "s3": "\nname = value" + } + } + } + }, + { + "name": "Invalid sub-property definitions cause an error.", + "input": { + "configFile": "[profile foo]\ns3 =\n invalid" + }, + "output": { + "errorContaining": "Expected an '=' sign defining a sub-property" + } + }, + { + "name": "Sub-property definitions can have an empty value.", + "input": { + "configFile": "[profile foo]\ns3 =\n name =" + }, + "output": { + "profiles": { + "foo": { + "s3": "\nname =" + } + } + } + }, + { + "name": "Sub-property definitions cannot have an empty name.", + "input": { + "configFile": "[profile foo]\ns3 =\n = value" + }, + "output": { + "errorContaining": "Sub-property did not have a name" + } + }, + { + "name": "Sub-property definitions can have an invalid name.", + "input": { + "configFile": "[profile foo]\ns3 =\n in valid = value" + }, + "output": { + "profiles": { + "foo": { + "s3": "\nin valid = value" + } + } + } + }, + { + "name": "Sub-properties can have blank lines that are ignored", + "input": { + "configFile": "[profile foo]\ns3 =\n name = value\n\t \n name2 = value2" + }, + "output": { + "profiles": { + "foo": { + "s3": "\nname = value\nname2 = value2" + } + } + } + }, + { + "name": "Profiles duplicated in multiple files are merged.", + "input": { + "configFile": "[profile foo]\nname = value", + "credentialsFile": "[foo]\nname2 = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value", + "name2": "value2" + } + } + } + }, + { + "name": "Default profiles with mixed prefixes in the config file ignore the one without prefix when merging.", + "input": { + "configFile": "[profile default]\nname = value\n[default]\nname2 = value2\n[profile default]\nname3 = value3" + }, + "output": { + "profiles": { + "default": { + "name": "value", + "name3": "value3" + } + } + } + }, + { + "name": "Default profiles with mixed prefixes merge with credentials", + "input": { + "configFile": "[profile default]\nname = value\n[default]\nname2 = value2\n[profile default]\nname3 = value3", + "credentialsFile": "[default]\nsecret=foo" + }, + "output": { + "profiles": { + "default": { + "name": "value", + "name3": "value3", + "secret": "foo" + } + } + } + }, + { + "name": "Duplicate properties between files uses credentials property.", + "input": { + "configFile": "[profile foo]\nname = value", + "credentialsFile": "[foo]\nname = value2" + }, + "output": { + "profiles": { + "foo": { + "name": "value2" + } + } + } + }, + { + "name": "Config profiles without prefix are ignored.", + "input": { + "configFile": "[foo]\nname = value" + }, + "output": { + "profiles": {} + } + }, + { + "name": "Credentials profiles with prefix are ignored.", + "input": { + "credentialsFile": "[profile foo]\nname = value" + }, + "output": { + "profiles": {} + } + }, + + { + "name": "Comment characters adjacent to profile decls", + "input": { + "configFile": "[profile foo]; semicolon\n[profile bar]# pound" + }, + "output": { + "profiles": { + "foo": {}, + "bar": {} + } + } + }, + { + "name": "Invalid continuation", + "input": { + "configFile": "[profile foo]\nname = value\n[profile foo]\n -continued" + }, + "output": { + "errorContaining": "Expected a property definition, found continuation" + } + }, + { + "name": "sneaky profile name", + "input": { + "configFile": "[profilefoo]\nname = value\n[profile bar]" + }, + "output": { + "profiles": { "bar": {} } + } + }, + + { + "name": "profile name with extra whitespace", + "input": { + "configFile": "[ profile foo ]\nname = value\n[profile bar]" + }, + "output": { + "profiles": { "bar": {}, "foo": {"name": "value"} } + } + }, + { + "name": "profile name with extra whitespace in credentials", + "input": { + "credentialsFile": "[ foo ]\nname = value\n[profile bar]" + }, + "output": { + "profiles": { "foo": {"name": "value"} } + } + } + ] +} diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/env.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/fs/home/.aws/credentials similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/fs/home/.aws/credentials rename to aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/fs/home/.aws/credentials diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/e2e_assume_role/test-case.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/e2e_assume_role/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/empty_config/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/empty_config/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/empty_config/env.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/empty_config/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/empty_config/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/empty_config/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/empty_config/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/empty_config/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/empty_config/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/empty_config/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/empty_config/test-case.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/empty_config/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/env.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/invalid_config/test-case.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/invalid_config/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/region_override/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/env.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/region_override/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/profile-provider/region_override/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/profile-provider/region_override/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/profile-provider/region_override/fs/home/.aws/credentials similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/fs/home/.aws/credentials rename to aws/rust-runtime/aws-config/test-data/profile-provider/region_override/fs/home/.aws/credentials diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/region_override/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/region_override/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/region_override/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/region_override/test-case.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/region_override/test-case.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/env.json b/aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/env.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/env.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/env.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/fs/home/.aws/config b/aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/fs/home/.aws/config similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/fs/home/.aws/config rename to aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/fs/home/.aws/config diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/fs/home/.aws/credentials b/aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/fs/home/.aws/credentials similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/fs/home/.aws/credentials rename to aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/fs/home/.aws/credentials diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/http-traffic.json b/aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/http-traffic.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/http-traffic.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/http-traffic.json diff --git a/aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/test-case.json b/aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/test-case.json similarity index 100% rename from aws/rust-runtime/aws-auth-providers/test-data/profile-provider/retry_on_error/test-case.json rename to aws/rust-runtime/aws-config/test-data/profile-provider/retry_on_error/test-case.json From 36320b3a887a917ebae7c1e99b7339add52d22fe Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 14:32:01 -0400 Subject: [PATCH 06/18] Complete refactoring of aws_auth_providers --- .../aws-auth-providers/Cargo.toml | 28 -- .../aws-auth-providers/src/chain.rs | 75 ---- .../src/default_provider_chain.rs | 234 ---------- .../aws-auth-providers/src/lib.rs | 60 --- .../aws-auth-providers/src/profile.rs | 419 ------------------ .../aws-auth-providers/src/profile/exec.rs | 206 --------- .../aws-auth-providers/src/profile/repr.rs | 412 ----------------- .../aws-auth-providers/src/sts_util.rs | 49 -- .../aws-auth-providers/src/test_case.rs | 175 -------- .../src/web_identity_token.rs | 304 ------------- .../aws-config/src/default_provider.rs | 238 +++++++++- .../aws-config/src/environment/mod.rs | 2 + aws/rust-runtime/aws-config/src/lib.rs | 4 +- .../aws-config/src/meta/credentials/chain.rs | 8 +- .../src/meta/credentials/lazy_caching.rs | 6 +- .../aws-config/src/meta/credentials/mod.rs | 2 +- .../aws-config/src/profile/credentials.rs | 54 +++ .../src/profile/credentials/exec.rs | 2 +- .../aws-config/src/profile/mod.rs | 2 +- 19 files changed, 303 insertions(+), 1977 deletions(-) delete mode 100644 aws/rust-runtime/aws-auth-providers/Cargo.toml delete mode 100644 aws/rust-runtime/aws-auth-providers/src/chain.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/lib.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/profile.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/profile/exec.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/profile/repr.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/sts_util.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/test_case.rs delete mode 100644 aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs diff --git a/aws/rust-runtime/aws-auth-providers/Cargo.toml b/aws/rust-runtime/aws-auth-providers/Cargo.toml deleted file mode 100644 index 1ee231521d..0000000000 --- a/aws/rust-runtime/aws-auth-providers/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "aws-auth-providers" -version = "0.1.0" -authors = ["AWS Rust SDK Team ", "Russell Cohen "] -edition = "2018" - -[features] -rustls = ["smithy-client/rustls"] -native-tls = ["smithy-client/native-tls"] -rt-tokio = ["smithy-async/rt-tokio"] -default = ["rustls", "rt-tokio"] - -[dependencies] -aws-auth = { path = "../../sdk/build/aws-sdk/aws-auth" } -aws-config = { path = "../../sdk/build/aws-sdk/aws-config"} -aws-types = { path = "../../sdk/build/aws-sdk/aws-types" } -aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sts"} -aws-hyper = { path = "../../sdk/build/aws-sdk/aws-hyper"} -smithy-async = { path = "../../sdk/build/aws-sdk/smithy-async" } -tracing = "0.1" -smithy-client = { path = "../../sdk/build/aws-sdk/smithy-client" } - -[dev-dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -smithy-client = { path = "../../sdk/build/aws-sdk/smithy-client", features = ["test-util", "hyper-rustls"]} -tokio = { version = "1", features = ["full"]} -tracing-test = "0.1.0" diff --git a/aws/rust-runtime/aws-auth-providers/src/chain.rs b/aws/rust-runtime/aws-auth-providers/src/chain.rs deleted file mode 100644 index f55988ccd6..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/chain.rs +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use std::borrow::Cow; - -use aws_auth::provider::{AsyncProvideCredentials, BoxFuture, CredentialsError, CredentialsResult}; -use tracing::Instrument; - -/// Credentials provider that checks a series of inner providers -/// -/// Each provider will be checked in turn. The first provider that returns a successful credential -/// will be used. -/// -/// ## Example -/// ```rust -/// use aws_auth_providers::chain::ChainProvider; -/// use aws_auth::provider::env::EnvironmentVariableCredentialsProvider; -/// use aws_types::Credentials; -/// let provider = ChainProvider::first_try("Environment", EnvironmentVariableCredentialsProvider::new()) -/// .or_else("Static", Credentials::from_keys("someacceskeyid", "somesecret", None)); -/// ``` -pub struct ChainProvider { - providers: Vec<(Cow<'static, str>, Box)>, -} - -impl ChainProvider { - pub fn first_try( - name: impl Into>, - provider: impl AsyncProvideCredentials + 'static, - ) -> Self { - ChainProvider { - providers: vec![(name.into(), Box::new(provider))], - } - } - - pub fn or_else( - mut self, - name: impl Into>, - provider: impl AsyncProvideCredentials + 'static, - ) -> Self { - self.providers.push((name.into(), Box::new(provider))); - self - } - - async fn credentials(&self) -> CredentialsResult { - for (name, provider) in &self.providers { - let span = tracing::info_span!("load_credentials", provider = %name); - match provider.provide_credentials().instrument(span).await { - Ok(credentials) => { - tracing::info!(provider = %name, "loaded credentials"); - return Ok(credentials); - } - Err(CredentialsError::CredentialsNotLoaded) => { - tracing::info!(provider = %name, "provider in chain did not provide credentials"); - } - Err(e) => { - tracing::warn!(provider = %name, error = %e, "provider failed to provide credentials"); - return Err(e); - } - } - } - return Err(CredentialsError::CredentialsNotLoaded); - } -} - -impl AsyncProvideCredentials for ChainProvider { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> - where - Self: 'a, - { - Box::pin(self.credentials()) - } -} diff --git a/aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs b/aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs deleted file mode 100644 index 2ac36568f8..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use std::borrow::Cow; - -use aws_auth::provider::env::EnvironmentVariableCredentialsProvider; -use aws_auth::provider::lazy_caching::LazyCachingCredentialsProvider; -use aws_auth::provider::BoxFuture; -use aws_auth::provider::{AsyncProvideCredentials, CredentialsResult}; -use aws_hyper::DynConnector; -use aws_types::os_shim_internal::{Env, Fs}; -use aws_types::region::Region; -use smithy_async::rt::sleep::AsyncSleep; - -/// Default AWS Credential Provider Chain -/// -/// Resolution order: -/// 1. Environment variables: [`EnvironmentVariableCredentialsProvider`](aws_auth::provider::env::EnvironmentVariableCredentialsProvider) -/// 2. Shared config (`~/.aws/config`, `~/.aws/credentials`): [`SharedConfigCredentialsProvider`](crate::profile::ProfileFileCredentialProvider) -/// -/// The outer provider is wrapped in a refreshing cache. -/// -/// More providers are a work in progress. -/// -/// ## Example: -/// Create a default chain with a custom region: -/// ```rust -/// use aws_types::region::Region; -/// let credentials_provider = aws_auth_providers::DefaultProviderChain::builder() -/// .region(Region::new("us-west-1")) -/// .build(); -/// ``` -/// -/// Create a default chain with no overrides: -/// ```rust -/// let credentials_provider = aws_auth_providers::default_provider(); -/// ``` -pub struct DefaultProviderChain(LazyCachingCredentialsProvider); - -impl DefaultProviderChain { - pub fn builder() -> Builder { - Builder::default() - } -} - -impl AsyncProvideCredentials for DefaultProviderChain { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> - where - Self: 'a, - { - self.0.provide_credentials() - } -} - -/// Builder for [`DefaultProviderChain`](DefaultProviderChain) -#[derive(Default)] -pub struct Builder { - profile_file_builder: crate::profile::Builder, - web_identity_builder: crate::web_identity_token::Builder, - credential_cache: aws_auth::provider::lazy_caching::builder::Builder, - env: Option, -} - -impl Builder { - /// Set the region used when making requests to AWS services (eg. STS) as part of the provider chain - /// - /// When unset, the default region resolver chain will be used. - pub fn region(mut self, region: Region) -> Self { - self.set_region(Some(region)); - self - } - - pub fn set_region(&mut self, region: Option) -> &mut Self { - self.profile_file_builder.set_region(region.clone()); - self.web_identity_builder.set_region(region); - self - } - - /// Override the HTTPS connector used for this provider - /// - /// If a connector other than Hyper is used or if the Tokio/Hyper features have been disabled - /// this method MUST be used to specify a custom connector. - pub fn connector(mut self, connector: DynConnector) -> Self { - self.profile_file_builder - .set_connector(Some(connector.clone())); - self.web_identity_builder.set_connector(Some(connector)); - self - } - - /// Override the sleep implementation used for this provider - /// - /// By default, Tokio will be used to support async sleep during credentials for timeouts - /// and reloading credentials. If the tokio default feature has been disabled, a custom - /// sleep implementation must be provided. - pub fn sleep(mut self, sleep: impl AsyncSleep + 'static) -> Self { - self.credential_cache = self.credential_cache.sleep(sleep); - self - } - - /// Add an additional credential source for the ProfileProvider - /// - /// Assume role profiles may specify named credential sources: - /// ```ini - /// [default] - /// role_arn = arn:aws:iam::123456789:role/RoleA - /// credential_source = MyCustomProvider - /// ``` - /// - /// Typically, these are built-in providers like `Environment`, however, custom sources may - /// also be used. Using custom sources must be registered: - /// ```rust - /// use aws_auth::provider::{ProvideCredentials, CredentialsError}; - /// use aws_types::Credentials; - /// use aws_auth_providers::DefaultProviderChain; - /// struct MyCustomProvider; - /// // there is a blanket implementation for `AsyncProvideCredentials` on ProvideCredentials - /// impl ProvideCredentials for MyCustomProvider { - /// fn provide_credentials(&self) -> Result { - /// todo!() - /// } - /// } - /// // assume role can now use `MyCustomProvider` when maed - /// let provider_chain = DefaultProviderChain::builder() - /// .with_custom_credential_source("MyCustomProvider", MyCustomProvider) - /// .build(); - /// ``` - pub fn with_custom_credential_source( - mut self, - name: impl Into>, - provider: impl AsyncProvideCredentials + 'static, - ) -> Self { - self.profile_file_builder = self - .profile_file_builder - .with_custom_provider(name, provider); - self - } - - #[doc(hidden)] - /// Override the filesystem used for this provider - /// - /// This method exists primarily for testing credential providers - pub fn fs(mut self, fs: Fs) -> Self { - self.profile_file_builder.set_fs(fs.clone()); - self.web_identity_builder.set_fs(fs); - self - } - - #[doc(hidden)] - /// Override the environment used for this provider - /// - /// This method exists primarily for testing credential providers - pub fn env(mut self, env: Env) -> Self { - self.env = Some(env.clone()); - self.profile_file_builder.set_env(env.clone()); - self.web_identity_builder.set_env(env); - self - } - - pub fn build(self) -> DefaultProviderChain { - let profile_provider = self.profile_file_builder.build(); - let env_provider = - EnvironmentVariableCredentialsProvider::new_with_env(self.env.unwrap_or_default()); - let web_identity_token_provider = self.web_identity_builder.build(); - let provider_chain = crate::chain::ChainProvider::first_try("Environment", env_provider) - .or_else("Profile", profile_provider) - .or_else("WebIdentityToken", web_identity_token_provider); - let cached_provider = self.credential_cache.load(provider_chain); - DefaultProviderChain(cached_provider.build()) - } -} - -#[cfg(test)] -mod test { - - macro_rules! make_test { - ($name: ident) => { - #[traced_test] - #[tokio::test] - async fn $name() { - crate::test_case::TestEnvironment::from_dir(concat!( - "./test-data/default-provider-chain/", - stringify!($name) - )) - .unwrap() - .execute(|fs, env, conn| { - crate::default_provider_chain::Builder::default() - .env(env) - .fs(fs) - .region(Region::from_static("us-east-1")) - .connector(conn) - .build() - }) - .await - } - }; - } - - use aws_sdk_sts::Region; - - use tracing_test::traced_test; - - make_test!(prefer_environment); - make_test!(profile_static_keys); - make_test!(web_identity_token_env); - make_test!(web_identity_source_profile_no_env); - make_test!(web_identity_token_invalid_jwt); - make_test!(web_identity_token_source_profile); - make_test!(web_identity_token_profile); - make_test!(profile_overrides_web_identity); - - /// Helper that uses `execute_and_update` instead of execute - /// - /// If you run this, it will add another HTTP traffic log which re-records the request - /// data - #[tokio::test] - #[ignore] - async fn update_test() { - crate::test_case::TestEnvironment::from_dir(concat!( - "./test-data/default-provider-chain/web_identity_token_source_profile", - )) - .unwrap() - .execute_and_update(|fs, env, conn| { - crate::default_provider_chain::Builder::default() - .env(env) - .fs(fs) - .region(Region::from_static("us-east-1")) - .connector(conn) - .build() - }) - .await - } -} diff --git a/aws/rust-runtime/aws-auth-providers/src/lib.rs b/aws/rust-runtime/aws-auth-providers/src/lib.rs deleted file mode 100644 index 567684c3d8..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/lib.rs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ -use aws_auth::provider::AsyncProvideCredentials; -use aws_hyper::DynConnector; - -pub use default_provider_chain::DefaultProviderChain; - -pub mod default_provider_chain; -pub mod profile; - -/// Credentials Provider that evaluates a series of providers -pub mod chain; -mod sts_util; -mod test_case; -pub mod web_identity_token; - -// create a default connector given the currently enabled cargo features. -// rustls | native tls | result -// ----------------------------- -// yes | yes | rustls -// yes | no | rustls -// no | yes | native_tls -// no | no | no default - -fn must_have_connector() -> DynConnector { - default_connector().expect("A connector was not available. Either set a custom connector or enable the `rustls` and `native-tls` crate features.") -} - -#[cfg(feature = "rustls")] -fn default_connector() -> Option { - Some(DynConnector::new(smithy_client::conns::https())) -} - -#[cfg(all(not(feature = "rustls"), feature = "native-tls"))] -fn default_connector() -> Option { - Some(DynConnector::new(smithy_client::conns::native_tls())) -} - -#[cfg(not(any(feature = "rustls", feature = "native-tls")))] -fn default_connector() -> Option { - None -} - -// because this doesn't provide any configuration, a runtime and connector must be provided. -#[cfg(all(any(feature = "native-tls", feature = "rustls"), feature = "rt-tokio"))] -/// Default AWS provider chain -/// -/// This provider chain will use defaults for all settings. The region will be resolved with the default -/// provider chain. To construct a custom provider, use [`default_provider_chain::Builder`](default_provider_chain::Builder). -pub async fn default_provider() -> impl AsyncProvideCredentials { - use aws_config::meta::region::ProvideRegion; - let resolved_region = aws_config::default_provider::region::default_provider() - .region() - .await; - let mut builder = default_provider_chain::Builder::default(); - builder.set_region(resolved_region); - builder.build() -} diff --git a/aws/rust-runtime/aws-auth-providers/src/profile.rs b/aws/rust-runtime/aws-auth-providers/src/profile.rs deleted file mode 100644 index e37c9d605f..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/profile.rs +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -//! Profile File Based Providers -//! -//! Profile file based providers combine two pieces: -//! -//! 1. Parsing and resolution of the assume role chain -//! 2. A user-modifiable hashmap of provider name to provider. -//! -//! Profile file based providers first determine the chain of providers that will be used to load -//! credentials. After determining and validating this chain, a `Vec` of providers will be created. -//! -//! Each subsequent provider will provide boostrap providers to the next provider in order to load -//! the final credentials. -//! -//! This module contains two sub modules: -//! - `repr` which contains an abstract representation of a provider chain and the logic to -//! build it from `~/.aws/credentials` and `~/.aws/config`. -//! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials -//! through a series of providers. -use std::borrow::Cow; -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::sync::Arc; - -use aws_auth::provider::env::EnvironmentVariableCredentialsProvider; -use aws_auth::provider::{AsyncProvideCredentials, BoxFuture, CredentialsError, CredentialsResult}; -use aws_config::meta::region::ProvideRegion; -use aws_hyper::DynConnector; -use aws_sdk_sts::Region; -use aws_types::os_shim_internal::{Env, Fs}; -use aws_types::profile::ProfileParseError; -use tracing::Instrument; - -use crate::must_have_connector; -use crate::profile::exec::named::NamedProviderFactory; -use crate::profile::exec::{ClientConfiguration, ProviderChain}; - -mod exec; -mod repr; - -impl AsyncProvideCredentials for ProfileFileCredentialProvider { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> - where - Self: 'a, - { - Box::pin(self.load_credentials().instrument(tracing::info_span!( - "load_credentials", - provider = "Profile" - ))) - } -} - -/// AWS Profile based credentials provider -/// -/// This credentials provider will load credentials from `~/.aws/config` and `~/.aws/credentials`. -/// The locations of these files are configurable, see [`profile::load`](aws_types::profile::load). -/// -/// Generally, this will be constructed via the default provider chain, however, it can be manually -/// constructed with the builder: -/// ```rust,no_run -/// use aws_auth_providers::profile::ProfileFileCredentialProvider; -/// let provider = ProfileFileCredentialProvider::builder().build(); -/// ``` -/// -/// **Note:** Profile providers to not implement any caching. They will reload and reparse the profile -/// from the file system when called. See [lazy_caching](aws_auth::provider::lazy_caching) for -/// more information about caching. -/// -/// This provider supports several different credentials formats: -/// ### Credentials defined explicitly within the file -/// ```ini -/// [default] -/// aws_access_key_id = 123 -/// aws_secret_access_key = 456 -/// ``` -/// -/// ### Assume Role Credentials loaded from a credential source -/// ```ini -/// [default] -/// role_arn = arn:aws:iam::123456789:role/RoleA -/// credential_source = Environment -/// ``` -/// -/// NOTE: Currently only the `Environment` credential source is supported although it is possible to -/// provide custom sources: -/// ```rust -/// use aws_auth_providers::profile::ProfileFileCredentialProvider; -/// use aws_auth::provider::{CredentialsResult, AsyncProvideCredentials, BoxFuture}; -/// use std::sync::Arc; -/// struct MyCustomProvider; -/// impl MyCustomProvider { -/// async fn load_credentials(&self) -> CredentialsResult { -/// todo!() -/// } -/// } -/// -/// impl AsyncProvideCredentials for MyCustomProvider { -/// fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> where Self: 'a { -/// Box::pin(self.load_credentials()) -/// } -/// } -/// let provider = ProfileFileCredentialProvider::builder() -/// .with_custom_provider("Custom", MyCustomProvider) -/// .build(); -/// ``` -/// -/// ### Assume role credentials from a source profile -/// ```ini -/// [default] -/// role_arn = arn:aws:iam::123456789:role/RoleA -/// source_profile = base -/// -/// [profile base] -/// aws_access_key_id = 123 -/// aws_secret_access_key = 456 -/// ``` -/// -/// Other more complex configurations are possible, consult `test-data/assume-role-tests.json`. -pub struct ProfileFileCredentialProvider { - factory: NamedProviderFactory, - client_config: ClientConfiguration, - fs: Fs, - env: Env, - region: Option, - connector: DynConnector, -} - -impl ProfileFileCredentialProvider { - pub fn builder() -> Builder { - Builder::default() - } - - async fn load_credentials(&self) -> CredentialsResult { - // 1. grab a read lock, use it to see if the base profile has already been loaded - // 2. If it's loaded, great, lets use it. - // If not, upgrade to a write lock and use that to load the profile file. - // 3. Finally, downgrade to ensure no one swapped in the intervening time, then use try_load() - // to pull the new state. - let profile = build_provider_chain( - &self.fs, - &self.env, - &self.region, - &self.connector, - &self.factory, - ) - .await; - let inner_provider = profile.map_err(|err| match err { - ProfileFileError::NoProfilesDefined => CredentialsError::CredentialsNotLoaded, - _ => CredentialsError::InvalidConfiguration( - format!("ProfileFile provider could not be built: {}", &err).into(), - ), - })?; - let mut creds = match inner_provider - .base() - .provide_credentials() - .instrument(tracing::info_span!("load_base_credentials")) - .await - { - Ok(creds) => { - tracing::info!(creds = ?creds, "loaded base credentials"); - creds - } - Err(e) => { - tracing::warn!(error = %e, "failed to load base credentials"); - return Err(CredentialsError::ProviderError(e.into())); - } - }; - for provider in inner_provider.chain().iter() { - let next_creds = provider - .credentials(creds, &self.client_config) - .instrument(tracing::info_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::ProviderError(e.into())); - } - } - } - Ok(creds) - } -} - -#[derive(Debug)] -#[non_exhaustive] -pub enum ProfileFileError { - CouldNotParseProfile(ProfileParseError), - NoProfilesDefined, - CredentialLoop { - profiles: Vec, - next: String, - }, - MissingCredentialSource { - profile: String, - message: Cow<'static, str>, - }, - InvalidCredentialSource { - profile: String, - message: Cow<'static, str>, - }, - MissingProfile { - profile: String, - message: Cow<'static, str>, - }, - UnknownProvider { - name: String, - }, -} - -impl Display for ProfileFileError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ProfileFileError::CouldNotParseProfile(err) => { - write!(f, "could not parse profile file: {}", err) - } - ProfileFileError::CredentialLoop { profiles, next } => write!( - f, - "profile formed an infinite loop. first we loaded {:?}, \ - then attempted to reload {}", - profiles, next - ), - ProfileFileError::MissingCredentialSource { profile, message } => { - write!(f, "missing credential source in `{}`: {}", profile, message) - } - ProfileFileError::InvalidCredentialSource { profile, message } => { - write!(f, "invalid credential source in `{}`: {}", profile, message) - } - ProfileFileError::MissingProfile { profile, message } => { - write!(f, "profile `{}` was not defined: {}", profile, message) - } - ProfileFileError::UnknownProvider { name } => write!( - f, - "profile referenced `{}` provider but that provider is not supported", - name - ), - ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"), - } - } -} - -impl Error for ProfileFileError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - ProfileFileError::CouldNotParseProfile(err) => Some(err), - _ => None, - } - } -} - -#[derive(Default)] -pub struct Builder { - fs: Fs, - env: Env, - region: Option, - connector: Option, - custom_providers: HashMap, Arc>, -} - -impl Builder { - pub fn fs(mut self, fs: Fs) -> Self { - self.fs = fs; - self - } - - pub fn set_fs(&mut self, fs: Fs) -> &mut Self { - self.fs = fs; - self - } - - pub fn env(mut self, env: Env) -> Self { - self.env = env; - self - } - - pub fn set_env(&mut self, env: Env) -> &mut Self { - self.env = env; - self - } - - pub fn connector(mut self, connector: DynConnector) -> Self { - self.connector = Some(connector); - self - } - - pub fn set_connector(&mut self, connector: Option) -> &mut Self { - self.connector = connector; - self - } - - pub fn region(mut self, region: Region) -> Self { - self.region = Some(region); - self - } - - pub fn set_region(&mut self, region: Option) -> &mut Self { - self.region = region; - self - } - - pub fn with_custom_provider( - mut self, - name: impl Into>, - provider: impl AsyncProvideCredentials + 'static, - ) -> Self { - self.custom_providers - .insert(name.into(), Arc::new(provider)); - self - } - - pub fn build(self) -> ProfileFileCredentialProvider { - let build_span = tracing::info_span!("build_profile_provider"); - let _enter = build_span.enter(); - let env = self.env.clone(); - let fs = self.fs; - let mut named_providers = self.custom_providers.clone(); - named_providers - .entry("Environment".into()) - .or_insert_with(|| { - Arc::new(EnvironmentVariableCredentialsProvider::new_with_env( - env.clone(), - )) - }); - // TODO: ECS, IMDS, and other named providers - let factory = exec::named::NamedProviderFactory::new(named_providers); - let connector = self.connector.clone().unwrap_or_else(must_have_connector); - let core_client = aws_hyper::Builder::<()>::new() - .map_connector(|_| connector.clone()) - .build(); - - ProfileFileCredentialProvider { - factory, - client_config: ClientConfiguration { - core_client, - region: self.region.clone(), - }, - fs, - env, - region: self.region.clone(), - connector, - } - } -} - -async fn build_provider_chain( - fs: &Fs, - env: &Env, - region: &dyn ProvideRegion, - connector: &DynConnector, - factory: &NamedProviderFactory, -) -> Result { - let profile_set = aws_types::profile::load(&fs, &env).await.map_err(|err| { - tracing::warn!(err = %err, "failed to parse profile"); - ProfileFileError::CouldNotParseProfile(err) - })?; - let repr = repr::resolve_chain(&profile_set)?; - tracing::info!(chain = ?repr, "constructed abstract provider from config file"); - exec::ProviderChain::from_repr(fs.clone(), connector, region.region().await, repr, &factory) -} - -#[cfg(test)] -mod test { - use aws_sdk_sts::Region; - use tracing_test::traced_test; - - use crate::profile::Builder; - use crate::test_case::TestEnvironment; - - macro_rules! make_test { - ($name: ident) => { - #[traced_test] - #[tokio::test] - async fn $name() { - TestEnvironment::from_dir(concat!( - "./test-data/profile-provider/", - stringify!($name) - )) - .unwrap() - .execute(|fs, env, conn| { - Builder::default() - .env(env) - .fs(fs) - .region(Region::from_static("us-east-1")) - .connector(conn) - .build() - }) - .await - } - }; - } - - make_test!(e2e_assume_role); - make_test!(empty_config); - make_test!(retry_on_error); - make_test!(invalid_config); - - #[tokio::test] - async fn region_override() { - TestEnvironment::from_dir("./test-data/profile-provider/region_override") - .unwrap() - .execute(|fs, env, conn| { - Builder::default() - .env(env) - .fs(fs) - .region(Region::from_static("us-east-2")) - .connector(conn) - .build() - }) - .await - } -} diff --git a/aws/rust-runtime/aws-auth-providers/src/profile/exec.rs b/aws/rust-runtime/aws-auth-providers/src/profile/exec.rs deleted file mode 100644 index f8d7a55184..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/profile/exec.rs +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use std::sync::Arc; - -use aws_auth::provider::{AsyncProvideCredentials, CredentialsError, CredentialsResult}; -use aws_hyper::{DynConnector, StandardClient}; -use aws_sdk_sts::operation::AssumeRole; -use aws_sdk_sts::Config; -use aws_types::region::Region; -use aws_types::Credentials; - -use crate::profile::repr::BaseProvider; -use crate::profile::ProfileFileError; - -use super::repr; -use crate::sts_util; -use crate::sts_util::default_session_name; -use crate::web_identity_token::{WebIdentityTokenCredentialProvider, WebIdentityTokenRole}; -use aws_types::os_shim_internal::Fs; -use std::fmt::{Debug, Formatter}; - -#[derive(Debug)] -pub struct AssumeRoleProvider { - role_arn: String, - external_id: Option, - session_name: Option, -} - -pub struct ClientConfiguration { - pub(crate) core_client: StandardClient, - pub(crate) region: Option, -} - -impl AssumeRoleProvider { - pub async fn credentials( - &self, - input_credentials: Credentials, - client_config: &ClientConfiguration, - ) -> CredentialsResult { - let config = Config::builder() - .credentials_provider(input_credentials) - .region(client_config.region.clone()) - .build(); - let session_name = &self - .session_name - .as_ref() - .cloned() - .unwrap_or_else(|| sts_util::default_session_name("assume-role-from-profile")); - let operation = AssumeRole::builder() - .role_arn(&self.role_arn) - .set_external_id(self.external_id.clone()) - .role_session_name(session_name) - .build() - .expect("operation is valid") - .make_operation(&config) - .expect("valid operation"); - let assume_role_creds = client_config - .core_client - .call(operation) - .await - .map_err(|err| CredentialsError::ProviderError(err.into()))? - .credentials; - crate::sts_util::into_credentials(assume_role_creds, "AssumeRoleProvider") - } -} - -pub(crate) struct ProviderChain { - base: Arc, - chain: Vec, -} - -impl Debug for ProviderChain { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - // TODO: AsyncProvideCredentials should probably mandate debug - f.debug_struct("ProviderChain").finish() - } -} - -impl ProviderChain { - pub fn base(&self) -> &dyn AsyncProvideCredentials { - self.base.as_ref() - } - - pub fn chain(&self) -> &[AssumeRoleProvider] { - &self.chain.as_slice() - } -} - -impl ProviderChain { - pub fn from_repr( - fs: Fs, - connector: &DynConnector, - region: Option, - repr: repr::ProfileChain, - factory: &named::NamedProviderFactory, - ) -> Result { - let base = match repr.base() { - BaseProvider::NamedSource(name) => { - factory - .provider(name) - .ok_or(ProfileFileError::UnknownProvider { - name: name.to_string(), - })? - } - BaseProvider::AccessKey(key) => Arc::new(key.clone()), - BaseProvider::WebIdentityTokenRole { - role_arn, - web_identity_token_file, - session_name, - } => { - let provider = WebIdentityTokenCredentialProvider::builder() - .static_configuration(WebIdentityTokenRole { - 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(|| default_session_name("web-identity-token-profile")), - }) - .fs(fs) - .connector(connector.clone()) - .region(region) - .build(); - Arc::new(provider) - } - }; - tracing::info!(base = ?repr.base(), "first credentials will be loaded from {:?}", repr.base()); - let chain = repr - .chain() - .iter() - .map(|role_arn| { - tracing::info!(role_arn = ?role_arn, "which will be used to assume a role"); - AssumeRoleProvider { - role_arn: role_arn.role_arn.into(), - external_id: role_arn.external_id.map(|id| id.into()), - session_name: role_arn.session_name.map(|id| id.into()), - } - }) - .collect(); - Ok(ProviderChain { base, chain }) - } -} - -pub mod named { - use std::collections::HashMap; - use std::sync::Arc; - - use aws_auth::provider::AsyncProvideCredentials; - use std::borrow::Cow; - - pub struct NamedProviderFactory { - providers: HashMap, Arc>, - } - - impl NamedProviderFactory { - pub fn new( - providers: HashMap, Arc>, - ) -> Self { - Self { providers } - } - - pub fn provider(&self, name: &str) -> Option> { - self.providers.get(name).cloned() - } - } -} - -#[cfg(test)] -mod test { - use crate::profile::exec::named::NamedProviderFactory; - use crate::profile::exec::ProviderChain; - use crate::profile::repr::{BaseProvider, ProfileChain}; - use aws_hyper::DynConnector; - use aws_sdk_sts::Region; - use smithy_client::dvr; - use std::collections::HashMap; - - fn stub_connector() -> DynConnector { - DynConnector::new(dvr::ReplayingConnection::new(vec![])) - } - - #[test] - fn error_on_unknown_provider() { - let factory = NamedProviderFactory::new(HashMap::new()); - let chain = ProviderChain::from_repr( - Default::default(), - &stub_connector(), - Some(Region::new("us-east-1")), - ProfileChain { - base: BaseProvider::NamedSource("floozle"), - chain: vec![], - }, - &factory, - ); - let err = chain.expect_err("no source by that name"); - assert!( - format!("{}", err).contains( - "profile referenced `floozle` provider but that provider is not supported" - ), - "`{}` did not match expected error", - err - ); - } -} diff --git a/aws/rust-runtime/aws-auth-providers/src/profile/repr.rs b/aws/rust-runtime/aws-auth-providers/src/profile/repr.rs deleted file mode 100644 index 187edb165a..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/profile/repr.rs +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -//! Flattened Representation of an AssumeRole chain -//! -//! Assume Role credentials in profile files can chain together credentials from multiple -//! different providers with subsequent credentials being used to configure subsequent providers. -//! -//! This module can parse and resolve the profile chain into a flattened representation with -//! 1-credential-per row (as opposed to a direct profile file representation which can combine -//! multiple actions into the same profile). - -use crate::profile::ProfileFileError; -use aws_types::profile::{Profile, ProfileSet}; -use aws_types::Credentials; - -/// Chain of Profile Providers -/// -/// Within a profile file, a chain of providers is produced. Starting with a base provider, -/// subsequent providers use the credentials from previous providers to perform their task. -/// -/// ProfileChain is a direct representation of the Profile. It can contain named providers -/// that don't actually have implementations. -#[derive(Debug)] -pub struct ProfileChain<'a> { - pub(crate) base: BaseProvider<'a>, - pub(crate) chain: Vec>, -} - -impl<'a> ProfileChain<'a> { - pub fn base(&self) -> &BaseProvider<'a> { - &self.base - } - - pub fn chain(&self) -> &[RoleArn<'a>] { - &self.chain.as_slice() - } -} - -/// A base member of the profile chain -/// -/// Base providers do not require input credentials to provide their own credentials, -/// eg. IMDS, ECS, Environment variables -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum BaseProvider<'a> { - /// A profile that specifies a named credential source - /// Eg: `credential_source = Ec2InstanceMetadata` - /// - /// The following profile produces two separate `ProfileProvider` rows: - /// 1. `BaseProvider::NamedSource("Ec2InstanceMetadata")` - /// 2. `RoleArn { role_arn: "...", ... } - /// ```ini - /// [profile assume-role] - /// role_arn = arn:aws:iam::123456789:role/MyRole - /// credential_source = Ec2InstanceMetadata - /// ``` - NamedSource(&'a str), - - /// A profile with explicitly configured access keys - /// - /// Example - /// ```ini - /// [profile C] - /// aws_access_key_id = abc123 - /// aws_secret_access_key = def456 - /// ``` - AccessKey(Credentials), - - WebIdentityTokenRole { - role_arn: &'a str, - web_identity_token_file: &'a str, - session_name: Option<&'a str>, - }, // TODO: add SSO support - /* - /// An SSO Provider - Sso { - sso_account_id: &'a str, - sso_region: &'a str, - sso_role_name: &'a str, - sso_start_url: &'a str, - }, - */ -} - -/// A profile that specifies a role to assume -/// -/// A RoleArn can only be created from either a profile with `source_profile` -/// or one with `credential_source`. -#[derive(Debug)] -pub struct RoleArn<'a> { - /// Role to assume - pub role_arn: &'a str, - /// external_id parameter to pass to the assume role provider - pub external_id: Option<&'a str>, - - /// session name parameter to pass to the assume role provider - pub session_name: Option<&'a str>, -} - -/// Resolve a ProfileChain from a ProfileSet or return an error -pub fn resolve_chain(profile_set: &ProfileSet) -> Result { - if profile_set.is_empty() { - return Err(ProfileFileError::NoProfilesDefined); - } - let mut source_profile_name = profile_set.selected_profile(); - let mut visited_profiles = vec![]; - let mut chain = vec![]; - let base = loop { - let profile = profile_set.get_profile(source_profile_name).ok_or( - ProfileFileError::MissingProfile { - profile: source_profile_name.into(), - message: format!( - "could not find source profile {} referenced from {}", - source_profile_name, - visited_profiles.last().unwrap_or(&"the root profile") - ) - .into(), - }, - )?; - if visited_profiles.contains(&source_profile_name) { - return Err(ProfileFileError::CredentialLoop { - profiles: visited_profiles - .into_iter() - .map(|s| s.to_string()) - .collect(), - next: source_profile_name.to_string(), - }); - } - visited_profiles.push(&source_profile_name); - // After the first item in the chain, we will prioritize static credentials if they exist - if visited_profiles.len() > 1 { - let try_static = static_creds_from_profile(&profile); - if let Ok(static_credentials) = try_static { - break BaseProvider::AccessKey(static_credentials); - } - } - let next_profile = match chain_provider(&profile) { - // this provider wasn't a chain provider, reload it as a base provider - None => { - break base_provider(profile)?; - } - Some(result) => { - let (chain_profile, next) = result?; - chain.push(chain_profile); - next - } - }; - match next_profile { - NextProfile::SelfReference => { - // self referential profile, don't go through the loop because it will error - // on the infinite loop check. Instead, reload this profile as a base profile - // and exit. - break base_provider(profile)?; - } - NextProfile::Named(name) => source_profile_name = name, - } - }; - chain.reverse(); - Ok(ProfileChain { base, chain }) -} - -mod role { - pub const ROLE_ARN: &str = "role_arn"; - pub const EXTERNAL_ID: &str = "external_id"; - pub const SESSION_NAME: &str = "role_session_name"; - - pub const CREDENTIAL_SOURCE: &str = "credential_source"; - pub const SOURCE_PROFILE: &str = "source_profile"; -} - -mod web_identity_token { - pub const TOKEN_FILE: &str = "web_identity_token_file"; -} - -mod static_credentials { - pub const AWS_ACCESS_KEY_ID: &str = "aws_access_key_id"; - pub const AWS_SECRET_ACCESS_KEY: &str = "aws_secret_access_key"; - pub const AWS_SESSION_TOKEN: &str = "aws_session_token"; -} -const PROVIDER_NAME: &str = "ProfileFile"; - -fn base_provider(profile: &Profile) -> Result { - // the profile must define either a `CredentialsSource` or a concrete set of access keys - match profile.get(role::CREDENTIAL_SOURCE) { - Some(source) => Ok(BaseProvider::NamedSource(source)), - None => web_identity_token_from_profile(profile) - .unwrap_or_else(|| Ok(BaseProvider::AccessKey(static_creds_from_profile(profile)?))), - } -} - -enum NextProfile<'a> { - SelfReference, - Named(&'a str), -} - -fn chain_provider(profile: &Profile) -> Option> { - let role_provider = role_arn_from_profile(&profile)?; - let (source_profile, credential_source) = ( - profile.get(role::SOURCE_PROFILE), - profile.get(role::CREDENTIAL_SOURCE), - ); - let profile = match (source_profile, credential_source) { - (Some(_), Some(_)) => Err(ProfileFileError::InvalidCredentialSource { - profile: profile.name().to_string(), - message: "profile contained both source_profile and credential_source. \ - Only one or the other can be defined" - .into(), - }), - (None, None) => Err(ProfileFileError::InvalidCredentialSource { - profile: profile.name().to_string(), - message: - "profile must contain source_profile or credentials_source but neither were defined" - .into(), - }), - (Some(source_profile), None) if source_profile == profile.name() => { - Ok((role_provider, NextProfile::SelfReference)) - } - - (Some(source_profile), None) => Ok((role_provider, NextProfile::Named(source_profile))), - // we want to loop back into this profile and pick up the credential source - (None, Some(_credential_source)) => Ok((role_provider, NextProfile::SelfReference)), - }; - Some(profile) -} - -fn role_arn_from_profile(profile: &Profile) -> Option { - // Web Identity Tokens are root providers, not chained roles - if profile.get(web_identity_token::TOKEN_FILE).is_some() { - return None; - } - let role_arn = profile.get(role::ROLE_ARN)?; - let session_name = profile.get(role::SESSION_NAME); - let external_id = profile.get(role::EXTERNAL_ID); - Some(RoleArn { - role_arn, - external_id, - session_name, - }) -} - -fn web_identity_token_from_profile( - profile: &Profile, -) -> Option> { - let session_name = profile.get(role::SESSION_NAME); - match ( - profile.get(role::ROLE_ARN), - profile.get(web_identity_token::TOKEN_FILE), - ) { - (Some(role_arn), Some(token_file)) => Some(Ok(BaseProvider::WebIdentityTokenRole { - role_arn, - web_identity_token_file: token_file, - session_name, - })), - (None, None) => None, - (Some(_role_arn), None) => None, - (None, Some(_token_file)) => Some(Err(ProfileFileError::InvalidCredentialSource { - profile: profile.name().to_string(), - message: "`web_identity_token_file` was specified but `role_arn` was missing".into(), - })), - } -} - -/// Load static credentials from a profile -/// -/// Example: -/// ```ini -/// [profile B] -/// aws_access_key_id = abc123 -/// aws_secret_access_key = def456 -/// ``` -fn static_creds_from_profile(profile: &Profile) -> Result { - use static_credentials::*; - let access_key = profile.get(AWS_ACCESS_KEY_ID); - let secret_key = profile.get(AWS_SECRET_ACCESS_KEY); - let session_token = profile.get(AWS_SESSION_TOKEN); - if let (None, None, None) = (access_key, secret_key, session_token) { - return Err(ProfileFileError::MissingCredentialSource { - profile: profile.name().to_string(), - message: "expected `aws_access_key_id` and `aws_secret_access_key` to be defined" - .into(), - }); - } - let access_key = access_key.ok_or_else(|| ProfileFileError::InvalidCredentialSource { - profile: profile.name().to_string(), - message: "profile missing aws_access_key_id".into(), - })?; - let secret_key = secret_key.ok_or_else(|| ProfileFileError::InvalidCredentialSource { - profile: profile.name().to_string(), - message: "profile missing aws_secret_access_key".into(), - })?; - Ok(Credentials::new( - access_key, - secret_key, - session_token.map(|s| s.to_string()), - None, - PROVIDER_NAME, - )) -} - -#[cfg(test)] -mod tests { - use crate::profile::repr::{resolve_chain, BaseProvider, ProfileChain}; - use aws_types::profile::ProfileSet; - use serde::Deserialize; - use std::collections::HashMap; - use std::error::Error; - use std::fs; - - #[test] - fn run_test_cases() -> Result<(), Box> { - let test_cases: Vec = - serde_json::from_str(&fs::read_to_string("./test-data/assume-role-tests.json")?)?; - for test_case in test_cases { - print!("checking: {}...", test_case.docs); - check(test_case); - println!("ok") - } - Ok(()) - } - - fn check(test_case: TestCase) { - let source = ProfileSet::new(test_case.input.profile, test_case.input.selected_profile); - let actual = resolve_chain(&source); - let expected = test_case.output; - match (expected, actual) { - (TestOutput::Error(s), Err(e)) => assert!( - format!("{}", e).contains(&s), - "expected {} to contain `{}`", - e, - s - ), - (TestOutput::ProfileChain(expected), Ok(actual)) => { - assert_eq!(to_test_output(actual), expected) - } - (expected, actual) => panic!( - "error/success mismatch. Expected:\n {:?}\nActual:\n {:?}", - &expected, actual - ), - } - } - - #[derive(Deserialize)] - struct TestCase { - docs: String, - input: TestInput, - output: TestOutput, - } - - #[derive(Deserialize)] - struct TestInput { - profile: HashMap>, - selected_profile: String, - } - - fn to_test_output(profile_chain: ProfileChain) -> Vec { - let mut output = vec![]; - match profile_chain.base { - BaseProvider::NamedSource(name) => output.push(Provider::NamedSource(name.into())), - BaseProvider::AccessKey(creds) => output.push(Provider::AccessKey { - access_key_id: creds.access_key_id().into(), - secret_access_key: creds.secret_access_key().into(), - session_token: creds.session_token().map(|tok| tok.to_string()), - }), - BaseProvider::WebIdentityTokenRole { - role_arn, - web_identity_token_file, - session_name, - } => output.push(Provider::WebIdentityToken { - role_arn: role_arn.into(), - web_identity_token_file: web_identity_token_file.into(), - role_session_name: session_name.map(|sess| sess.to_string()), - }), - }; - for role in profile_chain.chain { - output.push(Provider::AssumeRole { - role_arn: role.role_arn.into(), - external_id: role.external_id.map(ToString::to_string), - role_session_name: role.session_name.map(ToString::to_string), - }) - } - output - } - - #[derive(Deserialize, Debug, PartialEq, Eq)] - enum TestOutput { - ProfileChain(Vec), - Error(String), - } - - #[derive(Deserialize, Debug, Eq, PartialEq)] - enum Provider { - AssumeRole { - role_arn: String, - external_id: Option, - role_session_name: Option, - }, - AccessKey { - access_key_id: String, - secret_access_key: String, - session_token: Option, - }, - NamedSource(String), - WebIdentityToken { - role_arn: String, - web_identity_token_file: String, - role_session_name: Option, - }, - } -} diff --git a/aws/rust-runtime/aws-auth-providers/src/sts_util.rs b/aws/rust-runtime/aws-auth-providers/src/sts_util.rs deleted file mode 100644 index 93b477053a..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/sts_util.rs +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use aws_auth::provider::{CredentialsError, CredentialsResult}; -use aws_sdk_sts::model::Credentials as StsCredentials; -use aws_types::Credentials as AwsCredentials; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// Convert STS credentials to aws_types::Credentials -pub fn into_credentials( - sts_credentials: Option, - provider_name: &'static str, -) -> CredentialsResult { - let sts_credentials = sts_credentials - .ok_or_else(|| CredentialsError::Unhandled("STS credentials must be defined".into()))?; - let expiration = sts_credentials - .expiration - .ok_or_else(|| CredentialsError::Unhandled("missing expiration".into()))?; - let expiration = expiration.to_system_time().ok_or_else(|| { - CredentialsError::Unhandled( - format!("expiration is before unix epoch: {:?}", &expiration).into(), - ) - })?; - Ok(AwsCredentials::new( - sts_credentials.access_key_id.ok_or_else(|| { - CredentialsError::Unhandled("access key id missing from result".into()) - })?, - sts_credentials - .secret_access_key - .ok_or_else(|| CredentialsError::Unhandled("secret access token missing".into()))?, - sts_credentials.session_token, - Some(expiration), - provider_name, - )) -} - -/// Create a default STS session name -/// -/// STS Assume Role providers MUST assign a name to their generated session. When a user does not -/// provide a name for the session, the provider will choose a name composed of a base + a timestamp, -/// eg. `profile-file-provider-123456789` -pub fn default_session_name(base: &str) -> String { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("post epoch"); - format!("{}-{}", base, now.as_millis()) -} diff --git a/aws/rust-runtime/aws-auth-providers/src/test_case.rs b/aws/rust-runtime/aws-auth-providers/src/test_case.rs deleted file mode 100644 index 47e08eb71d..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/test_case.rs +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -#![cfg(test)] - -use std::collections::HashMap; -use std::error::Error; -use std::path::{Path, PathBuf}; -use std::time::UNIX_EPOCH; - -use aws_auth::provider::{AsyncProvideCredentials, CredentialsResult}; -use aws_hyper::DynConnector; -use aws_types::os_shim_internal::{Env, Fs}; -use serde::Deserialize; -use smithy_client::dvr::{NetworkTraffic, RecordingConnection, ReplayingConnection}; - -#[derive(Deserialize, Debug, Eq, PartialEq)] -struct Credentials { - access_key_id: String, - secret_access_key: String, - session_token: Option, - expiry: Option, -} - -/// Convert real credentials to test credentials -/// -/// Comparing equality on real credentials works, but it's a pain because the Debug implementation -/// hides the actual keys -impl From<&aws_types::Credentials> for Credentials { - fn from(credentials: &aws_types::Credentials) -> Self { - Self { - access_key_id: credentials.access_key_id().into(), - secret_access_key: credentials.secret_access_key().into(), - session_token: credentials.session_token().map(ToString::to_string), - expiry: credentials - .expiry() - .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs()), - } - } -} - -/// Credentials test environment -/// -/// A credentials test environment is a directory containing: -/// - an `fs` directory. This is loaded into the test as if it was mounted at `/` -/// - an `env.json` file containing environment variables -/// - an `http-traffic.json` file containing an http traffic log from [`dvr`](smithy_client::dvr) -/// - a `test-case.json` file defining the expected output of the test -pub struct TestEnvironment { - env: Env, - fs: Fs, - network_traffic: NetworkTraffic, - metadata: Metadata, - base_dir: PathBuf, -} - -#[derive(Deserialize)] -enum TestResult { - Ok(Credentials), - ErrorContains(String), -} - -#[derive(Deserialize)] -pub struct Metadata { - result: TestResult, - docs: String, - name: String, -} - -impl TestEnvironment { - pub fn from_dir(dir: impl AsRef) -> Result> { - let dir = dir.as_ref(); - let env = std::fs::read_to_string(dir.join("env.json")) - .map_err(|e| format!("failed to load env: {}", e))?; - let env: HashMap = - serde_json::from_str(&env).map_err(|e| format!("failed to parse env: {}", e))?; - let env = Env::from(env); - let fs = Fs::from_test_dir(dir.join("fs"), "/"); - let network_traffic = std::fs::read_to_string(dir.join("http-traffic.json")) - .map_err(|e| format!("failed to load http traffic: {}", e))?; - let network_traffic: NetworkTraffic = serde_json::from_str(&network_traffic)?; - - let metadata: Metadata = serde_json::from_str( - &std::fs::read_to_string(dir.join("test-case.json")) - .map_err(|e| format!("failed to load test case: {}", e))?, - )?; - Ok(TestEnvironment { - base_dir: dir.into(), - env, - fs, - network_traffic, - metadata, - }) - } - - /// Execute the test suite & record a new traffic log - /// - /// A connector will be created with the factory, then request traffic will be recorded. - /// Response are generated from the existing http-traffic.json. - pub async fn execute_and_update

(&self, make_provider: impl Fn(Fs, Env, DynConnector) -> P) - where - P: AsyncProvideCredentials, - { - let connector = RecordingConnection::new(ReplayingConnection::new( - self.network_traffic.events().clone(), - )); - let provider = make_provider( - self.fs.clone(), - self.env.clone(), - DynConnector::new(connector.clone()), - ); - let result = provider.provide_credentials().await; - std::fs::write( - self.base_dir.join("http-traffic-recorded.json"), - serde_json::to_string(&connector.network_traffic()).unwrap(), - ) - .unwrap(); - self.check_results(&result); - } - - fn log_info(&self) { - eprintln!("test case: {}. {}", self.metadata.name, self.metadata.docs); - } - - /// Execute a test case. Failures lead to panics. - pub async fn execute

(&self, make_provider: impl Fn(Fs, Env, DynConnector) -> P) - where - P: AsyncProvideCredentials, - { - let connector = ReplayingConnection::new(self.network_traffic.events().clone()); - let provider = make_provider( - self.fs.clone(), - self.env.clone(), - DynConnector::new(connector.clone()), - ); - let result = provider.provide_credentials().await; - self.log_info(); - self.check_results(&result); - // todo: validate bodies - match connector.validate(&["CONTENT-TYPE"], |_expected, _actual| Ok(())) { - Ok(()) => {} - Err(e) => panic!("{}", e), - } - } - - fn check_results(&self, result: &CredentialsResult) { - match (&result, &self.metadata.result) { - (Ok(actual), TestResult::Ok(expected)) => { - assert_eq!( - expected, - &Credentials::from(actual), - "incorrect credentials were returned" - ) - } - (Err(err), TestResult::ErrorContains(substr)) => { - assert!( - format!("{}", err).contains(substr), - "`{}` did not contain `{}`", - err, - substr - ) - } - (Err(actual_error), TestResult::Ok(expected_creds)) => panic!( - "expected credentials ({:?}) but an error was returned: {}", - expected_creds, actual_error - ), - (Ok(creds), TestResult::ErrorContains(substr)) => panic!( - "expected an error containing: `{}`, but credentials were returned: {:?}", - substr, creds - ), - } - } -} diff --git a/aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs b/aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs deleted file mode 100644 index 601b208566..0000000000 --- a/aws/rust-runtime/aws-auth-providers/src/web_identity_token.rs +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -//! Load Credentials from Web Identity Tokens -//! -//! WebIdentity tokens can be loaded via environment variables, or via profiles: -//! -//! ## Via Environment Variables -//! WebIdentityTokenCredentialProvider will load the following environment variables: -//! - `AWS_WEB_IDENTITY_TOKEN_FILE`: **required**, location to find the token file containing a JWT token -//! - `AWS_ROLE_ARN`: **required**, role ARN to assume -//! - `AWS_IAM_ROLE_SESSION_NAME`: **optional**: Session name to use when assuming the role -//! -//! ## Via Shared Config Profiles -//! Web identity token credentials can be loaded from `~/.aws/config` in two ways: -//! 1. Directly: -//! ```ini -//! [profile default] -//! role_arn = arn:aws:iam::1234567890123:role/RoleA -//! web_identity_token_file = /token.jwt -//! ``` -//! -//! 2. As a source profile for another role: -//! -//! ```ini -//! [profile default] -//! role_arn = arn:aws:iam::123456789:role/RoleA -//! source_profile = base -//! -//! [profile base] -//! role_arn = arn:aws:iam::123456789012:role/s3-reader -//! web_identity_token_file = /token.jwt -//! ``` - -use aws_hyper::{DynConnector, StandardClient}; -use aws_sdk_sts::Region; -use aws_types::os_shim_internal::{Env, Fs}; - -use crate::{must_have_connector, sts_util}; -use aws_auth::provider::{AsyncProvideCredentials, BoxFuture, CredentialsError, CredentialsResult}; -use std::borrow::Cow; -use std::path::{Path, PathBuf}; - -const ENV_VAR_TOKEN_FILE: &str = "AWS_WEB_IDENTITY_TOKEN_FILE"; -const ENV_VAR_ROLE_ARN: &str = "AWS_ROLE_ARN"; -const ENV_VAR_SESSION_NAME: &str = "AWS_ROLE_SESSION_NAME"; - -/// Credential provider to load credentials from Web Identity Tokens -/// -/// See Module documentation for more details -pub struct WebIdentityTokenCredentialProvider { - source: Source, - fs: Fs, - client: StandardClient, - region: Option, -} - -impl WebIdentityTokenCredentialProvider { - pub fn builder() -> Builder { - Builder::default() - } -} - -enum Source { - Env(Env), - Static(WebIdentityTokenRole), -} - -/// Hard-coded WebIdentityToken role -#[derive(Debug, Clone)] -pub struct WebIdentityTokenRole { - pub web_identity_token_file: PathBuf, - pub role_arn: String, - pub session_name: String, -} - -impl AsyncProvideCredentials for WebIdentityTokenCredentialProvider { - fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult> - where - Self: 'a, - { - Box::pin(self.credentials()) - } -} - -impl WebIdentityTokenCredentialProvider { - fn source(&self) -> Result, CredentialsError> { - match &self.source { - Source::Env(env) => { - let token_file = env - .get(ENV_VAR_TOKEN_FILE) - .map_err(|_| CredentialsError::CredentialsNotLoaded)?; - let role_arn = env.get(ENV_VAR_ROLE_ARN).map_err(|_| { - CredentialsError::InvalidConfiguration( - "AWS_ROLE_ARN environment variable must be set".into(), - ) - })?; - let session_name = env - .get(ENV_VAR_SESSION_NAME) - .unwrap_or_else(|_| sts_util::default_session_name("web-identity-token")); - Ok(Cow::Owned(WebIdentityTokenRole { - web_identity_token_file: token_file.into(), - role_arn, - session_name, - })) - } - Source::Static(conf) => Ok(Cow::Borrowed(conf)), - } - } - async fn credentials(&self) -> credentials::Result { - let conf = self.source()?; - load_credentials( - &self.fs, - &self.client, - &self.region.as_ref().cloned().ok_or_else(|| { - CredentialsError::InvalidConfiguration( - "region is required for WebIdentityTokenProvider".into(), - ) - })?, - &conf.web_identity_token_file, - &conf.role_arn, - &conf.session_name, - ) - .await - } -} - -#[derive(Default)] -pub struct Builder { - source: Option, - fs: Fs, - connector: Option, - region: Option, -} - -impl Builder { - pub fn fs(mut self, fs: Fs) -> Self { - self.fs = fs; - self - } - - pub fn set_fs(&mut self, fs: Fs) -> &mut Self { - self.fs = fs; - self - } - - pub fn env(mut self, env: Env) -> Self { - self.source = Some(Source::Env(env)); - self - } - - pub fn static_configuration(mut self, config: WebIdentityTokenRole) -> Self { - self.source = Some(Source::Static(config)); - self - } - - pub fn set_env(&mut self, env: Env) -> &mut Self { - self.source = Some(Source::Env(env)); - self - } - - pub fn connector(mut self, connector: DynConnector) -> Self { - self.connector = Some(connector); - self - } - - pub fn set_connector(&mut self, connector: Option) -> &mut Self { - self.connector = connector; - self - } - - pub fn region(mut self, region: Option) -> Self { - self.region = region; - self - } - - pub fn set_region(&mut self, region: Option) -> &mut Self { - self.region = region; - self - } - - pub fn build(self) -> WebIdentityTokenCredentialProvider { - let connector = self.connector.unwrap_or_else(must_have_connector); - let client = aws_hyper::Builder::<()>::new() - .map_connector(|_| connector) - .build(); - let source = self.source.unwrap_or_else(|| Source::Env(Env::default())); - WebIdentityTokenCredentialProvider { - source, - fs: self.fs, - client, - region: self.region, - } - } -} - -async fn load_credentials( - fs: &Fs, - client: &StandardClient, - region: &Region, - token_file: impl AsRef, - role_arn: &str, - session_name: &str, -) -> CredentialsResult { - let token = fs - .read_to_end(token_file) - .await - .map_err(|err| CredentialsError::ProviderError(err.into()))?; - let token = String::from_utf8(token).map_err(|_utf_8_error| { - CredentialsError::Unhandled("WebIdentityToken was not valid UTF-8".into()) - })?; - let conf = aws_sdk_sts::Config::builder() - .region(region.clone()) - .build(); - - let operation = aws_sdk_sts::operation::AssumeRoleWithWebIdentity::builder() - .role_arn(role_arn) - .role_session_name(session_name) - .web_identity_token(token) - .build() - .expect("valid operation") - .make_operation(&conf) - .expect("valid operation"); - let resp = client.call(operation).await.map_err(|sdk_error| { - tracing::warn!(error = ?sdk_error, "sts returned an error assuming web identity role"); - CredentialsError::ProviderError(sdk_error.into()) - })?; - sts_util::into_credentials(resp.credentials, "WebIdentityToken") -} - -#[cfg(test)] -mod test { - use crate::web_identity_token::{ - Builder, ENV_VAR_ROLE_ARN, ENV_VAR_SESSION_NAME, ENV_VAR_TOKEN_FILE, - }; - use aws_auth::provider::CredentialsError; - - use aws_sdk_sts::Region; - use aws_types::os_shim_internal::{Env, Fs}; - - use std::collections::HashMap; - - #[tokio::test] - async fn unloaded_provider() { - // empty environment - let env = Env::from_slice(&[]); - let provider = Builder::default() - .region(Some(Region::new("us-east-1"))) - .env(env) - .build(); - let err = provider - .credentials() - .await - .expect_err("should fail, provider not loaded"); - match err { - CredentialsError::CredentialsNotLoaded => { /* ok */ } - _ => panic!("incorrect error variant"), - } - } - - #[tokio::test] - async fn missing_env_var() { - let env = Env::from_slice(&[(ENV_VAR_TOKEN_FILE, "/token.jwt")]); - let provider = Builder::default() - .region(Some(Region::new("us-east-1"))) - .env(env) - .build(); - let err = provider - .credentials() - .await - .expect_err("should fail, provider not loaded"); - assert!( - format!("{}", err).contains("AWS_ROLE_ARN"), - "`{}` did not contain expected string", - err - ); - match err { - CredentialsError::InvalidConfiguration(_) => { /* ok */ } - _ => panic!("incorrect error variant"), - } - } - - #[tokio::test] - async fn fs_missing_file() { - let env = Env::from_slice(&[ - (ENV_VAR_TOKEN_FILE, "/token.jwt"), - (ENV_VAR_ROLE_ARN, "arn:aws:iam::123456789123:role/test-role"), - (ENV_VAR_SESSION_NAME, "test-session"), - ]); - let fs = Fs::from_map(HashMap::new()); - let provider = Builder::default() - .region(Some(Region::new("us-east-1"))) - .fs(fs) - .env(env) - .build(); - let err = provider.credentials().await.expect_err("no JWT token"); - match err { - CredentialsError::ProviderError(_) => { /* ok */ } - _ => panic!("incorrect error variant"), - } - } -} diff --git a/aws/rust-runtime/aws-config/src/default_provider.rs b/aws/rust-runtime/aws-config/src/default_provider.rs index b82c54faaa..20a5533f71 100644 --- a/aws/rust-runtime/aws-config/src/default_provider.rs +++ b/aws/rust-runtime/aws-config/src/default_provider.rs @@ -22,12 +22,240 @@ pub mod region { /// Default credentials provider chain pub mod credentials { use crate::environment::credentials::EnvironmentVariableCredentialsProvider; - use aws_types::credentials::ProvideCredentials; + use crate::meta::credentials::{CredentialsProviderChain, LazyCachingCredentialsProvider}; + use aws_types::credentials::{future, ProvideCredentials}; + use aws_types::os_shim_internal::{Env, Fs}; + use aws_types::region::Region; + use smithy_async::rt::sleep::AsyncSleep; + use smithy_client::erase::DynConnector; + use std::borrow::Cow; - /// Default Region Provider chain + #[cfg(any(feature = "rustls", feature = "native-tls"))] + /// Default Credentials Provider chain /// - /// This provider will load region from environment variables. - pub fn default_provider() -> impl ProvideCredentials { - EnvironmentVariableCredentialsProvider::new() + /// The region from the default region provider will be used + pub async fn default_provider() -> impl ProvideCredentials { + use crate::meta::region::ProvideRegion; + let region = super::region::default_provider().region().await; + let mut builder = DefaultCredentialsChain::builder(); + builder.set_region(region); + builder.build() + } + + /// Default AWS Credential Provider Chain + /// + /// Resolution order: + /// 1. Environment variables: [`EnvironmentVariableCredentialsProvider`](crate::environment::EnvironmentVariableCredentialsProvider) + /// 2. Shared config (`~/.aws/config`, `~/.aws/credentials`): [`SharedConfigCredentialsProvider`](crate::profile::ProfileFileCredentialsProvider) + /// + /// The outer provider is wrapped in a refreshing cache. + /// + /// More providers are a work in progress. + /// + /// ## Example: + /// Create a default chain with a custom region: + /// ```rust + /// use aws_types::region::Region; + /// use aws_config::default_provider::credentials::DefaultCredentialsChain; + /// let credentials_provider = DefaultCredentialsChain::builder() + /// .region(Region::new("us-west-1")) + /// .build(); + /// ``` + /// + /// Create a default chain with no overrides: + /// ```rust + /// use aws_config::default_provider::credentials::DefaultCredentialsChain; + /// let credentials_provider = DefaultCredentialsChain::builder().build(); + /// ``` + #[derive(Debug)] + pub struct DefaultCredentialsChain(LazyCachingCredentialsProvider); + + impl DefaultCredentialsChain { + /// Builder for `DefaultCredentialsChain` + pub fn builder() -> Builder { + Builder::default() + } + } + + impl ProvideCredentials for DefaultCredentialsChain { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + self.0.provide_credentials() + } + } + + /// Builder for [`DefaultCredentialsChain`](DefaultCredentialsChain) + #[derive(Default)] + pub struct Builder { + profile_file_builder: crate::profile::credentials::Builder, + web_identity_builder: crate::web_identity_token::Builder, + credential_cache: crate::meta::credentials::lazy_caching::Builder, + env: Option, + } + + impl Builder { + /// Sets the region used when making requests to AWS services + /// + /// When unset, the default region resolver chain will be used. + pub fn region(mut self, region: Region) -> Self { + self.set_region(Some(region)); + self + } + + /// Sets the region used when making requests to AWS services + /// + /// When unset, the default region resolver chain will be used. + pub fn set_region(&mut self, region: Option) -> &mut Self { + self.profile_file_builder.set_region(region.clone()); + self.web_identity_builder.set_region(region); + self + } + + /// Override the HTTPS connector used for this provider + /// + /// If a connector other than Hyper is used or if the Tokio/Hyper features have been disabled + /// this method MUST be used to specify a custom connector. + pub fn connector(mut self, connector: DynConnector) -> Self { + self.profile_file_builder + .set_connector(Some(connector.clone())); + self.web_identity_builder.set_connector(Some(connector)); + self + } + + /// Override the sleep implementation used for this provider + /// + /// By default, Tokio will be used to support async sleep during credentials for timeouts + /// and reloading credentials. If the tokio default feature has been disabled, a custom + /// sleep implementation must be provided. + pub fn sleep(mut self, sleep: impl AsyncSleep + 'static) -> Self { + self.credential_cache = self.credential_cache.sleep(sleep); + self + } + + /// Add an additional credential source for the ProfileProvider + /// + /// Assume role profiles may specify named credential sources: + /// ```ini + /// [default] + /// role_arn = arn:aws:iam::123456789:role/RoleA + /// credential_source = MyCustomProvider + /// ``` + /// + /// Typically, these are built-in providers like `Environment`, however, custom sources may + /// also be used. + /// + /// See [`with_custom_provider`](crate::profile::credentials::Builder::with_custom_provider) + pub fn with_custom_credential_source( + mut self, + name: impl Into>, + provider: impl ProvideCredentials + 'static, + ) -> Self { + self.profile_file_builder = self + .profile_file_builder + .with_custom_provider(name, provider); + self + } + + #[doc(hidden)] + /// Override the filesystem used for this provider + /// + /// This method exists to test credential providers + pub fn fs(mut self, fs: Fs) -> Self { + self.profile_file_builder.set_fs(fs.clone()); + self.web_identity_builder.set_fs(fs); + self + } + + #[doc(hidden)] + /// Override the environment used for this provider + /// + /// This method exists to test credential providers + pub fn env(mut self, env: Env) -> Self { + self.env = Some(env.clone()); + self.profile_file_builder.set_env(env.clone()); + self.web_identity_builder.set_env(env); + self + } + + /// Creates a `DefaultCredentialsChain` + /// + /// ## Panics + /// This function will panic if no connector has been set and neither `rustls` and `native-tls` + /// features have both been disabled. + pub fn build(self) -> DefaultCredentialsChain { + let profile_provider = self.profile_file_builder.build(); + let env_provider = + EnvironmentVariableCredentialsProvider::new_with_env(self.env.unwrap_or_default()); + let web_identity_token_provider = self.web_identity_builder.build(); + let provider_chain = CredentialsProviderChain::first_try("Environment", env_provider) + .or_else("Profile", profile_provider) + .or_else("WebIdentityToken", web_identity_token_provider); + let cached_provider = self.credential_cache.load(provider_chain); + DefaultCredentialsChain(cached_provider.build()) + } + } + + #[cfg(test)] + mod test { + + macro_rules! make_test { + ($name: ident) => { + #[traced_test] + #[tokio::test] + async fn $name() { + crate::test_case::TestEnvironment::from_dir(concat!( + "./test-data/default-provider-chain/", + stringify!($name) + )) + .unwrap() + .execute(|fs, env, conn| { + crate::default_provider::credentials::Builder::default() + .env(env) + .fs(fs) + .region(Region::from_static("us-east-1")) + .connector(conn) + .build() + }) + .await + } + }; + } + + use aws_sdk_sts::Region; + + use tracing_test::traced_test; + + make_test!(prefer_environment); + make_test!(profile_static_keys); + make_test!(web_identity_token_env); + make_test!(web_identity_source_profile_no_env); + make_test!(web_identity_token_invalid_jwt); + make_test!(web_identity_token_source_profile); + make_test!(web_identity_token_profile); + make_test!(profile_overrides_web_identity); + + /// Helper that uses `execute_and_update` instead of execute + /// + /// If you run this, it will add another HTTP traffic log which re-records the request + /// data + #[tokio::test] + #[ignore] + async fn update_test() { + crate::test_case::TestEnvironment::from_dir(concat!( + "./test-data/default-provider-chain/web_identity_token_source_profile", + )) + .unwrap() + .execute_and_update(|fs, env, conn| { + super::Builder::default() + .env(env) + .fs(fs) + .region(Region::from_static("us-east-1")) + .connector(conn) + .build() + }) + .await + } } } diff --git a/aws/rust-runtime/aws-config/src/environment/mod.rs b/aws/rust-runtime/aws-config/src/environment/mod.rs index 39131f865a..78c59447d6 100644 --- a/aws/rust-runtime/aws-config/src/environment/mod.rs +++ b/aws/rust-runtime/aws-config/src/environment/mod.rs @@ -5,5 +5,7 @@ /// Load credentials from the environment pub mod credentials; +pub use credentials::EnvironmentVariableCredentialsProvider; /// Load regions from the environment pub mod region; +pub use region::EnvironmentVariableRegionProvider; diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index c7a056129f..df25efd6f2 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -159,7 +159,9 @@ mod loader { let credentials_provider = if let Some(provider) = self.credentials_provider { provider } else { - SharedCredentialsProvider::new(credentials::default_provider()) + let mut builder = credentials::DefaultCredentialsChain::builder(); + builder.set_region(region.clone()); + SharedCredentialsProvider::new(builder.build()) }; Config::builder() .region(region) diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs b/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs index 9f6986648a..7a5da37943 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs @@ -55,19 +55,19 @@ impl CredentialsProviderChain { #[cfg(feature = "default-provider")] /// Add a fallback to the default provider chain - pub fn or_default_provider(self) -> Self { + pub async fn or_default_provider(self) -> Self { self.or_else( "DefaultProviderChain", - crate::default_provider::credentials::default_provider(), + crate::default_provider::credentials::default_provider().await, ) } #[cfg(feature = "default-provider")] /// Creates a credential provider chain that starts with the default provider - pub fn default_provider() -> Self { + pub async fn default_provider() -> Self { Self::first_try( "DefaultProviderChain", - crate::default_provider::credentials::default_provider(), + crate::default_provider::credentials::default_provider().await, ) } diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs index cf5593f0bb..da8cfe3740 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs @@ -108,7 +108,8 @@ impl ProvideCredentials for LazyCachingCredentialsProvider { } } -pub mod builder { +pub use builder::Builder; +mod builder { use std::sync::Arc; use std::time::Duration; @@ -147,6 +148,7 @@ pub mod builder { } impl Builder { + /// Creates a new builder pub fn new() -> Self { Default::default() } @@ -185,7 +187,7 @@ pub mod builder { /// (Optional) Default expiration time to set on credentials if they don't /// have an expiration time. This is only used if the given [`ProvideCredentials`] - /// returns [`Credentials`](crate::Credentials) that don't have their `expiry` set. + /// returns [`Credentials`](aws_types::Credentials) that don't have their `expiry` set. /// This must be at least 15 minutes. pub fn default_credential_expiration(mut self, duration: Duration) -> Self { self.default_credential_expiration = Some(duration); diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs index e17155603b..96ff1a331e 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs @@ -14,7 +14,7 @@ pub use chain::CredentialsProviderChain; mod credential_fn; pub use credential_fn::async_provide_credentials_fn; -mod lazy_caching; +pub mod lazy_caching; pub use lazy_caching::LazyCachingCredentialsProvider; // pub mod credential_fn; diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index 2c945462d6..f389c8257a 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -191,28 +191,48 @@ impl ProfileFileCredentialsProvider { } } +/// An Error building a Credential source from an AWS Profile #[derive(Debug)] #[non_exhaustive] pub enum ProfileFileError { + /// The profile was not a valid AWS profile CouldNotParseProfile(ProfileParseError), + + /// No profiles existed (the profile was empty) NoProfilesDefined, + + /// The profile contained an infinite loop of `source_profile` references CredentialLoop { + /// Vec of profiles leading to the loop profiles: Vec, + /// The next profile that caused the loop next: String, }, + + /// The profile was missing a credential source MissingCredentialSource { + /// The name of the profile profile: String, + /// Error message message: Cow<'static, str>, }, + /// The profile contained an invalid credential source InvalidCredentialSource { + /// The name of the profile profile: String, + /// Error message message: Cow<'static, str>, }, + /// The profile referred to a another profile by name that was not defined MissingProfile { + /// The name of the profile profile: String, + /// Error message message: Cow<'static, str>, }, + /// The profile referred to `credential_source` that was not defined UnknownProvider { + /// The name of the provider name: String, }, } @@ -257,6 +277,7 @@ impl Error for ProfileFileError { } } +/// Builder for [`ProfileFileCredentialsProvider`](ProfileFileCredentialsProvider) #[derive(Default)] pub struct Builder { fs: Fs, @@ -267,46 +288,78 @@ pub struct Builder { } impl Builder { + #[doc(hidden)] pub fn fs(mut self, fs: Fs) -> Self { self.fs = fs; self } + #[doc(hidden)] pub fn set_fs(&mut self, fs: Fs) -> &mut Self { self.fs = fs; self } + #[doc(hidden)] pub fn env(mut self, env: Env) -> Self { self.env = env; self } + #[doc(hidden)] pub fn set_env(&mut self, env: Env) -> &mut Self { self.env = env; self } + /// Sets the HTTPS connector used for requests to AWS pub fn connector(mut self, connector: DynConnector) -> Self { self.connector = Some(connector); self } + /// Sets the HTTPS connector used for requests to AWS pub fn set_connector(&mut self, connector: Option) -> &mut Self { self.connector = connector; self } + /// Sets the region used for requests to AWS pub fn region(mut self, region: Region) -> Self { self.region = Some(region); self } + /// Sets the region used for requests to AWS pub fn set_region(&mut self, region: Option) -> &mut Self { self.region = region; self } + /// Adds a custom credential source + /// + /// # Example + /// + /// ```rust + /// use aws_types::credentials::{self, ProvideCredentials, future}; + /// use aws_config::profile::ProfileFileCredentialsProvider; + /// #[derive(Debug)] + /// struct MyCustomProvider; + /// impl MyCustomProvider { + /// async fn load_credentials(&self) -> credentials::Result { + /// todo!() + /// } + /// } + /// + /// impl ProvideCredentials for MyCustomProvider { + /// fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a { + /// future::ProvideCredentials::new(self.load_credentials()) + /// } + /// } + /// let provider = ProfileFileCredentialsProvider::builder() + /// .with_custom_provider("Custom", MyCustomProvider) + /// .build(); + /// ``` pub fn with_custom_provider( mut self, name: impl Into>, @@ -317,6 +370,7 @@ impl Builder { self } + /// Builds a [`ProfileFileCredentialsProvider`](ProfileFileCredentialsProvider) pub fn build(self) -> ProfileFileCredentialsProvider { let build_span = tracing::info_span!("build_profile_provider"); let _enter = build_span.enter(); 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 fb50e7ca52..5cc421e7df 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/exec.rs @@ -65,7 +65,7 @@ impl AssumeRoleProvider { } } -pub(crate) struct ProviderChain { +pub(super) struct ProviderChain { base: Arc, chain: Vec, } diff --git a/aws/rust-runtime/aws-config/src/profile/mod.rs b/aws/rust-runtime/aws-config/src/profile/mod.rs index 3c0e8ea7df..52a4433c66 100644 --- a/aws/rust-runtime/aws-config/src/profile/mod.rs +++ b/aws/rust-runtime/aws-config/src/profile/mod.rs @@ -11,7 +11,7 @@ mod parser; pub use parser::{load, Profile, ProfileSet, Property}; -mod credentials; +pub mod credentials; pub use credentials::ProfileFileCredentialsProvider; /* pub mod credential; From c5514c96536e02aa1f658ab2b0cb9b598c930e25 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:04:00 -0400 Subject: [PATCH 07/18] Update examples --- aws/rust-runtime/Cargo.toml | 2 +- aws/sdk/build.gradle.kts | 1 - aws/sdk/examples/s3/Cargo.toml | 1 - aws/sdk/examples/s3/src/bin/list-objects.rs | 13 +------------ aws/sdk/examples/transcribestreaming/Cargo.toml | 1 - 5 files changed, 2 insertions(+), 16 deletions(-) diff --git a/aws/rust-runtime/Cargo.toml b/aws/rust-runtime/Cargo.toml index 04dbe548f9..14942c7d73 100644 --- a/aws/rust-runtime/Cargo.toml +++ b/aws/rust-runtime/Cargo.toml @@ -13,4 +13,4 @@ members = [ "aws-sigv4" ] -exclude = ["aws-auth-providers", "aws-config"] +exclude = ["aws-config"] diff --git a/aws/sdk/build.gradle.kts b/aws/sdk/build.gradle.kts index 7e1193c47f..7335cfbb67 100644 --- a/aws/sdk/build.gradle.kts +++ b/aws/sdk/build.gradle.kts @@ -34,7 +34,6 @@ val runtimeModules = listOf( ) val awsModules = listOf( "aws-auth", - "aws-auth-providers", "aws-config", "aws-endpoint", "aws-http", diff --git a/aws/sdk/examples/s3/Cargo.toml b/aws/sdk/examples/s3/Cargo.toml index d4c1f910e0..68cb5a2557 100644 --- a/aws/sdk/examples/s3/Cargo.toml +++ b/aws/sdk/examples/s3/Cargo.toml @@ -10,7 +10,6 @@ edition = "2018" aws-config = { path = "../../build/aws-sdk/aws-config" } aws-sdk-s3 = { package = "aws-sdk-s3", path = "../../build/aws-sdk/s3" } aws-types = { path = "../../build/aws-sdk/aws-types" } -aws-auth-providers = { path = "../../build/aws-sdk/aws-auth-providers" } tokio = { version = "1", features = ["full"] } diff --git a/aws/sdk/examples/s3/src/bin/list-objects.rs b/aws/sdk/examples/s3/src/bin/list-objects.rs index a400d2b8cb..99c51deac8 100644 --- a/aws/sdk/examples/s3/src/bin/list-objects.rs +++ b/aws/sdk/examples/s3/src/bin/list-objects.rs @@ -6,8 +6,6 @@ use aws_config::meta::region::RegionProviderChain; use aws_sdk_s3::{Client, Error, Region, PKG_VERSION}; -use aws_auth_providers::DefaultProviderChain; - use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -46,13 +44,10 @@ async fn main() -> Result<(), Error> { let region_provider = RegionProviderChain::first_try(region.map(Region::new)) .or_default_provider() .or_else(Region::new("us-west-2")); - let region = region_provider.region().await.expect("fallback exists"); let shared_config = aws_config::from_env().region(region_provider).load().await; + let client = Client::new(&shared_config); println!(); - let credential_provider = DefaultProviderChain::builder() - .region(region.clone()) - .build(); if verbose { println!("S3 client version: {}", PKG_VERSION); @@ -61,12 +56,6 @@ async fn main() -> Result<(), Error> { println!(); } - let config = aws_sdk_s3::config::Builder::from(&shared_config) - .credentials_provider(credential_provider) - .build(); - - let client = Client::from_conf(config); - let resp = client.list_objects_v2().bucket(&bucket).send().await?; println!("Objects:"); diff --git a/aws/sdk/examples/transcribestreaming/Cargo.toml b/aws/sdk/examples/transcribestreaming/Cargo.toml index 068a937598..095230d4ca 100644 --- a/aws/sdk/examples/transcribestreaming/Cargo.toml +++ b/aws/sdk/examples/transcribestreaming/Cargo.toml @@ -8,7 +8,6 @@ edition = "2018" [dependencies] aws-config = { path = "../../build/aws-sdk/aws-config" } -aws-auth-providers = { path = "../../build/aws-sdk/aws-auth-providers" } aws-sdk-transcribestreaming = { package = "aws-sdk-transcribestreaming", path = "../../build/aws-sdk/transcribestreaming" } aws-types = { path = "../../build/aws-sdk/aws-types" } From cc423119ba0305d67a3cce7ac570119d79c339d5 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:11:44 -0400 Subject: [PATCH 08/18] Delete profile parser from aws-types and move fuzz tests --- .../{aws-types => aws-config}/fuzz/.gitignore | 0 .../{aws-types => aws-config}/fuzz/Cargo.toml | 0 .../fuzz/fuzz_targets/profile-parser.rs | 2 +- aws/rust-runtime/aws-types/Cargo.toml | 4 - aws/rust-runtime/aws-types/src/lib.rs | 1 - aws/rust-runtime/aws-types/src/profile.rs | 363 --------- .../aws-types/src/profile/normalize.rs | 223 ------ .../aws-types/src/profile/parse.rs | 346 --------- .../aws-types/src/profile/source.rs | 353 --------- .../test-data/file-location-tests.json | 121 --- .../test-data/profile-parser-tests.json | 713 ------------------ 11 files changed, 1 insertion(+), 2125 deletions(-) rename aws/rust-runtime/{aws-types => aws-config}/fuzz/.gitignore (100%) rename aws/rust-runtime/{aws-types => aws-config}/fuzz/Cargo.toml (100%) rename aws/rust-runtime/{aws-types => aws-config}/fuzz/fuzz_targets/profile-parser.rs (96%) delete mode 100644 aws/rust-runtime/aws-types/src/profile.rs delete mode 100644 aws/rust-runtime/aws-types/src/profile/normalize.rs delete mode 100644 aws/rust-runtime/aws-types/src/profile/parse.rs delete mode 100644 aws/rust-runtime/aws-types/src/profile/source.rs delete mode 100644 aws/rust-runtime/aws-types/test-data/file-location-tests.json delete mode 100644 aws/rust-runtime/aws-types/test-data/profile-parser-tests.json diff --git a/aws/rust-runtime/aws-types/fuzz/.gitignore b/aws/rust-runtime/aws-config/fuzz/.gitignore similarity index 100% rename from aws/rust-runtime/aws-types/fuzz/.gitignore rename to aws/rust-runtime/aws-config/fuzz/.gitignore diff --git a/aws/rust-runtime/aws-types/fuzz/Cargo.toml b/aws/rust-runtime/aws-config/fuzz/Cargo.toml similarity index 100% rename from aws/rust-runtime/aws-types/fuzz/Cargo.toml rename to aws/rust-runtime/aws-config/fuzz/Cargo.toml diff --git a/aws/rust-runtime/aws-types/fuzz/fuzz_targets/profile-parser.rs b/aws/rust-runtime/aws-config/fuzz/fuzz_targets/profile-parser.rs similarity index 96% rename from aws/rust-runtime/aws-types/fuzz/fuzz_targets/profile-parser.rs rename to aws/rust-runtime/aws-config/fuzz/fuzz_targets/profile-parser.rs index 1160e40ac2..36f3af1f4b 100644 --- a/aws/rust-runtime/aws-types/fuzz/fuzz_targets/profile-parser.rs +++ b/aws/rust-runtime/aws-config/fuzz/fuzz_targets/profile-parser.rs @@ -1,6 +1,6 @@ #![no_main] +use aws_config::profile; use aws_types::os_shim_internal::{Env, Fs}; -use aws_types::profile; use libfuzzer_sys::fuzz_target; use std::collections::HashMap; use std::ffi::OsString; diff --git a/aws/rust-runtime/aws-types/Cargo.toml b/aws/rust-runtime/aws-types/Cargo.toml index 983119b88b..7ca5003d3c 100644 --- a/aws/rust-runtime/aws-types/Cargo.toml +++ b/aws/rust-runtime/aws-types/Cargo.toml @@ -14,10 +14,6 @@ smithy-async = { path = "../../../rust-runtime/smithy-async" } zeroize = "1.4.1" [dev-dependencies] -tracing-test = "0.1.0" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -arbitrary = "1" futures-util = "0.3.16" [build-dependencies] diff --git a/aws/rust-runtime/aws-types/src/lib.rs b/aws/rust-runtime/aws-types/src/lib.rs index b913cfb1c3..e20812be29 100644 --- a/aws/rust-runtime/aws-types/src/lib.rs +++ b/aws/rust-runtime/aws-types/src/lib.rs @@ -9,7 +9,6 @@ pub mod config; pub mod credentials; #[doc(hidden)] pub mod os_shim_internal; -pub mod profile; pub mod region; pub use credentials::Credentials; diff --git a/aws/rust-runtime/aws-types/src/profile.rs b/aws/rust-runtime/aws-types/src/profile.rs deleted file mode 100644 index 0f12cc553e..0000000000 --- a/aws/rust-runtime/aws-types/src/profile.rs +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -mod normalize; -mod parse; -mod source; - -// exposed only to remove unused code warnings until the parser side is added -use crate::os_shim_internal::{Env, Fs}; -use crate::profile::parse::parse_profile_file; -pub use crate::profile::parse::ProfileParseError; -use crate::profile::source::{FileKind, Source}; -use std::borrow::Cow; -use std::collections::HashMap; - -/// Read & parse AWS config files -/// -/// Loads and parses profile files according to the spec: -/// -/// ## Location of Profile Files -/// * The location of the config file will be loaded from `$AWS_CONFIG_FILE` with a fallback to -/// `~/.aws/config` -/// * The location of the credentials file will be loaded from `$AWS_SHARED_CREDENTIALS_FILE` with a -/// fallback to `~/.aws/credentials` -/// -/// ## Home directory resolution -/// Home directory resolution is implemented to match the behavior of the CLI & Python. `~` is only -/// used for home directory resolution when it: -/// - Starts the path -/// - Is followed immediately by `/` or a platform specific separator. (On windows, `~/` and `~\` both -/// resolve to the home directory. -/// -/// When determining the home directory, the following environment variables are checked: -/// - `$HOME` on all platforms -/// - `$USERPROFILE` on Windows -/// - `$HOMEDRIVE$HOMEPATH` on Windows -/// -/// ## Profile file syntax -/// -/// Profile files have a general form similar to INI but with a number of quirks and edge cases. These -/// behaviors are largely to match existing parser implementations and these cases are documented in `test-data/profile-parser-tests.json` -/// in this repo. -/// -/// ### The config file `~/.aws/config` -/// ```ini -/// # ~/.aws/config -/// [profile default] -/// key = value -/// -/// # profiles must begin with `profile` -/// [profile other] -/// key = value2 -/// ``` -/// -/// ### The credentials file `~/.aws/credentials` -/// The main difference is that in ~/.aws/credentials, profiles MUST NOT be prefixed with profile: -/// ```ini -/// [default] -/// aws_access_key_id = 123 -/// -/// [other] -/// aws_access_key_id = 456 -/// ``` -pub async fn load(fs: &Fs, env: &Env) -> Result { - let source = source::load(&env, &fs).await; - ProfileSet::parse(source) -} - -/// A top-level configuration source containing multiple named profiles -#[derive(Debug, Eq, Clone, PartialEq)] -pub struct ProfileSet { - profiles: HashMap, - selected_profile: Cow<'static, str>, -} - -impl ProfileSet { - /// Create a new Profile set directly from a HashMap - /// - /// This method creates a ProfileSet directly from a hashmap with no normalization. - /// - /// ## Note - /// - /// This is probably not what you want! In general, [`load`](load) should be used instead - /// because it will perform input normalization. However, for tests which operate on the - /// normalized profile, this method exists to facilitate easy construction of a ProfileSet - pub fn new( - profiles: HashMap>, - selected_profile: impl Into>, - ) -> Self { - let mut base = ProfileSet::empty(); - base.selected_profile = selected_profile.into(); - for (name, profile) in profiles { - base.profiles.insert( - name.clone(), - Profile::new( - name, - profile - .into_iter() - .map(|(k, v)| (k.clone(), Property::new(k, v))) - .collect(), - ), - ); - } - base - } - - /// Retrieves a key-value pair from the currently selected profile - pub fn get(&self, key: &str) -> Option<&str> { - self.profiles - .get(self.selected_profile.as_ref()) - .and_then(|profile| profile.get(key)) - } - - /// Retrieve a named profile from the profile set - pub fn get_profile(&self, profile_name: &str) -> Option<&Profile> { - self.profiles.get(profile_name) - } - - pub fn selected_profile(&self) -> &str { - self.selected_profile.as_ref() - } - - /// Returns true if no profiles are contained in this profile set - pub fn is_empty(&self) -> bool { - self.profiles.is_empty() - } - - fn parse(source: Source) -> Result { - let mut base = ProfileSet::empty(); - base.selected_profile = source.profile; - - normalize::merge_in( - &mut base, - parse_profile_file(&source.config_file)?, - FileKind::Config, - ); - normalize::merge_in( - &mut base, - parse_profile_file(&source.credentials_file)?, - FileKind::Credentials, - ); - Ok(base) - } - - fn empty() -> Self { - Self { - profiles: Default::default(), - selected_profile: "default".into(), - } - } -} - -/// An individual configuration profile -/// -/// An AWS config may be composed of a multiple named profiles within a [`ProfileSet`](ProfileSet) -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Profile { - name: String, - properties: HashMap, -} - -impl Profile { - pub fn new(name: String, properties: HashMap) -> Self { - Self { name, properties } - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn get(&self, name: &str) -> Option<&str> { - self.properties.get(name).map(|prop| prop.value()) - } -} - -/// Key-Value property pair -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Property { - key: String, - value: String, -} - -impl Property { - pub fn value(&self) -> &str { - &self.value - } - - pub fn key(&self) -> &str { - &self.key - } - - pub fn new(key: String, value: String) -> Self { - Property { key, value } - } -} - -#[cfg(test)] -mod test { - use crate::profile::source::{File, Source}; - use crate::profile::ProfileSet; - use arbitrary::{Arbitrary, Unstructured}; - use serde::Deserialize; - use std::collections::HashMap; - use std::error::Error; - use std::fs; - use tracing_test::traced_test; - - /// Run all tests from profile-parser-tests.json - /// - /// These represent the bulk of the test cases and reach effectively 100% coverage - #[test] - #[traced_test] - fn run_tests() -> Result<(), Box> { - let tests = fs::read_to_string("test-data/profile-parser-tests.json")?; - let tests: ParserTests = serde_json::from_str(&tests)?; - for (i, test) in tests.tests.into_iter().enumerate() { - eprintln!("test: {}", i); - check(test); - } - Ok(()) - } - - #[test] - fn empty_source_empty_profile() { - let source = Source { - config_file: File { - path: "~/.aws/config".to_string(), - contents: "".into(), - }, - credentials_file: File { - path: "~/.aws/credentials".to_string(), - contents: "".into(), - }, - profile: "default".into(), - }; - let profile_set = ProfileSet::parse(source).expect("empty profiles are valid"); - assert_eq!(profile_set.is_empty(), true); - } - - /// Run all tests from the fuzzing corpus to validate coverage - #[test] - #[ignore] - fn run_fuzz_tests() -> Result<(), Box> { - let fuzz_corpus = fs::read_dir("fuzz/corpus/profile-parser")? - .map(|res| res.map(|entry| entry.path())) - .collect::, _>>()?; - for file in fuzz_corpus { - let raw = fs::read(file)?; - let mut unstructured = Unstructured::new(&raw); - let (conf, creds): (Option<&str>, Option<&str>) = - Arbitrary::arbitrary(&mut unstructured)?; - let profile_source = Source { - config_file: File { - path: "~/.aws/config".to_string(), - contents: conf.unwrap_or_default().to_string(), - }, - credentials_file: File { - path: "~/.aws/config".to_string(), - contents: creds.unwrap_or_default().to_string(), - }, - profile: "default".into(), - }; - // don't care if parse fails, just don't panic - let _ = ProfileSet::parse(profile_source); - } - - Ok(()) - } - - // for test comparison purposes, flatten a profile into a hashmap - fn flatten(profile: ProfileSet) -> HashMap> { - profile - .profiles - .into_iter() - .map(|(_name, profile)| { - ( - profile.name, - profile - .properties - .into_iter() - .map(|(_, prop)| (prop.key, prop.value)) - .collect(), - ) - }) - .collect() - } - - fn make_source(input: ParserInput) -> Source { - Source { - config_file: File { - path: "~/.aws/config".to_string(), - contents: input.config_file.unwrap_or_default(), - }, - credentials_file: File { - path: "~/.aws/credentials".to_string(), - contents: input.credentials_file.unwrap_or_default(), - }, - profile: "default".into(), - } - } - - // wrapper to generate nicer errors during test failure - fn check(test_case: ParserTest) { - let copy = test_case.clone(); - let parsed = ProfileSet::parse(make_source(test_case.input)); - let res = match (parsed.map(flatten), &test_case.output) { - (Ok(actual), ParserOutput::Profiles(expected)) if &actual != expected => Err(format!( - "mismatch:\nExpected: {:#?}\nActual: {:#?}", - expected, actual - )), - (Ok(_), ParserOutput::Profiles(_)) => Ok(()), - (Err(msg), ParserOutput::ErrorContaining(substr)) => { - if format!("{}", msg).contains(substr) { - Ok(()) - } else { - Err(format!("Expected {} to contain {}", msg, substr)) - } - } - (Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!( - "expected an error: {} but parse succeeded:\n{:#?}", - err, output - )), - (Err(err), ParserOutput::Profiles(_expected)) => { - Err(format!("Expected to succeed but got: {}", err)) - } - }; - if let Err(e) = res { - eprintln!("Test case failed: {:#?}", copy); - eprintln!("failure: {}", e); - panic!("test failed") - } - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct ParserTests { - tests: Vec, - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(rename_all = "camelCase")] - struct ParserTest { - name: String, - input: ParserInput, - output: ParserOutput, - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(rename_all = "camelCase")] - enum ParserOutput { - Profiles(HashMap>), - ErrorContaining(String), - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(rename_all = "camelCase")] - struct ParserInput { - config_file: Option, - credentials_file: Option, - } -} diff --git a/aws/rust-runtime/aws-types/src/profile/normalize.rs b/aws/rust-runtime/aws-types/src/profile/normalize.rs deleted file mode 100644 index 702d996f5a..0000000000 --- a/aws/rust-runtime/aws-types/src/profile/normalize.rs +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use crate::profile::parse::{RawProfileSet, WHITESPACE}; -use crate::profile::source::FileKind; -use crate::profile::{Profile, ProfileSet, Property}; -use std::borrow::Cow; -use std::collections::HashMap; - -const DEFAULT: &str = "default"; -const PROFILE_PREFIX: &str = "profile"; - -#[derive(Eq, PartialEq, Hash, Debug)] -struct ProfileName<'a> { - name: &'a str, - has_profile_prefix: bool, -} - -impl ProfileName<'_> { - fn parse(input: &str) -> ProfileName { - let input = input.trim_matches(WHITESPACE); - let (name, has_profile_prefix) = match input.strip_prefix(PROFILE_PREFIX) { - // profilefoo isn't considered as having the profile prefix - Some(stripped) if stripped.starts_with(WHITESPACE) => (stripped.trim(), true), - _ => (input, false), - }; - ProfileName { - name, - has_profile_prefix, - } - } - - /// Validate a ProfileName for a given file key - /// - /// 1. `name` must ALWAYS be a valid identifier - /// 2. For Config files, the profile must either be `default` or it must have a profile prefix - /// 3. For credentials files, the profile name MUST NOT have a profile prefix - fn valid_for(self, kind: FileKind) -> Result { - if validate_identifier(self.name).is_err() { - return Err(format!( - "profile `{}` ignored because `{}` was not a valid identifier", - &self.name, &self.name - )); - } - match (self.name, kind, self.has_profile_prefix) { - (_, FileKind::Config, true) => Ok(self), - (DEFAULT, FileKind::Config, false) => Ok(self), - (_not_default, FileKind::Config, false) => Err(format!( - "profile `{}` ignored because config profiles must be of the form `[profile ]`", - self.name - )), - (_, FileKind::Credentials, true) => Err(format!( - "profile `{}` ignored because credential profiles must NOT begin with `profile`", - self.name - )), - (_, FileKind::Credentials, false) => Ok(self), - } - } -} - -/// Normalize a raw profile into a `MergedProfile` -/// -/// This function follows the following rules, codified in the tests & the reference Java implementation -/// - When the profile is a config file, strip `profile` and trim whitespace (`profile foo` => `foo`) -/// - Profile names are validated (see `validate_profile_name`) -/// - A profile named `profile default` takes priority over a profile named `default`. -/// - Profiles with identical names are merged -pub fn merge_in(base: &mut ProfileSet, raw_profile_set: RawProfileSet, kind: FileKind) { - // parse / validate profile names - let validated_profiles = raw_profile_set - .into_iter() - .map(|(name, profile)| (ProfileName::parse(name).valid_for(kind), profile)); - - // remove invalid profiles & emit warning - // valid_profiles contains only valid profiles but it may contain `[profile default]` and `[default]` - // which must be filtered later - let valid_profiles = validated_profiles - .filter_map(|(name, profile)| match name { - Ok(profile_name) => Some((profile_name, profile)), - Err(e) => { - tracing::warn!("{}", e); - None - } - }) - .collect::>(); - // if a `[profile default]` exists then we should ignore `[default]` - let ignore_unprefixed_default = valid_profiles - .iter() - .any(|(profile, _)| profile.name == DEFAULT && profile.has_profile_prefix); - - for (profile_name, raw_profile) in valid_profiles { - // When normalizing profiles, profiles should be merged. However, `[profile default]` and - // `[default]` are considered two separate profiles. Furthermore, `[profile default]` fully - // replaces any contents of `[default]`! - // for each profile in the raw set of profiles, normalize, then merge it into the base set - if ignore_unprefixed_default - && profile_name.name == DEFAULT - && !profile_name.has_profile_prefix - { - tracing::warn!("profile `default` ignored because `[profile default]` was found which takes priority"); - continue; - } - let profile = base - .profiles - .entry(profile_name.name.to_string()) - .or_insert_with(|| Profile::new(profile_name.name.to_string(), Default::default())); - merge_into_base(profile, raw_profile) - } -} - -fn merge_into_base<'a>(target: &mut Profile, profile: HashMap<&str, Cow<'a, str>>) { - for (k, v) in profile { - match validate_identifier(&k) { - Ok(k) => { - target - .properties - .insert(k.to_owned(), Property::new(k.to_owned(), v.into())); - } - Err(_) => { - tracing::warn!(profile = %&target.name, key = ?k, "key ignored because `{}` was not a valid identifier", k); - } - } - } -} - -/// Validate that a string is a valid identifier -/// -/// Identifiers must match `[A-Za-z0-9\-_]+` -fn validate_identifier(input: &str) -> Result<&str, ()> { - input - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '\\') - .then(|| input) - .ok_or(()) -} - -#[cfg(test)] -mod tests { - use crate::profile::normalize::{merge_in, ProfileName}; - use crate::profile::parse::RawProfileSet; - use crate::profile::source::FileKind; - use crate::profile::ProfileSet; - use std::collections::HashMap; - use tracing_test::traced_test; - - #[test] - fn profile_name_parsing() { - assert_eq!( - ProfileName::parse("profile name"), - ProfileName { - name: "name", - has_profile_prefix: true - } - ); - assert_eq!( - ProfileName::parse("name"), - ProfileName { - name: "name", - has_profile_prefix: false - } - ); - assert_eq!( - ProfileName::parse("profile\tname"), - ProfileName { - name: "name", - has_profile_prefix: true - } - ); - assert_eq!( - ProfileName::parse("profile name "), - ProfileName { - name: "name", - has_profile_prefix: true - } - ); - assert_eq!( - ProfileName::parse("profilename"), - ProfileName { - name: "profilename", - has_profile_prefix: false - } - ); - assert_eq!( - ProfileName::parse(" whitespace "), - ProfileName { - name: "whitespace", - has_profile_prefix: false - } - ); - } - - #[test] - #[traced_test] - fn ignored_key_generates_warning() { - let mut profile: RawProfileSet = HashMap::new(); - profile.insert("default", { - let mut out = HashMap::new(); - out.insert("invalid key", "value".into()); - out - }); - let mut base = ProfileSet::empty(); - merge_in(&mut base, profile, FileKind::Config); - assert!(base - .get_profile("default") - .expect("contains default profile") - .properties - .is_empty()); - assert!(logs_contain( - "key ignored because `invalid key` was not a valid identifier" - )); - } - - #[test] - #[traced_test] - fn invalid_profile_generates_warning() { - let mut profile: RawProfileSet = HashMap::new(); - profile.insert("foo", HashMap::new()); - merge_in(&mut ProfileSet::empty(), profile, FileKind::Config); - assert!(logs_contain("profile `foo` ignored")); - } -} diff --git a/aws/rust-runtime/aws-types/src/profile/parse.rs b/aws/rust-runtime/aws-types/src/profile/parse.rs deleted file mode 100644 index 092dc92537..0000000000 --- a/aws/rust-runtime/aws-types/src/profile/parse.rs +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -//! Profile file parsing -//! -//! This file implements profile file parsing at a very literal level. Prior to actually being used, -//! profiles must be normalized into a canonical form. Constructions that will eventually be -//! deemed invalid are accepted during parsing such as: -//! - keys that are invalid identifiers: `a b = c` -//! - profiles with invalid names -//! - profile name normalization (`profile foo` => `foo`) - -use crate::profile::source::File; -use std::borrow::Cow; -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{self, Display, Formatter}; - -/// A set of profiles that still carries a reference to the underlying data -pub type RawProfileSet<'a> = HashMap<&'a str, HashMap<&'a str, Cow<'a, str>>>; - -/// Characters considered to be whitespace by the spec -/// -/// Profile parsing is actually quite strict about what is and is not whitespace, so use this instead -/// of `.is_whitespace()` / `.trim()` -pub const WHITESPACE: &[char] = &[' ', '\t']; -const COMMENT: &[char] = &['#', ';']; - -/// Location for use during error reporting -#[derive(Clone, Debug, Eq, PartialEq)] -struct Location { - line_number: usize, - path: String, -} - -/// An error encountered while parsing a profile -#[derive(Debug)] -pub struct ProfileParseError { - /// Location where this error occurred - location: Location, - - /// Error message - message: String, -} - -impl Display for ProfileParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "error parsing {} on line {}:\n {}", - self.location.path, self.location.line_number, self.message - ) - } -} - -impl Error for ProfileParseError {} - -/// Validate that a line represents a valid subproperty -/// -/// - Subproperties looks like regular properties (`k=v`) that are nested within an existing property. -/// - Subproperties must be validated for compatibility with other SDKs, but they are not actually -/// parsed into structured data. -fn validate_subproperty(value: &str, location: Location) -> Result<(), ProfileParseError> { - if value.trim_matches(WHITESPACE).is_empty() { - Ok(()) - } else { - parse_property_line(value) - .map_err(|err| err.into_error("sub-property", location)) - .map(|_| ()) - } -} - -fn is_empty_line(line: &str) -> bool { - line.trim_matches(WHITESPACE).is_empty() -} - -fn is_comment_line(line: &str) -> bool { - line.starts_with(COMMENT) -} - -/// Parser for profile files -struct Parser<'a> { - /// In-progress profile representation - data: RawProfileSet<'a>, - - /// Parser state - state: State<'a>, - - /// Parser source location - /// - /// Location is tracked to facilitate error reporting - location: Location, -} - -enum State<'a> { - Starting, - ReadingProfile { - profile: &'a str, - property: Option<&'a str>, - is_subproperty: bool, - }, -} - -/// Parse `file` into a `RawProfileSet` -pub fn parse_profile_file(file: &File) -> Result { - let mut parser = Parser { - data: HashMap::new(), - state: State::Starting, - location: Location { - line_number: 0, - path: file.path.to_string(), - }, - }; - parser.parse_profile(&file.contents)?; - Ok(parser.data) -} - -impl<'a> Parser<'a> { - /// Parse `file` containing profile data into `self.data`. - fn parse_profile(&mut self, file: &'a str) -> Result<(), ProfileParseError> { - for (line_number, line) in file.lines().enumerate() { - self.location.line_number = line_number + 1; // store a 1-indexed line number - if is_empty_line(line) || is_comment_line(line) { - continue; - } - if line.starts_with('[') { - self.read_profile_line(line)?; - } else if line.starts_with(WHITESPACE) { - self.read_property_continuation(line)?; - } else { - self.read_property_line(line)?; - } - } - Ok(()) - } - - /// Parse a property line like `a = b` - /// - /// A property line is only valid when we're within a profile definition, `[profile foo]` - fn read_property_line(&mut self, line: &'a str) -> Result<(), ProfileParseError> { - let location = &self.location; - let (current_profile, name) = match &self.state { - State::Starting => return Err(self.make_error("Expected a profile definition")), - State::ReadingProfile { profile, .. } => ( - self.data.get_mut(*profile).expect("profile must exist"), - profile, - ), - }; - let (k, v) = parse_property_line(line) - .map_err(|err| err.into_error("property", location.clone()))?; - self.state = State::ReadingProfile { - profile: name, - property: Some(k), - is_subproperty: v.is_empty(), - }; - current_profile.insert(k, v.into()); - Ok(()) - } - - /// Create a location-tagged error message - fn make_error(&self, message: &str) -> ProfileParseError { - ProfileParseError { - location: self.location.clone(), - message: message.into(), - } - } - - /// Parse the lines of a property after the first line. - /// - /// This is triggered by lines that start with whitespace. - fn read_property_continuation(&mut self, line: &'a str) -> Result<(), ProfileParseError> { - let current_property = match &self.state { - State::Starting => return Err(self.make_error("Expected a profile definition")), - State::ReadingProfile { - profile, - property: Some(property), - is_subproperty, - } => { - if *is_subproperty { - validate_subproperty(line, self.location.clone())?; - } - self.data - .get_mut(*profile) - .expect("profile must exist") - .get_mut(*property) - .expect("property must exist") - } - State::ReadingProfile { - profile: _, - property: None, - .. - } => return Err(self.make_error("Expected a property definition, found continuation")), - }; - let line = line.trim_matches(WHITESPACE); - let current_property = current_property.to_mut(); - current_property.push('\n'); - current_property.push_str(line); - Ok(()) - } - - fn read_profile_line(&mut self, line: &'a str) -> Result<(), ProfileParseError> { - let line = prepare_line(line, false); - let profile_name = line - .strip_prefix('[') - .ok_or_else(|| self.make_error("Profile definition must start with ]"))? - .strip_suffix(']') - .ok_or_else(|| self.make_error("Profile definition must end with ']'"))?; - if !self.data.contains_key(profile_name) { - self.data.insert(profile_name, Default::default()); - } - self.state = State::ReadingProfile { - profile: profile_name, - property: None, - is_subproperty: false, - }; - Ok(()) - } -} - -/// Error encountered while parsing a property -#[derive(Debug, Eq, PartialEq)] -enum PropertyError { - NoEquals, - NoName, -} - -impl PropertyError { - fn into_error(self, ctx: &str, location: Location) -> ProfileParseError { - let mut ctx = ctx.to_string(); - match self { - PropertyError::NoName => { - ctx.get_mut(0..1).unwrap().make_ascii_uppercase(); - ProfileParseError { - location, - message: format!("{} did not have a name", ctx), - } - } - PropertyError::NoEquals => ProfileParseError { - location, - message: format!("Expected an '=' sign defining a {}", ctx), - }, - } - } -} - -/// Parse a property line into a key-value pair -fn parse_property_line(line: &str) -> Result<(&str, &str), PropertyError> { - let line = prepare_line(line, true); - let (k, v) = line.split_once('=').ok_or(PropertyError::NoEquals)?; - let k = k.trim_matches(WHITESPACE); - let v = v.trim_matches(WHITESPACE); - if k.is_empty() { - return Err(PropertyError::NoName); - } - Ok((k, v)) -} - -/// Prepare a line for parsing -/// -/// Because leading whitespace is significant, this method should only be called after determining -/// whether a line represents a property (no whitespace) or a sub-property (whitespace). -/// This function preprocesses a line to simplify parsing: -/// 1. Strip leading and trailing whitespace -/// 2. Remove trailing comments -/// -/// Depending on context, comment characters may need to be preceded by whitespace to be considered -/// comments. -fn prepare_line(line: &str, comments_need_whitespace: bool) -> &str { - let line = line.trim_matches(WHITESPACE); - let mut prev_char_whitespace = false; - let mut comment_idx = None; - for (idx, chr) in line.char_indices() { - if (COMMENT.contains(&chr)) && (prev_char_whitespace || !comments_need_whitespace) { - comment_idx = Some(idx); - break; - } - prev_char_whitespace = chr.is_whitespace(); - } - comment_idx - .map(|idx| &line[..idx]) - .unwrap_or(&line) - // trimming the comment might result in more whitespace that needs to be handled - .trim_matches(WHITESPACE) -} - -#[cfg(test)] -mod test { - use super::{parse_profile_file, prepare_line, Location}; - use crate::profile::parse::{parse_property_line, PropertyError}; - use crate::profile::source::File; - - // most test cases covered by the JSON test suite - - #[test] - fn property_parsing() { - assert_eq!(parse_property_line("a = b"), Ok(("a", "b"))); - assert_eq!(parse_property_line("a=b"), Ok(("a", "b"))); - assert_eq!(parse_property_line("a = b "), Ok(("a", "b"))); - assert_eq!(parse_property_line(" a = b "), Ok(("a", "b"))); - assert_eq!(parse_property_line(" a = b 🐱 "), Ok(("a", "b 🐱"))); - assert_eq!(parse_property_line("a b"), Err(PropertyError::NoEquals)); - assert_eq!(parse_property_line("= b"), Err(PropertyError::NoName)); - assert_eq!(parse_property_line("a = "), Ok(("a", ""))); - assert_eq!( - parse_property_line("something_base64=aGVsbG8gZW50aHVzaWFzdGljIHJlYWRlcg=="), - Ok(("something_base64", "aGVsbG8gZW50aHVzaWFzdGljIHJlYWRlcg==")) - ); - } - - #[test] - fn prepare_line_strips_comments() { - assert_eq!( - prepare_line("name = value # Comment with # sign", true), - "name = value" - ); - - assert_eq!( - prepare_line("name = value#Comment # sign", true), - "name = value#Comment" - ); - - assert_eq!( - prepare_line("name = value#Comment # sign", false), - "name = value" - ); - } - - #[test] - fn error_line_numbers() { - let file = File { - path: "~/.aws/config".into(), - contents: "[default\nk=v".into(), - }; - let err = parse_profile_file(&file).expect_err("parsing should fail"); - assert_eq!(err.message, "Profile definition must end with ']'"); - assert_eq!( - err.location, - Location { - path: "~/.aws/config".into(), - line_number: 1 - } - ) - } -} diff --git a/aws/rust-runtime/aws-types/src/profile/source.rs b/aws/rust-runtime/aws-types/src/profile/source.rs deleted file mode 100644 index 4d0034526b..0000000000 --- a/aws/rust-runtime/aws-types/src/profile/source.rs +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use crate::os_shim_internal; -use std::borrow::Cow; -use std::io::ErrorKind; -use std::path::{Component, Path, PathBuf}; -use tracing::Instrument; - -/// In-memory source of profile data -pub struct Source { - /// Contents and path of ~/.aws/config - pub config_file: File, - - /// Contents and path of ~/.aws/credentials - pub credentials_file: File, - - /// Profile to use - /// - /// Overridden via `$AWS_PROFILE`, defaults to `default` - pub profile: Cow<'static, str>, -} - -/// In-memory configuration file -pub struct File { - pub path: String, - pub contents: String, -} - -#[derive(Clone, Copy)] -pub enum FileKind { - Config, - Credentials, -} - -impl FileKind { - fn default_path(&self) -> &'static str { - match &self { - FileKind::Credentials => "~/.aws/credentials", - FileKind::Config => "~/.aws/config", - } - } - - fn override_environment_variable(&self) -> &'static str { - match &self { - FileKind::Config => "AWS_CONFIG_FILE", - FileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE", - } - } -} - -/// Load a [Source](Source) from a given environment and filesystem. -pub async fn load(proc_env: &os_shim_internal::Env, fs: &os_shim_internal::Fs) -> Source { - let home = home_dir(&proc_env, Os::real()); - let config = load_config_file(FileKind::Config, &home, &fs, &proc_env) - .instrument(tracing::info_span!("load_config_file")) - .await; - let credentials = load_config_file(FileKind::Credentials, &home, &fs, &proc_env) - .instrument(tracing::info_span!("load_credentials_file")) - .await; - - Source { - config_file: config, - credentials_file: credentials, - profile: proc_env - .get("AWS_PROFILE") - .map(Cow::Owned) - .unwrap_or(Cow::Borrowed("default")), - } -} - -/// Loads an AWS Config file -/// -/// Both the default & the overriding patterns may contain `~/` which MUST be expanded to the users -/// home directory in a platform-aware way (see [`expand_home`](expand_home)) -/// -/// Arguments: -/// * `kind`: The type of config file to load -/// * `home_directory`: Home directory to use during home directory expansion -/// * `fs`: Filesystem abstraction -/// * `environment`: Process environment abstraction -async fn load_config_file( - kind: FileKind, - home_directory: &Option, - fs: &os_shim_internal::Fs, - environment: &os_shim_internal::Env, -) -> File { - let path = environment - .get(kind.override_environment_variable()) - .map(Cow::Owned) - .ok() - .unwrap_or_else(|| kind.default_path().into()); - let expanded = expand_home(path.as_ref(), home_directory); - if path != expanded.to_string_lossy() { - tracing::debug!(before = ?path, after = ?expanded, "home directory expanded"); - } - // read the data at the specified path - // if the path does not exist, log a warning but pretend it was actually an empty file - let data = match fs.read_to_end(&expanded).await { - Ok(data) => data, - Err(e) => { - match e.kind() { - ErrorKind::NotFound if path == kind.default_path() => { - tracing::info!(path = %path, "config file not found") - } - ErrorKind::NotFound if path != kind.default_path() => { - // in the case where the user overrode the path with an environment variable, - // log more loudly than the case where the default path was missing - tracing::warn!(path = %path, env = %kind.override_environment_variable(), "config file overridden via environment variable not found") - } - _other => tracing::warn!(path = %path, error = %e, "failed to read config file"), - }; - Default::default() - } - }; - // if the file is not valid utf-8, log a warning and use an empty file instead - let data = match String::from_utf8(data) { - Ok(data) => data, - Err(e) => { - tracing::warn!(path = %path, error = %e, "config file did not contain utf-8 encoded data"); - Default::default() - } - }; - tracing::info!(path = %path, size = ?data.len(), "config file loaded"); - File { - // lossy is OK here, the name of this file is just for debugging purposes - path: expanded.to_string_lossy().into(), - contents: data, - } -} - -fn expand_home(path: impl AsRef, home_dir: &Option) -> PathBuf { - let path = path.as_ref(); - let mut components = path.components(); - let start = components.next(); - match start { - None => path.into(), // empty path, - Some(Component::Normal(s)) if s == "~" => { - // do homedir replacement - let path = match home_dir { - Some(dir) => { - tracing::debug!(home = ?dir, path = ?path, "performing home directory substitution"); - dir.clone() - } - None => { - tracing::warn!( - "could not determine home directory but home expansion was requested" - ); - // if we can't determine the home directory, just leave it as `~` - "~".into() - } - }; - let mut path: PathBuf = path.into(); - // rewrite the path using system-specific path separators - for component in components { - path.push(component); - } - path - } - // Finally, handle the case where it doesn't begin with some version of `~/`: - // NOTE: in this case we aren't performing path rewriting. This is correct because - // this path comes from an environment variable on the target - // platform, so in that case, the separators should already be correct. - _other => path.into(), - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum Os { - Windows, - NotWindows, -} - -impl Os { - pub fn real() -> Self { - match std::env::consts::OS { - "windows" => Os::Windows, - _ => Os::NotWindows, - } - } -} - -/// Resolve a home directory given a set of environment variables -fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option { - if let Ok(home) = env_var.get("HOME") { - tracing::debug!(src = "HOME", "loaded home directory"); - return Some(home); - } - - if os == Os::Windows { - if let Ok(home) = env_var.get("USERPROFILE") { - tracing::debug!(src = "USERPROFILE", "loaded home directory"); - return Some(home); - } - - let home_drive = env_var.get("HOMEDRIVE"); - let home_path = env_var.get("HOMEPATH"); - tracing::debug!(src = "HOMEDRIVE/HOMEPATH", "loaded home directory"); - if let (Ok(mut drive), Ok(path)) = (home_drive, home_path) { - drive.push_str(&path); - return Some(drive); - } - } - None -} - -#[cfg(test)] -mod tests { - use crate::os_shim_internal::{Env, Fs}; - use crate::profile::source::{expand_home, home_dir, load, Os}; - use serde::Deserialize; - use std::collections::HashMap; - use std::error::Error; - use std::fs; - - #[test] - fn only_expand_home_prefix() { - // ~ is only expanded as a single component (currently) - let path = "~aws/config"; - assert_eq!(expand_home(&path, &None).to_str().unwrap(), "~aws/config"); - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct SourceTests { - tests: Vec, - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct TestCase { - name: String, - environment: HashMap, - platform: String, - profile: Option, - config_location: String, - credentials_location: String, - } - - /// Run all tests from file-location-tests.json - #[test] - fn run_tests() -> Result<(), Box> { - let tests = fs::read_to_string("test-data/file-location-tests.json")?; - let tests: SourceTests = serde_json::from_str(&tests)?; - for (i, test) in tests.tests.into_iter().enumerate() { - eprintln!("test: {}", i); - check(test) - .now_or_never() - .expect("these futures should never poll"); - } - Ok(()) - } - - use futures_util::FutureExt; - use tracing_test::traced_test; - - #[traced_test] - #[test] - fn logs_produced_default() { - let env = Env::from_slice(&[("HOME", "/user/name")]); - let mut fs = HashMap::new(); - fs.insert( - "/user/name/.aws/config".to_string(), - "[default]\nregion = us-east-1".into(), - ); - - let fs = Fs::from_map(fs); - - let _src = load(&env, &fs).now_or_never(); - assert!(logs_contain("config file loaded")); - assert!(logs_contain("performing home directory substitution")); - } - - async fn check(test_case: TestCase) { - let fs = Fs::real(); - let env = Env::from(test_case.environment); - let platform_matches = (cfg!(windows) && test_case.platform == "windows") - || (!cfg!(windows) && test_case.platform != "windows"); - if platform_matches { - let source = load(&env, &fs).await; - if let Some(expected_profile) = test_case.profile { - assert_eq!(source.profile, expected_profile, "{}", &test_case.name); - } - assert_eq!( - source.config_file.path, test_case.config_location, - "{}", - &test_case.name - ); - assert_eq!( - source.credentials_file.path, test_case.credentials_location, - "{}", - &test_case.name - ) - } else { - println!( - "NOTE: ignoring test case for {} which does not apply to our platform: \n {}", - &test_case.platform, &test_case.name - ) - } - } - - #[test] - #[cfg_attr(windows, ignore)] - fn test_expand_home() { - let path = "~/.aws/config"; - assert_eq!( - expand_home(&path, &Some("/user/foo".to_string())) - .to_str() - .unwrap(), - "/user/foo/.aws/config" - ); - } - - #[test] - fn homedir_profile_only_windows() { - // windows specific variables should only be considered when the platform is windows - let env = Env::from_slice(&[("USERPROFILE", "C:\\Users\\name")]); - assert_eq!( - home_dir(&env, Os::Windows), - Some("C:\\Users\\name".to_string()) - ); - assert_eq!(home_dir(&env, Os::NotWindows), None); - } - - #[test] - fn expand_home_no_home() { - // there is an edge case around expansion when no home directory exists - // if no home directory can be determined, leave the path as is - if !cfg!(windows) { - assert_eq!(expand_home("~/config", &None).to_str().unwrap(), "~/config") - } else { - assert_eq!( - expand_home("~/config", &None).to_str().unwrap(), - "~\\config" - ) - } - } - - /// Test that a linux oriented path expands on windows - #[test] - #[cfg_attr(not(windows), ignore)] - fn test_expand_home_windows() { - let path = "~/.aws/config"; - assert_eq!( - expand_home(&path, &Some("C:\\Users\\name".to_string())) - .to_str() - .unwrap(), - "C:\\Users\\name\\.aws\\config" - ); - } -} diff --git a/aws/rust-runtime/aws-types/test-data/file-location-tests.json b/aws/rust-runtime/aws-types/test-data/file-location-tests.json deleted file mode 100644 index 81d32cc604..0000000000 --- a/aws/rust-runtime/aws-types/test-data/file-location-tests.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "description": [ - "These are test descriptions that specify which files and profiles should be loaded based on the specified environment ", - "variables.", - "See 'file-location-tests.schema.json' for a description of this file's structure." - ], - - "tests": [ - { - "name": "User home is loaded from $HOME with highest priority on non-windows platforms.", - "environment": { - "HOME": "/home/user", - "USERPROFILE": "ignored", - "HOMEDRIVE": "ignored", - "HOMEPATH": "ignored" - }, - "platform": "linux", - "profile": "default", - "configLocation": "/home/user/.aws/config", - "credentialsLocation": "/home/user/.aws/credentials" - }, - - { - "name": "User home is loaded from $HOME with highest priority on windows platforms.", - "environment": { - "HOME": "C:\\users\\user", - "USERPROFILE": "ignored", - "HOMEDRIVE": "ignored", - "HOMEPATH": "ignored" - }, - "platform": "windows", - "profile": "default", - "configLocation": "C:\\users\\user\\.aws\\config", - "credentialsLocation": "C:\\users\\user\\.aws\\credentials" - }, - - { - "name": "User home is loaded from $USERPROFILE on windows platforms when $HOME is not set.", - "environment": { - "USERPROFILE": "C:\\users\\user", - "HOMEDRIVE": "ignored", - "HOMEPATH": "ignored" - }, - "platform": "windows", - "profile": "default", - "configLocation": "C:\\users\\user\\.aws\\config", - "credentialsLocation": "C:\\users\\user\\.aws\\credentials" - }, - - { - "name": "User home is loaded from $HOMEDRIVE$HOMEPATH on windows platforms when $HOME and $USERPROFILE are not set.", - "environment": { - "HOMEDRIVE": "C:", - "HOMEPATH": "\\users\\user" - }, - "platform": "windows", - "profile": "default", - "configLocation": "C:\\users\\user\\.aws\\config", - "credentialsLocation": "C:\\users\\user\\.aws\\credentials" - }, - - { - "name": "The default config location can be overridden by the user on non-windows platforms.", - "environment": { - "AWS_CONFIG_FILE": "/other/path/config", - "HOME": "/home/user" - }, - "platform": "linux", - "configLocation": "/other/path/config", - "credentialsLocation": "/home/user/.aws/credentials" - }, - - { - "name": "The default credentials location can be overridden by the user on non-windows platforms.", - "environment": { - "AWS_SHARED_CREDENTIALS_FILE": "/other/path/credentials", - "HOME": "/home/user" - }, - "platform": "linux", - "profile": "default", - "configLocation": "/home/user/.aws/config", - "credentialsLocation": "/other/path/credentials" - }, - - { - "name": "The default credentials location can be overridden by the user on windows platforms.", - "environment": { - "AWS_CONFIG_FILE": "C:\\other\\path\\config", - "HOME": "C:\\users\\user" - }, - "platform": "windows", - "profile": "default", - "configLocation": "C:\\other\\path\\config", - "credentialsLocation": "C:\\users\\user\\.aws\\credentials" - }, - - { - "name": "The default credentials location can be overridden by the user on windows platforms.", - "environment": { - "AWS_SHARED_CREDENTIALS_FILE": "C:\\other\\path\\credentials", - "HOME": "C:\\users\\user" - }, - "platform": "windows", - "profile": "default", - "configLocation": "C:\\users\\user\\.aws\\config", - "credentialsLocation": "C:\\other\\path\\credentials" - }, - - { - "name": "The default profile can be overridden via environment variable.", - "environment": { - "AWS_PROFILE": "other", - "HOME": "/home/user" - }, - "platform": "linux", - "profile": "other", - "configLocation": "/home/user/.aws/config", - "credentialsLocation": "/home/user/.aws/credentials" - } - ] -} diff --git a/aws/rust-runtime/aws-types/test-data/profile-parser-tests.json b/aws/rust-runtime/aws-types/test-data/profile-parser-tests.json deleted file mode 100644 index cd0d980a32..0000000000 --- a/aws/rust-runtime/aws-types/test-data/profile-parser-tests.json +++ /dev/null @@ -1,713 +0,0 @@ -{ - "description": [ - "These are test descriptions that describe how to convert a raw configuration and credentials file into an ", - "in-memory representation of the profile file.", - "See 'parser-tests.schema.json' for a description of this file's structure." - ], - "tests": [ - { - "name": "Empty files have no profiles.", - "input": { - "configFile": "" - }, - "output": { - "profiles": {} - } - }, - { - "name": "Empty profiles have no properties.", - "input": { - "configFile": "[profile foo]" - }, - "output": { - "profiles": { - "foo": {} - } - } - }, - { - "name": "Profile definitions must end with brackets.", - "input": { - "configFile": "[profile foo" - }, - "output": { - "errorContaining": "Profile definition must end with ']'" - } - }, - { - "name": "Profile names should be trimmed.", - "input": { - "configFile": "[profile \tfoo \t]" - }, - "output": { - "profiles": { - "foo": {} - } - } - }, - { - "name": "Tabs can separate profile names from profile prefix.", - "input": { - "configFile": "[profile\tfoo]" - }, - "output": { - "profiles": { - "foo": {} - } - } - }, - { - "name": "Properties must be defined in a profile.", - "input": { - "configFile": "name = value" - }, - "output": { - "errorContaining": "Expected a profile definition" - } - }, - { - "name": "Profiles can contain properties.", - "input": { - "configFile": "[profile foo]\nname = value" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - } - } - } - }, - { - "name": "Windows style line endings are supported.", - "input": { - "configFile": "[profile foo]\r\nname = value" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - } - } - } - }, - { - "name": "Equals signs are supported in property values.", - "input": { - "configFile": "[profile foo]\nname = val=ue" - }, - "output": { - "profiles": { - "foo": { - "name": "val=ue" - } - } - } - }, - { - "name": "Unicode characters are supported in property values.", - "input": { - "configFile": "[profile foo]\nname = 😂" - }, - "output": { - "profiles": { - "foo": { - "name": "😂" - } - } - } - }, - { - "name": "Profiles can contain multiple properties.", - "input": { - "configFile": "[profile foo]\nname = value\nname2 = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" - } - } - } - }, - { - "name": "Profiles can contain multiple properties.", - "input": { - "configFile": "[profile foo]\nname = value\nname2 = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" - } - } - } - }, - { - "name": "Property keys and values are trimmed.", - "input": { - "configFile": "[profile foo]\nname \t= \tvalue \t" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - } - } - } - }, - { - "name": "Property values can be empty.", - "input": { - "configFile": "[profile foo]\nname =" - }, - "output": { - "profiles": { - "foo": { - "name": "" - } - } - } - }, - { - "name": "Property key cannot be empty.", - "input": { - "configFile": "[profile foo]\n= value" - }, - "output": { - "errorContaining": "Property did not have a name" - } - }, - { - "name": "Property definitions must contain an equals sign.", - "input": { - "configFile": "[profile foo]\nkey : value" - }, - "output": { - "errorContaining": "Expected an '=' sign defining a property" - } - }, - { - "name": "Multiple profiles can be empty.", - "input": { - "configFile": "[profile foo]\n[profile bar]" - }, - "output": { - "profiles": { - "foo": {}, - "bar": {} - } - } - }, - { - "name": "Multiple profiles can have properties.", - "input": { - "configFile": "[profile foo]\nname = value\n[profile bar]\nname2 = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - }, - "bar": { - "name2": "value2" - } - } - } - }, - { - "name": "Blank lines are ignored.", - "input": { - "configFile": "\t \n[profile foo]\n\t\n \nname = value\n\t \n[profile bar]\n \t" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - }, - "bar": {} - } - } - }, - { - "name": "Pound sign comments are ignored.", - "input": { - "configFile": "# Comment\n[profile foo] # Comment\nname = value # Comment with # sign" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - } - } - } - }, - { - "name": "Semicolon sign comments are ignored.", - "input": { - "configFile": "; Comment\n[profile foo] ; Comment\nname = value ; Comment with ; sign" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - } - } - } - }, - { - "name": "All comment types can be used together.", - "input": { - "configFile": "# Comment\n[profile foo] ; Comment\nname = value # Comment with ; sign" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - } - } - } - }, - { - "name": "Comments can be empty.", - "input": { - "configFile": ";\n[profile foo];\nname = value ;\n" - }, - "output": { - "profiles": { - "foo": { - "name": "value" - } - } - } - }, - { - "name": "Comments can be adjacent to profile names.", - "input": { - "configFile": "[profile foo]; Adjacent semicolons\n[profile bar]# Adjacent pound signs" - }, - "output": { - "profiles": { - "foo": {}, - "bar": {} - } - } - }, - { - "name": "Comments adjacent to values are included in the value.", - "input": { - "configFile": "[profile foo]\nname = value; Adjacent semicolons\nname2 = value# Adjacent pound signs" - }, - "output": { - "profiles": { - "foo": { - "name": "value; Adjacent semicolons", - "name2": "value# Adjacent pound signs" - } - } - } - }, - { - "name": "Property values can be continued on the next line.", - "input": { - "configFile": "[profile foo]\nname = value\n -continued" - }, - "output": { - "profiles": { - "foo": { - "name": "value\n-continued" - } - } - } - }, - { - "name": "Property values can be continued with multiple lines.", - "input": { - "configFile": "[profile foo]\nname = value\n -continued\n -and-continued" - }, - "output": { - "profiles": { - "foo": { - "name": "value\n-continued\n-and-continued" - } - } - } - }, - { - "name": "Continuations are trimmed.", - "input": { - "configFile": "[profile foo]\nname = value\n \t -continued \t " - }, - "output": { - "profiles": { - "foo": { - "name": "value\n-continued" - } - } - } - }, - { - "name": "Continuation values include pound comments.", - "input": { - "configFile": "[profile foo]\nname = value\n -continued # Comment" - }, - "output": { - "profiles": { - "foo": { - "name": "value\n-continued # Comment" - } - } - } - }, - { - "name": "Continuation values include semicolon comments.", - "input": { - "configFile": "[profile foo]\nname = value\n -continued ; Comment" - }, - "output": { - "profiles": { - "foo": { - "name": "value\n-continued ; Comment" - } - } - } - }, - { - "name": "Continuations cannot be used outside of a profile.", - "input": { - "configFile": " -continued" - }, - "output": { - "errorContaining": "Expected a profile definition" - } - }, - { - "name": "Continuations cannot be used outside of a property.", - "input": { - "configFile": "[profile foo]\n -continued" - }, - "output": { - "errorContaining": "Expected a property definition" - } - }, - { - "name": "Continuations reset with profile definitions.", - "input": { - "configFile": "[profile foo]\nname = value\n[profile foo]\n -continued" - }, - "output": { - "errorContaining": "Expected a property definition" - } - }, - { - "name": "Duplicate profiles in the same file merge properties.", - "input": { - "configFile": "[profile foo]\nname = value\n[profile foo]\nname2 = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" - } - } - } - }, - { - "name": "Duplicate properties in a profile use the last one defined.", - "input": { - "configFile": "[profile foo]\nname = value\nname = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value2" - } - } - } - }, - { - "name": "Duplicate properties in duplicate profiles use the last one defined.", - "input": { - "configFile": "[profile foo]\nname = value\n[profile foo]\nname = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value2" - } - } - } - }, - { - "name": "Default profile with profile prefix overrides default profile without prefix when profile prefix is first.", - "input": { - "configFile": "[profile default]\nname = value\n[default]\nname2 = value2" - }, - "output": { - "profiles": { - "default": { - "name": "value" - } - } - } - }, - { - "name": "Default profile with profile prefix overrides default profile without prefix when profile prefix is last.", - "input": { - "configFile": "[default]\nname2 = value2\n[profile default]\nname = value" - }, - "output": { - "profiles": { - "default": { - "name": "value" - } - } - } - }, - { - "name": "Invalid profile names are ignored.", - "input": { - "configFile": "[profile in valid]\nname = value", - "credentialsFile": "[in valid 2]\nname2 = value2" - }, - "output": { - "profiles": {} - } - }, - { - "name": "Invalid property names are ignored.", - "input": { - "configFile": "[profile foo]\nin valid = value" - }, - "output": { - "profiles": { - "foo": {} - } - } - }, - { - "name": "All valid profile name characters are supported.", - "input": { - "configFile": "[profile ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_]" - }, - "output": { - "profiles": { - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_": {} - } - } - }, - { - "name": "All valid property name characters are supported.", - "input": { - "configFile": "[profile foo]\nABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ = value" - }, - "output": { - "profiles": { - "foo": { - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_": "value" - } - } - } - }, - { - "name": "Properties can have sub-properties.", - "input": { - "configFile": "[profile foo]\ns3 =\n name = value" - }, - "output": { - "profiles": { - "foo": { - "s3": "\nname = value" - } - } - } - }, - { - "name": "Invalid sub-property definitions cause an error.", - "input": { - "configFile": "[profile foo]\ns3 =\n invalid" - }, - "output": { - "errorContaining": "Expected an '=' sign defining a sub-property" - } - }, - { - "name": "Sub-property definitions can have an empty value.", - "input": { - "configFile": "[profile foo]\ns3 =\n name =" - }, - "output": { - "profiles": { - "foo": { - "s3": "\nname =" - } - } - } - }, - { - "name": "Sub-property definitions cannot have an empty name.", - "input": { - "configFile": "[profile foo]\ns3 =\n = value" - }, - "output": { - "errorContaining": "Sub-property did not have a name" - } - }, - { - "name": "Sub-property definitions can have an invalid name.", - "input": { - "configFile": "[profile foo]\ns3 =\n in valid = value" - }, - "output": { - "profiles": { - "foo": { - "s3": "\nin valid = value" - } - } - } - }, - { - "name": "Sub-properties can have blank lines that are ignored", - "input": { - "configFile": "[profile foo]\ns3 =\n name = value\n\t \n name2 = value2" - }, - "output": { - "profiles": { - "foo": { - "s3": "\nname = value\nname2 = value2" - } - } - } - }, - { - "name": "Profiles duplicated in multiple files are merged.", - "input": { - "configFile": "[profile foo]\nname = value", - "credentialsFile": "[foo]\nname2 = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value", - "name2": "value2" - } - } - } - }, - { - "name": "Default profiles with mixed prefixes in the config file ignore the one without prefix when merging.", - "input": { - "configFile": "[profile default]\nname = value\n[default]\nname2 = value2\n[profile default]\nname3 = value3" - }, - "output": { - "profiles": { - "default": { - "name": "value", - "name3": "value3" - } - } - } - }, - { - "name": "Default profiles with mixed prefixes merge with credentials", - "input": { - "configFile": "[profile default]\nname = value\n[default]\nname2 = value2\n[profile default]\nname3 = value3", - "credentialsFile": "[default]\nsecret=foo" - }, - "output": { - "profiles": { - "default": { - "name": "value", - "name3": "value3", - "secret": "foo" - } - } - } - }, - { - "name": "Duplicate properties between files uses credentials property.", - "input": { - "configFile": "[profile foo]\nname = value", - "credentialsFile": "[foo]\nname = value2" - }, - "output": { - "profiles": { - "foo": { - "name": "value2" - } - } - } - }, - { - "name": "Config profiles without prefix are ignored.", - "input": { - "configFile": "[foo]\nname = value" - }, - "output": { - "profiles": {} - } - }, - { - "name": "Credentials profiles with prefix are ignored.", - "input": { - "credentialsFile": "[profile foo]\nname = value" - }, - "output": { - "profiles": {} - } - }, - - { - "name": "Comment characters adjacent to profile decls", - "input": { - "configFile": "[profile foo]; semicolon\n[profile bar]# pound" - }, - "output": { - "profiles": { - "foo": {}, - "bar": {} - } - } - }, - { - "name": "Invalid continuation", - "input": { - "configFile": "[profile foo]\nname = value\n[profile foo]\n -continued" - }, - "output": { - "errorContaining": "Expected a property definition, found continuation" - } - }, - { - "name": "sneaky profile name", - "input": { - "configFile": "[profilefoo]\nname = value\n[profile bar]" - }, - "output": { - "profiles": { "bar": {} } - } - }, - - { - "name": "profile name with extra whitespace", - "input": { - "configFile": "[ profile foo ]\nname = value\n[profile bar]" - }, - "output": { - "profiles": { "bar": {}, "foo": {"name": "value"} } - } - }, - { - "name": "profile name with extra whitespace in credentials", - "input": { - "credentialsFile": "[ foo ]\nname = value\n[profile bar]" - }, - "output": { - "profiles": { "foo": {"name": "value"} } - } - } - ] -} From e126c5967708de0161e52e6e7e870dd94966cd03 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:15:35 -0400 Subject: [PATCH 09/18] Cleanups --- aws/rust-runtime/aws-config/src/lib.rs | 2 +- .../aws-config/src/meta/credentials/mod.rs | 11 ----------- aws/rust-runtime/aws-config/src/meta/mod.rs | 5 ----- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index df25efd6f2..360751e762 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -45,7 +45,7 @@ pub mod default_provider; /// Providers that load configuration from environment variables pub mod environment; -/// Meta-Providers that combine multiple providers into a single provider +/// Meta-providers that augment existing providers with new behavior #[cfg(feature = "meta")] pub mod meta; diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs index 96ff1a331e..eb3ac6a130 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs @@ -3,11 +3,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - mod chain; pub use chain::CredentialsProviderChain; @@ -16,9 +11,3 @@ pub use credential_fn::async_provide_credentials_fn; pub mod lazy_caching; pub use lazy_caching::LazyCachingCredentialsProvider; - -// pub mod credential_fn; -// pub mod lazy_caching; - -// mod cache; -// mod time; diff --git a/aws/rust-runtime/aws-config/src/meta/mod.rs b/aws/rust-runtime/aws-config/src/meta/mod.rs index 6704e1cb7d..786a11f7a8 100644 --- a/aws/rust-runtime/aws-config/src/meta/mod.rs +++ b/aws/rust-runtime/aws-config/src/meta/mod.rs @@ -8,8 +8,3 @@ pub mod region; /// Credential Providers pub mod credentials; - -// coming soon: -// pub mod credentials: -// - CredentialProviderChain -// - LazyCachingProvider From fbb7f52a443775150c3aa4665b817e6869e2332a Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:23:17 -0400 Subject: [PATCH 10/18] Fix some test failures --- aws/rust-runtime/aws-auth/src/middleware.rs | 3 --- aws/rust-runtime/aws-config/src/profile/mod.rs | 6 ------ aws/rust-runtime/aws-config/src/profile/parser/parse.rs | 5 ----- aws/rust-runtime/aws-hyper/tests/e2e_test.rs | 9 +++++++-- aws/rust-runtime/aws-types/src/credentials/mod.rs | 1 + aws/rust-runtime/aws-types/src/os_shim_internal.rs | 4 ++-- 6 files changed, 10 insertions(+), 18 deletions(-) diff --git a/aws/rust-runtime/aws-auth/src/middleware.rs b/aws/rust-runtime/aws-auth/src/middleware.rs index 299ae5f260..52804cda9a 100644 --- a/aws/rust-runtime/aws-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-auth/src/middleware.rs @@ -106,8 +106,6 @@ impl AsyncMapRequest for CredentialsStage { mod tests { use super::CredentialsStage; use crate::set_provider; - use aws_types::credential::provide_credentials::future; - use aws_types::credential::{CredentialsError, ProvideCredentials}; use aws_types::credentials::{ future, CredentialsError, ProvideCredentials, SharedCredentialsProvider, }; @@ -115,7 +113,6 @@ mod tests { use smithy_http::body::SdkBody; use smithy_http::middleware::AsyncMapRequest; use smithy_http::operation; - use std::sync::Arc; #[derive(Debug)] struct Unhandled; diff --git a/aws/rust-runtime/aws-config/src/profile/mod.rs b/aws/rust-runtime/aws-config/src/profile/mod.rs index 52a4433c66..41da7ea444 100644 --- a/aws/rust-runtime/aws-config/src/profile/mod.rs +++ b/aws/rust-runtime/aws-config/src/profile/mod.rs @@ -13,9 +13,3 @@ pub use parser::{load, Profile, ProfileSet, Property}; pub mod credentials; pub use credentials::ProfileFileCredentialsProvider; -/* -pub mod credential; - -pub mod region; - -pub use parser::{Profile, ProfileSet, Property};*/ diff --git a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs index 5f921692b9..d6a2ecf20d 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser/parse.rs @@ -3,11 +3,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - //! Profile file parsing //! //! This file implements profile file parsing at a very literal level. Prior to actually being used, diff --git a/aws/rust-runtime/aws-hyper/tests/e2e_test.rs b/aws/rust-runtime/aws-hyper/tests/e2e_test.rs index dfea286d22..a6fc151319 100644 --- a/aws/rust-runtime/aws-hyper/tests/e2e_test.rs +++ b/aws/rust-runtime/aws-hyper/tests/e2e_test.rs @@ -9,6 +9,7 @@ use aws_http::user_agent::AwsUserAgent; use aws_http::AwsErrorRetryPolicy; use aws_hyper::{Client, RetryConfig}; use aws_sig_auth::signer::OperationSigningConfig; +use aws_types::credentials::SharedCredentialsProvider; use aws_types::region::Region; use aws_types::Credentials; use aws_types::SigningService; @@ -87,9 +88,13 @@ fn test_operation() -> Operation { signature_versions: SignatureVersion::V4, }), ); - aws_auth::provider::set_provider( + aws_auth::set_provider( &mut conf, - Arc::new(Credentials::from_keys("access_key", "secret_key", None)), + SharedCredentialsProvider::new(Credentials::from_keys( + "access_key", + "secret_key", + None, + )), ); conf.insert(Region::new("test-region")); conf.insert(OperationSigningConfig::default_config()); diff --git a/aws/rust-runtime/aws-types/src/credentials/mod.rs b/aws/rust-runtime/aws-types/src/credentials/mod.rs index 498af09f8b..88f7731509 100644 --- a/aws/rust-runtime/aws-types/src/credentials/mod.rs +++ b/aws/rust-runtime/aws-types/src/credentials/mod.rs @@ -39,6 +39,7 @@ //! the trait implementation. //! ```rust //! use aws_types::credentials::{CredentialsError, Credentials, ProvideCredentials, future, self}; +//! #[derive(Debug)] //! struct SubprocessCredentialProvider; //! //! async fn invoke_command(command: &str) -> String { 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 ba527d0d2b..18b681b3dc 100644 --- a/aws/rust-runtime/aws-types/src/os_shim_internal.rs +++ b/aws/rust-runtime/aws-types/src/os_shim_internal.rs @@ -226,9 +226,9 @@ mod test { #[test] fn fs_works() { - let fs = Fs::from_test_dir("test-data", "/users/test-data"); + let fs = Fs::from_test_dir(".", "/users/test-data"); let _ = fs - .read_to_end("/users/test-data/file-location-tests.json") + .read_to_end("/users/test-data/Cargo.toml") .now_or_never() .expect("future should not poll") .expect("file exists"); From 713495ce3c486668a8eade86d89b85a00bb8376c Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:29:33 -0400 Subject: [PATCH 11/18] Fix docs --- aws/rust-runtime/aws-auth/src/middleware.rs | 4 ++-- aws/rust-runtime/aws-types/src/credentials/mod.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aws/rust-runtime/aws-auth/src/middleware.rs b/aws/rust-runtime/aws-auth/src/middleware.rs index 52804cda9a..7f27f06ecd 100644 --- a/aws/rust-runtime/aws-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-auth/src/middleware.rs @@ -8,8 +8,8 @@ use smithy_http::operation::Request; use std::future::Future; use std::pin::Pin; -/// Middleware stage that requests credentials from a [CredentialsProvider] and places them in -/// the property bag of the request. +/// Middleware stage that loads credentials from a [CredentialsProvider](aws_types::credentials::ProvideCredentials) +/// and places them in the property bag of the request. /// /// [CredentialsStage] implements [`AsyncMapRequest`](smithy_http::middleware::AsyncMapRequest), and: /// 1. Retrieves a `CredentialsProvider` from the property bag. diff --git a/aws/rust-runtime/aws-types/src/credentials/mod.rs b/aws/rust-runtime/aws-types/src/credentials/mod.rs index 88f7731509..bdfd65744c 100644 --- a/aws/rust-runtime/aws-types/src/credentials/mod.rs +++ b/aws/rust-runtime/aws-types/src/credentials/mod.rs @@ -11,8 +11,8 @@ //! implement your own credential provider. //! //! ### With static credentials -//! [`Credentials`](credentials::Credentials) implement -//! [`ProvideCredentials](provide_credentials::ProvideCredentials) directly, so no custom provider +//! [`Credentials`](crate::Credentials) implement +//! [`ProvideCredentials](crate::credentials::ProvideCredentials) directly, so no custom provider //! implementation is required: //! ```rust //! use aws_types::Credentials; @@ -34,7 +34,7 @@ //! ``` //! ### With dynamically loaded credentials //! If you are loading credentials dynamically, you can provide your own implementation of -//! [`ProvideCredentials`](provide_credentials::ProvideCredentials). Generally, this is best done by +//! [`ProvideCredentials`](crate::credentials::ProvideCredentials). Generally, this is best done by //! defining an inherent `async fn` on your structure, then calling that method directly from //! the trait implementation. //! ```rust From af86767526128535862b25e900fc751054a399f8 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:32:41 -0400 Subject: [PATCH 12/18] Fix select-object-content example --- aws/sdk/examples/s3/src/bin/select-object-content.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/aws/sdk/examples/s3/src/bin/select-object-content.rs b/aws/sdk/examples/s3/src/bin/select-object-content.rs index dba5733c03..bd96590d41 100644 --- a/aws/sdk/examples/s3/src/bin/select-object-content.rs +++ b/aws/sdk/examples/s3/src/bin/select-object-content.rs @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -use aws_auth_providers::DefaultProviderChain; use aws_config::meta::region::RegionProviderChain; use aws_sdk_s3::model::{ CompressionType, CsvInput, CsvOutput, ExpressionType, FileHeaderInfo, InputSerialization, @@ -57,6 +56,7 @@ async fn main() -> Result<(), Error> { .or_default_provider() .or_else(Region::new("us-east-2")); let shared_config = aws_config::from_env().region(region_provider).load().await; + let client = Client::new(&shared_config); println!(); @@ -66,16 +66,6 @@ async fn main() -> Result<(), Error> { println!(); } - let credential_provider = DefaultProviderChain::builder() - .region(shared_config.region().unwrap().clone()) - .build(); - - let config = aws_sdk_s3::config::Builder::from(&shared_config) - .credentials_provider(credential_provider) - .build(); - - let client = Client::from_conf(config); - let mut output = client .select_object_content() .bucket(bucket) From 2584101ace45811b07699dd61a7a2c893a47de65 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:54:22 -0400 Subject: [PATCH 13/18] convert tests, remove STS --- .../src/meta/credentials/lazy_caching.rs | 16 ++++-- .../meta/credentials/lazy_caching/cache.rs | 4 +- aws/sdk/examples/sts/Cargo.toml | 16 ------ .../sts/src/bin/credentials-provider.rs | 54 ------------------- 4 files changed, 14 insertions(+), 76 deletions(-) delete mode 100644 aws/sdk/examples/sts/Cargo.toml delete mode 100644 aws/sdk/examples/sts/src/bin/credentials-provider.rs diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs index da8cfe3740..f2f34a14f7 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs @@ -231,6 +231,7 @@ mod tests { use aws_types::Credentials; use smithy_async::rt::sleep::TokioSleep; use tracing::info; + use tracing_test::traced_test; use crate::meta::credentials::credential_fn::async_provide_credentials_fn; @@ -300,7 +301,8 @@ mod tests { assert_eq!(Some(epoch_secs(expired_secs)), creds.expiry()); } - #[test_env_log::test(tokio::test)] + #[traced_test] + #[tokio::test] async fn initial_populate_credentials() { let time = TestTime::new(epoch_secs(100)); let loader = Arc::new(async_provide_credentials_fn(|| async { @@ -326,7 +328,8 @@ mod tests { ); } - #[test_env_log::test(tokio::test)] + #[traced_test] + #[tokio::test] async fn reload_expired_credentials() { let time = TestTime::new(epoch_secs(100)); let time_inner = time.time.clone(); @@ -349,7 +352,8 @@ mod tests { expect_creds(3000, &provider).await; } - #[test_env_log::test(tokio::test)] + #[traced_test] + #[tokio::test] async fn load_failed_error() { let time = TestTime::new(epoch_secs(100)); let time_inner = time.time.clone(); @@ -366,7 +370,8 @@ mod tests { assert!(provider.provide_credentials().await.is_err()); } - #[test_env_log::test] + #[traced_test] + #[test] fn load_contention() { let rt = tokio::runtime::Builder::new_multi_thread() .enable_time() @@ -411,7 +416,8 @@ mod tests { } } - #[test_env_log::test(tokio::test)] + #[tokio::test] + #[traced_test] async fn load_timeout() { let time = TestTime::new(epoch_secs(100)); let provider = LazyCachingCredentialsProvider::new( diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/cache.rs b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/cache.rs index 5b4c560ac6..d88ff8da30 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/cache.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching/cache.rs @@ -87,6 +87,7 @@ mod tests { use aws_types::credentials::CredentialsError; use aws_types::Credentials; use std::time::{Duration, SystemTime}; + use tracing_test::traced_test; fn credentials(expired_secs: u64) -> Result<(Credentials, SystemTime), CredentialsError> { let expiry = epoch_secs(expired_secs); @@ -106,7 +107,8 @@ mod tests { assert!(!expired(ts, Duration::from_secs(10), epoch_secs(10))); } - #[test_env_log::test(tokio::test)] + #[traced_test] + #[tokio::test] async fn cache_clears_if_expired_only() { let cache = Cache::new(Duration::from_secs(10)); assert!(cache diff --git a/aws/sdk/examples/sts/Cargo.toml b/aws/sdk/examples/sts/Cargo.toml deleted file mode 100644 index 3d6702715a..0000000000 --- a/aws/sdk/examples/sts/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "sts" -version = "0.1.0" -authors = ["Russell Cohen "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -aws-config = { path = "../../build/aws-sdk/aws-config" } -sts = { package = "aws-sdk-sts", path = "../../build/aws-sdk/sts" } -dynamodb = { package = "aws-sdk-dynamodb", path = "../../build/aws-sdk/dynamodb"} -aws-auth = { package = "aws-auth", path = "../../build/aws-sdk/aws-auth" } -aws-types = { package = "aws-types", path = "../../build/aws-sdk/aws-types"} -tokio = { version = "1", features = ["full"] } -tracing-subscriber = "0.2.18" diff --git a/aws/sdk/examples/sts/src/bin/credentials-provider.rs b/aws/sdk/examples/sts/src/bin/credentials-provider.rs deleted file mode 100644 index 4b6cb77438..0000000000 --- a/aws/sdk/examples/sts/src/bin/credentials-provider.rs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -use aws_auth::provider::lazy_caching::LazyCachingCredentialsProvider; -use aws_auth::provider::{async_provide_credentials_fn, CredentialsError}; - -use sts::Credentials; - -/// Implements a basic version of ProvideCredentials with AWS STS -/// and lists the tables in the region based on those credentials. -#[tokio::main] -async fn main() -> Result<(), dynamodb::Error> { - tracing_subscriber::fmt::init(); - let shared_config = aws_config::load_from_env().await; - let client = sts::Client::new(&shared_config); - - // `LazyCachingCredentialsProvider` will load credentials if it doesn't have any non-expired - // credentials cached. See the docs on the builder for the various configuration options, - // such as timeouts, default expiration times, and more. - let sts_provider = LazyCachingCredentialsProvider::builder() - .load(async_provide_credentials_fn(move || { - let client = client.clone(); - async move { - let session_token = client - .get_session_token() - .send() - .await - .map_err(|err| CredentialsError::Unhandled(Box::new(err)))?; - let sts_credentials = session_token - .credentials - .expect("should include credentials"); - Ok(Credentials::new( - sts_credentials.access_key_id.unwrap(), - sts_credentials.secret_access_key.unwrap(), - sts_credentials.session_token, - sts_credentials - .expiration - .map(|expiry| expiry.to_system_time().expect("sts sent a time < 0")), - "Sts", - )) - } - })) - .build(); - - let dynamodb_conf = dynamodb::config::Builder::from(&shared_config) - .credentials_provider(sts_provider) - .build(); - - let client = dynamodb::Client::from_conf(dynamodb_conf); - println!("tables: {:?}", client.list_tables().send().await?); - Ok(()) -} From 7654d39a248f945087e092329efa3900919cfd69 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:57:20 -0400 Subject: [PATCH 14/18] Fix doc comment --- aws/rust-runtime/aws-config/src/profile/credentials.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index f389c8257a..0a625012e6 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -57,7 +57,7 @@ impl ProvideCredentials for ProfileFileCredentialsProvider { /// AWS Profile based credentials provider /// /// This credentials provider will load credentials from `~/.aws/config` and `~/.aws/credentials`. -/// The locations of these files are configurable, see [`profile::load`](aws_types::profile::load). +/// The locations of these files are configurable, see [`profile::load`](aws_config::profile::load). /// /// Generally, this will be constructed via the default provider chain, however, it can be manually /// constructed with the builder: From 1bef78fde795cbd5f03bfb909af74c177e47e99d Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 15:59:07 -0400 Subject: [PATCH 15/18] remove usages of test-env-log --- aws/rust-runtime/aws-auth/Cargo.toml | 1 - aws/rust-runtime/aws-config/Cargo.toml | 3 --- 2 files changed, 4 deletions(-) diff --git a/aws/rust-runtime/aws-auth/Cargo.toml b/aws/rust-runtime/aws-auth/Cargo.toml index 55c6eb1b36..01f6d67950 100644 --- a/aws/rust-runtime/aws-auth/Cargo.toml +++ b/aws/rust-runtime/aws-auth/Cargo.toml @@ -23,7 +23,6 @@ zeroize = "1.2.0" async-trait = "0.1.50" env_logger = "*" http = "0.2.3" -test-env-log = { version = "0.2.7", features = ["trace"] } tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "test-util"] } tracing-subscriber = { version = "0.2.16", features = ["fmt"] } smithy-async = { path = "../../../rust-runtime/smithy-async", features = ["rt-tokio"] } diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index 121aaae56f..166ab7920a 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -35,9 +35,6 @@ aws-hyper = { path = "../../sdk/build/aws-sdk/aws-hyper", optional = true } [dev-dependencies] futures-util = "0.3.16" - -# TODO: unify usages of test-env-log and tracing-test -test-env-log = "0.2.7" tracing-test = "0.1.0" tokio = { version = "1", features = ["full"] } From d617ea6d4d35da72d22cb45f88554e8f4131fd77 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 31 Aug 2021 16:12:23 -0400 Subject: [PATCH 16/18] take 2 fix doc comment --- aws/rust-runtime/aws-config/src/profile/credentials.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index 0a625012e6..027ca6e659 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -57,7 +57,7 @@ impl ProvideCredentials for ProfileFileCredentialsProvider { /// AWS Profile based credentials provider /// /// This credentials provider will load credentials from `~/.aws/config` and `~/.aws/credentials`. -/// The locations of these files are configurable, see [`profile::load`](aws_config::profile::load). +/// The locations of these files are configurable, see [`profile::load`](crate::profile::load). /// /// Generally, this will be constructed via the default provider chain, however, it can be manually /// constructed with the builder: From a9693b9322268537b7e493a9c5ad10ae00a77272 Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Wed, 1 Sep 2021 09:36:08 -0400 Subject: [PATCH 17/18] Update changelog --- CHANGELOG.md | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4efac421f3..8765f2631f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,131 @@ vNext (Month Day, Year) ----------------------- +This release adds support for three commonly requested features: +- More powerful credential chain +- Support for constructing multiple clients from the same configuration +- Support for transcribe streaming and S3 Select + +In addition, this overhauls client configuration which lead to a number of breaking changes. Detailed changes are inline. + +Current Credential Provider Support: +- [x] Environment variables +- [x] Web Identity Token Credentials +- [ ] Profile file support (partial) + - [ ] Credentials + - [ ] SSO + - [ ] ECS Credential source + - [ ] IMDS credential source + - [x] Assume role from source profile + - [x] Static credentials source profile + - [x] WebTokenIdentity provider + - [ ] Region +- [ ] IMDS +- [ ] ECS + +## Upgrade Guide +### If you use `::Client::from_env` +`from_env` loaded region & credentials from environment variables _only_. Default sources have been removed from the generated +SDK clients and moved to the `aws-config` package. Note that the `aws-config` package default chain adds support for +profile file and web identity token profiles. + +1. Add a dependency on `aws-config`: + ```toml + [dependencies] + aws-config = { git = "https://github.com/awslabs/aws-sdk-rust", tag = "v0.0.17-alpha" } + ``` +2. Update your client creation code: + ```rust + // `shared_config` can be used to construct multiple different service clients! + let shared_config = aws_config::load_from_env().await; + // before: ::Client::from_env(); + let client = ::Client::new(&shared_config) + ``` + +### If you used `::Config::builder()` +`Config::build()` has been modified to _not_ fallback to a default provider. Instead, use `aws-config` to load and modify +the default chain. Note that when you switch to `aws-config`, support for profile files and web identity tokens will be added. + +1. Add a dependency on `aws-config`: + ```toml + [dependencies] + aws-config = { git = "https://github.com/awslabs/aws-sdk-rust", tag = "v0.0.17-alpha" } + ``` + +2. Update your client creation code: + + ```rust + fn before() { + let region = aws_types::region::ChainProvider::first_try(<1 provider>).or_default_provider(); + let config = ::Config::builder().region(region).build(); + let client = ::Client::from_conf(&config); + } + + async fn after() { + use aws_config::meta::region::RegionProviderChain; + let region_provider = RegionProviderChain::first_try(<1 provider>).or_default_provider(); + // `shared_config` can be used to construct multiple different service clients! + let shared_config = aws_config::from_env().region(region_provider).load().await; + let client = ::Client::new(&shared_config) + } + ``` + +### If you used `aws-auth-providers` +All credential providers that were in `aws-auth-providers` have been moved to `aws-config`. Unless you have a specific use case +for a specific credential provider, you should use the default provider chain: + +```rust + let shared_config = aws_config::load_from_env().await; + let client = ::Client::new(&shared_config); +``` + +### If you maintain your own credential provider +`AsyncProvideCredentials` has been renamed to `ProvideCredentials`. The trait has been moved from `aws-auth` to `aws-types`. +The original `ProvideCredentials` trait has been removed. The return type has been changed to by a custom future. + +For synchronous use cases: +```rust +use aws_types::credentials::{ProvideCredentials, future}; + +#[derive(Debug)] +struct CustomCreds; +impl ProvideCredentials for CustomCreds { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + // if your credentials are synchronous, use `::ready` + // if your credentials are loaded asynchronously, use `::new` + future::ProvideCredentials::ready(todo!()) // your credentials go here + } +} +``` + +For asynchronous use cases: +```rust +use aws_types::credentials::{ProvideCredentials, future, Result}; + +#[derive(Debug)] +struct CustomAsyncCreds; +impl CustomAsyncCreds { + async fn load_credentials(&self) -> Result { + Ok(Credentials::from_keys("my creds...", "secret", None)) + } +} + +impl ProvideCredentials for CustomCreds { + fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::new(self.load_credentials()) + } +} +``` + **Breaking Changes** +- Credential providers from `aws-auth-providers` have been moved to `aws-config` (#678) +- `AsyncProvideCredentials` has been renamed to `ProvideCredentials`. The original non-async provide credentials has been + removed. See the migration guide above. - `::from_env()` has been removed (#675). A drop-in replacement is available: 1. Add a dependency on `aws-config`: ```toml @@ -92,7 +217,7 @@ v0.20 (August 10th, 2021) **New This Week** - Add AssumeRoleProvider parser implementation. (#632) -- The closure passed to `async_provide_credentials_fn` can now borrow values (#637) +- The closure passed to `provide_credentials_fn` can now borrow values (#637) - Add `Sender`/`Receiver` implementations for Event Stream (#639) - Bring in the latest AWS models (#630) @@ -174,7 +299,7 @@ v0.16 (July 6th 2021) - :tada: Add support for EBS (#567) - :tada: Add support for Cognito (#573) - :tada: Add support for Snowball (#579, @landonxjames) -- Make it possible to asynchronously provide credentials with `async_provide_credentials_fn` (#572, #577) +- Make it possible to asynchronously provide credentials with `provide_credentials_fn` (#572, #577) - Improve RDS, QLDB, Polly, and KMS examples (#561, #560, #558, #556, #550) - Update AWS SDK models (#575) - :bug: Bugfix: Fill in message from error response even when it doesn't match the modeled case format (#565) From 3922d83adb6088ef1df020dbb8e9fa6b2e77124f Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Wed, 1 Sep 2021 09:37:44 -0400 Subject: [PATCH 18/18] rename asycn_provide_credentials_fn --- .../src/meta/credentials/credential_fn.rs | 24 +++++++++---------- .../src/meta/credentials/lazy_caching.rs | 12 +++++----- .../aws-config/src/meta/credentials/mod.rs | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs b/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs index 79fbe7810e..25f9397960 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/credential_fn.rs @@ -6,20 +6,20 @@ use std::marker::PhantomData; /// A [`ProvideCredentials`] implemented by a closure. /// -/// See [`async_provide_credentials_fn`] for more details. +/// See [`provide_credentials_fn`] for more details. #[derive(Copy, Clone)] -pub struct AsyncProvideCredentialsFn<'c, T> { +pub struct ProvideCredentialsFn<'c, T> { f: T, phantom: PhantomData<&'c T>, } -impl Debug for AsyncProvideCredentialsFn<'_, T> { +impl Debug for ProvideCredentialsFn<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "ProvideCredentialsFn") } } -impl<'c, T, F> ProvideCredentials for AsyncProvideCredentialsFn<'c, T> +impl<'c, T, F> ProvideCredentials for ProvideCredentialsFn<'c, T> where T: Fn() -> F + Send + Sync + 'c, F: Future + Send + 'static, @@ -40,24 +40,24 @@ where /// /// ``` /// use aws_types::Credentials; -/// use aws_config::meta::credentials::async_provide_credentials_fn; +/// use aws_config::meta::credentials::provide_credentials_fn; /// /// async fn load_credentials() -> Credentials { /// todo!() /// } /// -/// async_provide_credentials_fn(|| async { +/// provide_credentials_fn(|| async { /// // Async process to retrieve credentials goes here /// let credentials = load_credentials().await; /// Ok(credentials) /// }); /// ``` -pub fn async_provide_credentials_fn<'c, T, F>(f: T) -> AsyncProvideCredentialsFn<'c, T> +pub fn provide_credentials_fn<'c, T, F>(f: T) -> ProvideCredentialsFn<'c, T> where T: Fn() -> F + Send + Sync + 'c, F: Future + Send + 'static, { - AsyncProvideCredentialsFn { + ProvideCredentialsFn { f, phantom: Default::default(), } @@ -65,7 +65,7 @@ where #[cfg(test)] mod test { - use crate::meta::credentials::credential_fn::async_provide_credentials_fn; + use crate::meta::credentials::credential_fn::provide_credentials_fn; use async_trait::async_trait; use aws_types::credentials::ProvideCredentials; use aws_types::{credentials, Credentials}; @@ -104,9 +104,9 @@ mod test { } } - // Test that the closure passed to `async_provide_credentials_fn` is allowed to borrow things + // Test that the closure passed to `provide_credentials_fn` is allowed to borrow things #[tokio::test] - async fn async_provide_credentials_fn_closure_can_borrow() { + async fn provide_credentials_fn_closure_can_borrow() { fn check_is_str_ref(_input: &str) {} async fn test_async_provider(input: String) -> credentials::Result { Ok(Credentials::from_keys(&input, &input, None)) @@ -116,7 +116,7 @@ mod test { let mut providers = Vec::new(); for thing in &things_to_borrow { - let provider = async_provide_credentials_fn(move || { + let provider = provide_credentials_fn(move || { check_is_str_ref(thing); test_async_provider(thing.into()) }); diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs index f2f34a14f7..321597ebd5 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/lazy_caching.rs @@ -128,11 +128,11 @@ mod builder { /// /// ``` /// use aws_types::Credentials; - /// use aws_config::meta::credentials::async_provide_credentials_fn; + /// use aws_config::meta::credentials::provide_credentials_fn; /// use aws_config::meta::credentials::LazyCachingCredentialsProvider; /// /// let provider = LazyCachingCredentialsProvider::builder() - /// .load(async_provide_credentials_fn(|| async { + /// .load(provide_credentials_fn(|| async { /// // An async process to retrieve credentials would go here: /// Ok(Credentials::from_keys("example", "example", None)) /// })) @@ -233,7 +233,7 @@ mod tests { use tracing::info; use tracing_test::traced_test; - use crate::meta::credentials::credential_fn::async_provide_credentials_fn; + use crate::meta::credentials::credential_fn::provide_credentials_fn; use super::{ LazyCachingCredentialsProvider, TimeSource, DEFAULT_BUFFER_TIME, @@ -271,7 +271,7 @@ mod tests { LazyCachingCredentialsProvider::new( time, Box::new(TokioSleep::new()), - Arc::new(async_provide_credentials_fn(move || { + Arc::new(provide_credentials_fn(move || { let list = load_list.clone(); async move { let next = list.lock().unwrap().remove(0); @@ -305,7 +305,7 @@ mod tests { #[tokio::test] async fn initial_populate_credentials() { let time = TestTime::new(epoch_secs(100)); - let loader = Arc::new(async_provide_credentials_fn(|| async { + let loader = Arc::new(provide_credentials_fn(|| async { info!("refreshing the credentials"); Ok(credentials(1000)) })); @@ -423,7 +423,7 @@ mod tests { let provider = LazyCachingCredentialsProvider::new( time, Box::new(TokioSleep::new()), - Arc::new(async_provide_credentials_fn(|| async { + Arc::new(provide_credentials_fn(|| async { tokio::time::sleep(Duration::from_millis(10)).await; Ok(credentials(1000)) })), diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs index eb3ac6a130..4a67407abe 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/mod.rs @@ -7,7 +7,7 @@ mod chain; pub use chain::CredentialsProviderChain; mod credential_fn; -pub use credential_fn::async_provide_credentials_fn; +pub use credential_fn::provide_credentials_fn; pub mod lazy_caching; pub use lazy_caching::LazyCachingCredentialsProvider;