1use olai_http::CloudClient;
2use reqwest::IntoUrl;
3use unitycatalog_common::models::temporary_credentials::v1::TemporaryCredential;
4use unitycatalog_common::{
5 models::temporary_credentials::v1::{
6 GenerateTemporaryPathCredentialsRequest, GenerateTemporaryTableCredentialsRequest,
7 GenerateTemporaryVolumeCredentialsRequest,
8 generate_temporary_path_credentials_request::Operation as PthOperation,
9 generate_temporary_table_credentials_request::Operation as TblOperation,
10 generate_temporary_volume_credentials_request::Operation as VolOperation,
11 },
12 tables::v1::GetTableRequest,
13 volumes::v1::GetVolumeRequest,
14};
15use url::Url;
16use uuid::Uuid;
17
18use crate::Result;
19use crate::codegen::tables::TableServiceClient;
20pub(super) use crate::codegen::temporary_credentials::TemporaryCredentialClient as TemporaryCredentialClientBase;
21use crate::codegen::volumes::client::VolumeServiceClient;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum TableReference {
26 Id(Uuid),
28 Name(String),
30}
31
32impl From<String> for TableReference {
33 fn from(name: String) -> Self {
34 TableReference::Name(name)
35 }
36}
37
38impl From<&str> for TableReference {
39 fn from(name: &str) -> Self {
40 TableReference::Name(name.to_string())
41 }
42}
43
44impl From<Uuid> for TableReference {
45 fn from(id: Uuid) -> Self {
46 TableReference::Id(id)
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum VolumeReference {
58 Id(Uuid),
60 Name(String),
62}
63
64impl From<String> for VolumeReference {
65 fn from(name: String) -> Self {
66 VolumeReference::Name(name)
67 }
68}
69
70impl From<&str> for VolumeReference {
71 fn from(name: &str) -> Self {
72 VolumeReference::Name(name.to_string())
73 }
74}
75
76impl From<Uuid> for VolumeReference {
77 fn from(id: Uuid) -> Self {
78 VolumeReference::Id(id)
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum TableOperation {
85 Read,
87 ReadWrite,
89}
90
91impl From<TableOperation> for i32 {
92 fn from(operation: TableOperation) -> Self {
93 match operation {
94 TableOperation::Read => TblOperation::Read as i32,
95 TableOperation::ReadWrite => TblOperation::ReadWrite as i32,
96 }
97 }
98}
99
100impl From<TableOperation> for TblOperation {
101 fn from(operation: TableOperation) -> Self {
102 match operation {
103 TableOperation::Read => TblOperation::Read,
104 TableOperation::ReadWrite => TblOperation::ReadWrite,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum PathOperation {
112 Read,
114 ReadWrite,
116 CreateTable,
118}
119
120impl From<PathOperation> for i32 {
121 fn from(operation: PathOperation) -> Self {
122 match operation {
123 PathOperation::Read => PthOperation::PathRead as i32,
124 PathOperation::ReadWrite => PthOperation::PathReadWrite as i32,
125 PathOperation::CreateTable => PthOperation::PathCreateTable as i32,
126 }
127 }
128}
129
130impl From<PathOperation> for PthOperation {
131 fn from(operation: PathOperation) -> Self {
132 match operation {
133 PathOperation::Read => PthOperation::PathRead,
134 PathOperation::ReadWrite => PthOperation::PathReadWrite,
135 PathOperation::CreateTable => PthOperation::PathCreateTable,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum VolumeOperation {
143 Read,
145 ReadWrite,
147}
148
149impl From<VolumeOperation> for i32 {
150 fn from(operation: VolumeOperation) -> Self {
151 match operation {
152 VolumeOperation::Read => VolOperation::ReadVolume as i32,
153 VolumeOperation::ReadWrite => VolOperation::WriteVolume as i32,
154 }
155 }
156}
157
158impl From<VolumeOperation> for VolOperation {
159 fn from(operation: VolumeOperation) -> Self {
160 match operation {
161 VolumeOperation::Read => VolOperation::ReadVolume,
162 VolumeOperation::ReadWrite => VolOperation::WriteVolume,
163 }
164 }
165}
166
167#[derive(Clone)]
174pub struct TemporaryCredentialClient {
175 client: TemporaryCredentialClientBase,
176}
177
178impl TemporaryCredentialClient {
179 pub fn new_with_url(client: CloudClient, mut base_url: Url) -> Self {
184 if !base_url.path().ends_with('/') {
185 base_url.set_path(&format!("{}/", base_url.path()));
186 }
187 Self {
188 client: TemporaryCredentialClientBase::new(client, base_url),
189 }
190 }
191
192 pub fn new(client: TemporaryCredentialClientBase) -> Self {
194 Self { client }
195 }
196
197 async fn post_credential<R: serde::Serialize>(
212 &self,
213 path: &str,
214 request: &R,
215 ) -> Result<TemporaryCredential> {
216 let url = self.client.base_url.join(path)?;
217 let response = self.client.client.post(url).json(request).send().await?;
218 if !response.status().is_success() {
219 return Err(crate::error::parse_error_response(response).await);
220 }
221 let bytes = response.bytes().await?;
222
223 let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
227 if let Some(obj) = value.as_object_mut() {
228 const ONEOF_KEYS: [&str; 10] = [
229 "aws_temp_credentials",
230 "awsTempCredentials",
231 "azure_user_delegation_sas",
232 "azureUserDelegationSas",
233 "azure_aad",
234 "azureAad",
235 "gcp_oauth_token",
236 "gcpOauthToken",
237 "r2_temp_credentials",
238 "r2TempCredentials",
239 ];
240 obj.retain(|key, v| !(v.is_null() && ONEOF_KEYS.contains(&key.as_str())));
241 }
242 Ok(serde_json::from_value(value)?)
243 }
244
245 pub async fn temporary_table_credential(
254 &self,
255 table: impl Into<TableReference>,
256 operation: TableOperation,
257 ) -> Result<(TemporaryCredential, Uuid)> {
258 let (table_id, storage_location) = match table.into() {
261 TableReference::Id(id) => (id.as_hyphenated().to_string(), None),
262 TableReference::Name(name) => {
263 let table_client = TableServiceClient::new(
264 self.client.client.clone(),
265 self.client.base_url.clone(),
266 );
267 let table_info = table_client
268 .get_table(&GetTableRequest {
269 full_name: name,
270 include_browse: Some(false),
271 include_delta_metadata: Some(false),
272 include_manifest_capabilities: Some(false),
273 })
274 .await?;
275 (
276 table_info.table_id().to_string(),
277 table_info.storage_location.clone(),
278 )
279 }
280 };
281 let uuid =
282 Uuid::parse_str(&table_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
283 let mut credential = self
284 .post_credential(
285 "temporary-table-credentials",
286 &GenerateTemporaryTableCredentialsRequest {
287 table_id,
288 operation: operation.into(),
289 },
290 )
291 .await?;
292 backfill_credential_url(&mut credential, storage_location);
293 Ok((credential, uuid))
294 }
295
296 pub async fn temporary_path_credential(
301 &self,
302 path: impl IntoUrl,
303 operation: PathOperation,
304 dry_run: impl Into<Option<bool>>,
305 ) -> Result<(TemporaryCredential, Url)> {
306 let url = path.into_url()?;
307 Ok((
308 self.post_credential(
309 "temporary-path-credentials",
310 &GenerateTemporaryPathCredentialsRequest {
311 url: url.to_string(),
312 operation: operation.into(),
313 dry_run: dry_run.into(),
314 },
315 )
316 .await?,
317 url,
318 ))
319 }
320
321 pub async fn temporary_volume_credential(
338 &self,
339 volume: impl Into<VolumeReference>,
340 operation: VolumeOperation,
341 ) -> Result<(TemporaryCredential, Uuid)> {
342 let (volume_id, storage_location) = match volume.into() {
343 VolumeReference::Id(id) => (id.as_hyphenated().to_string(), None),
344 VolumeReference::Name(name) => {
345 let volume_client = VolumeServiceClient::new(
346 self.client.client.clone(),
347 self.client.base_url.clone(),
348 );
349 let info = volume_client
350 .get_volume(&GetVolumeRequest {
351 name,
352 include_browse: Some(false),
353 })
354 .await?;
355 (info.volume_id, Some(info.storage_location))
356 }
357 };
358 let uuid =
359 Uuid::parse_str(&volume_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
360 let mut credential = self
361 .post_credential(
362 "temporary-volume-credentials",
363 &GenerateTemporaryVolumeCredentialsRequest {
364 volume_id,
365 operation: operation.into(),
366 },
367 )
368 .await?;
369 backfill_credential_url(&mut credential, storage_location);
370 Ok((credential, uuid))
371 }
372}
373
374fn backfill_credential_url(credential: &mut TemporaryCredential, storage_location: Option<String>) {
383 if credential.url.is_empty()
384 && let Some(location) = storage_location
385 && !location.is_empty()
386 {
387 credential.url = location;
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
398 fn malformed_table_id_is_error_not_panic() {
399 let result =
400 Uuid::parse_str("not-a-uuid").map_err(unitycatalog_common::Error::InvalidIdentifier);
401 let err: crate::Error = result.unwrap_err().into();
402 assert!(matches!(err, crate::Error::Common { .. }));
403 }
404}