Unverified Commit 4b488c22 authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Add Default Provider Chain (#650)



* Add Default Provider Chain

This commit adds an initial implementation of the default credentials provder chain. First, it will use environment variables. Next it will use shared config / profiles.

* Update CHANGELOG.md

* Apply suggestions from code review

Co-authored-by: default avatarJohn DiSanti <jdisanti@amazon.com>

Co-authored-by: default avatarJohn DiSanti <jdisanti@amazon.com>
parent 47286edc
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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)
--------------------------
+8 −0
Original line number Diff line number Diff line
@@ -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"] }
+73 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
@@ -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