diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index a393436e73..2a43647379 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -38,6 +38,9 @@ ring = "0.16" hex = "0.4.3" zeroize = "1" +# implementation detail of IMDS credentials provider +fastrand = "1" + bytes = "1.1.0" http = "0.2.4" tower = { version = "0.4.8" } diff --git a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs index c2a31b5e79..fe000dab92 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/credentials.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/credentials.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use aws_credential_types::provider::{self, future, ProvideCredentials}; +use aws_smithy_async::rt::sleep::AsyncSleep; use tracing::Instrument; use crate::environment::credentials::EnvironmentVariableCredentialsProvider; @@ -74,6 +75,17 @@ impl DefaultCredentialsChain { .instrument(tracing::debug_span!("provide_credentials", provider = %"default_chain")) .await } + + async fn credentials_with_timeout( + &self, + sleeper: std::sync::Arc, + timeout: std::time::Duration, + ) -> provider::Result { + self.provider_chain + .provide_credentials_with_timeout(sleeper, timeout) + .instrument(tracing::debug_span!("provide_credentials", provider = %"default_chain")) + .await + } } impl ProvideCredentials for DefaultCredentialsChain { @@ -83,6 +95,17 @@ impl ProvideCredentials for DefaultCredentialsChain { { future::ProvideCredentials::new(self.credentials()) } + + fn provide_credentials_with_timeout<'a>( + &'a self, + sleeper: std::sync::Arc, + timeout: std::time::Duration, + ) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::new(self.credentials_with_timeout(sleeper, timeout)) + } } /// Builder for [`DefaultCredentialsChain`](DefaultCredentialsChain) @@ -238,6 +261,7 @@ mod test { "./test-data/default-provider-chain/", stringify!($name) )) + .await .unwrap() .$func(|conf| async { crate::default_provider::credentials::Builder::default() @@ -248,6 +272,26 @@ mod test { .await } }; + ($name: ident, $provider_config_builder: expr) => { + #[traced_test] + #[tokio::test] + async fn $name() { + crate::test_case::TestEnvironment::from_dir(concat!( + "./test-data/default-provider-chain/", + stringify!($name) + )) + .await + .unwrap() + .with_provider_config($provider_config_builder) + .execute(|conf| async { + crate::default_provider::credentials::Builder::default() + .configure(conf) + .build() + .await + }) + .await + } + }; } make_test!(prefer_environment); @@ -264,11 +308,23 @@ mod test { make_test!(imds_no_iam_role); make_test!(imds_default_chain_error); - make_test!(imds_default_chain_success); + make_test!(imds_default_chain_success, |config| { + config.with_time_source(aws_credential_types::time_source::TimeSource::testing( + &aws_credential_types::time_source::TestingTimeSource::new(std::time::UNIX_EPOCH), + )) + }); make_test!(imds_assume_role); - make_test!(imds_config_with_no_creds); + make_test!(imds_config_with_no_creds, |config| { + config.with_time_source(aws_credential_types::time_source::TimeSource::testing( + &aws_credential_types::time_source::TestingTimeSource::new(std::time::UNIX_EPOCH), + )) + }); make_test!(imds_disabled); - make_test!(imds_default_chain_retries); + make_test!(imds_default_chain_retries, |config| { + config.with_time_source(aws_credential_types::time_source::TimeSource::testing( + &aws_credential_types::time_source::TestingTimeSource::new(std::time::UNIX_EPOCH), + )) + }); make_test!(ecs_assume_role); make_test!(ecs_credentials); @@ -279,11 +335,12 @@ mod test { #[tokio::test] async fn profile_name_override() { - let (_, conf) = + let conf = TestEnvironment::from_dir("./test-data/default-provider-chain/profile_static_keys") + .await .unwrap() .provider_config() - .await; + .clone(); let provider = DefaultCredentialsChain::builder() .profile_name("secondary") .configure(conf) diff --git a/aws/rust-runtime/aws-config/src/imds/credentials.rs b/aws/rust-runtime/aws-config/src/imds/credentials.rs index c598f4ba72..69c1b7e0f6 100644 --- a/aws/rust-runtime/aws-config/src/imds/credentials.rs +++ b/aws/rust-runtime/aws-config/src/imds/credentials.rs @@ -14,11 +14,20 @@ use crate::imds::client::LazyClient; use crate::json_credentials::{parse_json_credentials, JsonCredentials, RefreshableCredentials}; use crate::provider_config::ProviderConfig; use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials}; +use aws_credential_types::time_source::TimeSource; use aws_credential_types::Credentials; +use aws_smithy_async::future::timeout::Timeout; +use aws_smithy_async::rt::sleep::AsyncSleep; use aws_types::os_shim_internal::Env; +use fastrand; use std::borrow::Cow; use std::error::Error as StdError; use std::fmt; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; +use tokio::sync::RwLock; + +const CREDENTIAL_EXPIRATION_INTERVAL: Duration = Duration::from_secs(15 * 60); #[derive(Debug)] struct ImdsCommunicationError { @@ -45,6 +54,8 @@ pub struct ImdsCredentialsProvider { client: LazyClient, env: Env, profile: Option, + time_source: TimeSource, + last_retrieved_credentials: Arc>>, } /// Builder for [`ImdsCredentialsProvider`] @@ -53,6 +64,7 @@ pub struct Builder { provider_config: Option, profile_override: Option, imds_override: Option, + last_retrieved_credentials: Option, } impl Builder { @@ -86,6 +98,13 @@ impl Builder { self } + #[allow(dead_code)] + #[cfg(test)] + fn last_retrieved_credentials(mut self, credentials: Credentials) -> Self { + self.last_retrieved_credentials = Some(credentials); + self + } + /// Create an [`ImdsCredentialsProvider`] from this builder. pub fn build(self) -> ImdsCredentialsProvider { let provider_config = self.provider_config.unwrap_or_default(); @@ -102,6 +121,8 @@ impl Builder { client, env, profile: self.profile_override, + time_source: provider_config.time_source(), + last_retrieved_credentials: Arc::new(RwLock::new(self.last_retrieved_credentials)), } } } @@ -117,6 +138,17 @@ impl ProvideCredentials for ImdsCredentialsProvider { { future::ProvideCredentials::new(self.credentials()) } + + fn provide_credentials_with_timeout<'a>( + &'a self, + sleeper: Arc, + timeout: Duration, + ) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + future::ProvideCredentials::new(self.credentials_with_timeout(sleeper, timeout)) + } } impl ImdsCredentialsProvider { @@ -167,7 +199,7 @@ impl ImdsCredentialsProvider { } } - async fn credentials(&self) -> provider::Result { + async fn retrieve_credentials(&self) -> provider::Result { if self.imds_disabled() { tracing::debug!("IMDS disabled because $AWS_EC2_METADATA_DISABLED was set to `true`"); return Err(CredentialsError::not_loaded( @@ -196,13 +228,18 @@ impl ImdsCredentialsProvider { session_token, expiration, .. - })) => Ok(Credentials::new( - access_key_id, - secret_access_key, - Some(session_token.to_string()), - expiration.into(), - "IMDSv2", - )), + })) => { + let expiration = self.extend_expiration(expiration); + let creds = Credentials::new( + access_key_id, + secret_access_key, + Some(session_token.to_string()), + expiration.into(), + "IMDSv2", + ); + *self.last_retrieved_credentials.write().await = Some(creds.clone()); + Ok(creds) + } Ok(JsonCredentials::Error { code, message }) if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS => { @@ -222,6 +259,61 @@ impl ImdsCredentialsProvider { Err(invalid) => Err(CredentialsError::unhandled(invalid)), } } + + async fn credentials(&self) -> provider::Result { + match self.retrieve_credentials().await { + creds @ Ok(_) => creds, + err => match &*self.last_retrieved_credentials.read().await { + Some(creds) => Ok(creds.clone()), + _ => err, + }, + } + } + + async fn credentials_with_timeout( + &self, + sleeper: Arc, + timeout: Duration, + ) -> provider::Result { + let sleep_future = sleeper.sleep(timeout); + let timeout_future = Timeout::new(self.provide_credentials(), sleep_future); + match timeout_future.await { + Ok(creds) => creds, + _ => match &*self.last_retrieved_credentials.read().await { + Some(creds) => Ok(creds.clone()), + _ => Err(CredentialsError::provider_timed_out(timeout)), + }, + } + } + + // Extend the cached expiration time if necessary + // + // This allows continued use of the credentials even when IMDS returns expired ones. + fn extend_expiration(&self, expiration: SystemTime) -> SystemTime { + let rng = fastrand::Rng::with_seed( + self.time_source + .now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("now should be after UNIX EPOCH") + .as_secs(), + ); + // calculate credentials' refresh offset with jitter + let refresh_offset = + CREDENTIAL_EXPIRATION_INTERVAL + Duration::from_secs(rng.u64(120..=600)); + let new_expiry = self.time_source.now() + refresh_offset; + + if new_expiry < expiration { + return expiration; + } + + tracing::warn!( + "Attempting credential expiration extension due to a credential service availability issue. \ + A refresh of these credentials will be attempted again within the next {:.2} minutes.", + refresh_offset.as_secs_f64() / 60.0, + ); + + new_expiry + } } #[cfg(test)] @@ -232,6 +324,7 @@ mod test { use crate::imds::credentials::ImdsCredentialsProvider; use aws_credential_types::provider::ProvideCredentials; use aws_smithy_client::test_connection::TestConnection; + use tracing_test::traced_test; const TOKEN_A: &str = "token_a"; @@ -259,13 +352,86 @@ mod test { imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST2\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"), ), ]); - let client = ImdsCredentialsProvider::builder() + let provider = ImdsCredentialsProvider::builder() .imds_client(make_client(&connection).await) .build(); - let creds1 = client.provide_credentials().await.expect("valid creds"); - let creds2 = client.provide_credentials().await.expect("valid creds"); + let creds1 = provider.provide_credentials().await.expect("valid creds"); + let creds2 = provider.provide_credentials().await.expect("valid creds"); assert_eq!(creds1.access_key_id(), "ASIARTEST"); assert_eq!(creds2.access_key_id(), "ASIARTEST2"); connection.assert_requests_match(&[]); } + + #[tokio::test] + #[traced_test] + async fn log_message_informing_expired_credentials_are_used() { + let connection = TestConnection::new(vec![ + ( + token_request("http://169.254.169.254", 21600), + token_response(21600, TOKEN_A), + ), + ( + imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A), + imds_response(r#"profile-name"#), + ), + ( + imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A), + imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"), + ), + ]); + let provider = ImdsCredentialsProvider::builder() + .imds_client(make_client(&connection).await) + .build(); + let _ = provider.provide_credentials().await.expect("valid creds"); + connection.assert_requests_match(&[]); + assert!(logs_contain("Attempting credential expiration extension")); + } + + #[tokio::test] + #[cfg(any(feature = "rustls", feature = "native-tls"))] + async fn read_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() { + let client = crate::imds::Client::builder() + // 240.* can never be resolved + .endpoint(http::Uri::from_static("http://240.0.0.0")) + .build() + .await + .expect("valid client"); + let provider = ImdsCredentialsProvider::builder() + .imds_client(client) + .last_retrieved_credentials(aws_credential_types::Credentials::for_tests()) + .build(); + let actual = provider + .provide_credentials_with_timeout( + aws_smithy_async::rt::sleep::default_async_sleep().unwrap(), + std::time::Duration::from_secs(5), + ) + .await + .unwrap(); + assert_eq!(actual, aws_credential_types::Credentials::for_tests()); + } + + #[tokio::test] + #[cfg(any(feature = "rustls", feature = "native-tls"))] + async fn read_timeout_during_credentials_refresh_should_error_without_last_retrieved_credentials( + ) { + let client = crate::imds::Client::builder() + // 240.* can never be resolved + .endpoint(http::Uri::from_static("http://240.0.0.0")) + .build() + .await + .expect("valid client"); + let provider = ImdsCredentialsProvider::builder() + .imds_client(client) + .build(); + let actual = provider + .provide_credentials_with_timeout( + aws_smithy_async::rt::sleep::default_async_sleep().unwrap(), + std::time::Duration::from_secs(5), + ) + .await; + assert!(matches!( + actual, + Err(aws_credential_types::provider::error::CredentialsError::CredentialsNotLoaded(_)) + )); + } } diff --git a/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs b/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs index 84bd0b5bfe..cfa0f145d5 100644 --- a/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs +++ b/aws/rust-runtime/aws-config/src/meta/credentials/chain.rs @@ -4,8 +4,12 @@ */ use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials}; +use aws_smithy_async::future::timeout::Timeout; +use aws_smithy_async::rt::sleep::AsyncSleep; use aws_smithy_types::error::display::DisplayErrorContext; use std::borrow::Cow; +use std::sync::Arc; +use std::time::Duration; use tracing::Instrument; /// Credentials provider that checks a series of inner providers @@ -95,6 +99,34 @@ impl CredentialsProviderChain { "no providers in chain provided credentials", )) } + + async fn credentials_with_timeout( + &self, + sleeper: Arc, + timeout: Duration, + ) -> provider::Result { + for (name, provider) in &self.providers { + let span = tracing::debug_span!("load_credentials", provider = %name); + match provider + .provide_credentials_with_timeout(Arc::clone(&sleeper), timeout) + .instrument(span) + .await + { + Ok(credentials) => { + tracing::debug!(provider = %name, "loaded credentials"); + return Ok(credentials); + } + Err(err @ CredentialsError::ProviderTimedOut(_)) => { + tracing::debug!(provider = %name, context = %DisplayErrorContext(&err), "provider in chain did not provide credentials"); + } + Err(err) => { + tracing::warn!(provider = %name, error = %DisplayErrorContext(&err), "provider failed to provide credentials"); + return Err(err); + } + } + } + Err(CredentialsError::provider_timed_out(timeout)) + } } impl ProvideCredentials for CredentialsProviderChain { @@ -104,4 +136,30 @@ impl ProvideCredentials for CredentialsProviderChain { { future::ProvideCredentials::new(self.credentials()) } + + fn provide_credentials_with_timeout<'a>( + &'a self, + sleeper: Arc, + timeout: Duration, + ) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + // We need to give `timeout` to the whole chain as well as to each credentials provider in the chain. + // One could argue that if the whole chain has `timeout` anyway, there is no point in having it + // in individual providers. This is due to the fact that the other method `provide_credentials` + // does not respect provider-specific read timeout behavior, e.g. the IMDS credentials provider + // wants to provide expired credentials, if any, in the case of read timeout. + let sleep_future = sleeper.sleep(timeout); + let timeout_future = Timeout::new( + self.credentials_with_timeout(sleeper, timeout), + sleep_future, + ); + future::ProvideCredentials::new(async move { + match timeout_future.await { + Ok(creds) => creds, + Err(_) => Err(CredentialsError::provider_timed_out(timeout)), + } + }) + } } diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index 845197f95e..ba7fb9241e 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -470,6 +470,7 @@ mod test { "./test-data/profile-provider/", stringify!($name) )) + .await .unwrap() .execute(|conf| async move { Builder::default().configure(&conf).build() }) .await diff --git a/aws/rust-runtime/aws-config/src/test_case.rs b/aws/rust-runtime/aws-config/src/test_case.rs index c8a5c13238..38593fcb46 100644 --- a/aws/rust-runtime/aws-config/src/test_case.rs +++ b/aws/rust-runtime/aws-config/src/test_case.rs @@ -65,11 +65,10 @@ impl From for Credentials { /// - an `http-traffic.json` file containing an http traffic log from [`dvr`](aws_smithy_client::dvr) /// - a `test-case.json` file defining the expected output of the test pub(crate) struct TestEnvironment { - env: Env, - fs: Fs, - network_traffic: NetworkTraffic, metadata: Metadata, base_dir: PathBuf, + connector: ReplayingConnection, + provider_config: ProviderConfig, } /// Connector which expects no traffic @@ -131,7 +130,7 @@ pub(crate) struct Metadata { } impl TestEnvironment { - pub(crate) fn from_dir(dir: impl AsRef) -> Result> { + pub(crate) async fn from_dir(dir: impl AsRef) -> Result> { let dir = dir.as_ref(); let env = std::fs::read_to_string(dir.join("env.json")) .map_err(|e| format!("failed to load env: {}", e))?; @@ -147,27 +146,32 @@ impl TestEnvironment { &std::fs::read_to_string(dir.join("test-case.json")) .map_err(|e| format!("failed to load test case: {}", e))?, )?; + let connector = ReplayingConnection::new(network_traffic.events().clone()); + let provider_config = ProviderConfig::empty() + .with_fs(fs.clone()) + .with_env(env.clone()) + .with_http_connector(DynConnector::new(connector.clone())) + .with_sleep(TokioSleep::new()) + .load_default_region() + .await; Ok(TestEnvironment { base_dir: dir.into(), - env, - fs, - network_traffic, metadata, + connector, + provider_config, }) } - pub(crate) async fn provider_config(&self) -> (ReplayingConnection, ProviderConfig) { - let connector = ReplayingConnection::new(self.network_traffic.events().clone()); - ( - connector.clone(), - ProviderConfig::empty() - .with_fs(self.fs.clone()) - .with_env(self.env.clone()) - .with_http_connector(DynConnector::new(connector.clone())) - .with_sleep(TokioSleep::new()) - .load_default_region() - .await, - ) + pub(crate) fn with_provider_config(mut self, provider_config_builder: F) -> Self + where + F: Fn(ProviderConfig) -> ProviderConfig, + { + self.provider_config = provider_config_builder(self.provider_config.clone()); + self + } + + pub(crate) fn provider_config(&self) -> &ProviderConfig { + &self.provider_config } #[allow(unused)] @@ -182,10 +186,13 @@ impl TestEnvironment { P: ProvideCredentials, { // swap out the connector generated from `http-traffic.json` for a real connector: - let (_test_connector, config) = self.provider_config().await; - let live_connector = default_connector(&Default::default(), config.sleep()).unwrap(); + let live_connector = + default_connector(&Default::default(), self.provider_config.sleep()).unwrap(); let live_connector = RecordingConnection::new(live_connector); - let config = config.with_http_connector(DynConnector::new(live_connector.clone())); + let config = self + .provider_config + .clone() + .with_http_connector(DynConnector::new(live_connector.clone())); let provider = make_provider(config).await; let result = provider.provide_credentials().await; std::fs::write( @@ -206,9 +213,11 @@ impl TestEnvironment { F: Future, P: ProvideCredentials, { - let (connector, config) = self.provider_config().await; - let recording_connector = RecordingConnection::new(connector); - let config = config.with_http_connector(DynConnector::new(recording_connector.clone())); + let recording_connector = RecordingConnection::new(self.connector.clone()); + let config = self + .provider_config + .clone() + .with_http_connector(DynConnector::new(recording_connector.clone())); let provider = make_provider(config).await; let result = provider.provide_credentials().await; std::fs::write( @@ -229,14 +238,15 @@ impl TestEnvironment { F: Future, P: ProvideCredentials, { - let (connector, conf) = self.provider_config().await; - let provider = make_provider(conf).await; + let provider = make_provider(self.provider_config.clone()).await; let result = provider.provide_credentials().await; tokio::time::pause(); self.log_info(); self.check_results(result); // todo: validate bodies - match connector + match self + .connector + .clone() .validate( &["CONTENT-TYPE", "x-aws-ec2-metadata-token"], |_expected, _actual| Ok(()), diff --git a/aws/rust-runtime/aws-credential-types/src/cache/lazy_caching.rs b/aws/rust-runtime/aws-credential-types/src/cache/lazy_caching.rs index c448e862bd..a47c2bc885 100644 --- a/aws/rust-runtime/aws-credential-types/src/cache/lazy_caching.rs +++ b/aws/rust-runtime/aws-credential-types/src/cache/lazy_caching.rs @@ -8,7 +8,6 @@ use std::sync::Arc; use std::time::{Duration, Instant}; -use aws_smithy_async::future::timeout::Timeout; use aws_smithy_async::rt::sleep::AsyncSleep; use tracing::{debug, info, info_span, Instrument}; @@ -57,7 +56,7 @@ impl ProvideCachedCredentials for LazyCredentialsCache { { let now = self.time.now(); let provider = self.provider.clone(); - let timeout_future = self.sleeper.sleep(self.load_timeout); + let sleeper = Arc::clone(&self.sleeper); let load_timeout = self.load_timeout; let cache = self.cache.clone(); let default_credential_expiration = self.default_credential_expiration; @@ -72,15 +71,14 @@ impl ProvideCachedCredentials for LazyCredentialsCache { // There may be other threads also loading simultaneously, but this is OK // since the futures are not eagerly executed, and the cache will only run one // of them. - let future = Timeout::new(provider.provide_credentials(), timeout_future); let start_time = Instant::now(); let result = cache .get_or_load(|| { let span = info_span!("lazy_load_credentials"); async move { - let credentials = future.await.map_err(|_err| { - CredentialsError::provider_timed_out(load_timeout) - })??; + let credentials = provider + .provide_credentials_with_timeout(sleeper, load_timeout) + .await?; // If the credentials don't have an expiration time, then create a default one let expiry = credentials .expiry() @@ -166,13 +164,21 @@ mod builder { self } - #[doc(hidden)] // because they only exist for tests + /// Time source of `LazyCredentialsCache`. + /// + /// This is available for tests that need to advance time programmatically, in which + /// case [`TestingTimeSource`](crate::time_source::TestingTimeSource) is specified. + #[doc(hidden)] pub fn time_source(mut self, time_source: TimeSource) -> Self { self.set_time_source(Some(time_source)); self } - #[doc(hidden)] // because they only exist for tests + /// Time source of `LazyCredentialsCache`. + /// + /// This is available for tests that need to advance time programmatically, in which + /// case [`TestingTimeSource`](crate::time_source::TestingTimeSource) is specified. + #[doc(hidden)] pub fn set_time_source(&mut self, time_source: Option) -> &mut Self { self.time_source = time_source; self diff --git a/aws/rust-runtime/aws-credential-types/src/provider.rs b/aws/rust-runtime/aws-credential-types/src/provider.rs index 497887597c..7c93f6ad23 100644 --- a/aws/rust-runtime/aws-credential-types/src/provider.rs +++ b/aws/rust-runtime/aws-credential-types/src/provider.rs @@ -71,8 +71,12 @@ construct credentials from hardcoded values. //! } //! ``` +use aws_smithy_async::{future::timeout::Timeout, rt::sleep::AsyncSleep}; + use crate::Credentials; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; + +use self::error::CredentialsError; /// Credentials provider errors pub mod error { @@ -280,6 +284,28 @@ pub trait ProvideCredentials: Send + Sync + std::fmt::Debug { fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> where Self: 'a; + + /// Returns a future that provides credentials within the given `timeout`. + /// + /// The default implementation races [`provide_credentials`](ProvideCredentials::provide_credentials) against + /// a timeout future created from `timeout`. + fn provide_credentials_with_timeout<'a>( + &'a self, + sleeper: Arc, + timeout: Duration, + ) -> future::ProvideCredentials<'a> + where + Self: 'a, + { + let timeout_future = sleeper.sleep(timeout); + let future = Timeout::new(self.provide_credentials(), timeout_future); + future::ProvideCredentials::new(async move { + let credentials = future + .await + .map_err(|_err| CredentialsError::provider_timed_out(timeout))?; + credentials + }) + } } impl ProvideCredentials for Credentials { diff --git a/aws/sdk/integration-tests/s3/tests/imds_fixture.rs b/aws/sdk/integration-tests/s3/tests/imds_fixture.rs new file mode 100644 index 0000000000..356f758896 --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/imds_fixture.rs @@ -0,0 +1,86 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{ + sync::Arc, + time::{Duration, SystemTime}, +}; + +use aws_config::{ + imds::{self, credentials::ImdsCredentialsProvider}, + provider_config::ProviderConfig, + SdkConfig, +}; +use aws_credential_types::{ + cache::CredentialsCache, + time_source::{TestingTimeSource, TimeSource}, +}; +use aws_smithy_async::rt::sleep::TokioSleep; +use aws_smithy_client::{ + dvr::{Event, ReplayingConnection}, + erase::DynConnector, +}; +use aws_types::region::Region; + +pub(crate) struct TestFixture { + replayer: ReplayingConnection, + time_source: TestingTimeSource, +} + +impl TestFixture { + #[allow(dead_code)] + pub(crate) fn new(http_traffic_json_str: &str, start_time: SystemTime) -> Self { + let events: Vec = serde_json::from_str(http_traffic_json_str).unwrap(); + Self { + replayer: ReplayingConnection::new(events), + time_source: TestingTimeSource::new(start_time), + } + } + + #[allow(dead_code)] + pub(crate) async fn setup(&self) -> SdkConfig { + let time_source = TimeSource::testing(&self.time_source); + + let provider_config = ProviderConfig::empty() + .with_http_connector(DynConnector::new(self.replayer.clone())) + .with_sleep(TokioSleep::new()) + .with_time_source(time_source.clone()); + + let client = imds::client::Client::builder() + .configure(&provider_config) + .build() + .await + .unwrap(); + + let provider = ImdsCredentialsProvider::builder() + .configure(&provider_config) + .imds_client(client) + .build(); + + SdkConfig::builder() + .region(Region::from_static("us-east-1")) + .credentials_cache( + CredentialsCache::lazy_builder() + .time_source(time_source) + .into_credentials_cache(), + ) + .credentials_provider(Arc::new(provider)) + .http_connector(self.replayer.clone()) + .build() + } + + #[allow(dead_code)] + pub(crate) fn advance_time(&mut self, delta: Duration) { + self.time_source.advance(delta); + } + + #[allow(dead_code)] + pub(crate) async fn verify(self, headers: &[&str]) { + self.replayer + .validate(headers, |_, _| Ok(())) + .await + .unwrap(); + } +} diff --git a/aws/sdk/integration-tests/s3/tests/send-first-request-with-expired-creds.rs b/aws/sdk/integration-tests/s3/tests/send-first-request-with-expired-creds.rs new file mode 100644 index 0000000000..15b3ded2da --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/send-first-request-with-expired-creds.rs @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod imds_fixture; + +use std::{ + convert::Infallible, + time::{Duration, UNIX_EPOCH}, +}; + +use aws_sdk_s3::Client; + +#[tokio::test] +async fn test_request_should_be_sent_when_first_call_to_imds_returns_expired_credentials() { + // This represents the time of a request being made, 21 Sep 2021 17:41:25 GMT. + let time_of_request = UNIX_EPOCH + Duration::from_secs(1632246085); + + let test_fixture = imds_fixture::TestFixture::new( + include_str!("test-data/send-first-request-with-expired-creds.json"), + time_of_request, + ); + + let sdk_config = test_fixture.setup().await; + let s3_client = Client::new(&sdk_config); + + tokio::time::pause(); + + // The JSON file above specifies the credentials expiry is 21 Sep 2021 11:29:29 GMT, + // which is already invalid at the time of the request but will be made valid as the + // code execution will go through the expiration extension. + s3_client + .create_bucket() + .bucket("test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is0") + .customize() + .await + .unwrap() + .map_operation(|mut op| { + op.properties_mut().insert(time_of_request); + Result::Ok::<_, Infallible>(op) + }) + .unwrap() + .send() + .await + .unwrap(); + + // The fact that the authorization of a request exists implies that the request has + // been properly generated out of expired credentials. + test_fixture.verify(&["authorization"]).await; +} diff --git a/aws/sdk/integration-tests/s3/tests/send-request-after-500-response-from-imds.rs b/aws/sdk/integration-tests/s3/tests/send-request-after-500-response-from-imds.rs new file mode 100644 index 0000000000..397c365f77 --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/send-request-after-500-response-from-imds.rs @@ -0,0 +1,70 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod imds_fixture; + +use std::{ + convert::Infallible, + time::{Duration, UNIX_EPOCH}, +}; + +use aws_sdk_s3::Client; + +#[tokio::test] +async fn test_request_should_be_sent_with_expired_credentials_after_imds_returns_500_during_credentials_refresh( +) { + // This represents the time of a request being made, 21 Sep 2021 17:41:25 GMT. + let time_of_request = UNIX_EPOCH + Duration::from_secs(1632246085); + + let mut test_fixture = imds_fixture::TestFixture::new( + include_str!("test-data/send-request-after-500-response-from-imds.json"), + time_of_request, + ); + + let sdk_config = test_fixture.setup().await; + let s3_client = Client::new(&sdk_config); + + tokio::time::pause(); + + // Requests are made at 21 Sep 2021 17:41:25 GMT and 21 Sep 2021 23:41:25 GMT. + let time_of_first_request = time_of_request; + let time_of_second_request = UNIX_EPOCH + Duration::from_secs(1632267685); + + // The JSON file above specifies credentials will expire at between the two requests, 21 Sep 2021 23:33:13 GMT. + // The second request will receive response 500 from IMDS but `s3_client` will eventually + // be able to send it thanks to expired credentials held by `ImdsCredentialsProvider`. + for (i, time_of_request_to_s3) in [time_of_first_request, time_of_second_request] + .into_iter() + .enumerate() + { + s3_client + .create_bucket() + .bucket(format!( + "test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is{}", + i + )) + .customize() + .await + .unwrap() + .map_operation(|mut op| { + op.properties_mut().insert(time_of_request_to_s3); + Result::Ok::<_, Infallible>(op) + }) + .unwrap() + .send() + .await + .unwrap(); + + test_fixture.advance_time( + time_of_second_request + .duration_since(time_of_first_request) + .unwrap(), + ); + } + + // The fact that the authorization of each request exists implies that the requests have + // been properly generated out of expired credentials. + test_fixture.verify(&["authorization"]).await; +} diff --git a/aws/sdk/integration-tests/s3/tests/send-successive-requests-with-expired-creds.rs b/aws/sdk/integration-tests/s3/tests/send-successive-requests-with-expired-creds.rs new file mode 100644 index 0000000000..12ba8ed8e9 --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/send-successive-requests-with-expired-creds.rs @@ -0,0 +1,61 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod imds_fixture; + +use std::{ + convert::Infallible, + time::{Duration, UNIX_EPOCH}, +}; + +use aws_sdk_s3::Client; + +#[tokio::test] +async fn test_successive_requests_should_be_sent_with_expired_credentials_and_imds_being_called_only_once( +) { + // This represents the time of a request being made, 21 Sep 2021 17:41:25 GMT. + let time_of_request = UNIX_EPOCH + Duration::from_secs(1632246085); + + let test_fixture = imds_fixture::TestFixture::new( + include_str!("test-data/send-successive-requests-with-expired-creds.json"), + time_of_request, + ); + + let sdk_config = test_fixture.setup().await; + let s3_client = Client::new(&sdk_config); + + tokio::time::pause(); + + // The JSON file above specifies the credentials expiry is 21 Sep 2021 11:29:29 GMT, + // which is already invalid at the time of the request but will be made valid as the + // code execution will go through the expiration extension. + for i in 1..=3 { + // If IMDS were called more than once, the last `unwrap` would fail with an error looking like: + // panicked at 'called `Result::unwrap()` on an `Err` value: ConstructionFailure(ConstructionFailure { source: CredentialsStageError { ... } })' + // This is because the accompanying JSON file assumes that connection_id 4 (and 5) represents a request to S3, + // not to IMDS, so its response cannot be serialized into `Credentials`. + s3_client + .create_bucket() + .bucket(format!( + "test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is{}", + i + )) + .customize() + .await + .unwrap() + .map_operation(|mut op| { + op.properties_mut().insert(time_of_request); + Result::Ok::<_, Infallible>(op) + }) + .unwrap() + .send() + .await + .unwrap(); + } + + // The fact that the authorization of each request exists implies that the requests have + // been properly generated out of expired credentials. + test_fixture.verify(&["authorization"]).await; +} diff --git a/aws/sdk/integration-tests/s3/tests/test-data/send-first-request-with-expired-creds.json b/aws/sdk/integration-tests/s3/tests/test-data/send-first-request-with-expired-creds.json new file mode 100644 index 0000000000..c592b183dc --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/test-data/send-first-request-with-expired-creds.json @@ -0,0 +1,373 @@ +[ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "connection": [ + "close" + ], + "content-length": [ + "56" + ], + "content-type": [ + "text/plain" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "imdssesiontoken==" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", + "headers": { + "x-aws-ec2-metadata-token": [ + "imdssesiontoken==" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 1, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "content-type": [ + "text/plain" + ], + "last-modified": [ + "Tue, 21 Sep 2021 17:30:41 GMT" + ], + "connection": [ + "close" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ], + "content-length": [ + "21" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "accept-ranges": [ + "none" + ], + "server": [ + "EC2ws" + ] + } + } + } + } + } + }, + { + "connection_id": 1, + "action": { + "Data": { + "data": { + "Utf8": "imds-assume-role-test" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-assume-role-test", + "headers": { + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ], + "x-aws-ec2-metadata-token": [ + "imdssesiontoken==" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 2, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "connection": [ + "close" + ], + "server": [ + "EC2ws" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ], + "accept-ranges": [ + "none" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "last-modified": [ + "Tue, 21 Sep 2021 17:30:41 GMT" + ], + "content-type": [ + "text/plain" + ], + "content-length": [ + "1322" + ] + } + } + } + } + } + }, + { + "connection_id": 2, + "action": { + "Data": { + "data": { + "Utf8": "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-21T17:31:21Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARSTSBASE\",\n \"SecretAccessKey\" : \"secretbase\",\n \"Token\" : \"tokenbase\",\n \"Expiration\" : \"2021-09-21T11:29:29Z\"\n}" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 3, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is0.s3.us-east-1.amazonaws.com", + "headers": { + "x-amz-date": [ + "20210921T174125Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "content-type": [ + "application/xml" + ], + "user-agent": [ + "aws-sdk-rust/0.51.0 os/linux lang/rust/1.62.1" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.51.0 api/s3/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ASIARSTSBASE/20210921/us-east-1/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=021dadec3fb3769ff21165f1abf9516887d1f93c504a98e399ccc5df8adef01f" + ], + "content-length": [ + "0" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 3, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 3, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 3, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "content-length": [ + "0" + ], + "location": [ + "/test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is0" + ], + "x-amz-id-2": [ + "hndxcVIq5ILod5JoH+4ULZAi4lo6BXJvJm78Oxro48vNw5s4MFsZqKCHM0GIhYlDf/RWsnmmHpg=" + ], + "x-amz-request-id": [ + "YYP6QSB3XZ50PZ07" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 3, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 3, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } +] diff --git a/aws/sdk/integration-tests/s3/tests/test-data/send-request-after-500-response-from-imds.json b/aws/sdk/integration-tests/s3/tests/test-data/send-request-after-500-response-from-imds.json new file mode 100644 index 0000000000..83a992e2fe --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/test-data/send-request-after-500-response-from-imds.json @@ -0,0 +1,804 @@ +[ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "connection": [ + "close" + ], + "content-length": [ + "56" + ], + "content-type": [ + "text/plain" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "imdssesiontoken==" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", + "headers": { + "x-aws-ec2-metadata-token": [ + "imdssesiontoken==" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 1, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "content-type": [ + "text/plain" + ], + "last-modified": [ + "Tue, 21 Sep 2021 17:30:41 GMT" + ], + "connection": [ + "close" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ], + "content-length": [ + "21" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "accept-ranges": [ + "none" + ], + "server": [ + "EC2ws" + ] + } + } + } + } + } + }, + { + "connection_id": 1, + "action": { + "Data": { + "data": { + "Utf8": "imds-assume-role-test" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-assume-role-test", + "headers": { + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ], + "x-aws-ec2-metadata-token": [ + "imdssesiontoken==" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 2, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "connection": [ + "close" + ], + "server": [ + "EC2ws" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ], + "accept-ranges": [ + "none" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "last-modified": [ + "Tue, 21 Sep 2021 17:30:41 GMT" + ], + "content-type": [ + "text/plain" + ], + "content-length": [ + "1322" + ] + } + } + } + } + } + }, + { + "connection_id": 2, + "action": { + "Data": { + "data": { + "Utf8": "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-21T17:31:21Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARSTSBASE\",\n \"SecretAccessKey\" : \"secretbase\",\n \"Token\" : \"tokenbase\",\n \"Expiration\" : \"2021-09-21T23:33:13Z\"\n}" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 3, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is0.s3.us-east-1.amazonaws.com", + "headers": { + "x-amz-date": [ + "20210921T174125Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "content-type": [ + "application/xml" + ], + "user-agent": [ + "aws-sdk-rust/0.51.0 os/linux lang/rust/1.62.1" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.51.0 api/s3/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ASIARSTSBASE/20210921/us-east-1/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=021dadec3fb3769ff21165f1abf9516887d1f93c504a98e399ccc5df8adef01f" + ], + "content-length": [ + "0" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 3, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 3, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 3, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "content-length": [ + "0" + ], + "location": [ + "/test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is0" + ], + "x-amz-id-2": [ + "hndxcVIq5ILod5JoH+4ULZAi4lo6BXJvJm78Oxro48vNw5s4MFsZqKCHM0GIhYlDf/RWsnmmHpg=" + ], + "x-amz-request-id": [ + "YYP6QSB3XZ50PZ07" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 3, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 3, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 4, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 4, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 4, + "action": { + "Response": { + "response": { + "Ok": { + "status": 500, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "connection": [ + "close" + ], + "content-length": [ + "363" + ], + "content-type": [ + "text/html" + ], + "date": [ + "Tue, 21 Sep 2021 23:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 4, + "action": { + "Data": { + "data": { + "Utf8": "\n\n\n \n 500 - Internal Server Error\n \n \n

