Unverified Commit ef85116f authored by John DiSanti's avatar John DiSanti Committed by GitHub
Browse files

Add ability to programmatically customize profile files (#1770)

parent 5b7aa7b8
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
@@ -53,3 +53,31 @@ of breaking changes and how to resolve them.
references = ["smithy-rs#1740", "smithy-rs#256"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
author = "jdisanti"

[[aws-sdk-rust]]
message = """
It is now possible to programmatically customize the locations of the profile config/credentials files in `aws-config`:
```rust
use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider};
use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind};

let profile_files = ProfileFiles::builder()
    .with_file(ProfileFileKind::Credentials, "some/path/to/credentials-file")
    .build();
let credentials_provider = ProfileFileCredentialsProvider::builder()
    .profile_files(profile_files.clone())
    .build();
let region_provider = ProfileFileRegionProvider::builder()
    .profile_files(profile_files)
    .build();

let sdk_config = aws_config::from_env()
    .credentials_provider(credentials_provider)
    .region(region_provider)
    .load()
    .await;
```
"""
references = ["aws-sdk-rust#237", "smithy-rs#1770"]
meta = { "breaking" = false, "tada" = true, "bug" = false }
author = "jdisanti"
+3 −3
Original line number Diff line number Diff line
@@ -37,7 +37,7 @@ use tokio::sync::OnceCell;

use crate::connector::expect_connector;
use crate::imds::client::token::TokenMiddleware;
use crate::profile::ProfileParseError;
use crate::profile::credentials::ProfileFileError;
use crate::provider_config::ProviderConfig;
use crate::{profile, PKG_VERSION};
use aws_sdk_sso::config::timeout::TimeoutConfig;
@@ -439,7 +439,7 @@ pub enum BuildError {
    InvalidEndpointMode(InvalidEndpointMode),

    /// The AWS Profile (e.g. `~/.aws/config`) was invalid
    InvalidProfile(ProfileParseError),
    InvalidProfile(ProfileFileError),

    /// The specified endpoint was not a valid URI
    InvalidEndpointUri(InvalidUri),
@@ -626,7 +626,7 @@ impl EndpointSource {
            }
            EndpointSource::Env(env, fs) => {
                // load an endpoint override from the environment
                let profile = profile::load(fs, env)
                let profile = profile::load(fs, env, &Default::default())
                    .await
                    .map_err(BuildError::InvalidProfile)?;
                let uri_override = if let Ok(uri) = env.get(env::ENDPOINT) {
+8 −1
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@

//! Load an app name from an AWS profile

use super::profile_file::ProfileFiles;
use crate::provider_config::ProviderConfig;
use aws_types::app_name::AppName;
use aws_types::os_shim_internal::{Env, Fs};
@@ -14,6 +15,8 @@ use aws_types::os_shim_internal::{Env, Fs};
/// This provider will attempt to shared AWS shared configuration and then read the
/// `sdk-ua-app-id` property from the active profile.
///
#[doc = include_str!("location_of_profile_files.md")]
///
/// # Examples
///
/// **Loads "my-app" as the app name**
@@ -35,6 +38,7 @@ pub struct ProfileFileAppNameProvider {
    fs: Fs,
    env: Env,
    profile_override: Option<String>,
    profile_files: ProfileFiles,
}

impl ProfileFileAppNameProvider {
@@ -46,6 +50,7 @@ impl ProfileFileAppNameProvider {
            fs: Fs::real(),
            env: Env::real(),
            profile_override: None,
            profile_files: Default::default(),
        }
    }

@@ -56,7 +61,7 @@ impl ProfileFileAppNameProvider {

    /// Parses the profile config and attempts to find an app name.
    pub async fn app_name(&self) -> Option<AppName> {
        let profile = super::parser::load(&self.fs, &self.env)
        let profile = super::parser::load(&self.fs, &self.env, &self.profile_files)
            .await
            .map_err(|err| tracing::warn!(err = %err, "failed to parse profile"))
            .ok()?;
@@ -82,6 +87,7 @@ impl ProfileFileAppNameProvider {
pub struct Builder {
    config: Option<ProviderConfig>,
    profile_override: Option<String>,
    profile_files: Option<ProfileFiles>,
}

impl Builder {
@@ -104,6 +110,7 @@ impl Builder {
            env: conf.env(),
            fs: conf.fs(),
            profile_override: self.profile_override,
            profile_files: self.profile_files.unwrap_or_default(),
        }
    }
}
+47 −32
Original line number Diff line number Diff line
@@ -22,22 +22,21 @@
//! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials
//! through a series of providers.

use crate::profile::credentials::exec::named::NamedProviderFactory;
use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain};
use crate::profile::parser::ProfileParseError;
use crate::profile::profile_file::ProfileFiles;
use crate::profile::Profile;
use crate::provider_config::ProviderConfig;
use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials};
use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::sync::Arc;

use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials};

use tracing::Instrument;

use crate::profile::credentials::exec::named::NamedProviderFactory;
use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain};
use crate::profile::parser::ProfileParseError;
use crate::profile::Profile;
use crate::provider_config::ProviderConfig;

mod exec;
mod repr;

@@ -142,29 +141,14 @@ impl ProvideCredentials for ProfileFileCredentialsProvider {
///
/// SSO can also be used as a source profile for assume role chains.
///
/// ## Location of Profile Files
/// * The location of the config file will be loaded from the `AWS_CONFIG_FILE` environment variable
/// with a fallback to `~/.aws/config`
/// * The location of the credentials file will be loaded from the `AWS_SHARED_CREDENTIALS_FILE`
/// environment variable 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
/// - The concatenation of `HOMEDRIVE` and `HOMEPATH` on Windows (`$HOMEDRIVE$HOMEPATH`)
#[doc = include_str!("location_of_profile_files.md")]
#[derive(Debug)]
pub struct ProfileFileCredentialsProvider {
    factory: NamedProviderFactory,
    client_config: ClientConfiguration,
    provider_config: ProviderConfig,
    profile_override: Option<String>,
    profile_files: ProfileFiles,
}

impl ProfileFileCredentialsProvider {
@@ -178,6 +162,7 @@ impl ProfileFileCredentialsProvider {
            &self.provider_config,
            &self.factory,
            self.profile_override.as_deref(),
            &self.profile_files,
        )
        .await
        .map_err(|err| match err {
@@ -225,6 +210,13 @@ impl ProfileFileCredentialsProvider {
    }
}

#[doc(hidden)]
#[derive(Debug)]
pub struct CouldNotReadProfileFile {
    pub(crate) path: PathBuf,
    pub(crate) cause: std::io::Error,
}

/// An Error building a Credential source from an AWS Profile
#[derive(Debug)]
#[non_exhaustive]
@@ -283,6 +275,10 @@ pub enum ProfileFileError {
        /// The name of the provider
        name: String,
    },

    /// A custom profile file location didn't exist or could not be read
    #[non_exhaustive]
    CouldNotReadProfileFile(CouldNotReadProfileFile),
}

impl ProfileFileError {
@@ -326,6 +322,13 @@ impl Display for ProfileFileError {
                "profile `{}` did not contain credential information",
                profile
            ),
            ProfileFileError::CouldNotReadProfileFile(details) => {
                write!(
                    f,
                    "Failed to read custom profile file at {:?}",
                    details.path
                )
            }
        }
    }
}
@@ -334,16 +337,24 @@ impl Error for ProfileFileError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ProfileFileError::CouldNotParseProfile(err) => Some(err),
            ProfileFileError::CouldNotReadProfileFile(details) => Some(&details.cause),
            _ => None,
        }
    }
}

