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(Debug, Eq, PartialEq)]
50pub struct AwsCredential {
51 pub key_id: String,
53 pub secret_key: String,
55 pub token: Option<String>,
57}
58
59impl AwsCredential {
60 fn sign(&self, to_sign: &str, date: DateTime<Utc>, region: &str, service: &str) -> String {
64 let date_string = date.format("%Y%m%d").to_string();
65 let date_hmac = hmac_sha256(format!("AWS4{}", self.secret_key), date_string);
66 let region_hmac = hmac_sha256(date_hmac, region);
67 let service_hmac = hmac_sha256(region_hmac, service);
68 let signing_hmac = hmac_sha256(service_hmac, b"aws4_request");
69 hex_encode(hmac_sha256(signing_hmac, to_sign).as_ref())
70 }
71}
72
73#[derive(Debug)]
77pub struct AwsAuthorizer<'a> {
78 date: Option<DateTime<Utc>>,
79 credential: &'a AwsCredential,
80 service: &'a str,
81 region: &'a str,
82}
83
84static DATE_HEADER: hyper::header::HeaderName =
85 hyper::header::HeaderName::from_static("x-amz-date");
86static HASH_HEADER: hyper::header::HeaderName =
87 hyper::header::HeaderName::from_static("x-amz-content-sha256");
88static TOKEN_HEADER: hyper::header::HeaderName =
89 hyper::header::HeaderName::from_static("x-amz-security-token");
90const ALGORITHM: &str = "AWS4-HMAC-SHA256";
91
92impl<'a> AwsAuthorizer<'a> {
93 pub fn new(credential: &'a AwsCredential, service: &'a str, region: &'a str) -> Self {
95 Self {
96 credential,
97 service,
98 region,
99 date: None,
100 }
101 }
102
103 pub fn authorize(
108 &self,
109 request: &mut Request,
110 pre_calculated_digest: Option<&[u8]>,
111 ) -> crate::Result<()> {
112 if let Some(ref token) = self.credential.token {
113 let token_val = HeaderValue::from_str(token)?;
114 request.headers_mut().insert(&TOKEN_HEADER, token_val);
115 }
116
117 let host = &request.url()[url::Position::BeforeHost..url::Position::AfterPort];
118 let host_val = HeaderValue::from_str(host)?;
119 request.headers_mut().insert("host", host_val);
120
121 let date = self.date.unwrap_or_else(Utc::now);
122 let date_str = date.format("%Y%m%dT%H%M%SZ").to_string();
123 let date_val = HeaderValue::from_str(&date_str)?;
124 request.headers_mut().insert(&DATE_HEADER, date_val);
125
126 let digest = match pre_calculated_digest {
127 Some(digest) => hex_encode(digest),
128 None => match request.body() {
129 None => EMPTY_SHA256_HASH.to_string(),
130 Some(body) => match body.as_bytes() {
131 Some(bytes) => hex_digest(bytes),
132 None => EMPTY_SHA256_HASH.to_string(),
133 },
134 },
135 };
136
137 let header_digest = HeaderValue::from_str(&digest)?;
138 request.headers_mut().insert(&HASH_HEADER, header_digest);
139
140 let (signed_headers, canonical_headers) = canonicalize_headers(request.headers());
141
142 let scope = self.scope(date);
143
144 let string_to_sign = self.string_to_sign(
145 date,
146 &scope,
147 request.method(),
148 request.url(),
149 &canonical_headers,
150 &signed_headers,
151 &digest,
152 );
153
154 let signature = self
155 .credential
156 .sign(&string_to_sign, date, self.region, self.service);
157
158 let authorisation = format!(
159 "{} Credential={}/{}, SignedHeaders={}, Signature={}",
160 ALGORITHM, self.credential.key_id, scope, signed_headers, signature
161 );
162
163 let authorization_val = HeaderValue::from_str(&authorisation)?;
164 request
165 .headers_mut()
166 .insert(&AUTHORIZATION, authorization_val);
167 Ok(())
168 }
169
170 #[allow(clippy::too_many_arguments)]
171 fn string_to_sign(
172 &self,
173 date: DateTime<Utc>,
174 scope: &str,
175 request_method: &Method,
176 url: &Url,
177 canonical_headers: &str,
178 signed_headers: &str,
179 digest: &str,
180 ) -> String {
181 let canonical_uri = match self.service {
185 "s3" => url.path().to_string(),
186 _ => utf8_percent_encode(url.path(), &STRICT_PATH_ENCODE_SET).to_string(),
187 };
188
189 let canonical_query = canonicalize_query(url);
190
191 let canonical_request = format!(
192 "{}\n{}\n{}\n{}\n{}\n{}",
193 request_method.as_str(),
194 canonical_uri,
195 canonical_query,
196 canonical_headers,
197 signed_headers,
198 digest
199 );
200
201 let hashed_canonical_request = hex_digest(canonical_request.as_bytes());
202
203 format!(
204 "{}\n{}\n{}\n{}",
205 ALGORITHM,
206 date.format("%Y%m%dT%H%M%SZ"),
207 scope,
208 hashed_canonical_request
209 )
210 }
211
212 fn scope(&self, date: DateTime<Utc>) -> String {
213 format!(
214 "{}/{}/{}/aws4_request",
215 date.format("%Y%m%d"),
216 self.region,
217 self.service
218 )
219 }
220}
221
222pub(crate) trait CredentialExt {
223 fn with_aws_sigv4(
225 self,
226 authorizer: Option<AwsAuthorizer<'_>>,
227 payload_sha256: Option<&[u8]>,
228 ) -> Self;
229}
230
231impl CredentialExt for RequestBuilder {
232 fn with_aws_sigv4(
233 self,
234 authorizer: Option<AwsAuthorizer<'_>>,
235 payload_sha256: Option<&[u8]>,
236 ) -> Self {
237 match authorizer {
238 Some(authorizer) => {
239 let (client, request) = self.build_split();
240 let mut request = request.expect("request valid");
241 authorizer
242 .authorize(&mut request, payload_sha256)
243 .expect("credential values must be valid header characters");
244
245 Self::from_parts(client, request)
246 }
247 None => self,
248 }
249 }
250}
251
252fn canonicalize_query(url: &Url) -> String {
256 use std::fmt::Write;
257
258 let capacity = match url.query() {
259 Some(q) if !q.is_empty() => q.len(),
260 _ => return String::new(),
261 };
262 let mut encoded = String::with_capacity(capacity + 1);
263
264 let mut headers = url.query_pairs().collect::<Vec<_>>();
265 headers.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
266
267 let mut first = true;
268 for (k, v) in headers {
269 if !first {
270 encoded.push('&');
271 }
272 first = false;
273 let _ = write!(
274 encoded,
275 "{}={}",
276 utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET),
277 utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET)
278 );
279 }
280 encoded
281}
282
283fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
287 let mut headers = BTreeMap::<&str, Vec<String>>::new();
290 let mut value_count = 0;
291 let mut value_bytes = 0;
292 let mut key_bytes = 0;
293
294 for (key, value) in header_map {
295 let key = key.as_str();
296 if ["authorization", "content-length", "user-agent"].contains(&key) {
297 continue;
298 }
299
300 let value = String::from_utf8_lossy(value.as_bytes()).into_owned();
301 key_bytes += key.len();
302 value_bytes += value.len();
303 value_count += 1;
304 headers.entry(key).or_default().push(value);
305 }
306
307 let mut signed_headers = String::with_capacity(key_bytes + headers.len());
308 let mut canonical_headers =
309 String::with_capacity(key_bytes + value_bytes + headers.len() + value_count);
310
311 for (header_idx, (name, values)) in headers.into_iter().enumerate() {
312 if header_idx != 0 {
313 signed_headers.push(';');
314 }
315
316 signed_headers.push_str(name);
317 canonical_headers.push_str(name);
318 canonical_headers.push(':');
319 for (value_idx, value) in values.into_iter().enumerate() {
320 if value_idx != 0 {
321 canonical_headers.push(',');
322 }
323 canonical_headers.push_str(value.trim());
324 }
325 canonical_headers.push('\n');
326 }
327
328 (signed_headers, canonical_headers)
329}
330
331#[derive(Debug)]
341pub(crate) struct InstanceCredentialProvider {
342 pub imdsv1_fallback: bool,
343 pub metadata_endpoint: String,
344}
345
346#[async_trait]
347impl TokenProvider for InstanceCredentialProvider {
348 type Credential = AwsCredential;
349
350 async fn fetch_token(
351 &self,
352 client: &Client,
353 service: &Arc<dyn HttpService>,
354 retry: &RetryConfig,
355 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
356 instance_creds(
357 client,
358 service,
359 retry,
360 &self.metadata_endpoint,
361 self.imdsv1_fallback,
362 )
363 .await
364 .map_err(|source| crate::Error::Generic { source })
365 }
366}
367
368#[derive(Debug)]
380pub(crate) struct WebIdentityProvider {
381 pub token_path: String,
382 pub role_arn: String,
383 pub session_name: String,
384 pub endpoint: String,
385}
386
387#[async_trait]
388impl TokenProvider for WebIdentityProvider {
389 type Credential = AwsCredential;
390
391 async fn fetch_token(
392 &self,
393 client: &Client,
394 service: &Arc<dyn HttpService>,
395 retry: &RetryConfig,
396 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
397 web_identity(
398 client,
399 service,
400 retry,
401 &self.token_path,
402 &self.role_arn,
403 &self.session_name,
404 &self.endpoint,
405 )
406 .await
407 .map_err(|source| crate::Error::Generic { source })
408 }
409}
410
411#[derive(Debug, Deserialize)]
412#[serde(rename_all = "PascalCase")]
413struct InstanceCredentials {
414 access_key_id: String,
415 secret_access_key: String,
416 token: String,
417 expiration: DateTime<Utc>,
418}
419
420impl From<InstanceCredentials> for AwsCredential {
421 fn from(s: InstanceCredentials) -> Self {
422 Self {
423 key_id: s.access_key_id,
424 secret_key: s.secret_access_key,
425 token: Some(s.token),
426 }
427 }
428}
429
430async fn instance_creds(
432 client: &Client,
433 service: &Arc<dyn HttpService>,
434 retry_config: &RetryConfig,
435 endpoint: &str,
436 imdsv1_fallback: bool,
437) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
438 const CREDENTIALS_PATH: &str = "latest/meta-data/iam/security-credentials";
439 const AWS_EC2_METADATA_TOKEN_HEADER: &str = "X-aws-ec2-metadata-token";
440
441 let token_url = format!("{endpoint}/latest/api/token");
442
443 let token_result = client
444 .request(Method::PUT, token_url)
445 .header("X-aws-ec2-metadata-token-ttl-seconds", "600") .retryable(retry_config, service.clone())
447 .idempotent(true)
448 .send()
449 .await;
450
451 let token = match token_result {
452 Ok(t) => Some(t.text().await?),
453 Err(e) if imdsv1_fallback && matches!(e.status(), Some(StatusCode::FORBIDDEN)) => {
454 warn!("received 403 from metadata endpoint, falling back to IMDSv1");
455 None
456 }
457 Err(e) => return Err(e.into()),
458 };
459
460 let role_url = format!("{endpoint}/{CREDENTIALS_PATH}/");
461 let mut role_request = client.request(Method::GET, role_url);
462
463 if let Some(token) = &token {
464 role_request = role_request.header(AWS_EC2_METADATA_TOKEN_HEADER, token);
465 }
466
467 let role = role_request
468 .send_retry(retry_config, service.clone())
469 .await?
470 .text()
471 .await?;
472
473 let creds_url = format!("{endpoint}/{CREDENTIALS_PATH}/{role}");
474 let mut creds_request = client.request(Method::GET, creds_url);
475 if let Some(token) = &token {
476 creds_request = creds_request.header(AWS_EC2_METADATA_TOKEN_HEADER, token);
477 }
478
479 let creds: InstanceCredentials = creds_request
480 .send_retry(retry_config, service.clone())
481 .await?
482 .json()
483 .await?;
484
485 let now = Utc::now();
486 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
487 Ok(TemporaryToken {
488 token: Arc::new(creds.into()),
489 expiry: Some(Instant::now() + ttl),
490 })
491}
492
493#[derive(Debug)]
505pub(crate) struct AssumeRoleProvider {
506 pub role_arn: String,
507 pub session_name: String,
508 pub endpoint: String,
509 pub base_credentials: Arc<dyn CredentialProvider<Credential = AwsCredential>>,
510 pub region: String,
511 pub policy: Option<String>,
514}
515
516#[async_trait]
517impl TokenProvider for AssumeRoleProvider {
518 type Credential = AwsCredential;
519
520 async fn fetch_token(
521 &self,
522 client: &Client,
523 service: &Arc<dyn HttpService>,
524 retry: &RetryConfig,
525 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
526 let base_cred = self
527 .base_credentials
528 .get_credential()
529 .await
530 .map_err(|source| crate::Error::Generic {
531 source: Box::new(source),
532 })?;
533
534 assume_role(
535 client,
536 service,
537 retry,
538 &base_cred,
539 &self.region,
540 &self.role_arn,
541 &self.session_name,
542 &self.endpoint,
543 self.policy.as_deref(),
544 )
545 .await
546 .map_err(|source| crate::Error::Generic { source })
547 }
548}
549
550#[allow(clippy::too_many_arguments)]
557async fn assume_role(
558 client: &Client,
559 service: &Arc<dyn HttpService>,
560 retry_config: &RetryConfig,
561 base_cred: &AwsCredential,
562 region: &str,
563 role_arn: &str,
564 session_name: &str,
565 endpoint: &str,
566 policy: Option<&str>,
567) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
568 let mut query: Vec<(&str, &str)> = vec![
569 ("Action", "AssumeRole"),
570 ("DurationSeconds", "3600"),
571 ("RoleArn", role_arn),
572 ("RoleSessionName", session_name),
573 ("Version", "2011-06-15"),
574 ];
575 if let Some(p) = policy {
576 query.push(("Policy", p));
577 }
578 let bytes = client
579 .request(Method::POST, endpoint)
580 .query(&query)
581 .with_aws_sigv4(Some(AwsAuthorizer::new(base_cred, "sts", region)), None)
582 .retryable(retry_config, service.clone())
583 .idempotent(true)
584 .send()
585 .await?
586 .bytes()
587 .await?;
588
589 let resp: AssumeRoleXmlResponse = quick_xml::de::from_reader(bytes.reader())
590 .map_err(|e| format!("Invalid AssumeRole response: {e}"))?;
591
592 let creds = resp.assume_role_result.credentials;
593 let now = Utc::now();
594 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
595
596 Ok(TemporaryToken {
597 token: Arc::new(creds.into()),
598 expiry: Some(Instant::now() + ttl),
599 })
600}
601
602#[derive(Debug, Deserialize)]
604#[serde(rename_all = "PascalCase")]
605struct AssumeRoleXmlResponse {
606 assume_role_result: AssumeRoleResult,
607}
608
609#[derive(Debug, Deserialize)]
610#[serde(rename_all = "PascalCase")]
611struct AssumeRoleResponse {
612 assume_role_with_web_identity_result: AssumeRoleResult,
613}
614
615#[derive(Debug, Deserialize)]
616#[serde(rename_all = "PascalCase")]
617struct AssumeRoleResult {
618 credentials: SessionCredentials,
619}
620
621#[derive(Debug, Deserialize)]
622#[serde(rename_all = "PascalCase")]
623struct SessionCredentials {
624 session_token: String,
625 secret_access_key: String,
626 access_key_id: String,
627 expiration: DateTime<Utc>,
628}
629
630impl From<SessionCredentials> for AwsCredential {
631 fn from(s: SessionCredentials) -> Self {
632 Self {
633 key_id: s.access_key_id,
634 secret_key: s.secret_access_key,
635 token: Some(s.session_token),
636 }
637 }
638}
639
640async fn web_identity(
642 client: &Client,
643 service: &Arc<dyn HttpService>,
644 retry_config: &RetryConfig,
645 token_path: &str,
646 role_arn: &str,
647 session_name: &str,
648 endpoint: &str,
649) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
650 let token = std::fs::read_to_string(token_path)
651 .map_err(|e| format!("Failed to read token file '{token_path}': {e}"))?;
652
653 let bytes = client
654 .request(Method::POST, endpoint)
655 .query(&[
656 ("Action", "AssumeRoleWithWebIdentity"),
657 ("DurationSeconds", "3600"),
658 ("RoleArn", role_arn),
659 ("RoleSessionName", session_name),
660 ("Version", "2011-06-15"),
661 ("WebIdentityToken", &token),
662 ])
663 .retryable(retry_config, service.clone())
664 .idempotent(true)
665 .sensitive(true)
666 .send()
667 .await?
668 .bytes()
669 .await?;
670
671 let resp: AssumeRoleResponse = quick_xml::de::from_reader(bytes.reader())
672 .map_err(|e| format!("Invalid AssumeRoleWithWebIdentity response: {e}"))?;
673
674 let creds = resp.assume_role_with_web_identity_result.credentials;
675 let now = Utc::now();
676 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
677
678 Ok(TemporaryToken {
679 token: Arc::new(creds.into()),
680 expiry: Some(Instant::now() + ttl),
681 })
682}
683
684#[derive(Debug)]
703pub(crate) struct TaskCredentialProvider {
704 pub url: String,
705 pub auth_token_file: Option<String>,
708 pub retry: RetryConfig,
709 pub client: Client,
710 pub service: Arc<dyn HttpService>,
711 pub cache: TokenCache<Arc<AwsCredential>>,
712}
713
714#[async_trait]
715impl CredentialProvider for TaskCredentialProvider {
716 type Credential = AwsCredential;
717
718 async fn get_credential(&self) -> Result<Arc<AwsCredential>> {
719 self.cache
720 .get_or_insert_with(|| {
721 task_credential(
722 &self.client,
723 &self.service,
724 &self.retry,
725 &self.url,
726 self.auth_token_file.as_deref(),
727 )
728 })
729 .await
730 .map_err(|source| crate::Error::Generic { source })
731 }
732}
733
734async fn task_credential(
736 client: &Client,
737 service: &Arc<dyn HttpService>,
738 retry: &RetryConfig,
739 url: &str,
740 auth_token_file: Option<&str>,
741) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
742 let mut req = client.get(url);
743
744 if let Some(token_file) = auth_token_file {
746 let token = std::fs::read_to_string(token_file)
747 .map_err(|e| format!("Failed to read auth token file '{token_file}': {e}"))?;
748 req = req.header(reqwest::header::AUTHORIZATION, token.trim());
749 }
750
751 let creds: InstanceCredentials = req.send_retry(retry, service.clone()).await?.json().await?;
752
753 let now = Utc::now();
754 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
755 Ok(TemporaryToken {
756 token: Arc::new(creds.into()),
757 expiry: Some(Instant::now() + ttl),
758 })
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764 use crate::service::ReqwestService;
765 use reqwest::{Client, Method};
766 use std::env;
767
768 #[test]
769 fn test_sign_with_signed_payload() {
770 let client = Client::new();
771
772 let credential = AwsCredential {
773 key_id: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
775 token: None,
776 };
777
778 let date = DateTime::parse_from_rfc3339("2022-08-06T18:01:34Z")
779 .unwrap()
780 .with_timezone(&Utc);
781
782 let mut request = client
783 .request(Method::GET, "https://ec2.amazon.com/")
784 .build()
785 .unwrap();
786
787 let signer = AwsAuthorizer {
788 date: Some(date),
789 credential: &credential,
790 service: "ec2",
791 region: "us-east-1",
792 };
793
794 signer.authorize(&mut request, None).unwrap();
795 assert_eq!(
796 request.headers().get(&AUTHORIZATION).unwrap(),
797 "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a3c787a7ed37f7fdfbfd2d7056a3d7c9d85e6d52a2bfbec73793c0be6e7862d4" )
799 }
801
802 #[test]
803 fn test_sign_port() {
804 let client = Client::new();
805
806 let credential = AwsCredential {
807 key_id: "H20ABqCkLZID4rLe".to_string(),
808 secret_key: "jMqRDgxSsBqqznfmddGdu1TmmZOJQxdM".to_string(),
809 token: None,
810 };
811
812 let date = DateTime::parse_from_rfc3339("2022-08-09T13:05:25Z")
813 .unwrap()
814 .with_timezone(&Utc);
815
816 let mut request = client
817 .request(Method::GET, "http://localhost:9000/tsm-schemas")
818 .query(&[
819 ("delimiter", "/"),
820 ("encoding-type", "url"),
821 ("list-type", "2"),
822 ("prefix", ""),
823 ])
824 .build()
825 .unwrap();
826
827 let authorizer = AwsAuthorizer {
828 date: Some(date),
829 credential: &credential,
830 service: "s3",
831 region: "us-east-1",
832 };
833
834 authorizer.authorize(&mut request, None).unwrap();
835 assert_eq!(
836 request.headers().get(&AUTHORIZATION).unwrap(),
837 "AWS4-HMAC-SHA256 Credential=H20ABqCkLZID4rLe/20220809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=9ebf2f92872066c99ac94e573b4e1b80f4dbb8a32b1e8e23178318746e7d1b4d"
838 )
839 }
840
841 #[tokio::test]
842 async fn test_instance_metadata() {
843 if env::var("TEST_INTEGRATION").is_err() {
844 eprintln!("skipping AWS integration test");
845 return;
846 }
847
848 let endpoint = env::var("EC2_METADATA_ENDPOINT").unwrap();
849 let client = Client::new();
850 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
851 let retry_config = RetryConfig::default();
852
853 let resp = client
854 .request(Method::GET, format!("{endpoint}/latest/meta-data/ami-id"))
855 .send()
856 .await
857 .unwrap();
858
859 assert_eq!(
860 resp.status(),
861 StatusCode::UNAUTHORIZED,
862 "Ensure metadata endpoint is set to only allow IMDSv2"
863 );
864
865 let creds = instance_creds(&client, &service, &retry_config, &endpoint, false)
866 .await
867 .unwrap();
868
869 let id = &creds.token.key_id;
870 let secret = &creds.token.secret_key;
871 let token = creds.token.token.as_ref().unwrap();
872
873 assert!(!id.is_empty());
874 assert!(!secret.is_empty());
875 assert!(!token.is_empty())
876 }
877 #[tokio::test]
878 async fn test_mock() {
879 let mut server = mockito::Server::new_async().await;
880
881 const IMDSV2_HEADER: &str = "X-aws-ec2-metadata-token";
882
883 let secret_access_key = "SECRET";
884 let access_key_id = "KEYID";
885 let token = "TOKEN";
886
887 let endpoint = server.url();
888 let client = Client::new();
889 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
890 let retry_config = RetryConfig::default();
891
892 let _mock1 = server
894 .mock("PUT", "/latest/api/token")
895 .with_status(200)
896 .with_body("cupcakes")
897 .create_async()
898 .await;
899
900 let _mock2 = server
901 .mock("GET", "/latest/meta-data/iam/security-credentials/")
902 .match_header(IMDSV2_HEADER, "cupcakes")
903 .with_status(200)
904 .with_body("myrole")
905 .create_async()
906 .await;
907
908 let _mock3 = server
909 .mock("GET", "/latest/meta-data/iam/security-credentials/myrole")
910 .match_header(IMDSV2_HEADER, "cupcakes")
911 .with_status(200)
912 .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"}"#)
913 .create_async()
914 .await;
915
916 let creds = instance_creds(&client, &service, &retry_config, &endpoint, true)
917 .await
918 .unwrap();
919
920 assert_eq!(creds.token.token.as_deref().unwrap(), token);
921 assert_eq!(&creds.token.key_id, access_key_id);
922 assert_eq!(&creds.token.secret_key, secret_access_key);
923
924 let _mock4 = server
926 .mock("PUT", "/latest/api/token")
927 .with_status(403)
928 .with_body("")
929 .create_async()
930 .await;
931
932 let _mock5 = server
933 .mock("GET", "/latest/meta-data/iam/security-credentials/")
934 .with_status(200)
935 .with_body("myrole")
936 .create_async()
937 .await;
938
939 let _mock6 = server
940 .mock("GET", "/latest/meta-data/iam/security-credentials/myrole")
941 .with_status(200)
942 .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"}"#)
943 .create_async()
944 .await;
945
946 let creds = instance_creds(&client, &service, &retry_config, &endpoint, true)
947 .await
948 .unwrap();
949
950 assert_eq!(creds.token.token.as_deref().unwrap(), token);
951 assert_eq!(&creds.token.key_id, access_key_id);
952 assert_eq!(&creds.token.secret_key, secret_access_key);
953
954 let _mock7 = server
956 .mock("PUT", "/latest/api/token")
957 .with_status(403)
958 .with_body("")
959 .create_async()
960 .await;
961
962 instance_creds(&client, &service, &retry_config, &endpoint, false)
964 .await
965 .unwrap_err();
966 }
967
968 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]
973 async fn test_web_identity_provider() {
974 use std::io::Write as _;
975 let mut server = mockito::Server::new_async().await;
976
977 let mut token_file = tempfile::NamedTempFile::new().unwrap();
979 write!(token_file, "fake-jwt-token").unwrap();
980
981 let sts_url = server.url();
982
983 let _mock = server
984 .mock("POST", "/")
985 .match_query(mockito::Matcher::AllOf(vec![
986 mockito::Matcher::UrlEncoded("Action".into(), "AssumeRoleWithWebIdentity".into()),
987 mockito::Matcher::UrlEncoded(
988 "RoleArn".into(),
989 "arn:aws:iam::123456789012:role/TestRole".into(),
990 ),
991 mockito::Matcher::UrlEncoded("WebIdentityToken".into(), "fake-jwt-token".into()),
992 ]))
993 .with_status(200)
994 .with_body(STS_WEB_IDENTITY_XML)
995 .create_async()
996 .await;
997
998 let client = Client::new();
999 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1000 let retry_config = RetryConfig::default();
1001
1002 let provider = WebIdentityProvider {
1003 token_path: token_file.path().to_str().unwrap().to_owned(),
1004 role_arn: "arn:aws:iam::123456789012:role/TestRole".into(),
1005 session_name: "test-session".into(),
1006 endpoint: sts_url,
1007 };
1008
1009 let creds = provider
1010 .fetch_token(&client, &service, &retry_config)
1011 .await
1012 .unwrap();
1013
1014 assert_eq!(creds.token.key_id, "AKIAIOSFODNN7EXAMPLE"); assert!(creds.token.token.is_some());
1016 assert!(creds.expiry.is_some());
1017
1018 _mock.assert_async().await;
1019 }
1020
1021 #[tokio::test]
1022 async fn test_task_credential_provider() {
1023 let mut server = mockito::Server::new_async().await;
1024
1025 let endpoint = server.url();
1026
1027 let _mock = server
1028 .mock("GET", "/v2/credentials/test")
1029 .with_status(200)
1030 .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"}"#)
1031 .create_async()
1032 .await;
1033
1034 let client = Client::new();
1035 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1036 let retry = RetryConfig::default();
1037
1038 let provider = TaskCredentialProvider {
1039 url: format!("{}/v2/credentials/test", endpoint),
1040 auth_token_file: None,
1041 retry: retry.clone(),
1042 client: client.clone(),
1043 service: service.clone(),
1044 cache: Default::default(),
1045 };
1046
1047 let creds = provider.get_credential().await.unwrap();
1048
1049 assert_eq!(creds.key_id, "TASKID");
1050 assert_eq!(creds.secret_key, "TASKSECRET");
1051 assert_eq!(creds.token.as_deref(), Some("TASKTOKEN"));
1052
1053 _mock.assert_async().await;
1054 }
1055
1056 #[tokio::test]
1057 async fn test_task_credential_with_auth_token() {
1058 use std::io::Write as _;
1059 let mut server = mockito::Server::new_async().await;
1060
1061 let mut auth_file = tempfile::NamedTempFile::new().unwrap();
1063 write!(auth_file, "my-pod-identity-token").unwrap();
1064
1065 let endpoint = server.url();
1066
1067 let _mock = server
1068 .mock("GET", "/v2/credentials/eks")
1069 .match_header("Authorization", "my-pod-identity-token")
1070 .with_status(200)
1071 .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"}"#)
1072 .create_async()
1073 .await;
1074
1075 let client = Client::new();
1076 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1077 let retry = RetryConfig::default();
1078
1079 let provider = TaskCredentialProvider {
1080 url: format!("{}/v2/credentials/eks", endpoint),
1081 auth_token_file: Some(auth_file.path().to_str().unwrap().to_owned()),
1082 retry: retry.clone(),
1083 client: client.clone(),
1084 service: service.clone(),
1085 cache: Default::default(),
1086 };
1087
1088 let creds = provider.get_credential().await.unwrap();
1089
1090 assert_eq!(creds.key_id, "EKSID");
1091 assert_eq!(creds.token.as_deref(), Some("EKSTOKEN"));
1092
1093 _mock.assert_async().await;
1094 }
1095
1096 #[tokio::test]
1097 async fn test_assume_role_provider() {
1098 use crate::StaticCredentialProvider;
1099 let mut server = mockito::Server::new_async().await;
1100
1101 let sts_endpoint = server.url();
1102
1103 let _mock = server
1104 .mock("POST", "/")
1105 .match_query(mockito::Matcher::AllOf(vec![
1106 mockito::Matcher::UrlEncoded("Action".into(), "AssumeRole".into()),
1107 mockito::Matcher::UrlEncoded(
1108 "RoleArn".into(),
1109 "arn:aws:iam::123456789012:role/AssumedRole".into(),
1110 ),
1111 ]))
1112 .with_status(200)
1113 .with_body(STS_ASSUME_ROLE_XML)
1114 .create_async()
1115 .await;
1116
1117 let base_cred = AwsCredential {
1118 key_id: "BASEKEYID".to_string(),
1119 secret_key: "BASESECRET".to_string(),
1120 token: None,
1121 };
1122 let base_provider: Arc<dyn CredentialProvider<Credential = AwsCredential>> =
1123 Arc::new(StaticCredentialProvider::new(base_cred));
1124
1125 let client = Client::new();
1126 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1127 let retry_config = RetryConfig::default();
1128
1129 let provider = AssumeRoleProvider {
1130 role_arn: "arn:aws:iam::123456789012:role/AssumedRole".into(),
1131 session_name: "test-assume-session".into(),
1132 endpoint: format!("{}/", sts_endpoint),
1133 base_credentials: base_provider,
1134 region: "us-east-1".into(),
1135 policy: None,
1136 };
1137
1138 let token = provider
1139 .fetch_token(&client, &service, &retry_config)
1140 .await
1141 .unwrap();
1142
1143 assert_eq!(token.token.key_id, "AKIAIOSFODNN7EXAMPLE"); assert!(token.token.token.is_some());
1145 assert!(token.expiry.is_some());
1146
1147 _mock.assert_async().await;
1148 }
1149}