mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
On Windows the token cache was memory-only because std has no way to set an owner-only DACL, and the Win32 calls need `unsafe`, which buzz-agent forbids. Each process therefore ran its own sign-in, and Databricks stayed gated off on Windows in the desktop app. The new buzz-private-file crate holds that FFI behind a safe API, so buzz-agent keeps `forbid(unsafe_code)` on every platform: - `create_new` creates a file with the protected DACL `D:P(A;;FA;;;OW)` set atomically at creation, the Windows counterpart of `mode(0o600)`. - `open_owned` opens without following a final link, refuses anything but a regular file owned by the process's default owner, and then re-applies that DACL on the open handle, like the Unix `O_NOFOLLOW` + `fchmod` repair. auth.rs now persists and reads the cache on Windows through these, with the same temp-file + rename lifecycle as Unix. Tests that were Unix-only only because Windows had no disk cache now run on both, and Windows CI runs the helper crate (including its elevated planted-owner and symlink tests) plus the buzz-agent token-cache unit tests. Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
187 lines
7.0 KiB
Rust
187 lines
7.0 KiB
Rust
//! Opt-in, workspace-scoped Databricks OAuth and catalog reuse.
|
|
//!
|
|
//! Legacy OAuth/catalog entry points retain their destination and intent policy.
|
|
//! This entry requires an explicit HTTPS workspace, app-owned cache root and
|
|
//! opener. Construction reads only that cache; it performs no network or browser
|
|
//! work. The caller must not lend this root to legacy APIs or other applications.
|
|
|
|
use std::{path::Path, sync::Arc, time::Duration};
|
|
|
|
use reqwest::{redirect::Policy, Client, ClientBuilder};
|
|
use url::Url;
|
|
|
|
use crate::{
|
|
auth::{AuthError, AuthIntent, BrowserOpener, PkceOAuthTokenSource},
|
|
catalog::{discover_databricks_models_with_client, ModelEntry},
|
|
config::{Config, DatabricksModelFilter, Provider},
|
|
AgentError,
|
|
};
|
|
|
|
/// An explicitly scoped connection, not an agent runtime or a sign-in side effect.
|
|
///
|
|
/// Only [`connect`](Self::connect) permits browser opening. Discovery is always
|
|
/// headless, including its one 401 refresh. Dropping either operation's future
|
|
/// cancels its actual HTTP/callback work; callers should impose their own total
|
|
/// deadline and fence stale results. Do not hold a controller lock while awaiting.
|
|
/// Tokens remain inside the connection; never serialize this object to a webview.
|
|
pub struct DatabricksConnection {
|
|
workspace: StrictWorkspace,
|
|
source: Arc<PkceOAuthTokenSource>,
|
|
http: Client,
|
|
}
|
|
|
|
impl DatabricksConnection {
|
|
/// Construct without network/browser work or ambient credential/host lookup.
|
|
///
|
|
/// `workspace` must be an HTTPS origin (optional trailing slash), without
|
|
/// userinfo, query or fragment. `cache_root` must be absolute and owned by
|
|
/// this app; a separate `databricks-strict` namespace prevents accidental
|
|
/// coalescing with legacy auth. Token files keep owner-only atomic
|
|
/// persistence.
|
|
/// The opener must not log the authorization URL or its own error details.
|
|
pub fn new(
|
|
workspace: &str,
|
|
cache_root: &Path,
|
|
opener: Arc<dyn BrowserOpener>,
|
|
) -> Result<Self, AgentError> {
|
|
Self::build(workspace, cache_root, opener, Client::builder())
|
|
}
|
|
|
|
fn build(
|
|
workspace: &str,
|
|
cache_root: &Path,
|
|
opener: Arc<dyn BrowserOpener>,
|
|
http: ClientBuilder,
|
|
) -> Result<Self, AgentError> {
|
|
let workspace = StrictWorkspace::new(workspace)?;
|
|
if !cache_root.is_absolute() {
|
|
return Err(AgentError::InvalidParams(
|
|
"an absolute app-owned OAuth cache root is required".into(),
|
|
));
|
|
}
|
|
// Apply policy AFTER the test transport's trust/DNS settings. No caller
|
|
// can inject a client that follows redirects or disable production TLS.
|
|
let http = http
|
|
.redirect(Policy::none())
|
|
.timeout(Duration::from_secs(30))
|
|
.build()
|
|
.map_err(|_| AgentError::Llm("Databricks HTTP client construction failed".into()))?;
|
|
let source = PkceOAuthTokenSource::for_workspace(
|
|
workspace.clone(),
|
|
cache_root,
|
|
opener,
|
|
http.clone(),
|
|
)
|
|
.map_err(|_| AgentError::Llm("Databricks private OAuth cache unavailable".into()))?;
|
|
Ok(Self {
|
|
workspace,
|
|
source,
|
|
http,
|
|
})
|
|
}
|
|
|
|
/// Authenticate after an explicit user action. May call the supplied opener.
|
|
/// A valid cached credential short-circuits; this does not fetch models.
|
|
pub async fn connect(&self) -> Result<(), AuthError> {
|
|
self.source
|
|
.acquire_with_intent(AuthIntent::UserInitiated, None)
|
|
.await
|
|
.map(|_| ())
|
|
}
|
|
|
|
/// Discover v2 models without a browser, using this connection's workspace
|
|
/// and credentials only. Preserves filtering, partial success and labelled
|
|
/// authenticated-empty/no-filter fallback; this is not proof of completeness.
|
|
/// Does not choose or mutate a model. Errors contain no raw provider content.
|
|
pub async fn discover_models(
|
|
&self,
|
|
filter: Option<DatabricksModelFilter>,
|
|
) -> Result<Vec<ModelEntry>, AgentError> {
|
|
let cfg = Config::for_discovery(
|
|
Provider::DatabricksV2,
|
|
String::new(),
|
|
self.workspace.as_str().into(),
|
|
filter,
|
|
);
|
|
discover_databricks_models_with_client(&cfg, self.source.clone(), &self.http)
|
|
.await
|
|
.map_err(|error| match error {
|
|
AgentError::LlmAuth(_) => {
|
|
AgentError::LlmAuth("Databricks authentication required".into())
|
|
}
|
|
_ => AgentError::Llm("Databricks model discovery unavailable".into()),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Unforgeable outside this module: every production value passed to auth was
|
|
/// validated as an explicit HTTPS origin. Endpoint validation occurs inside the
|
|
/// engine on the SAME discovery response subsequently used for both grants.
|
|
#[derive(Clone)]
|
|
pub(crate) struct StrictWorkspace(Url);
|
|
|
|
impl StrictWorkspace {
|
|
fn new(raw: &str) -> Result<Self, AgentError> {
|
|
let rest = raw.strip_prefix("https://").ok_or_else(invalid_workspace)?;
|
|
let authority = rest.strip_suffix('/').unwrap_or(rest);
|
|
if authority.contains(['/', '@']) {
|
|
return Err(invalid_workspace());
|
|
}
|
|
let url = Url::parse(raw).map_err(|_| invalid_workspace())?;
|
|
if raw.trim() != raw
|
|
|| raw.chars().any(char::is_control)
|
|
|| raw.contains('\\')
|
|
|| url.scheme() != "https"
|
|
|| url.host_str().is_none()
|
|
|| !url.username().is_empty()
|
|
|| url.password().is_some()
|
|
|| url.query().is_some()
|
|
|| url.fragment().is_some()
|
|
|| url.path() != "/"
|
|
{
|
|
return Err(invalid_workspace());
|
|
}
|
|
Ok(Self(url))
|
|
}
|
|
|
|
pub(crate) fn as_str(&self) -> &str {
|
|
self.0.as_str().trim_end_matches('/')
|
|
}
|
|
|
|
pub(crate) fn validate_endpoint(&self, raw: &str) -> Result<(), AgentError> {
|
|
let authority = raw
|
|
.strip_prefix("https://")
|
|
.ok_or_else(invalid_endpoint)?
|
|
.split('/')
|
|
.next()
|
|
.ok_or_else(invalid_endpoint)?;
|
|
if authority.contains('@') {
|
|
return Err(invalid_endpoint());
|
|
}
|
|
let endpoint = Url::parse(raw).map_err(|_| invalid_endpoint())?;
|
|
if endpoint.origin() != self.0.origin()
|
|
|| !endpoint.username().is_empty()
|
|
|| endpoint.password().is_some()
|
|
|| endpoint.query().is_some()
|
|
|| endpoint.fragment().is_some()
|
|
|| raw.trim() != raw
|
|
|| raw.chars().any(char::is_control)
|
|
|| raw.contains('\\')
|
|
{
|
|
return Err(invalid_endpoint());
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn invalid_workspace() -> AgentError {
|
|
AgentError::InvalidParams("Databricks workspace must be an explicit HTTPS origin".into())
|
|
}
|
|
fn invalid_endpoint() -> AgentError {
|
|
AgentError::Llm("Databricks OAuth endpoint outside workspace policy".into())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "databricks_tests.rs"]
|
|
mod tests;
|