This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs-object-store.git


The following commit(s) were added to refs/heads/main by this push:
     new ebfd02f  AWS S3: Support STS endpoint, WebIdentity, RoleArn, 
RoleSession configuration (#480)
ebfd02f is described below

commit ebfd02f0209e1cc3f5625815e3e5431ed0976d93
Author: Matt Friede <[email protected]>
AuthorDate: Fri Sep 19 10:11:06 2025 -0400

    AWS S3: Support STS endpoint, WebIdentity, RoleArn, RoleSession 
configuration (#480)
    
    * Allow setting STS endpoint via env var
    
    * Properly use AmazonS3Builder::credentials_from_env for 
AssumeRoleWithWebIdentity auth flow
    
    ---------
    
    Co-authored-by: Andrew Lamb <[email protected]>
---
 src/aws/builder.rs | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 128 insertions(+), 6 deletions(-)

diff --git a/src/aws/builder.rs b/src/aws/builder.rs
index 4dd41ff..06503ca 100644
--- a/src/aws/builder.rs
+++ b/src/aws/builder.rs
@@ -156,6 +156,14 @@ pub struct AmazonS3Builder {
     container_credentials_full_uri: Option<String>,
     /// Container authorization token file, see 
<https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
     container_authorization_token_file: Option<String>,
+    /// Web identity token file path for AssumeRoleWithWebIdentity
+    web_identity_token_file: Option<String>,
+    /// Role ARN to assume when using web identity token
+    role_arn: Option<String>,
+    /// Session name for web identity role assumption
+    role_session_name: Option<String>,
+    /// Custom STS endpoint for web identity token exchange
+    sts_endpoint: Option<String>,
     /// Client options
     client_options: ClientOptions,
     /// Credentials
@@ -319,6 +327,34 @@ pub enum AmazonS3ConfigKey {
     /// 
<https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
     ContainerAuthorizationTokenFile,
 
+    /// Web identity token file path for AssumeRoleWithWebIdentity
+    ///
+    /// Supported keys:
+    /// - `aws_web_identity_token_file`
+    /// - `web_identity_token_file`
+    WebIdentityTokenFile,
+
+    /// Role ARN to assume when using web identity token
+    ///
+    /// Supported keys:
+    /// - `aws_role_arn`
+    /// - `role_arn`
+    RoleArn,
+
+    /// Session name for web identity role assumption
+    ///
+    /// Supported keys:
+    /// - `aws_role_session_name`
+    /// - `role_session_name`
+    RoleSessionName,
+
+    /// Custom STS endpoint for web identity token exchange
+    ///
+    /// Supported keys:
+    /// - `aws_endpoint_url_sts`
+    /// - `endpoint_url_sts`
+    StsEndpoint,
+
     /// Configure how to provide `copy_if_not_exists`
     ///
     /// See [`S3CopyIfNotExists`]
@@ -381,6 +417,10 @@ impl AsRef<str> for AmazonS3ConfigKey {
             Self::ContainerCredentialsRelativeUri => 
"aws_container_credentials_relative_uri",
             Self::ContainerCredentialsFullUri => 
"aws_container_credentials_full_uri",
             Self::ContainerAuthorizationTokenFile => 
"aws_container_authorization_token_file",
+            Self::WebIdentityTokenFile => "aws_web_identity_token_file",
+            Self::RoleArn => "aws_role_arn",
+            Self::RoleSessionName => "aws_role_session_name",
+            Self::StsEndpoint => "aws_endpoint_url_sts",
             Self::SkipSignature => "aws_skip_signature",
             Self::CopyIfNotExists => "aws_copy_if_not_exists",
             Self::ConditionalPut => "aws_conditional_put",
@@ -415,6 +455,12 @@ impl FromStr for AmazonS3ConfigKey {
             "aws_container_credentials_relative_uri" => 
Ok(Self::ContainerCredentialsRelativeUri),
             "aws_container_credentials_full_uri" => 
Ok(Self::ContainerCredentialsFullUri),
             "aws_container_authorization_token_file" => 
Ok(Self::ContainerAuthorizationTokenFile),
+            "aws_web_identity_token_file" | "web_identity_token_file" => {
+                Ok(Self::WebIdentityTokenFile)
+            }
+            "aws_role_arn" | "role_arn" => Ok(Self::RoleArn),
+            "aws_role_session_name" | "role_session_name" => 
Ok(Self::RoleSessionName),
+            "aws_endpoint_url_sts" | "endpoint_url_sts" => 
Ok(Self::StsEndpoint),
             "aws_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
             "aws_copy_if_not_exists" | "copy_if_not_exists" => 
Ok(Self::CopyIfNotExists),
             "aws_conditional_put" | "conditional_put" => 
Ok(Self::ConditionalPut),
@@ -458,6 +504,10 @@ impl AmazonS3Builder {
     /// * `AWS_DEFAULT_REGION` -> region
     /// * `AWS_ENDPOINT` -> endpoint
     /// * `AWS_SESSION_TOKEN` -> token
+    /// * `AWS_WEB_IDENTITY_TOKEN_FILE` -> path to file containing web 
identity token for AssumeRoleWithWebIdentity
+    /// * `AWS_ROLE_ARN` -> ARN of the role to assume when using web identity 
token
+    /// * `AWS_ROLE_SESSION_NAME` -> optional session name for web identity 
role assumption (defaults to "WebIdentitySession")
+    /// * `AWS_ENDPOINT_URL_STS` -> optional custom STS endpoint for web 
identity token exchange (defaults to "https://sts.{region}.amazonaws.com";)
     /// * `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` -> 
<https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
     /// * `AWS_CONTAINER_CREDENTIALS_FULL_URI` -> 
<https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
     /// * `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` -> 
<https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
@@ -543,6 +593,18 @@ impl AmazonS3Builder {
             AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
                 self.container_authorization_token_file = Some(value.into());
             }
+            AmazonS3ConfigKey::WebIdentityTokenFile => {
+                self.web_identity_token_file = Some(value.into());
+            }
+            AmazonS3ConfigKey::RoleArn => {
+                self.role_arn = Some(value.into());
+            }
+            AmazonS3ConfigKey::RoleSessionName => {
+                self.role_session_name = Some(value.into());
+            }
+            AmazonS3ConfigKey::StsEndpoint => {
+                self.sts_endpoint = Some(value.into());
+            }
             AmazonS3ConfigKey::Client(key) => {
                 self.client_options = self.client_options.with_config(key, 
value)
             }
@@ -612,6 +674,10 @@ impl AmazonS3Builder {
             AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
                 self.container_authorization_token_file.clone()
             }
+            AmazonS3ConfigKey::WebIdentityTokenFile => 
self.web_identity_token_file.clone(),
+            AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
+            AmazonS3ConfigKey::RoleSessionName => 
self.role_session_name.clone(),
+            AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
             AmazonS3ConfigKey::SkipSignature => 
Some(self.skip_signature.to_string()),
             AmazonS3ConfigKey::CopyIfNotExists => {
                 self.copy_if_not_exists.as_ref().map(ToString::to_string)
@@ -959,21 +1025,25 @@ impl AmazonS3Builder {
             std::env::var("AWS_WEB_IDENTITY_TOKEN_FILE"),
             std::env::var("AWS_ROLE_ARN"),
         ) {
-            // TODO: Replace with `AmazonS3Builder::credentials_from_env`
             debug!("Using WebIdentity credential provider");
 
-            let session_name = std::env::var("AWS_ROLE_SESSION_NAME")
-                .unwrap_or_else(|_| "WebIdentitySession".to_string());
+            let session_name = self
+                .role_session_name
+                .clone()
+                .unwrap_or_else(|| "WebIdentitySession".to_string());
 
-            let endpoint = format!("https://sts.{region}.amazonaws.com";);
+            let endpoint = self
+                .sts_endpoint
+                .clone()
+                .unwrap_or_else(|| 
format!("https://sts.{region}.amazonaws.com";));
 
             // Disallow non-HTTPs requests
             let options = self.client_options.clone().with_allow_http(false);
 
             let token = WebIdentityProvider {
-                token_path,
+                token_path: token_path.clone(),
                 session_name,
-                role_arn,
+                role_arn: role_arn.clone(),
                 endpoint,
             };
 
@@ -1611,4 +1681,56 @@ mod tests {
             "expected EKS provider but got: {debug_str}"
         );
     }
+
+    #[test]
+    fn test_builder_web_identity_with_config() {
+        let builder = AmazonS3Builder::new()
+            .with_bucket_name("some-bucket")
+            .with_config(
+                AmazonS3ConfigKey::WebIdentityTokenFile,
+                "/tmp/fake-token-file",
+            )
+            .with_config(
+                AmazonS3ConfigKey::RoleArn,
+                "arn:aws:iam::123456789012:role/test-role",
+            )
+            .with_config(AmazonS3ConfigKey::RoleSessionName, "TestSession")
+            .with_config(
+                AmazonS3ConfigKey::StsEndpoint,
+                "https://sts.us-west-2.amazonaws.com";,
+            );
+
+        assert_eq!(
+            builder
+                .get_config_value(&AmazonS3ConfigKey::WebIdentityTokenFile)
+                .unwrap(),
+            "/tmp/fake-token-file"
+        );
+        assert_eq!(
+            builder
+                .get_config_value(&AmazonS3ConfigKey::RoleArn)
+                .unwrap(),
+            "arn:aws:iam::123456789012:role/test-role"
+        );
+        assert_eq!(
+            builder
+                .get_config_value(&AmazonS3ConfigKey::RoleSessionName)
+                .unwrap(),
+            "TestSession"
+        );
+        assert_eq!(
+            builder
+                .get_config_value(&AmazonS3ConfigKey::StsEndpoint)
+                .unwrap(),
+            "https://sts.us-west-2.amazonaws.com";
+        );
+
+        let s3 = builder.build().expect("should build successfully");
+        let creds = &s3.client.config.credentials;
+        let debug_str = format!("{creds:?}");
+        assert!(
+            debug_str.contains("TokenCredentialProvider"),
+            "expected TokenCredentialProvider but got: {debug_str}"
+        );
+    }
 }

Reply via email to