Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for credential_process from profiles #1356

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
168 changes: 115 additions & 53 deletions aws/rust-runtime/aws-config/src/credential_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,95 @@ use aws_smithy_types::date_time::Format;
use aws_smithy_types::DateTime;
use aws_types::credentials::{future, CredentialsError, ProvideCredentials};
use aws_types::{credentials, Credentials};
use std::borrow::Cow;
use std::fmt;
use std::process::Command;
use std::time::SystemTime;

/// Credentials Provider
pub struct CredentialProcessProvider {
command: String,
}
pub(crate) struct CommandWithSensitiveArgs<T>(T);

/// Returns the given `command` string with arguments redacted if there were any
pub(crate) fn debug_fmt_command_string(command: &str) -> Cow<'_, str> {
match command.find(char::is_whitespace) {
Some(index) => Cow::Owned(format!("{} ** arguments redacted **", &command[0..index])),
None => Cow::Borrowed(command),
impl<T> CommandWithSensitiveArgs<T>
where
T: AsRef<str>,
{
pub(crate) fn new(value: T) -> Self {
Self(value)
}

pub(crate) fn unredacted(&self) -> &str {
self.0.as_ref()
}
}

impl fmt::Debug for CredentialProcessProvider {
impl<T> fmt::Display for CommandWithSensitiveArgs<T>
where
T: AsRef<str>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Security: The arguments for command must be redacted since they can be sensitive
f.debug_struct("CredentialProcessProvider")
.field("command", &debug_fmt_command_string(&self.command))
.finish()
let command = self.0.as_ref();
match command.find(char::is_whitespace) {
Some(index) => write!(f, "{} ** arguments redacted **", &command[0..index]),
None => write!(f, "{}", command),
}
}
}

impl<T> fmt::Debug for CommandWithSensitiveArgs<T>
where
T: AsRef<str>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", format!("{}", self))
}
}

impl<T> Clone for CommandWithSensitiveArgs<T>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is just #[derive(Clone)]?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs the where T: Clone clause, but maybe I can just put that bound on the struct itself and then derive...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I take that approach, then each other trait impl needs to have a T: Clone bound added, so I don't think it's much of a savings.

where
T: Clone,
{
fn clone(&self) -> Self {
Self(self.0.clone())
}
}

/// External process credentials provider
///
/// This credentials provider runs a configured external process and parses
/// its output to retrieve credentials.
///
/// The external process must exit with status 0 and output the following
/// JSON format to `stdout` to provide credentials:
///
/// ```json
/// {
/// "Version:" 1,
/// "AccessKeyId": "access key id",
/// "SecretAccessKey": "secret access key",
/// "SessionToken": "session token",
/// "Expiration": "time that the expiration will expire"
/// }
/// ```
///
/// The `Version` must be set to 1. `AccessKeyId` and `SecretAccessKey` are always required.
/// `SessionToken` must be set if a session token is associated with the `AccessKeyId`.
/// The `Expiration` is optional, and must be given in the RFC 3339 date time format (e.g.,
/// `2022-05-26T12:34:56.789Z`).
///
/// If the external process exits with a non-zero status, then the contents of `stderr`
/// will be output as part of the credentials provider error message.
///
/// This credentials provider is included in the profile credentials provider, and can be
/// configured using the `credential_process` attribute. For example:
///
/// ```plain
/// [profile example]
/// credential_process = /path/to/my/process --some --arguments
/// ```
#[derive(Debug)]
pub struct CredentialProcessProvider {
command: CommandWithSensitiveArgs<String>,
}

