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