From b47981c51309a1c25cca07911dab6b87cf07daff Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 22 Jun 2026 21:46:24 +0530 Subject: [PATCH 1/5] Update CHANGELOG for native credential vending allowedLocations fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a8f5bcdc5..eb44a3a47a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -349,3 +349,4 @@ Apache Polaris 0.9.0 was released on March 11, 2025 as the first Polaris release [Open Policy Agent (OPA)]: https://www.openpolicyagent.org/ [Iceberg Metrics Reporting]: https://iceberg.apache.org/docs/latest/metrics-reporting/ [Apache Ranger Authorization]: https://ranger.apache.org/ +- Fixed native catalog credential vending paths (`loadCredentials` and `loadTable` with delegation) to re-validate locations against the *current* catalog `allowedLocations`. Previously these paths trusted persisted table entity locations, allowing stale credentials after an admin tightened allowed locations on a native catalog. (The federated path had a partial check.) From 9ead04fcb1ac7a78b8ec6a2856871da9c116a24c Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 22 Jun 2026 21:58:22 +0530 Subject: [PATCH 2/5] Fix native catalog credential vending skipping allowedLocations re-validation - Always validate table locations before vending credentials for native catalogs. - Added regression test for post-creation allowedLocations shrink. --- .../iceberg/IcebergCatalogHandler.java | 24 ++++-- .../iceberg/IcebergAllowedLocationTest.java | 86 +++++++++++++++++++ 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java index 46d33faa930..9133f3d18cc 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java @@ -1021,10 +1021,10 @@ private LoadTableResponse.Builder buildLoadTableResponseWithDelegationCredential Set tableLocations = StorageUtil.getLocationsUsedByTable(tableMetadata); - // For federated catalogs, validate that table locations are within allowed locations - if (isFederated) { - validateRemoteTableLocations(tableIdentifier, tableLocations, resolvedStoragePath); - } + // Validate that the table's locations are still within the catalog's current + // allowedLocations before vending credentials. This protects against cases where + // allowedLocations were tightened after the table was created. + validateTableLocations(tableIdentifier, tableLocations, resolvedStoragePath); StorageAccessConfig storageAccessConfig = storageAccessConfigProvider() @@ -1058,13 +1058,15 @@ private LoadTableResponse.Builder buildLoadTableResponseWithDelegationCredential return responseBuilder; } - private void validateRemoteTableLocations( + private void validateTableLocations( TableIdentifier tableIdentifier, Set tableLocations, PolarisResolvedPathWrapper resolvedStoragePath) { try { - // Delegate to common validation logic + // Delegate to common validation logic. This is called for both native and federated + // catalogs before vending credentials to ensure locations are still within the + // current catalog's allowedLocations (defense against policy changes after table creation). CatalogUtils.validateLocationsForTableLike( realmConfig(), tableIdentifier, tableLocations, resolvedStoragePath); @@ -1072,15 +1074,15 @@ private void validateRemoteTableLocations( .atInfo() .addKeyValue("tableIdentifier", tableIdentifier) .addKeyValue("tableLocations", tableLocations) - .log("Validated federated table locations"); + .log("Validated table locations for credential vending"); } catch (ForbiddenException e) { LOGGER .atError() .addKeyValue("tableIdentifier", tableIdentifier) .addKeyValue("tableLocations", tableLocations) - .log("Federated table locations validation failed"); + .log("Table locations validation failed for credential vending"); throw new ForbiddenException( - "Table '%s' in remote catalog has locations outside catalog's allowed locations: %s", + "Table '%s' has locations outside the catalog's current allowed locations: %s", tableIdentifier, e.getMessage()); } } @@ -1567,6 +1569,10 @@ private StorageAccessConfig vendCredentials( return null; } + // Re-validate before vending in case this is called from other paths in the future. + // Primary validation for loadCredentials and delegation happens at call sites. + validateTableLocations(tableIdentifier, tableLocations, resolvedStoragePath); + return storageAccessConfigProvider() .getStorageAccessConfig( tableIdentifier, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java index a8f3476aa1b..d27b39f9bb7 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import org.apache.iceberg.MetadataUpdate; import org.apache.iceberg.catalog.Namespace; @@ -51,6 +52,7 @@ import org.apache.polaris.core.admin.model.CreateCatalogRequest; import org.apache.polaris.core.admin.model.FileStorageConfigInfo; import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.core.admin.model.UpdateCatalogRequest; import org.apache.polaris.service.TestServices; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -549,4 +551,88 @@ private void createNamespace(TestServices services, String location) { assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); } } + + private void updateCatalogAllowedLocations( + TestServices services, List newAllowedLocations) { + // Fetch current catalog to get entity version for update + Catalog fetched; + try (Response getResp = + services + .catalogsApi() + .getCatalog(catalog, services.realmContext(), services.securityContext())) { + assertThat(getResp.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + fetched = getResp.readEntity(Catalog.class); + } + + StorageConfigInfo newConfig = + FileStorageConfigInfo.builder() + .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(newAllowedLocations) + .build(); + + UpdateCatalogRequest updateRequest = + UpdateCatalogRequest.builder() + .setCurrentEntityVersion(fetched.getEntityVersion()) + .setStorageConfigInfo(newConfig) + .build(); + + try (Response updateResp = + services + .catalogsApi() + .updateCatalog( + catalog, updateRequest, services.realmContext(), services.securityContext())) { + assertThat(updateResp.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + } + } + + @Test + void testNativeCredentialVendingRejectsStaleLocationsAfterAllowedLocationsShrink( + @TempDir Path tmpDir) { + var services = getTestServices(); + + var baseDir = tmpDir.resolve(catalog).toAbsolutePath().toUri().toString(); + var allowedWhenCreated = baseDir + "/original-data"; + var disallowedAfterShrink = baseDir + "/other-data"; + + // Catalog initially allows the original location + createCatalog(services, Map.of(), baseDir, List.of(allowedWhenCreated)); + createNamespace(services, allowedWhenCreated + "/" + namespace); + + String tableName = getTableName(); + TableIdentifier tableId = TableIdentifier.of(Namespace.of(namespace), tableName); + + var createReq = + CreateTableRequest.builder() + .withName(tableName) + .withSchema(SCHEMA) + .withLocation(allowedWhenCreated + "/" + namespace + "/" + tableName) + .build(); + + // Create succeeds under current allowed + try (Response createResp = + services + .restApi() + .createTable( + catalog, + namespace, + createReq, + "vended-credentials", + IDEMPOTENCY_KEY, + services.realmContext(), + services.securityContext())) { + assertThat(createResp.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + } + + // Now shrink: update catalog to only allow a completely different location + updateCatalogAllowedLocations(services, List.of(disallowedAfterShrink)); + + // Credential vending for the existing table (which has stale location in its entity) + # must now be rejected. + IcebergCatalogHandler handler = + services.catalogAdapter().newHandler(services.securityContext(), catalog); + + assertThatThrownBy(() -> handler.loadCredentials(tableId, Optional.empty())) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("outside the catalog's current allowed locations"); + } } From 3f3eeae40a7521fcdf582de8d9324a291a5d6038 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 22 Jun 2026 21:59:07 +0530 Subject: [PATCH 3/5] Update CHANGELOG for native credential vending allowedLocations fix --- CHANGELOG.md | 4 +--- .../service/catalog/iceberg/IcebergAllowedLocationTest.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb44a3a47a6..d488e22b71c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti - The configuration option `polaris.event-listener.type` is deprecated and will be removed later. Please use `polaris.event-listener.types` instead. ### Fixes +- Fixed native catalog credential vending paths (`loadCredentials` and `loadTable` with delegation) to re-validate locations against the *current* catalog `allowedLocations`. Previously these paths trusted persisted table entity locations, allowing stale credentials after an admin tightened allowed locations on a native catalog. (The federated path had a partial check.) ## [1.4.0] @@ -153,7 +154,6 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti - `PolarisConfigurationStore` has been deprecated for removal. ### Fixes - - Fixed error propagation in drop operations (`dropTable`, `dropView`, `dropNamespace`). Server errors now return appropriate HTTP status codes based on persistence result instead of always returning NotFound - Enable non-AWS STS role ARNs - Helm chart: fixed a bug that prevented CORS settings to be properly applied. A new setting `cors.enabled` has been introduced in the chart as part of the fix. @@ -231,7 +231,6 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti endpoints at `/q/metrics` and `/q/health` instead. ### Fixes - - Fixed incorrect Azure expires at field for the credentials refresh response, leading to client failure via #2633 ## [1.1.0-incubating] @@ -349,4 +348,3 @@ Apache Polaris 0.9.0 was released on March 11, 2025 as the first Polaris release [Open Policy Agent (OPA)]: https://www.openpolicyagent.org/ [Iceberg Metrics Reporting]: https://iceberg.apache.org/docs/latest/metrics-reporting/ [Apache Ranger Authorization]: https://ranger.apache.org/ -- Fixed native catalog credential vending paths (`loadCredentials` and `loadTable` with delegation) to re-validate locations against the *current* catalog `allowedLocations`. Previously these paths trusted persisted table entity locations, allowing stale credentials after an admin tightened allowed locations on a native catalog. (The federated path had a partial check.) diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java index d27b39f9bb7..f33f0597285 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java @@ -627,7 +627,7 @@ void testNativeCredentialVendingRejectsStaleLocationsAfterAllowedLocationsShrink updateCatalogAllowedLocations(services, List.of(disallowedAfterShrink)); // Credential vending for the existing table (which has stale location in its entity) - # must now be rejected. + // must now be rejected. IcebergCatalogHandler handler = services.catalogAdapter().newHandler(services.securityContext(), catalog); From 0b45cc8e91635e76733f47c9ec18bb2810dff56d Mon Sep 17 00:00:00 2001 From: vignesh_A <165256748+vigneshio@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:09:19 +0530 Subject: [PATCH 4/5] Fix comment formatting in IcebergAllowedLocationTest --- .../service/catalog/iceberg/IcebergAllowedLocationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java index f33f0597285..1dd17f93e69 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java @@ -554,7 +554,7 @@ private void createNamespace(TestServices services, String location) { private void updateCatalogAllowedLocations( TestServices services, List newAllowedLocations) { - // Fetch current catalog to get entity version for update + // Fetch current catalog to get entity version for update. Catalog fetched; try (Response getResp = services From 370cef27513937c67faa398728f94d860817dcfe Mon Sep 17 00:00:00 2001 From: vignesh_A Date: Thu, 25 Jun 2026 05:13:59 +0530 Subject: [PATCH 5/5] Fix CatalogFederationIT assertions for updated credential vending error message --- .../service/it/test/CatalogFederationIntegrationTest.java | 4 ++-- .../service/catalog/iceberg/IcebergAllowedLocationTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/CatalogFederationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/CatalogFederationIntegrationTest.java index 3e4738f5388..bdd338c8f61 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/CatalogFederationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/CatalogFederationIntegrationTest.java @@ -483,7 +483,7 @@ void testFederatedCatalogNotVendCredentialForTablesOutsideAllowedLocations() { assertThatThrownBy(() -> spark.sql("SELECT * FROM ns3.test_table ORDER BY id").collectAsList()) .isInstanceOf(ForbiddenException.class) .hasMessageContaining( - "Table 'ns3.test_table' in remote catalog has locations outside catalog's allowed locations:"); + "Table 'ns3.test_table' has locations outside the catalog's current allowed locations:"); // Case 3: TABLE_WRITE_DATA managementApi.revokeGrant(federatedCatalogName, federatedCatalogRoleName, tableReadDataGrant); @@ -501,6 +501,6 @@ void testFederatedCatalogNotVendCredentialForTablesOutsideAllowedLocations() { () -> spark.sql("INSERT INTO ns3.test_table VALUES (3, 'Charlie')").collectAsList()) .isInstanceOf(ForbiddenException.class) .hasMessageContaining( - "Table 'ns3.test_table' in remote catalog has locations outside catalog's allowed locations:"); + "Table 'ns3.test_table' has locations outside the catalog's current allowed locations:"); } } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java index 1dd17f93e69..83348531447 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java @@ -554,7 +554,7 @@ private void createNamespace(TestServices services, String location) { private void updateCatalogAllowedLocations( TestServices services, List newAllowedLocations) { - // Fetch current catalog to get entity version for update. + // Fetch current catalog to get entity version for update. Catalog fetched; try (Response getResp = services