dbx_tools_databricks_auth/
lib.rs1mod client;
2mod m2m;
3mod oauth;
4mod oauth_endpoints;
5mod profile;
6mod storage;
7
8pub use client::{AuthClient, AuthOptions};
9pub use dbx_tools_auth::AuthSession;
10pub use dbx_tools_auth::{default_callback_image_src, OAuthTemplate, OAuthTemplateContext};
11pub use dbx_tools_auth::{
12 AccessToken, AuthError as DatabricksAuthError, Storage, StorageAdapter, Token,
13};
14use dbx_tools_auth::{BindingResult, StorageHandle};
15pub use dbx_tools_auth::{Error, Result};
16use dbx_tools_databricks::{databricks_cli_available, is_databricks_app};
17pub use m2m::MachineToMachineFlow;
18pub use oauth::OAuthFlow;
19pub use profile::{
20 resolve_config_file, AuthKind, Profile, ProfileOptions, TargetKind, DEFAULT_ACCOUNTS_HOST,
21 DEFAULT_CLIENT_ID, DEFAULT_CONFIG_FILE,
22};
23use std::{path::PathBuf, sync::Arc};
24pub use storage::{
25 open_store, CredentialStore, FileStore, MemoryStore, StorageLock, StoreBackend, StoreOptions,
26};
27
28#[derive(Clone, uniffi::Record)]
30pub struct DatabricksAuthOptions {
31 #[uniffi(default = None)]
33 pub profile: Option<String>,
34 #[uniffi(default = None)]
36 pub host: Option<String>,
37 #[uniffi(default = None)]
39 pub account_id: Option<String>,
40 #[uniffi(default = None)]
42 pub workspace_id: Option<String>,
43 #[uniffi(default = None)]
45 pub config_file: Option<String>,
46 #[uniffi(default = None)]
48 pub client_id: Option<String>,
49 #[uniffi(default = None)]
51 pub group_id: Option<String>,
52 #[uniffi(default = None)]
54 pub auth_type: Option<String>,
55 #[uniffi(default = None)]
57 pub scopes: Option<Vec<String>>,
58 #[uniffi(default = None)]
60 pub target: Option<String>,
61 #[uniffi(default = None)]
63 pub cache_dir: Option<String>,
64 #[uniffi(default = None)]
66 pub auth: Option<AuthOptions>,
67 #[uniffi(default = true)]
69 pub prefer_user_to_machine: bool,
70}
71
72impl Default for DatabricksAuthOptions {
73 fn default() -> Self {
74 Self {
75 profile: None,
76 host: None,
77 account_id: None,
78 workspace_id: None,
79 config_file: None,
80 client_id: None,
81 group_id: None,
82 auth_type: None,
83 scopes: None,
84 target: None,
85 cache_dir: None,
86 auth: None,
87 prefer_user_to_machine: true,
88 }
89 }
90}
91
92#[derive(Clone, uniffi::Record)]
93pub struct DatabricksAuthStatus {
95 pub profile: String,
96 pub host: String,
97 pub storage: Storage,
98}
99
100#[derive(uniffi::Object)]
101pub struct PersistentAuth {
103 inner: AuthClient,
104}
105
106#[uniffi::export(async_runtime = "tokio", default(storage = None))]
107pub async fn create_persistent_auth(
109 options: DatabricksAuthOptions,
110 storage: Option<Storage>,
111) -> BindingResult<Arc<PersistentAuth>> {
112 let profile = resolve_profile(&options)?;
113 let in_app = is_databricks_app();
114 let use_databricks_cli = should_use_databricks_cli(
115 profile.auth_kind,
116 storage,
117 in_app,
118 databricks_cli_available(),
119 );
120 let backend = storage_backend(storage, in_app);
121 let store = open_binding_store(&options, backend).await?;
122 create_persistent_auth_with_store(options, profile, store, use_databricks_cli).await
123}
124
125#[uniffi::export(async_runtime = "tokio")]
126pub async fn create_persistent_auth_with_storage(
128 options: DatabricksAuthOptions,
129 storage: Arc<StorageHandle>,
130) -> BindingResult<Arc<PersistentAuth>> {
131 let profile = resolve_profile(&options)?;
132 create_persistent_auth_with_store(options, profile, storage.store.clone(), false).await
133}
134
135async fn create_persistent_auth_with_store(
136 options: DatabricksAuthOptions,
137 profile: Profile,
138 store: Arc<dyn CredentialStore>,
139 use_databricks_cli: bool,
140) -> BindingResult<Arc<PersistentAuth>> {
141 let inner = AuthClient::new(
142 profile,
143 store,
144 options.auth.unwrap_or_default(),
145 use_databricks_cli,
146 )
147 .map_err(binding_error)?;
148 Ok(Arc::new(PersistentAuth { inner }))
149}
150
151fn should_use_databricks_cli(
152 auth_kind: AuthKind,
153 storage: Option<Storage>,
154 in_app: bool,
155 available: bool,
156) -> bool {
157 auth_kind == AuthKind::UserToMachine
158 && !in_app
159 && available
160 && storage.is_none_or(|storage| storage == Storage::Auto)
161}
162
163fn storage_backend(storage: Option<Storage>, in_app: bool) -> Storage {
164 match storage {
165 Some(Storage::Memory) => Storage::Memory,
166 Some(Storage::File) => Storage::File,
167 Some(Storage::Auto) | None if in_app => Storage::Memory,
168 Some(Storage::Auto) | None => Storage::File,
169 }
170}
171
172fn resolve_profile(options: &DatabricksAuthOptions) -> BindingResult<Profile> {
173 Profile::from_sources(ProfileOptions {
174 profile: options.profile.clone(),
175 host: options.host.clone(),
176 account_id: options.account_id.clone(),
177 workspace_id: options.workspace_id.clone(),
178 client_id: options.client_id.clone(),
179 client_secret: None,
180 group_id: options.group_id.clone(),
181 auth_type: options.auth_type.clone(),
182 scopes: options.scopes.clone(),
183 target: options.target.as_deref().map(parse_target).transpose()?,
184 config_file: options.config_file.as_deref().map(PathBuf::from),
185 prefer_user_to_machine: options.prefer_user_to_machine,
186 })
187 .map_err(binding_error)
188}
189
190#[uniffi::export(async_runtime = "tokio")]
191impl PersistentAuth {
192 pub async fn challenge(&self) -> BindingResult<()> {
194 self.inner.login().await.map(|_| ()).map_err(binding_error)
195 }
196
197 #[uniffi::method(default(login = None))]
199 pub async fn token(&self, login: Option<bool>) -> BindingResult<AccessToken> {
200 self.inner
201 .token_with_login(login)
202 .await
203 .map(Into::into)
204 .map_err(binding_error)
205 }
206
207 pub async fn force_refresh_token(&self) -> BindingResult<AccessToken> {
209 self.inner
210 .force_refresh()
211 .await
212 .map(Into::into)
213 .map_err(binding_error)
214 }
215
216 pub async fn refresh_rejected_token(
218 &self,
219 stale_access_token: String,
220 ) -> BindingResult<AccessToken> {
221 self.inner
222 .refresh_rejected_token(&stale_access_token)
223 .await
224 .map(Into::into)
225 .map_err(binding_error)
226 }
227
228 pub async fn logout(&self) -> BindingResult<()> {
230 self.inner.logout().await.map_err(binding_error)
231 }
232
233 pub fn status(&self) -> DatabricksAuthStatus {
235 DatabricksAuthStatus {
236 profile: self.inner.profile().name.clone(),
237 host: self.inner.profile().host.to_string(),
238 storage: storage_from_name(self.inner.store_name()),
239 }
240 }
241}
242
243async fn open_binding_store(
244 options: &DatabricksAuthOptions,
245 storage: Storage,
246) -> BindingResult<Arc<dyn CredentialStore>> {
247 open_store(StoreOptions {
248 backend: Some(storage),
249 cache_dir: options.cache_dir.as_deref().map(PathBuf::from),
250 })
251 .await
252 .map_err(binding_error)
253}
254
255fn parse_target(value: &str) -> BindingResult<TargetKind> {
256 match value.trim().to_ascii_lowercase().as_str() {
257 "workspace" => Ok(TargetKind::Workspace),
258 "account" => Ok(TargetKind::Account),
259 "unified" => Ok(TargetKind::Unified),
260 _ => Err(DatabricksAuthError::Failure {
261 message: "target must be workspace, account, or unified".into(),
262 }),
263 }
264}
265
266fn storage_from_name(name: &str) -> Storage {
267 match name {
268 "memory" => Storage::Memory,
269 _ => Storage::File,
270 }
271}
272
273fn binding_error(error: impl std::fmt::Display) -> DatabricksAuthError {
274 DatabricksAuthError::Failure {
275 message: error.to_string(),
276 }
277}
278
279uniffi::setup_scaffolding!();
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn cli_refresh_requires_available_automatic_u2m() {
287 assert!(should_use_databricks_cli(
288 AuthKind::UserToMachine,
289 None,
290 false,
291 true
292 ));
293 assert!(should_use_databricks_cli(
294 AuthKind::UserToMachine,
295 Some(Storage::Auto),
296 false,
297 true
298 ));
299 assert!(!should_use_databricks_cli(
300 AuthKind::UserToMachine,
301 Some(Storage::File),
302 false,
303 true
304 ));
305 assert!(!should_use_databricks_cli(
306 AuthKind::UserToMachine,
307 Some(Storage::Memory),
308 false,
309 true
310 ));
311 assert!(!should_use_databricks_cli(
312 AuthKind::MachineToMachine,
313 None,
314 false,
315 true
316 ));
317 assert!(!should_use_databricks_cli(
318 AuthKind::UserToMachine,
319 None,
320 false,
321 false
322 ));
323 assert!(!should_use_databricks_cli(
324 AuthKind::UserToMachine,
325 None,
326 true,
327 true
328 ));
329 }
330
331 #[test]
332 fn automatic_storage_tracks_the_runtime_and_explicit_storage_is_preserved() {
333 assert_eq!(storage_backend(None, false), Storage::File);
334 assert_eq!(storage_backend(Some(Storage::Auto), false), Storage::File);
335 assert_eq!(storage_backend(None, true), Storage::Memory);
336 assert_eq!(storage_backend(Some(Storage::Auto), true), Storage::Memory);
337 assert_eq!(storage_backend(Some(Storage::File), true), Storage::File);
338 assert_eq!(
339 storage_backend(Some(Storage::Memory), true),
340 Storage::Memory
341 );
342 }
343}