500 - Internal Server Error

\n \n\n" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 4, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 5, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 5, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 5, + "action": { + "Response": { + "response": { + "Ok": { + "status": 500, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "connection": [ + "close" + ], + "content-length": [ + "363" + ], + "content-type": [ + "text/html" + ], + "date": [ + "Tue, 21 Sep 2021 23:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 5, + "action": { + "Data": { + "data": { + "Utf8": "\n\n\n \n 500 - Internal Server Error\n \n \n

500 - Internal Server Error

\n \n\n" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 5, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 6, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 6, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 6, + "action": { + "Response": { + "response": { + "Ok": { + "status": 500, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "connection": [ + "close" + ], + "content-length": [ + "363" + ], + "content-type": [ + "text/html" + ], + "date": [ + "Tue, 21 Sep 2021 23:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 6, + "action": { + "Data": { + "data": { + "Utf8": "\n\n\n \n 500 - Internal Server Error\n \n \n

500 - Internal Server Error

\n \n\n" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 6, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 7, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 7, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 7, + "action": { + "Response": { + "response": { + "Ok": { + "status": 500, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "connection": [ + "close" + ], + "content-length": [ + "363" + ], + "content-type": [ + "text/html" + ], + "date": [ + "Tue, 21 Sep 2021 23:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 7, + "action": { + "Data": { + "data": { + "Utf8": "\n\n\n \n 500 - Internal Server Error\n \n \n

500 - Internal Server Error

\n \n\n" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 7, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 8, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is1.s3.us-east-1.amazonaws.com", + "headers": { + "x-amz-date": [ + "20210921T234125Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "content-type": [ + "application/xml" + ], + "user-agent": [ + "aws-sdk-rust/0.51.0 os/linux lang/rust/1.62.1" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.51.0 api/s3/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ASIARSTSBASE/20210921/us-east-1/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=58c85ad354a8226e1bf6fef0180665e79a37f3c0f26a61dde9de40baf27e64c1" + ], + "content-length": [ + "0" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 8, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 8, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 8, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "content-length": [ + "0" + ], + "location": [ + "/test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is1" + ], + "x-amz-id-2": [ + "hndxcVIq5ILod5JoH+4ULZAi4lo6BXJvJm78Oxro48vNw5s4MFsZqKCHM0GIhYlDf/RWsnmmHpg=" + ], + "x-amz-request-id": [ + "1GNPPM15K9554BVN" + ], + "date": [ + "Tue, 21 Sep 2021 23:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 8, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 8, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } +] diff --git a/aws/sdk/integration-tests/s3/tests/test-data/send-successive-requests-with-expired-creds.json b/aws/sdk/integration-tests/s3/tests/test-data/send-successive-requests-with-expired-creds.json new file mode 100644 index 0000000000..55830da6e7 --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/test-data/send-successive-requests-with-expired-creds.json @@ -0,0 +1,587 @@ +[ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/api/token", + "headers": { + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "EC2ws" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "connection": [ + "close" + ], + "content-length": [ + "56" + ], + "content-type": [ + "text/plain" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "imdssesiontoken==" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", + "headers": { + "x-aws-ec2-metadata-token": [ + "imdssesiontoken==" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 1, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "content-type": [ + "text/plain" + ], + "last-modified": [ + "Tue, 21 Sep 2021 17:30:41 GMT" + ], + "connection": [ + "close" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ], + "content-length": [ + "21" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "accept-ranges": [ + "none" + ], + "server": [ + "EC2ws" + ] + } + } + } + } + } + }, + { + "connection_id": 1, + "action": { + "Data": { + "data": { + "Utf8": "imds-assume-role-test" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 1, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Request": { + "request": { + "uri": "http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-assume-role-test", + "headers": { + "user-agent": [ + "aws-sdk-rust/0.52.0 os/linux lang/rust/1.62.1" + ], + "x-aws-ec2-metadata-token": [ + "imdssesiontoken==" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.52.0 api/imds/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 2, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "connection": [ + "close" + ], + "server": [ + "EC2ws" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ], + "accept-ranges": [ + "none" + ], + "x-aws-ec2-metadata-token-ttl-seconds": [ + "21600" + ], + "last-modified": [ + "Tue, 21 Sep 2021 17:30:41 GMT" + ], + "content-type": [ + "text/plain" + ], + "content-length": [ + "1322" + ] + } + } + } + } + } + }, + { + "connection_id": 2, + "action": { + "Data": { + "data": { + "Utf8": "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-21T17:31:21Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARSTSBASE\",\n \"SecretAccessKey\" : \"secretbase\",\n \"Token\" : \"tokenbase\",\n \"Expiration\" : \"2021-09-21T11:29:29Z\"\n}" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 2, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 3, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is1.s3.us-east-1.amazonaws.com", + "headers": { + "x-amz-date": [ + "20210921T174125Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "content-type": [ + "application/xml" + ], + "user-agent": [ + "aws-sdk-rust/0.51.0 os/linux lang/rust/1.62.1" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.51.0 api/s3/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ASIARSTSBASE/20210921/us-east-1/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=00656bafea35c08230e6f15a3571e7357b53d6d2a265bd7c05773274a46575bd" + ], + "content-length": [ + "0" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 3, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 3, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 3, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "content-length": [ + "0" + ], + "location": [ + "/test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is1" + ], + "x-amz-id-2": [ + "hndxcVIq5ILod5JoH+4ULZAi4lo6BXJvJm78Oxro48vNw5s4MFsZqKCHM0GIhYlDf/RWsnmmHpg=" + ], + "x-amz-request-id": [ + "YYP6QSB3XZ50PZ07" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 3, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 3, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 4, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is2.s3.us-east-1.amazonaws.com", + "headers": { + "x-amz-date": [ + "20210921T174125Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "content-type": [ + "application/xml" + ], + "user-agent": [ + "aws-sdk-rust/0.51.0 os/linux lang/rust/1.62.1" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.51.0 api/s3/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ASIARSTSBASE/20210921/us-east-1/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=92e946d999548daeda9c93199d6a6c1d64b0b72c4c30be23bf1b22afcfb78820" + ], + "content-length": [ + "0" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 4, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 4, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 4, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "content-length": [ + "0" + ], + "location": [ + "/test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is2" + ], + "x-amz-id-2": [ + "hndxcVIq5ILod5JoH+4ULZAi4lo6BXJvJm78Oxro48vNw5s4MFsZqKCHM0GIhYlDf/RWsnmmHpg=" + ], + "x-amz-request-id": [ + "EEFG3N4NG60B4AW0" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 4, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 4, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 5, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is3.s3.us-east-1.amazonaws.com", + "headers": { + "x-amz-date": [ + "20210921T174125Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "content-type": [ + "application/xml" + ], + "user-agent": [ + "aws-sdk-rust/0.51.0 os/linux lang/rust/1.62.1" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.51.0 api/s3/0.0.0-smithy-rs-head os/linux lang/rust/1.62.1" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ASIARSTSBASE/20210921/us-east-1/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=158f6eff17078babdebbb01318d2380ac376a9ffe1a1ef0ff68f1e9432de50a1" + ], + "content-length": [ + "0" + ] + }, + "method": "PUT" + } + } + } + }, + { + "connection_id": 5, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Request" + } + } + }, + { + "connection_id": 5, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 5, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "content-length": [ + "0" + ], + "location": [ + "/test-bucket-s2dhlj57-3mg8-54949-bn28-fj37tnw91is3" + ], + "x-amz-id-2": [ + "hndxcVIq5ILod5JoH+4ULZAi4lo6BXJvJm78Oxro48vNw5s4MFsZqKCHM0GIhYlDf/RWsnmmHpg=" + ], + "x-amz-request-id": [ + "1GNPPM15K9554BVN" + ], + "date": [ + "Tue, 21 Sep 2021 17:41:25 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 5, + "action": { + "Data": { + "data": { + "Utf8": "" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 5, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } +]