mod client;
mod m2m;
mod oauth;
mod oauth_endpoints;
mod profile;
mod storage;
pub use client::{AuthClient, AuthOptions};
pub use dbx_tools_auth::AuthSession;
pub use dbx_tools_auth::{default_callback_image_src, OAuthTemplate, OAuthTemplateContext};
pub use dbx_tools_auth::{
AccessToken, AuthError as DatabricksAuthError, Storage, StorageAdapter, Token,
};
use dbx_tools_auth::{BindingResult, StorageHandle};
pub use dbx_tools_auth::{Error, Result};
use dbx_tools_databricks::{databricks_cli_available, is_databricks_app};
pub use m2m::MachineToMachineFlow;
pub use oauth::OAuthFlow;
pub use profile::{
resolve_config_file, AuthKind, Profile, ProfileOptions, TargetKind, DEFAULT_ACCOUNTS_HOST,
DEFAULT_CLIENT_ID, DEFAULT_CONFIG_FILE,
};
use std::{path::PathBuf, sync::Arc};
pub use storage::{
open_store, CredentialStore, FileStore, MemoryStore, StorageLock, StoreBackend, StoreOptions,
};
#[derive(Clone, uniffi::Record)]
pub struct DatabricksAuthOptions {
#[uniffi(default = None)]
pub profile: Option<String>,
#[uniffi(default = None)]
pub host: Option<String>,
#[uniffi(default = None)]
pub account_id: Option<String>,
#[uniffi(default = None)]
pub workspace_id: Option<String>,
#[uniffi(default = None)]
pub config_file: Option<String>,
#[uniffi(default = None)]
pub client_id: Option<String>,
#[uniffi(default = None)]
pub group_id: Option<String>,
#[uniffi(default = None)]
pub auth_type: Option<String>,
#[uniffi(default = None)]
pub scopes: Option<Vec<String>>,
#[uniffi(default = None)]
pub target: Option<String>,
#[uniffi(default = None)]
pub cache_dir: Option<String>,
#[uniffi(default = None)]
pub auth: Option<AuthOptions>,
#[uniffi(default = true)]
pub prefer_user_to_machine: bool,
}
impl Default for DatabricksAuthOptions {
fn default() -> Self {
Self {
profile: None,
host: None,
account_id: None,
workspace_id: None,
config_file: None,
client_id: None,
group_id: None,
auth_type: None,
scopes: None,
target: None,
cache_dir: None,
auth: None,
prefer_user_to_machine: true,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct DatabricksAuthStatus {
pub profile: String,
pub host: String,
pub storage: Storage,
}
#[derive(uniffi::Object)]
pub struct PersistentAuth {
inner: AuthClient,
}
#[uniffi::export(async_runtime = "tokio", default(storage = None))]
pub async fn create_persistent_auth(
options: DatabricksAuthOptions,
storage: Option<Storage>,
) -> BindingResult<Arc<PersistentAuth>> {
let in_app = is_databricks_app();
let profile = resolve_profile(&options, in_app)?;
let use_databricks_cli = should_use_databricks_cli(
profile.auth_kind,
storage,
in_app,
databricks_cli_available(),
);
let backend = storage_backend(storage, in_app);
let store = open_binding_store(&options, backend).await?;
create_persistent_auth_with_store(options, profile, store, use_databricks_cli).await
}
#[uniffi::export(async_runtime = "tokio")]
pub async fn create_persistent_auth_with_storage(
options: DatabricksAuthOptions,
storage: Arc<StorageHandle>,
) -> BindingResult<Arc<PersistentAuth>> {
let profile = resolve_profile(&options, is_databricks_app())?;
create_persistent_auth_with_store(options, profile, storage.store.clone(), false).await
}
async fn create_persistent_auth_with_store(
options: DatabricksAuthOptions,
profile: Profile,
store: Arc<dyn CredentialStore>,
use_databricks_cli: bool,
) -> BindingResult<Arc<PersistentAuth>> {
let inner = AuthClient::new(
profile,
store,
options.auth.unwrap_or_default(),
use_databricks_cli,
)
.map_err(binding_error)?;
Ok(Arc::new(PersistentAuth { inner }))
}
fn should_use_databricks_cli(
auth_kind: AuthKind,
storage: Option<Storage>,
in_app: bool,
available: bool,
) -> bool {
auth_kind == AuthKind::UserToMachine
&& !in_app
&& available
&& storage.is_none_or(|storage| storage == Storage::Auto)
}
fn storage_backend(storage: Option<Storage>, in_app: bool) -> Storage {
match storage {
Some(Storage::Memory) => Storage::Memory,
Some(Storage::File) => Storage::File,
Some(Storage::Auto) | None if in_app => Storage::Memory,
Some(Storage::Auto) | None => Storage::File,
}
}
fn resolve_profile(options: &DatabricksAuthOptions, in_app: bool) -> BindingResult<Profile> {
Profile::from_sources(ProfileOptions {
profile: options.profile.clone(),
host: options.host.clone(),
account_id: options.account_id.clone(),
workspace_id: options.workspace_id.clone(),
client_id: options.client_id.clone(),
client_secret: None,
access_token: None,
group_id: options.group_id.clone(),
auth_type: options.auth_type.clone(),
scopes: options.scopes.clone(),
target: options.target.as_deref().map(parse_target).transpose()?,
config_file: options.config_file.as_deref().map(PathBuf::from),
prefer_user_to_machine: options.prefer_user_to_machine,
skip_implicit_pat: in_app,
})
.map_err(binding_error)
}
#[uniffi::export(async_runtime = "tokio")]
impl PersistentAuth {
pub async fn challenge(&self) -> BindingResult<()> {
self.inner.login().await.map(|_| ()).map_err(binding_error)
}
#[uniffi::method(default(login = None))]
pub async fn token(&self, login: Option<bool>) -> BindingResult<AccessToken> {
self.inner
.token_with_login(login)
.await
.map(Into::into)
.map_err(binding_error)
}
pub async fn force_refresh_token(&self) -> BindingResult<AccessToken> {
self.inner
.force_refresh()
.await
.map(Into::into)
.map_err(binding_error)
}
pub async fn refresh_rejected_token(
&self,
stale_access_token: String,
) -> BindingResult<AccessToken> {
self.inner
.refresh_rejected_token(&stale_access_token)
.await
.map(Into::into)
.map_err(binding_error)
}
pub async fn logout(&self) -> BindingResult<()> {
self.inner.logout().await.map_err(binding_error)
}
pub fn status(&self) -> DatabricksAuthStatus {
DatabricksAuthStatus {
profile: self.inner.profile().name.clone(),
host: self.inner.profile().host.to_string(),
storage: storage_from_name(self.inner.store_name()),
}
}
}
async fn open_binding_store(
options: &DatabricksAuthOptions,
storage: Storage,
) -> BindingResult<Arc<dyn CredentialStore>> {
open_store(StoreOptions {
backend: Some(storage),
cache_dir: options.cache_dir.as_deref().map(PathBuf::from),
})
.await
.map_err(binding_error)
}
fn parse_target(value: &str) -> BindingResult<TargetKind> {
match value.trim().to_ascii_lowercase().as_str() {
"workspace" => Ok(TargetKind::Workspace),
"account" => Ok(TargetKind::Account),
"unified" => Ok(TargetKind::Unified),
_ => Err(DatabricksAuthError::Failure {
message: "target must be workspace, account, or unified".into(),
}),
}
}
fn storage_from_name(name: &str) -> Storage {
match name {
"memory" => Storage::Memory,
_ => Storage::File,
}
}
fn binding_error(error: impl std::fmt::Display) -> DatabricksAuthError {
DatabricksAuthError::Failure {
message: error.to_string(),
}
}
uniffi::setup_scaffolding!();
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_refresh_requires_available_automatic_u2m() {
assert!(should_use_databricks_cli(
AuthKind::UserToMachine,
None,
false,
true
));
assert!(should_use_databricks_cli(
AuthKind::UserToMachine,
Some(Storage::Auto),
false,
true
));
assert!(!should_use_databricks_cli(
AuthKind::UserToMachine,
Some(Storage::File),
false,
true
));
assert!(!should_use_databricks_cli(
AuthKind::UserToMachine,
Some(Storage::Memory),
false,
true
));
assert!(!should_use_databricks_cli(
AuthKind::MachineToMachine,
None,
false,
true
));
assert!(!should_use_databricks_cli(
AuthKind::PersonalAccessToken,
None,
false,
true
));
assert!(!should_use_databricks_cli(
AuthKind::UserToMachine,
None,
false,
false
));
assert!(!should_use_databricks_cli(
AuthKind::UserToMachine,
None,
true,
true
));
}
#[test]
fn automatic_storage_tracks_the_runtime_and_explicit_storage_is_preserved() {
assert_eq!(storage_backend(None, false), Storage::File);
assert_eq!(storage_backend(Some(Storage::Auto), false), Storage::File);
assert_eq!(storage_backend(None, true), Storage::Memory);
assert_eq!(storage_backend(Some(Storage::Auto), true), Storage::Memory);
assert_eq!(storage_backend(Some(Storage::File), true), Storage::File);
assert_eq!(
storage_backend(Some(Storage::Memory), true),
Storage::Memory
);
}
}