1use std::sync::Arc;
2use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
3
4use async_trait::async_trait;
5use reqwest::{Client, Method};
6use serde::Deserialize;
7
8use crate::gcp::GcpCredentialProvider;
9use crate::retry::RetryExt;
10use crate::service::HttpService;
11use crate::token::{TemporaryToken, jwt_exp_secs};
12use crate::{RetryConfig, TokenProvider};
13
14#[derive(Eq, PartialEq)]
20pub struct DatabricksCredential {
21 pub bearer: String,
23}
24
25impl std::fmt::Debug for DatabricksCredential {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.debug_struct("DatabricksCredential")
31 .field("bearer", &"<redacted>")
32 .finish()
33 }
34}
35
36#[derive(Deserialize, Debug)]
38pub(crate) struct DatabricksTokenResponse {
39 pub access_token: String,
40 pub expires_in: u64,
41}
42
43#[derive(Debug, thiserror::Error)]
44pub(crate) enum Error {
45 #[error("Error performing Databricks token request: {}", source)]
46 TokenRequest { source: crate::retry::Error },
47
48 #[error("Error reading Databricks token response body: {}", source)]
49 TokenResponseBody { source: reqwest::Error },
50}
51
52impl From<Error> for crate::Error {
53 fn from(value: Error) -> Self {
54 Self::Generic {
55 source: Box::new(value),
56 }
57 }
58}
59
60#[derive(Debug)]
70pub(crate) struct DatabricksM2MProvider {
71 pub token_url: String,
72 pub client_id: String,
73 pub client_secret: String,
74 pub scope: String,
75}
76
77#[async_trait]
78impl TokenProvider for DatabricksM2MProvider {
79 type Credential = DatabricksCredential;
80
81 async fn fetch_token(
82 &self,
83 client: &Client,
84 service: &Arc<dyn HttpService>,
85 retry: &RetryConfig,
86 ) -> crate::Result<TemporaryToken<Arc<DatabricksCredential>>> {
87 let response: DatabricksTokenResponse = client
88 .request(Method::POST, &self.token_url)
89 .form(&[
90 ("grant_type", "client_credentials"),
91 ("client_id", self.client_id.as_str()),
92 ("client_secret", self.client_secret.as_str()),
93 ("scope", self.scope.as_str()),
94 ])
95 .retryable(retry, service.clone())
96 .idempotent(true)
97 .send()
98 .await
99 .map_err(|source| Error::TokenRequest { source })?
100 .json()
101 .await
102 .map_err(|source| Error::TokenResponseBody { source })?;
103
104 Ok(TemporaryToken {
105 token: Arc::new(DatabricksCredential {
106 bearer: response.access_token,
107 }),
108 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
109 })
110 }
111}
112
113#[derive(Debug)]
118pub(crate) struct DatabricksGcpTokenExchangeProvider {
119 pub token_url: String,
120 pub gcp_provider: GcpCredentialProvider,
121}
122
123#[async_trait]
124impl TokenProvider for DatabricksGcpTokenExchangeProvider {
125 type Credential = DatabricksCredential;
126
127 async fn fetch_token(
128 &self,
129 client: &Client,
130 service: &Arc<dyn HttpService>,
131 retry: &RetryConfig,
132 ) -> crate::Result<TemporaryToken<Arc<DatabricksCredential>>> {
133 let gcp_cred = self.gcp_provider.get_credential().await?;
134
135 let response: DatabricksTokenResponse = client
136 .request(Method::POST, &self.token_url)
137 .form(&[
138 ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
139 ("assertion", gcp_cred.bearer.as_str()),
140 ])
141 .retryable(retry, service.clone())
142 .idempotent(true)
143 .send()
144 .await
145 .map_err(|source| Error::TokenRequest { source })?
146 .json()
147 .await
148 .map_err(|source| Error::TokenResponseBody { source })?;
149
150 Ok(TemporaryToken {
151 token: Arc::new(DatabricksCredential {
152 bearer: response.access_token,
153 }),
154 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
155 })
156 }
157}
158
159pub(crate) fn jwt_exp(token: &str) -> Option<Instant> {
163 let exp = jwt_exp_secs(token)?;
164 let now_secs = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
165 let secs_remaining = exp.checked_sub(now_secs)?;
166 Some(Instant::now() + Duration::from_secs(secs_remaining))
167}
168
169#[derive(Debug)]
178pub(crate) struct OidcEnvTokenProvider {
179 pub env_var: String,
181}
182
183#[async_trait]
184impl TokenProvider for OidcEnvTokenProvider {
185 type Credential = DatabricksCredential;
186
187 async fn fetch_token(
188 &self,
189 _client: &Client,
190 _service: &Arc<dyn HttpService>,
191 _retry: &RetryConfig,
192 ) -> crate::Result<TemporaryToken<Arc<DatabricksCredential>>> {
193 let token = std::env::var(&self.env_var).map_err(|_| crate::Error::Generic {
194 source: format!(
195 "env-oidc: environment variable {:?} is not set",
196 self.env_var
197 )
198 .into(),
199 })?;
200 let expiry = jwt_exp(&token);
201 Ok(TemporaryToken {
202 token: Arc::new(DatabricksCredential { bearer: token }),
203 expiry,
204 })
205 }
206}
207
208#[derive(Debug)]
218pub(crate) struct OidcFileTokenProvider {
219 pub filepath: String,
221}
222
223#[async_trait]
224impl TokenProvider for OidcFileTokenProvider {
225 type Credential = DatabricksCredential;
226
227 async fn fetch_token(
228 &self,
229 _client: &Client,
230 _service: &Arc<dyn HttpService>,
231 _retry: &RetryConfig,
232 ) -> crate::Result<TemporaryToken<Arc<DatabricksCredential>>> {
233 let token = std::fs::read_to_string(&self.filepath).map_err(|e| crate::Error::Generic {
234 source: format!(
235 "file-oidc: failed to read token from {:?}: {e}",
236 self.filepath
237 )
238 .into(),
239 })?;
240 let token = token.trim().to_owned();
241 let expiry = jwt_exp(&token);
242 Ok(TemporaryToken {
243 token: Arc::new(DatabricksCredential { bearer: token }),
244 expiry,
245 })
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use base64::Engine as _;
252
253 use super::*;
254 use crate::service::ReqwestService;
255
256 #[test]
257 fn test_debug_does_not_leak_secrets() {
258 let credential = DatabricksCredential {
262 bearer: "fake-token-do-not-print".to_string(),
263 };
264 let rendered = format!("{credential:?}");
265 assert!(
266 !rendered.contains("fake-token-do-not-print"),
267 "bearer token leaked: {rendered}"
268 );
269 assert!(rendered.contains("<redacted>"));
270 }
271
272 #[tokio::test]
273 async fn test_m2m_fetch_token() {
274 let mut server = mockito::Server::new_async().await;
275
276 let _mock = server
277 .mock("POST", "/oidc/v1/token")
278 .match_body(mockito::Matcher::AllOf(vec![
279 mockito::Matcher::Regex("grant_type=client_credentials".into()),
280 mockito::Matcher::Regex("client_id=myid".into()),
281 mockito::Matcher::Regex("scope=unity-catalog".into()),
282 ]))
283 .with_status(200)
284 .with_body(r#"{"access_token":"DBTOKEN","expires_in":3600,"token_type":"Bearer"}"#)
285 .create_async()
286 .await;
287
288 let client = Client::new();
289 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
290 let retry = RetryConfig::default();
291
292 let provider = DatabricksM2MProvider {
293 token_url: format!("{}/oidc/v1/token", server.url()),
294 client_id: "myid".into(),
295 client_secret: "mysecret".into(),
296 scope: "unity-catalog".into(),
297 };
298
299 let token = provider
300 .fetch_token(&client, &service, &retry)
301 .await
302 .unwrap();
303 assert_eq!(token.token.bearer, "DBTOKEN");
304 assert!(token.expiry.is_some());
305 }
306
307 fn make_jwt(exp: u64) -> String {
309 let header = base64::engine::general_purpose::URL_SAFE_NO_PAD
310 .encode(r#"{"alg":"none","typ":"JWT"}"#);
311 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
312 .encode(format!(r#"{{"sub":"test","exp":{exp}}}"#));
313 format!("{header}.{payload}.sig")
314 }
315
316 #[test]
317 fn test_jwt_exp_future() {
318 let future_exp = SystemTime::now()
319 .duration_since(UNIX_EPOCH)
320 .unwrap()
321 .as_secs()
322 + 3600;
323 let token = make_jwt(future_exp);
324 let expiry = jwt_exp(&token);
325 assert!(expiry.is_some());
326 let remaining = expiry.unwrap().duration_since(Instant::now());
328 assert!(remaining.as_secs() > 3590 && remaining.as_secs() <= 3600);
329 }
330
331 #[test]
332 fn test_jwt_exp_already_expired() {
333 let past_exp = SystemTime::now()
335 .duration_since(UNIX_EPOCH)
336 .unwrap()
337 .as_secs()
338 .saturating_sub(60);
339 let token = make_jwt(past_exp);
340 assert!(jwt_exp(&token).is_none());
341 }
342
343 #[tokio::test]
344 async fn test_env_oidc_provider() {
345 let future_exp = SystemTime::now()
346 .duration_since(UNIX_EPOCH)
347 .unwrap()
348 .as_secs()
349 + 3600;
350 let jwt = make_jwt(future_exp);
351 let env_var = "TEST_OIDC_TOKEN_UNIQUE_KEY_12345";
352 unsafe { std::env::set_var(env_var, &jwt) };
354
355 let client = Client::new();
356 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
357 let retry = RetryConfig::default();
358
359 let provider = OidcEnvTokenProvider {
360 env_var: env_var.to_owned(),
361 };
362 let token = provider
363 .fetch_token(&client, &service, &retry)
364 .await
365 .unwrap();
366 assert_eq!(token.token.bearer, jwt);
367 assert!(token.expiry.is_some());
368
369 unsafe { std::env::remove_var(env_var) };
371 }
372
373 #[tokio::test]
374 async fn test_file_oidc_provider() {
375 use std::io::Write as _;
376 let future_exp = SystemTime::now()
377 .duration_since(UNIX_EPOCH)
378 .unwrap()
379 .as_secs()
380 + 3600;
381 let jwt = make_jwt(future_exp);
382
383 let mut tmp = tempfile::NamedTempFile::new().unwrap();
384 write!(tmp, "{jwt}").unwrap();
385
386 let client = Client::new();
387 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
388 let retry = RetryConfig::default();
389
390 let provider = OidcFileTokenProvider {
391 filepath: tmp.path().to_str().unwrap().to_owned(),
392 };
393 let token = provider
394 .fetch_token(&client, &service, &retry)
395 .await
396 .unwrap();
397 assert_eq!(token.token.bearer, jwt);
398 assert!(token.expiry.is_some());
399 }
400
401 #[tokio::test]
402 async fn test_m2m_token_refresh() {
403 let mut server = mockito::Server::new_async().await;
404
405 let _mock1 = server
407 .mock("POST", "/oidc/v1/token")
408 .match_body(mockito::Matcher::AllOf(vec![
409 mockito::Matcher::Regex("grant_type=client_credentials".into()),
410 mockito::Matcher::Regex("client_id=refresh-client".into()),
411 ]))
412 .with_status(200)
413 .with_body(r#"{"access_token":"TOKEN_FIRST","expires_in":1,"token_type":"Bearer"}"#)
414 .create_async()
415 .await;
416
417 let client = Client::new();
418 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
419 let retry = RetryConfig::default();
420
421 let provider = DatabricksM2MProvider {
422 token_url: format!("{}/oidc/v1/token", server.url()),
423 client_id: "refresh-client".into(),
424 client_secret: "refresh-secret".into(),
425 scope: "unity-catalog".into(),
426 };
427
428 let token1 = provider
429 .fetch_token(&client, &service, &retry)
430 .await
431 .unwrap();
432 assert_eq!(token1.token.bearer, "TOKEN_FIRST");
433 assert!(token1.expiry.is_some());
435
436 let _mock2 = server
438 .mock("POST", "/oidc/v1/token")
439 .with_status(200)
440 .with_body(r#"{"access_token":"TOKEN_SECOND","expires_in":3600,"token_type":"Bearer"}"#)
441 .create_async()
442 .await;
443
444 let token2 = provider
445 .fetch_token(&client, &service, &retry)
446 .await
447 .unwrap();
448 assert_eq!(token2.token.bearer, "TOKEN_SECOND");
449
450 _mock1.assert_async().await;
451 _mock2.assert_async().await;
452 }
453
454 #[tokio::test]
455 async fn test_uc_credential_vending_aws() {
456 let mut server = mockito::Server::new_async().await;
459
460 let _token_mock = server
461 .mock("POST", "/oidc/v1/token")
462 .with_status(200)
463 .with_body(
464 r#"{"access_token":"UC_BEARER_TOKEN","expires_in":3600,"token_type":"Bearer"}"#,
465 )
466 .create_async()
467 .await;
468
469 let _creds_mock = server
470 .mock("POST", "/api/2.1/unity-catalog/temporary-table-credentials")
471 .match_header("Authorization", "Bearer UC_BEARER_TOKEN")
472 .with_status(200)
473 .with_body(
474 r#"{
475 "aws_temp_credentials": {
476 "access_key_id": "ASIATESTVENDING",
477 "secret_access_key": "vendingsecret",
478 "session_token": "vendingtoken"
479 },
480 "expiration_time": "2099-01-01T00:00:00Z"
481 }"#,
482 )
483 .create_async()
484 .await;
485
486 let client = Client::new();
487 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
488 let retry = RetryConfig::default();
489
490 let provider = DatabricksM2MProvider {
491 token_url: format!("{}/oidc/v1/token", server.url()),
492 client_id: "uc-client".into(),
493 client_secret: "uc-secret".into(),
494 scope: "unity-catalog".into(),
495 };
496
497 let token = provider
498 .fetch_token(&client, &service, &retry)
499 .await
500 .unwrap();
501
502 let vend_resp = client
504 .request(
505 Method::POST,
506 format!(
507 "{}/api/2.1/unity-catalog/temporary-table-credentials",
508 server.url()
509 ),
510 )
511 .bearer_auth(&token.token.bearer)
512 .send()
513 .await
514 .unwrap();
515
516 assert!(vend_resp.status().is_success());
517
518 let body: serde_json::Value = vend_resp.json().await.unwrap();
519 assert_eq!(
520 body["aws_temp_credentials"]["access_key_id"],
521 "ASIATESTVENDING"
522 );
523
524 _token_mock.assert_async().await;
525 _creds_mock.assert_async().await;
526 }
527}