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