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 serde::{Deserialize, Serialize};
420
421 use super::*;
422
423 const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
424 const READ_TIMEOUT: Duration = Duration::from_secs(10);
425
426 #[derive(Serialize, Deserialize, Clone, Debug)]
427 struct ComponentConfig {
428 assume_role: Option<String>,
429 external_id: Option<String>,
430 #[serde(default)]
431 auth: AwsAuthentication,
432 }
433
434 #[test]
435 fn parsing_default() {
436 let config = toml::from_str::<ComponentConfig>("").unwrap();
437
438 assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
439 }
440
441 #[test]
442 fn parsing_default_with_load_timeout() {
443 let config = toml::from_str::<ComponentConfig>(
444 "
445 auth.load_timeout_secs = 10
446 ",
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 = toml::from_str::<ComponentConfig>(
463 r#"
464 auth.region = "us-east-2"
465 "#,
466 )
467 .unwrap();
468
469 match config.auth {
470 AwsAuthentication::Default { region, .. } => {
471 assert_eq!(region.unwrap(), "us-east-2");
472 }
473 _ => panic!(),
474 }
475 }
476
477 #[test]
478 fn parsing_default_with_imds_client() {
479 let config = toml::from_str::<ComponentConfig>(
480 "
481 auth.imds.max_attempts = 5
482 auth.imds.connect_timeout_seconds = 30
483 auth.imds.read_timeout_seconds = 10
484 ",
485 )
486 .unwrap();
487
488 assert!(matches!(
489 config.auth,
490 AwsAuthentication::Default {
491 load_timeout_secs: None,
492 region: None,
493 imds: ImdsAuthentication {
494 max_attempts: 5,
495 connect_timeout: CONNECT_TIMEOUT,
496 read_timeout: READ_TIMEOUT,
497 },
498 }
499 ));
500 }
501
502 #[test]
503 fn parsing_old_assume_role() {
504 let config = toml::from_str::<ComponentConfig>(
505 r#"
506 assume_role = "root"
507 "#,
508 )
509 .unwrap();
510
511 assert!(matches!(config.auth, AwsAuthentication::Default { .. }));
512 }
513
514 #[test]
515 fn parsing_assume_role() {
516 let config = toml::from_str::<ComponentConfig>(
517 r#"
518 auth.assume_role = "root"
519 auth.load_timeout_secs = 10
520 "#,
521 )
522 .unwrap();
523
524 assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
525 }
526
527 #[test]
528 fn parsing_external_id_with_assume_role() {
529 let config = toml::from_str::<ComponentConfig>(
530 r#"
531 auth.assume_role = "root"
532 auth.external_id = "id"
533 auth.load_timeout_secs = 10
534 "#,
535 )
536 .unwrap();
537
538 assert!(matches!(config.auth, AwsAuthentication::Role { .. }));
539 }
540
541 #[test]
542 fn parsing_session_name_with_assume_role() {
543 let config = toml::from_str::<ComponentConfig>(
544 r#"
545 auth.assume_role = "root"
546 auth.session_name = "session_name"
547 auth.load_timeout_secs = 10
548 "#,
549 )
550 .unwrap();
551
552 match config.auth {
553 AwsAuthentication::Role { session_name, .. } => {
554 assert_eq!(session_name.unwrap(), "session_name");
555 }
556 _ => panic!(),
557 }
558 }
559
560 #[test]
561 fn parsing_assume_role_with_imds_client() {
562 let config = toml::from_str::<ComponentConfig>(
563 r#"
564 auth.assume_role = "root"
565 auth.imds.max_attempts = 5
566 auth.imds.connect_timeout_seconds = 30
567 auth.imds.read_timeout_seconds = 10
568 "#,
569 )
570 .unwrap();
571
572 match config.auth {
573 AwsAuthentication::Role {
574 assume_role,
575 external_id,
576 load_timeout_secs,
577 imds,
578 region,
579 session_name,
580 } => {
581 assert_eq!(&assume_role, "root");
582 assert_eq!(external_id, None);
583 assert_eq!(load_timeout_secs, None);
584 assert_eq!(session_name, None);
585 assert!(matches!(
586 imds,
587 ImdsAuthentication {
588 max_attempts: 5,
589 connect_timeout: CONNECT_TIMEOUT,
590 read_timeout: READ_TIMEOUT,
591 }
592 ));
593 assert_eq!(region, None);
594 }
595 _ => panic!(),
596 }
597 }
598
599 #[test]
600 fn parsing_both_assume_role() {
601 let config = toml::from_str::<ComponentConfig>(
602 r#"
603 assume_role = "root"
604 auth.assume_role = "auth.root"
605 auth.load_timeout_secs = 10
606 auth.region = "us-west-2"
607 "#,
608 )
609 .unwrap();
610
611 match config.auth {
612 AwsAuthentication::Role {
613 assume_role,
614 external_id,
615 load_timeout_secs,
616 imds,
617 region,
618 session_name,
619 } => {
620 assert_eq!(&assume_role, "auth.root");
621 assert_eq!(external_id, None);
622 assert_eq!(load_timeout_secs, Some(10));
623 assert_eq!(session_name, None);
624 assert!(matches!(imds, ImdsAuthentication { .. }));
625 assert_eq!(region.unwrap(), "us-west-2");
626 }
627 _ => panic!(),
628 }
629 }
630
631 #[test]
632 fn parsing_static() {
633 let config = toml::from_str::<ComponentConfig>(
634 r#"
635 auth.access_key_id = "key"
636 auth.secret_access_key = "other"
637 "#,
638 )
639 .unwrap();
640
641 assert!(matches!(config.auth, AwsAuthentication::AccessKey { .. }));
642 }
643
644 #[test]
645 fn parsing_static_with_assume_role() {
646 let config = toml::from_str::<ComponentConfig>(
647 r#"
648 auth.access_key_id = "key"
649 auth.secret_access_key = "other"
650 auth.assume_role = "root"
651 "#,
652 )
653 .unwrap();
654
655 match config.auth {
656 AwsAuthentication::AccessKey {
657 access_key_id,
658 secret_access_key,
659 assume_role,
660 ..
661 } => {
662 assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
663 assert_eq!(
664 &secret_access_key,
665 &SensitiveString::from("other".to_string())
666 );
667 assert_eq!(&assume_role, &Some("root".to_string()));
668 }
669 _ => panic!(),
670 }
671 }
672
673 #[test]
674 fn parsing_static_with_assume_role_and_external_id() {
675 let config = toml::from_str::<ComponentConfig>(
676 r#"
677 auth.access_key_id = "key"
678 auth.secret_access_key = "other"
679 auth.assume_role = "root"
680 auth.external_id = "id"
681 "#,
682 )
683 .unwrap();
684
685 match config.auth {
686 AwsAuthentication::AccessKey {
687 access_key_id,
688 secret_access_key,
689 assume_role,
690 external_id,
691 ..
692 } => {
693 assert_eq!(&access_key_id, &SensitiveString::from("key".to_string()));
694 assert_eq!(
695 &secret_access_key,
696 &SensitiveString::from("other".to_string())
697 );
698 assert_eq!(&assume_role, &Some("root".to_string()));
699 assert_eq!(&external_id, &Some("id".to_string()));
700 }
701 _ => panic!(),
702 }
703 }
704
705 #[test]
706 fn parsing_file() {
707 let config = toml::from_str::<ComponentConfig>(
708 r#"
709 auth.credentials_file = "/path/to/file"
710 auth.profile = "foo"
711 auth.region = "us-west-2"
712 "#,
713 )
714 .unwrap();
715
716 match config.auth {
717 AwsAuthentication::File {
718 credentials_file,
719 profile,
720 region,
721 } => {
722 assert_eq!(&credentials_file, "/path/to/file");
723 assert_eq!(&profile, "foo");
724 assert_eq!(region.unwrap(), "us-west-2");
725 }
726 _ => panic!(),
727 }
728
729 let config = toml::from_str::<ComponentConfig>(
730 r#"
731 auth.credentials_file = "/path/to/file"
732 "#,
733 )
734 .unwrap();
735
736 match config.auth {
737 AwsAuthentication::File {
738 credentials_file,
739 profile,
740 ..
741 } => {
742 assert_eq!(&credentials_file, "/path/to/file");
743 assert_eq!(profile, "default".to_string());
744 }
745 _ => panic!(),
746 }
747 }
748}