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                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    /// Set an option on the builder via a key - value pair.
273    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    /// Get config value via a [`AmazonS3ConfigKey`].
299    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    /// Set the AWS Access Key
319    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    /// Set the AWS Secret Access Key
325    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    /// Set the AWS Session Token to use for requests
331    pub fn with_token(mut self, token: impl Into<String>) -> Self {
332        self.token = Some(token.into());
333        self
334    }
335
336    /// Set the region, defaults to `us-east-1`
337    pub fn with_region(mut self, region: impl Into<String>) -> Self {
338        self.region = Some(region.into());
339        self
340    }
341
342    /// Set the credential provider overriding any other options
343    pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
344        self.credentials = Some(credentials);
345        self
346    }
347
348    /// Sets what protocol is allowed. If `allow_http` is :
349    /// * false (default):  Only HTTPS are allowed
350    /// * true:  HTTP and HTTPS are allowed
351    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    /// Set the retry configuration
357    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
358        self.retry_config = retry_config;
359        self
360    }
361
362    /// By default instance credentials will only be fetched over [IMDSv2], as AWS recommends
363    /// against having IMDSv1 enabled on EC2 instances as it is vulnerable to [SSRF attack]
364    ///
365    /// However, certain deployment environments, such as those running old versions of kube2iam,
366    /// may not support IMDSv2. This option will enable automatic fallback to using IMDSv1
367    /// if the token endpoint returns a 403 error indicating that IMDSv2 is not supported.
368    ///
369    /// [IMDSv2]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
370    /// [SSRF attack]: https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
371    pub fn with_imdsv1_fallback(mut self) -> Self {
372        self.imdsv1_fallback = true.into();
373        self
374    }
375
376    /// If enabled, requests will not be signed.
377    pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
378        self.skip_signature = skip_signature.into();
379        self
380    }
381
382    /// Set the [instance metadata endpoint](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html),
383    /// used primarily within AWS EC2.
384    ///
385    /// This defaults to the IPv4 endpoint: http://169.254.169.254. One can alternatively use the IPv6
386    /// endpoint http://fd00:ec2::254.
387    pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
388        self.metadata_endpoint = Some(endpoint.into());
389        self
390    }
391
392    /// Assume the given IAM role via STS `AssumeRole` after obtaining base credentials.
393    ///
394    /// When set, the builder resolves base credentials (static, IMDS, or WebIdentity)
395    /// and then exchanges them for temporary credentials scoped to `role_arn`.
396    ///
397    /// # References
398    /// - <https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html>
399    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    /// Set the session name used in `AssumeRole` requests (defaults to `"AssumeRoleSession"`).
405    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    /// Override the STS endpoint used for `AssumeRole` (defaults to the regional endpoint).
411    pub fn with_sts_endpoint(mut self, endpoint: impl Into<String>) -> Self {
412        self.sts_endpoint = Some(endpoint.into());
413        self
414    }
415
416    /// Set the proxy_url to be used by the underlying client
417    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    /// Set a trusted proxy CA certificate
423    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    /// Set a list of hosts to exclude from proxy connections
431    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    /// Sets the client options, overriding any already set
437    pub fn with_client_options(mut self, options: ClientOptions) -> Self {
438        self.client_options = options;
439        self
440    }
441
442    /// Build an [`AmazonConfig`] from the provided values, consuming `self`.
443    ///
444    /// If `runtime` is provided, all HTTP I/O (including credential refresh)
445    /// will be spawned on the given runtime handle.
446    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            // EKS Pod Identity and Lambda use a full absolute URI
499            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        // Optionally wrap base credentials with AssumeRole if a role ARN is configured.
545        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}