1use crate::http_credential_provider::HttpCredentialProvider;
59use crate::provider_config::ProviderConfig;
60use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
61use aws_smithy_http::endpoint::apply_endpoint;
62use aws_smithy_runtime_api::client::dns::{ResolveDns, ResolveDnsError, SharedDnsResolver};
63use aws_smithy_runtime_api::client::http::HttpConnectorSettings;
64use aws_smithy_runtime_api::shared::IntoShared;
65use aws_smithy_types::error::display::DisplayErrorContext;
66use aws_types::os_shim_internal::{Env, Fs};
67use http::header::InvalidHeaderValue;
68use http::uri::{InvalidUri, PathAndQuery, Scheme};
69use http::{HeaderValue, Uri};
70use std::error::Error;
71use std::fmt::{Display, Formatter};
72use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
73use std::time::Duration;
74use tokio::sync::OnceCell;
75
76const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(5);
77const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
78
79const BASE_HOST: &str = "http://169.254.170.2";
81const ENV_RELATIVE_URI: &str = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
82const ENV_FULL_URI: &str = "AWS_CONTAINER_CREDENTIALS_FULL_URI";
83const ENV_AUTHORIZATION_TOKEN: &str = "AWS_CONTAINER_AUTHORIZATION_TOKEN";
84const ENV_AUTHORIZATION_TOKEN_FILE: &str = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE";
85
86#[derive(Debug)]
92pub struct EcsCredentialsProvider {
93 inner: OnceCell<Provider>,
94 env: Env,
95 fs: Fs,
96 builder: Builder,
97}
98
99impl EcsCredentialsProvider {
100 pub fn builder() -> Builder {
102 Builder::default()
103 }
104
105 pub async fn credentials(&self) -> provider::Result {
107 let env_token_file = self.env.get(ENV_AUTHORIZATION_TOKEN_FILE).ok();
108 let env_token = self.env.get(ENV_AUTHORIZATION_TOKEN).ok();
109 let auth = if let Some(auth_token_file) = env_token_file {
110 let auth = self
111 .fs
112 .read_to_end(auth_token_file)
113 .await
114 .map_err(CredentialsError::provider_error)?;
115 Some(HeaderValue::from_bytes(auth.as_slice()).map_err(|err| {
116 let auth_token = String::from_utf8_lossy(auth.as_slice()).to_string();
117 tracing::warn!(token = %auth_token, "invalid auth token");
118 CredentialsError::invalid_configuration(EcsConfigurationError::InvalidAuthToken {
119 err,
120 value: auth_token,
121 })
122 })?)
123 } else if let Some(auth_token) = env_token {
124 Some(HeaderValue::from_str(&auth_token).map_err(|err| {
125 tracing::warn!(token = %auth_token, "invalid auth token");
126 CredentialsError::invalid_configuration(EcsConfigurationError::InvalidAuthToken {
127 err,
128 value: auth_token,
129 })
130 })?)
131 } else {
132 None
133 };
134 match self.provider().await {
135 Provider::NotConfigured => {
136 Err(CredentialsError::not_loaded("ECS provider not configured"))
137 }
138 Provider::InvalidConfiguration(err) => {
139 Err(CredentialsError::invalid_configuration(format!("{err}")))
140 }
141 Provider::Configured(provider) => provider.credentials(auth).await,
142 }
143 }
144
145 async fn provider(&self) -> &Provider {
146 self.inner
147 .get_or_init(|| Provider::make(self.builder.clone()))
148 .await
149 }
150}
151
152impl ProvideCredentials for EcsCredentialsProvider {
153 fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
154 where
155 Self: 'a,
156 {
157 future::ProvideCredentials::new(self.credentials())
158 }
159}
160
161#[derive(Debug)]
163#[allow(clippy::large_enum_variant)]
164enum Provider {
165 Configured(HttpCredentialProvider),
166 NotConfigured,
167 InvalidConfiguration(EcsConfigurationError),
168}
169
170impl Provider {
171 async fn uri(env: Env, dns: Option<SharedDnsResolver>) -> Result<Uri, EcsConfigurationError> {
172 let relative_uri = env.get(ENV_RELATIVE_URI).ok();
173 let full_uri = env.get(ENV_FULL_URI).ok();
174 if let Some(relative_uri) = relative_uri {
175 Self::build_full_uri(relative_uri)
176 } else if let Some(full_uri) = full_uri {
177 let dns = dns.or_else(default_dns);
178 validate_full_uri(&full_uri, dns)
179 .await
180 .map_err(|err| EcsConfigurationError::InvalidFullUri { err, uri: full_uri })
181 } else {
182 Err(EcsConfigurationError::NotConfigured)
183 }
184 }
185
186 async fn make(builder: Builder) -> Self {
187 let provider_config = builder.provider_config.unwrap_or_default();
188 let env = provider_config.env();
189 let uri = match Self::uri(env, builder.dns).await {
190 Ok(uri) => uri,
191 Err(EcsConfigurationError::NotConfigured) => return Provider::NotConfigured,
192 Err(err) => return Provider::InvalidConfiguration(err),
193 };
194 let path_and_query = match uri.path_and_query() {
195 Some(path_and_query) => path_and_query.to_string(),
196 None => uri.path().to_string(),
197 };
198 let endpoint = {
199 let mut parts = uri.into_parts();
200 parts.path_and_query = Some(PathAndQuery::from_static("/"));
201 Uri::from_parts(parts)
202 }
203 .expect("parts will be valid")
204 .to_string();
205
206 let http_provider = HttpCredentialProvider::builder()
207 .configure(&provider_config)
208 .http_connector_settings(
209 HttpConnectorSettings::builder()
210 .connect_timeout(DEFAULT_CONNECT_TIMEOUT)
211 .read_timeout(DEFAULT_READ_TIMEOUT)
212 .build(),
213 )
214 .build("EcsContainer", &endpoint, path_and_query);
215 Provider::Configured(http_provider)
216 }
217
218 fn build_full_uri(relative_uri: String) -> Result<Uri, EcsConfigurationError> {
219 let mut relative_uri = match relative_uri.parse::<Uri>() {
220 Ok(uri) => uri,
221 Err(invalid_uri) => {
222 tracing::warn!(uri = %DisplayErrorContext(&invalid_uri), "invalid URI loaded from environment");
223 return Err(EcsConfigurationError::InvalidRelativeUri {
224 err: invalid_uri,
225 uri: relative_uri,
226 });
227 }
228 };
229 let endpoint = Uri::from_static(BASE_HOST);
230 apply_endpoint(&mut relative_uri, &endpoint, None)
231 .expect("appending relative URLs to the ECS endpoint should always succeed");
232 Ok(relative_uri)
233 }
234}
235
236#[derive(Debug)]
237enum EcsConfigurationError {
238 InvalidRelativeUri {
239 err: InvalidUri,
240 uri: String,
241 },
242 InvalidFullUri {
243 err: InvalidFullUriError,
244 uri: String,
245 },
246 InvalidAuthToken {
247 err: InvalidHeaderValue,
248 value: String,
249 },
250 NotConfigured,
251}
252
253impl Display for EcsConfigurationError {
254 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
255 match self {
256 EcsConfigurationError::InvalidRelativeUri { err, uri } => {
257 write!(f, "invalid relative URI for ECS provider ({err}): {uri}",)
258 }
259 EcsConfigurationError::InvalidFullUri { err, uri } => {
260 write!(f, "invalid full URI for ECS provider ({err}): {uri}")
261 }
262 EcsConfigurationError::NotConfigured => write!(
263 f,
264 "No environment variables were set to configure ECS provider"
265 ),
266 EcsConfigurationError::InvalidAuthToken { err, value } => write!(
267 f,
268 "`{value}` could not be used as a header value for the auth token. {err}",
269 ),
270 }
271 }
272}
273
274impl Error for EcsConfigurationError {
275 fn source(&self) -> Option<&(dyn Error + 'static)> {
276 match &self {
277 EcsConfigurationError::InvalidRelativeUri { err, .. } => Some(err),
278 EcsConfigurationError::InvalidFullUri { err, .. } => Some(err),
279 EcsConfigurationError::InvalidAuthToken { err, .. } => Some(err),
280 EcsConfigurationError::NotConfigured => None,
281 }
282 }
283}
284
285#[derive(Default, Debug, Clone)]
287pub struct Builder {
288 provider_config: Option<ProviderConfig>,
289 dns: Option<SharedDnsResolver>,
290 connect_timeout: Option<Duration>,
291 read_timeout: Option<Duration>,
292}
293
294impl Builder {
295 pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
297 self.provider_config = Some(provider_config.clone());
298 self
299 }
300
301 pub fn dns(mut self, dns: impl ResolveDns + 'static) -> Self {
306 self.dns = Some(dns.into_shared());
307 self
308 }
309
310 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
314 self.connect_timeout = Some(timeout);
315 self
316 }
317
318 pub fn read_timeout(mut self, timeout: Duration) -> Self {
322 self.read_timeout = Some(timeout);
323 self
324 }
325
326 pub fn build(self) -> EcsCredentialsProvider {
328 let env = self
329 .provider_config
330 .as_ref()
331 .map(|config| config.env())
332 .unwrap_or_default();
333 let fs = self
334 .provider_config
335 .as_ref()
336 .map(|config| config.fs())
337 .unwrap_or_default();
338 EcsCredentialsProvider {
339 inner: OnceCell::new(),
340 env,
341 fs,
342 builder: self,
343 }
344 }
345}
346
347#[derive(Debug)]
348enum InvalidFullUriErrorKind {
349 #[non_exhaustive]
351 InvalidUri(InvalidUri),
352
353 #[non_exhaustive]
355 NoDnsResolver,
356
357 #[non_exhaustive]
359 MissingHost,
360
361 #[non_exhaustive]
363 DisallowedIP,
364
365 DnsLookupFailed(ResolveDnsError),
367}
368
369#[derive(Debug)]
374pub struct InvalidFullUriError {
375 kind: InvalidFullUriErrorKind,
376}
377
378impl Display for InvalidFullUriError {
379 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
380 use InvalidFullUriErrorKind::*;
381 match self.kind {
382 InvalidUri(_) => write!(f, "URI was invalid"),
383 MissingHost => write!(f, "URI did not specify a host"),
384 DisallowedIP => {
385 write!(f, "URI did not refer to an allowed IP address")
386 }
387 DnsLookupFailed(_) => {
388 write!(
389 f,
390 "failed to perform DNS lookup while validating URI"
391 )
392 }
393 NoDnsResolver => write!(f, "no DNS resolver was provided. Enable `rt-tokio` or provide a `dns` resolver to the builder.")
394 }
395 }
396}
397
398impl Error for InvalidFullUriError {
399 fn source(&self) -> Option<&(dyn Error + 'static)> {
400 use InvalidFullUriErrorKind::*;
401 match &self.kind {
402 InvalidUri(err) => Some(err),
403 DnsLookupFailed(err) => Some(err as _),
404 _ => None,
405 }
406 }
407}
408
409impl From<InvalidFullUriErrorKind> for InvalidFullUriError {
410 fn from(kind: InvalidFullUriErrorKind) -> Self {
411 Self { kind }
412 }
413}
414
415async fn validate_full_uri(
423 uri: &str,
424 dns: Option<SharedDnsResolver>,
425) -> Result<Uri, InvalidFullUriError> {
426 let uri = uri
427 .parse::<Uri>()
428 .map_err(InvalidFullUriErrorKind::InvalidUri)?;
429 if uri.scheme() == Some(&Scheme::HTTPS) {
430 return Ok(uri);
431 }
432 let host = uri.host().ok_or(InvalidFullUriErrorKind::MissingHost)?;
434 let maybe_ip = if host.starts_with('[') && host.ends_with(']') {
435 host[1..host.len() - 1].parse::<IpAddr>()
436 } else {
437 host.parse::<IpAddr>()
438 };
439 let is_allowed = match maybe_ip {
440 Ok(addr) => is_full_uri_ip_allowed(&addr),
441 Err(_domain_name) => {
442 let dns = dns.ok_or(InvalidFullUriErrorKind::NoDnsResolver)?;
443 dns.resolve_dns(host)
444 .await
445 .map_err(|err| InvalidFullUriErrorKind::DnsLookupFailed(ResolveDnsError::new(err)))?
446 .iter()
447 .all(|addr| {
448 if !is_full_uri_ip_allowed(addr) {
449 tracing::warn!(
450 addr = ?addr,
451 "HTTP credential provider cannot be used: Address does not resolve to an allowed IP."
452 )
453 };
454 is_full_uri_ip_allowed(addr)
455 })
456 }
457 };
458 match is_allowed {
459 true => Ok(uri),
460 false => Err(InvalidFullUriErrorKind::DisallowedIP.into()),
461 }
462}
463
464const ECS_CONTAINER_IPV4: IpAddr = IpAddr::V4(Ipv4Addr::new(169, 254, 170, 2));
466
467const EKS_CONTAINER_IPV4: IpAddr = IpAddr::V4(Ipv4Addr::new(169, 254, 170, 23));
469
470const EKS_CONTAINER_IPV6: IpAddr = IpAddr::V6(Ipv6Addr::new(0xFD00, 0x0EC2, 0, 0, 0, 0, 0, 0x23));
472fn is_full_uri_ip_allowed(ip: &IpAddr) -> bool {
473 ip.is_loopback()
474 || ip.eq(&ECS_CONTAINER_IPV4)
475 || ip.eq(&EKS_CONTAINER_IPV4)
476 || ip.eq(&EKS_CONTAINER_IPV6)
477}
478
479#[cfg(any(not(feature = "rt-tokio"), target_family = "wasm"))]
483fn default_dns() -> Option<SharedDnsResolver> {
484 None
485}
486#[cfg(all(feature = "rt-tokio", not(target_family = "wasm")))]
487fn default_dns() -> Option<SharedDnsResolver> {
488 use aws_smithy_runtime::client::dns::TokioDnsResolver;
489 Some(TokioDnsResolver::new().into_shared())
490}
491
492#[cfg(test)]
493mod test {
494 use super::*;
495 use crate::provider_config::ProviderConfig;
496 use crate::test_case::{no_traffic_client, GenericTestResult};
497 use aws_credential_types::provider::ProvideCredentials;
498 use aws_credential_types::Credentials;
499 use aws_smithy_async::future::never::Never;
500 use aws_smithy_async::rt::sleep::TokioSleep;
501 use aws_smithy_http_client::test_util::{ReplayEvent, StaticReplayClient};
502 use aws_smithy_runtime_api::client::dns::DnsFuture;
503 use aws_smithy_runtime_api::client::http::HttpClient;
504 use aws_smithy_runtime_api::shared::IntoShared;
505 use aws_smithy_types::body::SdkBody;
506 use aws_types::os_shim_internal::Env;
507 use futures_util::FutureExt;
508 use http::header::AUTHORIZATION;
509 use http::Uri;
510 use serde::Deserialize;
511 use std::collections::HashMap;
512 use std::error::Error;
513 use std::ffi::OsString;
514 use std::net::IpAddr;
515 use std::time::{Duration, UNIX_EPOCH};
516 use tracing_test::traced_test;
517
518 fn provider(
519 env: Env,
520 fs: Fs,
521 http_client: impl HttpClient + 'static,
522 ) -> EcsCredentialsProvider {
523 let provider_config = ProviderConfig::empty()
524 .with_env(env)
525 .with_fs(fs)
526 .with_http_client(http_client)
527 .with_sleep_impl(TokioSleep::new());
528 Builder::default().configure(&provider_config).build()
529 }
530
531 #[derive(Deserialize)]
532 struct EcsUriTest {
533 env: HashMap<String, String>,
534 result: GenericTestResult<String>,
535 }
536
537 impl EcsUriTest {
538 async fn check(&self) {
539 let env = Env::from(self.env.clone());
540 let uri = Provider::uri(env, Some(TestDns::default().into_shared()))
541 .await
542 .map(|uri| uri.to_string());
543 self.result.assert_matches(uri.as_ref());
544 }
545 }
546
547 #[tokio::test]
548 async fn run_config_tests() -> Result<(), Box<dyn Error>> {
549 let test_cases = std::fs::read_to_string("test-data/ecs-tests.json")?;
550 #[derive(Deserialize)]
551 struct TestCases {
552 tests: Vec<EcsUriTest>,
553 }
554
555 let test_cases: TestCases = serde_json::from_str(&test_cases)?;
556 let test_cases = test_cases.tests;
557 for test in test_cases {
558 test.check().await
559 }
560 Ok(())
561 }
562
563 #[test]
564 fn validate_uri_https() {
565 let dns = Some(NeverDns.into_shared());
567 assert_eq!(
568 validate_full_uri("https://amazon.com", None)
569 .now_or_never()
570 .unwrap()
571 .expect("valid"),
572 Uri::from_static("https://amazon.com")
573 );
574 assert!(
576 validate_full_uri("http://amazon.com", dns)
577 .now_or_never()
578 .is_none(),
579 "DNS lookup should occur, but it will never return"
580 );
581
582 let no_dns_error = validate_full_uri("http://amazon.com", None)
583 .now_or_never()
584 .unwrap()
585 .expect_err("DNS service is required");
586 assert!(
587 matches!(
588 no_dns_error,
589 InvalidFullUriError {
590 kind: InvalidFullUriErrorKind::NoDnsResolver
591 }
592 ),
593 "expected no dns service, got: {}",
594 no_dns_error
595 );
596 }
597
598 #[test]
599 fn valid_uri_loopback() {
600 assert_eq!(
601 validate_full_uri("http://127.0.0.1:8080/get-credentials", None)
602 .now_or_never()
603 .unwrap()
604 .expect("valid uri"),
605 Uri::from_static("http://127.0.0.1:8080/get-credentials")
606 );
607
608 let err = validate_full_uri("http://192.168.10.120/creds", None)
609 .now_or_never()
610 .unwrap()
611 .expect_err("not a loopback");
612 assert!(matches!(
613 err,
614 InvalidFullUriError {
615 kind: InvalidFullUriErrorKind::DisallowedIP
616 }
617 ));
618 }
619
620 #[test]
621 fn valid_uri_ecs_eks() {
622 assert_eq!(
623 validate_full_uri("http://169.254.170.2:8080/get-credentials", None)
624 .now_or_never()
625 .unwrap()
626 .expect("valid uri"),
627 Uri::from_static("http://169.254.170.2:8080/get-credentials")
628 );
629 assert_eq!(
630 validate_full_uri("http://169.254.170.23:8080/get-credentials", None)
631 .now_or_never()
632 .unwrap()
633 .expect("valid uri"),
634 Uri::from_static("http://169.254.170.23:8080/get-credentials")
635 );
636 assert_eq!(
637 validate_full_uri("http://[fd00:ec2::23]:8080/get-credentials", None)
638 .now_or_never()
639 .unwrap()
640 .expect("valid uri"),
641 Uri::from_static("http://[fd00:ec2::23]:8080/get-credentials")
642 );
643
644 let err = validate_full_uri("http://169.254.171.23/creds", None)
645 .now_or_never()
646 .unwrap()
647 .expect_err("not an ecs/eks container address");
648 assert!(matches!(
649 err,
650 InvalidFullUriError {
651 kind: InvalidFullUriErrorKind::DisallowedIP
652 }
653 ));
654
655 let err = validate_full_uri("http://[fd00:ec2::2]/creds", None)
656 .now_or_never()
657 .unwrap()
658 .expect_err("not an ecs/eks container address");
659 assert!(matches!(
660 err,
661 InvalidFullUriError {
662 kind: InvalidFullUriErrorKind::DisallowedIP
663 }
664 ));
665 }
666
667 #[test]
668 fn all_addrs_local() {
669 let dns = Some(
670 TestDns::with_fallback(vec![
671 "127.0.0.1".parse().unwrap(),
672 "127.0.0.2".parse().unwrap(),
673 "169.254.170.23".parse().unwrap(),
674 "fd00:ec2::23".parse().unwrap(),
675 ])
676 .into_shared(),
677 );
678 let resp = validate_full_uri("http://localhost:8888", dns)
679 .now_or_never()
680 .unwrap();
681 assert!(resp.is_ok(), "Should be valid: {:?}", resp);
682 }
683
684 #[test]
685 fn all_addrs_not_local() {
686 let dns = Some(
687 TestDns::with_fallback(vec![
688 "127.0.0.1".parse().unwrap(),
689 "192.168.0.1".parse().unwrap(),
690 ])
691 .into_shared(),
692 );
693 let resp = validate_full_uri("http://localhost:8888", dns)
694 .now_or_never()
695 .unwrap();
696 assert!(
697 matches!(
698 resp,
699 Err(InvalidFullUriError {
700 kind: InvalidFullUriErrorKind::DisallowedIP
701 })
702 ),
703 "Should be invalid: {:?}",
704 resp
705 );
706 }
707
708 fn creds_request(uri: &str, auth: Option<&str>) -> http::Request<SdkBody> {
709 let mut builder = http::Request::builder();
710 if let Some(auth) = auth {
711 builder = builder.header(AUTHORIZATION, auth);
712 }
713 builder.uri(uri).body(SdkBody::empty()).unwrap()
714 }
715
716 fn ok_creds_response() -> http::Response<SdkBody> {
717 http::Response::builder()
718 .status(200)
719 .body(SdkBody::from(
720 r#" {
721 "AccessKeyId" : "AKID",
722 "SecretAccessKey" : "SECRET",
723 "Token" : "TOKEN....=",
724 "AccountId" : "AID",
725 "Expiration" : "2009-02-13T23:31:30Z"
726 }"#,
727 ))
728 .unwrap()
729 }
730
731 #[track_caller]
732 fn assert_correct(creds: Credentials) {
733 assert_eq!(creds.access_key_id(), "AKID");
734 assert_eq!(creds.secret_access_key(), "SECRET");
735 assert_eq!(creds.account_id().unwrap().as_str(), "AID");
736 assert_eq!(creds.session_token().unwrap(), "TOKEN....=");
737 assert_eq!(
738 creds.expiry().unwrap(),
739 UNIX_EPOCH + Duration::from_secs(1234567890)
740 );
741 }
742
743 #[tokio::test]
744 async fn load_valid_creds_auth() {
745 let env = Env::from_slice(&[
746 ("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/credentials"),
747 ("AWS_CONTAINER_AUTHORIZATION_TOKEN", "Basic password"),
748 ]);
749 let http_client = StaticReplayClient::new(vec![ReplayEvent::new(
750 creds_request("http://169.254.170.2/credentials", Some("Basic password")),
751 ok_creds_response(),
752 )]);
753 let provider = provider(env, Fs::default(), http_client.clone());
754 let creds = provider
755 .provide_credentials()
756 .await
757 .expect("valid credentials");
758 assert_correct(creds);
759 http_client.assert_requests_match(&[]);
760 }
761
762 #[tokio::test]
763 async fn load_valid_creds_auth_file() {
764 let env = Env::from_slice(&[
765 (
766 "AWS_CONTAINER_CREDENTIALS_FULL_URI",
767 "http://169.254.170.23/v1/credentials",
768 ),
769 (
770 "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
771 "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token",
772 ),
773 ]);
774 let fs = Fs::from_raw_map(HashMap::from([(
775 OsString::from(
776 "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token",
777 ),
778 "Basic password".into(),
779 )]));
780
781 let http_client = StaticReplayClient::new(vec![ReplayEvent::new(
782 creds_request(
783 "http://169.254.170.23/v1/credentials",
784 Some("Basic password"),
785 ),
786 ok_creds_response(),
787 )]);
788 let provider = provider(env, fs, http_client.clone());
789 let creds = provider
790 .provide_credentials()
791 .await
792 .expect("valid credentials");
793 assert_correct(creds);
794 http_client.assert_requests_match(&[]);
795 }
796
797 #[tokio::test]
798 async fn auth_file_precedence_over_env() {
799 let env = Env::from_slice(&[
800 (
801 "AWS_CONTAINER_CREDENTIALS_FULL_URI",
802 "http://169.254.170.23/v1/credentials",
803 ),
804 (
805 "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
806 "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token",
807 ),
808 ("AWS_CONTAINER_AUTHORIZATION_TOKEN", "unused"),
809 ]);
810 let fs = Fs::from_raw_map(HashMap::from([(
811 OsString::from(
812 "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token",
813 ),
814 "Basic password".into(),
815 )]));
816
817 let http_client = StaticReplayClient::new(vec![ReplayEvent::new(
818 creds_request(
819 "http://169.254.170.23/v1/credentials",
820 Some("Basic password"),
821 ),
822 ok_creds_response(),
823 )]);
824 let provider = provider(env, fs, http_client.clone());
825 let creds = provider
826 .provide_credentials()
827 .await
828 .expect("valid credentials");
829 assert_correct(creds);
830 http_client.assert_requests_match(&[]);
831 }
832
833 #[tokio::test]
834 async fn query_params_should_be_included_in_credentials_http_request() {
835 let env = Env::from_slice(&[
836 (
837 "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
838 "/my-credentials/?applicationName=test2024",
839 ),
840 (
841 "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
842 "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token",
843 ),
844 ("AWS_CONTAINER_AUTHORIZATION_TOKEN", "unused"),
845 ]);
846 let fs = Fs::from_raw_map(HashMap::from([(
847 OsString::from(
848 "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token",
849 ),
850 "Basic password".into(),
851 )]));
852
853 let http_client = StaticReplayClient::new(vec![ReplayEvent::new(
854 creds_request(
855 "http://169.254.170.2/my-credentials/?applicationName=test2024",
856 Some("Basic password"),
857 ),
858 ok_creds_response(),
859 )]);
860 let provider = provider(env, fs, http_client.clone());
861 let creds = provider
862 .provide_credentials()
863 .await
864 .expect("valid credentials");
865 assert_correct(creds);
866 http_client.assert_requests_match(&[]);
867 }
868
869 #[tokio::test]
870 async fn fs_missing_file() {
871 let env = Env::from_slice(&[
872 (
873 "AWS_CONTAINER_CREDENTIALS_FULL_URI",
874 "http://169.254.170.23/v1/credentials",
875 ),
876 (
877 "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
878 "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token",
879 ),
880 ]);
881 let fs = Fs::from_raw_map(HashMap::new());
882
883 let provider = provider(env, fs, no_traffic_client());
884 let err = provider.credentials().await.expect_err("no JWT token file");
885 match err {
886 CredentialsError::ProviderError { .. } => { }
887 _ => panic!("incorrect error variant"),
888 }
889 }
890
891 #[tokio::test]
892 async fn retry_5xx() {
893 let env = Env::from_slice(&[("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/credentials")]);
894 let http_client = StaticReplayClient::new(vec![
895 ReplayEvent::new(
896 creds_request("http://169.254.170.2/credentials", None),
897 http::Response::builder()
898 .status(500)
899 .body(SdkBody::empty())
900 .unwrap(),
901 ),
902 ReplayEvent::new(
903 creds_request("http://169.254.170.2/credentials", None),
904 ok_creds_response(),
905 ),
906 ]);
907 tokio::time::pause();
908 let provider = provider(env, Fs::default(), http_client.clone());
909 let creds = provider
910 .provide_credentials()
911 .await
912 .expect("valid credentials");
913 assert_correct(creds);
914 }
915
916 #[tokio::test]
917 async fn load_valid_creds_no_auth() {
918 let env = Env::from_slice(&[("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/credentials")]);
919 let http_client = StaticReplayClient::new(vec![ReplayEvent::new(
920 creds_request("http://169.254.170.2/credentials", None),
921 ok_creds_response(),
922 )]);
923 let provider = provider(env, Fs::default(), http_client.clone());
924 let creds = provider
925 .provide_credentials()
926 .await
927 .expect("valid credentials");
928 assert_correct(creds);
929 http_client.assert_requests_match(&[]);
930 }
931
932 #[allow(unused_attributes)]
934 #[tokio::test]
935 #[traced_test]
936 #[ignore]
937 async fn real_dns_lookup() {
938 let dns = Some(
939 default_dns()
940 .expect("feature must be enabled")
941 .into_shared(),
942 );
943 let err = validate_full_uri("http://www.amazon.com/creds", dns.clone())
944 .await
945 .expect_err("not a valid IP");
946 assert!(
947 matches!(
948 err,
949 InvalidFullUriError {
950 kind: InvalidFullUriErrorKind::DisallowedIP
951 }
952 ),
953 "{:?}",
954 err
955 );
956 assert!(logs_contain("Address does not resolve to an allowed IP"));
957 validate_full_uri("http://localhost:8888/creds", dns.clone())
958 .await
959 .expect("localhost is the loopback interface");
960 validate_full_uri("http://169.254.170.2.backname.io:8888/creds", dns.clone())
961 .await
962 .expect("169.254.170.2.backname.io is the ecs container address");
963 validate_full_uri("http://169.254.170.23.backname.io:8888/creds", dns.clone())
964 .await
965 .expect("169.254.170.23.backname.io is the eks pod identity address");
966 validate_full_uri("http://fd00-ec2--23.backname.io:8888/creds", dns)
967 .await
968 .expect("fd00-ec2--23.backname.io is the eks pod identity address");
969 }
970
971 #[derive(Clone, Debug)]
973 struct TestDns {
974 addrs: HashMap<String, Vec<IpAddr>>,
975 fallback: Vec<IpAddr>,
976 }
977
978 impl Default for TestDns {
980 fn default() -> Self {
981 let mut addrs = HashMap::new();
982 addrs.insert(
983 "localhost".into(),
984 vec!["127.0.0.1".parse().unwrap(), "127.0.0.2".parse().unwrap()],
985 );
986 TestDns {
987 addrs,
988 fallback: vec!["72.21.210.29".parse().unwrap()],
990 }
991 }
992 }
993
994 impl TestDns {
995 fn with_fallback(fallback: Vec<IpAddr>) -> Self {
996 TestDns {
997 addrs: Default::default(),
998 fallback,
999 }
1000 }
1001 }
1002
1003 impl ResolveDns for TestDns {
1004 fn resolve_dns<'a>(&'a self, name: &'a str) -> DnsFuture<'a> {
1005 DnsFuture::ready(Ok(self.addrs.get(name).unwrap_or(&self.fallback).clone()))
1006 }
1007 }
1008
1009 #[derive(Debug)]
1010 struct NeverDns;
1011 impl ResolveDns for NeverDns {
1012 fn resolve_dns<'a>(&'a self, _name: &'a str) -> DnsFuture<'a> {
1013 DnsFuture::new(async {
1014 Never::new().await;
1015 unreachable!()
1016 })
1017 }
1018 }
1019}