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 SSO #1051

Merged
merged 8 commits into from
Jan 11, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/pull-request-bot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ jobs:
toolchain: ${{ env.rust_version }}
default: true
- name: Generate doc preview
# Only generate three of the smallest services since the doc build can be very large. One of these must be
# STS since aws-config depends on it. STS and Transcribe Streaming and DynamoDB (paginators/waiters) were chosen
# Only generate three of the smallest services since the doc build can be very large. STS and SSO must be
# included since aws-config depends on them. Transcribe Streaming and DynamoDB (paginators/waiters) were chosen
# below to stay small while still representing most features. Combined, they are about ~20MB at time of writing.
run: |
./gradlew -Paws.services=+sts,+transcribestreaming,+dynamodb :aws:sdk:assemble
./gradlew -Paws.services=+sts,+sso,+transcribestreaming,+dynamodb :aws:sdk:assemble

# Copy the Server runtime crate(s) in
cp -r rust-runtime/aws-smithy-http-server aws/sdk/build/aws-sdk/sdk
Expand Down
6 changes: 6 additions & 0 deletions aws/rust-runtime/aws-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ default = ["rustls", "rt-tokio"]

[dependencies]
aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sdk/sts", default-features = false }
aws-sdk-sso = { path = "../../sdk/build/aws-sdk/sdk/sso", default-features = false }
aws-smithy-async = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-async" }
aws-smithy-client = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-client" }
aws-smithy-types = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-types" }
Expand All @@ -29,6 +30,11 @@ aws-http = { path = "../../sdk/build/aws-sdk/sdk/aws-http" }
aws-smithy-http = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-http-tower = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-http-tower" }
aws-smithy-json = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-json" }

# implementation detail of SSO credential caching
ring = "0.16"
hex = "0.4.3"

