1mod 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 in_app = is_databricks_app();
113 let profile = resolve_profile(&options, in_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, is_databricks_app())?;
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, in_app: bool) -> 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 access_token: None,
181 group_id: options.group_id.clone(),
182 auth_type: options.auth_type.clone(),
183 scopes: options.scopes.clone(),
184 target: options.target.as_deref().map(parse_target).transpose()?,
185 config_file: options.config_file.as_deref().map(PathBuf::from),
186 prefer_user_to_machine: options.prefer_user_to_machine,
187 skip_implicit_pat: in_app,
188 })
189 .map_err(binding_error)
190}
191
192#[uniffi::export(async_runtime = "tokio")]
193impl PersistentAuth {
194 pub async fn challenge(&self) -> BindingResult<()> {
196 self.inner.login().await.map(|_| ()).map_err(binding_error)
197 }
198
199 #[uniffi::method(default(login = None))]
201 pub async fn token(&self, login: Option<bool>) -> BindingResult<AccessToken> {
202 self.inner
203 .token_with_login(login)
204 .await
205 .map(Into::into)
206 .map_err(binding_error)
207 }
208
209 pub async fn force_refresh_token(&self) -> BindingResult<AccessToken> {
211 self.inner
212 .force_refresh()
213 .await
214 .map(Into::into)
215 .map_err(binding_error)
216 }
217
218 pub async fn refresh_rejected_token(
220 &self,
221 stale_access_token: String,
222 ) -> BindingResult<AccessToken> {
223 self.inner
224 .refresh_rejected_token(&stale_access_token)
225 .await
226 .map(Into::into)
227 .map_err(binding_error)
228 }
229
230 pub async fn logout(&self) -> BindingResult<()> {
232 self.inner.logout().await.map_err(binding_error)
233 }
234
235 pub fn status(&self) -> DatabricksAuthStatus {
237 DatabricksAuthStatus {
238 profile: self.inner.profile().name.clone(),
239 host: self.inner.profile().host.to_string(),
240 storage: storage_from_name(self.inner.store_name()),
241 }
242 }
243}
244
245async fn open_binding_store(
246 options: &DatabricksAuthOptions,
247 storage: Storage,
248) -> BindingResult<Arc<dyn CredentialStore>> {
249 open_store(StoreOptions {
250 backend: Some(storage),
251 cache_dir: options.cache_dir.as_deref().map(PathBuf::from),
252 })
253 .await
254 .map_err(binding_error)
255}
256
257fn parse_target(value: &str) -> BindingResult<TargetKind> {
258 match value.trim().to_ascii_lowercase().as_str() {
259 "workspace" => Ok(TargetKind::Workspace),
260 "account" => Ok(TargetKind::Account),
261 "unified" => Ok(TargetKind::Unified),
262 _ => Err(DatabricksAuthError::Failure {
263 message: "target must be workspace, account, or unified".into(),
264 }),
265 }
266}
267
268fn storage_from_name(name: &str) -> Storage {
269 match name {
270 "memory" => Storage::Memory,
271 _ => Storage::File,
272 }
273}
274
275fn binding_error(error: impl std::fmt::Display) -> DatabricksAuthError {
276 DatabricksAuthError::Failure {
277 message: error.to_string(),
278 }
279}
280
281uniffi::setup_scaffolding!();
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn cli_refresh_requires_available_automatic_u2m() {
289 assert!(should_use_databricks_cli(
290 AuthKind::UserToMachine,
291 None,
292 false,
293 true
294 ));
295 assert!(should_use_databricks_cli(
296 AuthKind::UserToMachine,
297 Some(Storage::Auto),
298 false,
299 true
300 ));
301 assert!(!should_use_databricks_cli(
302 AuthKind::UserToMachine,
303 Some(Storage::File),
304 false,
305 true
306 ));
307 assert!(!should_use_databricks_cli(
308 AuthKind::UserToMachine,
309 Some(Storage::Memory),
310 false,
311 true
312 ));
313 assert!(!should_use_databricks_cli(
314 AuthKind::MachineToMachine,
315 None,
316 false,
317 true
318 ));
319 assert!(!should_use_databricks_cli(
320 AuthKind::PersonalAccessToken,
321 None,
322 false,
323 true
324 ));
325 assert!(!should_use_databricks_cli(
326 AuthKind::UserToMachine,
327 None,
328 false,
329 false
330 ));
331 assert!(!should_use_databricks_cli(
332 AuthKind::UserToMachine,
333 None,
334 true,
335 true
336 ));
337 }
338
339 #[test]
340 fn automatic_storage_tracks_the_runtime_and_explicit_storage_is_preserved() {
341 assert_eq!(storage_backend(None, false), Storage::File);
342 assert_eq!(storage_backend(Some(Storage::Auto), false), Storage::File);
343 assert_eq!(storage_backend(None, true), Storage::Memory);
344 assert_eq!(storage_backend(Some(Storage::Auto), true), Storage::Memory);
345 assert_eq!(storage_backend(Some(Storage::File), true), Storage::File);
346 assert_eq!(
347 storage_backend(Some(Storage::Memory), true),
348 Storage::Memory
349 );
350 }
351}