Skip to main content

unitycatalog_server/services/
delta_backend.rs

1//! The mangrove [`DeltaBackend`] adapter.
2//!
3//! Implements [`unitycatalog_delta_api::DeltaBackend`] for
4//! [`ServerHandler<RequestContext>`], expressing the narrow backend port over the
5//! server's existing handler traits (`TableHandler`, `StagingTableHandler`,
6//! `TemporaryCredentialHandler`), the `Policy` authorization surface, the resource
7//! store, and the commit coordinator. All Delta *semantics* — the managed-table
8//! contract, the `updateTable` action dispatcher, `loadTable` construction — live
9//! in [`unitycatalog_delta_api`]; this adapter only maps types and errors.
10//!
11//! The impl is on `ServerHandler<RequestContext>` directly (not a wrapper) so the
12//! router's `RequestContext: FromRequestParts<ServerHandler>` extraction is
13//! preserved, exactly as the previous blanket `impl DeltaApiHandler for T` was.
14
15use async_trait::async_trait;
16use buffa::Enumeration;
17
18use unitycatalog_common::models::staging_tables::v1::{CreateStagingTableRequest, StagingTable};
19use unitycatalog_common::models::tables::v1::{
20    Column as UcColumn, CreateTableRequest, DataSourceFormat, DeleteTableRequest, GetTableRequest,
21    Table, TableType,
22};
23use unitycatalog_common::models::temporary_credentials::v1::{
24    GenerateTemporaryPathCredentialsRequest, GenerateTemporaryTableCredentialsRequest,
25    TemporaryCredential, generate_temporary_path_credentials_request::Operation as PathOp,
26    generate_temporary_table_credentials_request::Operation as TableOp,
27};
28use unitycatalog_common::models::{ResourceIdent, ResourceName, ResourceRef};
29use unitycatalog_common::store::Precondition;
30
31use olai_storage_credentials::wire::{VendContext, unity};
32use unitycatalog_delta_api::authz::DeltaAction;
33use unitycatalog_delta_api::backend::{
34    BackendResult, CreateTableSpec, CredentialAccess, DeltaBackend, DeltaCapabilities,
35    ResolvedTable, SchemaRef, StagingReservation, TableRef, UpdateTableSpec, VendedCredential,
36};
37use unitycatalog_delta_api::column::{
38    Column as CrateColumn, ColumnTypeName as CrateColumnTypeName,
39};
40use unitycatalog_delta_api::coordinator::{CommitCoordinator, ProvidesCommitCoordinator};
41use unitycatalog_delta_api::error::DeltaBackendError;
42use unitycatalog_delta_api::models::DeltaTableType;
43
44use crate::api::RequestContext;
45use crate::api::staging_tables::find_staging_table_by_location;
46use crate::api::tables::TableHandler;
47use crate::codegen::staging_tables::StagingTableHandler;
48use crate::codegen::temporary_credentials::TemporaryCredentialHandler;
49use crate::policy::{Permission, Policy, Principal};
50use crate::services::ServerHandler;
51use crate::services::location::StorageLocationUrl;
52use crate::services::object_store::validate_external_storage_location;
53use crate::store::{ResourceStore, ResourceStoreReader};
54use crate::{Error, Result};
55
56// ===================================================================
57// Error mapping
58// ===================================================================
59
60/// Map the server's internal [`Error`] onto the crate's [`DeltaBackendError`].
61///
62/// Reproduces the previous server-side `DeltaError::parts` dispatch exactly (see
63/// the deleted `crates/server/src/rest/routers/delta/models.rs`), so response
64/// status codes and error types are unchanged by the extraction.
65fn to_backend_err(e: Error) -> DeltaBackendError {
66    match &e {
67        Error::NotFound | Error::ResourceStore { .. } => DeltaBackendError::NotFound(e.to_string()),
68        // The wrapped common error carries its own semantics; dispatch on its
69        // machine-readable code so e.g. an "already exists" doesn't surface as 404.
70        Error::Common { source } => match source.error_code() {
71            "RESOURCE_ALREADY_EXISTS" => DeltaBackendError::AlreadyExists(e.to_string()),
72            "INVALID_PARAMETER_VALUE" => DeltaBackendError::InvalidArgument(e.to_string()),
73            "PERMISSION_DENIED" => DeltaBackendError::PermissionDenied(e.to_string()),
74            "COMMIT_VERSION_CONFLICT" => DeltaBackendError::CommitVersionConflict(e.to_string()),
75            // A store CAS mismatch (`Precondition::Version`) surfaces as a generic
76            // conflict; for the Delta write path that is an `assert-etag` failure.
77            "RESOURCE_CONFLICT" => DeltaBackendError::UpdateRequirementConflict(e.to_string()),
78            "RESOURCE_EXHAUSTED" => DeltaBackendError::ResourceExhausted(e.to_string()),
79            _ => DeltaBackendError::NotFoundGeneric(e.to_string()),
80        },
81        Error::NotAllowed => DeltaBackendError::PermissionDenied(e.to_string()),
82        Error::Unauthenticated => DeltaBackendError::Unauthenticated(e.to_string()),
83        Error::AlreadyExists => DeltaBackendError::AlreadyExists(e.to_string()),
84        Error::CommitVersionConflict(m) => DeltaBackendError::CommitVersionConflict(m.clone()),
85        Error::UpdateRequirementConflict(m) => {
86            DeltaBackendError::UpdateRequirementConflict(m.clone())
87        }
88        Error::ResourceExhausted(m) => DeltaBackendError::ResourceExhausted(m.clone()),
89        Error::InvalidArgument(m) => DeltaBackendError::InvalidArgument(m.clone()),
90        Error::InvalidIdentifier(_) | Error::MissingRecipient => {
91            DeltaBackendError::InvalidArgument(e.to_string())
92        }
93        Error::NotImplemented(w) => DeltaBackendError::NotImplemented(w),
94        _ => DeltaBackendError::Internal(e.to_string()),
95    }
96}
97
98/// Parse the version out of an etag string produced by
99/// [`unitycatalog_delta_api::backend::etag_of`] (`etag-<version>`).
100///
101/// Returns `None` for a malformed etag, which the caller treats as an
102/// `assert-etag` conflict.
103fn parse_etag_version(etag: &str) -> Option<u64> {
104    etag.strip_prefix("etag-").and_then(|v| v.parse().ok())
105}
106
107// ===================================================================
108// Column mapping
109// ===================================================================
110
111/// Map a common UC column onto the crate's portable [`Column`](CrateColumn).
112fn uc_column_to_crate(c: UcColumn) -> CrateColumn {
113    CrateColumn {
114        name: c.name,
115        type_text: c.type_text,
116        type_json: c.type_json,
117        position: c.position,
118        type_name: CrateColumnTypeName::from(c.type_name.to_i32()),
119        comment: c.comment,
120        nullable: c.nullable,
121        partition_index: c.partition_index,
122    }
123}
124
125/// Map a crate column back onto the common UC column.
126///
127/// The Delta contract only produces the fields the crate `Column` carries; the
128/// remaining generated fields (`type_precision`, `type_scale`,
129/// `type_interval_type`, `column_id`) are left at their defaults, mirroring the
130/// columns the old contract-derived `CreateTableRequest` persisted.
131fn crate_column_to_uc(c: CrateColumn) -> UcColumn {
132    UcColumn {
133        name: c.name,
134        type_text: c.type_text,
135        type_json: c.type_json,
136        position: c.position,
137        type_name: (c.type_name as i32).into(),
138        comment: c.comment,
139        nullable: c.nullable,
140        partition_index: c.partition_index,
141        ..Default::default()
142    }
143}
144
145// ===================================================================
146// Table / staging / credential mapping
147// ===================================================================
148
149fn to_uc_table_type(t: DeltaTableType) -> TableType {
150    match t {
151        DeltaTableType::Managed => TableType::Managed,
152        DeltaTableType::External => TableType::External,
153    }
154}
155
156/// Map a stored data source format onto the crate's wire format enum. Formats
157/// the wire enum does not carry (`Unspecified`, unknown) map to `None`.
158fn to_delta_format(f: i32) -> Option<unitycatalog_delta_api::models::DeltaDataSourceFormat> {
159    use unitycatalog_delta_api::models::DeltaDataSourceFormat as F;
160    match DataSourceFormat::from_i32(f)? {
161        // UniForm tables are Delta tables that also publish Iceberg/Hudi metadata.
162        DataSourceFormat::DELTA
163        | DataSourceFormat::DELTA_UNIFORM_ICEBERG
164        | DataSourceFormat::DELTA_UNIFORM_HUDI => Some(F::Delta),
165        DataSourceFormat::ICEBERG => Some(F::Iceberg),
166        DataSourceFormat::HUDI => Some(F::Hudi),
167        DataSourceFormat::PARQUET => Some(F::Parquet),
168        DataSourceFormat::CSV => Some(F::Csv),
169        DataSourceFormat::JSON => Some(F::Json),
170        DataSourceFormat::ORC => Some(F::Orc),
171        DataSourceFormat::AVRO => Some(F::Avro),
172        DataSourceFormat::TEXT => Some(F::Text),
173        DataSourceFormat::UNITY_CATALOG => Some(F::UnityCatalog),
174        DataSourceFormat::DELTASHARING => Some(F::Deltasharing),
175        DataSourceFormat::HIVE | DataSourceFormat::DATA_SOURCE_FORMAT_UNSPECIFIED => None,
176    }
177}
178
179/// Map a stored [`Table`] into the crate's portable [`ResolvedTable`].
180///
181/// `version` is the store's per-object version, which drives the etag and the
182/// `assert-etag` compare-and-swap; callers read it via
183/// [`ResourceStoreReader::get_versioned`].
184///
185/// View-like table types (views, metric views, …) map to `table_type: None`,
186/// which the shared handler rejects with the spec's "not a Delta table" 400.
187fn table_to_resolved(table: Table, version: u64) -> ResolvedTable {
188    let table_type = match table.table_type.as_known() {
189        Some(TableType::MANAGED) => Some(DeltaTableType::Managed),
190        Some(TableType::EXTERNAL) => Some(DeltaTableType::External),
191        _ => None,
192    };
193    ResolvedTable {
194        table_id: table.table_id,
195        location: table.storage_location.unwrap_or_default(),
196        table_type,
197        data_source_format: to_delta_format(table.data_source_format.to_i32()),
198        columns: table.columns.into_iter().map(uc_column_to_crate).collect(),
199        properties: table.properties.into_iter().collect(),
200        created_at_ms: table.created_at,
201        updated_at_ms: table.updated_at,
202        version,
203    }
204}
205
206fn staging_to_reservation(st: StagingTable) -> StagingReservation {
207    StagingReservation {
208        table_id: st.id,
209        name: st.name,
210        location: st.staging_location,
211        created_by: st.created_by,
212        stage_committed: st.stage_committed,
213    }
214}
215
216/// Read a UC REST [`TemporaryCredential`] vended at `access` as the crate's
217/// [`VendedCredential`].
218fn to_vended_credential(
219    creds: TemporaryCredential,
220    access: CredentialAccess,
221) -> BackendResult<VendedCredential> {
222    let url = creds.url.clone();
223    let location = StorageLocationUrl::parse(&url).map_err(to_backend_err)?;
224    let context = VendContext::new(location.scope(), access);
225    let credential = unity::TemporaryCredential::from(creds)
226        .to_vended(&context)
227        .map_err(|e| to_backend_err(unitycatalog_common::Error::from(e).into()))?;
228    Ok(VendedCredential { url, credential })
229}
230
231fn to_table_op(access: CredentialAccess) -> i32 {
232    match access {
233        CredentialAccess::Read => TableOp::Read as i32,
234        CredentialAccess::ReadWrite => TableOp::ReadWrite as i32,
235    }
236}
237
238fn to_path_op(access: CredentialAccess) -> i32 {
239    match access {
240        CredentialAccess::Read => PathOp::PathRead as i32,
241        CredentialAccess::ReadWrite => PathOp::PathReadWrite as i32,
242    }
243}
244
245// ===================================================================
246// The adapter
247// ===================================================================
248
249impl ServerHandler<RequestContext> {
250    /// Resolve a staging reservation by uuid via the resource store.
251    async fn get_staging_by_id(&self, table_id: &str) -> Result<StagingTable> {
252        let uuid = uuid::Uuid::parse_str(table_id)
253            .map_err(|_| Error::invalid_argument("table_id is not a valid UUID"))?;
254        let ident = ResourceIdent::StagingTable(ResourceRef::Uuid(uuid));
255        let staging: StagingTable = self.get(&ident).await?.0.try_into()?;
256        Ok(staging)
257    }
258}
259
260#[async_trait]
261impl DeltaBackend<RequestContext> for ServerHandler<RequestContext> {
262    fn capabilities(&self) -> DeltaCapabilities {
263        // The store provides a native, versioned rename, so advertise renameTable.
264        DeltaCapabilities { rename: true }
265    }
266
267    async fn catalog_exists(&self, catalog: &str, _cx: &RequestContext) -> BackendResult<()> {
268        let ident = ResourceIdent::catalog(ResourceName::new([catalog]));
269        self.get(&ident)
270            .await
271            .map_err(Error::from)
272            .map_err(to_backend_err)?;
273        Ok(())
274    }
275
276    async fn resolve_table(
277        &self,
278        table: &TableRef,
279        cx: &RequestContext,
280    ) -> BackendResult<ResolvedTable> {
281        let t = TableHandler::get_table(
282            self,
283            GetTableRequest {
284                full_name: table.full_name(),
285                include_delta_metadata: None,
286                include_browse: None,
287                include_manifest_capabilities: None,
288                ..Default::default()
289            },
290            cx.clone(),
291        )
292        .await
293        .map_err(to_backend_err)?;
294        // Read the store version behind the table's id so the etag reflects the
295        // current row (the `assert-etag` CAS keys on this same version).
296        let version = match t.table_id.as_deref() {
297            Some(id) => match uuid::Uuid::parse_str(id) {
298                Ok(uuid) => {
299                    let ident = ResourceIdent::Table(ResourceRef::Uuid(uuid));
300                    self.get_versioned(&ident)
301                        .await
302                        .map(|(_, _, v)| v)
303                        .map_err(Error::from)
304                        .map_err(to_backend_err)?
305                }
306                Err(_) => 0,
307            },
308            None => 0,
309        };
310        Ok(table_to_resolved(t, version))
311    }
312
313    async fn authorize(&self, action: DeltaAction<'_>, cx: &RequestContext) -> BackendResult<()> {
314        match action {
315            DeltaAction::CreateTable {
316                at,
317                name,
318                table_type,
319            } => {
320                // Authorize CREATE on the target table via the same SecuredAction
321                // the UC-REST createTable uses.
322                let create_action = CreateTableRequest {
323                    name: name.to_string(),
324                    catalog_name: at.catalog.clone(),
325                    schema_name: at.schema.clone(),
326                    table_type: to_uc_table_type(table_type).into(),
327                    data_source_format: DataSourceFormat::Delta.into(),
328                    ..Default::default()
329                };
330                self.check_required(&create_action, cx)
331                    .await
332                    .map_err(to_backend_err)
333            }
334            DeltaAction::WriteTable { table_id, .. } => {
335                let uuid = uuid::Uuid::parse_str(table_id).map_err(|_| {
336                    DeltaBackendError::InvalidArgument("table id is not a valid UUID".into())
337                })?;
338                let ident = ResourceIdent::Table(ResourceRef::Uuid(uuid));
339                self.authorize_checked(&ident, &Permission::Write, cx)
340                    .await
341                    .map_err(to_backend_err)
342            }
343            DeltaAction::AdoptStaging { reservation } => {
344                // The creator-match, in mangrove's identity terms: the caller's
345                // principal name must equal the reservation's `created_by`
346                // (anonymous reservations, `created_by == None`, are adoptable by
347                // any anonymous caller — the pre-crate behavior).
348                let principal = match cx.recipient() {
349                    Principal::User(name) => Some(name.clone()),
350                    Principal::Anonymous => None,
351                };
352                if reservation.created_by.as_deref() != principal.as_deref() {
353                    return Err(DeltaBackendError::PermissionDenied(
354                        "caller is not the creator of the staging table".to_string(),
355                    ));
356                }
357                Ok(())
358            }
359            // Read / delete / rename / credential vending / staging creation are
360            // authorized by the downstream handler traits these operations
361            // delegate to (`TableHandler`, `StagingTableHandler`,
362            // `TemporaryCredentialHandler`), each of which runs `check_required`
363            // itself. Authorizing again here would double-check; matching the
364            // pre-crate behavior, the handler-level hook is a no-op for them.
365            DeltaAction::ReadTable { .. }
366            | DeltaAction::DeleteTable { .. }
367            | DeltaAction::RenameTable { .. }
368            | DeltaAction::VendTableCredential { .. }
369            | DeltaAction::VendPathCredential { .. }
370            | DeltaAction::CreateStaging { .. } => Ok(()),
371            // `DeltaAction` is `#[non_exhaustive]`. Fail closed on an action this
372            // adapter has not been taught: a newly added operation must not slip
373            // through unauthorized until its arm is written.
374            _ => Err(DeltaBackendError::PermissionDenied(
375                "unrecognized Delta action".to_string(),
376            )),
377        }
378    }
379
380    async fn validate_external_location(
381        &self,
382        location: &str,
383        _cx: &RequestContext,
384    ) -> BackendResult<()> {
385        let parsed = StorageLocationUrl::parse(location).map_err(to_backend_err)?;
386        validate_external_storage_location(self, &parsed)
387            .await
388            .map_err(to_backend_err)
389    }
390
391    async fn create_table_row(
392        &self,
393        spec: CreateTableSpec,
394        _cx: &RequestContext,
395    ) -> BackendResult<ResolvedTable> {
396        let adopt_ident = spec.adopt_staging.as_ref().map(|reservation| {
397            ResourceIdent::staging_table(ResourceName::new([reservation.name.as_str()]))
398        });
399        let table = Table {
400            name: spec.name,
401            catalog_name: spec.at.catalog,
402            schema_name: spec.at.schema,
403            table_type: to_uc_table_type(spec.table_type).into(),
404            data_source_format: DataSourceFormat::Delta.into(),
405            columns: spec.columns.into_iter().map(crate_column_to_uc).collect(),
406            storage_location: Some(spec.location),
407            comment: spec.comment,
408            properties: spec.properties.into_iter().collect(),
409            table_id: spec.table_id,
410            ..Default::default()
411        };
412        // Persist directly via the store (the request is already validated); the
413        // UC-REST create path re-reads the snapshot for the managed branch, which
414        // the Delta API does not want.
415        //
416        // Managed adoption is a *relabel* (StagingTable → Table at the same id):
417        // both cannot exist at that id, so `replace_atomically` consumes the
418        // reservation and creates the table in one transaction — closing the
419        // orphaned-reservation window. EXTERNAL tables have no reservation and are
420        // a plain create. Take the version the store assigned the new row so the
421        // returned etag matches what a later `loadTable` reads, without assuming a
422        // fixed initial version.
423        let (resource, _, version) = match adopt_ident {
424            Some(ident) => {
425                self.replace_atomically_versioned(&ident, table.into())
426                    .await
427            }
428            None => self.create_versioned(table.into()).await,
429        }
430        .map_err(Error::from)
431        .map_err(to_backend_err)?;
432        let stored: Table = resource
433            .try_into()
434            .map_err(Error::from)
435            .map_err(to_backend_err)?;
436        Ok(table_to_resolved(stored, version))
437    }
438
439    async fn update_table_row(
440        &self,
441        spec: UpdateTableSpec,
442        _cx: &RequestContext,
443    ) -> BackendResult<ResolvedTable> {
444        let uuid = uuid::Uuid::parse_str(&spec.table_id).map_err(|_| {
445            DeltaBackendError::InvalidArgument("table id is not a valid UUID".into())
446        })?;
447        let ident = ResourceIdent::Table(ResourceRef::Uuid(uuid));
448        // Read the row together with its version, so an `assert-etag` translates
449        // into a real compare-and-swap at write time.
450        let (resource, _, version) = self
451            .get_versioned(&ident)
452            .await
453            .map_err(Error::from)
454            .map_err(to_backend_err)?;
455        let mut table: Table = resource
456            .try_into()
457            .map_err(Error::from)
458            .map_err(to_backend_err)?;
459        // assert-etag compare-and-swap: parse the expected etag back to the version
460        // it encodes and pass it as a `Precondition::Version` to the update below,
461        // so the store rejects the write atomically if the row advanced between
462        // this read and the write (closing the read-modify-write race). A malformed
463        // or non-matching etag fails fast here.
464        let precondition = match &spec.expected_etag {
465            Some(expected) => {
466                let expected_version = parse_etag_version(expected).ok_or_else(|| {
467                    DeltaBackendError::UpdateRequirementConflict(
468                        "assert-etag failed: table has been modified".into(),
469                    )
470                })?;
471                if expected_version != version {
472                    return Err(DeltaBackendError::UpdateRequirementConflict(
473                        "assert-etag failed: table has been modified".into(),
474                    ));
475                }
476                Precondition::Version(expected_version)
477            }
478            None => Precondition::Any,
479        };
480        table.columns = spec.columns.into_iter().map(crate_column_to_uc).collect();
481        table.properties = spec.properties.into_iter().collect();
482        // `None` means "leave the stored comment unchanged" (see `UpdateTableSpec`):
483        // the handler only sets it when a set-table-comment action is present.
484        if let Some(comment) = spec.comment {
485            table.comment = Some(comment);
486        }
487        let (updated_resource, _, new_version) = self
488            .update_checked(&ident, table.into(), precondition)
489            .await
490            .map_err(Error::from)
491            .map_err(to_backend_err)?;
492        let updated: Table = updated_resource
493            .try_into()
494            .map_err(Error::from)
495            .map_err(to_backend_err)?;
496        Ok(table_to_resolved(updated, new_version))
497    }
498
499    async fn delete_table(&self, table: &TableRef, cx: &RequestContext) -> BackendResult<()> {
500        TableHandler::delete_table(
501            self,
502            DeleteTableRequest {
503                full_name: table.full_name(),
504                ..Default::default()
505            },
506            cx.clone(),
507        )
508        .await
509        .map_err(to_backend_err)
510    }
511
512    async fn rename_table(
513        &self,
514        from: &TableRef,
515        to_name: &str,
516        _cx: &RequestContext,
517    ) -> BackendResult<()> {
518        // A table's name is 3-part `[catalog, schema, table]`; a rename changes only
519        // the leaf, keeping catalog+schema. `ResourceStore::rename` re-keys the
520        // object and rewrites the leaf name inside its properties in one transaction,
521        // preserving the id, associations, and any secrets.
522        let from_ident = ResourceIdent::table(ResourceName::new([
523            from.catalog.as_str(),
524            from.schema.as_str(),
525            from.table.as_str(),
526        ]));
527        let new_name = ResourceName::new([from.catalog.as_str(), from.schema.as_str(), to_name]);
528        self.rename(&from_ident, &new_name, Precondition::Any)
529            .await
530            .map_err(Error::from)
531            .map_err(to_backend_err)?;
532        Ok(())
533    }
534
535    async fn allocate_staging(
536        &self,
537        at: &SchemaRef,
538        name: &str,
539        cx: &RequestContext,
540    ) -> BackendResult<StagingReservation> {
541        let staging = StagingTableHandler::create_staging_table(
542            self,
543            CreateStagingTableRequest {
544                name: name.to_string(),
545                catalog_name: at.catalog.clone(),
546                schema_name: at.schema.clone(),
547                ..Default::default()
548            },
549            cx.clone(),
550        )
551        .await
552        .map_err(to_backend_err)?;
553        Ok(staging_to_reservation(staging))
554    }
555
556    async fn resolve_staging_by_location(
557        &self,
558        location: &str,
559        _cx: &RequestContext,
560    ) -> BackendResult<StagingReservation> {
561        let staging = find_staging_table_by_location(self, location)
562            .await
563            .map_err(to_backend_err)?;
564        Ok(staging_to_reservation(staging))
565    }
566
567    async fn resolve_staging_by_id(
568        &self,
569        table_id: &str,
570        _cx: &RequestContext,
571    ) -> BackendResult<StagingReservation> {
572        let staging = self
573            .get_staging_by_id(table_id)
574            .await
575            .map_err(to_backend_err)?;
576        Ok(staging_to_reservation(staging))
577    }
578
579    async fn vend_table_credential(
580        &self,
581        table_id: &str,
582        access: CredentialAccess,
583        cx: &RequestContext,
584    ) -> BackendResult<VendedCredential> {
585        let creds = self
586            .generate_temporary_table_credentials(
587                GenerateTemporaryTableCredentialsRequest {
588                    table_id: table_id.to_string(),
589                    operation: to_table_op(access).into(),
590                    ..Default::default()
591                },
592                cx.clone(),
593            )
594            .await
595            .map_err(to_backend_err)?;
596        to_vended_credential(creds, access)
597    }
598
599    async fn vend_path_credential(
600        &self,
601        location: &str,
602        access: CredentialAccess,
603        cx: &RequestContext,
604    ) -> BackendResult<VendedCredential> {
605        let creds = self
606            .generate_temporary_path_credentials(
607                GenerateTemporaryPathCredentialsRequest {
608                    url: location.to_string(),
609                    operation: to_path_op(access).into(),
610                    dry_run: Some(false),
611                    ..Default::default()
612                },
613                cx.clone(),
614            )
615            .await
616            .map_err(to_backend_err)?;
617        to_vended_credential(creds, access)
618    }
619
620    fn commit_coordinator(&self) -> &dyn CommitCoordinator {
621        ProvidesCommitCoordinator::commit_coordinator(self)
622    }
623}