1use std::str::FromStr;
2use std::sync::Arc;
3
4use futures::future::BoxFuture;
5use serde::{Deserialize, Serialize};
6use tokio::runtime::Handle;
7
8use super::cfg_file::load_cfg_profile;
9use super::credential::{
10 DatabricksCredential, DatabricksGcpTokenExchangeProvider, DatabricksM2MProvider,
11 OidcEnvTokenProvider, OidcFileTokenProvider,
12};
13use crate::azure::credential::{AZURE_DATABRICKS_RESOURCE, ImdsManagedIdentityProvider};
14#[allow(unused_imports)]
16use crate::azure::credential::AZURE_DATABRICKS_SCOPE;
17use crate::service::make_service;
18use crate::{
19 ClientConfigKey, ClientOptions, CredentialProvider, RequestSigner, Result, RetryConfig,
20 StaticCredentialProvider, TokenCredentialProvider,
21};
22
23const DEFAULT_DATABRICKS_SCOPE: &str = "unity-catalog";
25
26const DEFAULT_OIDC_TOKEN_ENV: &str = "DATABRICKS_OIDC_TOKEN";
28
29type DatabricksCredentialProvider = Arc<dyn CredentialProvider<Credential = DatabricksCredential>>;
30
31#[derive(Debug, Clone)]
33pub struct DatabricksConfig {
34 pub credentials: DatabricksCredentialProvider,
35 pub retry_config: RetryConfig,
36 pub client_options: ClientOptions,
37}
38
39impl RequestSigner for DatabricksConfig {
40 fn sign<'a>(
41 &'a self,
42 req: reqwest::RequestBuilder,
43 ) -> BoxFuture<'a, Result<reqwest::RequestBuilder>> {
44 Box::pin(async move {
45 let cred = self.credentials.get_credential().await?;
46 Ok(req.bearer_auth(&cred.bearer))
47 })
48 }
49}
50
51#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Deserialize, Serialize)]
53#[non_exhaustive]
54pub enum DatabricksConfigKey {
55 Host,
59
60 Token,
64
65 ClientId,
69
70 ClientSecret,
74
75 AccountId,
79
80 AzureResourceId,
84
85 Scope,
89
90 ConfigFile,
94
95 ConfigProfile,
99
100 AuthType,
104
105 OidcTokenEnv,
110
111 OidcTokenFilepath,
115
116 Client(ClientConfigKey),
118}
119
120impl AsRef<str> for DatabricksConfigKey {
121 fn as_ref(&self) -> &str {
122 match self {
123 Self::Host => "databricks_host",
124 Self::Token => "databricks_token",
125 Self::ClientId => "databricks_client_id",
126 Self::ClientSecret => "databricks_client_secret",
127 Self::AccountId => "databricks_account_id",
128 Self::AzureResourceId => "databricks_azure_resource_id",
129 Self::Scope => "databricks_oauth_scope",
130 Self::ConfigFile => "databricks_config_file",
131 Self::ConfigProfile => "databricks_config_profile",
132 Self::AuthType => "databricks_auth_type",
133 Self::OidcTokenEnv => "databricks_oidc_token_env",
134 Self::OidcTokenFilepath => "databricks_oidc_token_filepath",
135 Self::Client(key) => key.as_ref(),
136 }
137 }
138}
139
140impl FromStr for DatabricksConfigKey {
141 type Err = crate::Error;
142
143 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 match s {
145 "databricks_host" | "host" => Ok(Self::Host),
146 "databricks_token" | "token" => Ok(Self::Token),
147 "databricks_client_id" | "client_id" => Ok(Self::ClientId),
148 "databricks_client_secret" | "client_secret" => Ok(Self::ClientSecret),
149 "databricks_account_id" | "account_id" => Ok(Self::AccountId),
150 "databricks_azure_resource_id" | "azure_resource_id" => Ok(Self::AzureResourceId),
151 "databricks_oauth_scope" | "oauth_scope" => Ok(Self::Scope),
152 "databricks_config_file" | "config_file" => Ok(Self::ConfigFile),
153 "databricks_config_profile" | "config_profile" | "profile" => Ok(Self::ConfigProfile),
154 "databricks_auth_type" | "auth_type" => Ok(Self::AuthType),
155 "databricks_oidc_token_env" | "oidc_token_env" => Ok(Self::OidcTokenEnv),
156 "databricks_oidc_token_filepath" | "oidc_token_filepath" => Ok(Self::OidcTokenFilepath),
157 _ => match s.strip_prefix("databricks_").unwrap_or(s).parse() {
158 Ok(key) => Ok(Self::Client(key)),
159 Err(_) => Err(crate::Error::UnknownConfigurationKey { key: s.into() }),
160 },
161 }
162 }
163}
164
165#[derive(Default, Clone)]
182pub struct DatabricksBuilder {
183 host: Option<String>,
184 token: Option<String>,
185 client_id: Option<String>,
186 client_secret: Option<String>,
187 account_id: Option<String>,
188 azure_resource_id: Option<String>,
189 scope: Option<String>,
190 config_file: Option<String>,
191 config_profile: Option<String>,
192 auth_type: Option<String>,
193 oidc_token_env: Option<String>,
194 oidc_token_filepath: Option<String>,
195 retry_config: RetryConfig,
196 client_options: ClientOptions,
197 credentials: Option<DatabricksCredentialProvider>,
198}
199
200impl std::fmt::Debug for DatabricksBuilder {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 write!(f, "DatabricksBuilder {{ host: {:?} }}", self.host)
203 }
204}
205
206impl DatabricksBuilder {
207 pub fn new() -> Self {
209 Default::default()
210 }
211
212 pub fn from_env() -> Self {
223 let mut builder = Self::default();
224
225 let mut env_map: Vec<(DatabricksConfigKey, String)> = Vec::new();
228 let mut config_file_env: Option<String> = None;
229 let mut config_profile_env: Option<String> = None;
230
231 for (os_key, os_value) in std::env::vars_os() {
232 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
233 if key.starts_with("DATABRICKS_") || key == "GOOGLE_APPLICATION_CREDENTIALS" {
234 if let Ok(config_key) = key.to_ascii_lowercase().parse::<DatabricksConfigKey>()
235 {
236 match config_key {
237 DatabricksConfigKey::ConfigFile => {
238 config_file_env = Some(value.to_owned())
239 }
240 DatabricksConfigKey::ConfigProfile => {
241 config_profile_env = Some(value.to_owned())
242 }
243 _ => env_map.push((config_key, value.to_owned())),
244 }
245 }
246 }
247 }
248 }
249
250 if let Ok(profile_values) =
252 load_cfg_profile(config_file_env.as_deref(), config_profile_env.as_deref())
253 {
254 for (k, v) in profile_values {
255 builder = builder.with_config(k, v);
256 }
257 }
258
259 for (key, value) in env_map {
261 builder = builder.with_config(key, value);
262 }
263
264 if let Some(v) = config_file_env {
266 builder.config_file = Some(v);
267 }
268 if let Some(v) = config_profile_env {
269 builder.config_profile = Some(v);
270 }
271
272 builder
273 }
274
275 pub fn with_config(mut self, key: DatabricksConfigKey, value: impl Into<String>) -> Self {
277 match key {
278 DatabricksConfigKey::Host => self.host = Some(value.into()),
279 DatabricksConfigKey::Token => self.token = Some(value.into()),
280 DatabricksConfigKey::ClientId => self.client_id = Some(value.into()),
281 DatabricksConfigKey::ClientSecret => self.client_secret = Some(value.into()),
282 DatabricksConfigKey::AccountId => self.account_id = Some(value.into()),
283 DatabricksConfigKey::AzureResourceId => self.azure_resource_id = Some(value.into()),
284 DatabricksConfigKey::Scope => self.scope = Some(value.into()),
285 DatabricksConfigKey::ConfigFile => self.config_file = Some(value.into()),
286 DatabricksConfigKey::ConfigProfile => self.config_profile = Some(value.into()),
287 DatabricksConfigKey::AuthType => self.auth_type = Some(value.into()),
288 DatabricksConfigKey::OidcTokenEnv => self.oidc_token_env = Some(value.into()),
289 DatabricksConfigKey::OidcTokenFilepath => self.oidc_token_filepath = Some(value.into()),
290 DatabricksConfigKey::Client(key) => {
291 self.client_options = self.client_options.with_config(key, value)
292 }
293 }
294 self
295 }
296
297 pub fn with_credentials(mut self, credentials: DatabricksCredentialProvider) -> Self {
299 self.credentials = Some(credentials);
300 self
301 }
302
303 pub fn with_host(mut self, host: impl Into<String>) -> Self {
305 self.host = Some(host.into());
306 self
307 }
308
309 pub fn with_token(mut self, token: impl Into<String>) -> Self {
311 self.token = Some(token.into());
312 self
313 }
314
315 pub fn with_auth_type(mut self, auth_type: impl Into<String>) -> Self {
317 self.auth_type = Some(auth_type.into());
318 self
319 }
320
321 pub fn with_oidc_token_env(mut self, env_var: impl Into<String>) -> Self {
323 self.oidc_token_env = Some(env_var.into());
324 self
325 }
326
327 pub fn with_oidc_token_filepath(mut self, path: impl Into<String>) -> Self {
329 self.oidc_token_filepath = Some(path.into());
330 self
331 }
332
333 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
335 self.retry_config = retry_config;
336 self
337 }
338
339 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
341 self.client_options = options;
342 self
343 }
344
345 pub fn build(self, runtime: Option<&Handle>) -> Result<DatabricksConfig> {
349 let scope = self
350 .scope
351 .as_deref()
352 .unwrap_or(DEFAULT_DATABRICKS_SCOPE)
353 .to_owned();
354
355 let has_pat = self.token.is_some();
357 let has_m2m = self.client_id.is_some() && self.client_secret.is_some();
358 let has_env_oidc =
359 self.oidc_token_env.is_some() || std::env::var(DEFAULT_OIDC_TOKEN_ENV).is_ok();
360 let has_file_oidc = self.oidc_token_filepath.is_some();
361 let has_azure_msi = self.azure_resource_id.is_some();
362 let has_gcp = std::env::var("GOOGLE_APPLICATION_CREDENTIALS").is_ok();
363
364 let retry_config = self.retry_config.clone();
366 let client_options = self.client_options.clone();
367
368 let credentials: DatabricksCredentialProvider = if let Some(creds) = self.credentials {
369 creds
371 } else if let Some(auth_type) = self.auth_type.clone() {
372 self.build_forced_auth(
374 auth_type,
375 scope,
376 runtime,
377 has_pat,
378 has_m2m,
379 has_env_oidc,
380 has_file_oidc,
381 has_azure_msi,
382 has_gcp,
383 )?
384 } else if has_pat {
385 Arc::new(StaticCredentialProvider::new(DatabricksCredential {
387 bearer: self.token.unwrap(),
388 }))
389 } else if has_m2m {
390 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
392 let token_url = format!("{host}/oidc/v1/token");
393 let provider = DatabricksM2MProvider {
394 token_url,
395 client_id: self.client_id.unwrap(),
396 client_secret: self.client_secret.unwrap(),
397 scope,
398 };
399 let client = client_options.client()?;
400 let service = make_service(client.clone(), runtime);
401 Arc::new(TokenCredentialProvider::new(
402 provider,
403 client,
404 service,
405 retry_config.clone(),
406 )) as _
407 } else if has_env_oidc {
408 let env_var = self
410 .oidc_token_env
411 .unwrap_or_else(|| DEFAULT_OIDC_TOKEN_ENV.to_owned());
412 let provider = OidcEnvTokenProvider { env_var };
413 let client = client_options.client()?;
414 let service = make_service(client.clone(), runtime);
415 Arc::new(TokenCredentialProvider::new(
416 provider,
417 client,
418 service,
419 retry_config.clone(),
420 )) as _
421 } else if has_file_oidc {
422 let provider = OidcFileTokenProvider {
424 filepath: self.oidc_token_filepath.unwrap(),
425 };
426 let client = client_options.client()?;
427 let service = make_service(client.clone(), runtime);
428 Arc::new(TokenCredentialProvider::new(
429 provider,
430 client,
431 service,
432 retry_config.clone(),
433 )) as _
434 } else if has_azure_msi {
435 let msi_provider = ImdsManagedIdentityProvider::new_with_resource(
437 None,
438 None,
439 None,
440 None,
441 AZURE_DATABRICKS_RESOURCE,
442 );
443 let client = client_options.metadata_client()?;
444 let service = make_service(client.clone(), runtime);
445 let azure_provider =
446 TokenCredentialProvider::new(msi_provider, client, service, retry_config.clone());
447 Arc::new(AzureToDatabricksBridge {
448 inner: Arc::new(azure_provider),
449 }) as _
450 } else if has_gcp {
451 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
453 let token_url = format!("{host}/oidc/v1/token");
454 let gcp_config = crate::gcp::GoogleBuilder::new().build(runtime)?;
455 let provider = DatabricksGcpTokenExchangeProvider {
456 token_url,
457 gcp_provider: gcp_config.credentials,
458 };
459 let client = client_options.client()?;
460 let service = make_service(client.clone(), runtime);
461 Arc::new(TokenCredentialProvider::new(
462 provider,
463 client,
464 service,
465 retry_config.clone(),
466 )) as _
467 } else {
468 return Err(crate::Error::Generic {
469 source: "No Databricks credentials configured. Set DATABRICKS_TOKEN, \
470 DATABRICKS_CLIENT_ID + DATABRICKS_CLIENT_SECRET, \
471 DATABRICKS_OIDC_TOKEN (env-oidc), DATABRICKS_OIDC_TOKEN_FILEPATH (file-oidc), \
472 DATABRICKS_AZURE_RESOURCE_ID, or GOOGLE_APPLICATION_CREDENTIALS."
473 .into(),
474 });
475 };
476
477 Ok(DatabricksConfig {
478 credentials,
479 retry_config,
480 client_options,
481 })
482 }
483
484 #[allow(clippy::too_many_arguments)]
486 fn build_forced_auth(
487 self,
488 auth_type: String,
489 scope: String,
490 runtime: Option<&Handle>,
491 has_pat: bool,
492 has_m2m: bool,
493 has_env_oidc: bool,
494 has_file_oidc: bool,
495 has_azure_msi: bool,
496 has_gcp: bool,
497 ) -> Result<DatabricksCredentialProvider> {
498 match auth_type.as_str() {
499 "pat" => {
500 if !has_pat {
501 return Err(crate::Error::Generic {
502 source: "auth_type=pat requires DATABRICKS_TOKEN to be set".into(),
503 });
504 }
505 Ok(Arc::new(StaticCredentialProvider::new(DatabricksCredential {
506 bearer: self.token.unwrap(),
507 })))
508 }
509 "oauth-m2m" => {
510 if !has_m2m {
511 return Err(crate::Error::Generic {
512 source: "auth_type=oauth-m2m requires DATABRICKS_CLIENT_ID and \
513 DATABRICKS_CLIENT_SECRET to be set"
514 .into(),
515 });
516 }
517 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
518 let token_url = format!("{host}/oidc/v1/token");
519 let provider = DatabricksM2MProvider {
520 token_url,
521 client_id: self.client_id.unwrap(),
522 client_secret: self.client_secret.unwrap(),
523 scope,
524 };
525 let client = self.client_options.client()?;
526 let service = make_service(client.clone(), runtime);
527 Ok(Arc::new(TokenCredentialProvider::new(
528 provider,
529 client,
530 service,
531 self.retry_config,
532 )) as _)
533 }
534 "env-oidc" => {
535 if !has_env_oidc {
536 return Err(crate::Error::Generic {
537 source: "auth_type=env-oidc requires DATABRICKS_OIDC_TOKEN or \
538 DATABRICKS_OIDC_TOKEN_ENV to be set"
539 .into(),
540 });
541 }
542 let env_var = self
543 .oidc_token_env
544 .unwrap_or_else(|| DEFAULT_OIDC_TOKEN_ENV.to_owned());
545 let provider = OidcEnvTokenProvider { env_var };
546 let client = self.client_options.client()?;
547 let service = make_service(client.clone(), runtime);
548 Ok(Arc::new(TokenCredentialProvider::new(
549 provider,
550 client,
551 service,
552 self.retry_config,
553 )) as _)
554 }
555 "file-oidc" => {
556 if !has_file_oidc {
557 return Err(crate::Error::Generic {
558 source: "auth_type=file-oidc requires DATABRICKS_OIDC_TOKEN_FILEPATH \
559 to be set"
560 .into(),
561 });
562 }
563 let provider = OidcFileTokenProvider {
564 filepath: self.oidc_token_filepath.unwrap(),
565 };
566 let client = self.client_options.client()?;
567 let service = make_service(client.clone(), runtime);
568 Ok(Arc::new(TokenCredentialProvider::new(
569 provider,
570 client,
571 service,
572 self.retry_config,
573 )) as _)
574 }
575 "azure-msi" => {
576 if !has_azure_msi {
577 return Err(crate::Error::Generic {
578 source: "auth_type=azure-msi requires DATABRICKS_AZURE_RESOURCE_ID \
579 to be set"
580 .into(),
581 });
582 }
583 let msi_provider = ImdsManagedIdentityProvider::new_with_resource(
584 None,
585 None,
586 None,
587 None,
588 AZURE_DATABRICKS_RESOURCE,
589 );
590 let client = self.client_options.metadata_client()?;
591 let service = make_service(client.clone(), runtime);
592 let azure_provider = TokenCredentialProvider::new(
593 msi_provider,
594 client,
595 service,
596 self.retry_config,
597 );
598 Ok(Arc::new(AzureToDatabricksBridge {
599 inner: Arc::new(azure_provider),
600 }) as _)
601 }
602 "gcp" => {
603 if !has_gcp {
604 return Err(crate::Error::Generic {
605 source: "auth_type=gcp requires GOOGLE_APPLICATION_CREDENTIALS to be set"
606 .into(),
607 });
608 }
609 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
610 let token_url = format!("{host}/oidc/v1/token");
611 let gcp_config = crate::gcp::GoogleBuilder::new().build(runtime)?;
612 let provider = DatabricksGcpTokenExchangeProvider {
613 token_url,
614 gcp_provider: gcp_config.credentials,
615 };
616 let client = self.client_options.client()?;
617 let service = make_service(client.clone(), runtime);
618 Ok(Arc::new(TokenCredentialProvider::new(
619 provider,
620 client,
621 service,
622 self.retry_config,
623 )) as _)
624 }
625 other => Err(crate::Error::Generic {
626 source: format!("Unknown auth_type: {other:?}. Valid values: pat, oauth-m2m, env-oidc, file-oidc, azure-msi, gcp").into(),
627 }),
628 }
629 }
630}
631
632#[derive(Debug)]
637struct AzureToDatabricksBridge {
638 inner: Arc<dyn CredentialProvider<Credential = crate::azure::credential::AzureCredential>>,
639}
640
641#[async_trait::async_trait]
642impl CredentialProvider for AzureToDatabricksBridge {
643 type Credential = DatabricksCredential;
644
645 async fn get_credential(&self) -> crate::Result<Arc<DatabricksCredential>> {
646 use crate::azure::credential::AzureCredential;
647 let cred = self.inner.get_credential().await?;
648 let AzureCredential::BearerToken(token) = cred.as_ref();
649 Ok(Arc::new(DatabricksCredential {
650 bearer: token.clone(),
651 }))
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use std::collections::HashMap;
658
659 use super::*;
660
661 #[test]
662 fn test_databricks_config_from_map() {
663 let options = HashMap::from([
664 (
665 "databricks_host",
666 "https://my-workspace.azuredatabricks.net",
667 ),
668 ("databricks_token", "dapi1234567890"),
669 ("databricks_oauth_scope", "all-apis"),
670 ]);
671
672 let builder = options
673 .into_iter()
674 .fold(DatabricksBuilder::new(), |b, (k, v)| {
675 b.with_config(k.parse().unwrap(), v)
676 });
677
678 assert_eq!(
679 builder.host.as_deref(),
680 Some("https://my-workspace.azuredatabricks.net")
681 );
682 assert_eq!(builder.token.as_deref(), Some("dapi1234567890"));
683 assert_eq!(builder.scope.as_deref(), Some("all-apis"));
684 }
685
686 #[test]
687 fn test_databricks_config_key_roundtrip() {
688 let cases = [
689 ("databricks_host", DatabricksConfigKey::Host),
690 ("host", DatabricksConfigKey::Host),
691 ("databricks_token", DatabricksConfigKey::Token),
692 ("token", DatabricksConfigKey::Token),
693 ("databricks_client_id", DatabricksConfigKey::ClientId),
694 ("client_id", DatabricksConfigKey::ClientId),
695 (
696 "databricks_client_secret",
697 DatabricksConfigKey::ClientSecret,
698 ),
699 ("client_secret", DatabricksConfigKey::ClientSecret),
700 ("databricks_account_id", DatabricksConfigKey::AccountId),
701 ("account_id", DatabricksConfigKey::AccountId),
702 (
703 "databricks_azure_resource_id",
704 DatabricksConfigKey::AzureResourceId,
705 ),
706 ("azure_resource_id", DatabricksConfigKey::AzureResourceId),
707 ("databricks_oauth_scope", DatabricksConfigKey::Scope),
708 ("oauth_scope", DatabricksConfigKey::Scope),
709 ("databricks_config_file", DatabricksConfigKey::ConfigFile),
710 ("config_file", DatabricksConfigKey::ConfigFile),
711 (
712 "databricks_config_profile",
713 DatabricksConfigKey::ConfigProfile,
714 ),
715 ("config_profile", DatabricksConfigKey::ConfigProfile),
716 ("profile", DatabricksConfigKey::ConfigProfile),
717 ("databricks_auth_type", DatabricksConfigKey::AuthType),
718 ("auth_type", DatabricksConfigKey::AuthType),
719 (
720 "databricks_oidc_token_env",
721 DatabricksConfigKey::OidcTokenEnv,
722 ),
723 ("oidc_token_env", DatabricksConfigKey::OidcTokenEnv),
724 (
725 "databricks_oidc_token_filepath",
726 DatabricksConfigKey::OidcTokenFilepath,
727 ),
728 (
729 "oidc_token_filepath",
730 DatabricksConfigKey::OidcTokenFilepath,
731 ),
732 ];
733
734 for (s, expected) in cases {
735 assert_eq!(
736 s.parse::<DatabricksConfigKey>().unwrap(),
737 expected,
738 "failed for {s}"
739 );
740 }
741 }
742
743 #[tokio::test]
744 async fn test_build_with_pat() {
745 let config = DatabricksBuilder::new()
746 .with_host("https://my.azuredatabricks.net")
747 .with_token("dapimytoken")
748 .build(None)
749 .unwrap();
750
751 let cred = config.credentials.get_credential().await.unwrap();
752 assert_eq!(cred.bearer, "dapimytoken");
753 }
754
755 #[test]
756 fn test_auth_type_enforcement_missing_fields() {
757 let err = DatabricksBuilder::new()
758 .with_auth_type("pat")
759 .build(None)
760 .unwrap_err();
761 assert!(err.to_string().contains("pat"));
762 }
763}