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 && (key.starts_with("DATABRICKS_") || key == "GOOGLE_APPLICATION_CREDENTIALS")
234 && let Ok(config_key) = key.to_ascii_lowercase().parse::<DatabricksConfigKey>()
235 {
236 match config_key {
237 DatabricksConfigKey::ConfigFile => config_file_env = Some(value.to_owned()),
238 DatabricksConfigKey::ConfigProfile => {
239 config_profile_env = Some(value.to_owned())
240 }
241 _ => env_map.push((config_key, value.to_owned())),
242 }
243 }
244 }
245
246 if let Ok(profile_values) =
248 load_cfg_profile(config_file_env.as_deref(), config_profile_env.as_deref())
249 {
250 for (k, v) in profile_values {
251 builder = builder.with_config(k, v);
252 }
253 }
254
255 for (key, value) in env_map {
257 builder = builder.with_config(key, value);
258 }
259
260 if let Some(v) = config_file_env {
262 builder.config_file = Some(v);
263 }
264 if let Some(v) = config_profile_env {
265 builder.config_profile = Some(v);
266 }
267
268 builder
269 }
270
271 pub fn with_config(mut self, key: DatabricksConfigKey, value: impl Into<String>) -> Self {
273 match key {
274 DatabricksConfigKey::Host => self.host = Some(value.into()),
275 DatabricksConfigKey::Token => self.token = Some(value.into()),
276 DatabricksConfigKey::ClientId => self.client_id = Some(value.into()),
277 DatabricksConfigKey::ClientSecret => self.client_secret = Some(value.into()),
278 DatabricksConfigKey::AccountId => self.account_id = Some(value.into()),
279 DatabricksConfigKey::AzureResourceId => self.azure_resource_id = Some(value.into()),
280 DatabricksConfigKey::Scope => self.scope = Some(value.into()),
281 DatabricksConfigKey::ConfigFile => self.config_file = Some(value.into()),
282 DatabricksConfigKey::ConfigProfile => self.config_profile = Some(value.into()),
283 DatabricksConfigKey::AuthType => self.auth_type = Some(value.into()),
284 DatabricksConfigKey::OidcTokenEnv => self.oidc_token_env = Some(value.into()),
285 DatabricksConfigKey::OidcTokenFilepath => self.oidc_token_filepath = Some(value.into()),
286 DatabricksConfigKey::Client(key) => {
287 self.client_options = self.client_options.with_config(key, value)
288 }
289 }
290 self
291 }
292
293 pub fn with_credentials(mut self, credentials: DatabricksCredentialProvider) -> Self {
295 self.credentials = Some(credentials);
296 self
297 }
298
299 pub fn with_host(mut self, host: impl Into<String>) -> Self {
301 self.host = Some(host.into());
302 self
303 }
304
305 pub fn with_token(mut self, token: impl Into<String>) -> Self {
307 self.token = Some(token.into());
308 self
309 }
310
311 pub fn with_auth_type(mut self, auth_type: impl Into<String>) -> Self {
313 self.auth_type = Some(auth_type.into());
314 self
315 }
316
317 pub fn with_oidc_token_env(mut self, env_var: impl Into<String>) -> Self {
319 self.oidc_token_env = Some(env_var.into());
320 self
321 }
322
323 pub fn with_oidc_token_filepath(mut self, path: impl Into<String>) -> Self {
325 self.oidc_token_filepath = Some(path.into());
326 self
327 }
328
329 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
331 self.retry_config = retry_config;
332 self
333 }
334
335 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
337 self.client_options = options;
338 self
339 }
340
341 pub fn build(self, runtime: Option<&Handle>) -> Result<DatabricksConfig> {
345 let scope = self
346 .scope
347 .as_deref()
348 .unwrap_or(DEFAULT_DATABRICKS_SCOPE)
349 .to_owned();
350
351 let has_pat = self.token.is_some();
353 let has_m2m = self.client_id.is_some() && self.client_secret.is_some();
354 let has_env_oidc =
355 self.oidc_token_env.is_some() || std::env::var(DEFAULT_OIDC_TOKEN_ENV).is_ok();
356 let has_file_oidc = self.oidc_token_filepath.is_some();
357 let has_azure_msi = self.azure_resource_id.is_some();
358 let has_gcp = std::env::var("GOOGLE_APPLICATION_CREDENTIALS").is_ok();
359
360 let retry_config = self.retry_config.clone();
362 let client_options = self.client_options.clone();
363
364 let credentials: DatabricksCredentialProvider = if let Some(creds) = self.credentials {
365 creds
367 } else if let Some(auth_type) = self.auth_type.clone() {
368 self.build_forced_auth(
370 auth_type,
371 scope,
372 runtime,
373 has_pat,
374 has_m2m,
375 has_env_oidc,
376 has_file_oidc,
377 has_azure_msi,
378 has_gcp,
379 )?
380 } else if has_pat {
381 Arc::new(StaticCredentialProvider::new(DatabricksCredential {
383 bearer: self.token.unwrap(),
384 }))
385 } else if has_m2m {
386 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
388 let token_url = format!("{host}/oidc/v1/token");
389 let provider = DatabricksM2MProvider {
390 token_url,
391 client_id: self.client_id.unwrap(),
392 client_secret: self.client_secret.unwrap(),
393 scope,
394 };
395 let client = client_options.client()?;
396 let service = make_service(client.clone(), runtime);
397 Arc::new(TokenCredentialProvider::new(
398 provider,
399 client,
400 service,
401 retry_config.clone(),
402 )) as _
403 } else if has_env_oidc {
404 let env_var = self
406 .oidc_token_env
407 .unwrap_or_else(|| DEFAULT_OIDC_TOKEN_ENV.to_owned());
408 let provider = OidcEnvTokenProvider { env_var };
409 let client = client_options.client()?;
410 let service = make_service(client.clone(), runtime);
411 Arc::new(TokenCredentialProvider::new(
412 provider,
413 client,
414 service,
415 retry_config.clone(),
416 )) as _
417 } else if has_file_oidc {
418 let provider = OidcFileTokenProvider {
420 filepath: self.oidc_token_filepath.unwrap(),
421 };
422 let client = client_options.client()?;
423 let service = make_service(client.clone(), runtime);
424 Arc::new(TokenCredentialProvider::new(
425 provider,
426 client,
427 service,
428 retry_config.clone(),
429 )) as _
430 } else if has_azure_msi {
431 let msi_provider = ImdsManagedIdentityProvider::new_with_resource(
433 None,
434 None,
435 None,
436 None,
437 AZURE_DATABRICKS_RESOURCE,
438 );
439 let client = client_options.metadata_client()?;
440 let service = make_service(client.clone(), runtime);
441 let azure_provider =
442 TokenCredentialProvider::new(msi_provider, client, service, retry_config.clone());
443 Arc::new(AzureToDatabricksBridge {
444 inner: Arc::new(azure_provider),
445 }) as _
446 } else if has_gcp {
447 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
449 let token_url = format!("{host}/oidc/v1/token");
450 let gcp_config = crate::gcp::GoogleBuilder::new().build(runtime)?;
451 let provider = DatabricksGcpTokenExchangeProvider {
452 token_url,
453 gcp_provider: gcp_config.credentials,
454 };
455 let client = client_options.client()?;
456 let service = make_service(client.clone(), runtime);
457 Arc::new(TokenCredentialProvider::new(
458 provider,
459 client,
460 service,
461 retry_config.clone(),
462 )) as _
463 } else {
464 return Err(crate::Error::Generic {
465 source: "No Databricks credentials configured. Set DATABRICKS_TOKEN, \
466 DATABRICKS_CLIENT_ID + DATABRICKS_CLIENT_SECRET, \
467 DATABRICKS_OIDC_TOKEN (env-oidc), DATABRICKS_OIDC_TOKEN_FILEPATH (file-oidc), \
468 DATABRICKS_AZURE_RESOURCE_ID, or GOOGLE_APPLICATION_CREDENTIALS."
469 .into(),
470 });
471 };
472
473 Ok(DatabricksConfig {
474 credentials,
475 retry_config,
476 client_options,
477 })
478 }
479
480 #[allow(clippy::too_many_arguments)]
482 fn build_forced_auth(
483 self,
484 auth_type: String,
485 scope: String,
486 runtime: Option<&Handle>,
487 has_pat: bool,
488 has_m2m: bool,
489 has_env_oidc: bool,
490 has_file_oidc: bool,
491 has_azure_msi: bool,
492 has_gcp: bool,
493 ) -> Result<DatabricksCredentialProvider> {
494 match auth_type.as_str() {
495 "pat" => {
496 if !has_pat {
497 return Err(crate::Error::Generic {
498 source: "auth_type=pat requires DATABRICKS_TOKEN to be set".into(),
499 });
500 }
501 Ok(Arc::new(StaticCredentialProvider::new(DatabricksCredential {
502 bearer: self.token.unwrap(),
503 })))
504 }
505 "oauth-m2m" => {
506 if !has_m2m {
507 return Err(crate::Error::Generic {
508 source: "auth_type=oauth-m2m requires DATABRICKS_CLIENT_ID and \
509 DATABRICKS_CLIENT_SECRET to be set"
510 .into(),
511 });
512 }
513 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
514 let token_url = format!("{host}/oidc/v1/token");
515 let provider = DatabricksM2MProvider {
516 token_url,
517 client_id: self.client_id.unwrap(),
518 client_secret: self.client_secret.unwrap(),
519 scope,
520 };
521 let client = self.client_options.client()?;
522 let service = make_service(client.clone(), runtime);
523 Ok(Arc::new(TokenCredentialProvider::new(
524 provider,
525 client,
526 service,
527 self.retry_config,
528 )) as _)
529 }
530 "env-oidc" => {
531 if !has_env_oidc {
532 return Err(crate::Error::Generic {
533 source: "auth_type=env-oidc requires DATABRICKS_OIDC_TOKEN or \
534 DATABRICKS_OIDC_TOKEN_ENV to be set"
535 .into(),
536 });
537 }
538 let env_var = self
539 .oidc_token_env
540 .unwrap_or_else(|| DEFAULT_OIDC_TOKEN_ENV.to_owned());
541 let provider = OidcEnvTokenProvider { env_var };
542 let client = self.client_options.client()?;
543 let service = make_service(client.clone(), runtime);
544 Ok(Arc::new(TokenCredentialProvider::new(
545 provider,
546 client,
547 service,
548 self.retry_config,
549 )) as _)
550 }
551 "file-oidc" => {
552 if !has_file_oidc {
553 return Err(crate::Error::Generic {
554 source: "auth_type=file-oidc requires DATABRICKS_OIDC_TOKEN_FILEPATH \
555 to be set"
556 .into(),
557 });
558 }
559 let provider = OidcFileTokenProvider {
560 filepath: self.oidc_token_filepath.unwrap(),
561 };
562 let client = self.client_options.client()?;
563 let service = make_service(client.clone(), runtime);
564 Ok(Arc::new(TokenCredentialProvider::new(
565 provider,
566 client,
567 service,
568 self.retry_config,
569 )) as _)
570 }
571 "azure-msi" => {
572 if !has_azure_msi {
573 return Err(crate::Error::Generic {
574 source: "auth_type=azure-msi requires DATABRICKS_AZURE_RESOURCE_ID \
575 to be set"
576 .into(),
577 });
578 }
579 let msi_provider = ImdsManagedIdentityProvider::new_with_resource(
580 None,
581 None,
582 None,
583 None,
584 AZURE_DATABRICKS_RESOURCE,
585 );
586 let client = self.client_options.metadata_client()?;
587 let service = make_service(client.clone(), runtime);
588 let azure_provider = TokenCredentialProvider::new(
589 msi_provider,
590 client,
591 service,
592 self.retry_config,
593 );
594 Ok(Arc::new(AzureToDatabricksBridge {
595 inner: Arc::new(azure_provider),
596 }) as _)
597 }
598 "gcp" => {
599 if !has_gcp {
600 return Err(crate::Error::Generic {
601 source: "auth_type=gcp requires GOOGLE_APPLICATION_CREDENTIALS to be set"
602 .into(),
603 });
604 }
605 let host = self.host.as_deref().unwrap_or("").trim_end_matches('/');
606 let token_url = format!("{host}/oidc/v1/token");
607 let gcp_config = crate::gcp::GoogleBuilder::new().build(runtime)?;
608 let provider = DatabricksGcpTokenExchangeProvider {
609 token_url,
610 gcp_provider: gcp_config.credentials,
611 };
612 let client = self.client_options.client()?;
613 let service = make_service(client.clone(), runtime);
614 Ok(Arc::new(TokenCredentialProvider::new(
615 provider,
616 client,
617 service,
618 self.retry_config,
619 )) as _)
620 }
621 other => Err(crate::Error::Generic {
622 source: format!("Unknown auth_type: {other:?}. Valid values: pat, oauth-m2m, env-oidc, file-oidc, azure-msi, gcp").into(),
623 }),
624 }
625 }
626}
627
628#[derive(Debug)]
633struct AzureToDatabricksBridge {
634 inner: Arc<dyn CredentialProvider<Credential = crate::azure::credential::AzureCredential>>,
635}
636
637#[async_trait::async_trait]
638impl CredentialProvider for AzureToDatabricksBridge {
639 type Credential = DatabricksCredential;
640
641 async fn get_credential(&self) -> crate::Result<Arc<DatabricksCredential>> {
642 use crate::azure::credential::AzureCredential;
643 let cred = self.inner.get_credential().await?;
644 let AzureCredential::BearerToken(token) = cred.as_ref();
645 Ok(Arc::new(DatabricksCredential {
646 bearer: token.clone(),
647 }))
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use std::collections::HashMap;
654
655 use super::*;
656
657 #[test]
658 fn test_databricks_config_from_map() {
659 let options = HashMap::from([
660 (
661 "databricks_host",
662 "https://my-workspace.azuredatabricks.net",
663 ),
664 ("databricks_token", "dapi1234567890"),
665 ("databricks_oauth_scope", "all-apis"),
666 ]);
667
668 let builder = options
669 .into_iter()
670 .fold(DatabricksBuilder::new(), |b, (k, v)| {
671 b.with_config(k.parse().unwrap(), v)
672 });
673
674 assert_eq!(
675 builder.host.as_deref(),
676 Some("https://my-workspace.azuredatabricks.net")
677 );
678 assert_eq!(builder.token.as_deref(), Some("dapi1234567890"));
679 assert_eq!(builder.scope.as_deref(), Some("all-apis"));
680 }
681
682 #[test]
683 fn test_databricks_config_key_roundtrip() {
684 let cases = [
685 ("databricks_host", DatabricksConfigKey::Host),
686 ("host", DatabricksConfigKey::Host),
687 ("databricks_token", DatabricksConfigKey::Token),
688 ("token", DatabricksConfigKey::Token),
689 ("databricks_client_id", DatabricksConfigKey::ClientId),
690 ("client_id", DatabricksConfigKey::ClientId),
691 (
692 "databricks_client_secret",
693 DatabricksConfigKey::ClientSecret,
694 ),
695 ("client_secret", DatabricksConfigKey::ClientSecret),
696 ("databricks_account_id", DatabricksConfigKey::AccountId),
697 ("account_id", DatabricksConfigKey::AccountId),
698 (
699 "databricks_azure_resource_id",
700 DatabricksConfigKey::AzureResourceId,
701 ),
702 ("azure_resource_id", DatabricksConfigKey::AzureResourceId),
703 ("databricks_oauth_scope", DatabricksConfigKey::Scope),
704 ("oauth_scope", DatabricksConfigKey::Scope),
705 ("databricks_config_file", DatabricksConfigKey::ConfigFile),
706 ("config_file", DatabricksConfigKey::ConfigFile),
707 (
708 "databricks_config_profile",
709 DatabricksConfigKey::ConfigProfile,
710 ),
711 ("config_profile", DatabricksConfigKey::ConfigProfile),
712 ("profile", DatabricksConfigKey::ConfigProfile),
713 ("databricks_auth_type", DatabricksConfigKey::AuthType),
714 ("auth_type", DatabricksConfigKey::AuthType),
715 (
716 "databricks_oidc_token_env",
717 DatabricksConfigKey::OidcTokenEnv,
718 ),
719 ("oidc_token_env", DatabricksConfigKey::OidcTokenEnv),
720 (
721 "databricks_oidc_token_filepath",
722 DatabricksConfigKey::OidcTokenFilepath,
723 ),
724 (
725 "oidc_token_filepath",
726 DatabricksConfigKey::OidcTokenFilepath,
727 ),
728 ];
729
730 for (s, expected) in cases {
731 assert_eq!(
732 s.parse::<DatabricksConfigKey>().unwrap(),
733 expected,
734 "failed for {s}"
735 );
736 }
737 }
738
739 #[tokio::test]
740 async fn test_build_with_pat() {
741 let config = DatabricksBuilder::new()
742 .with_host("https://my.azuredatabricks.net")
743 .with_token("dapimytoken")
744 .build(None)
745 .unwrap();
746
747 let cred = config.credentials.get_credential().await.unwrap();
748 assert_eq!(cred.bearer, "dapimytoken");
749 }
750
751 #[test]
752 fn test_auth_type_enforcement_missing_fields() {
753 let err = DatabricksBuilder::new()
754 .with_auth_type("pat")
755 .build(None)
756 .unwrap_err();
757 assert!(err.to_string().contains("pat"));
758 }
759}