dbx_tools_databricks_auth/
client.rs1use crate::{
2 AuthKind, AuthSession, CredentialStore, Error, MachineToMachineFlow, OAuthFlow, OAuthTemplate,
3 Profile, Result, Token,
4};
5pub use dbx_tools_auth::AuthOptions;
6use std::{sync::Arc, time::Duration};
7
8pub struct AuthClient {
10 profile: Profile,
11 inner: dbx_tools_auth::AuthClient,
12}
13
14enum AuthFlow {
15 UserToMachine(OAuthFlow),
16 UserToMachineCli(DatabricksCliFlow),
17 MachineToMachine(MachineToMachineFlow),
18 PersonalAccessToken(Token),
19}
20
21struct DatabricksCliFlow {
22 native: OAuthFlow,
23 profile: String,
24}
25
26impl DatabricksCliFlow {
27 fn new(native: OAuthFlow, profile: String) -> Self {
28 Self { native, profile }
29 }
30
31 async fn token(&self, force_refresh: bool) -> Result<Token> {
32 let profile = self.profile.clone();
33 tokio::task::spawn_blocking(move || {
34 let output = dbx_tools_databricks::databricks_cli_token(&profile, force_refresh)
35 .map_err(|error| Error::OAuth(error.to_string()))?;
36 serde_json::from_slice(&output).map_err(Into::into)
37 })
38 .await
39 .map_err(|error| Error::OAuth(format!("databricks auth token task failed: {error}")))?
40 }
41}
42
43impl AuthClient {
44 pub fn new(
45 profile: Profile,
46 store: Arc<dyn CredentialStore>,
47 options: AuthOptions,
48 use_databricks_cli: bool,
49 ) -> Result<Self> {
50 let flow = match profile.auth_kind {
51 AuthKind::UserToMachine => {
52 let native = OAuthFlow::new(profile.clone())?
53 .with_template(OAuthTemplate::new(options.callback_image_src.clone()));
54 if use_databricks_cli {
55 AuthFlow::UserToMachineCli(DatabricksCliFlow::new(native, profile.name.clone()))
56 } else {
57 AuthFlow::UserToMachine(native)
58 }
59 }
60 AuthKind::MachineToMachine => {
61 AuthFlow::MachineToMachine(MachineToMachineFlow::new(profile.clone())?)
62 }
63 AuthKind::PersonalAccessToken => AuthFlow::PersonalAccessToken(Token {
64 access_token: profile
65 .access_token()
66 .ok_or_else(|| Error::Config("pat requires token".into()))?
67 .to_owned(),
68 token_type: "Bearer".into(),
69 refresh_token: None,
70 expires_at: None,
71 scopes: Vec::new(),
72 }),
73 };
74 let inner =
75 dbx_tools_auth::AuthClient::new(profile.cache_key(), Arc::new(flow), store, options);
76 Ok(Self { profile, inner })
77 }
78 pub fn profile(&self) -> &Profile {
79 &self.profile
80 }
81}
82
83impl AuthSession for AuthClient {
84 fn auth_client(&self) -> &dbx_tools_auth::AuthClient {
85 &self.inner
86 }
87}
88
89#[async_trait::async_trait]
90impl dbx_tools_auth::TokenProvider for AuthFlow {
91 async fn authenticate(&self, timeout: Duration) -> Result<Token> {
92 match self {
93 Self::UserToMachine(flow) => flow.login(timeout).await,
94 Self::UserToMachineCli(flow) => flow.token(false).await,
95 Self::MachineToMachine(flow) => flow.token().await,
96 Self::PersonalAccessToken(token) => Ok(token.clone()),
97 }
98 }
99 async fn login(&self, timeout: Duration) -> Result<Token> {
100 match self {
101 Self::UserToMachine(flow) => flow.login(timeout).await,
102 Self::UserToMachineCli(flow) => flow.native.login(timeout).await,
103 Self::MachineToMachine(flow) => flow.token().await,
104 Self::PersonalAccessToken(token) => Ok(token.clone()),
105 }
106 }
107 async fn refresh(&self, token: &Token) -> Result<Token> {
108 match self {
109 Self::UserToMachine(flow) => flow.refresh(token).await,
110 Self::UserToMachineCli(flow) => flow.token(true).await,
111 Self::MachineToMachine(flow) => flow.token().await,
112 Self::PersonalAccessToken(token) => Ok(token.clone()),
113 }
114 }
115 fn can_authenticate_silently(&self) -> bool {
116 matches!(
117 self,
118 Self::UserToMachineCli(_) | Self::MachineToMachine(_) | Self::PersonalAccessToken(_)
119 )
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::{MemoryStore, TargetKind};
127 use url::Url;
128
129 #[test]
130 fn parses_databricks_cli_token_json() {
131 let token: Token = serde_json::from_slice(
132 br#"{"access_token":"access","token_type":"Bearer","refresh_token":"refresh","expiry":"2026-09-05T13:00:00Z","scopes":["all-apis","offline_access"]}"#,
133 )
134 .unwrap();
135 assert_eq!(token.access_token, "access");
136 assert_eq!(token.refresh_token.as_deref(), Some("refresh"));
137 assert_eq!(token.scopes, ["all-apis", "offline_access"]);
138 }
139
140 #[tokio::test]
141 async fn personal_access_token_authenticates_silently() {
142 let profile = Profile {
143 name: "DEFAULT".into(),
144 host: Url::parse("https://workspace.example").unwrap(),
145 account_id: None,
146 workspace_id: None,
147 client_id: String::new(),
148 group_id: None,
149 scopes: Vec::new(),
150 target: TargetKind::Workspace,
151 auth_kind: AuthKind::PersonalAccessToken,
152 client_secret: None,
153 access_token: Some("access".into()),
154 };
155 let client = AuthClient::new(
156 profile,
157 Arc::new(MemoryStore::new()),
158 AuthOptions::default(),
159 false,
160 )
161 .unwrap();
162
163 let token = client.token_with_login(Some(false)).await.unwrap();
164
165 assert_eq!(token.access_token, "access");
166 }
167}