impl From<ProfileParseError> for ProfileFileError {
    fn from(err: ProfileParseError) -> Self {
        ProfileFileError::CouldNotParseProfile(err)
    }
}

/// Builder for [`ProfileFileCredentialsProvider`]
#[derive(Debug, Default)]
pub struct Builder {
    provider_config: Option<ProviderConfig>,
    profile_override: Option<String>,
    profile_files: Option<ProfileFiles>,
    custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
}

@@ -409,6 +420,12 @@ impl Builder {
        self
    }

    /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
        self.profile_files = Some(profile_files);
        self
    }

    /// Builds a [`ProfileFileCredentialsProvider`]
    pub fn build(self) -> ProfileFileCredentialsProvider {
        let build_span = tracing::debug_span!("build_profile_provider");
@@ -453,6 +470,7 @@ impl Builder {
            },
            provider_config: conf,
            profile_override: self.profile_override,
            profile_files: self.profile_files.unwrap_or_default(),
        }
    }
}
@@ -461,13 +479,10 @@ async fn build_provider_chain(
    provider_config: &ProviderConfig,
    factory: &NamedProviderFactory,
    profile_override: Option<&str>,
    profile_files: &ProfileFiles,
) -> Result<ProviderChain, ProfileFileError> {
    let profile_set = super::parser::load(&provider_config.fs(), &provider_config.env())
        .await
        .map_err(|err| {
            tracing::warn!(err = %err, "failed to parse profile");
            ProfileFileError::CouldNotParseProfile(err)
        })?;
    let profile_set =
        super::parser::load(&provider_config.fs(), &provider_config.env(), profile_files).await?;
    let repr = repr::resolve_chain(&profile_set, profile_override)?;
    tracing::info!(chain = ?repr, "constructed abstract provider from config file");
    exec::ProviderChain::from_repr(provider_config, repr, factory)
+19 −0
Original line number Diff line number Diff line
## Location of Profile Files
* The location of the config file will be loaded from the `AWS_CONFIG_FILE` environment variable
with a fallback to `~/.aws/config`
* The location of the credentials file will be loaded from the `AWS_SHARED_CREDENTIALS_FILE`
environment variable with a fallback to `~/.aws/credentials`

The location of these files can also be customized programmatically using [`ProfileFiles`](crate::profile::profile_file::ProfileFiles).

## 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
- The concatenation of `HOMEDRIVE` and `HOMEPATH` on Windows (`$HOMEDRIVE$HOMEPATH`)
Loading