Skip to main content

olai_http/aws/
builder.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// Configure AWS authentication credentials.
63///
64/// # Example
65/// ```
66/// # let REGION = "foo";
67/// # let ACCESS_KEY_ID = "foo";
68/// # let SECRET_KEY = "foo";
69/// # use olai_http::aws::AmazonBuilder;
70/// let config = AmazonBuilder::new()
71///  .with_region(REGION)
72///  .with_access_key_id(ACCESS_KEY_ID)
73///  .with_secret_access_key(SECRET_KEY)
74///  .build(None);
75/// ```
76#[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    /// IAM role ARN to assume via STS `AssumeRole`.
90    role_arn: Option<String>,
91    /// Session name for the assumed role (defaults to `"AssumeRoleSession"`).
92    role_session_name: Option<String>,
93    /// STS endpoint override for `AssumeRole` (defaults to regional STS).
94    sts_endpoint: Option<String>,
95}
96
97/// Configuration keys for [`AmazonBuilder`]
98///
99/// Configuration via keys can be done via [`AmazonBuilder::with_config`]
100///
101/// # Example
102/// ```
103/// # use olai_http::aws::{AmazonBuilder, AmazonS3ConfigKey};
104/// let builder = AmazonBuilder::new()
105///     .with_config("aws_access_key_id".parse().unwrap(), "my-access-key-id")
106///     .with_config(AmazonS3ConfigKey::DefaultRegion, "my-default-region");
107/// ```
108#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
109#[non_exhaustive]
110pub enum AmazonS3ConfigKey {
111    /// AWS Access Key
112    ///
113    /// Supported keys:
114    /// - `aws_access_key_id`
115    /// - `access_key_id`
116    AccessKeyId,
117
118    /// Secret Access Key
119    ///
120    /// Supported keys:
121    /// - `aws_secret_access_key`
122    /// - `secret_access_key`
123    SecretAccessKey,
124
125    /// Region
126    ///
127    /// Supported keys:
128    /// - `aws_region`
129    /// - `region`
130    Region,
131
132    /// Default region
133    ///
134    /// Supported keys:
135    /// - `aws_default_region`
136    /// - `default_region`
137    DefaultRegion,
138
139    /// Token to use for requests (passed to underlying provider)
140    ///
141    /// Supported keys:
142    /// - `aws_session_token`
143    /// - `aws_token`
144    /// - `session_token`
145    /// - `token`
146    Token,
147
148    /// Fall back to ImdsV1
149    ///
150    /// Supported keys:
151    /// - `aws_imdsv1_fallback`
152    /// - `imdsv1_fallback`
153    ImdsV1Fallback,
154
155    /// Set the instance metadata endpoint
156    ///
157    /// Supported keys:
158    /// - `aws_metadata_endpoint`
159    /// - `metadata_endpoint`
160    MetadataEndpoint,
161
162    /// Set the container credentials relative URI
163    ///
164    /// <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
165    ContainerCredentialsRelativeUri,
166
167    /// Skip signing request
168    SkipSignature,
169
170    /// IAM role ARN to assume via STS `AssumeRole`.
171    ///
172    /// Supported keys:
173    /// - `aws_role_arn`
174    /// - `role_arn`
175    RoleArn,
176
177    /// Session name for the assumed role.
178    ///
179    /// Supported keys:
180    /// - `aws_role_session_name`
181    /// - `role_session_name`
182    RoleSessionName,
183
184    /// STS endpoint override for `AssumeRole`.
185    ///
186    /// Supported keys:
187    /// - `aws_sts_endpoint`
188    /// - `sts_endpoint`
189    StsEndpoint,
190
191    /// Client options
192    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    /// Create a new [`AmazonBuilder`] with default values.
243    pub fn new() -> Self {
244        Default::default()
245    }
246
247    /// Fill the [`AmazonBuilder`] with regular AWS environment variables
248    ///
249    /// Variables extracted from environment:
250    /// * `AWS_ACCESS_KEY_ID` -> access_key_id
251    /// * `AWS_SECRET_ACCESS_KEY` -> secret_access_key
252    /// * `AWS_DEFAULT_REGION` -> region
253    /// * `AWS_SESSION_TOKEN` -> token
254    /// * `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` -> <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
255    /// * `AWS_ALLOW_HTTP` -> set to "true" to permit HTTP connections without TLS
256    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    /// Set an option on the builder via a key - value pair.
272    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    /// Get config value via a [`AmazonS3ConfigKey`].
298    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    /// Set the AWS Access Key
318    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    /// Set the AWS Secret Access Key
324    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    /// Set the AWS Session Token to use for requests
330    pub fn with_token(mut self, token: impl Into<String>) -> Self {
331        self.token = Some(token.into());
332        self
333    }
334
335    /// Set the region, defaults to `us-east-1`
336    pub fn with_region(mut self, region: impl Into<String>) -> Self {
337        self.region = Some(region.into());
338        self
339    }
340
341    /// Set the credential provider overriding any other options
342    pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
343        self.credentials = Some(credentials);
344        self
345    }
346
347    /// Sets what protocol is allowed. If `allow_http` is :
348    /// * false (default):  Only HTTPS are allowed
349    /// * true:  HTTP and HTTPS are allowed
350    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    /// Set the retry configuration
356    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
357        self.retry_config = retry_config;
358        self
359    }
360
361    /// By default instance credentials will only be fetched over [IMDSv2], as AWS recommends
362    /// against having IMDSv1 enabled on EC2 instances as it is vulnerable to [SSRF attack]
363    ///
364    /// However, certain deployment environments, such as those running old versions of kube2iam,
365    /// may not support IMDSv2. This option will enable automatic fallback to using IMDSv1
366    /// if the token endpoint returns a 403 error indicating that IMDSv2 is not supported.
367    ///
368    /// [IMDSv2]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
369    /// [SSRF attack]: https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
370    pub fn with_imdsv1_fallback(mut self) -> Self {
371        self.imdsv1_fallback = true.into();
372        self
373    }
374
375    /// If enabled, requests will not be signed.
376    pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
377        self.skip_signature = skip_signature.into();
378        self
379    }
380
381    /// Set the [instance metadata endpoint](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html),
382    /// used primarily within AWS EC2.
383    ///
384    /// This defaults to the IPv4 endpoint: http://169.254.169.254. One can alternatively use the IPv6
385    /// endpoint http://fd00:ec2::254.
386    pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
387        self.metadata_endpoint = Some(endpoint.into());
388        self
389    }
390
391    /// Assume the given IAM role via STS `AssumeRole` after obtaining base credentials.
392    ///
393    /// When set, the builder resolves base credentials (static, IMDS, or WebIdentity)
394    /// and then exchanges them for temporary credentials scoped to `role_arn`.
395    ///
396    /// # References
397    /// - <https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html>
398    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    /// Set the session name used in `AssumeRole` requests (defaults to `"AssumeRoleSession"`).
404    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    /// Override the STS endpoint used for `AssumeRole` (defaults to the regional endpoint).
410    pub fn with_sts_endpoint(mut self, endpoint: impl Into<String>) -> Self {
411        self.sts_endpoint = Some(endpoint.into());
412        self
413    }
414
415    /// Set the proxy_url to be used by the underlying client
416    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    /// Set a trusted proxy CA certificate
422    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    /// Set a list of hosts to exclude from proxy connections
430    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    /// Sets the client options, overriding any already set
436    pub fn with_client_options(mut self, options: ClientOptions) -> Self {
437        self.client_options = options;
438        self
439    }
440
441    /// Build an [`AmazonConfig`] from the provided values, consuming `self`.
442    ///
443    /// If `runtime` is provided, all HTTP I/O (including credential refresh)
444    /// will be spawned on the given runtime handle.
445    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            // EKS Pod Identity and Lambda use a full absolute URI
498            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        // Optionally wrap base credentials with AssumeRole if a role ARN is configured.
544        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}