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