impl ProvideCredentials for CredentialProcessProvider {
fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
where
Expand All @@ -48,21 +110,24 @@ impl ProvideCredentials for CredentialProcessProvider {
}

impl CredentialProcessProvider {
/// Create new [`CredentialProcessProvider`]
/// Create new [`CredentialProcessProvider`] with the `command` needed to execute the external process.
pub fn new(command: String) -> Self {
Self { command }
Self {
command: CommandWithSensitiveArgs::new(command),
}
}

async fn credentials(&self) -> credentials::Result {
// Security: command arguments must be redacted at debug level
tracing::debug!(command = %self.command, "loading credentials from external process");

let mut command = if cfg!(windows) {
let mut command = Command::new("cmd.exe");
command.args(&["/C", &self.command]);
command.args(&["/C", self.command.unredacted()]);
command
} else {
let mut command = Command::new("sh");
command.args(&["-c", &self.command]);
command.args(&["-c", self.command.unredacted()]);
command
};

Expand All @@ -73,10 +138,8 @@ impl CredentialProcessProvider {
))
})?;

// Security: command arguments can be logged at trace level, but must be redacted at debug level
// since they can contain sensitive information.
// Security: command arguments can be logged at trace level
tracing::trace!(command = ?command, status = ?output.status, "executed command (unredacted)");
tracing::debug!(command = ?debug_fmt_command_string(&self.command), status = ?output.status, "executed command");

if !output.status.success() {
let reason =
Expand Down Expand Up @@ -162,41 +225,40 @@ pub(crate) fn parse_credential_process_json_credentials(
})?;

match version {
Some(1) => {
let access_key_id =
access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?;
let secret_access_key =
secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?;
let session_token =
session_token.ok_or(InvalidJsonCredentials::MissingField("Token"))?;
let expiration =
expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?;
let expiration = SystemTime::try_from(
DateTime::from_str(expiration.as_ref(), Format::DateTime).map_err(|err| {
InvalidJsonCredentials::InvalidField {
field: "Expiration",
err: err.into(),
}
})?,
)
.map_err(|_| {
InvalidJsonCredentials::Other(
"credential expiration time cannot be represented by a DateTime".into(),
)
})?;
Ok(RefreshableCredentials {
access_key_id,
secret_access_key,
session_token,
expiration,
Some(1) => { /* continue */ }
None => return Err(InvalidJsonCredentials::MissingField("Version")),
Some(version) => {
return Err(InvalidJsonCredentials::InvalidField {
field: "version",
err: format!("unknown version number: {}", version).into(),
})
}
None => Err(InvalidJsonCredentials::MissingField("Version")),
Some(version) => Err(InvalidJsonCredentials::InvalidField {
field: "version",
err: format!("unknown version number: {}", version).into(),
}),
}

let access_key_id = access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?;
let secret_access_key =
secret_access_key.ok_or(InvalidJsonCredentials::MissingField("SecretAccessKey"))?;
let session_token = session_token.ok_or(InvalidJsonCredentials::MissingField("Token"))?;
let expiration = expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?;
let expiration = SystemTime::try_from(
DateTime::from_str(expiration.as_ref(), Format::DateTime).map_err(|err| {
InvalidJsonCredentials::InvalidField {
field: "Expiration",
err: err.into(),
}
})?,
)
.map_err(|_| {
InvalidJsonCredentials::Other(
"credential expiration time cannot be represented by a DateTime".into(),
)
})?;
Ok(RefreshableCredentials {
access_key_id,
secret_access_key,
session_token,
expiration,
})
}

#[cfg(test)]
Expand Down
2 changes: 2 additions & 0 deletions aws/rust-runtime/aws-config/src/profile/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,4 +502,6 @@ mod test {
make_test!(invalid_config);
make_test!(region_override);
make_test!(credential_process);
make_test!(credential_process_failure);
make_test!(credential_process_invalid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl ProviderChain {
}
BaseProvider::AccessKey(key) => Arc::new(key.clone()),
BaseProvider::CredentialProcess(credential_process) => Arc::new(
CredentialProcessProvider::new(credential_process.to_string()),
CredentialProcessProvider::new(credential_process.unredacted().into()),
),
BaseProvider::WebIdentityTokenRole {
role_arn,
Expand Down