Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add Default Provider Chain #650

Merged
merged 4 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ vNext (Month Day, Year)
- Add profile file credential provider implementation. This implementation currently does not support credential sources
for assume role providers other than environment variables. (#640)
- :bug: Fix name collision that occurred when a model had both a union and a structure named `Result` (#643)
- Add initial implementation of a default provider chain. (#650)
- Update smithy-client to simplify creating HTTP/HTTPS connectors (#650)

v0.20 (August 10th, 2021)
--------------------------
Expand Down
8 changes: 8 additions & 0 deletions aws/rust-runtime/aws-auth-providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ version = "0.1.0"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>", "Russell Cohen <rcoh@amazon.com>"]
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-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"] }
Expand Down
73 changes: 73 additions & 0 deletions aws/rust-runtime/aws-auth-providers/src/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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_auth::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<dyn AsyncProvideCredentials>)>,
}

impl ChainProvider {
pub fn first_try(
name: impl Into<Cow<'static, str>>,
provider: impl AsyncProvideCredentials + 'static,
) -> Self {
ChainProvider {
providers: vec![(name.into(), Box::new(provider))],
}
}

pub fn or_else(
mut self,
name: impl Into<Cow<'static, str>>,
provider: impl AsyncProvideCredentials + 'static,
) -> Self {
self.providers.push((name.into(), Box::new(provider)));
self
}

async fn credentials(&self) -> CredentialsResult {
let mut last_error = CredentialsError::Unhandled("no providers".into());
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(e) => {
tracing::info!(provider = %name, error = %e, "provider in chain did not provide credentials");
last_error = e
}
}
}
return Err(last_error);
}
}

impl AsyncProvideCredentials for ChainProvider {
fn provide_credentials<'a>(&'a self) -> BoxFuture<'a, CredentialsResult>
where
Self: 'a,
{
Box::pin(self.credentials())
}
}
212 changes: 212 additions & 0 deletions aws/rust-runtime/aws-auth-providers/src/default_provider_chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* 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::ProvideRegion;
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,
credential_cache: aws_auth::provider::lazy_caching::builder::Builder,
env: Option<Env>,
}

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: &dyn ProvideRegion) -> Self {
self.profile_file_builder.set_region(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));
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_auth::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<Credentials, CredentialsError> {
/// 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<Cow<'static, str>>,
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(Some(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(Some(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 provider_chain = crate::chain::ChainProvider::first_try("Environment", env_provider)
.or_else("Profile", profile_provider);
let cached_provider = self.credential_cache.load(provider_chain);
DefaultProviderChain(cached_provider.build())
}
}

#[cfg(test)]
mod test {
use crate::DefaultProviderChain;
use aws_auth::provider::AsyncProvideCredentials;
use aws_hyper::DynConnector;
use aws_types::os_shim_internal::{Env, Fs};
use smithy_client::dvr::ReplayingConnection;
use tracing_test::traced_test;

#[tokio::test]
async fn prefer_environment() {
let env = Env::from_slice(&[
("AWS_ACCESS_KEY_ID", "correct_key"),
("AWS_SECRET_ACCESS_KEY", "correct_secret"),
("HOME", "/Users/me"),
]);

let fs = Fs::from_test_dir("test-data/aws-config/e2e-assume-role", "/Users/me");
// empty connection will error if it is used
let connection = ReplayingConnection::new(vec![]);
let provider = DefaultProviderChain::builder()
.fs(fs)
.env(env)
.connector(DynConnector::new(connection))
.build();
// empty connection will error if it is used
let creds = provider.provide_credentials().await.expect("valid creds");
assert_eq!(creds.access_key_id(), "correct_key");
assert_eq!(creds.secret_access_key(), "correct_secret")
}

#[traced_test]
#[tokio::test]
async fn fallback_to_profile() {
let env = Env::from_slice(&[
// access keys not in environment
("HOME", "/Users/me"),
]);

let fs = Fs::from_test_dir("./test-data/static-keys/aws-config", "/Users/me/.aws");
// empty connection will error if it is used
let connection = ReplayingConnection::new(vec![]);
let provider = DefaultProviderChain::builder()
.fs(fs)
.env(env)
.connector(DynConnector::new(connection))
.build();
let creds = provider.provide_credentials().await.expect("valid creds");
assert_eq!(creds.access_key_id(), "correct_key");
assert_eq!(creds.secret_access_key(), "correct_secret")
}
}
39 changes: 39 additions & 0 deletions aws/rust-runtime/aws-auth-providers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,43 @@
* 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;

// 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

#[cfg(feature = "rustls")]
fn default_connector() -> Option<DynConnector> {
Some(DynConnector::new(smithy_client::conns::https()))
}

#[cfg(all(not(feature = "rustls"), feature = "native-tls"))]
fn default_connector() -> Option<DynConnector> {
Some(DynConnector::new(smithy_client::conns::native_tls()))
}

#[cfg(not(any(feature = "rustls", feature = "native-tls")))]
fn default_connector() -> Option<DynConnector> {
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
pub fn default_provider() -> impl AsyncProvideCredentials {
default_provider_chain::Builder::default().build()
}
Loading