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 if key.starts_with("AZURE_") {
326 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
327 builder = builder.with_config(config_key, value);
328 }
329 }
330 }
331 }
332
333 if let Ok(text) = std::env::var(MSI_ENDPOINT_ENV_KEY) {
334 builder = builder.with_msi_endpoint(text);
335 }
336
337 builder
338 }
339
340 pub fn with_config(mut self, key: AzureConfigKey, value: impl Into<String>) -> Self {
342 match key {
343 AzureConfigKey::AccessKey => self.access_key = Some(value.into()),
344 AzureConfigKey::AccountName => self.account_name = Some(value.into()),
345 AzureConfigKey::ClientId => self.client_id = Some(value.into()),
346 AzureConfigKey::ClientSecret => self.client_secret = Some(value.into()),
347 AzureConfigKey::AuthorityId => self.tenant_id = Some(value.into()),
348 AzureConfigKey::AuthorityHost => self.authority_host = Some(value.into()),
349 AzureConfigKey::Scope => self.scope = Some(value.into()),
350 AzureConfigKey::Token => self.bearer_token = Some(value.into()),
351 AzureConfigKey::MsiEndpoint => self.msi_endpoint = Some(value.into()),
352 AzureConfigKey::ObjectId => self.object_id = Some(value.into()),
353 AzureConfigKey::MsiResourceId => self.msi_resource_id = Some(value.into()),
354 AzureConfigKey::FederatedTokenFile => self.federated_token_file = Some(value.into()),
355 AzureConfigKey::UseAzureCli => self.use_azure_cli.parse(value),
356 AzureConfigKey::SkipSignature => self.skip_signature.parse(value),
357 AzureConfigKey::Endpoint => self.endpoint = Some(value.into()),
358 AzureConfigKey::Client(key) => {
359 self.client_options = self.client_options.with_config(key, value)
360 }
361 };
362 self
363 }
364
365 pub fn get_config_value(&self, key: &AzureConfigKey) -> Option<String> {
367 match key {
368 AzureConfigKey::AccountName => self.account_name.clone(),
369 AzureConfigKey::AccessKey => self.access_key.clone(),
370 AzureConfigKey::ClientId => self.client_id.clone(),
371 AzureConfigKey::ClientSecret => self.client_secret.clone(),
372 AzureConfigKey::AuthorityId => self.tenant_id.clone(),
373 AzureConfigKey::AuthorityHost => self.authority_host.clone(),
374 AzureConfigKey::Scope => self.scope.clone(),
375 AzureConfigKey::Token => self.bearer_token.clone(),
376 AzureConfigKey::Endpoint => self.endpoint.clone(),
377 AzureConfigKey::MsiEndpoint => self.msi_endpoint.clone(),
378 AzureConfigKey::ObjectId => self.object_id.clone(),
379 AzureConfigKey::MsiResourceId => self.msi_resource_id.clone(),
380 AzureConfigKey::FederatedTokenFile => self.federated_token_file.clone(),
381 AzureConfigKey::UseAzureCli => Some(self.use_azure_cli.to_string()),
382 AzureConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
383 AzureConfigKey::Client(key) => self.client_options.get_config_value(key),
384 }
385 }
386
387 pub fn with_account(mut self, account: impl Into<String>) -> Self {
389 self.account_name = Some(account.into());
390 self
391 }
392
393 pub fn with_access_key(mut self, access_key: impl Into<String>) -> Self {
395 self.access_key = Some(access_key.into());
396 self
397 }
398
399 pub fn with_bearer_token_authorization(mut self, bearer_token: impl Into<String>) -> Self {
401 self.bearer_token = Some(bearer_token.into());
402 self
403 }
404
405 pub fn with_client_secret_authorization(
407 mut self,
408 client_id: impl Into<String>,
409 client_secret: impl Into<String>,
410 tenant_id: impl Into<String>,
411 ) -> Self {
412 self.client_id = Some(client_id.into());
413 self.client_secret = Some(client_secret.into());
414 self.tenant_id = Some(tenant_id.into());
415 self
416 }
417
418 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
420 self.client_id = Some(client_id.into());
421 self
422 }
423
424 pub fn with_client_secret(mut self, client_secret: impl Into<String>) -> Self {
426 self.client_secret = Some(client_secret.into());
427 self
428 }
429
430 pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
432 self.tenant_id = Some(tenant_id.into());
433 self
434 }
435
436 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
438 self.scope = Some(scope.into());
439 self
440 }
441
442 pub fn with_credentials(mut self, credentials: AzureCredentialProvider) -> Self {
444 self.credentials = Some(credentials);
445 self
446 }
447
448 pub fn with_endpoint(mut self, endpoint: String) -> Self {
450 self.endpoint = Some(endpoint);
451 self
452 }
453
454 pub fn with_allow_http(mut self, allow_http: bool) -> Self {
456 self.client_options = self.client_options.with_allow_http(allow_http);
457 self
458 }
459
460 pub fn with_authority_host(mut self, authority_host: impl Into<String>) -> Self {
464 self.authority_host = Some(authority_host.into());
465 self
466 }
467
468 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
470 self.retry_config = retry_config;
471 self
472 }
473
474 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
476 self.client_options = self.client_options.with_proxy_url(proxy_url);
477 self
478 }
479
480 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
482 self.client_options = self
483 .client_options
484 .with_proxy_ca_certificate(proxy_ca_certificate);
485 self
486 }
487
488 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
490 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
491 self
492 }
493
494 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
496 self.client_options = options;
497 self
498 }
499
500 pub fn with_msi_endpoint(mut self, msi_endpoint: impl Into<String>) -> Self {
502 self.msi_endpoint = Some(msi_endpoint.into());
503 self
504 }
505
506 pub fn with_federated_token_file(mut self, federated_token_file: impl Into<String>) -> Self {
508 self.federated_token_file = Some(federated_token_file.into());
509 self
510 }
511
512 pub fn with_use_azure_cli(mut self, use_azure_cli: bool) -> Self {
514 self.use_azure_cli = use_azure_cli.into();
515 self
516 }
517
518 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
520 self.skip_signature = skip_signature.into();
521 self
522 }
523
524 pub fn build(self, runtime: Option<&Handle>) -> Result<AzureConfig> {
530 let static_creds = |credential: AzureCredential| -> AzureCredentialProvider {
531 Arc::new(StaticCredentialProvider::new(credential))
532 };
533
534 let auth = if let Some(credential) = self.credentials {
535 credential
536 } else if let Some(bearer_token) = self.bearer_token {
537 static_creds(AzureCredential::BearerToken(bearer_token))
538 } else if let (Some(client_id), Some(tenant_id), Some(federated_token_file)) =
539 (&self.client_id, &self.tenant_id, self.federated_token_file)
540 {
541 let client_credential = WorkloadIdentityOAuthProvider::new(
542 client_id,
543 federated_token_file,
544 tenant_id,
545 self.authority_host,
546 );
547 let client = self.client_options.client()?;
548 let service = make_service(client.clone(), runtime);
549 Arc::new(TokenCredentialProvider::new(
550 client_credential,
551 client,
552 service,
553 self.retry_config.clone(),
554 )) as _
555 } else if let (Some(client_id), Some(client_secret), Some(tenant_id)) =
556 (&self.client_id, self.client_secret, &self.tenant_id)
557 {
558 let scope = self.scope.ok_or(Error::MissingScope)?;
559 let client_credential = ClientSecretOAuthProvider::new_with_scope(
560 client_id.clone(),
561 client_secret,
562 tenant_id,
563 self.authority_host,
564 &scope,
565 );
566 let client = self.client_options.client()?;
567 let service = make_service(client.clone(), runtime);
568 Arc::new(TokenCredentialProvider::new(
569 client_credential,
570 client,
571 service,
572 self.retry_config.clone(),
573 )) as _
574 } else if self.use_azure_cli.get()? {
575 Arc::new(AzureCliCredential::new()) as _
576 } else {
577 let msi_credential = ImdsManagedIdentityProvider::new(
578 self.client_id,
579 self.object_id,
580 self.msi_resource_id,
581 self.msi_endpoint,
582 );
583 let client = self.client_options.metadata_client()?;
584 let service = make_service(client.clone(), runtime);
585 Arc::new(TokenCredentialProvider::new(
586 msi_credential,
587 client,
588 service,
589 self.retry_config.clone(),
590 )) as _
591 };
592
593 Ok(AzureConfig {
594 skip_signature: self.skip_signature.get()?,
595 retry_config: self.retry_config,
596 client_options: self.client_options,
597 credentials: auth,
598 })
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use std::collections::HashMap;
606
607 #[test]
608 fn azure_test_config_from_map() {
609 let azure_client_id = "object_store:fake_access_key_id";
610 let azure_storage_account_name = "object_store:fake_secret_key";
611 let azure_storage_token = "object_store:fake_default_region";
612 let options = HashMap::from([
613 ("azure_client_id", azure_client_id),
614 ("azure_storage_account_name", azure_storage_account_name),
615 ("azure_storage_token", azure_storage_token),
616 ]);
617
618 let builder = options
619 .into_iter()
620 .fold(AzureBuilder::new(), |builder, (key, value)| {
621 builder.with_config(key.parse().unwrap(), value)
622 });
623 assert_eq!(builder.client_id.unwrap(), azure_client_id);
624 assert_eq!(builder.account_name.unwrap(), azure_storage_account_name);
625 assert_eq!(builder.bearer_token.unwrap(), azure_storage_token);
626 }
627
628 #[test]
629 fn azure_test_client_opts() {
630 let key = "AZURE_PROXY_URL";
631 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
632 assert_eq!(
633 AzureConfigKey::Client(ClientConfigKey::ProxyUrl),
634 config_key
635 );
636 } else {
637 panic!("{key} not propagated as ClientConfigKey");
638 }
639 }
640}