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 if key.starts_with("AWS_") {
262 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
263 builder = builder.with_config(config_key, value);
264 }
265 }
266 }
267 }
268
269 builder
270 }
271
272 pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
274 match key {
275 AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
276 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
277 AmazonS3ConfigKey::Region => self.region = Some(value.into()),
278 AmazonS3ConfigKey::Token => self.token = Some(value.into()),
279 AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
280 AmazonS3ConfigKey::DefaultRegion => {
281 self.region = self.region.or_else(|| Some(value.into()))
282 }
283 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
284 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
285 self.container_credentials_relative_uri = Some(value.into())
286 }
287 AmazonS3ConfigKey::Client(key) => {
288 self.client_options = self.client_options.with_config(key, value)
289 }
290 AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
291 AmazonS3ConfigKey::RoleArn => self.role_arn = Some(value.into()),
292 AmazonS3ConfigKey::RoleSessionName => self.role_session_name = Some(value.into()),
293 AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint = Some(value.into()),
294 };
295 self
296 }
297
298 pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
300 match key {
301 AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
302 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
303 AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
304 AmazonS3ConfigKey::Token => self.token.clone(),
305 AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
306 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
307 AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
308 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
309 self.container_credentials_relative_uri.clone()
310 }
311 AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
312 AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
313 AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
314 AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
315 }
316 }
317
318 pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
320 self.access_key_id = Some(access_key_id.into());
321 self
322 }
323
324 pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
326 self.secret_access_key = Some(secret_access_key.into());
327 self
328 }
329
330 pub fn with_token(mut self, token: impl Into<String>) -> Self {
332 self.token = Some(token.into());
333 self
334 }
335
336 pub fn with_region(mut self, region: impl Into<String>) -> Self {
338 self.region = Some(region.into());
339 self
340 }
341
342 pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
344 self.credentials = Some(credentials);
345 self
346 }
347
348 pub fn with_allow_http(mut self, allow_http: bool) -> Self {
352 self.client_options = self.client_options.with_allow_http(allow_http);
353 self
354 }
355
356 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
358 self.retry_config = retry_config;
359 self
360 }
361
362 pub fn with_imdsv1_fallback(mut self) -> Self {
372 self.imdsv1_fallback = true.into();
373 self
374 }
375
376 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
378 self.skip_signature = skip_signature.into();
379 self
380 }
381
382 pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
388 self.metadata_endpoint = Some(endpoint.into());
389 self
390 }
391
392 pub fn with_role_arn(mut self, role_arn: impl Into<String>) -> Self {
400 self.role_arn = Some(role_arn.into());
401 self
402 }
403
404 pub fn with_role_session_name(mut self, session_name: impl Into<String>) -> Self {
406 self.role_session_name = Some(session_name.into());
407 self
408 }
409
410 pub fn with_sts_endpoint(mut self, endpoint: impl Into<String>) -> Self {
412 self.sts_endpoint = Some(endpoint.into());
413 self
414 }
415
416 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
418 self.client_options = self.client_options.with_proxy_url(proxy_url);
419 self
420 }
421
422 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
424 self.client_options = self
425 .client_options
426 .with_proxy_ca_certificate(proxy_ca_certificate);
427 self
428 }
429
430 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
432 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
433 self
434 }
435
436 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
438 self.client_options = options;
439 self
440 }
441
442 pub fn build(self, runtime: Option<&Handle>) -> Result<AmazonConfig> {
447 let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
448
449 let credentials = if let Some(credentials) = self.credentials {
450 credentials
451 } else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
452 match (self.access_key_id, self.secret_access_key, self.token) {
453 (Some(key_id), Some(secret_key), token) => {
454 info!("Using Static credential provider");
455 let credential = AwsCredential {
456 key_id,
457 secret_key,
458 token,
459 };
460 Arc::new(StaticCredentialProvider::new(credential)) as _
461 }
462 (None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
463 (Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
464 (None, None, _) => unreachable!(),
465 }
466 } else if let (Ok(token_path), Ok(role_arn)) = (
467 std::env::var("AWS_WEB_IDENTITY_TOKEN_FILE"),
468 std::env::var("AWS_ROLE_ARN"),
469 ) {
470 info!("Using WebIdentity credential provider");
471
472 let session_name = std::env::var("AWS_ROLE_SESSION_NAME")
473 .unwrap_or_else(|_| "WebIdentitySession".to_string());
474
475 let endpoint = format!("https://sts.{region}.amazonaws.com");
476
477 let client = self
478 .client_options
479 .clone()
480 .with_allow_http(false)
481 .client()?;
482
483 let token = WebIdentityProvider {
484 token_path,
485 session_name,
486 role_arn,
487 endpoint,
488 };
489
490 let service = make_service(client.clone(), runtime);
491 Arc::new(TokenCredentialProvider::new(
492 token,
493 client,
494 service,
495 self.retry_config.clone(),
496 )) as _
497 } else if let Ok(full_uri) = std::env::var("AWS_CONTAINER_CREDENTIALS_FULL_URI") {
498 info!("Using Task credential provider (full URI)");
500 let auth_token_file = std::env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE").ok();
501 let client = self.client_options.clone().with_allow_http(true).client()?;
502 let service = make_service(client.clone(), runtime);
503 Arc::new(TaskCredentialProvider {
504 url: full_uri,
505 auth_token_file,
506 retry: self.retry_config.clone(),
507 client,
508 service,
509 cache: Default::default(),
510 }) as _
511 } else if let Some(uri) = self.container_credentials_relative_uri {
512 info!("Using Task credential provider (relative URI)");
513 let auth_token_file = std::env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE").ok();
514 let client = self.client_options.clone().with_allow_http(true).client()?;
515 let service = make_service(client.clone(), runtime);
516 Arc::new(TaskCredentialProvider {
517 url: format!("http://169.254.170.2{uri}"),
518 auth_token_file,
519 retry: self.retry_config.clone(),
520 client,
521 service,
522 cache: Default::default(),
523 }) as _
524 } else {
525 info!("Using Instance credential provider");
526
527 let token = InstanceCredentialProvider {
528 imdsv1_fallback: self.imdsv1_fallback.get()?,
529 metadata_endpoint: self
530 .metadata_endpoint
531 .unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
532 };
533
534 let client = self.client_options.metadata_client()?;
535 let service = make_service(client.clone(), runtime);
536 Arc::new(TokenCredentialProvider::new(
537 token,
538 client,
539 service,
540 self.retry_config.clone(),
541 )) as _
542 };
543
544 let credentials = if let Some(role_arn) = self.role_arn {
546 info!("Wrapping credentials with AssumeRole provider");
547 let session_name = self
548 .role_session_name
549 .unwrap_or_else(|| "AssumeRoleSession".to_string());
550 let endpoint = self
551 .sts_endpoint
552 .unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
553 let client = self
554 .client_options
555 .clone()
556 .with_allow_http(false)
557 .client()?;
558 let service = make_service(client.clone(), runtime);
559 Arc::new(TokenCredentialProvider::new(
560 AssumeRoleProvider {
561 role_arn,
562 session_name,
563 endpoint,
564 base_credentials: credentials,
565 region: region.clone(),
566 policy: None,
567 },
568 client,
569 service,
570 self.retry_config.clone(),
571 )) as _
572 } else {
573 credentials
574 };
575
576 Ok(AmazonConfig {
577 region,
578 credentials,
579 retry_config: self.retry_config,
580 client_options: self.client_options,
581 skip_signature: self.skip_signature.get()?,
582 })
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use std::collections::HashMap;
590
591 #[test]
592 fn s3_test_config_from_map() {
593 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
594 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
595 let aws_default_region = "object_store:fake_default_region".to_string();
596 let aws_session_token = "object_store:fake_session_token".to_string();
597 let options = HashMap::from([
598 ("aws_access_key_id", aws_access_key_id.clone()),
599 ("aws_secret_access_key", aws_secret_access_key),
600 ("aws_default_region", aws_default_region.clone()),
601 ("aws_session_token", aws_session_token.clone()),
602 ]);
603
604 let builder = options
605 .into_iter()
606 .fold(AmazonBuilder::new(), |builder, (key, value)| {
607 builder.with_config(key.parse().unwrap(), value)
608 })
609 .with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
610
611 assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
612 assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
613 assert_eq!(builder.region.unwrap(), aws_default_region);
614 assert_eq!(builder.token.unwrap(), aws_session_token);
615 }
616
617 #[test]
618 fn s3_test_config_get_value() {
619 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
620 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
621 let aws_default_region = "object_store:fake_default_region".to_string();
622 let aws_session_token = "object_store:fake_session_token".to_string();
623
624 let builder = AmazonBuilder::new()
625 .with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
626 .with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
627 .with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
628 .with_config(AmazonS3ConfigKey::Token, &aws_session_token);
629
630 assert_eq!(
631 builder
632 .get_config_value(&AmazonS3ConfigKey::AccessKeyId)
633 .unwrap(),
634 aws_access_key_id
635 );
636 assert_eq!(
637 builder
638 .get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
639 .unwrap(),
640 aws_secret_access_key
641 );
642 assert_eq!(
643 builder
644 .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
645 .unwrap(),
646 aws_default_region
647 );
648 assert_eq!(
649 builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
650 aws_session_token
651 );
652 }
653
654 #[test]
655 fn s3_default_region() {
656 let config = AmazonBuilder::new().build(None).unwrap();
657 assert_eq!(config.region, "us-east-1");
658 }
659
660 #[tokio::test]
661 async fn s3_test_proxy_url() {
662 let s3 = AmazonBuilder::new()
663 .with_access_key_id("access_key_id")
664 .with_secret_access_key("secret_access_key")
665 .with_region("region")
666 .with_allow_http(true)
667 .with_proxy_url("https://example.com")
668 .build(None);
669
670 assert!(s3.is_ok());
671 }
672
673 #[test]
674 fn test_invalid_config() {
675 let err = AmazonBuilder::new()
676 .with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
677 .with_region("region")
678 .build(None)
679 .unwrap_err()
680 .to_string();
681
682 assert_eq!(err, "Generic error: failed to parse \"enabled\" as boolean");
683 }
684
685 #[test]
686 fn aws_test_client_opts() {
687 let key = "AWS_PROXY_URL";
688 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
689 assert_eq!(
690 AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
691 config_key
692 );
693 } else {
694 panic!("{key} not propagated as ClientConfigKey");
695 }
696 }
697}