unitycatalog_server/api/
temporary_credentials.rs1use unitycatalog_common::models::credentials::v1::GetCredentialRequest;
2use unitycatalog_common::models::model_versions::v1::ModelVersion;
3use unitycatalog_common::models::tables::v1::Table;
4use unitycatalog_common::models::temporary_credentials::v1::*;
5use unitycatalog_common::models::volumes::v1::Volume;
6use unitycatalog_common::models::{ResourceIdent, ResourceName, ResourceRef};
7
8use super::RequestContext;
9use crate::api::CredentialHandler;
10use crate::api::credentials::CredentialHandlerExt;
11pub use crate::codegen::temporary_credentials::TemporaryCredentialHandler;
12use crate::policy::{Permission, Policy};
13use crate::services::location::StorageLocationUrl;
14use crate::services::object_store::find_external_location_for_url;
15use crate::services::vending::{mint, to_temporary_credential};
16use crate::store::ResourceStore;
17use crate::{Error, Result};
18
19use buffa::Enumeration;
20use olai_storage_credentials::AccessLevel;
21
22pub(crate) async fn vend_for_location<T>(
29 handler: &T,
30 location: &StorageLocationUrl,
31 access: AccessLevel,
32) -> Result<TemporaryCredential>
33where
34 T: ResourceStore + CredentialHandlerExt + ?Sized,
35{
36 let credential = if location.is_local() {
37 None
38 } else {
39 let ext_loc = find_external_location_for_url(location, handler).await?;
40 Some(
41 handler
42 .get_credential_internal(GetCredentialRequest {
43 name: ext_loc.credential_name.clone(),
44 ..Default::default()
45 })
46 .await?,
47 )
48 };
49 let vended = mint(credential.as_ref(), location, access).await?;
50 Ok(to_temporary_credential(&vended, location))
51}
52
53fn required_permission(access: AccessLevel) -> Permission {
58 match access {
59 AccessLevel::ReadWrite => Permission::Write,
60 _ => Permission::Read,
61 }
62}
63
64fn to_access_level(operation: i32) -> AccessLevel {
73 use generate_temporary_path_credentials_request::Operation as PathOp;
74 use generate_temporary_table_credentials_request::Operation as TableOp;
75 use generate_temporary_volume_credentials_request::Operation as VolumeOp;
76
77 match PathOp::from_i32(operation) {
80 Some(PathOp::PATH_READ_WRITE | PathOp::PATH_CREATE_TABLE) => {
81 return AccessLevel::ReadWrite;
82 }
83 Some(PathOp::PATH_READ | PathOp::UNSPECIFIED) | None => {}
84 }
85 match (TableOp::from_i32(operation), VolumeOp::from_i32(operation)) {
89 (Some(TableOp::READ_WRITE), _) | (_, Some(VolumeOp::WRITE_VOLUME)) => {
90 AccessLevel::ReadWrite
91 }
92 _ => AccessLevel::Read,
93 }
94}
95
96#[async_trait::async_trait]
97impl<
98 T: ResourceStore
99 + Policy<RequestContext>
100 + CredentialHandler<RequestContext>
101 + CredentialHandlerExt,
102> TemporaryCredentialHandler<RequestContext> for T
103{
104 #[tracing::instrument(skip(self, context))]
105 async fn generate_temporary_path_credentials(
106 &self,
107 request: GenerateTemporaryPathCredentialsRequest,
108 context: RequestContext,
109 ) -> Result<TemporaryCredential> {
110 let operation = to_access_level(request.operation.to_i32());
111 let storage_url = StorageLocationUrl::parse(&request.url)?;
112
113 if storage_url.is_local() {
117 return vend_for_location(self, &storage_url, operation).await;
118 }
119
120 let ext_loc = find_external_location_for_url(&storage_url, self).await?;
121 self.authorize_checked(
124 &(&ext_loc).into(),
125 &required_permission(operation),
126 &context,
127 )
128 .await?;
129 vend_for_location(self, &storage_url, operation).await
130 }
131
132 #[tracing::instrument(skip(self, context))]
141 async fn generate_temporary_volume_credentials(
142 &self,
143 request: GenerateTemporaryVolumeCredentialsRequest,
144 context: RequestContext,
145 ) -> Result<TemporaryCredential> {
146 let operation = to_access_level(request.operation.to_i32());
147 let volume_id = uuid::Uuid::parse_str(&request.volume_id)
148 .map_err(|_| Error::invalid_argument("volume_id is not a valid UUID"))?;
149 let volume_ident = ResourceIdent::Volume(ResourceRef::Uuid(volume_id));
152 self.authorize_checked(&volume_ident, &required_permission(operation), &context)
153 .await?;
154 let (resource, _) = self.get(&volume_ident).await?;
155 let volume: Volume = resource.try_into()?;
156 let location = volume.storage_location;
157 let storage_url = StorageLocationUrl::parse(&location)?;
158 vend_for_location(self, &storage_url, operation).await
159 }
160
161 #[tracing::instrument(skip(self, context))]
170 async fn generate_temporary_model_version_credentials(
171 &self,
172 request: GenerateTemporaryModelVersionCredentialsRequest,
173 context: RequestContext,
174 ) -> Result<TemporaryCredential> {
175 let operation = to_access_level(request.operation.to_i32());
176 let ident = ResourceIdent::model_version(ResourceName::new([
179 request.catalog_name.clone(),
180 request.schema_name.clone(),
181 request.model_name.clone(),
182 request.version.to_string(),
183 ]));
184 self.authorize_checked(&ident, &required_permission(operation), &context)
187 .await?;
188 let (resource, _) = self.get(&ident).await?;
189 let model_version: ModelVersion = resource.try_into()?;
190 let location = model_version.storage_location.ok_or_else(|| {
191 Error::invalid_argument("model version does not have a storage location")
192 })?;
193 let storage_url = StorageLocationUrl::parse(&location)?;
194 vend_for_location(self, &storage_url, operation).await
195 }
196
197 #[tracing::instrument(skip(self, context))]
198 async fn generate_temporary_table_credentials(
199 &self,
200 request: GenerateTemporaryTableCredentialsRequest,
201 context: RequestContext,
202 ) -> Result<TemporaryCredential> {
203 let operation = to_access_level(request.operation.to_i32());
204 let table_id = uuid::Uuid::parse_str(&request.table_id)
205 .map_err(|_| Error::invalid_argument("table_id is not a valid UUID"))?;
206 let table_ident = ResourceIdent::Table(ResourceRef::Uuid(table_id));
209 self.authorize_checked(&table_ident, &required_permission(operation), &context)
210 .await?;
211 let (resource, _) = self.get(&table_ident).await?;
212 let table: Table = resource.try_into()?;
213 let location = table
214 .storage_location
215 .ok_or_else(|| Error::invalid_argument("table does not have a storage location"))?;
216 let storage_url = StorageLocationUrl::parse(&location)?;
217 vend_for_location(self, &storage_url, operation).await
218 }
219}
220#[cfg(test)]
227mod tests {
228 use super::*;
229 use generate_temporary_path_credentials_request::Operation as PathOp;
230 use generate_temporary_table_credentials_request::Operation as TableOp;
231 use generate_temporary_volume_credentials_request::Operation as VolumeOp;
232
233 #[test]
234 fn vend_operation_volume_read_is_read() {
235 assert_eq!(
236 to_access_level(VolumeOp::ReadVolume as i32),
237 AccessLevel::Read
238 );
239 }
240
241 #[test]
242 fn vend_operation_volume_write_is_read_write() {
243 assert_eq!(
244 to_access_level(VolumeOp::WriteVolume as i32),
245 AccessLevel::ReadWrite
246 );
247 }
248
249 #[test]
250 fn vend_operation_unspecified_defaults_to_read() {
251 assert_eq!(
252 to_access_level(VolumeOp::Unspecified as i32),
253 AccessLevel::Read
254 );
255 assert_eq!(to_access_level(99), AccessLevel::Read);
257 }
258
259 #[test]
260 fn vend_operation_table_and_path_unchanged() {
261 assert_eq!(to_access_level(TableOp::Read as i32), AccessLevel::Read);
262 assert_eq!(
263 to_access_level(TableOp::ReadWrite as i32),
264 AccessLevel::ReadWrite
265 );
266 assert_eq!(
267 to_access_level(PathOp::PathCreateTable as i32),
268 AccessLevel::ReadWrite
269 );
270 }
271
272 #[cfg(feature = "memory")]
276 #[tokio::test]
277 async fn local_locations_vend_without_a_credential() {
278 use std::sync::Arc;
279
280 use unitycatalog_common::services::encryption::{EnvelopeEncryptor, LocalKeyProvider};
281
282 use crate::memory::InMemoryResourceStore;
283 use crate::policy::ConstantPolicy;
284 use crate::services::ServerHandler;
285
286 let encryptor =
287 EnvelopeEncryptor::local(LocalKeyProvider::single("test", vec![0x42; 32]).unwrap());
288 let store = Arc::new(InMemoryResourceStore::new(encryptor));
289 let policy: Arc<dyn Policy<RequestContext>> = Arc::new(ConstantPolicy::default());
290 let handler = ServerHandler::try_new_tokio(policy, store).unwrap();
291
292 let location = StorageLocationUrl::parse("file:///tmp/uc/tables/t1").unwrap();
293 let credential = vend_for_location(&handler, &location, AccessLevel::ReadWrite)
294 .await
295 .unwrap();
296 assert!(credential.credentials.is_none());
297 assert_eq!(credential.url, "file:///tmp/uc/tables/t1");
298 assert_eq!(credential.expiration_time, 0);
299
300 let location = StorageLocationUrl::parse("s3://bucket/tables/t1").unwrap();
302 assert!(matches!(
303 vend_for_location(&handler, &location, AccessLevel::Read).await,
304 Err(Error::NotFound)
305 ));
306 }
307}