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)]
83pub enum TableOperation {
84 Read,
85 ReadWrite,
86}
87
88impl From<TableOperation> for i32 {
89 fn from(operation: TableOperation) -> Self {
90 match operation {
91 TableOperation::Read => TblOperation::Read as i32,
92 TableOperation::ReadWrite => TblOperation::ReadWrite as i32,
93 }
94 }
95}
96
97impl From<TableOperation> for TblOperation {
98 fn from(operation: TableOperation) -> Self {
99 match operation {
100 TableOperation::Read => TblOperation::Read,
101 TableOperation::ReadWrite => TblOperation::ReadWrite,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum PathOperation {
108 Read,
109 ReadWrite,
110 CreateTable,
111}
112
113impl From<PathOperation> for i32 {
114 fn from(operation: PathOperation) -> Self {
115 match operation {
116 PathOperation::Read => PthOperation::PathRead as i32,
117 PathOperation::ReadWrite => PthOperation::PathReadWrite as i32,
118 PathOperation::CreateTable => PthOperation::PathCreateTable as i32,
119 }
120 }
121}
122
123impl From<PathOperation> for PthOperation {
124 fn from(operation: PathOperation) -> Self {
125 match operation {
126 PathOperation::Read => PthOperation::PathRead,
127 PathOperation::ReadWrite => PthOperation::PathReadWrite,
128 PathOperation::CreateTable => PthOperation::PathCreateTable,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum VolumeOperation {
136 Read,
138 ReadWrite,
140}
141
142impl From<VolumeOperation> for i32 {
143 fn from(operation: VolumeOperation) -> Self {
144 match operation {
145 VolumeOperation::Read => VolOperation::ReadVolume as i32,
146 VolumeOperation::ReadWrite => VolOperation::WriteVolume as i32,
147 }
148 }
149}
150
151impl From<VolumeOperation> for VolOperation {
152 fn from(operation: VolumeOperation) -> Self {
153 match operation {
154 VolumeOperation::Read => VolOperation::ReadVolume,
155 VolumeOperation::ReadWrite => VolOperation::WriteVolume,
156 }
157 }
158}
159
160#[derive(Clone)]
161pub struct TemporaryCredentialClient {
162 client: TemporaryCredentialClientBase,
163}
164
165impl TemporaryCredentialClient {
166 pub fn new_with_url(client: CloudClient, mut base_url: Url) -> Self {
167 if !base_url.path().ends_with('/') {
168 base_url.set_path(&format!("{}/", base_url.path()));
169 }
170 Self {
171 client: TemporaryCredentialClientBase::new(client, base_url),
172 }
173 }
174
175 pub fn new(client: TemporaryCredentialClientBase) -> Self {
176 Self { client }
177 }
178
179 async fn post_credential<R: serde::Serialize>(
194 &self,
195 path: &str,
196 request: &R,
197 ) -> Result<TemporaryCredential> {
198 let url = self.client.base_url.join(path)?;
199 let response = self.client.client.post(url).json(request).send().await?;
200 if !response.status().is_success() {
201 return Err(crate::error::parse_error_response(response).await);
202 }
203 let bytes = response.bytes().await?;
204
205 let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
209 if let Some(obj) = value.as_object_mut() {
210 const ONEOF_KEYS: [&str; 10] = [
211 "aws_temp_credentials",
212 "awsTempCredentials",
213 "azure_user_delegation_sas",
214 "azureUserDelegationSas",
215 "azure_aad",
216 "azureAad",
217 "gcp_oauth_token",
218 "gcpOauthToken",
219 "r2_temp_credentials",
220 "r2TempCredentials",
221 ];
222 obj.retain(|key, v| !(v.is_null() && ONEOF_KEYS.contains(&key.as_str())));
223 }
224 Ok(serde_json::from_value(value)?)
225 }
226
227 pub async fn temporary_table_credential(
238 &self,
239 table: impl Into<TableReference>,
240 operation: TableOperation,
241 ) -> Result<(TemporaryCredential, Uuid)> {
242 let (table_id, storage_location) = match table.into() {
245 TableReference::Id(id) => (id.as_hyphenated().to_string(), None),
246 TableReference::Name(name) => {
247 let table_client = TableServiceClient::new(
248 self.client.client.clone(),
249 self.client.base_url.clone(),
250 );
251 let table_info = table_client
252 .get_table(&GetTableRequest {
253 full_name: name,
254 include_browse: Some(false),
255 include_delta_metadata: Some(false),
256 include_manifest_capabilities: Some(false),
257 })
258 .await?;
259 (
260 table_info.table_id().to_string(),
261 table_info.storage_location.clone(),
262 )
263 }
264 };
265 let uuid =
266 Uuid::parse_str(&table_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
267 let mut credential = self
268 .post_credential(
269 "temporary-table-credentials",
270 &GenerateTemporaryTableCredentialsRequest {
271 table_id,
272 operation: operation.into(),
273 },
274 )
275 .await?;
276 backfill_credential_url(&mut credential, storage_location);
277 Ok((credential, uuid))
278 }
279
280 pub async fn temporary_path_credential(
281 &self,
282 path: impl IntoUrl,
283 operation: PathOperation,
284 dry_run: impl Into<Option<bool>>,
285 ) -> Result<(TemporaryCredential, Url)> {
286 let url = path.into_url()?;
287 Ok((
288 self.post_credential(
289 "temporary-path-credentials",
290 &GenerateTemporaryPathCredentialsRequest {
291 url: url.to_string(),
292 operation: operation.into(),
293 dry_run: dry_run.into(),
294 },
295 )
296 .await?,
297 url,
298 ))
299 }
300
301 pub async fn temporary_volume_credential(
319 &self,
320 volume: impl Into<VolumeReference>,
321 operation: VolumeOperation,
322 ) -> Result<(TemporaryCredential, Uuid)> {
323 let (volume_id, storage_location) = match volume.into() {
324 VolumeReference::Id(id) => (id.as_hyphenated().to_string(), None),
325 VolumeReference::Name(name) => {
326 let volume_client = VolumeServiceClient::new(
327 self.client.client.clone(),
328 self.client.base_url.clone(),
329 );
330 let info = volume_client
331 .get_volume(&GetVolumeRequest {
332 name,
333 include_browse: Some(false),
334 })
335 .await?;
336 (info.volume_id, Some(info.storage_location))
337 }
338 };
339 let uuid =
340 Uuid::parse_str(&volume_id).map_err(unitycatalog_common::Error::InvalidIdentifier)?;
341 let mut credential = self
342 .post_credential(
343 "temporary-volume-credentials",
344 &GenerateTemporaryVolumeCredentialsRequest {
345 volume_id,
346 operation: operation.into(),
347 },
348 )
349 .await?;
350 backfill_credential_url(&mut credential, storage_location);
351 Ok((credential, uuid))
352 }
353}
354
355fn backfill_credential_url(credential: &mut TemporaryCredential, storage_location: Option<String>) {
364 if credential.url.is_empty()
365 && let Some(location) = storage_location
366 && !location.is_empty()
367 {
368 credential.url = location;
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
379 fn malformed_table_id_is_error_not_panic() {
380 let result =
381 Uuid::parse_str("not-a-uuid").map_err(unitycatalog_common::Error::InvalidIdentifier);
382 let err: crate::Error = result.unwrap_err().into();
383 assert!(matches!(err, crate::Error::Common { .. }));
384 }
385}