-
Notifications
You must be signed in to change notification settings - Fork 187
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
jdisanti
merged 18 commits into
smithy-lang:main
from
jszwedko:jszwedko/add-support-for-credential-process
May 26, 2022
Merged
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
c3a595c
Starting to add support for `credential_process` from profiles
jszwedko f8b05ad
Remove dbg statement
jszwedko 14abfdd
Re-order credential_process later to match boto ordering
jszwedko 14ad7a9
Avoid parsing the command
jszwedko b5f1c6b
correct provider name
jszwedko 865a6ee
Remove no longer needed dependency
jszwedko a787b61
Add failing test case for credential_process
jszwedko 7de0201
Add JSON parsing for credential_process responses
jszwedko 2e0ae8f
Add test
jszwedko 0e4f34c
Update changelog
jszwedko 6433a24
Merge branch 'main' into jszwedko/add-support-for-credential-process
jszwedko 9f88a6b
Merge branch 'main' into jszwedko/add-support-for-credential-process
jdisanti 16a8495
Merge branch 'main' into jszwedko/add-support-for-credential-process
jdisanti ce75253
Fix clippy lints
jdisanti 7b867af
Incorporate feedback
jdisanti 3be6523
Add more tests and refactor redaction
jdisanti 2a888d4
Merge remote-tracking branch 'upstream/main' into jszwedko/add-suppor…
jdisanti 652345d
Add more documentation to `CredentialProcessProvider`
jdisanti File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
//! Credentials Provider for external process | ||
|
||
use crate::json_credentials::{json_parse_loop, InvalidJsonCredentials, RefreshableCredentials}; | ||
use aws_smithy_json::deserialize::Token; | ||
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, | ||
} | ||
|
||
/// 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 fmt::Debug for CredentialProcessProvider { | ||
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() | ||
} | ||
} | ||
|
||
impl ProvideCredentials for CredentialProcessProvider { | ||
fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> | ||
where | ||
Self: 'a, | ||
{ | ||
future::ProvideCredentials::new(self.credentials()) | ||
} | ||
} | ||
|
||
impl CredentialProcessProvider { | ||
/// Create new [`CredentialProcessProvider`] | ||
pub fn new(command: String) -> Self { | ||
Self { command } | ||
} | ||
|
||
async fn credentials(&self) -> credentials::Result { | ||
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 | ||
} else { | ||
let mut command = Command::new("sh"); | ||
command.args(&["-c", &self.command]); | ||
command | ||
}; | ||
|
||
let output = command.output().map_err(|e| { | ||
CredentialsError::provider_error(format!( | ||
"Error retrieving credentials from external process: {}", | ||
e | ||
)) | ||
})?; | ||
|
||
// Security: command arguments can be logged at trace level, but must be redacted at debug level | ||
// since they can contain sensitive information. | ||
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 = | ||
std::str::from_utf8(&output.stderr).unwrap_or("could not decode stderr as UTF-8"); | ||
return Err(CredentialsError::provider_error(format!( | ||
"Error retrieving credentials: external process exited with code {}. Stderr: {}", | ||
output.status, reason | ||
))); | ||
} | ||
|
||
let output = std::str::from_utf8(&output.stdout).map_err(|e| { | ||
CredentialsError::provider_error(format!( | ||
"Error retrieving credentials from external process: could not decode output as UTF-8: {}", | ||
e | ||
)) | ||
})?; | ||
|
||
match parse_credential_process_json_credentials(output) { | ||
Ok(RefreshableCredentials { | ||
access_key_id, | ||
secret_access_key, | ||
session_token, | ||
expiration, | ||
.. | ||
}) => Ok(Credentials::new( | ||
access_key_id, | ||
secret_access_key, | ||
Some(session_token.to_string()), | ||
expiration.into(), | ||
"CredentialProcess", | ||
)), | ||
Err(invalid) => Err(CredentialsError::provider_error(format!( | ||
"Error retrieving credentials from external process, could not parse response: {}", | ||
invalid | ||
))), | ||
} | ||
} | ||
} | ||
|
||
/// Deserialize a credential_process response from a string | ||
/// | ||
/// Returns an error if the response cannot be successfully parsed or is missing keys. | ||
/// | ||
/// Keys are case insensitive. | ||
pub(crate) fn parse_credential_process_json_credentials( | ||
credentials_response: &str, | ||
) -> Result<RefreshableCredentials, InvalidJsonCredentials> { | ||
let mut version = None; | ||
let mut access_key_id = None; | ||
let mut secret_access_key = None; | ||
let mut session_token = None; | ||
let mut expiration = None; | ||
json_parse_loop(credentials_response.as_bytes(), |key, value| { | ||
match (key, value) { | ||
/* | ||
"Version": 1, | ||
"AccessKeyId": "ASIARTESTID", | ||
"SecretAccessKey": "TESTSECRETKEY", | ||
"SessionToken": "TESTSESSIONTOKEN", | ||
"Expiration": "2022-05-02T18:36:00+00:00" | ||
*/ | ||
(key, Token::ValueNumber { value, .. }) if key.eq_ignore_ascii_case("Version") => { | ||
version = Some(value.to_i32()) | ||
} | ||
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccessKeyId") => { | ||
access_key_id = Some(value.to_unescaped()?) | ||
} | ||
(key, Token::ValueString { value, .. }) | ||
if key.eq_ignore_ascii_case("SecretAccessKey") => | ||
{ | ||
secret_access_key = Some(value.to_unescaped()?) | ||
} | ||
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("SessionToken") => { | ||
session_token = Some(value.to_unescaped()?) | ||
} | ||
(key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("Expiration") => { | ||
expiration = Some(value.to_unescaped()?) | ||
} | ||
|
||
_ => {} | ||
}; | ||
Ok(()) | ||
})?; | ||
|
||
match version { | ||
Some(1) => { | ||
let access_key_id = | ||
access_key_id.ok_or(InvalidJsonCredentials::MissingField("AccessKeyId"))?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as a minor nit for readability, I'd probably handle the error cases & abort in the match then have the bulk of the code be un-nested |
||
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, | ||
}) | ||
} | ||
None => Err(InvalidJsonCredentials::MissingField("Version")), | ||
Some(version) => Err(InvalidJsonCredentials::InvalidField { | ||
field: "version", | ||
err: format!("unknown version number: {}", version).into(), | ||
}), | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use crate::credential_process::CredentialProcessProvider; | ||
use aws_smithy_types::date_time::Format; | ||
use aws_smithy_types::DateTime; | ||
use aws_types::credentials::ProvideCredentials; | ||
use std::time::SystemTime; | ||
|
||
#[tokio::test] | ||
async fn test_credential_process() { | ||
let provider = CredentialProcessProvider::new(String::from( | ||
r#"echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "Expiration": "2022-05-02T18:36:00+00:00" }'"#, | ||
)); | ||
let creds = provider.provide_credentials().await.expect("valid creds"); | ||
assert_eq!(creds.access_key_id(), "ASIARTESTID"); | ||
assert_eq!(creds.secret_access_key(), "TESTSECRETKEY"); | ||
assert_eq!(creds.session_token(), Some("TESTSESSIONTOKEN")); | ||
assert_eq!( | ||
creds.expiry(), | ||
Some( | ||
SystemTime::try_from( | ||
DateTime::from_str("2022-05-02T18:36:00+00:00", Format::DateTime) | ||
.expect("static datetime") | ||
) | ||
.expect("static datetime") | ||
) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need the
if windows / else
sincestd::process::Command::new
will use thePATH
to find the executable if an absolute path wasn't given: https://doc.rust-lang.org/stable/std/process/struct.Command.html#method.newThe SDKs don't support things like shell aliases or argument variable substitution, so it shouldn't run through a shell.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly, this is the way the Go V2 SDK and Java V2 SDKs are doing it. Boto3 looks like it does its own platform-based splitting, with its own implementation for Windows and using
shlex
for other platforms. I'll dig into this some more to figure out what we should do for Rust.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not going to block on this since Go V2 and Java V2 are doing it this way.