dbx_tools_databricks_auth/
lib.rs1mod client;
2mod error;
3mod m2m;
4mod oauth;
5mod oauth_endpoints;
6mod oauth_template;
7mod profile;
8mod storage;
9mod token;
10
11pub use client::{AuthClient, AuthOptions};
12pub use error::{Error, Result};
13pub use m2m::MachineToMachineFlow;
14pub use oauth::OAuthFlow;
15pub use oauth_template::{default_callback_image_src, OAuthTemplate, OAuthTemplateContext};
16pub use profile::{
17 resolve_config_file, AuthKind, Profile, ProfileOptions, TargetKind, DEFAULT_ACCOUNTS_HOST,
18 DEFAULT_CLIENT_ID, DEFAULT_CONFIG_FILE,
19};
20use std::{path::PathBuf, sync::Arc, time::Duration};
21#[cfg(feature = "keyring")]
22pub use storage::KeyringStore;
23pub use storage::{
24 open_store, CredentialStore, FileStore, MemoryStore, StorageLock, StoreBackend, StoreOptions,
25};
26pub use token::Token;
27
28use time::Duration as TimeDuration;
29
30#[uniffi::export(with_foreign)]
31#[async_trait::async_trait]
32pub trait StorageAdapter: Send + Sync {
33 async fn load(&self, profile: String) -> BindingResult<Option<String>>;
34 async fn prepare_write(&self) -> BindingResult<()>;
35 async fn save(&self, profile: String, token: String) -> BindingResult<()>;
36 async fn remove(&self, profile: String) -> BindingResult<()>;
37 async fn acquire_lock(&self, profile: String, timeout_millis: u64) -> BindingResult<String>;
38 async fn release_lock(&self, lease: String) -> BindingResult<()>;
39 fn name(&self) -> String;
40}
41
42#[derive(Clone, uniffi::Record)]
44pub struct DatabricksAuthOptions {
45 #[uniffi(default = None)]
46 pub profile: Option<String>,
47 #[uniffi(default = None)]
48 pub host: Option<String>,
49 #[uniffi(default = None)]
50 pub account_id: Option<String>,
51 #[uniffi(default = None)]
52 pub workspace_id: Option<String>,
53 #[uniffi(default = None)]
54 pub config_file: Option<String>,
55 #[uniffi(default = None)]
56 pub client_id: Option<String>,
57 #[uniffi(default = None)]
59 pub group_id: Option<String>,
60 #[uniffi(default = None)]
62 pub auth_type: Option<String>,
63 #[uniffi(default = None)]
64 pub scopes: Option<Vec<String>>,
65 #[uniffi(default = None)]
66 pub target: Option<String>,
67 #[uniffi(default = None)]
68 pub cache_dir: Option<String>,
69 #[uniffi(default = None)]
71 pub callback_image_src: Option<String>,
72 #[uniffi(default = 30)]
73 pub lock_timeout_seconds: u64,
74 #[uniffi(default = 3600)]
75 pub login_timeout_seconds: u64,
76 #[uniffi(default = 300)]
77 pub refresh_buffer_seconds: i64,
78 #[uniffi(default = true)]
80 pub prefer_user_to_machine: bool,
81}
82
83impl Default for DatabricksAuthOptions {
84 fn default() -> Self {
85 Self {
86 profile: None,
87 host: None,
88 account_id: None,
89 workspace_id: None,
90 config_file: None,
91 client_id: None,
92 group_id: None,
93 auth_type: None,
94 scopes: None,
95 target: None,
96 cache_dir: None,
97 callback_image_src: None,
98 lock_timeout_seconds: 30,
99 login_timeout_seconds: 3600,
100 refresh_buffer_seconds: 300,
101 prefer_user_to_machine: true,
102 }
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
107pub enum Storage {
108 Auto,
109 Memory,
110 File,
111 Keyring,
112}
113
114#[derive(Clone, uniffi::Record)]
115pub struct AccessToken {
116 pub access_token: String,
117 pub token_type: String,
118 pub expiry: Option<String>,
119 pub scopes: Vec<String>,
120}
121
122#[derive(Clone, uniffi::Record)]
123pub struct DatabricksAuthStatus {
124 pub profile: String,
125 pub host: String,
126 pub storage: Storage,
127}
128
129#[derive(Debug, thiserror::Error, uniffi::Error)]
130pub enum DatabricksAuthError {
131 #[error("{message}")]
132 Failure { message: String },
133}
134
135impl From<uniffi::UnexpectedUniFFICallbackError> for DatabricksAuthError {
136 fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self {
137 Self::Failure {
138 message: error.to_string(),
139 }
140 }
141}
142
143type BindingResult<T> = std::result::Result<T, DatabricksAuthError>;
144
145#[derive(uniffi::Object)]
146pub struct PersistentAuth {
147 inner: AuthClient,
148}
149
150#[uniffi::export(async_runtime = "tokio", default(storage = None))]
151pub async fn create_persistent_auth(
152 options: DatabricksAuthOptions,
153 storage: Option<Storage>,
154) -> BindingResult<Arc<PersistentAuth>> {
155 let store = open_binding_store(&options, storage).await?;
156 create_persistent_auth_with_store(options, store).await
157}
158
159#[uniffi::export(async_runtime = "tokio")]
160pub async fn create_persistent_auth_with_storage(
161 options: DatabricksAuthOptions,
162 storage: Arc<dyn StorageAdapter>,
163) -> BindingResult<Arc<PersistentAuth>> {
164 create_persistent_auth_with_store(options, Arc::new(ForeignStore { storage })).await
165}
166
167async fn create_persistent_auth_with_store(
168 options: DatabricksAuthOptions,
169 store: Arc<dyn CredentialStore>,
170) -> BindingResult<Arc<PersistentAuth>> {
171 let profile = Profile::from_sources(ProfileOptions {
172 profile: options.profile.clone(),
173 host: options.host.clone(),
174 account_id: options.account_id.clone(),
175 workspace_id: options.workspace_id.clone(),
176 client_id: options.client_id.clone(),
177 client_secret: None,
178 group_id: options.group_id.clone(),
179 auth_type: options.auth_type.clone(),
180 scopes: options.scopes.clone(),
181 target: options.target.as_deref().map(parse_target).transpose()?,
182 config_file: options.config_file.as_deref().map(PathBuf::from),
183 prefer_user_to_machine: options.prefer_user_to_machine,
184 })
185 .map_err(binding_error)?;
186 let inner = AuthClient::new(
187 profile,
188 store,
189 AuthOptions {
190 refresh_buffer: TimeDuration::seconds(options.refresh_buffer_seconds),
191 lock_timeout: Duration::from_secs(options.lock_timeout_seconds),
192 login_timeout: Duration::from_secs(options.login_timeout_seconds),
193 callback_image_src: options.callback_image_src.clone(),
194 },
195 )
196 .map_err(binding_error)?;
197 Ok(Arc::new(PersistentAuth { inner }))
198}
199
200struct ForeignStore {
201 storage: Arc<dyn StorageAdapter>,
202}
203
204struct ForeignLock {
205 storage: Arc<dyn StorageAdapter>,
206 lease: String,
207}
208
209#[async_trait::async_trait]
210impl StorageLock for ForeignLock {
211 async fn release(self: Box<Self>) -> Result<()> {
212 self.storage
213 .release_lock(self.lease)
214 .await
215 .map_err(|error| Error::Storage(error.to_string()))
216 }
217}
218
219#[async_trait::async_trait]
220impl CredentialStore for ForeignStore {
221 async fn load(&self, profile: &str) -> Result<Option<Token>> {
222 self.storage
223 .load(profile.to_owned())
224 .await
225 .map_err(|error| Error::Storage(error.to_string()))?
226 .map(|token| serde_json::from_str(&token).map_err(Into::into))
227 .transpose()
228 }
229
230 async fn prepare_write(&self) -> Result<()> {
231 self.storage
232 .prepare_write()
233 .await
234 .map_err(|error| Error::Storage(error.to_string()))
235 }
236
237 async fn save(&self, profile: &str, token: &Token) -> Result<()> {
238 self.storage
239 .save(profile.to_owned(), serde_json::to_string(token)?)
240 .await
241 .map_err(|error| Error::Storage(error.to_string()))
242 }
243
244 async fn delete(&self, profile: &str) -> Result<()> {
245 self.storage
246 .remove(profile.to_owned())
247 .await
248 .map_err(|error| Error::Storage(error.to_string()))
249 }
250
251 async fn lock(&self, profile: &str, timeout: Duration) -> Result<Box<dyn StorageLock>> {
252 let timeout_millis = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX);
253 let lease = self
254 .storage
255 .acquire_lock(profile.to_owned(), timeout_millis)
256 .await
257 .map_err(|error| Error::Storage(error.to_string()))?;
258 Ok(Box::new(ForeignLock {
259 storage: Arc::clone(&self.storage),
260 lease,
261 }))
262 }
263
264 fn name(&self) -> &'static str {
265 "custom"
266 }
267}
268
269#[uniffi::export(async_runtime = "tokio")]
270impl PersistentAuth {
271 pub async fn challenge(&self) -> BindingResult<()> {
272 self.inner.login().await.map(|_| ()).map_err(binding_error)
273 }
274
275 #[uniffi::method(default(login = None))]
276 pub async fn token(&self, login: Option<bool>) -> BindingResult<AccessToken> {
277 let token = match login {
278 Some(true) => self.inner.login().await,
279 None => self.inner.token_or_login().await,
280 Some(false) => self.inner.token().await,
281 };
282 token.map(Into::into).map_err(binding_error)
283 }
284
285 pub async fn force_refresh_token(&self) -> BindingResult<AccessToken> {
286 self.inner
287 .force_refresh()
288 .await
289 .map(Into::into)
290 .map_err(binding_error)
291 }
292
293 pub async fn logout(&self) -> BindingResult<()> {
294 self.inner.logout().await.map_err(binding_error)
295 }
296
297 pub fn status(&self) -> DatabricksAuthStatus {
298 DatabricksAuthStatus {
299 profile: self.inner.profile().name.clone(),
300 host: self.inner.profile().host.to_string(),
301 storage: storage_from_name(self.inner.store_name()),
302 }
303 }
304}
305
306impl From<Token> for AccessToken {
307 fn from(token: Token) -> Self {
308 Self {
309 access_token: token.access_token,
310 token_type: token.token_type,
311 expiry: token.expires_at.map(|value| value.to_string()),
312 scopes: token.scopes,
313 }
314 }
315}
316
317async fn open_binding_store(
318 options: &DatabricksAuthOptions,
319 storage: Option<Storage>,
320) -> BindingResult<Arc<dyn CredentialStore>> {
321 open_store(StoreOptions {
322 backend: storage.map(Into::into),
323 cache_dir: options.cache_dir.as_deref().map(PathBuf::from),
324 config_file: options.config_file.as_deref().map(PathBuf::from),
325 })
326 .await
327 .map_err(binding_error)
328}
329
330fn parse_target(value: &str) -> BindingResult<TargetKind> {
331 match value.trim().to_ascii_lowercase().as_str() {
332 "workspace" => Ok(TargetKind::Workspace),
333 "account" => Ok(TargetKind::Account),
334 "unified" => Ok(TargetKind::Unified),
335 _ => Err(DatabricksAuthError::Failure {
336 message: "target must be workspace, account, or unified".into(),
337 }),
338 }
339}
340
341impl From<Storage> for StoreBackend {
342 fn from(storage: Storage) -> Self {
343 match storage {
344 Storage::Auto => Self::Auto,
345 Storage::Memory => Self::Memory,
346 Storage::File => Self::File,
347 Storage::Keyring => Self::Keyring,
348 }
349 }
350}
351
352fn storage_from_name(name: &str) -> Storage {
353 match name {
354 "memory" => Storage::Memory,
355 "keyring" => Storage::Keyring,
356 _ => Storage::File,
357 }
358}
359
360fn binding_error(error: impl std::fmt::Display) -> DatabricksAuthError {
361 DatabricksAuthError::Failure {
362 message: error.to_string(),
363 }
364}
365
366uniffi::setup_scaffolding!();