bytes = "1.1.0"
http = "0.2.4"
tower = { version = "0.4.8" }
Expand Down
3 changes: 3 additions & 0 deletions aws/rust-runtime/aws-config/src/default_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,9 @@ pub mod credentials {
make_test!(ecs_assume_role);
make_test!(ecs_credentials);

make_test!(sso_assume_role);
make_test!(sso_no_token_file);

#[tokio::test]
async fn profile_name_override() {
let (_, conf) =
Expand Down
62 changes: 62 additions & 0 deletions aws/rust-runtime/aws-config/src/fs_util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

use aws_types::os_shim_internal;

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum Os {
Windows,
NotWindows,
}

impl Os {
pub fn real() -> Self {
match std::env::consts::OS {
"windows" => Os::Windows,
_ => Os::NotWindows,
}
}
}

/// Resolve a home directory given a set of environment variables
pub(crate) fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option<String> {
if let Ok(home) = env_var.get("HOME") {
tracing::debug!(src = "HOME", "loaded home directory");
return Some(home);
}

if os == Os::Windows {
if let Ok(home) = env_var.get("USERPROFILE") {
tracing::debug!(src = "USERPROFILE", "loaded home directory");
return Some(home);
}

let home_drive = env_var.get("HOMEDRIVE");
let home_path = env_var.get("HOMEPATH");
tracing::debug!(src = "HOMEDRIVE/HOMEPATH", "loaded home directory");
if let (Ok(mut drive), Ok(path)) = (home_drive, home_path) {
drive.push_str(&path);
return Some(drive);
}
}
None
}

#[cfg(test)]
mod test {
use super::*;
use aws_types::os_shim_internal::Env;

#[test]
fn homedir_profile_only_windows() {
// windows specific variables should only be considered when the platform is windows
let env = Env::from_slice(&[("USERPROFILE", "C:\\Users\\name")]);
assert_eq!(
home_dir(&env, Os::Windows),
Some("C:\\Users\\name".to_string())
);
assert_eq!(home_dir(&env, Os::NotWindows), None);
}
}
124 changes: 69 additions & 55 deletions aws/rust-runtime/aws-config/src/json_credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ pub(crate) enum InvalidJsonCredentials {
/// The response was missing a required field
MissingField(&'static str),

/// A field was invalid
InvalidField {
field: &'static str,
err: Box<dyn Error + Send + Sync>,
},

/// Another unhandled error occurred
Other(Cow<'static, str>),
}
Expand Down Expand Up @@ -48,6 +54,9 @@ impl Display for InvalidJsonCredentials {
field
),
InvalidJsonCredentials::Other(msg) => write!(f, "{}", msg),
InvalidJsonCredentials::InvalidField { field, err } => {
write!(f, "Invalid field in response: `{}`. {}", field, err)
}
}
}
}
Expand Down Expand Up @@ -99,68 +108,34 @@ pub(crate) enum JsonCredentials<'a> {
pub(crate) fn parse_json_credentials(
credentials_response: &str,
) -> Result<JsonCredentials, InvalidJsonCredentials> {
let mut tokens = json_token_iter(credentials_response.as_bytes()).peekable();
let mut code = None;
let mut access_key_id = None;
let mut secret_access_key = None;
let mut session_token = None;
let mut expiration = None;
let mut message = None;
if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) {
return Err(InvalidJsonCredentials::JsonError(
"expected a JSON document starting with `{`".into(),
));
}
loop {
match tokens.next().transpose()? {
Some(Token::EndObject { .. }) => break,
Some(Token::ObjectKey { key, .. }) => {
if let Some(Ok(Token::ValueString { value, .. })) = tokens.peek() {
match key.to_unescaped()? {
/*
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId" : "accessKey",
"SecretAccessKey" : "secret",
"Token" : "token",
"Expiration" : "....",
"LastUpdated" : "2009-11-23T0:00:00Z"
*/
c if c.eq_ignore_ascii_case("Code") => code = Some(value.to_unescaped()?),
c if c.eq_ignore_ascii_case("AccessKeyId") => {
access_key_id = Some(value.to_unescaped()?)
}
c if c.eq_ignore_ascii_case("SecretAccessKey") => {
secret_access_key = Some(value.to_unescaped()?)
}
c if c.eq_ignore_ascii_case("Token") => {
session_token = Some(value.to_unescaped()?)
}
c if c.eq_ignore_ascii_case("Expiration") => {
expiration = Some(value.to_unescaped()?)
}
json_parse_loop(credentials_response.as_bytes(), |key, value| {
match key {
/*
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId" : "accessKey",
"SecretAccessKey" : "secret",
"Token" : "token",
"Expiration" : "....",
"LastUpdated" : "2009-11-23T0:00:00Z"
*/
c if c.eq_ignore_ascii_case("Code") => code = Some(value),
c if c.eq_ignore_ascii_case("AccessKeyId") => access_key_id = Some(value),
c if c.eq_ignore_ascii_case("SecretAccessKey") => secret_access_key = Some(value),
c if c.eq_ignore_ascii_case("Token") => session_token = Some(value),
c if c.eq_ignore_ascii_case("Expiration") => expiration = Some(value),

// Error case handling: message will be set
c if c.eq_ignore_ascii_case("Message") => {
message = Some(value.to_unescaped()?)
}
_ => {}
}
}
skip_value(&mut tokens)?;
}
other => {
return Err(InvalidJsonCredentials::Other(
format!("expected object key, found: {:?}", other,).into(),
));
}
// Error case handling: message will be set
c if c.eq_ignore_ascii_case("Message") => message = Some(value),
_ => {}
}
}
if tokens.next().is_some() {
return Err(InvalidJsonCredentials::Other(
"found more JSON tokens after completing parsing".into(),
));
}
})?;
match code {
// IMDS does not appear to reply with a `Code` missing, but documentation indicates it
// may be possible
Expand All @@ -175,7 +150,10 @@ pub(crate) fn parse_json_credentials(
expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?;
let expiration = SystemTime::try_from(
DateTime::from_str(expiration.as_ref(), Format::DateTime).map_err(|err| {
InvalidJsonCredentials::Other(format!("invalid date: {}", err).into())
InvalidJsonCredentials::InvalidField {
field: "Expiration",
err: err.into(),
}
})?,
)
.map_err(|_| {
Expand All @@ -197,6 +175,42 @@ pub(crate) fn parse_json_credentials(
}
}

pub(crate) fn json_parse_loop<'a>(
input: &'a [u8],
mut f: impl FnMut(Cow<'a, str>, Cow<'a, str>),
) -> Result<(), InvalidJsonCredentials> {
let mut tokens = json_token_iter(input).peekable();
if !matches!(tokens.next().transpose()?, Some(Token::StartObject { .. })) {
return Err(InvalidJsonCredentials::JsonError(
"expected a JSON document starting with `{`".into(),
));
}
loop {
match tokens.next().transpose()? {
Some(Token::EndObject { .. }) => break,
Some(Token::ObjectKey { key, .. }) => {
if let Some(Ok(Token::ValueString { value, .. })) = tokens.peek() {
let key = key.to_unescaped()?;
let value = value.to_unescaped()?;
f(key, value)
}
skip_value(&mut tokens)?;
}
other => {
return Err(InvalidJsonCredentials::Other(
format!("expected object key, found: {:?}", other,).into(),
rcoh marked this conversation as resolved.
Show resolved Hide resolved
));
}
}
}
if tokens.next().is_some() {
return Err(InvalidJsonCredentials::Other(
"found more JSON tokens after completing parsing".into(),
));
}
Ok(())
}

#[cfg(test)]
mod test {
use crate::json_credentials::{
Expand Down
2 changes: 2 additions & 0 deletions aws/rust-runtime/aws-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ pub mod imds;

mod json_credentials;

mod fs_util;
mod http_provider;
pub mod sso;

// Re-export types from smithy-types
pub use aws_smithy_types::retry::RetryConfig;
Expand Down
Loading