Skip to main content

object_store/azure/
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 crate::azure::client::{AzureClient, AzureConfig, AzureEncryptionHeaders};
19use crate::azure::credential::{
20    AzureAccessKey, AzureCliCredential, ClientSecretOAuthProvider, FabricTokenOAuthProvider,
21    ImdsManagedIdentityProvider, WorkloadIdentityOAuthProvider,
22};
23use crate::azure::{AzureCredential, AzureCredentialProvider, MicrosoftAzure, STORE};
24use crate::client::{CryptoProvider, HttpConnector, TokenCredentialProvider, http_connector};
25use crate::config::ConfigValue;
26use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider};
27use percent_encoding::percent_decode_str;
28use serde::{Deserialize, Serialize};
29use std::str::FromStr;
30use std::sync::Arc;
31use url::Url;
32
33/// The well-known account used by Azurite and the legacy Azure Storage Emulator.
34///
35/// <https://docs.microsoft.com/azure/storage/common/storage-use-azurite#well-known-storage-account-and-key>
36const EMULATOR_ACCOUNT: &str = "devstoreaccount1";
37
38/// The well-known account key used by Azurite and the legacy Azure Storage Emulator.
39///
40/// <https://docs.microsoft.com/azure/storage/common/storage-use-azurite#well-known-storage-account-and-key>
41const EMULATOR_ACCOUNT_KEY: &str =
42    "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
43
44const MSI_ENDPOINT_ENV_KEY: &str = "IDENTITY_ENDPOINT";
45
46/// A specialized `Error` for Azure builder-related errors
47#[derive(Debug, thiserror::Error)]
48enum Error {
49    #[error("Unable parse source url. Url: {}, Error: {}", url, source)]
50    UnableToParseUrl {
51        source: url::ParseError,
52        url: String,
53    },
54
55    #[error(
56        "Unable parse emulator url {}={}, Error: {}",
57        env_name,
58        env_value,
59        source
60    )]
61    UnableToParseEmulatorUrl {
62        env_name: String,
63        env_value: String,
64        source: url::ParseError,
65    },
66
67    #[error("Account must be specified")]
68    MissingAccount {},
69
70    #[error("Container name must be specified")]
71    MissingContainerName {},
72
73    #[error(
74        "Unknown url scheme cannot be parsed into storage location: {}",
75        scheme
76    )]
77    UnknownUrlScheme { scheme: String },
78
79    #[error("URL did not match any known pattern for scheme: {}", url)]
80    UrlNotRecognised { url: String },
81
82    #[error("Failed parsing an SAS key")]
83    DecodeSasKey { source: std::str::Utf8Error },
84
85    #[error("Missing component in SAS query pair")]
86    MissingSasComponent {},
87
88    #[error("Invalid encryption key: {source}")]
89    InvalidEncryptionKey {
90        source: Box<dyn std::error::Error + Send + Sync + 'static>,
91    },
92
93    #[error("Configuration key: '{}' is not known.", key)]
94    UnknownConfigurationKey { key: String },
95
96    #[error(
97        "Unknown credential type: '{}'. Supported values: auto, bearer_token, access_key, client_secret, workload_identity, sas_token, azure_cli, managed_identity",
98        credential_type
99    )]
100    UnknownCredentialType { credential_type: String },
101
102    #[error(
103        "Credential type '{}' was requested but required configuration is missing",
104        credential_type
105    )]
106    MissingCredentialConfig { credential_type: String },
107}
108
109impl From<Error> for crate::Error {
110    fn from(source: Error) -> Self {
111        match source {
112            Error::UnknownConfigurationKey { key } => {
113                Self::UnknownConfigurationKey { store: STORE, key }
114            }
115            _ => Self::Generic {
116                store: STORE,
117                source: Box::new(source),
118            },
119        }
120    }
121}
122
123/// Configure a connection to Microsoft Azure Blob Storage container using
124/// the specified credentials.
125///
126/// # Example
127/// ```
128/// # let ACCOUNT = "foo";
129/// # let BUCKET_NAME = "foo";
130/// # let ACCESS_KEY = "foo";
131/// # use object_store::azure::MicrosoftAzureBuilder;
132/// let azure = MicrosoftAzureBuilder::new()
133///  .with_account(ACCOUNT)
134///  .with_access_key(ACCESS_KEY)
135///  .with_container_name(BUCKET_NAME)
136///  .build();
137/// ```
138#[derive(Default, Clone)]
139pub struct MicrosoftAzureBuilder {
140    /// Account name
141    account_name: Option<String>,
142    /// Access key
143    access_key: Option<String>,
144    /// Container name
145    container_name: Option<String>,
146    /// Bearer token
147    bearer_token: Option<String>,
148    /// Client id
149    client_id: Option<String>,
150    /// Client secret
151    client_secret: Option<String>,
152    /// Tenant id
153    tenant_id: Option<String>,
154    /// Query pairs for shared access signature authorization
155    sas_query_pairs: Option<Vec<(String, String)>>,
156    /// Shared access signature
157    sas_key: Option<String>,
158    /// Authority host
159    authority_host: Option<String>,
160    /// Url
161    url: Option<String>,
162    /// When set to true, azurite storage emulator has to be used
163    use_emulator: ConfigValue<bool>,
164    /// Storage endpoint
165    endpoint: Option<String>,
166    /// Msi endpoint for acquiring managed identity token
167    msi_endpoint: Option<String>,
168    /// Object id for use with managed identity authentication
169    object_id: Option<String>,
170    /// Msi resource id for use with managed identity authentication
171    msi_resource_id: Option<String>,
172    /// File containing token for Azure AD workload identity federation
173    federated_token_file: Option<String>,
174    /// When set to true, azure cli has to be used for acquiring access token
175    use_azure_cli: ConfigValue<bool>,
176    /// Retry config
177    retry_config: RetryConfig,
178    /// Client options
179    client_options: ClientOptions,
180    /// Credentials
181    credentials: Option<AzureCredentialProvider>,
182    /// The [`CryptoProvider`] to use
183    crypto: Option<Arc<dyn CryptoProvider>>,
184    /// Skip signing requests
185    skip_signature: ConfigValue<bool>,
186    /// When set to true, fabric url scheme will be used
187    ///
188    /// i.e. https://{account_name}.dfs.fabric.microsoft.com
189    use_fabric_endpoint: ConfigValue<bool>,
190    /// When set to true, skips tagging objects
191    disable_tagging: ConfigValue<bool>,
192    /// Fabric token service url
193    fabric_token_service_url: Option<String>,
194    /// Fabric workload host
195    fabric_workload_host: Option<String>,
196    /// Fabric session token
197    fabric_session_token: Option<String>,
198    /// Fabric cluster identifier
199    fabric_cluster_identifier: Option<String>,
200    /// Credential type override
201    credential_type: Option<String>,
202    /// Base64-encoded 256-bit customer-provided encryption key
203    encryption_key: Option<String>,
204    /// The [`HttpConnector`] to use
205    http_connector: Option<Arc<dyn HttpConnector>>,
206}
207
208/// Configuration keys for [`MicrosoftAzureBuilder`]
209///
210/// Configuration via keys can be done via [`MicrosoftAzureBuilder::with_config`]
211///
212/// # Example
213/// ```
214/// # use object_store::azure::{MicrosoftAzureBuilder, AzureConfigKey};
215/// let builder = MicrosoftAzureBuilder::new()
216///     .with_config("azure_client_id".parse().unwrap(), "my-client-id")
217///     .with_config(AzureConfigKey::AuthorityId, "my-tenant-id");
218/// ```
219#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Deserialize, Serialize)]
220#[non_exhaustive]
221pub enum AzureConfigKey {
222    /// The name of the azure storage account
223    ///
224    /// Supported keys:
225    /// - `azure_storage_account_name`
226    /// - `account_name`
227    AccountName,
228
229    /// Master key for accessing storage account
230    ///
231    /// Supported keys:
232    /// - `azure_storage_account_key`
233    /// - `azure_storage_access_key`
234    /// - `azure_storage_master_key`
235    /// - `access_key`
236    /// - `account_key`
237    /// - `master_key`
238    AccessKey,
239
240    /// Service principal client id for authorizing requests
241    ///
242    /// Supported keys:
243    /// - `azure_storage_client_id`
244    /// - `azure_client_id`
245    /// - `client_id`
246    ClientId,
247
248    /// Service principal client secret for authorizing requests
249    ///
250    /// Supported keys:
251    /// - `azure_storage_client_secret`
252    /// - `azure_client_secret`
253    /// - `client_secret`
254    ClientSecret,
255
256    /// Tenant id used in oauth flows
257    ///
258    /// Supported keys:
259    /// - `azure_storage_tenant_id`
260    /// - `azure_storage_authority_id`
261    /// - `azure_tenant_id`
262    /// - `azure_authority_id`
263    /// - `tenant_id`
264    /// - `authority_id`
265    AuthorityId,
266
267    /// Authority host used in oauth flows
268    ///
269    /// Supported keys:
270    /// - `azure_storage_authority_host`
271    /// - `azure_authority_host`
272    /// - `authority_host`
273    AuthorityHost,
274
275    /// Shared access signature.
276    ///
277    /// The signature is expected to be percent-encoded, much like they are provided
278    /// in the azure storage explorer or azure portal.
279    ///
280    /// Supported keys:
281    /// - `azure_storage_sas_key`
282    /// - `azure_storage_sas_token`
283    /// - `sas_key`
284    /// - `sas_token`
285    SasKey,
286
287    /// Bearer token
288    ///
289    /// Supported keys:
290    /// - `azure_storage_token`
291    /// - `bearer_token`
292    /// - `token`
293    Token,
294
295    /// Use object store with azurite storage emulator
296    ///
297    /// Supported keys:
298    /// - `azure_storage_use_emulator`
299    /// - `object_store_use_emulator`
300    /// - `use_emulator`
301    UseEmulator,
302
303    /// Override the endpoint used to communicate with blob storage
304    ///
305    /// Supported keys:
306    /// - `azure_storage_endpoint`
307    /// - `azure_endpoint`
308    /// - `endpoint`
309    Endpoint,
310
311    /// Use object store with url scheme account.dfs.fabric.microsoft.com
312    ///
313    /// Supported keys:
314    /// - `azure_use_fabric_endpoint`
315    /// - `use_fabric_endpoint`
316    UseFabricEndpoint,
317
318    /// Endpoint to request a imds managed identity token
319    ///
320    /// Supported keys:
321    /// - `azure_msi_endpoint`
322    /// - `azure_identity_endpoint`
323    /// - `identity_endpoint`
324    /// - `msi_endpoint`
325    MsiEndpoint,
326
327    /// Object id for use with managed identity authentication
328    ///
329    /// Supported keys:
330    /// - `azure_object_id`
331    /// - `object_id`
332    ObjectId,
333
334    /// Msi resource id for use with managed identity authentication
335    ///
336    /// Supported keys:
337    /// - `azure_msi_resource_id`
338    /// - `msi_resource_id`
339    MsiResourceId,
340
341    /// File containing token for Azure AD workload identity federation
342    ///
343    /// Supported keys:
344    /// - `azure_federated_token_file`
345    /// - `federated_token_file`
346    FederatedTokenFile,
347
348    /// Use azure cli for acquiring access token
349    ///
350    /// Supported keys:
351    /// - `azure_use_azure_cli`
352    /// - `use_azure_cli`
353    UseAzureCli,
354
355    /// Skip signing requests
356    ///
357    /// Supported keys:
358    /// - `azure_skip_signature`
359    /// - `skip_signature`
360    SkipSignature,
361
362    /// Container name
363    ///
364    /// Supported keys:
365    /// - `azure_container_name`
366    /// - `container_name`
367    ContainerName,
368
369    /// Disables tagging objects
370    ///
371    /// This can be desirable if not supported by the backing store
372    ///
373    /// Supported keys:
374    /// - `azure_disable_tagging`
375    /// - `disable_tagging`
376    DisableTagging,
377
378    /// Fabric token service url
379    ///
380    /// Supported keys:
381    /// - `azure_fabric_token_service_url`
382    /// - `fabric_token_service_url`
383    FabricTokenServiceUrl,
384
385    /// Fabric workload host
386    ///
387    /// Supported keys:
388    /// - `azure_fabric_workload_host`
389    /// - `fabric_workload_host`
390    FabricWorkloadHost,
391
392    /// Fabric session token
393    ///
394    /// Supported keys:
395    /// - `azure_fabric_session_token`
396    /// - `fabric_session_token`
397    FabricSessionToken,
398
399    /// Fabric cluster identifier
400    ///
401    /// Supported keys:
402    /// - `azure_fabric_cluster_identifier`
403    /// - `fabric_cluster_identifier`
404    FabricClusterIdentifier,
405
406    /// Credential type to use for authentication
407    ///
408    /// When multiple credential configurations are present, this key forces
409    /// the builder to use a specific credential type instead of relying on
410    /// the default resolution order.
411    ///
412    /// Supported values:
413    /// - `auto` (default) — use the built-in priority chain
414    /// - `bearer_token` — use a static bearer token
415    /// - `access_key` — use an access key
416    /// - `client_secret` — use client secret (service principal) OAuth
417    /// - `workload_identity` — use workload identity federation
418    /// - `sas_token` — use a shared access signature
419    /// - `azure_cli` — use Azure CLI
420    /// - `managed_identity` — use IMDS managed identity
421    ///
422    /// Supported keys:
423    /// - `azure_credential_type`
424    /// - `credential_type`
425    CredentialType,
426
427    /// Base64-encoded customer-provided encryption key
428    ///
429    /// Supported keys:
430    /// - `azure_storage_encryption_key`
431    /// - `encryption_key`
432    EncryptionKey,
433
434    /// Client options
435    Client(ClientConfigKey),
436}
437
438impl AsRef<str> for AzureConfigKey {
439    fn as_ref(&self) -> &str {
440        match self {
441            Self::AccountName => "azure_storage_account_name",
442            Self::AccessKey => "azure_storage_account_key",
443            Self::ClientId => "azure_storage_client_id",
444            Self::ClientSecret => "azure_storage_client_secret",
445            Self::AuthorityId => "azure_storage_tenant_id",
446            Self::AuthorityHost => "azure_storage_authority_host",
447            Self::SasKey => "azure_storage_sas_key",
448            Self::Token => "azure_storage_token",
449            Self::UseEmulator => "azure_storage_use_emulator",
450            Self::UseFabricEndpoint => "azure_use_fabric_endpoint",
451            Self::Endpoint => "azure_storage_endpoint",
452            Self::MsiEndpoint => "azure_msi_endpoint",
453            Self::ObjectId => "azure_object_id",
454            Self::MsiResourceId => "azure_msi_resource_id",
455            Self::FederatedTokenFile => "azure_federated_token_file",
456            Self::UseAzureCli => "azure_use_azure_cli",
457            Self::SkipSignature => "azure_skip_signature",
458            Self::ContainerName => "azure_container_name",
459            Self::DisableTagging => "azure_disable_tagging",
460            Self::FabricTokenServiceUrl => "azure_fabric_token_service_url",
461            Self::FabricWorkloadHost => "azure_fabric_workload_host",
462            Self::FabricSessionToken => "azure_fabric_session_token",
463            Self::FabricClusterIdentifier => "azure_fabric_cluster_identifier",
464            Self::CredentialType => "azure_credential_type",
465            Self::EncryptionKey => "azure_storage_encryption_key",
466            Self::Client(key) => key.as_ref(),
467        }
468    }
469}
470
471impl FromStr for AzureConfigKey {
472    type Err = crate::Error;
473
474    fn from_str(s: &str) -> Result<Self, Self::Err> {
475        match s {
476            "azure_storage_account_key"
477            | "azure_storage_access_key"
478            | "azure_storage_master_key"
479            | "master_key"
480            | "account_key"
481            | "access_key" => Ok(Self::AccessKey),
482            "azure_storage_account_name" | "account_name" => Ok(Self::AccountName),
483            "azure_storage_client_id" | "azure_client_id" | "client_id" => Ok(Self::ClientId),
484            "azure_storage_client_secret" | "azure_client_secret" | "client_secret" => {
485                Ok(Self::ClientSecret)
486            }
487            "azure_storage_tenant_id"
488            | "azure_storage_authority_id"
489            | "azure_tenant_id"
490            | "azure_authority_id"
491            | "tenant_id"
492            | "authority_id" => Ok(Self::AuthorityId),
493            "azure_storage_authority_host" | "azure_authority_host" | "authority_host" => {
494                Ok(Self::AuthorityHost)
495            }
496            "azure_storage_sas_key" | "azure_storage_sas_token" | "sas_key" | "sas_token" => {
497                Ok(Self::SasKey)
498            }
499            "azure_storage_token" | "bearer_token" | "token" => Ok(Self::Token),
500            "azure_storage_use_emulator" | "use_emulator" => Ok(Self::UseEmulator),
501            "azure_storage_endpoint" | "azure_endpoint" | "endpoint" => Ok(Self::Endpoint),
502            "azure_msi_endpoint"
503            | "azure_identity_endpoint"
504            | "identity_endpoint"
505            | "msi_endpoint" => Ok(Self::MsiEndpoint),
506            "azure_object_id" | "object_id" => Ok(Self::ObjectId),
507            "azure_msi_resource_id" | "msi_resource_id" => Ok(Self::MsiResourceId),
508            "azure_federated_token_file" | "federated_token_file" => Ok(Self::FederatedTokenFile),
509            "azure_use_fabric_endpoint" | "use_fabric_endpoint" => Ok(Self::UseFabricEndpoint),
510            "azure_use_azure_cli" | "use_azure_cli" => Ok(Self::UseAzureCli),
511            "azure_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
512            "azure_container_name" | "container_name" => Ok(Self::ContainerName),
513            "azure_disable_tagging" | "disable_tagging" => Ok(Self::DisableTagging),
514            "azure_fabric_token_service_url" | "fabric_token_service_url" => {
515                Ok(Self::FabricTokenServiceUrl)
516            }
517            "azure_fabric_workload_host" | "fabric_workload_host" => Ok(Self::FabricWorkloadHost),
518            "azure_fabric_session_token" | "fabric_session_token" => Ok(Self::FabricSessionToken),
519            "azure_fabric_cluster_identifier" | "fabric_cluster_identifier" => {
520                Ok(Self::FabricClusterIdentifier)
521            }
522            "azure_credential_type" | "credential_type" => Ok(Self::CredentialType),
523            "azure_storage_encryption_key" | "encryption_key" => Ok(Self::EncryptionKey),
524            // Backwards compatibility
525            "azure_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
526            _ => match s.strip_prefix("azure_").unwrap_or(s).parse() {
527                Ok(key) => Ok(Self::Client(key)),
528                Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
529            },
530        }
531    }
532}
533
534impl std::fmt::Debug for MicrosoftAzureBuilder {
535    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536        write!(
537            f,
538            "MicrosoftAzureBuilder {{ account: {:?}, container_name: {:?} }}",
539            self.account_name, self.container_name
540        )
541    }
542}
543
544impl MicrosoftAzureBuilder {
545    /// Create a new [`MicrosoftAzureBuilder`] with default values.
546    pub fn new() -> Self {
547        Default::default()
548    }
549
550    /// Create an instance of [`MicrosoftAzureBuilder`] with values pre-populated from environment variables.
551    ///
552    /// Variables extracted from environment:
553    /// * AZURE_STORAGE_ACCOUNT_NAME: storage account name
554    /// * AZURE_STORAGE_ACCOUNT_KEY: storage account master key
555    /// * AZURE_STORAGE_ACCESS_KEY: alias for AZURE_STORAGE_ACCOUNT_KEY
556    /// * AZURE_STORAGE_CLIENT_ID -> client id for service principal authorization
557    /// * AZURE_STORAGE_CLIENT_SECRET -> client secret for service principal authorization
558    /// * AZURE_STORAGE_TENANT_ID -> tenant id used in oauth flows
559    /// # Example
560    /// ```
561    /// use object_store::azure::MicrosoftAzureBuilder;
562    ///
563    /// let azure = MicrosoftAzureBuilder::from_env()
564    ///     .with_container_name("foo")
565    ///     .build();
566    /// ```
567    pub fn from_env() -> Self {
568        let mut builder = Self::default();
569        for (os_key, os_value) in std::env::vars_os() {
570            if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
571                if key.starts_with("AZURE_") {
572                    if let Ok(config_key) = key.to_ascii_lowercase().parse() {
573                        builder = builder.with_config(config_key, value);
574                    }
575                }
576            }
577        }
578
579        if let Ok(text) = std::env::var(MSI_ENDPOINT_ENV_KEY) {
580            builder = builder.with_msi_endpoint(text);
581        }
582
583        builder
584    }
585
586    /// Parse available connection info form a well-known storage URL.
587    ///
588    /// The supported url schemes are:
589    ///
590    /// - `abfs[s]://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
591    /// - `abfs[s]://<file_system>@<account_name>.dfs.core.windows.net/<path>`
592    /// - `abfs[s]://<file_system>@<account_name>.dfs.fabric.microsoft.com/<path>`
593    /// - `az://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
594    /// - `adl://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
595    /// - `azure://<container>/<path>` (custom)
596    /// - `https://<account>.dfs.core.windows.net`
597    /// - `https://<account>.blob.core.windows.net`
598    /// - `https://<account>.blob.core.windows.net/<container>`
599    /// - `https://<account>.dfs.fabric.microsoft.com`
600    /// - `https://<account>.dfs.fabric.microsoft.com/<container>`
601    /// - `https://<account>.blob.fabric.microsoft.com`
602    /// - `https://<account>.blob.fabric.microsoft.com/<container>`
603    ///
604    /// Note: Settings derived from the URL will override any others set on this builder
605    ///
606    /// # Example
607    /// ```
608    /// use object_store::azure::MicrosoftAzureBuilder;
609    ///
610    /// let azure = MicrosoftAzureBuilder::from_env()
611    ///     .with_url("abfss://file_system@account.dfs.core.windows.net/")
612    ///     .build();
613    /// ```
614    pub fn with_url(mut self, url: impl Into<String>) -> Self {
615        self.url = Some(url.into());
616        self
617    }
618
619    /// Set an option on the builder via a key - value pair.
620    pub fn with_config(mut self, key: AzureConfigKey, value: impl Into<String>) -> Self {
621        match key {
622            AzureConfigKey::AccessKey => self.access_key = Some(value.into()),
623            AzureConfigKey::AccountName => self.account_name = Some(value.into()),
624            AzureConfigKey::ClientId => self.client_id = Some(value.into()),
625            AzureConfigKey::ClientSecret => self.client_secret = Some(value.into()),
626            AzureConfigKey::AuthorityId => self.tenant_id = Some(value.into()),
627            AzureConfigKey::AuthorityHost => self.authority_host = Some(value.into()),
628            AzureConfigKey::SasKey => self.sas_key = Some(value.into()),
629            AzureConfigKey::Token => self.bearer_token = Some(value.into()),
630            AzureConfigKey::MsiEndpoint => self.msi_endpoint = Some(value.into()),
631            AzureConfigKey::ObjectId => self.object_id = Some(value.into()),
632            AzureConfigKey::MsiResourceId => self.msi_resource_id = Some(value.into()),
633            AzureConfigKey::FederatedTokenFile => self.federated_token_file = Some(value.into()),
634            AzureConfigKey::UseAzureCli => self.use_azure_cli.parse(value),
635            AzureConfigKey::SkipSignature => self.skip_signature.parse(value),
636            AzureConfigKey::UseEmulator => self.use_emulator.parse(value),
637            AzureConfigKey::Endpoint => self.endpoint = Some(value.into()),
638            AzureConfigKey::UseFabricEndpoint => self.use_fabric_endpoint.parse(value),
639            AzureConfigKey::Client(key) => {
640                self.client_options = self.client_options.with_config(key, value)
641            }
642            AzureConfigKey::ContainerName => self.container_name = Some(value.into()),
643            AzureConfigKey::DisableTagging => self.disable_tagging.parse(value),
644            AzureConfigKey::FabricTokenServiceUrl => {
645                self.fabric_token_service_url = Some(value.into())
646            }
647            AzureConfigKey::FabricWorkloadHost => self.fabric_workload_host = Some(value.into()),
648            AzureConfigKey::FabricSessionToken => self.fabric_session_token = Some(value.into()),
649            AzureConfigKey::FabricClusterIdentifier => {
650                self.fabric_cluster_identifier = Some(value.into())
651            }
652            AzureConfigKey::CredentialType => self.credential_type = Some(value.into()),
653            AzureConfigKey::EncryptionKey => self.encryption_key = Some(value.into()),
654        };
655        self
656    }
657
658    /// Get config value via a [`AzureConfigKey`].
659    ///
660    /// # Example
661    /// ```
662    /// use object_store::azure::{MicrosoftAzureBuilder, AzureConfigKey};
663    ///
664    /// let builder = MicrosoftAzureBuilder::from_env()
665    ///     .with_account("foo");
666    /// let account_name = builder.get_config_value(&AzureConfigKey::AccountName).unwrap_or_default();
667    /// assert_eq!("foo", &account_name);
668    /// ```
669    pub fn get_config_value(&self, key: &AzureConfigKey) -> Option<String> {
670        match key {
671            AzureConfigKey::AccountName => self.account_name.clone(),
672            AzureConfigKey::AccessKey => self.access_key.clone(),
673            AzureConfigKey::ClientId => self.client_id.clone(),
674            AzureConfigKey::ClientSecret => self.client_secret.clone(),
675            AzureConfigKey::AuthorityId => self.tenant_id.clone(),
676            AzureConfigKey::AuthorityHost => self.authority_host.clone(),
677            AzureConfigKey::SasKey => self.sas_key.clone(),
678            AzureConfigKey::Token => self.bearer_token.clone(),
679            AzureConfigKey::UseEmulator => Some(self.use_emulator.to_string()),
680            AzureConfigKey::UseFabricEndpoint => Some(self.use_fabric_endpoint.to_string()),
681            AzureConfigKey::Endpoint => self.endpoint.clone(),
682            AzureConfigKey::MsiEndpoint => self.msi_endpoint.clone(),
683            AzureConfigKey::ObjectId => self.object_id.clone(),
684            AzureConfigKey::MsiResourceId => self.msi_resource_id.clone(),
685            AzureConfigKey::FederatedTokenFile => self.federated_token_file.clone(),
686            AzureConfigKey::UseAzureCli => Some(self.use_azure_cli.to_string()),
687            AzureConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
688            AzureConfigKey::Client(key) => self.client_options.get_config_value(key),
689            AzureConfigKey::ContainerName => self.container_name.clone(),
690            AzureConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
691            AzureConfigKey::FabricTokenServiceUrl => self.fabric_token_service_url.clone(),
692            AzureConfigKey::FabricWorkloadHost => self.fabric_workload_host.clone(),
693            AzureConfigKey::FabricSessionToken => self.fabric_session_token.clone(),
694            AzureConfigKey::FabricClusterIdentifier => self.fabric_cluster_identifier.clone(),
695            AzureConfigKey::CredentialType => self.credential_type.clone(),
696            AzureConfigKey::EncryptionKey => self.encryption_key.clone(),
697        }
698    }
699
700    /// Sets properties on this builder based on a URL
701    ///
702    /// This is a separate member function to allow fallible computation to
703    /// be deferred until [`Self::build`] which in turn allows deriving [`Clone`]
704    fn parse_url(&mut self, url: &str) -> Result<()> {
705        let parsed = Url::parse(url).map_err(|source| {
706            let url = url.into();
707            Error::UnableToParseUrl { url, source }
708        })?;
709
710        let host = parsed
711            .host_str()
712            .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
713
714        let validate = |s: &str| match s.contains('.') {
715            true => Err(Error::UrlNotRecognised { url: url.into() }),
716            false => Ok(s.to_string()),
717        };
718
719        match parsed.scheme() {
720            "adl" | "azure" => self.container_name = Some(validate(host)?),
721            "az" | "abfs" | "abfss" => {
722                // abfs(s) might refer to the fsspec convention abfs://<container>/<path>
723                // or the convention for the hadoop driver abfs[s]://<file_system>@<account_name>.dfs.core.windows.net/<path>
724                if parsed.username().is_empty() {
725                    self.container_name = Some(validate(host)?);
726                } else {
727                    match host.split_once('.') {
728                        // Workspace-level Private Link detection
729                        // "{workspaceid}.z??.(onelake|dfs|blob).fabric.microsoft.com"
730                        Some((workspaceid, rest))
731                            if rest.starts_with('z') && rest.ends_with("fabric.microsoft.com") =>
732                        {
733                            // Account name for WS-PL is two labels: "{workspaceid}.z{xy}"
734                            let (zone, _) = rest.split_once('.').unwrap_or((rest, ""));
735
736                            self.account_name = Some(format!("{workspaceid}.{zone}"));
737                            self.endpoint = Some(format!("https://{}", host));
738
739                            self.container_name = Some(validate(parsed.username())?);
740                            self.use_fabric_endpoint = true.into();
741                        }
742
743                        Some((a, "dfs.core.windows.net")) | Some((a, "blob.core.windows.net")) => {
744                            self.account_name = Some(validate(a)?);
745                            self.container_name = Some(validate(parsed.username())?);
746                        }
747
748                        Some((a, "dfs.fabric.microsoft.com"))
749                        | Some((a, "blob.fabric.microsoft.com")) => {
750                            self.account_name = Some(validate(a)?);
751                            self.container_name = Some(validate(parsed.username())?);
752                            self.use_fabric_endpoint = true.into();
753                        }
754                        _ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
755                    }
756                }
757            }
758            "https" => match host.split_once('.') {
759                // Workspace-level Private Link detection
760                // "{workspaceid}.z??.(onelake|dfs|blob).fabric.microsoft.com"
761                Some((workspaceid, rest))
762                    if rest.starts_with('z') && rest.ends_with("fabric.microsoft.com") =>
763                {
764                    // rest looks like: "z28.dfs.fabric.microsoft.com" / "z28.blob.fabric.microsoft.com" / etc.
765                    // Account name for WS-PL is two labels: "{workspaceid}.z{xy}"
766                    let (zone, _) = rest.split_once('.').unwrap_or((rest, ""));
767
768                    self.account_name = Some(format!("{workspaceid}.{zone}"));
769                    self.endpoint = Some(format!("https://{}", host));
770
771                    // Attempt to infer the container name from the URL
772                    let container = parsed.path_segments().unwrap().next().expect(
773                        "iterator always contains at least one string (which may be empty)",
774                    );
775
776                    if !container.is_empty() {
777                        self.container_name = Some(validate(container)?);
778                    }
779
780                    self.use_fabric_endpoint = true.into();
781                }
782
783                Some((a, "dfs.core.windows.net")) | Some((a, "blob.core.windows.net")) => {
784                    self.account_name = Some(validate(a)?);
785                    let container = parsed.path_segments().unwrap().next().expect(
786                        "iterator always contains at least one string (which may be empty)",
787                    );
788                    if !container.is_empty() {
789                        self.container_name = Some(validate(container)?);
790                    }
791                }
792                Some((a, "dfs.fabric.microsoft.com")) | Some((a, "blob.fabric.microsoft.com")) => {
793                    self.account_name = Some(validate(a)?);
794                    // Attempt to infer the container name from the URL
795                    // - https://onelake.dfs.fabric.microsoft.com/<workspaceGUID>/<itemGUID>/Files/test.csv
796                    // - https://onelake.dfs.fabric.microsoft.com/<workspace>/<item>.<itemtype>/<path>/<fileName>
797                    //
798                    // See <https://learn.microsoft.com/en-us/fabric/onelake/onelake-access-api>
799                    let workspace = parsed.path_segments().unwrap().next().expect(
800                        "iterator always contains at least one string (which may be empty)",
801                    );
802                    if !workspace.is_empty() {
803                        self.container_name = Some(workspace.to_string())
804                    }
805                    self.use_fabric_endpoint = true.into();
806                }
807                _ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
808            },
809            scheme => {
810                let scheme = scheme.into();
811                return Err(Error::UnknownUrlScheme { scheme }.into());
812            }
813        }
814        Ok(())
815    }
816
817    /// Set the Azure Account (required)
818    pub fn with_account(mut self, account: impl Into<String>) -> Self {
819        self.account_name = Some(account.into());
820        self
821    }
822
823    /// Set the Azure Container Name (required)
824    pub fn with_container_name(mut self, container_name: impl Into<String>) -> Self {
825        self.container_name = Some(container_name.into());
826        self
827    }
828
829    /// Set the Azure Access Key (required - one of access key, bearer token, or client credentials)
830    pub fn with_access_key(mut self, access_key: impl Into<String>) -> Self {
831        self.access_key = Some(access_key.into());
832        self
833    }
834
835    /// Set a static bearer token to be used for authorizing requests
836    pub fn with_bearer_token_authorization(mut self, bearer_token: impl Into<String>) -> Self {
837        self.bearer_token = Some(bearer_token.into());
838        self
839    }
840
841    /// Set a client secret used for client secret authorization
842    pub fn with_client_secret_authorization(
843        mut self,
844        client_id: impl Into<String>,
845        client_secret: impl Into<String>,
846        tenant_id: impl Into<String>,
847    ) -> Self {
848        self.client_id = Some(client_id.into());
849        self.client_secret = Some(client_secret.into());
850        self.tenant_id = Some(tenant_id.into());
851        self
852    }
853
854    /// Sets the client id for use in client secret or k8s federated credential flow
855    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
856        self.client_id = Some(client_id.into());
857        self
858    }
859
860    /// Sets the client secret for use in client secret flow
861    pub fn with_client_secret(mut self, client_secret: impl Into<String>) -> Self {
862        self.client_secret = Some(client_secret.into());
863        self
864    }
865
866    /// Sets the tenant id for use in client secret or k8s federated credential flow
867    pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
868        self.tenant_id = Some(tenant_id.into());
869        self
870    }
871
872    /// Set query pairs appended to the url for shared access signature authorization
873    pub fn with_sas_authorization(mut self, query_pairs: impl Into<Vec<(String, String)>>) -> Self {
874        self.sas_query_pairs = Some(query_pairs.into());
875        self
876    }
877
878    /// Set the credential provider overriding any other options
879    pub fn with_credentials(mut self, credentials: AzureCredentialProvider) -> Self {
880        self.credentials = Some(credentials);
881        self
882    }
883
884    /// The [`CryptoProvider`] to use
885    pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>) -> Self {
886        self.crypto = Some(provider);
887        self
888    }
889
890    /// Set if the Azure emulator should be used (defaults to false)
891    pub fn with_use_emulator(mut self, use_emulator: bool) -> Self {
892        self.use_emulator = use_emulator.into();
893        self
894    }
895
896    /// Override the endpoint used to communicate with blob storage
897    ///
898    /// Defaults to `https://{account}.blob.core.windows.net`
899    ///
900    /// By default, only HTTPS schemes are enabled. To connect to an HTTP endpoint, enable
901    /// [`Self::with_allow_http`].
902    pub fn with_endpoint(mut self, endpoint: String) -> Self {
903        self.endpoint = Some(endpoint);
904        self
905    }
906
907    /// Set if Microsoft Fabric url scheme should be used (defaults to false)
908    ///
909    /// When disabled the url scheme used is `https://{account}.blob.core.windows.net`
910    /// When enabled the url scheme used is `https://{account}.dfs.fabric.microsoft.com`
911    ///
912    /// Note: [`Self::with_endpoint`] will take precedence over this option
913    pub fn with_use_fabric_endpoint(mut self, use_fabric_endpoint: bool) -> Self {
914        self.use_fabric_endpoint = use_fabric_endpoint.into();
915        self
916    }
917
918    /// Sets what protocol is allowed
919    ///
920    /// If `allow_http` is :
921    /// * false (default):  Only HTTPS are allowed
922    /// * true:  HTTP and HTTPS are allowed
923    pub fn with_allow_http(mut self, allow_http: bool) -> Self {
924        self.client_options = self.client_options.with_allow_http(allow_http);
925        self
926    }
927
928    /// Sets an alternative authority host for OAuth based authorization
929    ///
930    /// Common hosts for azure clouds are defined in [authority_hosts](crate::azure::authority_hosts).
931    ///
932    /// Defaults to <https://login.microsoftonline.com>
933    pub fn with_authority_host(mut self, authority_host: impl Into<String>) -> Self {
934        self.authority_host = Some(authority_host.into());
935        self
936    }
937
938    /// Set the retry configuration
939    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
940        self.retry_config = retry_config;
941        self
942    }
943
944    /// Set the proxy_url to be used by the underlying client
945    pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
946        self.client_options = self.client_options.with_proxy_url(proxy_url);
947        self
948    }
949
950    /// Set a trusted proxy CA certificate
951    pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
952        self.client_options = self
953            .client_options
954            .with_proxy_ca_certificate(proxy_ca_certificate);
955        self
956    }
957
958    /// Set a list of hosts to exclude from proxy connections
959    pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
960        self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
961        self
962    }
963
964    /// Sets the client options, overriding any already set
965    pub fn with_client_options(mut self, options: ClientOptions) -> Self {
966        self.client_options = options;
967        self
968    }
969
970    /// Sets the endpoint for acquiring managed identity token
971    pub fn with_msi_endpoint(mut self, msi_endpoint: impl Into<String>) -> Self {
972        self.msi_endpoint = Some(msi_endpoint.into());
973        self
974    }
975
976    /// Sets a file path for acquiring azure federated identity token in k8s
977    ///
978    /// requires `client_id` and `tenant_id` to be set
979    pub fn with_federated_token_file(mut self, federated_token_file: impl Into<String>) -> Self {
980        self.federated_token_file = Some(federated_token_file.into());
981        self
982    }
983
984    /// Set if the Azure Cli should be used for acquiring access token
985    ///
986    /// <https://learn.microsoft.com/en-us/cli/azure/account?view=azure-cli-latest#az-account-get-access-token>
987    pub fn with_use_azure_cli(mut self, use_azure_cli: bool) -> Self {
988        self.use_azure_cli = use_azure_cli.into();
989        self
990    }
991
992    /// Set the credential type to use for authentication.
993    ///
994    /// When multiple credential configurations are present (e.g. both workload identity
995    /// and client secret), this forces the builder to use a specific credential type
996    /// instead of relying on the default resolution order.
997    ///
998    /// Supported values: `auto`, `bearer_token`, `access_key`, `client_secret`,
999    /// `workload_identity`, `sas_token`, `azure_cli`, `managed_identity`.
1000    pub fn with_credential_type(mut self, credential_type: impl Into<String>) -> Self {
1001        self.credential_type = Some(credential_type.into());
1002        self
1003    }
1004
1005    /// If enabled, [`MicrosoftAzure`] will not fetch credentials and will not sign requests
1006    ///
1007    /// This can be useful when interacting with public containers
1008    pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
1009        self.skip_signature = skip_signature.into();
1010        self
1011    }
1012
1013    /// If set to `true` will ignore any tags provided to put_opts
1014    pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1015        self.disable_tagging = ignore.into();
1016        self
1017    }
1018
1019    /// Set the customer-provided encryption key (CPK) used to encrypt blob content.
1020    ///
1021    /// `key` must be a base64-encoded 256-bit AES key (the decoded value must be
1022    /// exactly 32 bytes). The same key must be supplied on every subsequent read,
1023    /// write, or copy of any blob created with it; if the key is lost or omitted
1024    /// the data is unrecoverable. CPK material is sent to Azure on every request,
1025    /// so the configured endpoint must use HTTPS.
1026    ///
1027    /// Only a subset of Blob storage operations support CPK
1028    /// (see the [Azure documentation][cpk-ops]). When CPK is enabled, `copy`
1029    /// switches from the asynchronous `Copy Blob` API to `Put Blob From URL`,
1030    /// which is synchronous and limits the source blob to 5,000 MiB.
1031    ///
1032    /// [cpk-ops]: https://learn.microsoft.com/en-us/azure/storage/blobs/encryption-customer-provided-keys#blob-storage-operations-supporting-customer-provided-keys
1033    pub fn with_encryption_key(mut self, key: impl Into<String>) -> Self {
1034        self.encryption_key = Some(key.into());
1035        self
1036    }
1037
1038    /// The [`HttpConnector`] to use
1039    ///
1040    /// On non-WASM32 platforms uses [`reqwest`] by default, on WASM32 platforms must be provided
1041    pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
1042        self.http_connector = Some(Arc::new(connector));
1043        self
1044    }
1045
1046    fn resolve_credential_auto(
1047        &mut self,
1048        http: &Arc<dyn HttpConnector>,
1049    ) -> Result<AzureCredentialProvider> {
1050        if let (
1051            Some(fabric_token_service_url),
1052            Some(fabric_workload_host),
1053            Some(fabric_session_token),
1054            Some(fabric_cluster_identifier),
1055        ) = (
1056            &self.fabric_token_service_url,
1057            &self.fabric_workload_host,
1058            &self.fabric_session_token,
1059            &self.fabric_cluster_identifier,
1060        ) {
1061            let fabric_credential = FabricTokenOAuthProvider::new(
1062                fabric_token_service_url,
1063                fabric_workload_host,
1064                fabric_session_token,
1065                fabric_cluster_identifier,
1066                self.bearer_token.clone(),
1067            );
1068            Ok(Arc::new(TokenCredentialProvider::new(
1069                fabric_credential,
1070                http.connect(&self.client_options)?,
1071                self.retry_config.clone(),
1072            )) as _)
1073        } else if self.bearer_token.is_some() {
1074            self.resolve_bearer_token()
1075        } else if self.access_key.is_some() {
1076            self.resolve_access_key()
1077        } else if self.client_id.is_some()
1078            && self.tenant_id.is_some()
1079            && self.federated_token_file.is_some()
1080        {
1081            self.resolve_workload_identity(http)
1082        } else if self.client_id.is_some()
1083            && self.client_secret.is_some()
1084            && self.tenant_id.is_some()
1085        {
1086            self.resolve_client_secret(http)
1087        } else if self.sas_query_pairs.is_some() || self.sas_key.is_some() {
1088            self.resolve_sas_token()
1089        } else if self.use_azure_cli.get()? {
1090            Ok(Arc::new(AzureCliCredential::new()) as _)
1091        } else {
1092            self.resolve_managed_identity(http)
1093        }
1094    }
1095
1096    fn resolve_bearer_token(&mut self) -> Result<AzureCredentialProvider> {
1097        let bearer_token = self
1098            .bearer_token
1099            .take()
1100            .ok_or(Error::MissingCredentialConfig {
1101                credential_type: "bearer_token".to_string(),
1102            })?;
1103        Ok(Arc::new(StaticCredentialProvider::new(
1104            AzureCredential::BearerToken(bearer_token),
1105        )))
1106    }
1107
1108    fn resolve_access_key(&mut self) -> Result<AzureCredentialProvider> {
1109        let access_key = self
1110            .access_key
1111            .take()
1112            .ok_or(Error::MissingCredentialConfig {
1113                credential_type: "access_key".to_string(),
1114            })?;
1115        let key = AzureAccessKey::try_new(&access_key)?;
1116        Ok(Arc::new(StaticCredentialProvider::new(
1117            AzureCredential::AccessKey(key),
1118        )))
1119    }
1120
1121    fn resolve_client_secret(
1122        &mut self,
1123        http: &Arc<dyn HttpConnector>,
1124    ) -> Result<AzureCredentialProvider> {
1125        let client_id = self
1126            .client_id
1127            .take()
1128            .ok_or(Error::MissingCredentialConfig {
1129                credential_type: "client_secret".to_string(),
1130            })?;
1131        let client_secret = self
1132            .client_secret
1133            .take()
1134            .ok_or(Error::MissingCredentialConfig {
1135                credential_type: "client_secret".to_string(),
1136            })?;
1137        let tenant_id = self
1138            .tenant_id
1139            .take()
1140            .ok_or(Error::MissingCredentialConfig {
1141                credential_type: "client_secret".to_string(),
1142            })?;
1143        let client_credential = ClientSecretOAuthProvider::new(
1144            client_id,
1145            client_secret,
1146            &tenant_id,
1147            self.authority_host.take(),
1148        );
1149        Ok(Arc::new(TokenCredentialProvider::new(
1150            client_credential,
1151            http.connect(&self.client_options)?,
1152            self.retry_config.clone(),
1153        )) as _)
1154    }
1155
1156    fn resolve_workload_identity(
1157        &mut self,
1158        http: &Arc<dyn HttpConnector>,
1159    ) -> Result<AzureCredentialProvider> {
1160        let client_id = self
1161            .client_id
1162            .take()
1163            .ok_or(Error::MissingCredentialConfig {
1164                credential_type: "workload_identity".to_string(),
1165            })?;
1166        let tenant_id = self
1167            .tenant_id
1168            .take()
1169            .ok_or(Error::MissingCredentialConfig {
1170                credential_type: "workload_identity".to_string(),
1171            })?;
1172        let federated_token_file =
1173            self.federated_token_file
1174                .take()
1175                .ok_or(Error::MissingCredentialConfig {
1176                    credential_type: "workload_identity".to_string(),
1177                })?;
1178        let client_credential = WorkloadIdentityOAuthProvider::new(
1179            &client_id,
1180            federated_token_file,
1181            &tenant_id,
1182            self.authority_host.take(),
1183        );
1184        Ok(Arc::new(TokenCredentialProvider::new(
1185            client_credential,
1186            http.connect(&self.client_options)?,
1187            self.retry_config.clone(),
1188        )) as _)
1189    }
1190
1191    fn resolve_sas_token(&mut self) -> Result<AzureCredentialProvider> {
1192        if let Some(query_pairs) = self.sas_query_pairs.take() {
1193            Ok(Arc::new(StaticCredentialProvider::new(
1194                AzureCredential::SASToken(query_pairs),
1195            )))
1196        } else if let Some(sas) = self.sas_key.take() {
1197            Ok(Arc::new(StaticCredentialProvider::new(
1198                AzureCredential::SASToken(split_sas(&sas)?),
1199            )))
1200        } else {
1201            Err(Error::MissingCredentialConfig {
1202                credential_type: "sas_token".to_string(),
1203            }
1204            .into())
1205        }
1206    }
1207
1208    fn resolve_managed_identity(
1209        &mut self,
1210        http: &Arc<dyn HttpConnector>,
1211    ) -> Result<AzureCredentialProvider> {
1212        let msi_credential = ImdsManagedIdentityProvider::new(
1213            self.client_id.take(),
1214            self.object_id.take(),
1215            self.msi_resource_id.take(),
1216            self.msi_endpoint.take(),
1217        );
1218        Ok(Arc::new(TokenCredentialProvider::new(
1219            msi_credential,
1220            http.connect(&self.client_options.metadata_options())?,
1221            self.retry_config.clone(),
1222        )) as _)
1223    }
1224
1225    /// Configure a connection to container with given name on Microsoft Azure Blob store.
1226    pub fn build(mut self) -> Result<MicrosoftAzure> {
1227        if let Some(url) = self.url.take() {
1228            self.parse_url(&url)?;
1229        }
1230
1231        let container = self
1232            .container_name
1233            .take()
1234            .ok_or(Error::MissingContainerName {})?;
1235
1236        let static_creds = |credential: AzureCredential| -> AzureCredentialProvider {
1237            Arc::new(StaticCredentialProvider::new(credential))
1238        };
1239
1240        let http = http_connector(self.http_connector.take())?;
1241
1242        let (is_emulator, storage_url, auth, account) = if self.use_emulator.get()? {
1243            let account_name = self
1244                .account_name
1245                .unwrap_or_else(|| EMULATOR_ACCOUNT.to_string());
1246            // Allow overriding defaults. Values taken from
1247            // from https://docs.rs/azure_storage/0.2.0/src/azure_storage/core/clients/storage_account_client.rs.html#129-141
1248            let url = url_from_env("AZURITE_BLOB_STORAGE_URL", "http://127.0.0.1:10000")?;
1249            let credential = if let Some(k) = self.access_key {
1250                AzureCredential::AccessKey(AzureAccessKey::try_new(&k)?)
1251            } else if let Some(bearer_token) = self.bearer_token {
1252                AzureCredential::BearerToken(bearer_token)
1253            } else if let Some(query_pairs) = self.sas_query_pairs {
1254                AzureCredential::SASToken(query_pairs)
1255            } else if let Some(sas) = self.sas_key {
1256                AzureCredential::SASToken(split_sas(&sas)?)
1257            } else {
1258                AzureCredential::AccessKey(AzureAccessKey::try_new(EMULATOR_ACCOUNT_KEY)?)
1259            };
1260
1261            self.client_options = self.client_options.with_allow_http(true);
1262            (true, url, static_creds(credential), account_name)
1263        } else {
1264            let account_name = self.account_name.take().ok_or(Error::MissingAccount {})?;
1265            let account_url = match self.endpoint.take() {
1266                Some(account_url) => account_url,
1267                None => match self.use_fabric_endpoint.get()? {
1268                    true => {
1269                        format!("https://{}.blob.fabric.microsoft.com", account_name)
1270                    }
1271                    false => format!("https://{}.blob.core.windows.net", account_name),
1272                },
1273            };
1274
1275            let url = Url::parse(&account_url).map_err(|source| {
1276                let url = account_url.clone();
1277                Error::UnableToParseUrl { url, source }
1278            })?;
1279
1280            let credential = if let Some(credential) = self.credentials {
1281                credential
1282            } else {
1283                let credential_type = self.credential_type.as_deref().unwrap_or("auto");
1284
1285                match credential_type {
1286                    "auto" => self.resolve_credential_auto(&http)?,
1287                    "bearer_token" => self.resolve_bearer_token()?,
1288                    "access_key" => self.resolve_access_key()?,
1289                    "client_secret" => self.resolve_client_secret(&http)?,
1290                    "workload_identity" => self.resolve_workload_identity(&http)?,
1291                    "sas_token" => self.resolve_sas_token()?,
1292                    "azure_cli" => Arc::new(AzureCliCredential::new()) as _,
1293                    "managed_identity" => self.resolve_managed_identity(&http)?,
1294                    other => {
1295                        return Err(Error::UnknownCredentialType {
1296                            credential_type: other.to_string(),
1297                        }
1298                        .into());
1299                    }
1300                }
1301            };
1302            (false, url, credential, account_name)
1303        };
1304
1305        let encryption_headers =
1306            AzureEncryptionHeaders::try_new(self.crypto.as_deref(), self.encryption_key).map_err(
1307                |source| Error::InvalidEncryptionKey {
1308                    source: match source {
1309                        crate::Error::Generic { source, .. } => source,
1310                        other => Box::new(other),
1311                    },
1312                },
1313            )?;
1314
1315        let config = AzureConfig {
1316            account,
1317            is_emulator,
1318            skip_signature: self.skip_signature.get()?,
1319            container,
1320            disable_tagging: self.disable_tagging.get()?,
1321            retry_config: self.retry_config,
1322            client_options: self.client_options,
1323            service: storage_url,
1324            credentials: auth,
1325            crypto: self.crypto,
1326            encryption_headers,
1327        };
1328
1329        let http_client = http.connect(&config.client_options)?;
1330        let client = Arc::new(AzureClient::new(config, http_client));
1331
1332        Ok(MicrosoftAzure { client })
1333    }
1334}
1335
1336/// Parses the contents of the environment variable `env_name` as a URL
1337/// if present, otherwise falls back to default_url
1338fn url_from_env(env_name: &str, default_url: &str) -> Result<Url> {
1339    let url = match std::env::var(env_name) {
1340        Ok(env_value) => {
1341            Url::parse(&env_value).map_err(|source| Error::UnableToParseEmulatorUrl {
1342                env_name: env_name.into(),
1343                env_value,
1344                source,
1345            })?
1346        }
1347        Err(_) => Url::parse(default_url).expect("Failed to parse default URL"),
1348    };
1349    Ok(url)
1350}
1351
1352/// Parse a SAS token string into the query pairs expected by [`AzureCredential::SASToken`].
1353pub fn split_sas(sas: &str) -> Result<Vec<(String, String)>> {
1354    let sas = percent_decode_str(sas)
1355        .decode_utf8()
1356        .map_err(|source| Error::DecodeSasKey { source })?;
1357    let kv_str_pairs = sas
1358        .trim_start_matches('?')
1359        .split('&')
1360        .filter(|s| !s.chars().all(char::is_whitespace));
1361    let mut pairs = Vec::new();
1362    for kv_pair_str in kv_str_pairs {
1363        let (k, v) = kv_pair_str
1364            .trim()
1365            .split_once('=')
1366            .ok_or(Error::MissingSasComponent {})?;
1367        pairs.push((k.into(), v.into()))
1368    }
1369    Ok(pairs)
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374    use super::*;
1375    use base64::Engine;
1376    use base64::prelude::BASE64_STANDARD;
1377    use std::collections::HashMap;
1378
1379    #[test]
1380    fn azure_blob_test_urls() {
1381        let mut builder = MicrosoftAzureBuilder::new();
1382        builder
1383            .parse_url("abfss://file_system@account.dfs.core.windows.net/")
1384            .unwrap();
1385        assert_eq!(builder.account_name, Some("account".to_string()));
1386        assert_eq!(builder.container_name, Some("file_system".to_string()));
1387        assert!(!builder.use_fabric_endpoint.get().unwrap());
1388
1389        let mut builder = MicrosoftAzureBuilder::new();
1390        builder
1391            .parse_url("az://container@account.dfs.core.windows.net/path-part/file")
1392            .unwrap();
1393        assert_eq!(builder.account_name, Some("account".to_string()));
1394        assert_eq!(builder.container_name, Some("container".to_string()));
1395        assert!(!builder.use_fabric_endpoint.get().unwrap());
1396
1397        let mut builder = MicrosoftAzureBuilder::new();
1398        builder
1399            .parse_url("az://container@account.blob.core.windows.net/path-part/file")
1400            .unwrap();
1401        assert_eq!(builder.account_name, Some("account".to_string()));
1402        assert_eq!(builder.container_name, Some("container".to_string()));
1403        assert!(!builder.use_fabric_endpoint.get().unwrap());
1404
1405        let mut builder = MicrosoftAzureBuilder::new();
1406        builder
1407            .parse_url("az://container@account.dfs.fabric.microsoft.com/path-part/file")
1408            .unwrap();
1409        assert_eq!(builder.account_name, Some("account".to_string()));
1410        assert_eq!(builder.container_name, Some("container".to_string()));
1411        assert!(builder.use_fabric_endpoint.get().unwrap());
1412
1413        let mut builder = MicrosoftAzureBuilder::new();
1414        builder
1415            .parse_url("az://container@account.blob.fabric.microsoft.com/path-part/file")
1416            .unwrap();
1417        assert_eq!(builder.account_name, Some("account".to_string()));
1418        assert_eq!(builder.container_name, Some("container".to_string()));
1419        assert!(builder.use_fabric_endpoint.get().unwrap());
1420
1421        let mut builder = MicrosoftAzureBuilder::new();
1422        builder
1423            .parse_url("abfss://file_system@account.dfs.fabric.microsoft.com/")
1424            .unwrap();
1425        assert_eq!(builder.account_name, Some("account".to_string()));
1426        assert_eq!(builder.container_name, Some("file_system".to_string()));
1427        assert!(builder.use_fabric_endpoint.get().unwrap());
1428
1429        let mut builder = MicrosoftAzureBuilder::new();
1430        builder.parse_url("abfs://container/path").unwrap();
1431        assert_eq!(builder.container_name, Some("container".to_string()));
1432
1433        let mut builder = MicrosoftAzureBuilder::new();
1434        builder.parse_url("az://container").unwrap();
1435        assert_eq!(builder.container_name, Some("container".to_string()));
1436
1437        let mut builder = MicrosoftAzureBuilder::new();
1438        builder.parse_url("az://container/path").unwrap();
1439        assert_eq!(builder.container_name, Some("container".to_string()));
1440
1441        let mut builder = MicrosoftAzureBuilder::new();
1442        builder
1443            .parse_url("https://account.dfs.core.windows.net/")
1444            .unwrap();
1445        assert_eq!(builder.account_name, Some("account".to_string()));
1446        assert!(!builder.use_fabric_endpoint.get().unwrap());
1447
1448        let mut builder =
1449            MicrosoftAzureBuilder::new().with_container_name("explicit_container_name");
1450        builder
1451            .parse_url("https://account.blob.core.windows.net/")
1452            .unwrap();
1453        assert_eq!(builder.account_name, Some("account".to_string()));
1454        assert_eq!(
1455            builder.container_name,
1456            Some("explicit_container_name".to_string())
1457        );
1458        assert!(!builder.use_fabric_endpoint.get().unwrap());
1459
1460        let mut builder = MicrosoftAzureBuilder::new();
1461        builder
1462            .parse_url("https://account.blob.core.windows.net/container")
1463            .unwrap();
1464        assert_eq!(builder.account_name, Some("account".to_string()));
1465        assert_eq!(builder.container_name, Some("container".to_string()));
1466        assert!(!builder.use_fabric_endpoint.get().unwrap());
1467
1468        let mut builder = MicrosoftAzureBuilder::new();
1469        builder
1470            .parse_url("https://account.dfs.fabric.microsoft.com/")
1471            .unwrap();
1472        assert_eq!(builder.account_name, Some("account".to_string()));
1473        assert_eq!(builder.container_name, None);
1474        assert!(builder.use_fabric_endpoint.get().unwrap());
1475
1476        let mut builder = MicrosoftAzureBuilder::new();
1477        builder
1478            .parse_url("https://account.dfs.fabric.microsoft.com/container")
1479            .unwrap();
1480        assert_eq!(builder.account_name, Some("account".to_string()));
1481        assert_eq!(builder.container_name.as_deref(), Some("container"));
1482        assert!(builder.use_fabric_endpoint.get().unwrap());
1483
1484        let mut builder = MicrosoftAzureBuilder::new();
1485        builder
1486            .parse_url("https://onelake.dfs.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456.lakehouse/Files/tables/sales/data.parquet")
1487            .unwrap();
1488        assert_eq!(builder.account_name, Some("onelake".to_string()));
1489        assert_eq!(
1490            builder.container_name.as_deref(),
1491            Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3")
1492        );
1493        assert!(builder.use_fabric_endpoint.get().unwrap());
1494
1495        let mut builder = MicrosoftAzureBuilder::new();
1496        builder
1497            .parse_url("https://account.blob.fabric.microsoft.com/")
1498            .unwrap();
1499        assert_eq!(builder.account_name, Some("account".to_string()));
1500        assert_eq!(builder.container_name, None);
1501        assert!(builder.use_fabric_endpoint.get().unwrap());
1502
1503        let mut builder = MicrosoftAzureBuilder::new();
1504        builder
1505            .parse_url("https://account.blob.fabric.microsoft.com/container")
1506            .unwrap();
1507        assert_eq!(builder.account_name, Some("account".to_string()));
1508        assert_eq!(builder.container_name.as_deref(), Some("container"));
1509        assert!(builder.use_fabric_endpoint.get().unwrap());
1510
1511        let err_cases = [
1512            "mailto://account.blob.core.windows.net/",
1513            "az://blob.mydomain/",
1514            "abfs://container.foo/path",
1515            "abfss://file_system@account.foo.dfs.core.windows.net/",
1516            "abfss://file_system.bar@account.dfs.core.windows.net/",
1517            "https://blob.mydomain/",
1518            "https://blob.foo.dfs.core.windows.net/",
1519        ];
1520        let mut builder = MicrosoftAzureBuilder::new();
1521        for case in err_cases {
1522            builder.parse_url(case).unwrap_err();
1523        }
1524    }
1525
1526    #[test]
1527    fn azure_test_workspace_private_link() {
1528        let test_cases: Vec<(&str, &str, Option<&str>)> = vec![
1529            (
1530                "https://Ab000000000000000000000000000000.zAb.dfs.fabric.microsoft.com/",
1531                "ab000000000000000000000000000000.zab",
1532                None,
1533            ),
1534            (
1535                "https://ab000000000000000000000000000000.zab.dfs.fabric.microsoft.com/",
1536                "ab000000000000000000000000000000.zab",
1537                None,
1538            ),
1539            (
1540                "https://c047b3e34e89407a98d7cf9949ae92a3.zc0.blob.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
1541                "c047b3e34e89407a98d7cf9949ae92a3.zc0",
1542                Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
1543            ),
1544            (
1545                "https://c047b3e34e89407a98d7cf9949ae92a3.zc0.dfs.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
1546                "c047b3e34e89407a98d7cf9949ae92a3.zc0",
1547                Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
1548            ),
1549            (
1550                "https://c047b3e34e89407a98d7cf9949ae92a3.zc0.onelake.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
1551                "c047b3e34e89407a98d7cf9949ae92a3.zc0",
1552                Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
1553            ),
1554            (
1555                "https://c047b3e34e89407a98d7cf9949ae92a3.zc0.w.api.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
1556                "c047b3e34e89407a98d7cf9949ae92a3.zc0",
1557                Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
1558            ),
1559            (
1560                "https://c047b3e34e89407a98d7cf9949ae92a3.zc0.c.api.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
1561                "c047b3e34e89407a98d7cf9949ae92a3.zc0",
1562                Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
1563            ),
1564            (
1565                "abfss://c047b3e34e89407a98d7cf9949ae92a3@c047b3e34e89407a98d7cf9949ae92a3.zc0.dfs.fabric.microsoft.com/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
1566                "c047b3e34e89407a98d7cf9949ae92a3.zc0",
1567                Some("c047b3e34e89407a98d7cf9949ae92a3"),
1568            ),
1569            (
1570                "abfss://c047b3e34e89407a98d7cf9949ae92a3@c047b3e34e89407a98d7cf9949ae92a3.zc0.blob.fabric.microsoft.com/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
1571                "c047b3e34e89407a98d7cf9949ae92a3.zc0",
1572                Some("c047b3e34e89407a98d7cf9949ae92a3"),
1573            ),
1574        ];
1575
1576        for (url, expected_account, expected_container) in &test_cases {
1577            let mut builder = MicrosoftAzureBuilder::new();
1578            builder.parse_url(url).unwrap();
1579
1580            assert_eq!(
1581                builder.account_name.as_deref(),
1582                Some(*expected_account),
1583                "account mismatch for URL: {url}"
1584            );
1585            assert_eq!(
1586                builder.container_name.as_deref(),
1587                *expected_container,
1588                "container mismatch for URL: {url}"
1589            );
1590            assert!(
1591                builder.use_fabric_endpoint.get().unwrap(),
1592                "use_fabric_endpoint not set for URL: {url}"
1593            );
1594        }
1595    }
1596
1597    #[test]
1598    fn azure_encryption_key_roundtrip() {
1599        let key = BASE64_STANDARD.encode([7_u8; 32]);
1600        let builder = MicrosoftAzureBuilder::new().with_encryption_key(&key);
1601
1602        assert_eq!(
1603            builder
1604                .get_config_value(&AzureConfigKey::EncryptionKey)
1605                .as_deref(),
1606            Some(key.as_str())
1607        );
1608    }
1609
1610    #[test]
1611    fn azure_encryption_key_rejects_malformed_base64() {
1612        let err = MicrosoftAzureBuilder::new()
1613            .with_account("account")
1614            .with_container_name("container")
1615            .with_access_key(EMULATOR_ACCOUNT_KEY)
1616            .with_encryption_key("not-base64!!!")
1617            .build()
1618            .unwrap_err();
1619
1620        let msg = err.to_string();
1621        assert!(msg.contains("Invalid encryption key") || msg.contains("Invalid byte"));
1622    }
1623
1624    #[test]
1625    fn azure_encryption_key_rejects_short_decoded_key() {
1626        let err = MicrosoftAzureBuilder::new()
1627            .with_account("account")
1628            .with_container_name("container")
1629            .with_access_key(EMULATOR_ACCOUNT_KEY)
1630            .with_encryption_key(BASE64_STANDARD.encode([7_u8; 31]))
1631            .build()
1632            .unwrap_err();
1633
1634        assert!(err.to_string().contains("must decode to 32 bytes, got 31"));
1635    }
1636
1637    #[test]
1638    fn azure_encryption_key_rejects_long_decoded_key() {
1639        let err = MicrosoftAzureBuilder::new()
1640            .with_account("account")
1641            .with_container_name("container")
1642            .with_access_key(EMULATOR_ACCOUNT_KEY)
1643            .with_encryption_key(BASE64_STANDARD.encode([7_u8; 33]))
1644            .build()
1645            .unwrap_err();
1646
1647        assert!(err.to_string().contains("must decode to 32 bytes, got 33"));
1648    }
1649
1650    #[test]
1651    fn azure_test_config_from_map() {
1652        let azure_client_id = "object_store:fake_access_key_id";
1653        let azure_storage_account_name = "object_store:fake_secret_key";
1654        let azure_storage_token = "object_store:fake_default_region";
1655        let options = HashMap::from([
1656            ("azure_client_id", azure_client_id),
1657            ("azure_storage_account_name", azure_storage_account_name),
1658            ("azure_storage_token", azure_storage_token),
1659        ]);
1660
1661        let builder = options
1662            .into_iter()
1663            .fold(MicrosoftAzureBuilder::new(), |builder, (key, value)| {
1664                builder.with_config(key.parse().unwrap(), value)
1665            });
1666        assert_eq!(builder.client_id.unwrap(), azure_client_id);
1667        assert_eq!(builder.account_name.unwrap(), azure_storage_account_name);
1668        assert_eq!(builder.bearer_token.unwrap(), azure_storage_token);
1669    }
1670
1671    #[test]
1672    fn azure_test_credential_type_config() {
1673        let builder = MicrosoftAzureBuilder::new()
1674            .with_config(AzureConfigKey::CredentialType, "client_secret");
1675        assert_eq!(builder.credential_type, Some("client_secret".to_string()));
1676        assert_eq!(
1677            builder
1678                .get_config_value(&AzureConfigKey::CredentialType)
1679                .unwrap(),
1680            "client_secret"
1681        );
1682
1683        let builder =
1684            MicrosoftAzureBuilder::new().with_config("credential_type".parse().unwrap(), "auto");
1685        assert_eq!(builder.credential_type, Some("auto".to_string()));
1686    }
1687
1688    #[test]
1689    fn azure_test_credential_type_client_secret() {
1690        let builder = MicrosoftAzureBuilder::new()
1691            .with_account("account")
1692            .with_container_name("container")
1693            .with_client_id("client_id")
1694            .with_client_secret("client_secret")
1695            .with_tenant_id("tenant_id")
1696            .with_federated_token_file("/tmp/token")
1697            .with_credential_type("client_secret");
1698        // Should succeed — client_secret is forced even though workload_identity fields are present
1699        let result = builder.build();
1700        // Build will fail because it can't actually connect, but it should not fail with
1701        // a MissingCredentialConfig error — it should get past credential resolution
1702        assert!(
1703            result.is_ok(),
1704            "Expected build to succeed with credential_type=client_secret, got: {:?}",
1705            result.err()
1706        );
1707    }
1708
1709    #[test]
1710    fn azure_test_credential_type_unknown() {
1711        let builder = MicrosoftAzureBuilder::new()
1712            .with_account("account")
1713            .with_container_name("container")
1714            .with_credential_type("invalid_type");
1715        let result = builder.build();
1716        assert!(result.is_err());
1717        let err = result.unwrap_err().to_string();
1718        assert!(
1719            err.contains("Unknown credential type"),
1720            "Expected unknown credential type error, got: {err}"
1721        );
1722    }
1723
1724    #[test]
1725    fn azure_test_credential_type_missing_config() {
1726        let builder = MicrosoftAzureBuilder::new()
1727            .with_account("account")
1728            .with_container_name("container")
1729            .with_credential_type("client_secret");
1730        let result = builder.build();
1731        assert!(result.is_err());
1732        let err = result.unwrap_err().to_string();
1733        assert!(
1734            err.contains("required configuration is missing"),
1735            "Expected missing config error, got: {err}"
1736        );
1737    }
1738
1739    #[test]
1740    fn azure_test_split_sas() {
1741        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";
1742        let expected = vec![
1743            ("sv".to_string(), "2021-10-04".to_string()),
1744            ("st".to_string(), "2023-01-04T17:48:57Z".to_string()),
1745            ("se".to_string(), "2023-01-04T18:15:00Z".to_string()),
1746            ("sr".to_string(), "c".to_string()),
1747            ("sp".to_string(), "rcwl".to_string()),
1748            (
1749                "sig".to_string(),
1750                "C7+ZeEOWbrxPA3R0Cw/w1EZz0+4KBvQexeKZKe+B6h0=".to_string(),
1751            ),
1752        ];
1753        let pairs = split_sas(raw_sas).unwrap();
1754        assert_eq!(expected, pairs);
1755    }
1756
1757    #[test]
1758    fn azure_test_split_sas_trims_leading_question_mark_and_skips_empties() {
1759        let pairs = split_sas("?&sv=2021-10-04& &sp=r").unwrap();
1760        assert_eq!(
1761            pairs,
1762            vec![
1763                ("sv".to_string(), "2021-10-04".to_string()),
1764                ("sp".to_string(), "r".to_string()),
1765            ],
1766        );
1767    }
1768
1769    #[test]
1770    fn azure_test_split_sas_rejects_missing_equals() {
1771        let err = split_sas("sv=2021-10-04&bogus").unwrap_err();
1772        assert!(
1773            err.to_string().contains("Missing component"),
1774            "unexpected error: {err}",
1775        );
1776    }
1777
1778    #[test]
1779    fn azure_test_client_opts() {
1780        let key = "AZURE_PROXY_URL";
1781        if let Ok(config_key) = key.to_ascii_lowercase().parse() {
1782            assert_eq!(
1783                AzureConfigKey::Client(ClientConfigKey::ProxyUrl),
1784                config_key
1785            );
1786        } else {
1787            panic!("{key} not propagated as ClientConfigKey");
1788        }
1789    }
1790}