2 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 3 | * SPDX-License-Identifier: Apache-2.0
|
4 4 | */
|
5 5 |
|
6 6 | //! IMDSv2 Credentials Provider
|
7 7 | //!
|
8 8 | //! # Important
|
9 9 | //! This credential provider will NOT fallback to IMDSv1. Ensure that IMDSv2 is enabled on your instances.
|
10 10 |
|
11 11 | use super::client::error::ImdsError;
|
12 + | use crate::environment::parse_bool;
|
12 13 | use crate::imds::{self, Client};
|
13 14 | use crate::json_credentials::{parse_json_credentials, JsonCredentials, RefreshableCredentials};
|
14 15 | use crate::provider_config::ProviderConfig;
|
16 + | use aws_credential_types::attributes::AccountId;
|
15 17 | use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
|
16 18 | use aws_credential_types::Credentials;
|
19 + | use aws_runtime::env_config::EnvConfigValue;
|
17 20 | use aws_smithy_async::time::SharedTimeSource;
|
18 - | use aws_types::os_shim_internal::Env;
|
21 + | use aws_smithy_types::error::display::DisplayErrorContext;
|
22 + | use aws_types::origin::Origin;
|
19 23 | use std::borrow::Cow;
|
20 24 | use std::error::Error as StdError;
|
21 25 | use std::fmt;
|
22 26 | use std::sync::{Arc, RwLock};
|
23 27 | use std::time::{Duration, SystemTime};
|
24 28 |
|
25 29 | const CREDENTIAL_EXPIRATION_INTERVAL: Duration = Duration::from_secs(10 * 60);
|
26 30 | const WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY: &str =
|
27 31 | "Attempting credential expiration extension due to a credential service availability issue. \
|
28 32 | A refresh of these credentials will be attempted again within the next";
|
29 33 |
|
30 34 | #[derive(Debug)]
|
31 35 | struct ImdsCommunicationError {
|
32 36 | source: Box<dyn StdError + Send + Sync + 'static>,
|
33 37 | }
|
34 38 |
|
35 39 | impl fmt::Display for ImdsCommunicationError {
|
36 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
37 41 | write!(f, "could not communicate with IMDS")
|
38 42 | }
|
39 43 | }
|
40 44 |
|
41 45 | impl StdError for ImdsCommunicationError {
|
42 46 | fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
43 47 | Some(self.source.as_ref())
|
44 48 | }
|
45 49 | }
|
46 50 |
|
51 + | // Enum representing the type of IMDS endpoint that the credentials provider should access
|
52 + | // when retrieving the IMDS profile name or credentials.
|
53 + | #[derive(Clone, Debug, Default, Eq, PartialEq)]
|
54 + | enum ApiVersion {
|
55 + | #[default]
|
56 + | Unknown,
|
57 + | Extended,
|
58 + | Legacy,
|
59 + | }
|
60 + |
|
61 + | // A state maintained by the IMDS credentials provider to manage the retrieval of the IMDS profile name or credentials.
|
62 + | #[derive(Clone, Debug, Default)]
|
63 + | struct ProviderState {
|
64 + | api_version: ApiVersion,
|
65 + | resolved_profile: Option<String>,
|
66 + | }
|
67 + |
|
47 68 | /// IMDSv2 Credentials Provider
|
48 69 | ///
|
49 70 | /// _Note: This credentials provider will NOT fallback to the IMDSv1 flow._
|
50 71 | #[derive(Debug)]
|
51 72 | pub struct ImdsCredentialsProvider {
|
52 73 | client: Client,
|
53 - | env: Env,
|
74 + | provider_config: ProviderConfig,
|
54 75 | profile: Option<String>,
|
55 76 | time_source: SharedTimeSource,
|
56 77 | last_retrieved_credentials: Arc<RwLock<Option<Credentials>>>,
|
78 + | provider_state: Arc<RwLock<ProviderState>>,
|
57 79 | }
|
58 80 |
|
59 81 | /// Builder for [`ImdsCredentialsProvider`]
|
60 82 | #[derive(Default, Debug)]
|
61 83 | pub struct Builder {
|
62 84 | provider_config: Option<ProviderConfig>,
|
63 85 | profile_override: Option<String>,
|
64 86 | imds_override: Option<imds::Client>,
|
65 87 | last_retrieved_credentials: Option<Credentials>,
|
66 88 | }
|
67 89 |
|
68 90 | impl Builder {
|
69 91 | /// Override the configuration used for this provider
|
70 92 | pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
|
71 93 | self.provider_config = Some(provider_config.clone());
|
72 94 | self
|
73 95 | }
|
74 96 |
|
75 97 | /// Override the [instance profile](instance-profile) used for this provider.
|
76 98 | ///
|
77 99 | /// When retrieving IMDS credentials, a call must first be made to
|
78 100 | /// `<IMDS_BASE_URL>/latest/meta-data/iam/security-credentials/`. This returns the instance
|
79 101 | /// profile used. By setting this parameter, retrieving the profile is skipped
|
80 102 | /// and the provided value is used instead.
|
81 103 | ///
|
82 104 | /// [instance-profile]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#ec2-instance-profile
|
83 105 | pub fn profile(mut self, profile: impl Into<String>) -> Self {
|
84 106 | self.profile_override = Some(profile.into());
|
85 107 | self
|
86 108 | }
|
87 109 |
|
88 110 | /// Override the IMDS client used for this provider
|
89 111 | ///
|
90 112 | /// The IMDS client will be loaded and configured via `~/.aws/config` and environment variables,
|
91 113 | /// however, if necessary the entire client may be provided directly.
|
92 114 | ///
|
93 115 | /// For more information about IMDS client configuration loading see [`imds::Client`]
|
94 116 | pub fn imds_client(mut self, client: imds::Client) -> Self {
|
95 117 | self.imds_override = Some(client);
|
96 118 | self
|
97 119 | }
|
98 120 |
|
99 121 | #[allow(dead_code)]
|
100 122 | #[cfg(test)]
|
101 123 | fn last_retrieved_credentials(mut self, credentials: Credentials) -> Self {
|
102 124 | self.last_retrieved_credentials = Some(credentials);
|
103 125 | self
|
104 126 | }
|
105 127 |
|
106 128 | /// Create an [`ImdsCredentialsProvider`] from this builder.
|
107 129 | pub fn build(self) -> ImdsCredentialsProvider {
|
108 130 | let provider_config = self.provider_config.unwrap_or_default();
|
109 - | let env = provider_config.env();
|
110 131 | let client = self
|
111 132 | .imds_override
|
112 133 | .unwrap_or_else(|| imds::Client::builder().configure(&provider_config).build());
|
113 134 | ImdsCredentialsProvider {
|
114 135 | client,
|
115 - | env,
|
116 136 | profile: self.profile_override,
|
117 137 | time_source: provider_config.time_source(),
|
138 + | provider_config,
|
118 139 | last_retrieved_credentials: Arc::new(RwLock::new(self.last_retrieved_credentials)),
|
140 + | provider_state: Arc::new(RwLock::new(ProviderState::default())),
|
119 141 | }
|
120 142 | }
|
121 143 | }
|
122 144 |
|
123 145 | mod codes {
|
124 146 | pub(super) const ASSUME_ROLE_UNAUTHORIZED_ACCESS: &str = "AssumeRoleUnauthorizedAccess";
|
125 147 | }
|
126 148 |
|
127 149 | impl ProvideCredentials for ImdsCredentialsProvider {
|
128 150 | fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
|
129 151 | where
|
130 152 | Self: 'a,
|
131 153 | {
|
132 154 | future::ProvideCredentials::new(self.credentials())
|
133 155 | }
|
134 156 |
|
135 157 | fn fallback_on_interrupt(&self) -> Option<Credentials> {
|
136 158 | self.last_retrieved_credentials.read().unwrap().clone()
|
137 159 | }
|
138 160 | }
|
139 161 |
|
140 162 | impl ImdsCredentialsProvider {
|
141 163 | /// Builder for [`ImdsCredentialsProvider`]
|
142 164 | pub fn builder() -> Builder {
|
143 165 | Builder::default()
|
144 166 | }
|
145 167 |
|
146 - | fn imds_disabled(&self) -> bool {
|
147 - | match self.env.get(super::env::EC2_METADATA_DISABLED) {
|
148 - | Ok(value) => value.eq_ignore_ascii_case("true"),
|
149 - | _ => false,
|
168 + | // Retrieve the value for "disable ec2 metadata". If the value is `true`, the method also returns
|
169 + | // the source that set it to `true`.
|
170 + | //
|
171 + | // This checks the following sources:
|
172 + | // 1. The environment variable `AWS_EC2_METADATA_DISABLED=true/false`
|
173 + | // 2. The profile key `disable_ec2_metadata=true/false`
|
174 + | async fn imds_disabled(&self) -> (bool, Origin) {
|
175 + | EnvConfigValue::new()
|
176 + | .env(super::env::EC2_METADATA_DISABLED)
|
177 + | .profile(super::profile_key::EC2_METADATA_DISABLED)
|
178 + | .validate_and_return_origin(
|
179 + | &self.provider_config.env(),
|
180 + | self.provider_config.profile().await,
|
181 + | parse_bool,
|
182 + | )
|
183 + | .map_err(
|
184 + | |err| tracing::warn!(err = %DisplayErrorContext(&err), "invalid value for `disable ec2 metadata` setting"),
|
185 + | )
|
186 + | .map(|(disabled, origin)| (disabled.unwrap_or_default(), origin))
|
187 + | .unwrap_or_default()
|
188 + | }
|
189 + |
|
190 + | // Return a configured instance profile name. If the profile name is blank, the method returns
|
191 + | // a `CredentialsError`.
|
192 + | //
|
193 + | // This checks the following sources:
|
194 + | // 1. The profile name configured via [`Builder::profile`]
|
195 + | // 2. The environment variable `AWS_EC2_INSTANCE_PROFILE_NAME`
|
196 + | // 3. The profile key `ec2_instance_profile_name`
|
197 + | async fn configured_instance_profile_name(
|
198 + | &self,
|
199 + | ) -> Result<Option<Cow<'_, str>>, CredentialsError> {
|
200 + | let configured = match &self.profile {
|
201 + | Some(profile) => Some(profile.into()),
|
202 + | None => EnvConfigValue::new()
|
203 + | .env(super::env::EC2_INSTANCE_PROFILE_NAME)
|
204 + | .profile(super::profile_key::EC2_INSTANCE_PROFILE_NAME)
|
205 + | .validate(
|
206 + | &self.provider_config.env(),
|
207 + | self.provider_config.profile().await,
|
208 + | |s| Ok::<String, std::convert::Infallible>(s.to_owned()),
|
209 + | )
|
210 + | .expect("validator is infallible")
|
211 + | .map(Cow::Owned),
|
212 + | };
|
213 + |
|
214 + | match configured {
|
215 + | Some(configured) if configured.trim().is_empty() => Err(CredentialsError::not_loaded(
|
216 + | "blank profile name is not supported",
|
217 + | )),
|
218 + | otherwise => Ok(otherwise),
|
150 219 | }
|
151 220 | }
|
152 221 |
|
153 - | /// Retrieve the instance profile from IMDS
|
154 - | async fn get_profile_uncached(&self) -> Result<String, CredentialsError> {
|
155 - | match self
|
156 - | .client
|
157 - | .get("/latest/meta-data/iam/security-credentials/")
|
158 - | .await
|
222 + | fn uri_base(&self) -> &str {
|
223 + | let api_version = &self
|
224 + | .provider_state
|
225 + | .read()
|
226 + | .expect("write critical section does not cause panic")
|
227 + | .api_version;
|
228 + | use ApiVersion::*;
|
229 + | match api_version {
|
230 + | Legacy => "/latest/meta-data/iam/security-credentials/",
|
231 + | _ => "/latest/meta-data/iam/security-credentials-extended/",
|
232 + | }
|
233 + | }
|
234 + |
|
235 + | // Retrieve the instance profile from IMDS
|
236 + | //
|
237 + | // Starting with `ApiVersion::Unknown`, the method first attempts to retrive it using the extended API.
|
238 + | // If the call is successful, it updates `ProviderState` to remember that the extended API is functional and moves on.
|
239 + | // Otherwise, it updates `ProviderState` to the legacy mode and tries again.
|
240 + | // In the end, if the legacy API does not work either, the method gives up and returns a `CredentialsError`.
|
241 + | async fn resolve_profile_name(&self) -> Result<Cow<'_, str>, CredentialsError> {
|
242 + | if let Some(profile) = self.configured_instance_profile_name().await? {
|
243 + | return Ok(profile);
|
244 + | }
|
245 + |
|
246 + | if let Some(profile) = &self
|
247 + | .provider_state
|
248 + | .read()
|
249 + | .expect("write critical section does not cause panic")
|
250 + | .resolved_profile
|
159 251 | {
|
160 - | Ok(profile) => Ok(profile.as_ref().into()),
|
252 + | return Ok(profile.clone().into());
|
253 + | }
|
254 + |
|
255 + | match self.client.get(self.uri_base()).await {
|
256 + | Ok(profile) => {
|
257 + | let state = &mut self
|
258 + | .provider_state
|
259 + | .write()
|
260 + | .expect("write critical section does not cause panic");
|
261 + | state.resolved_profile = Some(profile.clone().into());
|
262 + | if state.api_version == ApiVersion::Unknown {
|
263 + | state.api_version = ApiVersion::Extended;
|
264 + | }
|
265 + | Ok(Cow::Owned(profile.into()))
|
266 + | }
|
161 267 | Err(ImdsError::ErrorResponse(context))
|
162 268 | if context.response().status().as_u16() == 404 =>
|
163 269 | {
|
164 270 | tracing::warn!(
|
165 271 | "received 404 from IMDS when loading profile information. \
|
166 272 | Hint: This instance may not have an IAM role associated."
|
167 273 | );
|
168 - | Err(CredentialsError::not_loaded("received 404 from IMDS"))
|
274 + |
|
275 + | {
|
276 + | let state = &mut self
|
277 + | .provider_state
|
278 + | .write()
|
279 + | .expect("write critical section does not cause panic");
|
280 + | if state.api_version == ApiVersion::Unknown {
|
281 + | tracing::debug!("retrieving an IMDS profile name failed using the extended API, switching to the legacy API and trying again");
|
282 + | state.api_version = ApiVersion::Legacy;
|
283 + | } else {
|
284 + | return Err(CredentialsError::not_loaded("received 404 from IMDS"));
|
285 + | }
|
286 + | }
|
287 + |
|
288 + | Box::pin(self.resolve_profile_name()).await
|
169 289 | }
|
170 290 | Err(ImdsError::FailedToLoadToken(context)) if context.is_dispatch_failure() => {
|
171 291 | Err(CredentialsError::not_loaded(ImdsCommunicationError {
|
172 292 | source: context.into_source().into(),
|
173 293 | }))
|
174 294 | }
|
175 295 | Err(other) => Err(CredentialsError::provider_error(other)),
|
176 296 | }
|
177 297 | }
|
178 298 |
|
179 299 | // Extend the cached expiration time if necessary
|
180 300 | //
|
181 301 | // This allows continued use of the credentials even when IMDS returns expired ones.
|
182 302 | fn maybe_extend_expiration(&self, expiration: SystemTime) -> SystemTime {
|
183 303 | let now = self.time_source.now();
|
184 304 | // If credentials from IMDS are not stale, use them as they are.
|
185 305 | if now < expiration {
|
186 306 | return expiration;
|
187 307 | }
|
188 308 |
|
189 309 | let mut rng = fastrand::Rng::with_seed(
|
190 310 | now.duration_since(SystemTime::UNIX_EPOCH)
|
191 311 | .expect("now should be after UNIX EPOCH")
|
192 312 | .as_secs(),
|
193 313 | );
|
194 314 | // Calculate credentials' refresh offset with jitter, which should be less than 15 minutes
|
195 315 | // the smallest amount of time credentials are valid for.
|
196 316 | // Setting it to something longer than that may have the risk of the credentials expiring
|
197 317 | // before the next refresh.
|
198 318 | let refresh_offset = CREDENTIAL_EXPIRATION_INTERVAL + Duration::from_secs(rng.u64(0..=300));
|
199 319 | let new_expiry = now + refresh_offset;
|
200 320 |
|
201 321 | tracing::warn!(
|
202 322 | "{WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY} {:.2} minutes.",
|
203 323 | refresh_offset.as_secs_f64() / 60.0,
|
204 324 | );
|
205 325 |
|
206 326 | new_expiry
|
207 327 | }
|
208 328 |
|
209 329 | async fn retrieve_credentials(&self) -> provider::Result {
|
210 - | if self.imds_disabled() {
|
211 - | let err = format!(
|
212 - | "IMDS disabled by {} env var set to `true`",
|
213 - | super::env::EC2_METADATA_DISABLED
|
214 - | );
|
330 + | if let (true, origin) = self.imds_disabled().await {
|
331 + | let err = format!("IMDS disabled by {origin} set to `true`",);
|
215 332 | tracing::debug!(err);
|
216 333 | return Err(CredentialsError::not_loaded(err));
|
217 334 | }
|
335 + |
|
218 336 | tracing::debug!("loading credentials from IMDS");
|
219 - | let profile: Cow<'_, str> = match &self.profile {
|
220 - | Some(profile) => profile.into(),
|
221 - | None => self.get_profile_uncached().await?.into(),
|
222 - | };
|
337 + |
|
338 + | let profile = self.resolve_profile_name().await?;
|
223 339 | tracing::debug!(profile = %profile, "loaded profile");
|
224 - | let credentials = self
|
340 + |
|
341 + | let credentials = match self
|
225 342 | .client
|
226 - | .get(format!(
|
227 - | "/latest/meta-data/iam/security-credentials/{}",
|
228 - | profile
|
229 - | ))
|
343 + | .get(format!("{uri_base}{profile}", uri_base = self.uri_base()))
|
230 344 | .await
|
345 + | {
|
346 + | Ok(credentials) => {
|
347 + | let state = &mut self.provider_state.write().expect("write critical section does not cause panic");
|
348 + | state.resolved_profile = Some(profile.to_string());
|
349 + | if state.api_version == ApiVersion::Unknown {
|
350 + | state.api_version = ApiVersion::Extended;
|
351 + | }
|
352 + | Ok(credentials)
|
353 + | }
|
354 + | Err(ImdsError::ErrorResponse(raw)) if raw.response().status().as_u16() == 404 => {
|
355 + | {
|
356 + | let state = &mut self.provider_state.write().expect("write critical section does not cause panic");
|
357 + | if state.api_version == ApiVersion::Unknown {
|
358 + | tracing::debug!("retrieving credentials failed using the extended API, switching to the legacy API and trying again");
|
359 + | state.api_version = ApiVersion::Legacy;
|
360 + | } else if self.profile.is_none() {
|
361 + | tracing::debug!("retrieving credentials failed using {:?}, clearing cached profile and trying again", state.api_version);
|
362 + | state.resolved_profile = None;
|
363 + | } else {
|
364 + | return Err(CredentialsError::provider_error(ImdsError::ErrorResponse(
|
365 + | raw,
|
366 + | )));
|
367 + | }
|
368 + | }
|
369 + | return Box::pin(self.retrieve_credentials()).await;
|
370 + | }
|
371 + | otherwise => otherwise,
|
372 + | }
|
231 373 | .map_err(CredentialsError::provider_error)?;
|
374 + |
|
232 375 | match parse_json_credentials(credentials.as_ref()) {
|
233 376 | Ok(JsonCredentials::RefreshableCredentials(RefreshableCredentials {
|
234 377 | access_key_id,
|
235 378 | secret_access_key,
|
236 379 | session_token,
|
237 380 | account_id,
|
238 381 | expiration,
|
239 382 | ..
|
240 383 | })) => {
|
241 - | // TODO(IMDSv2.X): Use `account_id` once the design is finalized
|
242 - | let _ = account_id;
|
243 384 | let expiration = self.maybe_extend_expiration(expiration);
|
244 - | let creds = Credentials::new(
|
245 - | access_key_id,
|
246 - | secret_access_key,
|
247 - | Some(session_token.to_string()),
|
248 - | expiration.into(),
|
249 - | "IMDSv2",
|
250 - | );
|
385 + | let mut builder = Credentials::builder()
|
386 + | .access_key_id(access_key_id)
|
387 + | .secret_access_key(secret_access_key)
|
388 + | .session_token(session_token)
|
389 + | .expiry(expiration)
|
390 + | .provider_name("IMDSv2");
|
391 + | builder.set_account_id(account_id.map(AccountId::from));
|
392 + | let creds = builder.build();
|
251 393 | *self.last_retrieved_credentials.write().unwrap() = Some(creds.clone());
|
252 394 | Ok(creds)
|
253 395 | }
|
254 396 | Ok(JsonCredentials::Error { code, message })
|
255 397 | if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS =>
|
256 398 | {
|
257 399 | Err(CredentialsError::invalid_configuration(format!(
|
258 400 | "Incorrect IMDS/IAM configuration: [{}] {}. \
|
259 401 | Hint: Does this role have a trust relationship with EC2?",
|
260 402 | code, message
|
261 403 | )))
|
262 404 | }
|
263 405 | Ok(JsonCredentials::Error { code, message }) => {
|
264 406 | Err(CredentialsError::provider_error(format!(
|
265 407 | "Error retrieving credentials from IMDS: {} {}",
|
266 408 | code, message
|
267 409 | )))
|
268 410 | }
|
269 411 | // got bad data from IMDS, should not occur during normal operation:
|
270 412 | Err(invalid) => Err(CredentialsError::unhandled(invalid)),
|
271 413 | }
|
272 414 | }
|
273 415 |
|
274 416 | async fn credentials(&self) -> provider::Result {
|
275 417 | match self.retrieve_credentials().await {
|
276 418 | creds @ Ok(_) => creds,
|
277 419 | // Any failure while retrieving credentials MUST NOT impede use of existing credentials.
|
278 420 | err => match &*self.last_retrieved_credentials.read().unwrap() {
|
279 421 | Some(creds) => Ok(creds.clone()),
|
280 422 | _ => err,
|
281 423 | },
|
282 424 | }
|
283 425 | }
|
284 426 | }
|
285 427 |
|
286 428 | #[cfg(test)]
|
287 429 | mod test {
|
288 430 | use super::*;
|
289 431 | use crate::imds::client::test::{
|
290 - | imds_request, imds_response, make_imds_client, token_request, token_response,
|
432 + | imds_request, imds_response, imds_response_404, make_imds_client, token_request,
|
433 + | token_response,
|
291 434 | };
|
292 435 | use crate::provider_config::ProviderConfig;
|
293 436 | use aws_credential_types::provider::ProvideCredentials;
|
294 437 | use aws_smithy_async::test_util::instant_time_and_sleep;
|
295 438 | use aws_smithy_http_client::test_util::{ReplayEvent, StaticReplayClient};
|
296 439 | use aws_smithy_types::body::SdkBody;
|
440 + | use std::convert::identity as IdentityFn;
|
441 + | use std::future::Future;
|
442 + | use std::pin::Pin;
|
297 443 | use std::time::{Duration, UNIX_EPOCH};
|
298 444 | use tracing_test::traced_test;
|
299 445 |
|
300 446 | const TOKEN_A: &str = "token_a";
|
301 447 |
|
302 448 | #[tokio::test]
|
303 - | async fn profile_is_not_cached() {
|
304 - | let http_client = StaticReplayClient::new(vec![
|
305 - | ReplayEvent::new(
|
306 - | token_request("http://169.254.169.254", 21600),
|
307 - | token_response(21600, TOKEN_A),
|
449 + | #[traced_test]
|
450 + | #[cfg(feature = "default-https-client")]
|
451 + | async fn warn_on_invalid_value_for_disable_ec2_metadata() {
|
452 + | let provider_config =
|
453 + | ProviderConfig::empty().with_env(aws_types::os_shim_internal::Env::from_slice(&[(
|
454 + | imds::env::EC2_METADATA_DISABLED,
|
455 + | "not-a-boolean",
|
456 + | )]));
|
457 + | let client = crate::imds::Client::builder()
|
458 + | .configure(&provider_config)
|
459 + | .build();
|
460 + | let provider = ImdsCredentialsProvider::builder()
|
461 + | .configure(&provider_config)
|
462 + | .imds_client(client)
|
463 + | .build();
|
464 + | assert!(!provider.imds_disabled().await.0);
|
465 + | assert!(logs_contain(
|
466 + | "invalid value for `disable ec2 metadata` setting"
|
467 + | ));
|
468 + | assert!(logs_contain(imds::env::EC2_METADATA_DISABLED));
|
469 + | }
|
470 + |
|
471 + | #[tokio::test]
|
472 + | #[traced_test]
|
473 + | #[cfg(feature = "default-https-client")]
|
474 + | async fn environment_priority_on_disable_ec2_metadata() {
|
475 + | let provider_config = ProviderConfig::empty()
|
476 + | .with_env(aws_types::os_shim_internal::Env::from_slice(&[(
|
477 + | imds::env::EC2_METADATA_DISABLED,
|
478 + | "TRUE",
|
479 + | )]))
|
480 + | .with_profile_config(
|
481 + | Some(
|
482 + | #[allow(deprecated)]
|
483 + | crate::profile::profile_file::ProfileFiles::builder()
|
484 + | .with_file(
|
485 + | #[allow(deprecated)]
|
486 + | crate::profile::profile_file::ProfileFileKind::Config,
|
487 + | "conf",
|
488 + | )
|
489 + | .build(),
|
308 490 | ),
|
309 - | ReplayEvent::new(
|
310 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
311 - | imds_response(r#"profile-name"#),
|
491 + | None,
|
492 + | )
|
493 + | .with_fs(aws_types::os_shim_internal::Fs::from_slice(&[(
|
494 + | "conf",
|
495 + | "[default]\ndisable_ec2_metadata = false",
|
496 + | )]));
|
497 + | let client = crate::imds::Client::builder()
|
498 + | .configure(&provider_config)
|
499 + | .build();
|
500 + | let provider = ImdsCredentialsProvider::builder()
|
501 + | .configure(&provider_config)
|
502 + | .imds_client(client)
|
503 + | .build();
|
504 + | assert_eq!(
|
505 + | (true, Origin::shared_environment_variable()),
|
506 + | provider.imds_disabled().await
|
507 + | );
|
508 + | }
|
509 + |
|
510 + | #[tokio::test]
|
511 + | #[traced_test]
|
512 + | #[cfg(feature = "default-https-client")]
|
513 + | async fn disable_ec2_metadata_via_profile_file() {
|
514 + | let provider_config = ProviderConfig::empty()
|
515 + | .with_profile_config(
|
516 + | Some(
|
517 + | #[allow(deprecated)]
|
518 + | crate::profile::profile_file::ProfileFiles::builder()
|
519 + | .with_file(
|
520 + | #[allow(deprecated)]
|
521 + | crate::profile::profile_file::ProfileFileKind::Config,
|
522 + | "conf",
|
523 + | )
|
524 + | .build(),
|
312 525 | ),
|
313 - | ReplayEvent::new(
|
314 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
|
315 - | 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}"),
|
526 + | None,
|
527 + | )
|
528 + | .with_fs(aws_types::os_shim_internal::Fs::from_slice(&[(
|
529 + | "conf",
|
530 + | "[default]\ndisable_ec2_metadata = true",
|
531 + | )]));
|
532 + | let client = crate::imds::Client::builder()
|
533 + | .configure(&provider_config)
|
534 + | .build();
|
535 + | let provider = ImdsCredentialsProvider::builder()
|
536 + | .configure(&provider_config)
|
537 + | .imds_client(client)
|
538 + | .build();
|
539 + | assert_eq!(
|
540 + | (true, Origin::shared_profile_file()),
|
541 + | provider.imds_disabled().await
|
542 + | );
|
543 + | }
|
544 + |
|
545 + | #[tokio::test]
|
546 + | #[traced_test]
|
547 + | #[cfg(feature = "default-https-client")]
|
548 + | async fn creds_provider_configuration_priority_on_ec2_instance_profile_name() {
|
549 + | let provider_config = ProviderConfig::empty()
|
550 + | .with_env(aws_types::os_shim_internal::Env::from_slice(&[(
|
551 + | imds::env::EC2_INSTANCE_PROFILE_NAME,
|
552 + | "profile-via-env",
|
553 + | )]))
|
554 + | .with_profile_config(
|
555 + | Some(
|
556 + | #[allow(deprecated)]
|
557 + | crate::profile::profile_file::ProfileFiles::builder()
|
558 + | .with_file(
|
559 + | #[allow(deprecated)]
|
560 + | crate::profile::profile_file::ProfileFileKind::Config,
|
561 + | "conf",
|
562 + | )
|
563 + | .build(),
|
316 564 | ),
|
317 - | ReplayEvent::new(
|
318 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
319 - | imds_response(r#"different-profile"#),
|
565 + | None,
|
566 + | )
|
567 + | .with_fs(aws_types::os_shim_internal::Fs::from_slice(&[(
|
568 + | "conf",
|
569 + | "[default]\nec2_instance_profile_name = profile-via-profile-file",
|
570 + | )]));
|
571 + |
|
572 + | let client = crate::imds::Client::builder()
|
573 + | .configure(&provider_config)
|
574 + | .build();
|
575 + | let provider = ImdsCredentialsProvider::builder()
|
576 + | .profile("profile-via-creds-provider")
|
577 + | .configure(&provider_config)
|
578 + | .imds_client(client.clone())
|
579 + | .build();
|
580 + | assert_eq!(
|
581 + | Some(Cow::Borrowed("profile-via-creds-provider")),
|
582 + | provider.configured_instance_profile_name().await.unwrap()
|
583 + | );
|
584 + |
|
585 + | // negative test with a blank profile name
|
586 + | let provider = ImdsCredentialsProvider::builder()
|
587 + | .profile("")
|
588 + | .configure(&provider_config)
|
589 + | .imds_client(client)
|
590 + | .build();
|
591 + | let err = provider
|
592 + | .configured_instance_profile_name()
|
593 + | .await
|
594 + | .err()
|
595 + | .unwrap();
|
596 + | assert!(format!("{}", DisplayErrorContext(&err))
|
597 + | .contains("blank profile name is not supported"));
|
598 + | }
|
599 + |
|
600 + | #[tokio::test]
|
601 + | #[traced_test]
|
602 + | #[cfg(feature = "default-https-client")]
|
603 + | async fn environment_priority_on_ec2_instance_profile_name() {
|
604 + | let provider_config = ProviderConfig::empty()
|
605 + | .with_env(aws_types::os_shim_internal::Env::from_slice(&[(
|
606 + | imds::env::EC2_INSTANCE_PROFILE_NAME,
|
607 + | "profile-via-env",
|
608 + | )]))
|
609 + | .with_profile_config(
|
610 + | Some(
|
611 + | #[allow(deprecated)]
|
612 + | crate::profile::profile_file::ProfileFiles::builder()
|
613 + | .with_file(
|
614 + | #[allow(deprecated)]
|
615 + | crate::profile::profile_file::ProfileFileKind::Config,
|
616 + | "conf",
|
617 + | )
|
618 + | .build(),
|
320 619 | ),
|
321 - | ReplayEvent::new(
|
322 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
|
323 - | 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}"),
|
620 + | None,
|
621 + | )
|
622 + | .with_fs(aws_types::os_shim_internal::Fs::from_slice(&[(
|
623 + | "conf",
|
624 + | "[default]\nec2_instance_profile_name = profile-via-profile-file",
|
625 + | )]));
|
626 + | let client = crate::imds::Client::builder()
|
627 + | .configure(&provider_config)
|
628 + | .build();
|
629 + | let provider = ImdsCredentialsProvider::builder()
|
630 + | .configure(&provider_config)
|
631 + | .imds_client(client)
|
632 + | .build();
|
633 + | assert_eq!(
|
634 + | Some(Cow::Borrowed("profile-via-env")),
|
635 + | provider.configured_instance_profile_name().await.unwrap()
|
636 + | );
|
637 + | }
|
638 + |
|
639 + | #[tokio::test]
|
640 + | #[traced_test]
|
641 + | #[cfg(feature = "default-https-client")]
|
642 + | async fn ec2_instance_profile_name_via_profile_file() {
|
643 + | let provider_config = ProviderConfig::empty()
|
644 + | .with_profile_config(
|
645 + | Some(
|
646 + | #[allow(deprecated)]
|
647 + | crate::profile::profile_file::ProfileFiles::builder()
|
648 + | .with_file(
|
649 + | #[allow(deprecated)]
|
650 + | crate::profile::profile_file::ProfileFileKind::Config,
|
651 + | "conf",
|
652 + | )
|
653 + | .build(),
|
324 654 | ),
|
325 - | ]);
|
326 - | let client = ImdsCredentialsProvider::builder()
|
327 - | .imds_client(make_imds_client(&http_client))
|
328 - | .configure(&ProviderConfig::no_configuration())
|
655 + | None,
|
656 + | )
|
657 + | .with_fs(aws_types::os_shim_internal::Fs::from_slice(&[(
|
658 + | "conf",
|
659 + | "[default]\nec2_instance_profile_name = profile-via-profile-file",
|
660 + | )]));
|
661 + | let client = crate::imds::Client::builder()
|
662 + | .configure(&provider_config)
|
329 663 | .build();
|
330 - | let creds1 = client.provide_credentials().await.expect("valid creds");
|
331 - | let creds2 = client.provide_credentials().await.expect("valid creds");
|
332 - | assert_eq!(creds1.access_key_id(), "ASIARTEST");
|
333 - | assert_eq!(creds2.access_key_id(), "ASIARTEST2");
|
334 - | http_client.assert_requests_match(&[]);
|
664 + | let provider = ImdsCredentialsProvider::builder()
|
665 + | .configure(&provider_config)
|
666 + | .imds_client(client)
|
667 + | .build();
|
668 + | assert_eq!(
|
669 + | Some(Cow::Borrowed("profile-via-profile-file")),
|
670 + | provider.configured_instance_profile_name().await.unwrap()
|
671 + | );
|
335 672 | }
|
336 673 |
|
337 674 | #[tokio::test]
|
338 675 | #[traced_test]
|
339 676 | async fn credentials_not_stale_should_be_used_as_they_are() {
|
340 677 | let http_client = StaticReplayClient::new(vec![
|
341 678 | ReplayEvent::new(
|
342 679 | token_request("http://169.254.169.254", 21600),
|
343 680 | token_response(21600, TOKEN_A),
|
344 681 | ),
|
345 682 | ReplayEvent::new(
|
346 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
683 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
347 684 | imds_response(r#"profile-name"#),
|
348 685 | ),
|
349 686 | ReplayEvent::new(
|
350 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
|
687 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/profile-name", TOKEN_A),
|
351 688 | 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}"),
|
352 689 | ),
|
353 690 | ]);
|
354 691 |
|
355 692 | // set to 2021-09-21T04:16:50Z that makes returned credentials' expiry (2021-09-21T04:16:53Z)
|
356 693 | // not stale
|
357 694 | let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632197810);
|
358 695 | let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
|
359 696 |
|
360 697 | let provider_config = ProviderConfig::no_configuration()
|
361 698 | .with_http_client(http_client.clone())
|
362 699 | .with_sleep_impl(sleep)
|
363 700 | .with_time_source(time_source);
|
364 701 | let client = crate::imds::Client::builder()
|
365 702 | .configure(&provider_config)
|
366 703 | .build();
|
367 704 | let provider = ImdsCredentialsProvider::builder()
|
368 705 | .configure(&provider_config)
|
369 706 | .imds_client(client)
|
370 707 | .build();
|
371 708 | let creds = provider.provide_credentials().await.expect("valid creds");
|
372 709 | // The expiry should be equal to what is originally set (==2021-09-21T04:16:53Z).
|
373 710 | assert_eq!(
|
374 711 | creds.expiry(),
|
375 712 | UNIX_EPOCH.checked_add(Duration::from_secs(1632197813))
|
376 713 | );
|
714 + | assert!(creds.account_id().is_none());
|
377 715 | http_client.assert_requests_match(&[]);
|
378 716 |
|
379 717 | // There should not be logs indicating credentials are extended for stability.
|
380 718 | assert!(!logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
|
381 719 | }
|
720 + |
|
382 721 | #[tokio::test]
|
383 722 | #[traced_test]
|
384 723 | async fn expired_credentials_should_be_extended() {
|
385 724 | let http_client = StaticReplayClient::new(vec![
|
386 725 | ReplayEvent::new(
|
387 726 | token_request("http://169.254.169.254", 21600),
|
388 727 | token_response(21600, TOKEN_A),
|
389 728 | ),
|
390 729 | ReplayEvent::new(
|
391 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
730 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
392 731 | imds_response(r#"profile-name"#),
|
393 732 | ),
|
394 733 | ReplayEvent::new(
|
395 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
|
734 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/profile-name", TOKEN_A),
|
396 735 | 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}"),
|
397 736 | ),
|
398 737 | ]);
|
399 738 |
|
400 739 | // set to 2021-09-21T17:41:25Z that renders fetched credentials already expired (2021-09-21T04:16:53Z)
|
401 740 | let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632246085);
|
402 741 | let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
|
403 742 |
|
404 743 | let provider_config = ProviderConfig::no_configuration()
|
405 744 | .with_http_client(http_client.clone())
|
406 745 | .with_sleep_impl(sleep)
|
407 746 | .with_time_source(time_source);
|
408 747 | let client = crate::imds::Client::builder()
|
409 748 | .configure(&provider_config)
|
410 749 | .build();
|
411 750 | let provider = ImdsCredentialsProvider::builder()
|
412 751 | .configure(&provider_config)
|
413 752 | .imds_client(client)
|
414 753 | .build();
|
415 754 | let creds = provider.provide_credentials().await.expect("valid creds");
|
416 755 | assert!(creds.expiry().unwrap() > time_of_request_to_fetch_credentials);
|
417 756 | http_client.assert_requests_match(&[]);
|
418 757 |
|
419 758 | // We should inform customers that expired credentials are being used for stability.
|
420 759 | assert!(logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
|
421 760 | }
|
422 761 |
|
423 762 | #[tokio::test]
|
424 763 | #[cfg(feature = "default-https-client")]
|
425 764 | async fn read_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
|
426 765 | let client = crate::imds::Client::builder()
|
427 766 | // 240.* can never be resolved
|
428 767 | .endpoint("http://240.0.0.0")
|
429 768 | .unwrap()
|
430 769 | .build();
|
431 770 | let expected = aws_credential_types::Credentials::for_tests();
|
432 771 | let provider = ImdsCredentialsProvider::builder()
|
433 772 | .imds_client(client)
|
434 773 | // seed fallback credentials for testing
|
435 774 | .last_retrieved_credentials(expected.clone())
|
436 775 | .build();
|
437 - | let actual = provider.provide_credentials().await;
|
438 - | assert_eq!(actual.unwrap(), expected);
|
776 + | let actual = provider.provide_credentials().await.unwrap();
|
777 + | assert_eq!(expected, actual);
|
439 778 | }
|
440 779 |
|
441 780 | #[tokio::test]
|
442 781 | #[cfg(feature = "default-https-client")]
|
443 782 | async fn read_timeout_during_credentials_refresh_should_error_without_last_retrieved_credentials(
|
444 783 | ) {
|
445 784 | let client = crate::imds::Client::builder()
|
446 785 | // 240.* can never be resolved
|
447 786 | .endpoint("http://240.0.0.0")
|
448 787 | .unwrap()
|
449 788 | .build();
|
450 789 | let provider = ImdsCredentialsProvider::builder()
|
451 790 | .imds_client(client)
|
452 791 | // no fallback credentials provided
|
453 792 | .build();
|
454 - | let actual = provider.provide_credentials().await;
|
793 + | let actual = provider.provide_credentials().await.err().unwrap();
|
455 794 | assert!(
|
456 - | matches!(actual, Err(CredentialsError::CredentialsNotLoaded(_))),
|
795 + | matches!(actual, CredentialsError::CredentialsNotLoaded(_)),
|
457 796 | "\nexpected: Err(CredentialsError::CredentialsNotLoaded(_))\nactual: {actual:?}"
|
458 797 | );
|
459 798 | }
|
460 799 |
|
461 - | // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
|
462 - | #[cfg_attr(windows, ignore)]
|
463 800 | #[tokio::test]
|
464 801 | #[cfg(feature = "default-https-client")]
|
465 802 | async fn external_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
|
466 803 | use aws_smithy_async::rt::sleep::AsyncSleep;
|
467 804 | let client = crate::imds::Client::builder()
|
468 805 | // 240.* can never be resolved
|
469 806 | .endpoint("http://240.0.0.0")
|
470 807 | .unwrap()
|
471 808 | .build();
|
472 809 | let expected = aws_credential_types::Credentials::for_tests();
|
473 810 | let provider = ImdsCredentialsProvider::builder()
|
474 811 | .imds_client(client)
|
475 812 | .configure(&ProviderConfig::no_configuration())
|
476 813 | // seed fallback credentials for testing
|
477 814 | .last_retrieved_credentials(expected.clone())
|
478 815 | .build();
|
479 816 | let sleeper = aws_smithy_async::rt::sleep::TokioSleep::new();
|
480 817 | let timeout = aws_smithy_async::future::timeout::Timeout::new(
|
481 818 | provider.provide_credentials(),
|
482 819 | // make sure `sleeper.sleep` will be timed out first by setting a shorter duration than connect timeout
|
483 820 | sleeper.sleep(std::time::Duration::from_millis(100)),
|
484 821 | );
|
485 822 | match timeout.await {
|
486 823 | Ok(_) => panic!("provide_credentials completed before timeout future"),
|
487 824 | Err(_err) => match provider.fallback_on_interrupt() {
|
488 - | Some(actual) => assert_eq!(actual, expected),
|
825 + | Some(actual) => assert_eq!(expected, actual),
|
489 826 | None => panic!(
|
490 827 | "provide_credentials timed out and no credentials returned from fallback_on_interrupt"
|
491 828 | ),
|
492 829 | },
|
493 830 | };
|
494 831 | }
|
495 832 |
|
496 833 | #[tokio::test]
|
497 834 | async fn fallback_credentials_should_be_used_when_imds_returns_500_during_credentials_refresh()
|
498 835 | {
|
499 836 | let http_client = StaticReplayClient::new(vec![
|
500 837 | // The next three request/response pairs will correspond to the first call to `provide_credentials`.
|
501 838 | // During the call, it populates last_retrieved_credentials.
|
502 839 | ReplayEvent::new(
|
503 840 | token_request("http://169.254.169.254", 21600),
|
504 841 | token_response(21600, TOKEN_A),
|
505 842 | ),
|
506 843 | ReplayEvent::new(
|
507 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
844 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
508 845 | imds_response(r#"profile-name"#),
|
509 846 | ),
|
510 847 | ReplayEvent::new(
|
511 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
|
848 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/profile-name", TOKEN_A),
|
512 849 | 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}"),
|
513 850 | ),
|
514 851 | // The following request/response pair corresponds to the second call to `provide_credentials`.
|
515 852 | // During the call, IMDS returns response code 500.
|
516 853 | ReplayEvent::new(
|
517 - | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
854 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/profile-name", TOKEN_A),
|
518 855 | http::Response::builder().status(500).body(SdkBody::empty()).unwrap(),
|
519 856 | ),
|
520 857 | ]);
|
521 858 | let provider = ImdsCredentialsProvider::builder()
|
522 859 | .imds_client(make_imds_client(&http_client))
|
523 860 | .configure(&ProviderConfig::no_configuration())
|
524 861 | .build();
|
525 862 | let creds1 = provider.provide_credentials().await.expect("valid creds");
|
526 - | assert_eq!(creds1.access_key_id(), "ASIARTEST");
|
527 - | // `creds1` should be returned as fallback credentials and assigned to `creds2`
|
528 - | let creds2 = provider.provide_credentials().await.expect("valid creds");
|
529 - | assert_eq!(creds1, creds2);
|
863 + | assert_eq!("ASIARTEST", creds1.access_key_id());
|
864 + | // `creds1` should be returned as fallback credentials
|
865 + | assert_eq!(
|
866 + | creds1,
|
867 + | provider.provide_credentials().await.expect("valid creds")
|
868 + | );
|
530 869 | http_client.assert_requests_match(&[]);
|
531 870 | }
|
871 + |
|
872 + | async fn run_test<F>(
|
873 + | events: Vec<ReplayEvent>,
|
874 + | update_builder: fn(Builder) -> Builder,
|
875 + | runner: F,
|
876 + | ) where
|
877 + | F: Fn(ImdsCredentialsProvider) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
|
878 + | {
|
879 + | let http_client = StaticReplayClient::new(events);
|
880 + | let builder = ImdsCredentialsProvider::builder()
|
881 + | .imds_client(make_imds_client(&http_client))
|
882 + | .configure(&ProviderConfig::no_configuration());
|
883 + | let provider = update_builder(builder).build();
|
884 + | runner(provider).await;
|
885 + | http_client.assert_requests_match(&[]);
|
886 + | }
|
887 + |
|
888 + | async fn assert(provider: ImdsCredentialsProvider, expected: &[(Option<&str>, Option<&str>)]) {
|
889 + | for (expected_access_key_id, expected_account_id) in expected {
|
890 + | let creds = provider.provide_credentials().await.expect("valid creds");
|
891 + | assert_eq!(expected_access_key_id, &Some(creds.access_key_id()),);
|
892 + | assert_eq!(
|
893 + | expected_account_id,
|
894 + | &creds.account_id().map(|id| id.as_str())
|
895 + | );
|
896 + | }
|
897 + | }
|
898 + |
|
899 + | #[tokio::test]
|
900 + | async fn returns_valid_credentials_with_account_id() {
|
901 + | let extended_api_events = vec![
|
902 + | ReplayEvent::new(
|
903 + | token_request("http://169.254.169.254", 21600),
|
904 + | token_response(21600, TOKEN_A),
|
905 + | ),
|
906 + | // A profile is not cached, so we should expect a network call to obtain one.
|
907 + | ReplayEvent::new(
|
908 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
909 + | imds_response(r#"my-profile-0001"#),
|
910 + | ),
|
911 + | ReplayEvent::new(
|
912 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0001", TOKEN_A),
|
913 + | 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 \"AccountId\" : \"123456789101\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
|
914 + | ),
|
915 + | // For the second call to `provide_credentials`, we shouldn't expect a network call to obtain a profile since it's been resolved and cached.
|
916 + | ReplayEvent::new(
|
917 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0001", TOKEN_A),
|
918 + | 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 \"AccountId\" : \"123456789101\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
|
919 + | ),
|
920 + | ];
|
921 + | run_test(extended_api_events, IdentityFn, |provider| {
|
922 + | Box::pin(assert(
|
923 + | provider,
|
924 + | &[
|
925 + | (Some("ASIARTEST"), Some("123456789101")),
|
926 + | (Some("ASIARTEST"), Some("123456789101")),
|
927 + | ],
|
928 + | ))
|
929 + | })
|
930 + | .await;
|
931 + |
|
932 + | let legacy_api_events = vec![
|
933 + | ReplayEvent::new(
|
934 + | token_request("http://169.254.169.254", 21600),
|
935 + | token_response(21600, TOKEN_A),
|
936 + | ),
|
937 + | // Obtaining a profile from IMDS using the extended API results in 404.
|
938 + | ReplayEvent::new(
|
939 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
940 + | imds_response_404(),
|
941 + | ),
|
942 + | // Should be retried using the legacy API.
|
943 + | ReplayEvent::new(
|
944 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
945 + | imds_response(r#"my-profile-0009"#),
|
946 + | ),
|
947 + | ReplayEvent::new(
|
948 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0009", TOKEN_A),
|
949 + | 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}"),
|
950 + | ),
|
951 + | ReplayEvent::new(
|
952 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0009", TOKEN_A),
|
953 + | 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}"),
|
954 + | ),
|
955 + | ];
|
956 + | run_test(legacy_api_events, IdentityFn, |provider| {
|
957 + | Box::pin(assert(
|
958 + | provider,
|
959 + | &[(Some("ASIARTEST"), None), (Some("ASIARTEST"), None)],
|
960 + | ))
|
961 + | })
|
962 + | .await;
|
963 + | }
|
964 + |
|
965 + | #[tokio::test]
|
966 + | async fn should_return_credentials_when_profile_is_configured_by_user() {
|
967 + | let extended_api_events = vec![
|
968 + | ReplayEvent::new(
|
969 + | token_request("http://169.254.169.254", 21600),
|
970 + | token_response(21600, TOKEN_A),
|
971 + | ),
|
972 + | ReplayEvent::new(
|
973 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0002", TOKEN_A),
|
974 + | 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 \"AccountId\" : \"234567891011\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
|
975 + | ),
|
976 + | ReplayEvent::new(
|
977 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0002", TOKEN_A),
|
978 + | 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 \"AccountId\" : \"234567891011\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
|
979 + | ),
|
980 + | ];
|
981 + | run_test(
|
982 + | extended_api_events,
|
983 + | |b| b.profile("my-profile-0002"),
|
984 + | |provider| {
|
985 + | Box::pin(assert(
|
986 + | provider,
|
987 + | &[
|
988 + | (Some("ASIARTEST"), Some("234567891011")),
|
989 + | (Some("ASIARTEST"), Some("234567891011")),
|
990 + | ],
|
991 + | ))
|
992 + | },
|
993 + | )
|
994 + | .await;
|
995 + |
|
996 + | let legacy_api_events = vec![
|
997 + | ReplayEvent::new(
|
998 + | token_request("http://169.254.169.254", 21600),
|
999 + | token_response(21600, TOKEN_A),
|
1000 + | ),
|
1001 + | // Obtaining a credentials using the extended API results in 404.
|
1002 + | ReplayEvent::new(
|
1003 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0010", TOKEN_A),
|
1004 + | imds_response_404(),
|
1005 + | ),
|
1006 + | // Obtain credentials using the legacy API with the configured profile.
|
1007 + | ReplayEvent::new(
|
1008 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0010", TOKEN_A),
|
1009 + | 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}"),
|
1010 + | ),
|
1011 + | ReplayEvent::new(
|
1012 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0010", TOKEN_A),
|
1013 + | 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}"),
|
1014 + | ),
|
1015 + | ];
|
1016 + | run_test(
|
1017 + | legacy_api_events,
|
1018 + | |b| b.profile("my-profile-0010"),
|
1019 + | |provider| {
|
1020 + | Box::pin(assert(
|
1021 + | provider,
|
1022 + | &[(Some("ASIARTEST"), None), (Some("ASIARTEST"), None)],
|
1023 + | ))
|
1024 + | },
|
1025 + | )
|
1026 + | .await;
|
1027 + | }
|
1028 + |
|
1029 + | #[tokio::test]
|
1030 + | async fn should_return_valid_credentials_when_profile_is_unstable() {
|
1031 + | let extended_api_events = vec![
|
1032 + | // First call to `provide_credentials` succeeds with the extended API.
|
1033 + | ReplayEvent::new(
|
1034 + | token_request("http://169.254.169.254", 21600),
|
1035 + | token_response(21600, TOKEN_A),
|
1036 + | ),
|
1037 + | ReplayEvent::new(
|
1038 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
1039 + | imds_response(r#"my-profile-0003"#),
|
1040 + | ),
|
1041 + | ReplayEvent::new(
|
1042 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0003", TOKEN_A),
|
1043 + | 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 \"AccountId\" : \"345678910112\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
|
1044 + | ),
|
1045 + |
|
1046 + | // Credentials retrieval failed due to unstable profile.
|
1047 + | ReplayEvent::new(
|
1048 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0003", TOKEN_A),
|
1049 + | imds_response_404(),
|
1050 + | ),
|
1051 + | // Start over and retrieve a new profile with the extended API.
|
1052 + | ReplayEvent::new(
|
1053 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
1054 + | imds_response(r#"my-profile-0003-b"#),
|
1055 + | ),
|
1056 + | ReplayEvent::new(
|
1057 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0003-b", TOKEN_A),
|
1058 + | 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 \"AccountId\" : \"314253647589\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
|
1059 + | ),
|
1060 + | ];
|
1061 + | run_test(extended_api_events, IdentityFn, |provider| {
|
1062 + | Box::pin(assert(
|
1063 + | provider,
|
1064 + | &[
|
1065 + | (Some("ASIARTEST"), Some("345678910112")),
|
1066 + | (Some("ASIARTEST"), Some("314253647589")),
|
1067 + | ],
|
1068 + | ))
|
1069 + | })
|
1070 + | .await;
|
1071 + |
|
1072 + | let legacy_api_events = vec![
|
1073 + | ReplayEvent::new(
|
1074 + | token_request("http://169.254.169.254", 21600),
|
1075 + | token_response(21600, TOKEN_A),
|
1076 + | ),
|
1077 + | ReplayEvent::new(
|
1078 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
1079 + | imds_response_404()
|
1080 + | ),
|
1081 + | ReplayEvent::new(
|
1082 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
1083 + | imds_response(r#"my-profile-0011"#),
|
1084 + | ),
|
1085 + | ReplayEvent::new(
|
1086 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0011", TOKEN_A),
|
1087 + | 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}"),
|
1088 + | ),
|
1089 + | // Credentials retrieval failed due to unstable profile.
|
1090 + | ReplayEvent::new(
|
1091 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0011", TOKEN_A),
|
1092 + | imds_response_404()
|
1093 + | ),
|
1094 + | // Start over and retrieve a new profile with the legacy API.
|
1095 + | ReplayEvent::new(
|
1096 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
|
1097 + | imds_response(r#"my-profile-0011-b"#),
|
1098 + | ),
|
1099 + | ReplayEvent::new(
|
1100 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0011-b", TOKEN_A),
|
1101 + | 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}"),
|
1102 + | ),
|
1103 + | ];
|
1104 + | run_test(legacy_api_events, IdentityFn, |provider| {
|
1105 + | Box::pin(assert(
|
1106 + | provider,
|
1107 + | &[(Some("ASIARTEST"), None), (Some("ASIARTEST"), None)],
|
1108 + | ))
|
1109 + | })
|
1110 + | .await;
|
1111 + | }
|
1112 + |
|
1113 + | #[tokio::test]
|
1114 + | async fn should_error_when_imds_remains_unstable_with_profile_configured_by_user() {
|
1115 + | // This negative test exercises the same code path for both the extended and legacy APIs.
|
1116 + | // A single set of events is sufficient for testing both.
|
1117 + | let events = vec![
|
1118 + | ReplayEvent::new(
|
1119 + | token_request("http://169.254.169.254", 21600),
|
1120 + | token_response(21600, TOKEN_A),
|
1121 + | ),
|
1122 + | ReplayEvent::new(
|
1123 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0004", TOKEN_A),
|
1124 + | imds_response_404(),
|
1125 + | ),
|
1126 + | // Try obtaining credentials again with the legacy API
|
1127 + | ReplayEvent::new(
|
1128 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/my-profile-0004", TOKEN_A),
|
1129 + | imds_response_404(),
|
1130 + | ),
|
1131 + | ];
|
1132 + | run_test(
|
1133 + | events,
|
1134 + | |b| b.profile("my-profile-0004"),
|
1135 + | |provider| {
|
1136 + | Box::pin(async move {
|
1137 + | let err = provider.provide_credentials().await.err().unwrap();
|
1138 + | matches!(err, CredentialsError::CredentialsNotLoaded(_));
|
1139 + | })
|
1140 + | },
|
1141 + | )
|
1142 + | .await;
|
1143 + | }
|
1144 + |
|
1145 + | #[tokio::test]
|
1146 + | async fn returns_valid_credentials_without_account_id_using_extended_api() {
|
1147 + | let extended_api_events = vec![
|
1148 + | ReplayEvent::new(
|
1149 + | token_request("http://169.254.169.254", 21600),
|
1150 + | token_response(21600, TOKEN_A),
|
1151 + | ),
|
1152 + | ReplayEvent::new(
|
1153 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/", TOKEN_A),
|
1154 + | imds_response(r#"my-profile-0005"#),
|
1155 + | ),
|
1156 + | ReplayEvent::new(
|
1157 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0005", TOKEN_A),
|
1158 + | 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}"),
|
1159 + | ),
|
1160 + | ReplayEvent::new(
|
1161 + | imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials-extended/my-profile-0005", TOKEN_A),
|
1162 + | 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}"),
|
1163 + | ),
|
1164 + | ];
|
1165 + | run_test(extended_api_events, IdentityFn, |provider| {
|
1166 + | Box::pin(assert(
|
1167 + | provider,
|
1168 + | &[(Some("ASIARTEST"), None), (Some("ASIARTEST"), None)],
|
1169 + | ))
|
1170 + | })
|
1171 + | .await;
|
1172 + | }
|
532 1173 | }
|