diff --git a/rustfs/src/admin/handlers/table_catalog/credentials.rs b/rustfs/src/admin/handlers/table_catalog/credentials.rs index 62efde2e5..0e89999dc 100644 --- a/rustfs/src/admin/handlers/table_catalog/credentials.rs +++ b/rustfs/src/admin/handlers/table_catalog/credentials.rs @@ -25,10 +25,19 @@ impl Operation for RestLoadCredentialsHandler { let resource = TableCatalogResource::table(&warehouse, &namespace, &table); let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableCredentialsAction).await?; ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?; - let store = table_catalog_store_from_extensions(&req.extensions)?; + let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?; + let store = table_catalog_store_from_backend(metadata_backend.clone())?; let issuer = IamTableCredentialIssuer::from_request(&req)?; - let response = - load_credentials_response(&store, &warehouse, &namespace, &table, &issuer, Some(&principal.credentials)).await?; + let response = load_credentials_response( + &store, + &metadata_backend, + &warehouse, + &namespace, + &table, + &issuer, + Some(&principal.credentials), + ) + .await?; build_sensitive_json_response(StatusCode::OK, &response) } } diff --git a/rustfs/src/admin/handlers/table_catalog/mod.rs b/rustfs/src/admin/handlers/table_catalog/mod.rs index 73825122c..976d70532 100644 --- a/rustfs/src/admin/handlers/table_catalog/mod.rs +++ b/rustfs/src/admin/handlers/table_catalog/mod.rs @@ -2632,7 +2632,10 @@ fn table_credential_ttl_seconds() -> i64 { .unwrap_or(DEFAULT_TABLE_CATALOG_CREDENTIAL_TTL_SECONDS) } -fn table_credential_scope(entry: &crate::table_catalog::TableEntry) -> S3Result { +fn table_credential_scope( + entry: &crate::table_catalog::TableEntry, + metadata: &serde_json::Value, +) -> S3Result { let location = entry .warehouse_location .strip_prefix("s3://") @@ -2645,17 +2648,13 @@ fn table_credential_scope(entry: &crate::table_catalog::TableEntry) -> S3Result< } let object_prefix = normalize_table_credential_object_prefix(object_prefix)?; validate_persisted_table_metadata_location(entry, &entry.metadata_location)?; + // Rename preserves the metadata pointer; ownership follows its UUID and warehouse, not its old path identifier. + validate_persisted_table_metadata(entry, metadata, true)?; let metadata_object = crate::table_catalog::table_catalog_object_key_from_location(&entry.table_bucket, &entry.metadata_location) .ok_or_else(|| persisted_metadata_error("table"))?; - let namespace = crate::table_catalog::Namespace::parse(&entry.namespace).map_err(|_| persisted_metadata_error("table"))?; - let table = - crate::table_catalog::IdentifierSegment::parse(entry.table.clone()).map_err(|_| persisted_metadata_error("table"))?; - if crate::table_catalog::is_reserved_table_object_key(&metadata_object) - && !crate::table_catalog::is_valid_table_metadata_location(&namespace, &table, &metadata_object) - { - return Err(persisted_metadata_error("table")); - } + crate::table_catalog::Namespace::parse(&entry.namespace).map_err(|_| persisted_metadata_error("table"))?; + crate::table_catalog::IdentifierSegment::parse(entry.table.clone()).map_err(|_| persisted_metadata_error("table"))?; let metadata_scope_prefix = table_metadata_location_for_client(&entry.table_bucket, &entry.metadata_location); Ok(TableCredentialScope { warehouse_scope_prefix: format!("s3://{bucket}/{object_prefix}"), @@ -2920,13 +2919,14 @@ fn add_table_credential_scope_config(config: &mut BTreeMap, scop async fn load_credentials_response_from_entry( entry: &crate::table_catalog::TableEntry, + metadata: &serde_json::Value, issuer: &dyn TableCredentialIssuer, principal: Option<&rustfs_credentials::Credentials>, ) -> S3Result { if !issuer.enabled() { return Ok(client_provided_credentials_response(CREDENTIAL_VENDING_DISABLED_REASON)); } - let scope = table_credential_scope(entry)?; + let scope = table_credential_scope(entry, metadata)?; let request = TableCredentialIssueRequest { entry, principal, @@ -2976,7 +2976,7 @@ async fn enrich_load_table_response_with_credentials( issuer: &dyn TableCredentialIssuer, principal: Option<&rustfs_credentials::Credentials>, ) -> S3Result { - let credential_response = load_credentials_response_from_entry(entry, issuer, principal).await?; + let credential_response = load_credentials_response_from_entry(entry, &response.metadata, issuer, principal).await?; Ok(apply_credentials_to_load_table_response(response, credential_response)) } @@ -6176,6 +6176,7 @@ where async fn load_credentials_response( store: &S, + metadata_backend: &impl crate::table_catalog::TableCatalogObjectBackend, bucket: &str, namespace: &crate::table_catalog::Namespace, table: &str, @@ -6192,7 +6193,12 @@ where else { return Err(iceberg_rest_error(ICEBERG_ERROR_NO_SUCH_TABLE, StatusCode::NOT_FOUND, "table not found")); }; - load_credentials_response_from_entry(&entry, issuer, principal).await + let metadata = if issuer.enabled() { + read_persisted_table_metadata_for_entry(metadata_backend, &entry, &entry.metadata_location, true).await? + } else { + serde_json::Value::Null + }; + load_credentials_response_from_entry(&entry, &metadata, issuer, principal).await } async fn get_table_metadata_location_response( diff --git a/rustfs/src/admin/handlers/table_catalog/tests.rs b/rustfs/src/admin/handlers/table_catalog/tests.rs index 2f806d2c6..7ce53bf22 100644 --- a/rustfs/src/admin/handlers/table_catalog/tests.rs +++ b/rustfs/src/admin/handlers/table_catalog/tests.rs @@ -10739,7 +10739,7 @@ fn table_entry_for_credentials() -> crate::table_catalog::TableEntry { #[tokio::test] async fn disabled_table_credential_issuer_keeps_credentials_empty() { let issuer = DisabledTableCredentialIssuer; - let response = load_credentials_response_from_entry(&table_entry_for_credentials(), &issuer, None) + let response = load_credentials_response_from_entry(&table_entry_for_credentials(), &serde_json::Value::Null, &issuer, None) .await .expect("disabled issuer should build an empty response"); @@ -10764,7 +10764,7 @@ async fn disabled_table_credential_issuer_skips_scope_validation() { let mut entry = table_entry_for_credentials(); entry.warehouse_location = "s3://warehouse/".to_string(); - let response = load_credentials_response_from_entry(&entry, &issuer, None) + let response = load_credentials_response_from_entry(&entry, &serde_json::Value::Null, &issuer, None) .await .expect("disabled issuer should not validate credential scopes"); @@ -10792,9 +10792,10 @@ impl TableCredentialIssuer for UnavailableTableCredentialIssuer { #[tokio::test] async fn unavailable_table_credential_issuer_reports_fallback_scope() { let issuer = UnavailableTableCredentialIssuer; - let response = load_credentials_response_from_entry(&table_entry_for_credentials(), &issuer, None) - .await - .expect("unavailable issuer should build a fallback response"); + let response = + load_credentials_response_from_entry(&table_entry_for_credentials(), &table_metadata_for_credentials(), &issuer, None) + .await + .expect("unavailable issuer should build a fallback response"); assert!(response.storage_credentials.is_empty()); assert_eq!( @@ -10821,6 +10822,294 @@ async fn unavailable_table_credential_issuer_reports_fallback_scope() { struct TestTableCredentialIssuer; +fn table_metadata_for_credentials() -> serde_json::Value { + test_table_metadata_json("table-uuid", "s3://warehouse/tables/table-id") +} + +#[derive(Default)] +struct RecordingTableCredentialIssuer { + calls: std::sync::atomic::AtomicUsize, +} + +#[async_trait::async_trait] +impl TableCredentialIssuer for RecordingTableCredentialIssuer { + async fn issue_table_credentials( + &self, + request: TableCredentialIssueRequest<'_>, + ) -> S3Result> { + self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + assert_eq!( + request.scope_prefix, + format!("{}/", request.entry.warehouse_location.trim_end_matches('/')) + ); + assert_eq!( + request.metadata_object, + crate::table_catalog::table_catalog_object_key_from_location( + &request.entry.table_bucket, + &request.entry.metadata_location, + ) + .expect("metadata must belong to the table bucket") + ); + Ok(Some(IssuedTableCredentials { + access_key_id: "temporary-access-key".to_string(), + secret_access_key: "temporary-secret-key".to_string(), + session_token: "temporary-session-token".to_string(), + expiration: OffsetDateTime::from_unix_timestamp(1_800_000_000).expect("test timestamp should be valid"), + })) + } +} + +#[tokio::test] +async fn renamed_table_vending_preserves_identity_and_isolates_recreated_metadata_across_backings() { + for mode in [ + crate::table_catalog::TableCatalogBackingMode::ObjectBacked, + crate::table_catalog::TableCatalogBackingMode::DurableStrong, + ] { + for destination in ["analytics", "curated"] { + let backend = TestTableCatalogObjectBackend::content_addressed(); + let store = crate::table_catalog::ConfiguredTableCatalogStore::new_for_test(backend.clone(), mode); + let source = crate::table_catalog::Namespace::parse("analytics").expect("source namespace"); + let destination_namespace = crate::table_catalog::Namespace::parse(destination).expect("destination namespace"); + create_standard_events_table(&store, &backend, &source).await; + if destination != "analytics" { + create_namespace_response( + &store, + "warehouse", + CreateNamespaceRequest { + namespace: vec![destination.to_string()], + properties: BTreeMap::new(), + }, + true, + ) + .await + .expect("destination namespace should be created"); + } + let before = store + .load_table("warehouse", "analytics", "events") + .await + .expect("lookup") + .expect("table"); + store + .rename_table("warehouse", "analytics", "events", destination, "renamed_events") + .await + .expect("rename should succeed"); + let renamed = store + .load_table("warehouse", destination, "renamed_events") + .await + .expect("lookup") + .expect("table"); + assert_eq!(renamed.table_id, before.table_id); + assert_eq!(renamed.table_uuid, before.table_uuid); + assert_eq!(renamed.warehouse_location, before.warehouse_location); + assert_eq!(renamed.metadata_location, before.metadata_location); + assert_eq!(renamed.version_token, before.version_token); + assert_eq!(renamed.generation, before.generation); + let issuer = RecordingTableCredentialIssuer::default(); + let delegated = load_table_response_with_credentials( + &store, + &backend, + "warehouse", + &destination_namespace, + "renamed_events", + &issuer, + None, + ) + .await + .expect("renamed table should vend credentials for its retained metadata"); + let credentials = + load_credentials_response(&store, &backend, "warehouse", &destination_namespace, "renamed_events", &issuer, None) + .await + .expect("credentials endpoint should accept the retained metadata"); + assert_eq!( + serde_json::to_value(&credentials.storage_credentials).expect("credential JSON"), + serde_json::to_value(&delegated.storage_credentials).expect("delegated JSON") + ); + assert_eq!(delegated.metadata["table-uuid"], before.table_uuid); + assert_eq!(delegated.storage_credentials.len(), 2); + assert_eq!(delegated.storage_credentials[0].prefix, format!("{}/", before.warehouse_location)); + assert_eq!( + delegated.storage_credentials[1].prefix, + table_metadata_location_for_client("warehouse", &before.metadata_location) + ); + let missing = load_table_response_with_credentials(&store, &backend, "warehouse", &source, "events", &issuer, None) + .await + .expect_err("old identifier must not remain loadable"); + assert_eq!(missing.status_code(), Some(StatusCode::NOT_FOUND)); + let request: CreateTableRequest = serde_json::from_value(serde_json::json!({ + "name": "events", "schema": {"type": "struct", "schema-id": 0, "fields": []} + })) + .expect("recreate request"); + create_table_response( + &store, + &TableCommitObjectBackend::trusted(backend.clone()), + "warehouse", + &source, + request, + true, + ) + .await + .expect("old identifier should be reusable"); + let recreated = store + .load_table("warehouse", "analytics", "events") + .await + .expect("lookup") + .expect("table"); + assert_ne!(recreated.table_id, renamed.table_id); + assert_ne!(recreated.table_uuid, renamed.table_uuid); + assert_ne!(recreated.metadata_location, renamed.metadata_location); + assert_ne!(recreated.warehouse_location, renamed.warehouse_location); + let metadata_object = + table_metadata_location_for_catalog("warehouse", &renamed.metadata_location).expect("metadata key"); + let prefix = renamed + .warehouse_location + .strip_prefix("s3://warehouse/") + .expect("warehouse prefix"); + let policy = + table_credential_session_policy(&renamed, &format!("{prefix}/"), &metadata_object).expect("session policy"); + let conditions = HashMap::new(); + let claims = HashMap::new(); + for (object, allowed) in [ + (metadata_object.clone(), true), + (format!("{metadata_object}.neighbor"), false), + ( + table_metadata_location_for_catalog("warehouse", &recreated.metadata_location).expect("recreated key"), + false, + ), + ] { + assert_eq!( + policy + .is_allowed(&rustfs_policy::policy::Args { + account: "temporary-access-key", + groups: &None, + action: Action::S3Action(rustfs_policy::policy::action::S3Action::GetObjectAction), + bucket: "warehouse", + conditions: &conditions, + is_owner: false, + object: &object, + claims: &claims, + deny_only: false, + }) + .await, + allowed, + "{mode:?} {destination} {object}" + ); + } + let reloaded = crate::table_catalog::ConfiguredTableCatalogStore::new_for_test(backend.clone(), mode); + for (namespace, name, expected) in [ + (&destination_namespace, "renamed_events", &renamed), + (&source, "events", &recreated), + ] { + let response = + load_table_response_with_credentials(&reloaded, &backend, "warehouse", namespace, name, &issuer, None) + .await + .expect("fresh store should vend for the correct persisted identity"); + assert_eq!(response.metadata["table-uuid"], expected.table_uuid); + assert_eq!(response.storage_credentials[0].prefix, format!("{}/", expected.warehouse_location)); + assert_eq!( + response.storage_credentials[1].prefix, + table_metadata_location_for_client("warehouse", &expected.metadata_location) + ); + let credentials = load_credentials_response(&reloaded, &backend, "warehouse", namespace, name, &issuer, None) + .await + .expect("fresh store credentials endpoint should bind the persisted identity"); + assert_eq!( + serde_json::to_value(&credentials.storage_credentials).expect("credential JSON"), + serde_json::to_value(&response.storage_credentials).expect("delegated JSON") + ); + } + } + } +} + +#[tokio::test] +async fn credential_vending_rejects_corrupt_or_foreign_metadata_before_issuance_across_backings() { + for mode in [ + crate::table_catalog::TableCatalogBackingMode::ObjectBacked, + crate::table_catalog::TableCatalogBackingMode::DurableStrong, + ] { + let backend = TestTableCatalogObjectBackend::content_addressed(); + let store = crate::table_catalog::ConfiguredTableCatalogStore::new_for_test(backend.clone(), mode); + let namespace = crate::table_catalog::Namespace::parse("analytics").expect("namespace"); + let created = create_standard_events_table(&store, &backend, &namespace).await; + let before = store + .load_table("warehouse", "analytics", "events") + .await + .expect("lookup") + .expect("table"); + let issuer = RecordingTableCredentialIssuer::default(); + let mut foreign_uuid = created.metadata.clone(); + foreign_uuid["table-uuid"] = serde_json::Value::from("foreign-table-uuid"); + let mut foreign_warehouse = created.metadata.clone(); + foreign_warehouse["location"] = serde_json::Value::from("s3://warehouse/tables/another-table"); + let mut cross_bucket = created.metadata.clone(); + cross_bucket["location"] = serde_json::Value::from("s3://other/tables/another-table"); + let mut missing_uuid = created.metadata.clone(); + missing_uuid.as_object_mut().expect("metadata object").remove("table-uuid"); + let mut unsupported_format = created.metadata.clone(); + unsupported_format["format-version"] = serde_json::Value::from(99); + for metadata in [ + foreign_uuid, + foreign_warehouse, + cross_bucket, + missing_uuid, + unsupported_format, + serde_json::Value::Null, + ] { + backend.put_json("warehouse", &before.metadata_location, metadata).await; + let credentials_error = load_credentials_response(&store, &backend, "warehouse", &namespace, "events", &issuer, None) + .await + .expect_err("invalid persisted metadata must not produce credentials"); + let delegated_error = + load_table_response_with_credentials(&store, &backend, "warehouse", &namespace, "events", &issuer, None) + .await + .expect_err("delegated load must fail closed"); + for error in [credentials_error, delegated_error] { + assert_eq!(error.status_code(), Some(StatusCode::INTERNAL_SERVER_ERROR)); + assert_eq!(error.code(), &S3ErrorCode::Custom(ICEBERG_ERROR_REST.into())); + assert_eq!(error.message(), Some("persisted table metadata is invalid")); + } + assert_eq!(issuer.calls.load(std::sync::atomic::Ordering::SeqCst), 0); + let after = store + .load_table("warehouse", "analytics", "events") + .await + .expect("lookup") + .expect("table"); + assert_eq!(after.table_uuid, before.table_uuid); + assert_eq!(after.metadata_location, before.metadata_location); + assert_eq!(after.version_token, before.version_token); + assert_eq!(after.generation, before.generation); + } + let disabled = + load_credentials_response(&store, &backend, "warehouse", &namespace, "events", &DisabledTableCredentialIssuer, None) + .await + .expect("disabled vending should not read or validate metadata"); + assert!(disabled.storage_credentials.is_empty()); + backend + .put_json("warehouse", &before.metadata_location, created.metadata) + .await; + backend.fail_next_read("warehouse", &before.metadata_location).await; + let read_error = load_credentials_response(&store, &backend, "warehouse", &namespace, "events", &issuer, None) + .await + .expect_err("metadata read failure must prevent issuance"); + assert_eq!(read_error.status_code(), Some(StatusCode::INTERNAL_SERVER_ERROR)); + assert_eq!(read_error.message(), Some("persisted table metadata is invalid")); + backend + .delete_object("warehouse", &before.metadata_location) + .await + .expect("remove metadata fixture"); + let missing_metadata = load_credentials_response(&store, &backend, "warehouse", &namespace, "events", &issuer, None) + .await + .expect_err("missing metadata must prevent issuance"); + assert_eq!(missing_metadata.status_code(), Some(StatusCode::INTERNAL_SERVER_ERROR)); + assert_eq!(issuer.calls.load(std::sync::atomic::Ordering::SeqCst), 0); + let missing = + load_credentials_response(&store, &backend, "warehouse", &namespace, "missing", &DisabledTableCredentialIssuer, None) + .await + .expect_err("disabled vending must still reject missing tables"); + assert_eq!(missing.status_code(), Some(StatusCode::NOT_FOUND)); + } +} + #[async_trait::async_trait] impl TableCredentialIssuer for TestTableCredentialIssuer { async fn issue_table_credentials( @@ -10852,9 +11141,14 @@ async fn credential_issuer_returns_temporary_scoped_storage_credentials() { ..Default::default() }; - let response = load_credentials_response_from_entry(&table_entry_for_credentials(), &issuer, Some(&principal)) - .await - .expect("issuer should build a scoped credential response"); + let response = load_credentials_response_from_entry( + &table_entry_for_credentials(), + &table_metadata_for_credentials(), + &issuer, + Some(&principal), + ) + .await + .expect("issuer should build a scoped credential response"); assert_eq!( response.config.get(CREDENTIAL_VENDING_CONFIG_KEY), @@ -10893,11 +11187,7 @@ async fn credential_issuer_returns_temporary_scoped_storage_credentials() { #[tokio::test] async fn load_table_uses_the_shared_credential_vending_result() { let entry = table_entry_for_credentials(); - let metadata = serde_json::json!({ - "format-version": 2, - "table-uuid": "table-uuid", - "location": "s3://warehouse/tables/table-id" - }); + let metadata = table_metadata_for_credentials(); let principal = rustfs_credentials::Credentials { access_key: "parent-access-key".to_string(), secret_key: "parent-secret-key".to_string(), @@ -10911,9 +11201,14 @@ async fn load_table_uses_the_shared_credential_vending_result() { ) .await .expect("load table should include vended credentials"); - let credentials = load_credentials_response_from_entry(&entry, &TestTableCredentialIssuer, Some(&principal)) - .await - .expect("credentials endpoint should include vended credentials"); + let credentials = load_credentials_response_from_entry( + &entry, + &table_metadata_for_credentials(), + &TestTableCredentialIssuer, + Some(&principal), + ) + .await + .expect("credentials endpoint should include vended credentials"); assert_eq!( load_table.config.get(CREDENTIAL_VENDING_CONFIG_KEY), @@ -10952,11 +11247,12 @@ impl TableCredentialIssuer for RefusingTableCredentialIssuer { #[tokio::test] async fn load_table_and_credentials_endpoint_propagate_issuer_refusal() { let entry = table_entry_for_credentials(); - let credentials_error = load_credentials_response_from_entry(&entry, &RefusingTableCredentialIssuer, None) - .await - .expect_err("credentials endpoint should propagate issuer refusal"); + let credentials_error = + load_credentials_response_from_entry(&entry, &table_metadata_for_credentials(), &RefusingTableCredentialIssuer, None) + .await + .expect_err("credentials endpoint should propagate issuer refusal"); let load_table_error = enrich_load_table_response_with_credentials( - load_table_response_from_entry(entry.clone(), serde_json::json!({})), + load_table_response_from_entry(entry.clone(), table_metadata_for_credentials()), &entry, &RefusingTableCredentialIssuer, None, @@ -10965,6 +11261,7 @@ async fn load_table_and_credentials_endpoint_propagate_issuer_refusal() { .expect_err("load table should propagate issuer refusal"); assert_eq!(load_table_error.code(), credentials_error.code()); + assert_eq!(credentials_error.code(), &S3ErrorCode::AccessDenied); assert_eq!(load_table_error.status_code(), credentials_error.status_code()); assert_eq!(load_table_error.message(), credentials_error.message()); } @@ -10972,9 +11269,10 @@ async fn load_table_and_credentials_endpoint_propagate_issuer_refusal() { #[tokio::test] async fn credential_response_serializes_sensitive_config_only_inside_storage_credentials() { let issuer = TestTableCredentialIssuer; - let response = load_credentials_response_from_entry(&table_entry_for_credentials(), &issuer, None) - .await - .expect("issuer should build a scoped credential response"); + let response = + load_credentials_response_from_entry(&table_entry_for_credentials(), &table_metadata_for_credentials(), &issuer, None) + .await + .expect("issuer should build a scoped credential response"); let value = serde_json::to_value(&response).expect("credential response should serialize"); @@ -11006,9 +11304,14 @@ fn credential_http_response_disables_caching() { #[tokio::test] async fn credential_debug_output_redacts_secrets_and_tokens() { - let response = load_credentials_response_from_entry(&table_entry_for_credentials(), &TestTableCredentialIssuer, None) - .await - .expect("issuer should build a scoped credential response"); + let response = load_credentials_response_from_entry( + &table_entry_for_credentials(), + &table_metadata_for_credentials(), + &TestTableCredentialIssuer, + None, + ) + .await + .expect("issuer should build a scoped credential response"); let debug_output = format!("{response:?}"); assert!(!debug_output.contains("temporary-access-key")); @@ -11241,24 +11544,25 @@ async fn table_credential_session_policy_includes_table_resource_actions() { fn table_credential_scope_rejects_cross_bucket_or_unsafe_prefix() { let mut entry = table_entry_for_credentials(); entry.warehouse_location = "s3://other-warehouse/tables/table-id".to_string(); - assert!(table_credential_scope(&entry).is_err()); + assert!(table_credential_scope(&entry, &table_metadata_for_credentials()).is_err()); let mut entry = table_entry_for_credentials(); entry.warehouse_location = "s3://warehouse/tables/../table-id".to_string(); - assert!(table_credential_scope(&entry).is_err()); + assert!(table_credential_scope(&entry, &table_metadata_for_credentials()).is_err()); let mut entry = table_entry_for_credentials(); entry.warehouse_location = "s3://warehouse/.rustfs-table".to_string(); - assert!(table_credential_scope(&entry).is_err()); + assert!(table_credential_scope(&entry, &table_metadata_for_credentials()).is_err()); let mut entry = table_entry_for_credentials(); entry.metadata_location = "s3://other/.rustfs-table/metadata/00001.metadata.json".to_string(); - assert!(table_credential_scope(&entry).is_err()); + assert!(table_credential_scope(&entry, &table_metadata_for_credentials()).is_err()); let mut entry = table_entry_for_credentials(); entry.metadata_location = ".rustfs-table/warehouses/default/namespaces/analytics/tables/orders/metadata/00001.metadata.json".to_string(); - assert!(table_credential_scope(&entry).is_err()); + let foreign_metadata = test_table_metadata_json("orders-table-uuid", &entry.warehouse_location); + assert!(table_credential_scope(&entry, &foreign_metadata).is_err()); } #[test] @@ -11266,7 +11570,8 @@ fn table_credential_scope_accepts_entry_relative_metadata_location() { let mut entry = table_entry_for_credentials(); entry.metadata_location = "s3://warehouse/tables/table-id/metadata/v1.metadata.json".to_string(); - let scope = table_credential_scope(&entry).expect("entry-relative metadata should remain vendable"); + let scope = table_credential_scope(&entry, &table_metadata_for_credentials()) + .expect("entry-relative metadata should remain vendable"); assert_eq!(scope.metadata_object, "tables/table-id/metadata/v1.metadata.json"); assert_eq!(scope.metadata_scope_prefix, "s3://warehouse/tables/table-id/metadata/v1.metadata.json");