1use std::str::FromStr;
19use std::sync::Arc;
20
21use serde::{Deserialize, Serialize};
22use tokio::runtime::Handle;
23
24use super::AzureConfig;
25use crate::azure::credential::{
26 AzureCliCredential, ClientSecretOAuthProvider, ImdsManagedIdentityProvider,
27 WorkloadIdentityOAuthProvider,
28};
29use crate::azure::{AzureCredential, AzureCredentialProvider};
30use crate::config::ConfigValue;
31use crate::service::make_service;
32use crate::{
33 ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider,
34 TokenCredentialProvider,
35};
36
37const MSI_ENDPOINT_ENV_KEY: &str = "IDENTITY_ENDPOINT";
38
39#[derive(Debug, thiserror::Error)]
40enum Error {
41 #[error("OAuth scope must be set when using client secret authentication")]
42 MissingScope,
43
44 #[error("Configuration key: '{}' is not known.", key)]
45 UnknownConfigurationKey { key: String },
46}
47
48impl From<Error> for crate::Error {
49 fn from(source: Error) -> Self {
50 match source {
51 Error::UnknownConfigurationKey { key } => Self::UnknownConfigurationKey { key },
52 Error::MissingScope => Self::Generic {
53 source: Box::new(source),
54 },
55 }
56 }
57}
58
59#[derive(Default, Clone)]
72pub struct AzureBuilder {
73 account_name: Option<String>,
74 access_key: Option<String>,
75 bearer_token: Option<String>,
76 client_id: Option<String>,
77 client_secret: Option<String>,
78 tenant_id: Option<String>,
79 scope: Option<String>,
80 authority_host: Option<String>,
81 endpoint: Option<String>,
82 msi_endpoint: Option<String>,
83 object_id: Option<String>,
84 msi_resource_id: Option<String>,
85 federated_token_file: Option<String>,
86 use_azure_cli: ConfigValue<bool>,
87 retry_config: RetryConfig,
88 client_options: ClientOptions,
89 credentials: Option<AzureCredentialProvider>,
90 skip_signature: ConfigValue<bool>,
91}
92
93#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Deserialize, Serialize)]
105#[non_exhaustive]
106pub enum AzureConfigKey {
107 AccountName,
113
114 AccessKey,
124
125 ClientId,
132
133 ClientSecret,
140
141 AuthorityId,
151
152 AuthorityHost,
159
160 Scope,
166
167 Token,
174
175 Endpoint,
182
183 MsiEndpoint,
191
192 ObjectId,
198
199 MsiResourceId,
205
206 FederatedTokenFile,
212
213 UseAzureCli,
219
220 SkipSignature,
226
227 Client(ClientConfigKey),
229}
230
231impl AsRef<str> for AzureConfigKey {
232 fn as_ref(&self) -> &str {
233 match self {
234 Self::AccountName => "azure_storage_account_name",
235 Self::AccessKey => "azure_storage_account_key",
236 Self::ClientId => "azure_storage_client_id",
237 Self::ClientSecret => "azure_storage_client_secret",
238 Self::AuthorityId => "azure_storage_tenant_id",
239 Self::AuthorityHost => "azure_storage_authority_host",
240 Self::Scope => "azure_scope",
241 Self::Token => "azure_storage_token",
242 Self::Endpoint => "azure_storage_endpoint",
243 Self::MsiEndpoint => "azure_msi_endpoint",
244 Self::ObjectId => "azure_object_id",
245 Self::MsiResourceId => "azure_msi_resource_id",
246 Self::FederatedTokenFile => "azure_federated_token_file",
247 Self::UseAzureCli => "azure_use_azure_cli",
248 Self::SkipSignature => "azure_skip_signature",
249 Self::Client(key) => key.as_ref(),
250 }
251 }
252}
253
254impl FromStr for AzureConfigKey {
255 type Err = crate::Error;
256
257 fn from_str(s: &str) -> Result<Self, Self::Err> {
258 match s {
259 "azure_storage_account_key"
260 | "azure_storage_access_key"
261 | "azure_storage_master_key"
262 | "master_key"
263 | "account_key"
264 | "access_key" => Ok(Self::AccessKey),
265 "azure_storage_account_name" | "account_name" => Ok(Self::AccountName),
266 "azure_storage_client_id" | "azure_client_id" | "client_id" => Ok(Self::ClientId),
267 "azure_storage_client_secret" | "azure_client_secret" | "client_secret" => {
268 Ok(Self::ClientSecret)
269 }
270 "azure_storage_tenant_id"
271 | "azure_storage_authority_id"
272 | "azure_tenant_id"
273 | "azure_authority_id"
274 | "tenant_id"
275 | "authority_id" => Ok(Self::AuthorityId),
276 "azure_storage_authority_host" | "azure_authority_host" | "authority_host" => {
277 Ok(Self::AuthorityHost)
278 }
279 "azure_scope" | "scope" => Ok(Self::Scope),
280 "azure_storage_token" | "bearer_token" | "token" => Ok(Self::Token),
281 "azure_storage_endpoint" | "azure_endpoint" | "endpoint" => Ok(Self::Endpoint),
282 "azure_msi_endpoint"
283 | "azure_identity_endpoint"
284 | "identity_endpoint"
285 | "msi_endpoint" => Ok(Self::MsiEndpoint),
286 "azure_object_id" | "object_id" => Ok(Self::ObjectId),
287 "azure_msi_resource_id" | "msi_resource_id" => Ok(Self::MsiResourceId),
288 "azure_federated_token_file" | "federated_token_file" => Ok(Self::FederatedTokenFile),
289 "azure_use_azure_cli" | "use_azure_cli" => Ok(Self::UseAzureCli),
290 "azure_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
291 "azure_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
292 _ => match s.strip_prefix("azure_").unwrap_or(s).parse() {
293 Ok(key) => Ok(Self::Client(key)),
294 Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
295 },
296 }
297 }
298}
299
300impl std::fmt::Debug for AzureBuilder {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 write!(f, "AzureBuilder {{ account: {:?} }}", self.account_name)
303 }
304}
305
306impl AzureBuilder {
307 pub fn new() -> Self {
309 Default::default()
310 }
311
312 pub fn from_env() -> Self {
322 let mut builder = Self::default();
323 for (os_key, os_value) in std::env::vars_os() {
324 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str())
325 && key.starts_with("AZURE_")
326 && let Ok(config_key) = key.to_ascii_lowercase().parse()
327 {
328 builder = builder.with_config(config_key, value);
329 }
330 }
331
332 if let Ok(text) = std::env::var(MSI_ENDPOINT_ENV_KEY) {
333 builder = builder.with_msi_endpoint(text);
334 }
335
336 builder
337 }
338
339 pub fn with_config(mut self, key: AzureConfigKey, value: impl Into<String>) -> Self {
341 match key {
342 AzureConfigKey::AccessKey => self.access_key = Some(value.into()),
343 AzureConfigKey::AccountName => self.account_name = Some(value.into()),
344 AzureConfigKey::ClientId => self.client_id = Some(value.into()),
345 AzureConfigKey::ClientSecret => self.client_secret = Some(value.into()),
346 AzureConfigKey::AuthorityId => self.tenant_id = Some(value.into()),
347 AzureConfigKey::AuthorityHost => self.authority_host = Some(value.into()),
348 AzureConfigKey::Scope => self.scope = Some(value.into()),
349 AzureConfigKey::Token => self.bearer_token = Some(value.into()),
350 AzureConfigKey::MsiEndpoint => self.msi_endpoint = Some(value.into()),
351 AzureConfigKey::ObjectId => self.object_id = Some(value.into()),
352 AzureConfigKey::MsiResourceId => self.msi_resource_id = Some(value.into()),
353 AzureConfigKey::FederatedTokenFile => self.federated_token_file = Some(value.into()),
354 AzureConfigKey::UseAzureCli => self.use_azure_cli.parse(value),
355 AzureConfigKey::SkipSignature => self.skip_signature.parse(value),
356 AzureConfigKey::Endpoint => self.endpoint = Some(value.into()),
357 AzureConfigKey::Client(key) => {
358 self.client_options = self.client_options.with_config(key, value)
359 }
360 };
361 self
362 }
363
364 pub fn get_config_value(&self, key: &AzureConfigKey) -> Option<String> {
366 match key {
367 AzureConfigKey::AccountName => self.account_name.clone(),
368 AzureConfigKey::AccessKey => self.access_key.clone(),
369 AzureConfigKey::ClientId => self.client_id.clone(),
370 AzureConfigKey::ClientSecret => self.client_secret.clone(),
371 AzureConfigKey::AuthorityId => self.tenant_id.clone(),
372 AzureConfigKey::AuthorityHost => self.authority_host.clone(),
373 AzureConfigKey::Scope => self.scope.clone(),
374 AzureConfigKey::Token => self.bearer_token.clone(),
375 AzureConfigKey::Endpoint => self.endpoint.clone(),
376 AzureConfigKey::MsiEndpoint => self.msi_endpoint.clone(),
377 AzureConfigKey::ObjectId => self.object_id.clone(),
378 AzureConfigKey::MsiResourceId => self.msi_resource_id.clone(),
379 AzureConfigKey::FederatedTokenFile => self.federated_token_file.clone(),
380 AzureConfigKey::UseAzureCli => Some(self.use_azure_cli.to_string()),
381 AzureConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
382 AzureConfigKey::Client(key) => self.client_options.get_config_value(key),
383 }
384 }
385
386 pub fn with_account(mut self, account: impl Into<String>) -> Self {
388 self.account_name = Some(account.into());
389 self
390 }
391
392 pub fn with_access_key(mut self, access_key: impl Into<String>) -> Self {
394 self.access_key = Some(access_key.into());
395 self
396 }
397
398 pub fn with_bearer_token_authorization(mut self, bearer_token: impl Into<String>) -> Self {
400 self.bearer_token = Some(bearer_token.into());
401 self
402 }
403
404 pub fn with_client_secret_authorization(
406 mut self,
407 client_id: impl Into<String>,
408 client_secret: impl Into<String>,
409 tenant_id: impl Into<String>,
410 ) -> Self {
411 self.client_id = Some(client_id.into());
412 self.client_secret = Some(client_secret.into());
413 self.tenant_id = Some(tenant_id.into());
414 self
415 }
416
417 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
419 self.client_id = Some(client_id.into());
420 self
421 }
422
423 pub fn with_client_secret(mut self, client_secret: impl Into<String>) -> Self {
425 self.client_secret = Some(client_secret.into());
426 self
427 }
428
429 pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
431 self.tenant_id = Some(tenant_id.into());
432 self
433 }
434
435 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
437 self.scope = Some(scope.into());
438 self
439 }
440
441 pub fn with_credentials(mut self, credentials: AzureCredentialProvider) -> Self {
443 self.credentials = Some(credentials);
444 self
445 }
446
447 pub fn with_endpoint(mut self, endpoint: String) -> Self {
449 self.endpoint = Some(endpoint);
450 self
451 }
452
453 pub fn with_allow_http(mut self, allow_http: bool) -> Self {
455 self.client_options = self.client_options.with_allow_http(allow_http);
456 self
457 }
458
459 pub fn with_authority_host(mut self, authority_host: impl Into<String>) -> Self {
463 self.authority_host = Some(authority_host.into());
464 self
465 }
466
467 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
469 self.retry_config = retry_config;
470 self
471 }
472
473 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
475 self.client_options = self.client_options.with_proxy_url(proxy_url);
476 self
477 }
478
479 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
481 self.client_options = self
482 .client_options
483 .with_proxy_ca_certificate(proxy_ca_certificate);
484 self
485 }
486
487 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
489 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
490 self
491 }
492
493 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
495 self.client_options = options;
496 self
497 }
498
499 pub fn with_msi_endpoint(mut self, msi_endpoint: impl Into<String>) -> Self {
501 self.msi_endpoint = Some(msi_endpoint.into());
502 self
503 }
504
505 pub fn with_federated_token_file(mut self, federated_token_file: impl Into<String>) -> Self {
507 self.federated_token_file = Some(federated_token_file.into());
508 self
509 }
510
511 pub fn with_use_azure_cli(mut self, use_azure_cli: bool) -> Self {
513 self.use_azure_cli = use_azure_cli.into();
514 self
515 }
516
517 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
519 self.skip_signature = skip_signature.into();
520 self
521 }
522
523 pub fn build(self, runtime: Option<&Handle>) -> Result<AzureConfig> {
529 let static_creds = |credential: AzureCredential| -> AzureCredentialProvider {
530 Arc::new(StaticCredentialProvider::new(credential))
531 };
532
533 let auth = if let Some(credential) = self.credentials {
534 credential
535 } else if let Some(bearer_token) = self.bearer_token {
536 static_creds(AzureCredential::BearerToken(bearer_token))
537 } else if let (Some(client_id), Some(tenant_id), Some(federated_token_file)) =
538 (&self.client_id, &self.tenant_id, self.federated_token_file)
539 {
540 let client_credential = WorkloadIdentityOAuthProvider::new(
541 client_id,
542 federated_token_file,
543 tenant_id,
544 self.authority_host,
545 );
546 let client = self.client_options.client()?;
547 let service = make_service(client.clone(), runtime);
548 Arc::new(TokenCredentialProvider::new(
549 client_credential,
550 client,
551 service,
552 self.retry_config.clone(),
553 )) as _
554 } else if let (Some(client_id), Some(client_secret), Some(tenant_id)) =
555 (&self.client_id, self.client_secret, &self.tenant_id)
556 {
557 let scope = self.scope.ok_or(Error::MissingScope)?;
558 let client_credential = ClientSecretOAuthProvider::new_with_scope(
559 client_id.clone(),
560 client_secret,
561 tenant_id,
562 self.authority_host,
563 &scope,
564 );
565 let client = self.client_options.client()?;
566 let service = make_service(client.clone(), runtime);
567 Arc::new(TokenCredentialProvider::new(
568 client_credential,
569 client,
570 service,
571 self.retry_config.clone(),
572 )) as _
573 } else if self.use_azure_cli.get()? {
574 Arc::new(AzureCliCredential::new()) as _
575 } else {
576 let msi_credential = ImdsManagedIdentityProvider::new(
577 self.client_id,
578 self.object_id,
579 self.msi_resource_id,
580 self.msi_endpoint,
581 );
582 let client = self.client_options.metadata_client()?;
583 let service = make_service(client.clone(), runtime);
584 Arc::new(TokenCredentialProvider::new(
585 msi_credential,
586 client,
587 service,
588 self.retry_config.clone(),
589 )) as _
590 };
591
592 Ok(AzureConfig {
593 skip_signature: self.skip_signature.get()?,
594 retry_config: self.retry_config,
595 client_options: self.client_options,
596 credentials: auth,
597 })
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use std::collections::HashMap;
605
606 #[test]
607 fn azure_test_config_from_map() {
608 let azure_client_id = "object_store:fake_access_key_id";
609 let azure_storage_account_name = "object_store:fake_secret_key";
610 let azure_storage_token = "object_store:fake_default_region";
611 let options = HashMap::from([
612 ("azure_client_id", azure_client_id),
613 ("azure_storage_account_name", azure_storage_account_name),
614 ("azure_storage_token", azure_storage_token),
615 ]);
616
617 let builder = options
618 .into_iter()
619 .fold(AzureBuilder::new(), |builder, (key, value)| {
620 builder.with_config(key.parse().unwrap(), value)
621 });
622 assert_eq!(builder.client_id.unwrap(), azure_client_id);
623 assert_eq!(builder.account_name.unwrap(), azure_storage_account_name);
624 assert_eq!(builder.bearer_token.unwrap(), azure_storage_token);
625 }
626
627 #[test]
628 fn azure_test_client_opts() {
629 let key = "AZURE_PROXY_URL";
630 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
631 assert_eq!(
632 AzureConfigKey::Client(ClientConfigKey::ProxyUrl),
633 config_key
634 );
635 } else {
636 panic!("{key} not propagated as ClientConfigKey");
637 }
638 }
639}