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 c219109  feat: add AzureConfigKey::CredentialType to select Azure 
credential method (#710)
c219109 is described below

commit c219109b5e9cb368c5da35adcc45e93d804a3cae
Author: Jack Ye <[email protected]>
AuthorDate: Wed Jun 17 14:27:31 2026 -0700

    feat: add AzureConfigKey::CredentialType to select Azure credential method 
(#710)
    
    When multiple Azure credential configurations are present in
    environment variables (e.g. both workload identity and client secret),
    the builder's fixed resolution order may pick an undesired credential.
    This adds a `credential_type` config key that lets users explicitly
    select which credential method to use, bypassing the priority chain.
    
    Supported values: auto, bearer_token, access_key, client_secret,
    workload_identity, sas_token, azure_cli, managed_identity.
    
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
    Co-authored-by: Andrew Lamb <[email protected]>
---
 src/azure/builder.rs | 402 +++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 324 insertions(+), 78 deletions(-)

diff --git a/src/azure/builder.rs b/src/azure/builder.rs
index c1f0d72..852f970 100644
--- a/src/azure/builder.rs
+++ b/src/azure/builder.rs
@@ -92,6 +92,18 @@ enum Error {
 
     #[error("Configuration key: '{}' is not known.", key)]
     UnknownConfigurationKey { key: String },
+
+    #[error(
+        "Unknown credential type: '{}'. Supported values: auto, bearer_token, 
access_key, client_secret, workload_identity, sas_token, azure_cli, 
managed_identity",
+        credential_type
+    )]
+    UnknownCredentialType { credential_type: String },
+
+    #[error(
+        "Credential type '{}' was requested but required configuration is 
missing",
+        credential_type
+    )]
+    MissingCredentialConfig { credential_type: String },
 }
 
 impl From<Error> for crate::Error {
@@ -185,6 +197,8 @@ pub struct MicrosoftAzureBuilder {
     fabric_session_token: Option<String>,
     /// Fabric cluster identifier
     fabric_cluster_identifier: Option<String>,
+    /// Credential type override
+    credential_type: Option<String>,
     /// Base64-encoded 256-bit customer-provided encryption key
     encryption_key: Option<String>,
     /// The [`HttpConnector`] to use
@@ -389,6 +403,27 @@ pub enum AzureConfigKey {
     /// - `fabric_cluster_identifier`
     FabricClusterIdentifier,
 
+    /// Credential type to use for authentication
+    ///
+    /// When multiple credential configurations are present, this key forces
+    /// the builder to use a specific credential type instead of relying on
+    /// the default resolution order.
+    ///
+    /// Supported values:
+    /// - `auto` (default) — use the built-in priority chain
+    /// - `bearer_token` — use a static bearer token
+    /// - `access_key` — use an access key
+    /// - `client_secret` — use client secret (service principal) OAuth
+    /// - `workload_identity` — use workload identity federation
+    /// - `sas_token` — use a shared access signature
+    /// - `azure_cli` — use Azure CLI
+    /// - `managed_identity` — use IMDS managed identity
+    ///
+    /// Supported keys:
+    /// - `azure_credential_type`
+    /// - `credential_type`
+    CredentialType,
+
     /// Base64-encoded customer-provided encryption key
     ///
     /// Supported keys:
@@ -426,6 +461,7 @@ impl AsRef<str> for AzureConfigKey {
             Self::FabricWorkloadHost => "azure_fabric_workload_host",
             Self::FabricSessionToken => "azure_fabric_session_token",
             Self::FabricClusterIdentifier => "azure_fabric_cluster_identifier",
+            Self::CredentialType => "azure_credential_type",
             Self::EncryptionKey => "azure_storage_encryption_key",
             Self::Client(key) => key.as_ref(),
         }
@@ -483,6 +519,7 @@ impl FromStr for AzureConfigKey {
             "azure_fabric_cluster_identifier" | "fabric_cluster_identifier" => 
{
                 Ok(Self::FabricClusterIdentifier)
             }
+            "azure_credential_type" | "credential_type" => 
Ok(Self::CredentialType),
             "azure_storage_encryption_key" | "encryption_key" => 
Ok(Self::EncryptionKey),
             // Backwards compatibility
             "azure_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
@@ -612,6 +649,7 @@ impl MicrosoftAzureBuilder {
             AzureConfigKey::FabricClusterIdentifier => {
                 self.fabric_cluster_identifier = Some(value.into())
             }
+            AzureConfigKey::CredentialType => self.credential_type = 
Some(value.into()),
             AzureConfigKey::EncryptionKey => self.encryption_key = 
Some(value.into()),
         };
         self
@@ -654,6 +692,7 @@ impl MicrosoftAzureBuilder {
             AzureConfigKey::FabricWorkloadHost => 
self.fabric_workload_host.clone(),
             AzureConfigKey::FabricSessionToken => 
self.fabric_session_token.clone(),
             AzureConfigKey::FabricClusterIdentifier => 
self.fabric_cluster_identifier.clone(),
+            AzureConfigKey::CredentialType => self.credential_type.clone(),
             AzureConfigKey::EncryptionKey => self.encryption_key.clone(),
         }
     }
@@ -950,6 +989,19 @@ impl MicrosoftAzureBuilder {
         self
     }
 
+    /// Set the credential type to use for authentication.
+    ///
+    /// When multiple credential configurations are present (e.g. both 
workload identity
+    /// and client secret), this forces the builder to use a specific 
credential type
+    /// instead of relying on the default resolution order.
+    ///
+    /// Supported values: `auto`, `bearer_token`, `access_key`, 
`client_secret`,
+    /// `workload_identity`, `sas_token`, `azure_cli`, `managed_identity`.
+    pub fn with_credential_type(mut self, credential_type: impl Into<String>) 
-> Self {
+        self.credential_type = Some(credential_type.into());
+        self
+    }
+
     /// If enabled, [`MicrosoftAzure`] will not fetch credentials and will not 
sign requests
     ///
     /// This can be useful when interacting with public containers
@@ -991,19 +1043,201 @@ impl MicrosoftAzureBuilder {
         self
     }
 
+    fn resolve_credential_auto(
+        &mut self,
+        http: &Arc<dyn HttpConnector>,
+    ) -> Result<AzureCredentialProvider> {
+        if let (
+            Some(fabric_token_service_url),
+            Some(fabric_workload_host),
+            Some(fabric_session_token),
+            Some(fabric_cluster_identifier),
+        ) = (
+            &self.fabric_token_service_url,
+            &self.fabric_workload_host,
+            &self.fabric_session_token,
+            &self.fabric_cluster_identifier,
+        ) {
+            let fabric_credential = FabricTokenOAuthProvider::new(
+                fabric_token_service_url,
+                fabric_workload_host,
+                fabric_session_token,
+                fabric_cluster_identifier,
+                self.bearer_token.clone(),
+            );
+            Ok(Arc::new(TokenCredentialProvider::new(
+                fabric_credential,
+                http.connect(&self.client_options)?,
+                self.retry_config.clone(),
+            )) as _)
+        } else if self.bearer_token.is_some() {
+            self.resolve_bearer_token()
+        } else if self.access_key.is_some() {
+            self.resolve_access_key()
+        } else if self.client_id.is_some()
+            && self.tenant_id.is_some()
+            && self.federated_token_file.is_some()
+        {
+            self.resolve_workload_identity(http)
+        } else if self.client_id.is_some()
+            && self.client_secret.is_some()
+            && self.tenant_id.is_some()
+        {
+            self.resolve_client_secret(http)
+        } else if self.sas_query_pairs.is_some() || self.sas_key.is_some() {
+            self.resolve_sas_token()
+        } else if self.use_azure_cli.get()? {
+            Ok(Arc::new(AzureCliCredential::new()) as _)
+        } else {
+            self.resolve_managed_identity(http)
+        }
+    }
+
+    fn resolve_bearer_token(&mut self) -> Result<AzureCredentialProvider> {
+        let bearer_token = self
+            .bearer_token
+            .take()
+            .ok_or(Error::MissingCredentialConfig {
+                credential_type: "bearer_token".to_string(),
+            })?;
+        Ok(Arc::new(StaticCredentialProvider::new(
+            AzureCredential::BearerToken(bearer_token),
+        )))
+    }
+
+    fn resolve_access_key(&mut self) -> Result<AzureCredentialProvider> {
+        let access_key = self
+            .access_key
+            .take()
+            .ok_or(Error::MissingCredentialConfig {
+                credential_type: "access_key".to_string(),
+            })?;
+        let key = AzureAccessKey::try_new(&access_key)?;
+        Ok(Arc::new(StaticCredentialProvider::new(
+            AzureCredential::AccessKey(key),
+        )))
+    }
+
+    fn resolve_client_secret(
+        &mut self,
+        http: &Arc<dyn HttpConnector>,
+    ) -> Result<AzureCredentialProvider> {
+        let client_id = self
+            .client_id
+            .take()
+            .ok_or(Error::MissingCredentialConfig {
+                credential_type: "client_secret".to_string(),
+            })?;
+        let client_secret = self
+            .client_secret
+            .take()
+            .ok_or(Error::MissingCredentialConfig {
+                credential_type: "client_secret".to_string(),
+            })?;
+        let tenant_id = self
+            .tenant_id
+            .take()
+            .ok_or(Error::MissingCredentialConfig {
+                credential_type: "client_secret".to_string(),
+            })?;
+        let client_credential = ClientSecretOAuthProvider::new(
+            client_id,
+            client_secret,
+            &tenant_id,
+            self.authority_host.take(),
+        );
+        Ok(Arc::new(TokenCredentialProvider::new(
+            client_credential,
+            http.connect(&self.client_options)?,
+            self.retry_config.clone(),
+        )) as _)
+    }
+
+    fn resolve_workload_identity(
+        &mut self,
+        http: &Arc<dyn HttpConnector>,
+    ) -> Result<AzureCredentialProvider> {
+        let client_id = self
+            .client_id
+            .take()
+            .ok_or(Error::MissingCredentialConfig {
+                credential_type: "workload_identity".to_string(),
+            })?;
+        let tenant_id = self
+            .tenant_id
+            .take()
+            .ok_or(Error::MissingCredentialConfig {
+                credential_type: "workload_identity".to_string(),
+            })?;
+        let federated_token_file =
+            self.federated_token_file
+                .take()
+                .ok_or(Error::MissingCredentialConfig {
+                    credential_type: "workload_identity".to_string(),
+                })?;
+        let client_credential = WorkloadIdentityOAuthProvider::new(
+            &client_id,
+            federated_token_file,
+            &tenant_id,
+            self.authority_host.take(),
+        );
+        Ok(Arc::new(TokenCredentialProvider::new(
+            client_credential,
+            http.connect(&self.client_options)?,
+            self.retry_config.clone(),
+        )) as _)
+    }
+
+    fn resolve_sas_token(&mut self) -> Result<AzureCredentialProvider> {
+        if let Some(query_pairs) = self.sas_query_pairs.take() {
+            Ok(Arc::new(StaticCredentialProvider::new(
+                AzureCredential::SASToken(query_pairs),
+            )))
+        } else if let Some(sas) = self.sas_key.take() {
+            Ok(Arc::new(StaticCredentialProvider::new(
+                AzureCredential::SASToken(split_sas(&sas)?),
+            )))
+        } else {
+            Err(Error::MissingCredentialConfig {
+                credential_type: "sas_token".to_string(),
+            }
+            .into())
+        }
+    }
+
+    fn resolve_managed_identity(
+        &mut self,
+        http: &Arc<dyn HttpConnector>,
+    ) -> Result<AzureCredentialProvider> {
+        let msi_credential = ImdsManagedIdentityProvider::new(
+            self.client_id.take(),
+            self.object_id.take(),
+            self.msi_resource_id.take(),
+            self.msi_endpoint.take(),
+        );
+        Ok(Arc::new(TokenCredentialProvider::new(
+            msi_credential,
+            http.connect(&self.client_options.metadata_options())?,
+            self.retry_config.clone(),
+        )) as _)
+    }
+
     /// Configure a connection to container with given name on Microsoft Azure 
Blob store.
     pub fn build(mut self) -> Result<MicrosoftAzure> {
         if let Some(url) = self.url.take() {
             self.parse_url(&url)?;
         }
 
-        let container = self.container_name.ok_or(Error::MissingContainerName 
{})?;
+        let container = self
+            .container_name
+            .take()
+            .ok_or(Error::MissingContainerName {})?;
 
         let static_creds = |credential: AzureCredential| -> 
AzureCredentialProvider {
             Arc::new(StaticCredentialProvider::new(credential))
         };
 
-        let http = http_connector(self.http_connector)?;
+        let http = http_connector(self.http_connector.take())?;
 
         let (is_emulator, storage_url, auth, account) = if 
self.use_emulator.get()? {
             let account_name = self
@@ -1027,8 +1261,8 @@ impl MicrosoftAzureBuilder {
             self.client_options = self.client_options.with_allow_http(true);
             (true, url, static_creds(credential), account_name)
         } else {
-            let account_name = self.account_name.ok_or(Error::MissingAccount 
{})?;
-            let account_url = match self.endpoint {
+            let account_name = 
self.account_name.take().ok_or(Error::MissingAccount {})?;
+            let account_url = match self.endpoint.take() {
                 Some(account_url) => account_url,
                 None => match self.use_fabric_endpoint.get()? {
                     true => {
@@ -1045,81 +1279,25 @@ impl MicrosoftAzureBuilder {
 
             let credential = if let Some(credential) = self.credentials {
                 credential
-            } else if let (
-                Some(fabric_token_service_url),
-                Some(fabric_workload_host),
-                Some(fabric_session_token),
-                Some(fabric_cluster_identifier),
-            ) = (
-                &self.fabric_token_service_url,
-                &self.fabric_workload_host,
-                &self.fabric_session_token,
-                &self.fabric_cluster_identifier,
-            ) {
-                // This case should precede the bearer token case because it 
is more specific and will utilize the bearer token.
-                let fabric_credential = FabricTokenOAuthProvider::new(
-                    fabric_token_service_url,
-                    fabric_workload_host,
-                    fabric_session_token,
-                    fabric_cluster_identifier,
-                    self.bearer_token.clone(),
-                );
-                Arc::new(TokenCredentialProvider::new(
-                    fabric_credential,
-                    http.connect(&self.client_options)?,
-                    self.retry_config.clone(),
-                )) as _
-            } else if let Some(bearer_token) = self.bearer_token {
-                static_creds(AzureCredential::BearerToken(bearer_token))
-            } else if let Some(access_key) = self.access_key {
-                let key = AzureAccessKey::try_new(&access_key)?;
-                static_creds(AzureCredential::AccessKey(key))
-            } else if let (Some(client_id), Some(tenant_id), 
Some(federated_token_file)) =
-                (&self.client_id, &self.tenant_id, self.federated_token_file)
-            {
-                let client_credential = WorkloadIdentityOAuthProvider::new(
-                    client_id,
-                    federated_token_file,
-                    tenant_id,
-                    self.authority_host,
-                );
-                Arc::new(TokenCredentialProvider::new(
-                    client_credential,
-                    http.connect(&self.client_options)?,
-                    self.retry_config.clone(),
-                )) as _
-            } else if let (Some(client_id), Some(client_secret), 
Some(tenant_id)) =
-                (&self.client_id, self.client_secret, &self.tenant_id)
-            {
-                let client_credential = ClientSecretOAuthProvider::new(
-                    client_id.clone(),
-                    client_secret,
-                    tenant_id,
-                    self.authority_host,
-                );
-                Arc::new(TokenCredentialProvider::new(
-                    client_credential,
-                    http.connect(&self.client_options)?,
-                    self.retry_config.clone(),
-                )) as _
-            } else if let Some(query_pairs) = self.sas_query_pairs {
-                static_creds(AzureCredential::SASToken(query_pairs))
-            } else if let Some(sas) = self.sas_key {
-                static_creds(AzureCredential::SASToken(split_sas(&sas)?))
-            } else if self.use_azure_cli.get()? {
-                Arc::new(AzureCliCredential::new()) as _
             } else {
-                let msi_credential = ImdsManagedIdentityProvider::new(
-                    self.client_id,
-                    self.object_id,
-                    self.msi_resource_id,
-                    self.msi_endpoint,
-                );
-                Arc::new(TokenCredentialProvider::new(
-                    msi_credential,
-                    http.connect(&self.client_options.metadata_options())?,
-                    self.retry_config.clone(),
-                )) as _
+                let credential_type = 
self.credential_type.as_deref().unwrap_or("auto");
+
+                match credential_type {
+                    "auto" => self.resolve_credential_auto(&http)?,
+                    "bearer_token" => self.resolve_bearer_token()?,
+                    "access_key" => self.resolve_access_key()?,
+                    "client_secret" => self.resolve_client_secret(&http)?,
+                    "workload_identity" => 
self.resolve_workload_identity(&http)?,
+                    "sas_token" => self.resolve_sas_token()?,
+                    "azure_cli" => Arc::new(AzureCliCredential::new()) as _,
+                    "managed_identity" => 
self.resolve_managed_identity(&http)?,
+                    other => {
+                        return Err(Error::UnknownCredentialType {
+                            credential_type: other.to_string(),
+                        }
+                        .into());
+                    }
+                }
             };
             (false, url, credential, account_name)
         };
@@ -1490,6 +1668,74 @@ mod tests {
         assert_eq!(builder.bearer_token.unwrap(), azure_storage_token);
     }
 
+    #[test]
+    fn azure_test_credential_type_config() {
+        let builder = MicrosoftAzureBuilder::new()
+            .with_config(AzureConfigKey::CredentialType, "client_secret");
+        assert_eq!(builder.credential_type, Some("client_secret".to_string()));
+        assert_eq!(
+            builder
+                .get_config_value(&AzureConfigKey::CredentialType)
+                .unwrap(),
+            "client_secret"
+        );
+
+        let builder =
+            
MicrosoftAzureBuilder::new().with_config("credential_type".parse().unwrap(), 
"auto");
+        assert_eq!(builder.credential_type, Some("auto".to_string()));
+    }
+
+    #[test]
+    fn azure_test_credential_type_client_secret() {
+        let builder = MicrosoftAzureBuilder::new()
+            .with_account("account")
+            .with_container_name("container")
+            .with_client_id("client_id")
+            .with_client_secret("client_secret")
+            .with_tenant_id("tenant_id")
+            .with_federated_token_file("/tmp/token")
+            .with_credential_type("client_secret");
+        // Should succeed — client_secret is forced even though 
workload_identity fields are present
+        let result = builder.build();
+        // Build will fail because it can't actually connect, but it should 
not fail with
+        // a MissingCredentialConfig error — it should get past credential 
resolution
+        assert!(
+            result.is_ok(),
+            "Expected build to succeed with credential_type=client_secret, 
got: {:?}",
+            result.err()
+        );
+    }
+
+    #[test]
+    fn azure_test_credential_type_unknown() {
+        let builder = MicrosoftAzureBuilder::new()
+            .with_account("account")
+            .with_container_name("container")
+            .with_credential_type("invalid_type");
+        let result = builder.build();
+        assert!(result.is_err());
+        let err = result.unwrap_err().to_string();
+        assert!(
+            err.contains("Unknown credential type"),
+            "Expected unknown credential type error, got: {err}"
+        );
+    }
+
+    #[test]
+    fn azure_test_credential_type_missing_config() {
+        let builder = MicrosoftAzureBuilder::new()
+            .with_account("account")
+            .with_container_name("container")
+            .with_credential_type("client_secret");
+        let result = builder.build();
+        assert!(result.is_err());
+        let err = result.unwrap_err().to_string();
+        assert!(
+            err.contains("required configuration is missing"),
+            "Expected missing config error, got: {err}"
+        );
+    }
+
     #[test]
     fn azure_test_split_sas() {
         let raw_sas = 
"?sv=2021-10-04&st=2023-01-04T17%3A48%3A57Z&se=2023-01-04T18%3A15%3A00Z&sr=c&sp=rcwl&sig=C7%2BZeEOWbrxPA3R0Cw%2Fw1EZz0%2B4KBvQexeKZKe%2BB6h0%3D";

Reply via email to