1use std::collections::BTreeMap;
19use std::sync::Arc;
20use std::time::Instant;
21
22use async_trait::async_trait;
23use bytes::Buf;
24use chrono::{DateTime, Utc};
25use percent_encoding::utf8_percent_encode;
26use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
27use reqwest::{Client, Method, Request, RequestBuilder, StatusCode};
28use serde::Deserialize;
29use tracing::warn;
30use url::Url;
31
32use crate::retry::RetryExt;
33use crate::service::HttpService;
34use crate::token::{TemporaryToken, TokenCache};
35use crate::util::{hex_digest, hex_encode, hmac_sha256};
36use crate::{CredentialProvider, Result, RetryConfig, TokenProvider};
37
38use crate::util::STRICT_ENCODE_SET;
39
40const STRICT_PATH_ENCODE_SET: percent_encoding::AsciiSet = STRICT_ENCODE_SET.remove(b'/');
42
43type StdError = Box<dyn std::error::Error + Send + Sync>;
44
45static EMPTY_SHA256_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
47
48#[derive(Eq, PartialEq)]
50pub struct AwsCredential {
51 pub key_id: String,
53 pub secret_key: String,
55 pub token: Option<String>,
57}
58
59impl std::fmt::Debug for AwsCredential {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("AwsCredential")
65 .field("key_id", &"<redacted>")
66 .field("secret_key", &"<redacted>")
67 .field("token", &self.token.as_ref().map(|_| "<redacted>"))
68 .finish()
69 }
70}
71
72impl AwsCredential {
73 fn sign(&self, to_sign: &str, date: DateTime<Utc>, region: &str, service: &str) -> String {
77 let date_string = date.format("%Y%m%d").to_string();
78 let date_hmac = hmac_sha256(format!("AWS4{}", self.secret_key), date_string);
79 let region_hmac = hmac_sha256(date_hmac, region);
80 let service_hmac = hmac_sha256(region_hmac, service);
81 let signing_hmac = hmac_sha256(service_hmac, b"aws4_request");
82 hex_encode(hmac_sha256(signing_hmac, to_sign).as_ref())
83 }
84}
85
86#[derive(Debug)]
90pub struct AwsAuthorizer<'a> {
91 date: Option<DateTime<Utc>>,
92 credential: &'a AwsCredential,
93 service: &'a str,
94 region: &'a str,
95}
96
97static DATE_HEADER: hyper::header::HeaderName =
98 hyper::header::HeaderName::from_static("x-amz-date");
99static HASH_HEADER: hyper::header::HeaderName =
100 hyper::header::HeaderName::from_static("x-amz-content-sha256");
101static TOKEN_HEADER: hyper::header::HeaderName =
102 hyper::header::HeaderName::from_static("x-amz-security-token");
103const ALGORITHM: &str = "AWS4-HMAC-SHA256";
104
105impl<'a> AwsAuthorizer<'a> {
106 pub fn new(credential: &'a AwsCredential, service: &'a str, region: &'a str) -> Self {
108 Self {
109 credential,
110 service,
111 region,
112 date: None,
113 }
114 }
115
116 pub fn authorize(
121 &self,
122 request: &mut Request,
123 pre_calculated_digest: Option<&[u8]>,
124 ) -> crate::Result<()> {
125 if let Some(ref token) = self.credential.token {
126 let token_val = HeaderValue::from_str(token)?;
127 request.headers_mut().insert(&TOKEN_HEADER, token_val);
128 }
129
130 let host = &request.url()[url::Position::BeforeHost..url::Position::AfterPort];
131 let host_val = HeaderValue::from_str(host)?;
132 request.headers_mut().insert("host", host_val);
133
134 let date = self.date.unwrap_or_else(Utc::now);
135 let date_str = date.format("%Y%m%dT%H%M%SZ").to_string();
136 let date_val = HeaderValue::from_str(&date_str)?;
137 request.headers_mut().insert(&DATE_HEADER, date_val);
138
139 let digest = match pre_calculated_digest {
140 Some(digest) => hex_encode(digest),
141 None => match request.body() {
142 None => EMPTY_SHA256_HASH.to_string(),
143 Some(body) => match body.as_bytes() {
144 Some(bytes) => hex_digest(bytes),
145 None => EMPTY_SHA256_HASH.to_string(),
146 },
147 },
148 };
149
150 let header_digest = HeaderValue::from_str(&digest)?;
151 request.headers_mut().insert(&HASH_HEADER, header_digest);
152
153 let (signed_headers, canonical_headers) = canonicalize_headers(request.headers());
154
155 let scope = self.scope(date);
156
157 let string_to_sign = self.string_to_sign(
158 date,
159 &scope,
160 request.method(),
161 request.url(),
162 &canonical_headers,
163 &signed_headers,
164 &digest,
165 );
166
167 let signature = self
168 .credential
169 .sign(&string_to_sign, date, self.region, self.service);
170
171 let authorisation = format!(
172 "{} Credential={}/{}, SignedHeaders={}, Signature={}",
173 ALGORITHM, self.credential.key_id, scope, signed_headers, signature
174 );
175
176 let authorization_val = HeaderValue::from_str(&authorisation)?;
177 request
178 .headers_mut()
179 .insert(&AUTHORIZATION, authorization_val);
180 Ok(())
181 }
182
183 #[allow(clippy::too_many_arguments)]
184 fn string_to_sign(
185 &self,
186 date: DateTime<Utc>,
187 scope: &str,
188 request_method: &Method,
189 url: &Url,
190 canonical_headers: &str,
191 signed_headers: &str,
192 digest: &str,
193 ) -> String {
194 let canonical_uri = match self.service {
198 "s3" => url.path().to_string(),
199 _ => utf8_percent_encode(url.path(), &STRICT_PATH_ENCODE_SET).to_string(),
200 };
201
202 let canonical_query = canonicalize_query(url);
203
204 let canonical_request = format!(
205 "{}\n{}\n{}\n{}\n{}\n{}",
206 request_method.as_str(),
207 canonical_uri,
208 canonical_query,
209 canonical_headers,
210 signed_headers,
211 digest
212 );
213
214 let hashed_canonical_request = hex_digest(canonical_request.as_bytes());
215
216 format!(
217 "{}\n{}\n{}\n{}",
218 ALGORITHM,
219 date.format("%Y%m%dT%H%M%SZ"),
220 scope,
221 hashed_canonical_request
222 )
223 }
224
225 fn scope(&self, date: DateTime<Utc>) -> String {
226 format!(
227 "{}/{}/{}/aws4_request",
228 date.format("%Y%m%d"),
229 self.region,
230 self.service
231 )
232 }
233}
234
235pub(crate) trait CredentialExt {
236 fn with_aws_sigv4(
249 self,
250 authorizer: Option<AwsAuthorizer<'_>>,
251 payload_sha256: Option<&[u8]>,
252 ) -> Self;
253}
254
255impl CredentialExt for RequestBuilder {
256 fn with_aws_sigv4(
257 self,
258 authorizer: Option<AwsAuthorizer<'_>>,
259 payload_sha256: Option<&[u8]>,
260 ) -> Self {
261 match authorizer {
262 Some(authorizer) => {
263 let (client, request) = self.build_split();
264 let mut request = request.expect("request valid");
265 authorizer
266 .authorize(&mut request, payload_sha256)
267 .expect("credential values must be valid header characters");
268
269 Self::from_parts(client, request)
270 }
271 None => self,
272 }
273 }
274}
275
276fn canonicalize_query(url: &Url) -> String {
280 use std::fmt::Write;
281
282 let capacity = match url.query() {
283 Some(q) if !q.is_empty() => q.len(),
284 _ => return String::new(),
285 };
286 let mut encoded = String::with_capacity(capacity + 1);
287
288 let mut headers = url.query_pairs().collect::<Vec<_>>();
289 headers.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
290
291 let mut first = true;
292 for (k, v) in headers {
293 if !first {
294 encoded.push('&');
295 }
296 first = false;
297 let _ = write!(
298 encoded,
299 "{}={}",
300 utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET),
301 utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET)
302 );
303 }
304 encoded
305}
306
307fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
311 let mut headers = BTreeMap::<&str, Vec<String>>::new();
314 let mut value_count = 0;
315 let mut value_bytes = 0;
316 let mut key_bytes = 0;
317
318 for (key, value) in header_map {
319 let key = key.as_str();
320 if ["authorization", "content-length", "user-agent"].contains(&key) {
321 continue;
322 }
323
324 let value = String::from_utf8_lossy(value.as_bytes()).into_owned();
325 key_bytes += key.len();
326 value_bytes += value.len();
327 value_count += 1;
328 headers.entry(key).or_default().push(value);
329 }
330
331 let mut signed_headers = String::with_capacity(key_bytes + headers.len());
332 let mut canonical_headers =
333 String::with_capacity(key_bytes + value_bytes + headers.len() + value_count);
334
335 for (header_idx, (name, values)) in headers.into_iter().enumerate() {
336 if header_idx != 0 {
337 signed_headers.push(';');
338 }
339
340 signed_headers.push_str(name);
341 canonical_headers.push_str(name);
342 canonical_headers.push(':');
343 for (value_idx, value) in values.into_iter().enumerate() {
344 if value_idx != 0 {
345 canonical_headers.push(',');
346 }
347 canonical_headers.push_str(value.trim());
348 }
349 canonical_headers.push('\n');
350 }
351
352 (signed_headers, canonical_headers)
353}
354
355#[derive(Debug)]
365pub(crate) struct InstanceCredentialProvider {
366 pub imdsv1_fallback: bool,
367 pub metadata_endpoint: String,
368}
369
370#[async_trait]
371impl TokenProvider for InstanceCredentialProvider {
372 type Credential = AwsCredential;
373
374 async fn fetch_token(
385 &self,
386 client: &Client,
387 service: &Arc<dyn HttpService>,
388 retry: &RetryConfig,
389 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
390 instance_creds(
391 client,
392 service,
393 retry,
394 &self.metadata_endpoint,
395 self.imdsv1_fallback,
396 )
397 .await
398 .map_err(|source| crate::Error::Generic { source })
399 }
400}
401
402#[derive(Debug)]
414pub(crate) struct WebIdentityProvider {
415 pub token_path: String,
416 pub role_arn: String,
417 pub session_name: String,
418 pub endpoint: String,
419}
420
421#[async_trait]
422impl TokenProvider for WebIdentityProvider {
423 type Credential = AwsCredential;
424
425 async fn fetch_token(
436 &self,
437 client: &Client,
438 service: &Arc<dyn HttpService>,
439 retry: &RetryConfig,
440 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
441 web_identity(
442 client,
443 service,
444 retry,
445 &self.token_path,
446 &self.role_arn,
447 &self.session_name,
448 &self.endpoint,
449 )
450 .await
451 .map_err(|source| crate::Error::Generic { source })
452 }
453}
454
455#[derive(Debug, Deserialize)]
456#[serde(rename_all = "PascalCase")]
457struct InstanceCredentials {
458 access_key_id: String,
459 secret_access_key: String,
460 token: String,
461 expiration: DateTime<Utc>,
462}
463
464impl From<InstanceCredentials> for AwsCredential {
465 fn from(s: InstanceCredentials) -> Self {
466 Self {
467 key_id: s.access_key_id,
468 secret_key: s.secret_access_key,
469 token: Some(s.token),
470 }
471 }
472}
473
474async fn instance_creds(
476 client: &Client,
477 service: &Arc<dyn HttpService>,
478 retry_config: &RetryConfig,
479 endpoint: &str,
480 imdsv1_fallback: bool,
481) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
482 const CREDENTIALS_PATH: &str = "latest/meta-data/iam/security-credentials";
483 const AWS_EC2_METADATA_TOKEN_HEADER: &str = "X-aws-ec2-metadata-token";
484
485 let token_url = format!("{endpoint}/latest/api/token");
486
487 let token_result = client
488 .request(Method::PUT, token_url)
489 .header("X-aws-ec2-metadata-token-ttl-seconds", "600") .retryable(retry_config, service.clone())
491 .idempotent(true)
492 .send()
493 .await;
494
495 let token = match token_result {
496 Ok(t) => Some(t.text().await?),
497 Err(e) if imdsv1_fallback && matches!(e.status(), Some(StatusCode::FORBIDDEN)) => {
498 warn!("received 403 from metadata endpoint, falling back to IMDSv1");
499 None
500 }
501 Err(e) => return Err(e.into()),
502 };
503
504 let role_url = format!("{endpoint}/{CREDENTIALS_PATH}/");
505 let mut role_request = client.request(Method::GET, role_url);
506
507 if let Some(token) = &token {
508 role_request = role_request.header(AWS_EC2_METADATA_TOKEN_HEADER, token);
509 }
510
511 let role = role_request
512 .send_retry(retry_config, service.clone())
513 .await?
514 .text()
515 .await?;
516
517 let creds_url = format!("{endpoint}/{CREDENTIALS_PATH}/{role}");
518 let mut creds_request = client.request(Method::GET, creds_url);
519 if let Some(token) = &token {
520 creds_request = creds_request.header(AWS_EC2_METADATA_TOKEN_HEADER, token);
521 }
522
523 let creds: InstanceCredentials = creds_request
524 .send_retry(retry_config, service.clone())
525 .await?
526 .json()
527 .await?;
528
529 let now = Utc::now();
530 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
531 Ok(TemporaryToken {
532 token: Arc::new(creds.into()),
533 expiry: Some(Instant::now() + ttl),
534 })
535}
536
537#[derive(Debug)]
549pub(crate) struct AssumeRoleProvider {
550 pub role_arn: String,
551 pub session_name: String,
552 pub endpoint: String,
553 pub base_credentials: Arc<dyn CredentialProvider<Credential = AwsCredential>>,
554 pub region: String,
555 pub policy: Option<String>,
558}
559
560#[async_trait]
561impl TokenProvider for AssumeRoleProvider {
562 type Credential = AwsCredential;
563
564 async fn fetch_token(
575 &self,
576 client: &Client,
577 service: &Arc<dyn HttpService>,
578 retry: &RetryConfig,
579 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
580 let base_cred = self
581 .base_credentials
582 .get_credential()
583 .await
584 .map_err(|source| crate::Error::Generic {
585 source: Box::new(source),
586 })?;
587
588 assume_role(
589 client,
590 service,
591 retry,
592 &base_cred,
593 &self.region,
594 &self.role_arn,
595 &self.session_name,
596 &self.endpoint,
597 self.policy.as_deref(),
598 )
599 .await
600 .map_err(|source| crate::Error::Generic { source })
601 }
602}
603
604#[allow(clippy::too_many_arguments)]
611async fn assume_role(
612 client: &Client,
613 service: &Arc<dyn HttpService>,
614 retry_config: &RetryConfig,
615 base_cred: &AwsCredential,
616 region: &str,
617 role_arn: &str,
618 session_name: &str,
619 endpoint: &str,
620 policy: Option<&str>,
621) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
622 let mut query: Vec<(&str, &str)> = vec![
623 ("Action", "AssumeRole"),
624 ("DurationSeconds", "3600"),
625 ("RoleArn", role_arn),
626 ("RoleSessionName", session_name),
627 ("Version", "2011-06-15"),
628 ];
629 if let Some(p) = policy {
630 query.push(("Policy", p));
631 }
632 let bytes = client
633 .request(Method::POST, endpoint)
634 .query(&query)
635 .with_aws_sigv4(Some(AwsAuthorizer::new(base_cred, "sts", region)), None)
636 .retryable(retry_config, service.clone())
637 .idempotent(true)
638 .send()
639 .await?
640 .bytes()
641 .await?;
642
643 let resp: AssumeRoleXmlResponse = quick_xml::de::from_reader(bytes.reader())
644 .map_err(|e| format!("Invalid AssumeRole response: {e}"))?;
645
646 let creds = resp.assume_role_result.credentials;
647 let now = Utc::now();
648 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
649
650 Ok(TemporaryToken {
651 token: Arc::new(creds.into()),
652 expiry: Some(Instant::now() + ttl),
653 })
654}
655
656#[derive(Debug, Deserialize)]
658#[serde(rename_all = "PascalCase")]
659struct AssumeRoleXmlResponse {
660 assume_role_result: AssumeRoleResult,
661}
662
663#[derive(Debug, Deserialize)]
664#[serde(rename_all = "PascalCase")]
665struct AssumeRoleResponse {
666 assume_role_with_web_identity_result: AssumeRoleResult,
667}
668
669#[derive(Debug, Deserialize)]
670#[serde(rename_all = "PascalCase")]
671struct AssumeRoleResult {
672 credentials: SessionCredentials,
673}
674
675#[derive(Debug, Deserialize)]
676#[serde(rename_all = "PascalCase")]
677struct SessionCredentials {
678 session_token: String,
679 secret_access_key: String,
680 access_key_id: String,
681 expiration: DateTime<Utc>,
682}
683
684impl From<SessionCredentials> for AwsCredential {
685 fn from(s: SessionCredentials) -> Self {
686 Self {
687 key_id: s.access_key_id,
688 secret_key: s.secret_access_key,
689 token: Some(s.session_token),
690 }
691 }
692}
693
694async fn web_identity(
696 client: &Client,
697 service: &Arc<dyn HttpService>,
698 retry_config: &RetryConfig,
699 token_path: &str,
700 role_arn: &str,
701 session_name: &str,
702 endpoint: &str,
703) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
704 let token = std::fs::read_to_string(token_path)
705 .map_err(|e| format!("Failed to read token file '{token_path}': {e}"))?;
706
707 let bytes = client
708 .request(Method::POST, endpoint)
709 .query(&[
710 ("Action", "AssumeRoleWithWebIdentity"),
711 ("DurationSeconds", "3600"),
712 ("RoleArn", role_arn),
713 ("RoleSessionName", session_name),
714 ("Version", "2011-06-15"),
715 ("WebIdentityToken", &token),
716 ])
717 .retryable(retry_config, service.clone())
718 .idempotent(true)
719 .sensitive(true)
720 .send()
721 .await?
722 .bytes()
723 .await?;
724
725 let resp: AssumeRoleResponse = quick_xml::de::from_reader(bytes.reader())
726 .map_err(|e| format!("Invalid AssumeRoleWithWebIdentity response: {e}"))?;
727
728 let creds = resp.assume_role_with_web_identity_result.credentials;
729 let now = Utc::now();
730 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
731
732 Ok(TemporaryToken {
733 token: Arc::new(creds.into()),
734 expiry: Some(Instant::now() + ttl),
735 })
736}
737
738#[derive(Debug)]
757pub(crate) struct TaskCredentialProvider {
758 pub url: String,
759 pub auth_token_file: Option<String>,
762 pub retry: RetryConfig,
763 pub client: Client,
764 pub service: Arc<dyn HttpService>,
765 pub cache: TokenCache<Arc<AwsCredential>>,
766}
767
768#[async_trait]
769impl CredentialProvider for TaskCredentialProvider {
770 type Credential = AwsCredential;
771
772 async fn get_credential(&self) -> Result<Arc<AwsCredential>> {
773 self.cache
774 .get_or_insert_with(|| {
775 task_credential(
776 &self.client,
777 &self.service,
778 &self.retry,
779 &self.url,
780 self.auth_token_file.as_deref(),
781 )
782 })
783 .await
784 .map_err(|source| crate::Error::Generic { source })
785 }
786}
787
788async fn task_credential(
790 client: &Client,
791 service: &Arc<dyn HttpService>,
792 retry: &RetryConfig,
793 url: &str,
794 auth_token_file: Option<&str>,
795) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
796 let mut req = client.get(url);
797
798 if let Some(token_file) = auth_token_file {
800 let token = std::fs::read_to_string(token_file)
801 .map_err(|e| format!("Failed to read auth token file '{token_file}': {e}"))?;
802 req = req.header(reqwest::header::AUTHORIZATION, token.trim());
803 }
804
805 let creds: InstanceCredentials = req.send_retry(retry, service.clone()).await?.json().await?;
806
807 let now = Utc::now();
808 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
809 Ok(TemporaryToken {
810 token: Arc::new(creds.into()),
811 expiry: Some(Instant::now() + ttl),
812 })
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818 use crate::service::ReqwestService;
819 use reqwest::{Client, Method};
820 use std::env;
821
822 #[test]
823 fn test_debug_does_not_leak_secrets() {
824 let credential = AwsCredential {
828 key_id: "fake-key-id-do-not-print".to_string(),
829 secret_key: "fake-secret-do-not-print".to_string(),
830 token: Some("fake-session-token-do-not-print".to_string()),
831 };
832 let rendered = format!("{credential:?}");
833 assert!(
834 !rendered.contains("fake-key-id-do-not-print"),
835 "key_id leaked: {rendered}"
836 );
837 assert!(
838 !rendered.contains("fake-secret-do-not-print"),
839 "secret_key leaked: {rendered}"
840 );
841 assert!(
842 !rendered.contains("fake-session-token-do-not-print"),
843 "session token leaked: {rendered}"
844 );
845 assert!(rendered.contains("<redacted>"));
846 }
847
848 #[test]
849 fn test_sign_with_signed_payload() {
850 let client = Client::new();
851
852 let credential = AwsCredential {
853 key_id: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(), token: None,
859 };
860
861 let date = DateTime::parse_from_rfc3339("2022-08-06T18:01:34Z")
862 .unwrap()
863 .with_timezone(&Utc);
864
865 let mut request = client
866 .request(Method::GET, "https://ec2.amazon.com/")
867 .build()
868 .unwrap();
869
870 let signer = AwsAuthorizer {
871 date: Some(date),
872 credential: &credential,
873 service: "ec2",
874 region: "us-east-1",
875 };
876
877 signer.authorize(&mut request, None).unwrap();
878 assert_eq!(
879 request.headers().get(&AUTHORIZATION).unwrap(),
880 "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a3c787a7ed37f7fdfbfd2d7056a3d7c9d85e6d52a2bfbec73793c0be6e7862d4" )
882 }
884
885 #[test]
886 fn test_sign_port() {
887 let client = Client::new();
888
889 let credential = AwsCredential {
890 key_id: "H20ABqCkLZID4rLe".to_string(),
891 secret_key: "jMqRDgxSsBqqznfmddGdu1TmmZOJQxdM".to_string(),
892 token: None,
893 };
894
895 let date = DateTime::parse_from_rfc3339("2022-08-09T13:05:25Z")
896 .unwrap()
897 .with_timezone(&Utc);
898
899 let mut request = client
900 .request(Method::GET, "http://localhost:9000/tsm-schemas")
901 .query(&[
902 ("delimiter", "/"),
903 ("encoding-type", "url"),
904 ("list-type", "2"),
905 ("prefix", ""),
906 ])
907 .build()
908 .unwrap();
909
910 let authorizer = AwsAuthorizer {
911 date: Some(date),
912 credential: &credential,
913 service: "s3",
914 region: "us-east-1",
915 };
916
917 authorizer.authorize(&mut request, None).unwrap();
918 assert_eq!(
919 request.headers().get(&AUTHORIZATION).unwrap(),
920 "AWS4-HMAC-SHA256 Credential=H20ABqCkLZID4rLe/20220809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=9ebf2f92872066c99ac94e573b4e1b80f4dbb8a32b1e8e23178318746e7d1b4d"
921 )
922 }
923
924 #[tokio::test]
925 async fn test_instance_metadata() {
926 if env::var("TEST_INTEGRATION").is_err() {
927 eprintln!("skipping AWS integration test");
928 return;
929 }
930
931 let endpoint = env::var("EC2_METADATA_ENDPOINT").unwrap();
932 let client = Client::new();
933 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
934 let retry_config = RetryConfig::default();
935
936 let resp = client
937 .request(Method::GET, format!("{endpoint}/latest/meta-data/ami-id"))
938 .send()
939 .await
940 .unwrap();
941
942 assert_eq!(
943 resp.status(),
944 StatusCode::UNAUTHORIZED,
945 "Ensure metadata endpoint is set to only allow IMDSv2"
946 );
947
948 let creds = instance_creds(&client, &service, &retry_config, &endpoint, false)
949 .await
950 .unwrap();
951
952 let id = &creds.token.key_id;
953 let secret = &creds.token.secret_key;
954 let token = creds.token.token.as_ref().unwrap();
955
956 assert!(!id.is_empty());
957 assert!(!secret.is_empty());
958 assert!(!token.is_empty())
959 }
960 #[tokio::test]
961 async fn test_mock() {
962 let mut server = mockito::Server::new_async().await;
963
964 const IMDSV2_HEADER: &str = "X-aws-ec2-metadata-token";
965
966 let secret_access_key = "SECRET";
967 let access_key_id = "KEYID";
968 let token = "TOKEN";
969
970 let endpoint = server.url();
971 let client = Client::new();
972 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
973 let retry_config = RetryConfig::default();
974
975 let _mock1 = server
977 .mock("PUT", "/latest/api/token")
978 .with_status(200)
979 .with_body("cupcakes")
980 .create_async()
981 .await;
982
983 let _mock2 = server
984 .mock("GET", "/latest/meta-data/iam/security-credentials/")
985 .match_header(IMDSV2_HEADER, "cupcakes")
986 .with_status(200)
987 .with_body("myrole")
988 .create_async()
989 .await;
990
991 let _mock3 = server
992 .mock("GET", "/latest/meta-data/iam/security-credentials/myrole")
993 .match_header(IMDSV2_HEADER, "cupcakes")
994 .with_status(200)
995 .with_body(r#"{"AccessKeyId":"KEYID","Code":"Success","Expiration":"2022-08-30T10:51:04Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"SECRET","Token":"TOKEN","Type":"AWS-HMAC"}"#)
996 .create_async()
997 .await;
998
999 let creds = instance_creds(&client, &service, &retry_config, &endpoint, true)
1000 .await
1001 .unwrap();
1002
1003 assert_eq!(creds.token.token.as_deref().unwrap(), token);
1004 assert_eq!(&creds.token.key_id, access_key_id);
1005 assert_eq!(&creds.token.secret_key, secret_access_key);
1006
1007 let _mock4 = server
1009 .mock("PUT", "/latest/api/token")
1010 .with_status(403)
1011 .with_body("")
1012 .create_async()
1013 .await;
1014
1015 let _mock5 = server
1016 .mock("GET", "/latest/meta-data/iam/security-credentials/")
1017 .with_status(200)
1018 .with_body("myrole")
1019 .create_async()
1020 .await;
1021
1022 let _mock6 = server
1023 .mock("GET", "/latest/meta-data/iam/security-credentials/myrole")
1024 .with_status(200)
1025 .with_body(r#"{"AccessKeyId":"KEYID","Code":"Success","Expiration":"2022-08-30T10:51:04Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"SECRET","Token":"TOKEN","Type":"AWS-HMAC"}"#)
1026 .create_async()
1027 .await;
1028
1029 let creds = instance_creds(&client, &service, &retry_config, &endpoint, true)
1030 .await
1031 .unwrap();
1032
1033 assert_eq!(creds.token.token.as_deref().unwrap(), token);
1034 assert_eq!(&creds.token.key_id, access_key_id);
1035 assert_eq!(&creds.token.secret_key, secret_access_key);
1036
1037 let _mock7 = server
1039 .mock("PUT", "/latest/api/token")
1040 .with_status(403)
1041 .with_body("")
1042 .create_async()
1043 .await;
1044
1045 instance_creds(&client, &service, &retry_config, &endpoint, false)
1047 .await
1048 .unwrap_err();
1049 }
1050
1051 const STS_WEB_IDENTITY_XML: &str = r#"<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"><AssumeRoleWithWebIdentityResult><Credentials><AccessKeyId>AKIAIOSFODNN7EXAMPLE</AccessKeyId><SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY</SecretAccessKey><SessionToken>FwoGZXIvYXdzEJr//////////wEaDHer8</SessionToken><Expiration>2099-01-01T00:00:00Z</Expiration></Credentials></AssumeRoleWithWebIdentityResult></AssumeRoleWithWebIdentityResponse>"#; const STS_ASSUME_ROLE_XML: &str = r#"<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"><AssumeRoleResult><Credentials><AccessKeyId>AKIAIOSFODNN7EXAMPLE</AccessKeyId><SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY</SecretAccessKey><SessionToken>FwoGZXIvYXdzEJr//////////wEaDHer8</SessionToken><Expiration>2099-01-01T00:00:00Z</Expiration></Credentials></AssumeRoleResult></AssumeRoleResponse>"#; #[tokio::test]
1056 async fn test_web_identity_provider() {
1057 use std::io::Write as _;
1058 let mut server = mockito::Server::new_async().await;
1059
1060 let mut token_file = tempfile::NamedTempFile::new().unwrap();
1062 write!(token_file, "fake-jwt-token").unwrap();
1063
1064 let sts_url = server.url();
1065
1066 let _mock = server
1067 .mock("POST", "/")
1068 .match_query(mockito::Matcher::AllOf(vec![
1069 mockito::Matcher::UrlEncoded("Action".into(), "AssumeRoleWithWebIdentity".into()),
1070 mockito::Matcher::UrlEncoded(
1071 "RoleArn".into(),
1072 "arn:aws:iam::123456789012:role/TestRole".into(),
1073 ),
1074 mockito::Matcher::UrlEncoded("WebIdentityToken".into(), "fake-jwt-token".into()),
1075 ]))
1076 .with_status(200)
1077 .with_body(STS_WEB_IDENTITY_XML)
1078 .create_async()
1079 .await;
1080
1081 let client = Client::new();
1082 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1083 let retry_config = RetryConfig::default();
1084
1085 let provider = WebIdentityProvider {
1086 token_path: token_file.path().to_str().unwrap().to_owned(),
1087 role_arn: "arn:aws:iam::123456789012:role/TestRole".into(),
1088 session_name: "test-session".into(),
1089 endpoint: sts_url,
1090 };
1091
1092 let creds = provider
1093 .fetch_token(&client, &service, &retry_config)
1094 .await
1095 .unwrap();
1096
1097 assert_eq!(creds.token.key_id, "AKIAIOSFODNN7EXAMPLE"); assert!(creds.token.token.is_some());
1099 assert!(creds.expiry.is_some());
1100
1101 _mock.assert_async().await;
1102 }
1103
1104 #[tokio::test]
1105 async fn test_task_credential_provider() {
1106 let mut server = mockito::Server::new_async().await;
1107
1108 let endpoint = server.url();
1109
1110 let _mock = server
1111 .mock("GET", "/v2/credentials/test")
1112 .with_status(200)
1113 .with_body(r#"{"AccessKeyId":"TASKID","Code":"Success","Expiration":"2099-01-01T00:00:00Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"TASKSECRET","Token":"TASKTOKEN","Type":"AWS-HMAC"}"#)
1114 .create_async()
1115 .await;
1116
1117 let client = Client::new();
1118 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1119 let retry = RetryConfig::default();
1120
1121 let provider = TaskCredentialProvider {
1122 url: format!("{}/v2/credentials/test", endpoint),
1123 auth_token_file: None,
1124 retry: retry.clone(),
1125 client: client.clone(),
1126 service: service.clone(),
1127 cache: Default::default(),
1128 };
1129
1130 let creds = provider.get_credential().await.unwrap();
1131
1132 assert_eq!(creds.key_id, "TASKID");
1133 assert_eq!(creds.secret_key, "TASKSECRET");
1134 assert_eq!(creds.token.as_deref(), Some("TASKTOKEN"));
1135
1136 _mock.assert_async().await;
1137 }
1138
1139 #[tokio::test]
1140 async fn test_task_credential_with_auth_token() {
1141 use std::io::Write as _;
1142 let mut server = mockito::Server::new_async().await;
1143
1144 let mut auth_file = tempfile::NamedTempFile::new().unwrap();
1146 write!(auth_file, "my-pod-identity-token").unwrap();
1147
1148 let endpoint = server.url();
1149
1150 let _mock = server
1151 .mock("GET", "/v2/credentials/eks")
1152 .match_header("Authorization", "my-pod-identity-token")
1153 .with_status(200)
1154 .with_body(r#"{"AccessKeyId":"EKSID","Code":"Success","Expiration":"2099-01-01T00:00:00Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"EKSSECRET","Token":"EKSTOKEN","Type":"AWS-HMAC"}"#)
1155 .create_async()
1156 .await;
1157
1158 let client = Client::new();
1159 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1160 let retry = RetryConfig::default();
1161
1162 let provider = TaskCredentialProvider {
1163 url: format!("{}/v2/credentials/eks", endpoint),
1164 auth_token_file: Some(auth_file.path().to_str().unwrap().to_owned()),
1165 retry: retry.clone(),
1166 client: client.clone(),
1167 service: service.clone(),
1168 cache: Default::default(),
1169 };
1170
1171 let creds = provider.get_credential().await.unwrap();
1172
1173 assert_eq!(creds.key_id, "EKSID");
1174 assert_eq!(creds.token.as_deref(), Some("EKSTOKEN"));
1175
1176 _mock.assert_async().await;
1177 }
1178
1179 #[tokio::test]
1180 async fn test_assume_role_provider() {
1181 use crate::StaticCredentialProvider;
1182 let mut server = mockito::Server::new_async().await;
1183
1184 let sts_endpoint = server.url();
1185
1186 let _mock = server
1187 .mock("POST", "/")
1188 .match_query(mockito::Matcher::AllOf(vec![
1189 mockito::Matcher::UrlEncoded("Action".into(), "AssumeRole".into()),
1190 mockito::Matcher::UrlEncoded(
1191 "RoleArn".into(),
1192 "arn:aws:iam::123456789012:role/AssumedRole".into(),
1193 ),
1194 ]))
1195 .with_status(200)
1196 .with_body(STS_ASSUME_ROLE_XML)
1197 .create_async()
1198 .await;
1199
1200 let base_cred = AwsCredential {
1201 key_id: "BASEKEYID".to_string(),
1202 secret_key: "BASESECRET".to_string(),
1203 token: None,
1204 };
1205 let base_provider: Arc<dyn CredentialProvider<Credential = AwsCredential>> =
1206 Arc::new(StaticCredentialProvider::new(base_cred));
1207
1208 let client = Client::new();
1209 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1210 let retry_config = RetryConfig::default();
1211
1212 let provider = AssumeRoleProvider {
1213 role_arn: "arn:aws:iam::123456789012:role/AssumedRole".into(),
1214 session_name: "test-assume-session".into(),
1215 endpoint: format!("{}/", sts_endpoint),
1216 base_credentials: base_provider,
1217 region: "us-east-1".into(),
1218 policy: None,
1219 };
1220
1221 let token = provider
1222 .fetch_token(&client, &service, &retry_config)
1223 .await
1224 .unwrap();
1225
1226 assert_eq!(token.token.key_id, "AKIAIOSFODNN7EXAMPLE"); assert!(token.token.token.is_some());
1228 assert!(token.expiry.is_some());
1229
1230 _mock.assert_async().await;
1231 }
1232}