blob: d1d68fe8dfa9dff8aee92b675d973f7ef41aa05c [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::azure::client::{AzureClient, AzureConfig, AzureEncryptionHeaders};
use crate::azure::credential::{
AzureAccessKey, AzureCliCredential, ClientSecretOAuthProvider, FabricTokenOAuthProvider,
ImdsManagedIdentityProvider, WorkloadIdentityOAuthProvider,
};
use crate::azure::{AzureCredential, AzureCredentialProvider, MicrosoftAzure, STORE};
use crate::client::{CryptoProvider, HttpConnector, TokenCredentialProvider, http_connector};
use crate::config::ConfigValue;
use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider};
use percent_encoding::percent_decode_str;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;
use url::Url;
/// The well-known account used by Azurite and the legacy Azure Storage Emulator.
///
/// <https://docs.microsoft.com/azure/storage/common/storage-use-azurite#well-known-storage-account-and-key>
const EMULATOR_ACCOUNT: &str = "devstoreaccount1";
/// The well-known account key used by Azurite and the legacy Azure Storage Emulator.
///
/// <https://docs.microsoft.com/azure/storage/common/storage-use-azurite#well-known-storage-account-and-key>
const EMULATOR_ACCOUNT_KEY: &str =
"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
const MSI_ENDPOINT_ENV_KEY: &str = "IDENTITY_ENDPOINT";
/// A specialized `Error` for Azure builder-related errors
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("Unable parse source url. Url: {}, Error: {}", url, source)]
UnableToParseUrl {
source: url::ParseError,
url: String,
},
#[error(
"Unable parse emulator url {}={}, Error: {}",
env_name,
env_value,
source
)]
UnableToParseEmulatorUrl {
env_name: String,
env_value: String,
source: url::ParseError,
},
#[error("Account must be specified")]
MissingAccount {},
#[error("Container name must be specified")]
MissingContainerName {},
#[error(
"Unknown url scheme cannot be parsed into storage location: {}",
scheme
)]
UnknownUrlScheme { scheme: String },
#[error("URL did not match any known pattern for scheme: {}", url)]
UrlNotRecognised { url: String },
#[error("Failed parsing an SAS key")]
DecodeSasKey { source: std::str::Utf8Error },
#[error("Missing component in SAS query pair")]
MissingSasComponent {},
#[error("Invalid encryption key: {source}")]
InvalidEncryptionKey {
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
#[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 {
fn from(source: Error) -> Self {
match source {
Error::UnknownConfigurationKey { key } => {
Self::UnknownConfigurationKey { store: STORE, key }
}
_ => Self::Generic {
store: STORE,
source: Box::new(source),
},
}
}
}
/// Configure a connection to Microsoft Azure Blob Storage container using
/// the specified credentials.
///
/// # Example
/// ```
/// # let ACCOUNT = "foo";
/// # let BUCKET_NAME = "foo";
/// # let ACCESS_KEY = "foo";
/// # use object_store::azure::MicrosoftAzureBuilder;
/// let azure = MicrosoftAzureBuilder::new()
/// .with_account(ACCOUNT)
/// .with_access_key(ACCESS_KEY)
/// .with_container_name(BUCKET_NAME)
/// .build();
/// ```
#[derive(Default, Clone)]
pub struct MicrosoftAzureBuilder {
/// Account name
account_name: Option<String>,
/// Access key
access_key: Option<String>,
/// Container name
container_name: Option<String>,
/// Bearer token
bearer_token: Option<String>,
/// Client id
client_id: Option<String>,
/// Client secret
client_secret: Option<String>,
/// Tenant id
tenant_id: Option<String>,
/// Query pairs for shared access signature authorization
sas_query_pairs: Option<Vec<(String, String)>>,
/// Shared access signature
sas_key: Option<String>,
/// Authority host
authority_host: Option<String>,
/// Url
url: Option<String>,
/// When set to true, azurite storage emulator has to be used
use_emulator: ConfigValue<bool>,
/// Storage endpoint
endpoint: Option<String>,
/// Msi endpoint for acquiring managed identity token
msi_endpoint: Option<String>,
/// Object id for use with managed identity authentication
object_id: Option<String>,
/// Msi resource id for use with managed identity authentication
msi_resource_id: Option<String>,
/// File containing token for Azure AD workload identity federation
federated_token_file: Option<String>,
/// When set to true, azure cli has to be used for acquiring access token
use_azure_cli: ConfigValue<bool>,
/// Retry config
retry_config: RetryConfig,
/// Client options
client_options: ClientOptions,
/// Credentials
credentials: Option<AzureCredentialProvider>,
/// The [`CryptoProvider`] to use
crypto: Option<Arc<dyn CryptoProvider>>,
/// Skip signing requests
skip_signature: ConfigValue<bool>,
/// When set to true, fabric url scheme will be used
///
/// i.e. https://{account_name}.dfs.fabric.microsoft.com
use_fabric_endpoint: ConfigValue<bool>,
/// When set to true, skips tagging objects
disable_tagging: ConfigValue<bool>,
/// Fabric token service url
fabric_token_service_url: Option<String>,
/// Fabric workload host
fabric_workload_host: Option<String>,
/// Fabric session token
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
http_connector: Option<Arc<dyn HttpConnector>>,
}
/// Configuration keys for [`MicrosoftAzureBuilder`]
///
/// Configuration via keys can be done via [`MicrosoftAzureBuilder::with_config`]
///
/// # Example
/// ```
/// # use object_store::azure::{MicrosoftAzureBuilder, AzureConfigKey};
/// let builder = MicrosoftAzureBuilder::new()
/// .with_config("azure_client_id".parse().unwrap(), "my-client-id")
/// .with_config(AzureConfigKey::AuthorityId, "my-tenant-id");
/// ```
#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Deserialize, Serialize)]
#[non_exhaustive]
pub enum AzureConfigKey {
/// The name of the azure storage account
///
/// Supported keys:
/// - `azure_storage_account_name`
/// - `account_name`
AccountName,
/// Master key for accessing storage account
///
/// Supported keys:
/// - `azure_storage_account_key`
/// - `azure_storage_access_key`
/// - `azure_storage_master_key`
/// - `access_key`
/// - `account_key`
/// - `master_key`
AccessKey,
/// Service principal client id for authorizing requests
///
/// Supported keys:
/// - `azure_storage_client_id`
/// - `azure_client_id`
/// - `client_id`
ClientId,
/// Service principal client secret for authorizing requests
///
/// Supported keys:
/// - `azure_storage_client_secret`
/// - `azure_client_secret`
/// - `client_secret`
ClientSecret,
/// Tenant id used in oauth flows
///
/// Supported keys:
/// - `azure_storage_tenant_id`
/// - `azure_storage_authority_id`
/// - `azure_tenant_id`
/// - `azure_authority_id`
/// - `tenant_id`
/// - `authority_id`
AuthorityId,
/// Authority host used in oauth flows
///
/// Supported keys:
/// - `azure_storage_authority_host`
/// - `azure_authority_host`
/// - `authority_host`
AuthorityHost,
/// Shared access signature.
///
/// The signature is expected to be percent-encoded, much like they are provided
/// in the azure storage explorer or azure portal.
///
/// Supported keys:
/// - `azure_storage_sas_key`
/// - `azure_storage_sas_token`
/// - `sas_key`
/// - `sas_token`
SasKey,
/// Bearer token
///
/// Supported keys:
/// - `azure_storage_token`
/// - `bearer_token`
/// - `token`
Token,
/// Use object store with azurite storage emulator
///
/// Supported keys:
/// - `azure_storage_use_emulator`
/// - `object_store_use_emulator`
/// - `use_emulator`
UseEmulator,
/// Override the endpoint used to communicate with blob storage
///
/// Supported keys:
/// - `azure_storage_endpoint`
/// - `azure_endpoint`
/// - `endpoint`
Endpoint,
/// Use object store with url scheme account.dfs.fabric.microsoft.com
///
/// Supported keys:
/// - `azure_use_fabric_endpoint`
/// - `use_fabric_endpoint`
UseFabricEndpoint,
/// Endpoint to request a imds managed identity token
///
/// Supported keys:
/// - `azure_msi_endpoint`
/// - `azure_identity_endpoint`
/// - `identity_endpoint`
/// - `msi_endpoint`
MsiEndpoint,
/// Object id for use with managed identity authentication
///
/// Supported keys:
/// - `azure_object_id`
/// - `object_id`
ObjectId,
/// Msi resource id for use with managed identity authentication
///
/// Supported keys:
/// - `azure_msi_resource_id`
/// - `msi_resource_id`
MsiResourceId,
/// File containing token for Azure AD workload identity federation
///
/// Supported keys:
/// - `azure_federated_token_file`
/// - `federated_token_file`
FederatedTokenFile,
/// Use azure cli for acquiring access token
///
/// Supported keys:
/// - `azure_use_azure_cli`
/// - `use_azure_cli`
UseAzureCli,
/// Skip signing requests
///
/// Supported keys:
/// - `azure_skip_signature`
/// - `skip_signature`
SkipSignature,
/// Container name
///
/// Supported keys:
/// - `azure_container_name`
/// - `container_name`
ContainerName,
/// Disables tagging objects
///
/// This can be desirable if not supported by the backing store
///
/// Supported keys:
/// - `azure_disable_tagging`
/// - `disable_tagging`
DisableTagging,
/// Fabric token service url
///
/// Supported keys:
/// - `azure_fabric_token_service_url`
/// - `fabric_token_service_url`
FabricTokenServiceUrl,
/// Fabric workload host
///
/// Supported keys:
/// - `azure_fabric_workload_host`
/// - `fabric_workload_host`
FabricWorkloadHost,
/// Fabric session token
///
/// Supported keys:
/// - `azure_fabric_session_token`
/// - `fabric_session_token`
FabricSessionToken,
/// Fabric cluster identifier
///
/// Supported keys:
/// - `azure_fabric_cluster_identifier`
/// - `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:
/// - `azure_storage_encryption_key`
/// - `encryption_key`
EncryptionKey,
/// Client options
Client(ClientConfigKey),
}
impl AsRef<str> for AzureConfigKey {
fn as_ref(&self) -> &str {
match self {
Self::AccountName => "azure_storage_account_name",
Self::AccessKey => "azure_storage_account_key",
Self::ClientId => "azure_storage_client_id",
Self::ClientSecret => "azure_storage_client_secret",
Self::AuthorityId => "azure_storage_tenant_id",
Self::AuthorityHost => "azure_storage_authority_host",
Self::SasKey => "azure_storage_sas_key",
Self::Token => "azure_storage_token",
Self::UseEmulator => "azure_storage_use_emulator",
Self::UseFabricEndpoint => "azure_use_fabric_endpoint",
Self::Endpoint => "azure_storage_endpoint",
Self::MsiEndpoint => "azure_msi_endpoint",
Self::ObjectId => "azure_object_id",
Self::MsiResourceId => "azure_msi_resource_id",
Self::FederatedTokenFile => "azure_federated_token_file",
Self::UseAzureCli => "azure_use_azure_cli",
Self::SkipSignature => "azure_skip_signature",
Self::ContainerName => "azure_container_name",
Self::DisableTagging => "azure_disable_tagging",
Self::FabricTokenServiceUrl => "azure_fabric_token_service_url",
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(),
}
}
}
impl FromStr for AzureConfigKey {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"azure_storage_account_key"
| "azure_storage_access_key"
| "azure_storage_master_key"
| "master_key"
| "account_key"
| "access_key" => Ok(Self::AccessKey),
"azure_storage_account_name" | "account_name" => Ok(Self::AccountName),
"azure_storage_client_id" | "azure_client_id" | "client_id" => Ok(Self::ClientId),
"azure_storage_client_secret" | "azure_client_secret" | "client_secret" => {
Ok(Self::ClientSecret)
}
"azure_storage_tenant_id"
| "azure_storage_authority_id"
| "azure_tenant_id"
| "azure_authority_id"
| "tenant_id"
| "authority_id" => Ok(Self::AuthorityId),
"azure_storage_authority_host" | "azure_authority_host" | "authority_host" => {
Ok(Self::AuthorityHost)
}
"azure_storage_sas_key" | "azure_storage_sas_token" | "sas_key" | "sas_token" => {
Ok(Self::SasKey)
}
"azure_storage_token" | "bearer_token" | "token" => Ok(Self::Token),
"azure_storage_use_emulator" | "use_emulator" => Ok(Self::UseEmulator),
"azure_storage_endpoint" | "azure_endpoint" | "endpoint" => Ok(Self::Endpoint),
"azure_msi_endpoint"
| "azure_identity_endpoint"
| "identity_endpoint"
| "msi_endpoint" => Ok(Self::MsiEndpoint),
"azure_object_id" | "object_id" => Ok(Self::ObjectId),
"azure_msi_resource_id" | "msi_resource_id" => Ok(Self::MsiResourceId),
"azure_federated_token_file" | "federated_token_file" => Ok(Self::FederatedTokenFile),
"azure_use_fabric_endpoint" | "use_fabric_endpoint" => Ok(Self::UseFabricEndpoint),
"azure_use_azure_cli" | "use_azure_cli" => Ok(Self::UseAzureCli),
"azure_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
"azure_container_name" | "container_name" => Ok(Self::ContainerName),
"azure_disable_tagging" | "disable_tagging" => Ok(Self::DisableTagging),
"azure_fabric_token_service_url" | "fabric_token_service_url" => {
Ok(Self::FabricTokenServiceUrl)
}
"azure_fabric_workload_host" | "fabric_workload_host" => Ok(Self::FabricWorkloadHost),
"azure_fabric_session_token" | "fabric_session_token" => Ok(Self::FabricSessionToken),
"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)),
_ => match s.strip_prefix("azure_").unwrap_or(s).parse() {
Ok(key) => Ok(Self::Client(key)),
Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
},
}
}
}
impl std::fmt::Debug for MicrosoftAzureBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"MicrosoftAzureBuilder {{ account: {:?}, container_name: {:?} }}",
self.account_name, self.container_name
)
}
}
impl MicrosoftAzureBuilder {
/// Create a new [`MicrosoftAzureBuilder`] with default values.
pub fn new() -> Self {
Default::default()
}
/// Create an instance of [`MicrosoftAzureBuilder`] with values pre-populated from environment variables.
///
/// Variables extracted from environment:
/// * AZURE_STORAGE_ACCOUNT_NAME: storage account name
/// * AZURE_STORAGE_ACCOUNT_KEY: storage account master key
/// * AZURE_STORAGE_ACCESS_KEY: alias for AZURE_STORAGE_ACCOUNT_KEY
/// * AZURE_STORAGE_CLIENT_ID -> client id for service principal authorization
/// * AZURE_STORAGE_CLIENT_SECRET -> client secret for service principal authorization
/// * AZURE_STORAGE_TENANT_ID -> tenant id used in oauth flows
/// # Example
/// ```
/// use object_store::azure::MicrosoftAzureBuilder;
///
/// let azure = MicrosoftAzureBuilder::from_env()
/// .with_container_name("foo")
/// .build();
/// ```
pub fn from_env() -> Self {
let mut builder = Self::default();
for (os_key, os_value) in std::env::vars_os() {
if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
if key.starts_with("AZURE_") {
if let Ok(config_key) = key.to_ascii_lowercase().parse() {
builder = builder.with_config(config_key, value);
}
}
}
}
if let Ok(text) = std::env::var(MSI_ENDPOINT_ENV_KEY) {
builder = builder.with_msi_endpoint(text);
}
builder
}
/// Parse available connection info form a well-known storage URL.
///
/// The supported url schemes are:
///
/// - `abfs[s]://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
/// - `abfs[s]://<file_system>@<account_name>.dfs.core.windows.net/<path>`
/// - `abfs[s]://<file_system>@<account_name>.dfs.fabric.microsoft.com/<path>`
/// - `az://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
/// - `adl://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
/// - `azure://<container>/<path>` (custom)
/// - `https://<account>.dfs.core.windows.net`
/// - `https://<account>.blob.core.windows.net`
/// - `https://<account>.blob.core.windows.net/<container>`
/// - `https://<account>.dfs.fabric.microsoft.com`
/// - `https://<account>.dfs.fabric.microsoft.com/<container>`
/// - `https://<account>.blob.fabric.microsoft.com`
/// - `https://<account>.blob.fabric.microsoft.com/<container>`
///
/// Note: Settings derived from the URL will override any others set on this builder
///
/// # Example
/// ```
/// use object_store::azure::MicrosoftAzureBuilder;
///
/// let azure = MicrosoftAzureBuilder::from_env()
/// .with_url("abfss://file_system@account.dfs.core.windows.net/")
/// .build();
/// ```
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
/// Set an option on the builder via a key - value pair.
pub fn with_config(mut self, key: AzureConfigKey, value: impl Into<String>) -> Self {
match key {
AzureConfigKey::AccessKey => self.access_key = Some(value.into()),
AzureConfigKey::AccountName => self.account_name = Some(value.into()),
AzureConfigKey::ClientId => self.client_id = Some(value.into()),
AzureConfigKey::ClientSecret => self.client_secret = Some(value.into()),
AzureConfigKey::AuthorityId => self.tenant_id = Some(value.into()),
AzureConfigKey::AuthorityHost => self.authority_host = Some(value.into()),
AzureConfigKey::SasKey => self.sas_key = Some(value.into()),
AzureConfigKey::Token => self.bearer_token = Some(value.into()),
AzureConfigKey::MsiEndpoint => self.msi_endpoint = Some(value.into()),
AzureConfigKey::ObjectId => self.object_id = Some(value.into()),
AzureConfigKey::MsiResourceId => self.msi_resource_id = Some(value.into()),
AzureConfigKey::FederatedTokenFile => self.federated_token_file = Some(value.into()),
AzureConfigKey::UseAzureCli => self.use_azure_cli.parse(value),
AzureConfigKey::SkipSignature => self.skip_signature.parse(value),
AzureConfigKey::UseEmulator => self.use_emulator.parse(value),
AzureConfigKey::Endpoint => self.endpoint = Some(value.into()),
AzureConfigKey::UseFabricEndpoint => self.use_fabric_endpoint.parse(value),
AzureConfigKey::Client(key) => {
self.client_options = self.client_options.with_config(key, value)
}
AzureConfigKey::ContainerName => self.container_name = Some(value.into()),
AzureConfigKey::DisableTagging => self.disable_tagging.parse(value),
AzureConfigKey::FabricTokenServiceUrl => {
self.fabric_token_service_url = Some(value.into())
}
AzureConfigKey::FabricWorkloadHost => self.fabric_workload_host = Some(value.into()),
AzureConfigKey::FabricSessionToken => self.fabric_session_token = Some(value.into()),
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
}
/// Get config value via a [`AzureConfigKey`].
///
/// # Example
/// ```
/// use object_store::azure::{MicrosoftAzureBuilder, AzureConfigKey};
///
/// let builder = MicrosoftAzureBuilder::from_env()
/// .with_account("foo");
/// let account_name = builder.get_config_value(&AzureConfigKey::AccountName).unwrap_or_default();
/// assert_eq!("foo", &account_name);
/// ```
pub fn get_config_value(&self, key: &AzureConfigKey) -> Option<String> {
match key {
AzureConfigKey::AccountName => self.account_name.clone(),
AzureConfigKey::AccessKey => self.access_key.clone(),
AzureConfigKey::ClientId => self.client_id.clone(),
AzureConfigKey::ClientSecret => self.client_secret.clone(),
AzureConfigKey::AuthorityId => self.tenant_id.clone(),
AzureConfigKey::AuthorityHost => self.authority_host.clone(),
AzureConfigKey::SasKey => self.sas_key.clone(),
AzureConfigKey::Token => self.bearer_token.clone(),
AzureConfigKey::UseEmulator => Some(self.use_emulator.to_string()),
AzureConfigKey::UseFabricEndpoint => Some(self.use_fabric_endpoint.to_string()),
AzureConfigKey::Endpoint => self.endpoint.clone(),
AzureConfigKey::MsiEndpoint => self.msi_endpoint.clone(),
AzureConfigKey::ObjectId => self.object_id.clone(),
AzureConfigKey::MsiResourceId => self.msi_resource_id.clone(),
AzureConfigKey::FederatedTokenFile => self.federated_token_file.clone(),
AzureConfigKey::UseAzureCli => Some(self.use_azure_cli.to_string()),
AzureConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
AzureConfigKey::Client(key) => self.client_options.get_config_value(key),
AzureConfigKey::ContainerName => self.container_name.clone(),
AzureConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
AzureConfigKey::FabricTokenServiceUrl => self.fabric_token_service_url.clone(),
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(),
}
}
/// Sets properties on this builder based on a URL
///
/// This is a separate member function to allow fallible computation to
/// be deferred until [`Self::build`] which in turn allows deriving [`Clone`]
fn parse_url(&mut self, url: &str) -> Result<()> {
let parsed = Url::parse(url).map_err(|source| {
let url = url.into();
Error::UnableToParseUrl { url, source }
})?;
let host = parsed
.host_str()
.ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
let validate = |s: &str| match s.contains('.') {
true => Err(Error::UrlNotRecognised { url: url.into() }),
false => Ok(s.to_string()),
};
match parsed.scheme() {
"adl" | "azure" => self.container_name = Some(validate(host)?),
"az" | "abfs" | "abfss" => {
// abfs(s) might refer to the fsspec convention abfs://<container>/<path>
// or the convention for the hadoop driver abfs[s]://<file_system>@<account_name>.dfs.core.windows.net/<path>
if parsed.username().is_empty() {
self.container_name = Some(validate(host)?);
} else {
match host.split_once('.') {
// Workspace-level Private Link detection
// "{workspaceid}.z??.(onelake|dfs|blob).fabric.microsoft.com"
Some((workspaceid, rest))
if rest.starts_with('z') && rest.ends_with("fabric.microsoft.com") =>
{
// Account name for WS-PL is two labels: "{workspaceid}.z{xy}"
let (zone, _) = rest.split_once('.').unwrap_or((rest, ""));
self.account_name = Some(format!("{workspaceid}.{zone}"));
self.endpoint = Some(format!("https://{}", host));
self.container_name = Some(validate(parsed.username())?);
self.use_fabric_endpoint = true.into();
}
Some((a, "dfs.core.windows.net")) | Some((a, "blob.core.windows.net")) => {
self.account_name = Some(validate(a)?);
self.container_name = Some(validate(parsed.username())?);
}
Some((a, "dfs.fabric.microsoft.com"))
| Some((a, "blob.fabric.microsoft.com")) => {
self.account_name = Some(validate(a)?);
self.container_name = Some(validate(parsed.username())?);
self.use_fabric_endpoint = true.into();
}
_ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
}
}
}
"https" => match host.split_once('.') {
// Workspace-level Private Link detection
// "{workspaceid}.z??.(onelake|dfs|blob).fabric.microsoft.com"
Some((workspaceid, rest))
if rest.starts_with('z') && rest.ends_with("fabric.microsoft.com") =>
{
// rest looks like: "z28.dfs.fabric.microsoft.com" / "z28.blob.fabric.microsoft.com" / etc.
// Account name for WS-PL is two labels: "{workspaceid}.z{xy}"
let (zone, _) = rest.split_once('.').unwrap_or((rest, ""));
self.account_name = Some(format!("{workspaceid}.{zone}"));
self.endpoint = Some(format!("https://{}", host));
// Attempt to infer the container name from the URL
let container = parsed.path_segments().unwrap().next().expect(
"iterator always contains at least one string (which may be empty)",
);
if !container.is_empty() {
self.container_name = Some(validate(container)?);
}
self.use_fabric_endpoint = true.into();
}
Some((a, "dfs.core.windows.net")) | Some((a, "blob.core.windows.net")) => {
self.account_name = Some(validate(a)?);
let container = parsed.path_segments().unwrap().next().expect(
"iterator always contains at least one string (which may be empty)",
);
if !container.is_empty() {
self.container_name = Some(validate(container)?);
}
}
Some((a, "dfs.fabric.microsoft.com")) | Some((a, "blob.fabric.microsoft.com")) => {
self.account_name = Some(validate(a)?);
// Attempt to infer the container name from the URL
// - https://onelake.dfs.fabric.microsoft.com/<workspaceGUID>/<itemGUID>/Files/test.csv
// - https://onelake.dfs.fabric.microsoft.com/<workspace>/<item>.<itemtype>/<path>/<fileName>
//
// See <https://learn.microsoft.com/en-us/fabric/onelake/onelake-access-api>
let workspace = parsed.path_segments().unwrap().next().expect(
"iterator always contains at least one string (which may be empty)",
);
if !workspace.is_empty() {
self.container_name = Some(workspace.to_string())
}
self.use_fabric_endpoint = true.into();
}
_ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
},
scheme => {
let scheme = scheme.into();
return Err(Error::UnknownUrlScheme { scheme }.into());
}
}
Ok(())
}
/// Set the Azure Account (required)
pub fn with_account(mut self, account: impl Into<String>) -> Self {
self.account_name = Some(account.into());
self
}
/// Set the Azure Container Name (required)
pub fn with_container_name(mut self, container_name: impl Into<String>) -> Self {
self.container_name = Some(container_name.into());
self
}
/// Set the Azure Access Key (required - one of access key, bearer token, or client credentials)
pub fn with_access_key(mut self, access_key: impl Into<String>) -> Self {
self.access_key = Some(access_key.into());
self
}
/// Set a static bearer token to be used for authorizing requests
pub fn with_bearer_token_authorization(mut self, bearer_token: impl Into<String>) -> Self {
self.bearer_token = Some(bearer_token.into());
self
}
/// Set a client secret used for client secret authorization
pub fn with_client_secret_authorization(
mut self,
client_id: impl Into<String>,
client_secret: impl Into<String>,
tenant_id: impl Into<String>,
) -> Self {
self.client_id = Some(client_id.into());
self.client_secret = Some(client_secret.into());
self.tenant_id = Some(tenant_id.into());
self
}
/// Sets the client id for use in client secret or k8s federated credential flow
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
/// Sets the client secret for use in client secret flow
pub fn with_client_secret(mut self, client_secret: impl Into<String>) -> Self {
self.client_secret = Some(client_secret.into());
self
}
/// Sets the tenant id for use in client secret or k8s federated credential flow
pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
self.tenant_id = Some(tenant_id.into());
self
}
/// Set query pairs appended to the url for shared access signature authorization
pub fn with_sas_authorization(mut self, query_pairs: impl Into<Vec<(String, String)>>) -> Self {
self.sas_query_pairs = Some(query_pairs.into());
self
}
/// Set the credential provider overriding any other options
pub fn with_credentials(mut self, credentials: AzureCredentialProvider) -> Self {
self.credentials = Some(credentials);
self
}
/// The [`CryptoProvider`] to use
pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>) -> Self {
self.crypto = Some(provider);
self
}
/// Set if the Azure emulator should be used (defaults to false)
pub fn with_use_emulator(mut self, use_emulator: bool) -> Self {
self.use_emulator = use_emulator.into();
self
}
/// Override the endpoint used to communicate with blob storage
///
/// Defaults to `https://{account}.blob.core.windows.net`
///
/// By default, only HTTPS schemes are enabled. To connect to an HTTP endpoint, enable
/// [`Self::with_allow_http`].
pub fn with_endpoint(mut self, endpoint: String) -> Self {
self.endpoint = Some(endpoint);
self
}
/// Set if Microsoft Fabric url scheme should be used (defaults to false)
///
/// When disabled the url scheme used is `https://{account}.blob.core.windows.net`
/// When enabled the url scheme used is `https://{account}.dfs.fabric.microsoft.com`
///
/// Note: [`Self::with_endpoint`] will take precedence over this option
pub fn with_use_fabric_endpoint(mut self, use_fabric_endpoint: bool) -> Self {
self.use_fabric_endpoint = use_fabric_endpoint.into();
self
}
/// Sets what protocol is allowed
///
/// If `allow_http` is :
/// * false (default): Only HTTPS are allowed
/// * true: HTTP and HTTPS are allowed
pub fn with_allow_http(mut self, allow_http: bool) -> Self {
self.client_options = self.client_options.with_allow_http(allow_http);
self
}
/// Sets an alternative authority host for OAuth based authorization
///
/// Common hosts for azure clouds are defined in [authority_hosts](crate::azure::authority_hosts).
///
/// Defaults to <https://login.microsoftonline.com>
pub fn with_authority_host(mut self, authority_host: impl Into<String>) -> Self {
self.authority_host = Some(authority_host.into());
self
}
/// Set the retry configuration
pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
self.retry_config = retry_config;
self
}
/// Set the proxy_url to be used by the underlying client
pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
self.client_options = self.client_options.with_proxy_url(proxy_url);
self
}
/// Set a trusted proxy CA certificate
pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
self.client_options = self
.client_options
.with_proxy_ca_certificate(proxy_ca_certificate);
self
}
/// Set a list of hosts to exclude from proxy connections
pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
self
}
/// Sets the client options, overriding any already set
pub fn with_client_options(mut self, options: ClientOptions) -> Self {
self.client_options = options;
self
}
/// Sets the endpoint for acquiring managed identity token
pub fn with_msi_endpoint(mut self, msi_endpoint: impl Into<String>) -> Self {
self.msi_endpoint = Some(msi_endpoint.into());
self
}
/// Sets a file path for acquiring azure federated identity token in k8s
///
/// requires `client_id` and `tenant_id` to be set
pub fn with_federated_token_file(mut self, federated_token_file: impl Into<String>) -> Self {
self.federated_token_file = Some(federated_token_file.into());
self
}
/// Set if the Azure Cli should be used for acquiring access token
///
/// <https://learn.microsoft.com/en-us/cli/azure/account?view=azure-cli-latest#az-account-get-access-token>
pub fn with_use_azure_cli(mut self, use_azure_cli: bool) -> Self {
self.use_azure_cli = use_azure_cli.into();
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
pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
self.skip_signature = skip_signature.into();
self
}
/// If set to `true` will ignore any tags provided to put_opts
pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
self.disable_tagging = ignore.into();
self
}
/// Set the customer-provided encryption key (CPK) used to encrypt blob content.
///
/// `key` must be a base64-encoded 256-bit AES key (the decoded value must be
/// exactly 32 bytes). The same key must be supplied on every subsequent read,
/// write, or copy of any blob created with it; if the key is lost or omitted
/// the data is unrecoverable. CPK material is sent to Azure on every request,
/// so the configured endpoint must use HTTPS.
///
/// Only a subset of Blob storage operations support CPK
/// (see the [Azure documentation][cpk-ops]). When CPK is enabled, `copy`
/// switches from the asynchronous `Copy Blob` API to `Put Blob From URL`,
/// which is synchronous and limits the source blob to 5,000 MiB.
///
/// [cpk-ops]: https://learn.microsoft.com/en-us/azure/storage/blobs/encryption-customer-provided-keys#blob-storage-operations-supporting-customer-provided-keys
pub fn with_encryption_key(mut self, key: impl Into<String>) -> Self {
self.encryption_key = Some(key.into());
self
}
/// The [`HttpConnector`] to use
///
/// On non-WASM32 platforms uses [`reqwest`] by default, on WASM32 platforms must be provided
pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
self.http_connector = Some(Arc::new(connector));
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
.take()
.ok_or(Error::MissingContainerName {})?;
let static_creds = |credential: AzureCredential| -> AzureCredentialProvider {
Arc::new(StaticCredentialProvider::new(credential))
};
let http = http_connector(self.http_connector.take())?;
let (is_emulator, storage_url, auth, account) = if self.use_emulator.get()? {
let account_name = self
.account_name
.unwrap_or_else(|| EMULATOR_ACCOUNT.to_string());
// Allow overriding defaults. Values taken from
// from https://docs.rs/azure_storage/0.2.0/src/azure_storage/core/clients/storage_account_client.rs.html#129-141
let url = url_from_env("AZURITE_BLOB_STORAGE_URL", "http://127.0.0.1:10000")?;
let credential = if let Some(k) = self.access_key {
AzureCredential::AccessKey(AzureAccessKey::try_new(&k)?)
} else if let Some(bearer_token) = self.bearer_token {
AzureCredential::BearerToken(bearer_token)
} else if let Some(query_pairs) = self.sas_query_pairs {
AzureCredential::SASToken(query_pairs)
} else if let Some(sas) = self.sas_key {
AzureCredential::SASToken(split_sas(&sas)?)
} else {
AzureCredential::AccessKey(AzureAccessKey::try_new(EMULATOR_ACCOUNT_KEY)?)
};
self.client_options = self.client_options.with_allow_http(true);
(true, url, static_creds(credential), account_name)
} else {
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 => {
format!("https://{}.blob.fabric.microsoft.com", account_name)
}
false => format!("https://{}.blob.core.windows.net", account_name),
},
};
let url = Url::parse(&account_url).map_err(|source| {
let url = account_url.clone();
Error::UnableToParseUrl { url, source }
})?;
let credential = if let Some(credential) = self.credentials {
credential
} else {
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)
};
let encryption_headers =
AzureEncryptionHeaders::try_new(self.crypto.as_deref(), self.encryption_key).map_err(
|source| Error::InvalidEncryptionKey {
source: match source {
crate::Error::Generic { source, .. } => source,
other => Box::new(other),
},
},
)?;
let config = AzureConfig {
account,
is_emulator,
skip_signature: self.skip_signature.get()?,
container,
disable_tagging: self.disable_tagging.get()?,
retry_config: self.retry_config,
client_options: self.client_options,
service: storage_url,
credentials: auth,
crypto: self.crypto,
encryption_headers,
};
let http_client = http.connect(&config.client_options)?;
let client = Arc::new(AzureClient::new(config, http_client));
Ok(MicrosoftAzure { client })
}
}
/// Parses the contents of the environment variable `env_name` as a URL
/// if present, otherwise falls back to default_url
fn url_from_env(env_name: &str, default_url: &str) -> Result<Url> {
let url = match std::env::var(env_name) {
Ok(env_value) => {
Url::parse(&env_value).map_err(|source| Error::UnableToParseEmulatorUrl {
env_name: env_name.into(),
env_value,
source,
})?
}
Err(_) => Url::parse(default_url).expect("Failed to parse default URL"),
};
Ok(url)
}
/// Parse a SAS token string into the query pairs expected by [`AzureCredential::SASToken`].
pub fn split_sas(sas: &str) -> Result<Vec<(String, String)>> {
let sas = percent_decode_str(sas)
.decode_utf8()
.map_err(|source| Error::DecodeSasKey { source })?;
let kv_str_pairs = sas
.trim_start_matches('?')
.split('&')
.filter(|s| !s.chars().all(char::is_whitespace));
let mut pairs = Vec::new();
for kv_pair_str in kv_str_pairs {
let (k, v) = kv_pair_str
.trim()
.split_once('=')
.ok_or(Error::MissingSasComponent {})?;
pairs.push((k.into(), v.into()))
}
Ok(pairs)
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use std::collections::HashMap;
#[test]
fn azure_blob_test_urls() {
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("abfss://file_system@account.dfs.core.windows.net/")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, Some("file_system".to_string()));
assert!(!builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("az://container@account.dfs.core.windows.net/path-part/file")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, Some("container".to_string()));
assert!(!builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("az://container@account.blob.core.windows.net/path-part/file")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, Some("container".to_string()));
assert!(!builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("az://container@account.dfs.fabric.microsoft.com/path-part/file")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, Some("container".to_string()));
assert!(builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("az://container@account.blob.fabric.microsoft.com/path-part/file")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, Some("container".to_string()));
assert!(builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("abfss://file_system@account.dfs.fabric.microsoft.com/")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, Some("file_system".to_string()));
assert!(builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder.parse_url("abfs://container/path").unwrap();
assert_eq!(builder.container_name, Some("container".to_string()));
let mut builder = MicrosoftAzureBuilder::new();
builder.parse_url("az://container").unwrap();
assert_eq!(builder.container_name, Some("container".to_string()));
let mut builder = MicrosoftAzureBuilder::new();
builder.parse_url("az://container/path").unwrap();
assert_eq!(builder.container_name, Some("container".to_string()));
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("https://account.dfs.core.windows.net/")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert!(!builder.use_fabric_endpoint.get().unwrap());
let mut builder =
MicrosoftAzureBuilder::new().with_container_name("explicit_container_name");
builder
.parse_url("https://account.blob.core.windows.net/")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(
builder.container_name,
Some("explicit_container_name".to_string())
);
assert!(!builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("https://account.blob.core.windows.net/container")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, Some("container".to_string()));
assert!(!builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("https://account.dfs.fabric.microsoft.com/")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, None);
assert!(builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("https://account.dfs.fabric.microsoft.com/container")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name.as_deref(), Some("container"));
assert!(builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("https://onelake.dfs.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456.lakehouse/Files/tables/sales/data.parquet")
.unwrap();
assert_eq!(builder.account_name, Some("onelake".to_string()));
assert_eq!(
builder.container_name.as_deref(),
Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3")
);
assert!(builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("https://account.blob.fabric.microsoft.com/")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name, None);
assert!(builder.use_fabric_endpoint.get().unwrap());
let mut builder = MicrosoftAzureBuilder::new();
builder
.parse_url("https://account.blob.fabric.microsoft.com/container")
.unwrap();
assert_eq!(builder.account_name, Some("account".to_string()));
assert_eq!(builder.container_name.as_deref(), Some("container"));
assert!(builder.use_fabric_endpoint.get().unwrap());
let err_cases = [
"mailto://account.blob.core.windows.net/",
"az://blob.mydomain/",
"abfs://container.foo/path",
"abfss://file_system@account.foo.dfs.core.windows.net/",
"abfss://file_system.bar@account.dfs.core.windows.net/",
"https://blob.mydomain/",
"https://blob.foo.dfs.core.windows.net/",
];
let mut builder = MicrosoftAzureBuilder::new();
for case in err_cases {
builder.parse_url(case).unwrap_err();
}
}
#[test]
fn azure_test_workspace_private_link() {
let test_cases: Vec<(&str, &str, Option<&str>)> = vec![
(
"https://Ab000000000000000000000000000000.zAb.dfs.fabric.microsoft.com/",
"ab000000000000000000000000000000.zab",
None,
),
(
"https://ab000000000000000000000000000000.zab.dfs.fabric.microsoft.com/",
"ab000000000000000000000000000000.zab",
None,
),
(
"https://c047b3e34e89407a98d7cf9949ae92a3.zc0.blob.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
"c047b3e34e89407a98d7cf9949ae92a3.zc0",
Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
),
(
"https://c047b3e34e89407a98d7cf9949ae92a3.zc0.dfs.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
"c047b3e34e89407a98d7cf9949ae92a3.zc0",
Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
),
(
"https://c047b3e34e89407a98d7cf9949ae92a3.zc0.onelake.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
"c047b3e34e89407a98d7cf9949ae92a3.zc0",
Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
),
(
"https://c047b3e34e89407a98d7cf9949ae92a3.zc0.w.api.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
"c047b3e34e89407a98d7cf9949ae92a3.zc0",
Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
),
(
"https://c047b3e34e89407a98d7cf9949ae92a3.zc0.c.api.fabric.microsoft.com/c047b3e3-4e89-407a-98d7-cf9949ae92a3/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
"c047b3e34e89407a98d7cf9949ae92a3.zc0",
Some("c047b3e3-4e89-407a-98d7-cf9949ae92a3"),
),
(
"abfss://c047b3e34e89407a98d7cf9949ae92a3@c047b3e34e89407a98d7cf9949ae92a3.zc0.dfs.fabric.microsoft.com/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
"c047b3e34e89407a98d7cf9949ae92a3.zc0",
Some("c047b3e34e89407a98d7cf9949ae92a3"),
),
(
"abfss://c047b3e34e89407a98d7cf9949ae92a3@c047b3e34e89407a98d7cf9949ae92a3.zc0.blob.fabric.microsoft.com/9f1a2b3c-4d5e-6f70-8a9b-c0d1e2f3a456/file",
"c047b3e34e89407a98d7cf9949ae92a3.zc0",
Some("c047b3e34e89407a98d7cf9949ae92a3"),
),
];
for (url, expected_account, expected_container) in &test_cases {
let mut builder = MicrosoftAzureBuilder::new();
builder.parse_url(url).unwrap();
assert_eq!(
builder.account_name.as_deref(),
Some(*expected_account),
"account mismatch for URL: {url}"
);
assert_eq!(
builder.container_name.as_deref(),
*expected_container,
"container mismatch for URL: {url}"
);
assert!(
builder.use_fabric_endpoint.get().unwrap(),
"use_fabric_endpoint not set for URL: {url}"
);
}
}
#[test]
fn azure_encryption_key_roundtrip() {
let key = BASE64_STANDARD.encode([7_u8; 32]);
let builder = MicrosoftAzureBuilder::new().with_encryption_key(&key);
assert_eq!(
builder
.get_config_value(&AzureConfigKey::EncryptionKey)
.as_deref(),
Some(key.as_str())
);
}
#[test]
fn azure_encryption_key_rejects_malformed_base64() {
let err = MicrosoftAzureBuilder::new()
.with_account("account")
.with_container_name("container")
.with_access_key(EMULATOR_ACCOUNT_KEY)
.with_encryption_key("not-base64!!!")
.build()
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Invalid encryption key") || msg.contains("Invalid byte"));
}
#[test]
fn azure_encryption_key_rejects_short_decoded_key() {
let err = MicrosoftAzureBuilder::new()
.with_account("account")
.with_container_name("container")
.with_access_key(EMULATOR_ACCOUNT_KEY)
.with_encryption_key(BASE64_STANDARD.encode([7_u8; 31]))
.build()
.unwrap_err();
assert!(err.to_string().contains("must decode to 32 bytes, got 31"));
}
#[test]
fn azure_encryption_key_rejects_long_decoded_key() {
let err = MicrosoftAzureBuilder::new()
.with_account("account")
.with_container_name("container")
.with_access_key(EMULATOR_ACCOUNT_KEY)
.with_encryption_key(BASE64_STANDARD.encode([7_u8; 33]))
.build()
.unwrap_err();
assert!(err.to_string().contains("must decode to 32 bytes, got 33"));
}
#[test]
fn azure_test_config_from_map() {
let azure_client_id = "object_store:fake_access_key_id";
let azure_storage_account_name = "object_store:fake_secret_key";
let azure_storage_token = "object_store:fake_default_region";
let options = HashMap::from([
("azure_client_id", azure_client_id),
("azure_storage_account_name", azure_storage_account_name),
("azure_storage_token", azure_storage_token),
]);
let builder = options
.into_iter()
.fold(MicrosoftAzureBuilder::new(), |builder, (key, value)| {
builder.with_config(key.parse().unwrap(), value)
});
assert_eq!(builder.client_id.unwrap(), azure_client_id);
assert_eq!(builder.account_name.unwrap(), azure_storage_account_name);
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";
let expected = vec![
("sv".to_string(), "2021-10-04".to_string()),
("st".to_string(), "2023-01-04T17:48:57Z".to_string()),
("se".to_string(), "2023-01-04T18:15:00Z".to_string()),
("sr".to_string(), "c".to_string()),
("sp".to_string(), "rcwl".to_string()),
(
"sig".to_string(),
"C7+ZeEOWbrxPA3R0Cw/w1EZz0+4KBvQexeKZKe+B6h0=".to_string(),
),
];
let pairs = split_sas(raw_sas).unwrap();
assert_eq!(expected, pairs);
}
#[test]
fn azure_test_split_sas_trims_leading_question_mark_and_skips_empties() {
let pairs = split_sas("?&sv=2021-10-04& &sp=r").unwrap();
assert_eq!(
pairs,
vec![
("sv".to_string(), "2021-10-04".to_string()),
("sp".to_string(), "r".to_string()),
],
);
}
#[test]
fn azure_test_split_sas_rejects_missing_equals() {
let err = split_sas("sv=2021-10-04&bogus").unwrap_err();
assert!(
err.to_string().contains("Missing component"),
"unexpected error: {err}",
);
}
#[test]
fn azure_test_client_opts() {
let key = "AZURE_PROXY_URL";
if let Ok(config_key) = key.to_ascii_lowercase().parse() {
assert_eq!(
AzureConfigKey::Client(ClientConfigKey::ProxyUrl),
config_key
);
} else {
panic!("{key} not propagated as ClientConfigKey");
}
}
}