1use std::time::Duration;
3
4use aws_config::{
5 default_provider::credentials::DefaultCredentialsChain, identity::IdentityCache, imds,
6 profile::ProfileFileCredentialsProvider, provider_config::ProviderConfig,
7 sts::AssumeRoleProviderBuilder,
8};
9use aws_credential_types::{Credentials, provider::SharedCredentialsProvider};
10use aws_runtime::env_config::file::{EnvConfigFileKind, EnvConfigFiles};
11use aws_smithy_async::time::SystemTimeSource;
12use aws_smithy_runtime_api::client::identity::SharedIdentityCache;
13use aws_types::{SdkConfig, region::Region};
14use serde_with::serde_as;
15use vector_lib::{
16 config::proxy::ProxyConfig, configurable::configurable_component,
17 sensitive_string::SensitiveString, tls::TlsConfig,
18};
19
20const DEFAULT_LOAD_TIMEOUT: Duration = Duration::from_secs(5);
23const DEFAULT_PROFILE_NAME: &str = "default";
24
25#[serde_as]
27#[configurable_component]
28#[derive(Copy, Clone, Debug, Derivative, Eq, PartialEq)]
29#[derivative(Default)]
30#[serde(deny_unknown_fields)]
31pub struct ImdsAuthentication {
32 #[serde(default = "default_max_attempts")]
34 #[derivative(Default(value = "default_max_attempts()"))]
35 max_attempts: u32,
36
37 #[serde(default = "default_timeout")]
39 #[serde(rename = "connect_timeout_seconds")]
40 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
41 #[derivative(Default(value = "default_timeout()"))]
42 connect_timeout: Duration,
43
44 #[serde(default = "default_timeout")]
46 #[serde(rename = "read_timeout_seconds")]
47 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
48 #[derivative(Default(value = "default_timeout()"))]
49 read_timeout: Duration,
50}
51
52const fn default_max_attempts() -> u32 {
53 4
54}
55
56const fn default_timeout() -> Duration {
57 Duration::from_secs(1)
58}
59
60#[configurable_component]
62#[derive(Clone, Debug, Derivative, Eq, PartialEq)]
63#[derivative(Default)]
64#[serde(deny_unknown_fields, untagged)]
65pub enum AwsAuthentication {
66 AccessKey {
68 #[configurable(metadata(docs::examples = "AKIAIOSFODNN7EXAMPLE"))]
70 access_key_id: SensitiveString,
71
72 #[configurable(metadata(docs::examples = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"))]
74 secret_access_key: SensitiveString,
75
76 #[configurable(metadata(docs::examples = "AQoDYXdz...AQoDYXdz..."))]
79 session_token: Option<SensitiveString>,
80
81 #[configurable(metadata(docs::examples = "arn:aws:iam::123456789098:role/my_role"))]
85 assume_role: Option<String>,
86
87 #[configurable(metadata(docs::examples = "randomEXAMPLEidString"))]
91 external_id: Option<String>,
92
93 #[configurable(metadata(docs::examples = "us-west-2"))]
100 region: Option<String>,
101
102 #[configurable(metadata(docs::examples = "vector-indexer-role"))]
109 session_name: Option<String>,
110 },
111
112 File {
118 #[configurable(metadata(docs::examples = "/my/aws/credentials"))]
120 credentials_file: String,
121
122 #[configurable(metadata(docs::examples = "develop"))]
126 #[serde(default = "default_profile")]
127 profile: String,
128
129 #[configurable(metadata(docs::examples = "us-west-2"))]
136 region: Option<String>,
137 },
138
139 Role {
141 #[configurable(metadata(docs::examples = "arn:aws:iam::123456789098:role/my_role"))]
145 assume_role: String,
146
147 #[configurable(metadata(docs::examples = "randomEXAMPLEidString"))]
151 external_id: Option<String>,
152
153 #[configurable(metadata(docs::type_unit = "seconds"))]
157 #[configurable(metadata(docs::examples = 30))]
158 #[configurable(metadata(docs::human_name = "Load Timeout"))]
159 load_timeout_secs: Option<u64>,
160
161 #[serde(default)]
163 imds: ImdsAuthentication,
164
165 #[configurable(metadata(docs::examples = "us-west-2"))]
172 region: Option<String>,
173
174 #[configurable(metadata(docs::examples = "vector-indexer-role"))]
181 session_name: Option<String>,
182 },
183
184 #[derivative(Default)]
186 Default {
187 #[configurable(metadata(docs::type_unit = "seconds"))]
191 #[configurable(metadata(docs::examples = 30))]
192 #[configurable(metadata(docs::human_name = "Load Timeout"))]
193 load_timeout_secs: Option<u64>,
194
195 #[serde(default)]
197 imds: ImdsAuthentication,
198
199 #[configurable(metadata(docs::examples = "us-west-2"))]
206 region: Option<String>,
207 },
208}
209
210fn default_profile() -> String {
211 DEFAULT_PROFILE_NAME.to_string()
212}
213
214impl AwsAuthentication {
215 pub(super) async fn credentials_cache(&self) -> crate::Result<SharedIdentityCache> {
217 match self {
218 AwsAuthentication::Role {
219 load_timeout_secs, ..
220 }
221 | AwsAuthentication::Default {
222 load_timeout_secs, ..
223 } => {
224 let credentials_cache = IdentityCache::lazy()
225 .load_timeout(
226 load_timeout_secs
227 .map(Duration::from_secs)
228 .unwrap_or(DEFAULT_LOAD_TIMEOUT),
229 )
230 .build();
231
232 Ok(credentials_cache)
233 }
234 _ => Ok(IdentityCache::lazy().build()),
235 }
236 }
237
238 fn assume_role_provider_builder(
241 proxy: &ProxyConfig,
242 tls_options: Option<&TlsConfig>,
243 region: &Region,
244 assume_role: &str,
245 external_id: Option<&str>,
246 session_name: Option<&str>,
247 ) -> crate::Result<AssumeRoleProviderBuilder> {
248 let connector = super::connector(proxy, tls_options)?;
249 let config = SdkConfig::builder()
250 .http_client(connector)
251 .region(region.clone())
252 .time_source(SystemTimeSource::new())
253 .build();
254
255 let mut builder = AssumeRoleProviderBuilder::new(assume_role)
256 .region(region.clone())
257 .configure(&config);
258
259 if let Some(external_id) = external_id {
260 builder = builder.external_id(external_id)
261 }
262
263 if let Some(session_name) = session_name {
264 builder = builder.session_name(session_name)
265 }
266
267 Ok(builder)
268 }
269
270 pub async fn credentials_provider(
272 &self,
273 service_region: Region,
274 proxy: &ProxyConfig,
275 tls_options: Option<&TlsConfig>,
276 ) -> crate::Result<SharedCredentialsProvider> {
277 match self {
278 Self::AccessKey {
279 access_key_id,
280 secret_access_key,
281 assume_role,
282 external_id,
283 region,
284 session_name,
285 session_token,
286 } => {
287 let provider = SharedCredentialsProvider::new(Credentials::from_keys(
288 access_key_id.inner(),
289 secret_access_key.inner(),
290 session_token.clone().map(|v| v.inner().into()),
291 ));
292 if let Some(assume_role) = assume_role {
293 let auth_region = region.clone().map(Region::new).unwrap_or(service_region);
294 let builder = Self::assume_role_provider_builder(
295 proxy,
296 tls_options,
297 &auth_region,
298 assume_role,
299 external_id.as_deref(),
300 session_name.as_deref(),
301 )?;
302
303 let provider = builder.build_from_provider(provider).await;
304
305 return Ok(SharedCredentialsProvider::new(provider));
306 }
307 Ok(provider)
308 }
309 AwsAuthentication::File {
310 credentials_file,
311 profile,
312 region,
313 } => {
314 let connector = super::connector(proxy, tls_options)?;
315
316 let profile_files = EnvConfigFiles::builder()
319 .with_file(EnvConfigFileKind::Credentials, credentials_file)
320 .build();
321
322 let auth_region = region.clone().map(Region::new).unwrap_or(service_region);
323 let provider_config = ProviderConfig::empty()
324 .with_region(Option::from(auth_region))
325 .with_http_client(connector);
326
327 let profile_provider = ProfileFileCredentialsProvider::builder()
328 .profile_files(profile_files)
329 .profile_name(profile)
330 .configure(&provider_config)
331 .build();
332 Ok(SharedCredentialsProvider::new(profile_provider))
333 }
334 AwsAuthentication::Role {
335 assume_role,
336 external_id,
337 imds,
338 region,
339 session_name,
340 ..
341 } => {
342 let auth_region = region.clone().map(Region::new).unwrap_or(service_region);
343 let builder = Self::assume_role_provider_builder(
344 proxy,
345 tls_options,
346 &auth_region,
347 assume_role,
348 external_id.as_deref(),
349 session_name.as_deref(),
350 )?;
351
352 let provider = builder
353 .build_from_provider(
354 default_credentials_provider(auth_region, proxy, tls_options, *imds)
355 .await?,
356 )
357 .await;
358
359 Ok(SharedCredentialsProvider::new(provider))
360 }
361 AwsAuthentication::Default { imds, region, .. } => Ok(SharedCredentialsProvider::new(
362 default_credentials_provider(
363 region.clone().map(Region::new).unwrap_or(service_region),
364 proxy,
365 tls_options,
366 *imds,
367 )
368 .await?,
369 )),
370 }
371 }
372
373 #[cfg(test)]
374 pub fn test_auth() -> AwsAuthentication {
376 AwsAuthentication::AccessKey {
377 access_key_id: "dummy".to_string().into(),
378 secret_access_key: "dummy".to_string().into(),
379 assume_role: None,
380 external_id: None,
381 region: None,
382 session_name: None,
383 session_token: None,
384 }
385 }
386}
387
388async fn default_credentials_provider(
389 region: Region,
390 proxy: &ProxyConfig,
391 tls_options: Option<&TlsConfig>,
392 imds: ImdsAuthentication,
393) -> crate::Result<SharedCredentialsProvider> {
394 let connector = super::connector(proxy, tls_options)?;
395
396 let provider_config = ProviderConfig::empty()
397 .with_region(Some(region.clone()))
398 .with_http_client(connector);
399
400 let client = imds::Client::builder()
401 .max_attempts(imds.max_attempts)
402 .connect_timeout(imds.connect_timeout)
403 .read_timeout(imds.read_timeout)
404 .configure(&provider_config)
405 .build();
406
407 let credentials_provider = DefaultCredentialsChain::builder()
408 .region(region)
409 .imds_client(client)
410 .configure(provider_config)
411 .build()
412 .await;
413
414 Ok(SharedCredentialsProvider::new(credentials_provider))
415}
416
417#[cfg(test)]
418mod tests {
419 use indoc::indoc;
420 use serde::{Deserialize, Serialize};
421
422 use super::*;
423
424 const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
425 const READ_TIMEOUT: Duration = Duration::from_secs(10);
426
427 #[derive(Serialize, Deserialize, Clone, Debug)]
428 struct ComponentConfig {
429 assume_role: Option<String>,
430 external_id: Option<String>,
431 #[serde(default)]
432 auth: AwsAuthentication,
433 }
434
435 #[test]
436 fn parsing_default() {
437 let config = serde_yaml::from_str::<ComponentConfig>("").unwrap();
438
439 assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
440 }
441
442 #[test]
443 fn parsing_default_with_load_timeout() {
444 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {"
445 auth:
446 load_timeout_secs: 10
447 "})
448 .unwrap();
449
450 assert!(matches!(
451 config.auth,
452 AwsAuthentication::Default {
453 load_timeout_secs: Some(10),
454 imds: ImdsAuthentication { .. },
455 region: None,
456 }
457 ));
458 }
459
460 #[test]
461 fn parsing_default_with_region() {
462 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
463 auth:
464 region: "us-east-2"
465 "#})
466 .unwrap();
467
468 match config.auth {
469 AwsAuthentication::Default { region, .. } => {
470 assert_eq!(region.unwrap(), "us-east-2");
471 }
472 _ => panic!(),
473 }
474 }
475
476 #[test]
477 fn parsing_default_with_imds_client() {
478 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {"
479 auth:
480 imds:
481 max_attempts: 5
482 connect_timeout_seconds: 30
483 read_timeout_seconds: 10
484 "})
485 .unwrap();
486
487 assert!(matches!(
488 config.auth,
489 AwsAuthentication::Default {
490 load_timeout_secs: None,
491 region: None,
492 imds: ImdsAuthentication {
493 max_attempts: 5,
494 connect_timeout: CONNECT_TIMEOUT,
495 read_timeout: READ_TIMEOUT,
496 },
497 }
498 ));
499 }
500
501 #[test]
502 fn parsing_old_assume_role() {
503 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
504 assume_role: "root"
505 "#})
506 .unwrap();
507
508 assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
509 }
510
511 #[test]
512 fn parsing_assume_role() {
513 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
514 auth:
515 assume_role: "root"
516 load_timeout_secs: 10
517 "#})
518 .unwrap();
519
520 assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
521 }
522
523 #[test]
524 fn parsing_external_id_with_assume_role() {
525 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
526 auth:
527 assume_role: "root"
528 external_id: "id"
529 load_timeout_secs: 10
530 "#})
531 .unwrap();
532
533 assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
534 }
535
536 #[test]
537 fn parsing_session_name_with_assume_role() {
538 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
539 auth:
540 assume_role: "root"
541 session_name: "session_name"
542 load_timeout_secs: 10
543 "#})
544 .unwrap();
545
546 match config.auth {
547 AwsAuthentication::Role { session_name, .. } => {
548 assert_eq!(session_name.unwrap(), "session_name");
549 }
550 _ => panic!(),
551 }
552 }
553
554 #[test]
555 fn parsing_assume_role_with_imds_client() {
556 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
557 auth:
558 assume_role: "root"
559 imds:
560 max_attempts: 5
561 connect_timeout_seconds: 30
562 read_timeout_seconds: 10
563 "#})
564 .unwrap();
565
566 match config.auth {
567 AwsAuthentication::Role {
568 assume_role,
569 external_id,
570 load_timeout_secs,
571 imds,
572 region,
573 session_name,
574 } => {
575 assert_eq!(&assume_role, "root");
576 assert_eq!(external_id, None);
577 assert_eq!(load_timeout_secs, None);
578 assert_eq!(session_name, None);
579 assert!(matches!(
580 imds,
581 ImdsAuthentication {
582 max_attempts: 5,
583 connect_timeout: CONNECT_TIMEOUT,
584 read_timeout: READ_TIMEOUT,
585 }
586 ));
587 assert_eq!(region, None);
588 }
589 _ => panic!(),
590 }
591 }
592
593 #[test]
594 fn parsing_both_assume_role() {
595 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
596 assume_role: "root"
597 auth:
598 assume_role: "auth.root"
599 load_timeout_secs: 10
600 region: "us-west-2"
601 "#})
602 .unwrap();
603
604 match config.auth {
605 AwsAuthentication::Role {
606 assume_role,
607 external_id,
608 load_timeout_secs,
609 imds,
610 region,
611 session_name,
612 } => {
613 assert_eq!(&assume_role, "auth.root");
614 assert_eq!(external_id, None);
615 assert_eq!(load_timeout_secs, Some(10));
616 assert_eq!(session_name, None);
617 assert!(matches!(imds, ImdsAuthentication { .. }));
618 assert_eq!(region.unwrap(), "us-west-2");
619 }
620 _ => panic!(),
621 }
622 }
623
624 #[test]
625 fn parsing_static() {
626 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
627 auth:
628 access_key_id: "key"
629 secret_access_key: "other"
630 "#})
631 .unwrap();
632
633 assert!(matches!(config.auth, AwsAuthentication::AccessKey { .. }));
634 }
635
636 #[test]
637 fn parsing_static_with_assume_role() {
638 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
639 auth:
640 access_key_id: "key"
641 secret_access_key: "other"
642 assume_role: "root"
643 "#})
644 .unwrap();
645
646 match config.auth {
647 AwsAuthentication::AccessKey {
648 access_key_id,
649 secret_access_key,
650 assume_role,
651 ..
652 } => {
653 assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
654 assert_eq!(
655 &secret_access_key,
656 &SensitiveString::from("other".to_string())
657 );
658 assert_eq!(&assume_role, &Some("root".to_string()));
659 }
660 _ => panic!(),
661 }
662 }
663
664 #[test]
665 fn parsing_static_with_assume_role_and_external_id() {
666 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
667 auth:
668 access_key_id: "key"
669 secret_access_key: "other"
670 assume_role: "root"
671 external_id: "id"
672 "#})
673 .unwrap();
674
675 match config.auth {
676 AwsAuthentication::AccessKey {
677 access_key_id,
678 secret_access_key,
679 assume_role,
680 external_id,
681 ..
682 } => {
683 assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
684 assert_eq!(
685 &secret_access_key,
686 &SensitiveString::from("other".to_string())
687 );
688 assert_eq!(&assume_role, &Some("root".to_string()));
689 assert_eq!(&external_id, &Some("id".to_string()));
690 }
691 _ => panic!(),
692 }
693 }
694
695 #[test]
696 fn parsing_file() {
697 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
698 auth:
699 credentials_file: "/path/to/file"
700 profile: "foo"
701 region: "us-west-2"
702 "#})
703 .unwrap();
704
705 match config.auth {
706 AwsAuthentication::File {
707 credentials_file,
708 profile,
709 region,
710 } => {
711 assert_eq!(&credentials_file, "/path/to/file");
712 assert_eq!(&profile, "foo");
713 assert_eq!(region.unwrap(), "us-west-2");
714 }
715 _ => panic!(),
716 }
717
718 let config = serde_yaml::from_str::<ComponentConfig>(indoc! {r#"
719 auth:
720 credentials_file: "/path/to/file"
721 "#})
722 .unwrap();
723
724 match config.auth {
725 AwsAuthentication::File {
726 credentials_file,
727 profile,
728 ..
729 } => {
730 assert_eq!(&credentials_file, "/path/to/file");
731 assert_eq!(profile, "default".to_string());
732 }
733 _ => panic!(),
734 }
735 }
736}