1use crate::Transport;
2use reqwest::IntoUrl;
3use unitycatalog_common::models::temporary_credentials::v1::TemporaryCredential;
4use unitycatalog_common::{
5 model_versions::v1::GetModelVersionRequest,
6 models::temporary_credentials::v1::{
7 GenerateTemporaryModelVersionCredentialsRequest, GenerateTemporaryPathCredentialsRequest,
8 GenerateTemporaryTableCredentialsRequest, GenerateTemporaryVolumeCredentialsRequest,
9 generate_temporary_model_version_credentials_request::Operation as MvOperation,
10 generate_temporary_path_credentials_request::Operation as PthOperation,
11 generate_temporary_table_credentials_request::Operation as TblOperation,
12 generate_temporary_volume_credentials_request::Operation as VolOperation,
13 },
14 tables::v1::GetTableRequest,
15 volumes::v1::GetVolumeRequest,
16};
17use url::Url;
18use uuid::Uuid;
19
20use crate::Result;
21use crate::codegen::tables::TableServiceClient;
22pub(super) use crate::codegen::temporary_credentials::TemporaryCredentialClient as TemporaryCredentialClientBase;
23use crate::codegen::volumes::client::VolumeServiceClient;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum TableReference {
28 Id(Uuid),
30 Name(String),
32}
33
34impl From<String> for TableReference {
35 fn from(name: String) -> Self {
36 TableReference::Name(name)
37 }
38}
39
40impl From<&str> for TableReference {
41 fn from(name: &str) -> Self {
42 TableReference::Name(name.to_string())
43 }
44}
45
46impl From<Uuid> for TableReference {
47 fn from(id: Uuid) -> Self {
48 TableReference::Id(id)
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum VolumeReference {
60 Id(Uuid),
62 Name(String),
64}
65
66impl From<String> for VolumeReference {
67 fn from(name: String) -> Self {
68 VolumeReference::Name(name)
69 }
70}
71
72impl From<&str> for VolumeReference {
73 fn from(name: &str) -> Self {
74 VolumeReference::Name(name.to_string())
75 }
76}
77
78impl From<Uuid> for VolumeReference {
79 fn from(id: Uuid) -> Self {
80 VolumeReference::Id(id)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum TableOperation {
87 Read,
89 ReadWrite,
91}
92
93impl From<TableOperation> for i32 {
94 fn from(operation: TableOperation) -> Self {
95 match operation {
96 TableOperation::Read => TblOperation::Read as i32,
97 TableOperation::ReadWrite => TblOperation::ReadWrite as i32,
98 }
99 }
100}
101
102impl From<TableOperation> for TblOperation {
103 fn from(operation: TableOperation) -> Self {
104 match operation {
105 TableOperation::Read => TblOperation::Read,
106 TableOperation::ReadWrite => TblOperation::ReadWrite,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum PathOperation {
114 Read,
116 ReadWrite,
118 CreateTable,
120}
121
122impl From<PathOperation> for i32 {
123 fn from(operation: PathOperation) -> Self {
124 match operation {
125 PathOperation::Read => PthOperation::PathRead as i32,
126 PathOperation::ReadWrite => PthOperation::PathReadWrite as i32,
127 PathOperation::CreateTable => PthOperation::PathCreateTable as i32,
128 }
129 }
130}
131
132impl From<PathOperation> for PthOperation {
133 fn from(operation: PathOperation) -> Self {
134 match operation {
135 PathOperation::Read => PthOperation::PathRead,
136 PathOperation::ReadWrite => PthOperation::PathReadWrite,
137 PathOperation::CreateTable => PthOperation::PathCreateTable,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum VolumeOperation {
145 Read,
147 ReadWrite,
149}
150
151impl From<VolumeOperation> for i32 {
152 fn from(operation: VolumeOperation) -> Self {
153 match operation {
154 VolumeOperation::Read => VolOperation::ReadVolume as i32,
155 VolumeOperation::ReadWrite => VolOperation::WriteVolume as i32,
156 }
157 }
158}
159
160impl From<VolumeOperation> for VolOperation {
161 fn from(operation: VolumeOperation) -> Self {
162 match operation {
163 VolumeOperation::Read => VolOperation::ReadVolume,
164 VolumeOperation::ReadWrite => VolOperation::WriteVolume,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum ModelVersionOperation {
172 Read,
174 ReadWrite,
176}
177
178impl From<ModelVersionOperation> for i32 {
179 fn from(operation: ModelVersionOperation) -> Self {
180 match operation {
181 ModelVersionOperation::Read => MvOperation::ReadModelVersion as i32,
182 ModelVersionOperation::ReadWrite => MvOperation::ReadWriteModelVersion as i32,
183 }
184 }
185}
186
187impl From<ModelVersionOperation> for MvOperation {
188 fn from(operation: ModelVersionOperation) -> Self {
189 match operation {
190 ModelVersionOperation::Read => MvOperation::ReadModelVersion,
191 ModelVersionOperation::ReadWrite => MvOperation::ReadWriteModelVersion,
192 }
193 }
194}
195
196#[derive(Clone)]
203pub struct TemporaryCredentialClient {
204 client: TemporaryCredentialClientBase,
205}
206
207impl TemporaryCredentialClient {
208 pub fn new_with_url(client: Transport, mut base_url: Url) -> Self {
213 if !base_url.path().ends_with('/') {
214 base_url.set_path(&format!("{}/", base_url.path()));
215 }
216 Self {
217 client: TemporaryCredentialClientBase::new(client, base_url),
218 }
219 }
220
221 pub fn new(client: TemporaryCredentialClientBase) -> Self {
223 Self { client }
224 }
225
226 async fn post_credential<R: serde::Serialize>(
241 &self,
242 path: &str,
243 request: &R,
244 ) -> Result<TemporaryCredential> {
245 let url = self.client.base_url.join(path)?;
246 let response = self
247 .client
248 .client
249 .post(url)
250 .json(request)
251 .send_raw()
252 .await
253 .map_err(crate::Error::from_api_send)?;
254 let response = crate::error::check_api_response(response).await?;
255 let bytes = response.bytes().await?;
256
257 let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
261 if let Some(obj) = value.as_object_mut() {
262 const ONEOF_KEYS: [&str; 10] = [
263 "aws_temp_credentials",
264 "awsTempCredentials",
265 "azure_user_delegation_sas",
266 "azureUserDelegationSas",
267 "azure_aad",
268 "azureAad",
269 "gcp_oauth_token",
270 "gcpOauthToken",
271 "r2_temp_credentials",
272 "r2TempCredentials",
273 ];
274 obj.retain(|key, v| !(v.is_null() && ONEOF_KEYS.contains(&key.as_str())));
275 }
276 Ok(serde_json::from_value(value)?)
277 }
278
279 pub async fn temporary_table_credential(
288 &self,
289 table: impl Into<TableReference>,
290 operation: TableOperation,
291 ) -> Result<(TemporaryCredential, Uuid)> {
292 let (table_id, storage_location) = match table.into() {
295 TableReference::Id(id) => (id.as_hyphenated().to_string(), None),
296 TableReference::Name(name) => {
297 let table_client = TableServiceClient::new(
298 self.client.client.clone(),
299 self.client.base_url.clone(),
300 );
301 let table_info = table_client
302 .get_table(&GetTableRequest {
303 full_name: name,
304 include_browse: Some(false),
305 include_delta_metadata: Some(false),
306 include_manifest_capabilities: Some(false),
307 ..Default::default()
308 })
309 .await?;
310 (
311 table_info.table_id.clone().unwrap_or_default(),
312 table_info.storage_location.clone(),
313 )
314 }
315 };
316 let uuid =
317 Uuid::parse_str(&table_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
318 let mut credential = self
319 .post_credential(
320 "temporary-table-credentials",
321 &GenerateTemporaryTableCredentialsRequest {
322 table_id,
323 operation: TblOperation::from(operation).into(),
324 ..Default::default()
325 },
326 )
327 .await?;
328 backfill_credential_url(&mut credential, storage_location);
329 Ok((credential, uuid))
330 }
331
332 pub async fn temporary_path_credential(
337 &self,
338 path: impl IntoUrl,
339 operation: PathOperation,
340 dry_run: impl Into<Option<bool>>,
341 ) -> Result<(TemporaryCredential, Url)> {
342 let url = path.into_url()?;
343 Ok((
344 self.post_credential(
345 "temporary-path-credentials",
346 &GenerateTemporaryPathCredentialsRequest {
347 url: url.to_string(),
348 operation: PthOperation::from(operation).into(),
349 dry_run: dry_run.into(),
350 ..Default::default()
351 },
352 )
353 .await?,
354 url,
355 ))
356 }
357
358 pub async fn temporary_volume_credential(
375 &self,
376 volume: impl Into<VolumeReference>,
377 operation: VolumeOperation,
378 ) -> Result<(TemporaryCredential, Uuid)> {
379 let (volume_id, storage_location) = match volume.into() {
380 VolumeReference::Id(id) => (id.as_hyphenated().to_string(), None),
381 VolumeReference::Name(name) => {
382 let volume_client = VolumeServiceClient::new(
383 self.client.client.clone(),
384 self.client.base_url.clone(),
385 );
386 let info = volume_client
387 .get_volume(&GetVolumeRequest {
388 name,
389 include_browse: Some(false),
390 ..Default::default()
391 })
392 .await?;
393 (info.volume_id, Some(info.storage_location))
394 }
395 };
396 let uuid =
397 Uuid::parse_str(&volume_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
398 let mut credential = self
399 .post_credential(
400 "temporary-volume-credentials",
401 &GenerateTemporaryVolumeCredentialsRequest {
402 volume_id,
403 operation: VolOperation::from(operation).into(),
404 ..Default::default()
405 },
406 )
407 .await?;
408 backfill_credential_url(&mut credential, storage_location);
409 Ok((credential, uuid))
410 }
411
412 pub async fn temporary_model_version_credential(
425 &self,
426 full_name: impl Into<String>,
427 version: i64,
428 operation: ModelVersionOperation,
429 ) -> Result<TemporaryCredential> {
430 let full_name = full_name.into();
431 let [catalog_name, schema_name, model_name] =
432 <[String; 3]>::try_from(full_name.split('.').map(str::to_string).collect::<Vec<_>>())
433 .map_err(|_| {
434 unitycatalog_common::Error::invalid_argument(
435 "full_name must be a three-level catalog.schema.model name",
436 )
437 })?;
438
439 let mv_client = crate::codegen::model_versions::ModelVersionServiceClient::new(
442 self.client.client.clone(),
443 self.client.base_url.clone(),
444 );
445 let storage_location = mv_client
446 .get_model_version(&GetModelVersionRequest {
447 full_name: full_name.clone(),
448 version,
449 ..Default::default()
450 })
451 .await
452 .ok()
453 .and_then(|mv| mv.storage_location);
454
455 let mut credential = self
456 .post_credential(
457 "temporary-model-version-credentials",
458 &GenerateTemporaryModelVersionCredentialsRequest {
459 catalog_name,
460 schema_name,
461 model_name,
462 version,
463 operation: MvOperation::from(operation).into(),
464 ..Default::default()
465 },
466 )
467 .await?;
468 backfill_credential_url(&mut credential, storage_location);
469 Ok(credential)
470 }
471}
472
473fn backfill_credential_url(credential: &mut TemporaryCredential, storage_location: Option<String>) {
482 if credential.url.is_empty()
483 && let Some(location) = storage_location
484 && !location.is_empty()
485 {
486 credential.url = location;
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
497 fn malformed_table_id_is_error_not_panic() {
498 let result =
499 Uuid::parse_str("not-a-uuid").map_err(unitycatalog_common::Error::InvalidIdentifier);
500 let err: crate::Error = result.unwrap_err().into();
501 assert!(matches!(err, crate::Error::Common { .. }));
502 }
503}