1use std::str::FromStr;
19use std::sync::Arc;
20
21use serde::{Deserialize, Serialize};
22use tokio::runtime::Handle;
23use tracing::info;
24
25use super::AmazonConfig;
26use crate::aws::credential::{
27 AssumeRoleProvider, InstanceCredentialProvider, TaskCredentialProvider, WebIdentityProvider,
28};
29use crate::aws::{AwsCredential, AwsCredentialProvider};
30use crate::config::ConfigValue;
31use crate::service::make_service;
32use crate::{
33 ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider,
34 TokenCredentialProvider,
35};
36
37static DEFAULT_METADATA_ENDPOINT: &str = "http://169.254.169.254";
38
39#[derive(Debug, thiserror::Error)]
40enum Error {
41 #[error("Missing AccessKeyId")]
42 MissingAccessKeyId,
43
44 #[error("Missing SecretAccessKey")]
45 MissingSecretAccessKey,
46
47 #[error("Configuration key: '{}' is not known.", key)]
48 UnknownConfigurationKey { key: String },
49}
50
51impl From<Error> for crate::Error {
52 fn from(source: Error) -> Self {
53 match source {
54 Error::UnknownConfigurationKey { key } => Self::UnknownConfigurationKey { key },
55 _ => Self::Generic {
56 source: Box::new(source),
57 },
58 }
59 }
60}
61
62#[derive(Debug, Default, Clone)]
77pub struct AmazonBuilder {
78 access_key_id: Option<String>,
79 secret_access_key: Option<String>,
80 region: Option<String>,
81 token: Option<String>,
82 retry_config: RetryConfig,
83 imdsv1_fallback: ConfigValue<bool>,
84 metadata_endpoint: Option<String>,
85 container_credentials_relative_uri: Option<String>,
86 client_options: ClientOptions,
87 credentials: Option<AwsCredentialProvider>,
88 skip_signature: ConfigValue<bool>,
89 role_arn: Option<String>,
91 role_session_name: Option<String>,
93 sts_endpoint: Option<String>,
95}
96
97#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
109#[non_exhaustive]
110pub enum AmazonS3ConfigKey {
111 AccessKeyId,
117
118 SecretAccessKey,
124
125 Region,
131
132 DefaultRegion,
138
139 Token,
147
148 ImdsV1Fallback,
154
155 MetadataEndpoint,
161
162 ContainerCredentialsRelativeUri,
166
167 SkipSignature,
169
170 RoleArn,
176
177 RoleSessionName,
183
184 StsEndpoint,
190
191 Client(ClientConfigKey),
193}
194
195impl AsRef<str> for AmazonS3ConfigKey {
196 fn as_ref(&self) -> &str {
197 match self {
198 Self::AccessKeyId => "aws_access_key_id",
199 Self::SecretAccessKey => "aws_secret_access_key",
200 Self::Region => "aws_region",
201 Self::Token => "aws_session_token",
202 Self::ImdsV1Fallback => "aws_imdsv1_fallback",
203 Self::DefaultRegion => "aws_default_region",
204 Self::MetadataEndpoint => "aws_metadata_endpoint",
205 Self::ContainerCredentialsRelativeUri => "aws_container_credentials_relative_uri",
206 Self::SkipSignature => "aws_skip_signature",
207 Self::RoleArn => "aws_role_arn",
208 Self::RoleSessionName => "aws_role_session_name",
209 Self::StsEndpoint => "aws_sts_endpoint",
210 Self::Client(opt) => opt.as_ref(),
211 }
212 }
213}
214
215impl FromStr for AmazonS3ConfigKey {
216 type Err = crate::Error;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 match s {
220 "aws_access_key_id" | "access_key_id" => Ok(Self::AccessKeyId),
221 "aws_secret_access_key" | "secret_access_key" => Ok(Self::SecretAccessKey),
222 "aws_default_region" | "default_region" => Ok(Self::DefaultRegion),
223 "aws_region" | "region" => Ok(Self::Region),
224 "aws_session_token" | "aws_token" | "session_token" | "token" => Ok(Self::Token),
225 "aws_imdsv1_fallback" | "imdsv1_fallback" => Ok(Self::ImdsV1Fallback),
226 "aws_metadata_endpoint" | "metadata_endpoint" => Ok(Self::MetadataEndpoint),
227 "aws_container_credentials_relative_uri" => Ok(Self::ContainerCredentialsRelativeUri),
228 "aws_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
229 "aws_role_arn" | "role_arn" => Ok(Self::RoleArn),
230 "aws_role_session_name" | "role_session_name" => Ok(Self::RoleSessionName),
231 "aws_sts_endpoint" | "sts_endpoint" => Ok(Self::StsEndpoint),
232 "aws_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
233 _ => match s.strip_prefix("aws_").unwrap_or(s).parse() {
234 Ok(key) => Ok(Self::Client(key)),
235 Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
236 },
237 }
238 }
239}
240
241impl AmazonBuilder {
242 pub fn new() -> Self {
244 Default::default()
245 }
246
247 pub fn from_env() -> Self {
257 let mut builder: Self = Default::default();
258
259 for (os_key, os_value) in std::env::vars_os() {
260 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str())
261 && key.starts_with("AWS_")
262 && let Ok(config_key) = key.to_ascii_lowercase().parse()
263 {
264 builder = builder.with_config(config_key, value);
265 }
266 }
267
268 builder
269 }
270
271 pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
273 match key {
274 AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
275 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
276 AmazonS3ConfigKey::Region => self.region = Some(value.into()),
277 AmazonS3ConfigKey::Token => self.token = Some(value.into()),
278 AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
279 AmazonS3ConfigKey::DefaultRegion => {
280 self.region = self.region.or_else(|| Some(value.into()))
281 }
282 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
283 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
284 self.container_credentials_relative_uri = Some(value.into())
285 }
286 AmazonS3ConfigKey::Client(key) => {
287 self.client_options = self.client_options.with_config(key, value)
288 }
289 AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
290 AmazonS3ConfigKey::RoleArn => self.role_arn = Some(value.into()),
291 AmazonS3ConfigKey::RoleSessionName => self.role_session_name = Some(value.into()),
292 AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint = Some(value.into()),
293 };
294 self
295 }
296
297 pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
299 match key {
300 AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
301 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
302 AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
303 AmazonS3ConfigKey::Token => self.token.clone(),
304 AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
305 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
306 AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
307 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
308 self.container_credentials_relative_uri.clone()
309 }
310 AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
311 AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
312 AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
313 AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
314 }
315 }
316
317 pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
319 self.access_key_id = Some(access_key_id.into());
320 self
321 }
322
323 pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
325 self.secret_access_key = Some(secret_access_key.into());
326 self
327 }
328
329 pub fn with_token(mut self, token: impl Into<String>) -> Self {
331 self.token = Some(token.into());
332 self
333 }
334
335 pub fn with_region(mut self, region: impl Into<String>) -> Self {
337 self.region = Some(region.into());
338 self
339 }
340
341 pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
343 self.credentials = Some(credentials);
344 self
345 }
346
347 pub fn with_allow_http(mut self, allow_http: bool) -> Self {
351 self.client_options = self.client_options.with_allow_http(allow_http);
352 self
353 }
354
355 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
357 self.retry_config = retry_config;
358 self
359 }
360
361 pub fn with_imdsv1_fallback(mut self) -> Self {
371 self.imdsv1_fallback = true.into();
372 self
373 }
374
375 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
377 self.skip_signature = skip_signature.into();
378 self
379 }
380
381 pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
387 self.metadata_endpoint = Some(endpoint.into());
388 self
389 }
390
391 pub fn with_role_arn(mut self, role_arn: impl Into<String>) -> Self {
399 self.role_arn = Some(role_arn.into());
400 self
401 }
402
403 pub fn with_role_session_name(mut self, session_name: impl Into<String>) -> Self {
405 self.role_session_name = Some(session_name.into());
406 self
407 }
408
409 pub fn with_sts_endpoint(mut self, endpoint: impl Into<String>) -> Self {
411 self.sts_endpoint = Some(endpoint.into());
412 self
413 }
414
415 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
417 self.client_options = self.client_options.with_proxy_url(proxy_url);
418 self
419 }
420
421 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
423 self.client_options = self
424 .client_options
425 .with_proxy_ca_certificate(proxy_ca_certificate);
426 self
427 }
428
429 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
431 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
432 self
433 }
434
435 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
437 self.client_options = options;
438 self
439 }
440
441 pub fn build(self, runtime: Option<&Handle>) -> Result<AmazonConfig> {
446 let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
447
448 let credentials = if let Some(credentials) = self.credentials {
449 credentials
450 } else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
451 match (self.access_key_id, self.secret_access_key, self.token) {
452 (Some(key_id), Some(secret_key), token) => {
453 info!("Using Static credential provider");
454 let credential = AwsCredential {
455 key_id,
456 secret_key,
457 token,
458 };
459 Arc::new(StaticCredentialProvider::new(credential)) as _
460 }
461 (None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
462 (Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
463 (None, None, _) => unreachable!(),
464 }
465 } else if let (Ok(token_path), Ok(role_arn)) = (
466 std::env::var("AWS_WEB_IDENTITY_TOKEN_FILE"),
467 std::env::var("AWS_ROLE_ARN"),
468 ) {
469 info!("Using WebIdentity credential provider");
470
471 let session_name = std::env::var("AWS_ROLE_SESSION_NAME")
472 .unwrap_or_else(|_| "WebIdentitySession".to_string());
473
474 let endpoint = format!("https://sts.{region}.amazonaws.com");
475
476 let client = self
477 .client_options
478 .clone()
479 .with_allow_http(false)
480 .client()?;
481
482 let token = WebIdentityProvider {
483 token_path,
484 session_name,
485 role_arn,
486 endpoint,
487 };
488
489 let service = make_service(client.clone(), runtime);
490 Arc::new(TokenCredentialProvider::new(
491 token,
492 client,
493 service,
494 self.retry_config.clone(),
495 )) as _
496 } else if let Ok(full_uri) = std::env::var("AWS_CONTAINER_CREDENTIALS_FULL_URI") {
497 info!("Using Task credential provider (full URI)");
499 let auth_token_file = std::env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE").ok();
500 let client = self.client_options.clone().with_allow_http(true).client()?;
501 let service = make_service(client.clone(), runtime);
502 Arc::new(TaskCredentialProvider {
503 url: full_uri,
504 auth_token_file,
505 retry: self.retry_config.clone(),
506 client,
507 service,
508 cache: Default::default(),
509 }) as _
510 } else if let Some(uri) = self.container_credentials_relative_uri {
511 info!("Using Task credential provider (relative URI)");
512 let auth_token_file = std::env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE").ok();
513 let client = self.client_options.clone().with_allow_http(true).client()?;
514 let service = make_service(client.clone(), runtime);
515 Arc::new(TaskCredentialProvider {
516 url: format!("http://169.254.170.2{uri}"),
517 auth_token_file,
518 retry: self.retry_config.clone(),
519 client,
520 service,
521 cache: Default::default(),
522 }) as _
523 } else {
524 info!("Using Instance credential provider");
525
526 let token = InstanceCredentialProvider {
527 imdsv1_fallback: self.imdsv1_fallback.get()?,
528 metadata_endpoint: self
529 .metadata_endpoint
530 .unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
531 };
532
533 let client = self.client_options.metadata_client()?;
534 let service = make_service(client.clone(), runtime);
535 Arc::new(TokenCredentialProvider::new(
536 token,
537 client,
538 service,
539 self.retry_config.clone(),
540 )) as _
541 };
542
543 let credentials = if let Some(role_arn) = self.role_arn {
545 info!("Wrapping credentials with AssumeRole provider");
546 let session_name = self
547 .role_session_name
548 .unwrap_or_else(|| "AssumeRoleSession".to_string());
549 let endpoint = self
550 .sts_endpoint
551 .unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
552 let client = self
553 .client_options
554 .clone()
555 .with_allow_http(false)
556 .client()?;
557 let service = make_service(client.clone(), runtime);
558 Arc::new(TokenCredentialProvider::new(
559 AssumeRoleProvider {
560 role_arn,
561 session_name,
562 endpoint,
563 base_credentials: credentials,
564 region: region.clone(),
565 policy: None,
566 },
567 client,
568 service,
569 self.retry_config.clone(),
570 )) as _
571 } else {
572 credentials
573 };
574
575 Ok(AmazonConfig {
576 region,
577 credentials,
578 retry_config: self.retry_config,
579 client_options: self.client_options,
580 skip_signature: self.skip_signature.get()?,
581 })
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use std::collections::HashMap;
589
590 #[test]
591 fn s3_test_config_from_map() {
592 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
593 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
594 let aws_default_region = "object_store:fake_default_region".to_string();
595 let aws_session_token = "object_store:fake_session_token".to_string();
596 let options = HashMap::from([
597 ("aws_access_key_id", aws_access_key_id.clone()),
598 ("aws_secret_access_key", aws_secret_access_key),
599 ("aws_default_region", aws_default_region.clone()),
600 ("aws_session_token", aws_session_token.clone()),
601 ]);
602
603 let builder = options
604 .into_iter()
605 .fold(AmazonBuilder::new(), |builder, (key, value)| {
606 builder.with_config(key.parse().unwrap(), value)
607 })
608 .with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
609
610 assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
611 assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
612 assert_eq!(builder.region.unwrap(), aws_default_region);
613 assert_eq!(builder.token.unwrap(), aws_session_token);
614 }
615
616 #[test]
617 fn s3_test_config_get_value() {
618 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
619 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
620 let aws_default_region = "object_store:fake_default_region".to_string();
621 let aws_session_token = "object_store:fake_session_token".to_string();
622
623 let builder = AmazonBuilder::new()
624 .with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
625 .with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
626 .with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
627 .with_config(AmazonS3ConfigKey::Token, &aws_session_token);
628
629 assert_eq!(
630 builder
631 .get_config_value(&AmazonS3ConfigKey::AccessKeyId)
632 .unwrap(),
633 aws_access_key_id
634 );
635 assert_eq!(
636 builder
637 .get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
638 .unwrap(),
639 aws_secret_access_key
640 );
641 assert_eq!(
642 builder
643 .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
644 .unwrap(),
645 aws_default_region
646 );
647 assert_eq!(
648 builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
649 aws_session_token
650 );
651 }
652
653 #[test]
654 fn s3_default_region() {
655 let config = AmazonBuilder::new().build(None).unwrap();
656 assert_eq!(config.region, "us-east-1");
657 }
658
659 #[tokio::test]
660 async fn s3_test_proxy_url() {
661 let s3 = AmazonBuilder::new()
662 .with_access_key_id("access_key_id")
663 .with_secret_access_key("secret_access_key")
664 .with_region("region")
665 .with_allow_http(true)
666 .with_proxy_url("https://example.com")
667 .build(None);
668
669 assert!(s3.is_ok());
670 }
671
672 #[test]
673 fn test_invalid_config() {
674 let err = AmazonBuilder::new()
675 .with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
676 .with_region("region")
677 .build(None)
678 .unwrap_err()
679 .to_string();
680
681 assert_eq!(err, "Generic error: failed to parse \"enabled\" as boolean");
682 }
683
684 #[test]
685 fn aws_test_client_opts() {
686 let key = "AWS_PROXY_URL";
687 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
688 assert_eq!(
689 AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
690 config_key
691 );
692 } else {
693 panic!("{key} not propagated as ClientConfigKey");
694 }
695 }
696}