1use 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
33const EMULATOR_ACCOUNT: &str = "devstoreaccount1";
37
38const EMULATOR_ACCOUNT_KEY: &str =
42 "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
43
44const MSI_ENDPOINT_ENV_KEY: &str = "IDENTITY_ENDPOINT";
45
46#[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#[derive(Default, Clone)]
139pub struct MicrosoftAzureBuilder {
140 account_name: Option<String>,
142 access_key: Option<String>,
144 container_name: Option<String>,
146 bearer_token: Option<String>,
148 client_id: Option<String>,
150 client_secret: Option<String>,
152 tenant_id: Option<String>,
154 sas_query_pairs: Option<Vec<(String, String)>>,
156 sas_key: Option<String>,
158 authority_host: Option<String>,
160 url: Option<String>,
162 use_emulator: ConfigValue<bool>,
164 endpoint: Option<String>,
166 msi_endpoint: Option<String>,
168 object_id: Option<String>,
170 msi_resource_id: Option<String>,
172 federated_token_file: Option<String>,
174 use_azure_cli: ConfigValue<bool>,
176 retry_config: RetryConfig,
178 client_options: ClientOptions,
180 credentials: Option<AzureCredentialProvider>,
182 crypto: Option<Arc<dyn CryptoProvider>>,
184 skip_signature: ConfigValue<bool>,
186 use_fabric_endpoint: ConfigValue<bool>,
190 disable_tagging: ConfigValue<bool>,
192 fabric_token_service_url: Option<String>,
194 fabric_workload_host: Option<String>,
196 fabric_session_token: Option<String>,
198 fabric_cluster_identifier: Option<String>,
200 credential_type: Option<String>,
202 encryption_key: Option<String>,
204 http_connector: Option<Arc<dyn HttpConnector>>,
206}
207
208#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Deserialize, Serialize)]
220#[non_exhaustive]
221pub enum AzureConfigKey {
222 AccountName,
228
229 AccessKey,
239
240 ClientId,
247
248 ClientSecret,
255
256 AuthorityId,
266
267 AuthorityHost,
274
275 SasKey,
286
287 Token,
294
295 UseEmulator,
302
303 Endpoint,
310
311 UseFabricEndpoint,
317
318 MsiEndpoint,
326
327 ObjectId,
333
334 MsiResourceId,
340
341 FederatedTokenFile,
347
348 UseAzureCli,
354
355 SkipSignature,
361
362 ContainerName,
368
369 DisableTagging,
377
378 FabricTokenServiceUrl,
384
385 FabricWorkloadHost,
391
392 FabricSessionToken,
398
399 FabricClusterIdentifier,
405
406 CredentialType,
426
427 EncryptionKey,
433
434 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 "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 pub fn new() -> Self {
547 Default::default()
548 }
549
550 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 pub fn with_url(mut self, url: impl Into<String>) -> Self {
615 self.url = Some(url.into());
616 self
617 }
618
619 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 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 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 if parsed.username().is_empty() {
725 self.container_name = Some(validate(host)?);
726 } else {
727 match host.split_once('.') {
728 Some((workspaceid, rest))
731 if rest.starts_with('z') && rest.ends_with("fabric.microsoft.com") =>
732 {
733 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 Some((workspaceid, rest))
762 if rest.starts_with('z') && rest.ends_with("fabric.microsoft.com") =>
763 {
764 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 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 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 pub fn with_account(mut self, account: impl Into<String>) -> Self {
819 self.account_name = Some(account.into());
820 self
821 }
822
823 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 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 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 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 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 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 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 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 pub fn with_credentials(mut self, credentials: AzureCredentialProvider) -> Self {
880 self.credentials = Some(credentials);
881 self
882 }
883
884 pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>) -> Self {
886 self.crypto = Some(provider);
887 self
888 }
889
890 pub fn with_use_emulator(mut self, use_emulator: bool) -> Self {
892 self.use_emulator = use_emulator.into();
893 self
894 }
895
896 pub fn with_endpoint(mut self, endpoint: String) -> Self {
903 self.endpoint = Some(endpoint);
904 self
905 }
906
907 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 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 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 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
940 self.retry_config = retry_config;
941 self
942 }
943
944 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 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 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 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
966 self.client_options = options;
967 self
968 }
969
970 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 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 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 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 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
1009 self.skip_signature = skip_signature.into();
1010 self
1011 }
1012
1013 pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1015 self.disable_tagging = ignore.into();
1016 self
1017 }
1018
1019 pub fn with_encryption_key(mut self, key: impl Into<String>) -> Self {
1034 self.encryption_key = Some(key.into());
1035 self
1036 }
1037
1038 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 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 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
1336fn 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
1352pub 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 let result = builder.build();
1700 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}