From c8aa4bacd6faeca2b666978f96bab01e48d4568c Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 11:35:18 -0700 Subject: [PATCH 01/22] writing test --- .../PolarisApplicationIntegrationTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java index 2e0acc5a32..1949569598 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java @@ -641,4 +641,26 @@ public void testRequestBodyTooLarge() throws Exception { }); } } + + @Test + public void testNamespaceOutsideCatalog() throws IOException { + String catalogName = client.newEntityName("testNamespaceOutsideCatalog"); + String catalogLocation = baseLocation.resolve("testNamespaceOutsideCatalog").resolve("catalog").toString(); + String namespaceLocation = baseLocation.resolve("testNamespaceOutsideCatalog").resolve("ns").toString(); + createCatalog( + catalogName, + Catalog.TypeEnum.INTERNAL, + principalRoleName, + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of(catalogLocation)) + .build(), + catalogLocation); + try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { + SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); + Namespace ns = Namespace.of("ns"); + sessionCatalog.createNamespace(sessionContext, ns, Map.of("location", namespaceLocation)); + } finally { + + } + } } From b3985fd2421b638f12c42e7386fd14a49d3a3cab Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 11:36:31 -0700 Subject: [PATCH 02/22] wip --- .../service/it/test/PolarisApplicationIntegrationTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java index 1949569598..5657b27cd4 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java @@ -659,8 +659,6 @@ public void testNamespaceOutsideCatalog() throws IOException { SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); Namespace ns = Namespace.of("ns"); sessionCatalog.createNamespace(sessionContext, ns, Map.of("location", namespaceLocation)); - } finally { - } } } From 8f058a038d12f9f173eb3b32acddee0af3807835 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 11:39:04 -0700 Subject: [PATCH 03/22] fixed --- .../service/it/test/CatalogFederationIntegrationTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 890e92cae7..0edc80af32 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 @@ -66,6 +66,7 @@ public class CatalogFederationIntegrationTest { private static ClientCredentials adminCredentials; private static SparkSession spark; private static String sparkToken; + private static String adminToken; private static final String PRINCIPAL_NAME = "test-catalog-federation-user"; private static final String LOCAL_CATALOG_NAME = "test_catalog_local"; @@ -82,7 +83,8 @@ public class CatalogFederationIntegrationTest { static void setup(PolarisApiEndpoints apiEndpoints, ClientCredentials credentials) { endpoints = apiEndpoints; client = polarisClient(endpoints); - managementApi = client.managementApi(credentials); + adminToken = client.obtainToken(credentials); + managementApi = client.managementApi(adminToken); adminCredentials = credentials; sparkToken = client.obtainToken(credentials); } From 68e83ff7f661695945f8a381e14d1da125b0a037 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 11:41:39 -0700 Subject: [PATCH 04/22] cleanups --- .../service/it/test/CatalogFederationIntegrationTest.java | 2 -- 1 file changed, 2 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 0edc80af32..48ae129fce 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 @@ -63,7 +63,6 @@ public class CatalogFederationIntegrationTest { private static PolarisClient client; private static ManagementApi managementApi; private static PolarisApiEndpoints endpoints; - private static ClientCredentials adminCredentials; private static SparkSession spark; private static String sparkToken; private static String adminToken; @@ -85,7 +84,6 @@ static void setup(PolarisApiEndpoints apiEndpoints, ClientCredentials credential client = polarisClient(endpoints); adminToken = client.obtainToken(credentials); managementApi = client.managementApi(adminToken); - adminCredentials = credentials; sparkToken = client.obtainToken(credentials); } From a02197e3c354552d0e6065d9246b17734afb1c6b Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 12:08:01 -0700 Subject: [PATCH 05/22] test is stable --- .../it/test/PolarisApplicationIntegrationTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java index 5657b27cd4..a0ab7d60f1 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; +import com.google.common.collect.ImmutableMap; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.Invocation; @@ -644,9 +645,9 @@ public void testRequestBodyTooLarge() throws Exception { @Test public void testNamespaceOutsideCatalog() throws IOException { - String catalogName = client.newEntityName("testNamespaceOutsideCatalog"); - String catalogLocation = baseLocation.resolve("testNamespaceOutsideCatalog").resolve("catalog").toString(); - String namespaceLocation = baseLocation.resolve("testNamespaceOutsideCatalog").resolve("ns").toString(); + String catalogName = client.newEntityName("testNamespaceOutsideCatalog_specificLocation"); + String catalogLocation = baseLocation.resolve(catalogName + "/catalog").toString(); + String namespaceLocation = baseLocation.resolve(catalogName + "/ns").toString(); createCatalog( catalogName, Catalog.TypeEnum.INTERNAL, @@ -657,8 +658,11 @@ public void testNamespaceOutsideCatalog() throws IOException { catalogLocation); try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); - Namespace ns = Namespace.of("ns"); - sessionCatalog.createNamespace(sessionContext, ns, Map.of("location", namespaceLocation)); + sessionCatalog.createNamespace(sessionContext, Namespace.of("good_namespace")); + sessionCatalog.createNamespace( + sessionContext, + Namespace.of("bad_namespace"), + ImmutableMap.of("location", namespaceLocation)); } } } From 7fc8180f1a75659a97169841bba25ba194614d7e Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 12:15:45 -0700 Subject: [PATCH 06/22] parameterize test --- .../PolarisApplicationIntegrationTest.java | 43 ++++++++++++++++--- .../core/config/FeatureConfiguration.java | 10 +++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java index a0ab7d60f1..24b33ded0d 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java @@ -20,6 +20,7 @@ import static org.apache.polaris.service.it.env.PolarisClient.polarisClient; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; @@ -68,6 +69,7 @@ import org.apache.polaris.core.admin.model.PolarisCatalog; import org.apache.polaris.core.admin.model.PrincipalRole; import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.service.it.env.ClientPrincipal; @@ -77,6 +79,7 @@ import org.apache.polaris.service.it.env.RestApi; import org.apache.polaris.service.it.ext.PolarisIntegrationTestExtension; import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.ThrowableAssert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -85,6 +88,8 @@ import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * @implSpec This test expects the server to be configured with the following features configured: @@ -187,11 +192,24 @@ private static void createCatalog( String principalRoleName, StorageConfigInfo storageConfig, String defaultBaseLocation) { - CatalogProperties props = + createCatalog(catalogName, catalogType, principalRoleName, storageConfig, defaultBaseLocation, ImmutableMap.of()); + } + + private static void createCatalog( + String catalogName, + Catalog.TypeEnum catalogType, + String principalRoleName, + StorageConfigInfo storageConfig, + String defaultBaseLocation, + Map additionalProperties) { + CatalogProperties.Builder propsBuilder = CatalogProperties.builder(defaultBaseLocation) .addProperty( - CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:/") - .build(); + CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:/"); + for (var entry: additionalProperties.entrySet()) { + propsBuilder.addProperty(entry.getKey(), entry.getValue()); + } + CatalogProperties props = propsBuilder.build(); Catalog catalog = catalogType.equals(Catalog.TypeEnum.INTERNAL) ? PolarisCatalog.builder() @@ -643,8 +661,9 @@ public void testRequestBodyTooLarge() throws Exception { } } - @Test - public void testNamespaceOutsideCatalog() throws IOException { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testNamespaceOutsideCatalog(boolean allowNamespaceLocationEscape) throws IOException { String catalogName = client.newEntityName("testNamespaceOutsideCatalog_specificLocation"); String catalogLocation = baseLocation.resolve(catalogName + "/catalog").toString(); String namespaceLocation = baseLocation.resolve(catalogName + "/ns").toString(); @@ -655,14 +674,24 @@ public void testNamespaceOutsideCatalog() throws IOException { FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) .setAllowedLocations(List.of(catalogLocation)) .build(), - catalogLocation); + catalogLocation, + ImmutableMap.of( + FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_ESCAPE.catalogConfig(), + String.valueOf(allowNamespaceLocationEscape))); try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); sessionCatalog.createNamespace(sessionContext, Namespace.of("good_namespace")); - sessionCatalog.createNamespace( + ThrowableAssert.ThrowingCallable createBadNamespace = () -> sessionCatalog.createNamespace( sessionContext, Namespace.of("bad_namespace"), ImmutableMap.of("location", namespaceLocation)); + if (!allowNamespaceLocationEscape) { + assertThatThrownBy(createBadNamespace) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("location"); + } else { + assertThatCode(createBadNamespace).doesNotThrowAnyException(); + } } } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index e01e065a1c..d1e216524e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -100,6 +100,16 @@ public static void enforceFeatureEnabledOrThrow( .defaultValue(false) .buildFeatureConfiguration(); + public static final FeatureConfiguration ALLOW_NAMESPACE_LOCATION_ESCAPE = + PolarisConfiguration.builder() + .key("ALLOW_NAMESPACE_LOCATION_ESCAPE") + .catalogConfig("polaris.config.namespace-location-escape.enabled") + .description( + "If set to true, allow namespaces to be created with locations outside of the parent catalog's" + + "location.") + .defaultValue(false) + .buildFeatureConfiguration(); + public static final FeatureConfiguration ALLOW_EXTERNAL_METADATA_FILE_LOCATION = PolarisConfiguration.builder() .key("ALLOW_EXTERNAL_METADATA_FILE_LOCATION") From 27dc824ce4d444547aa1e4dce9bdc5c24c475ed4 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 12:58:11 -0700 Subject: [PATCH 07/22] stable --- .../PolarisApplicationIntegrationTest.java | 39 ++++++++--- .../core/config/FeatureConfiguration.java | 4 +- .../catalog/iceberg/IcebergCatalog.java | 69 +++++++++++++++++++ 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java index 24b33ded0d..073af0db0c 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java @@ -192,7 +192,13 @@ private static void createCatalog( String principalRoleName, StorageConfigInfo storageConfig, String defaultBaseLocation) { - createCatalog(catalogName, catalogType, principalRoleName, storageConfig, defaultBaseLocation, ImmutableMap.of()); + createCatalog( + catalogName, + catalogType, + principalRoleName, + storageConfig, + defaultBaseLocation, + ImmutableMap.of()); } private static void createCatalog( @@ -206,7 +212,7 @@ private static void createCatalog( CatalogProperties.builder(defaultBaseLocation) .addProperty( CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:/"); - for (var entry: additionalProperties.entrySet()) { + for (var entry : additionalProperties.entrySet()) { propsBuilder.addProperty(entry.getKey(), entry.getValue()); } CatalogProperties props = propsBuilder.build(); @@ -662,11 +668,11 @@ public void testRequestBodyTooLarge() throws Exception { } @ParameterizedTest - @ValueSource(booleans = {false, true}) + @ValueSource(booleans = {true, false}) public void testNamespaceOutsideCatalog(boolean allowNamespaceLocationEscape) throws IOException { String catalogName = client.newEntityName("testNamespaceOutsideCatalog_specificLocation"); String catalogLocation = baseLocation.resolve(catalogName + "/catalog").toString(); - String namespaceLocation = baseLocation.resolve(catalogName + "/ns").toString(); + String badLocation = baseLocation.resolve(catalogName + "/ns").toString(); createCatalog( catalogName, Catalog.TypeEnum.INTERNAL, @@ -681,17 +687,32 @@ public void testNamespaceOutsideCatalog(boolean allowNamespaceLocationEscape) th try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); sessionCatalog.createNamespace(sessionContext, Namespace.of("good_namespace")); - ThrowableAssert.ThrowingCallable createBadNamespace = () -> sessionCatalog.createNamespace( - sessionContext, - Namespace.of("bad_namespace"), - ImmutableMap.of("location", namespaceLocation)); + ThrowableAssert.ThrowingCallable createBadNamespace = + () -> + sessionCatalog.createNamespace( + sessionContext, + Namespace.of("bad_namespace"), + ImmutableMap.of("location", badLocation)); if (!allowNamespaceLocationEscape) { assertThatThrownBy(createBadNamespace) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("location"); } else { assertThatCode(createBadNamespace).doesNotThrowAnyException(); } + ThrowableAssert.ThrowingCallable createBadChildGoodParent = + () -> + sessionCatalog.createNamespace( + sessionContext, + Namespace.of("good_namespace", "bad_child"), + ImmutableMap.of("location", badLocation)); + if (!allowNamespaceLocationEscape) { + assertThatThrownBy(createBadChildGoodParent) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("location"); + } else { + assertThatCode(createBadChildGoodParent).doesNotThrowAnyException(); + } } } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index d1e216524e..d4d781de8e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -105,8 +105,8 @@ public static void enforceFeatureEnabledOrThrow( .key("ALLOW_NAMESPACE_LOCATION_ESCAPE") .catalogConfig("polaris.config.namespace-location-escape.enabled") .description( - "If set to true, allow namespaces to be created with locations outside of the parent catalog's" + - "location.") + "If set to true, allow namespaces to be created with locations outside of the parent catalog's" + + "location.") .defaultValue(false) .buildFeatureConfiguration(); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index 4c0f7af2e0..ac1964ed71 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -508,6 +508,11 @@ private void createNamespaceInternal( } else { LOGGER.debug("Skipping location overlap validation for namespace '{}'", namespace); } + if (!realmConfig.getConfig( + FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_ESCAPE, catalogEntity)) { + LOGGER.debug("Validating that namespace {} has a location inside its parent", namespace); + validateNamespaceLocation(entity, resolvedParent); + } PolarisEntity returnedEntity = PolarisEntity.of( getMetaStoreManager() @@ -671,6 +676,11 @@ public boolean setProperties(Namespace namespace, Map properties } else { LOGGER.debug("Skipping location overlap validation for namespace '{}'", namespace); } + if (!realmConfig.getConfig( + FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_ESCAPE, catalogEntity)) { + LOGGER.debug("Validating that namespace {} has a location inside its parent", namespace); + validateNamespaceLocation(NamespaceEntity.of(entity), resolvedEntities); + } List parentPath = resolvedEntities.getRawFullPath(); PolarisEntity returnedEntity = @@ -1083,6 +1093,65 @@ private void validateNoLocationOverlap( } } + /** Checks whether the location of a namespace is valid given its parent */ + private void validateNamespaceLocation( + NamespaceEntity namespace, PolarisResolvedPathWrapper resolvedParent) { + StorageLocation namespaceLocation = + StorageLocation.of( + resolveNamespaceLocation(namespace.asNamespace(), namespace.getPropertiesAsMap())); + PolarisEntity parent = resolvedParent.getResolvedLeafEntity().getEntity(); + if (parent.getType().equals(PolarisEntityType.CATALOG)) { + CatalogEntity parentEntity = CatalogEntity.of(parent); + LOGGER.debug( + "Validating namespace {} given parent catalog {}", + namespace.getName(), + parentEntity.getName()); + var storageConfigInfo = parentEntity.getStorageConfigurationInfo(); + if (storageConfigInfo == null) { + throw new IllegalArgumentException( + "Cannot create namespace without a parent storage configuration"); + } + System.out.println("#### Validating " + namespace.getName()); + System.out.println("#### Parent location is a catalog"); + System.out.println("#### New location is " + namespaceLocation); + for (var l : parentEntity.getStorageConfigurationInfo().getAllowedLocations()) { + System.out.println("#### parent location -> " + l); + } + boolean allowed = + parentEntity.getStorageConfigurationInfo().getAllowedLocations().stream() + .filter(java.util.Objects::nonNull) + .map(StorageLocation::of) + .anyMatch(namespaceLocation::isChildOf); + if (!allowed) { + throw new IllegalArgumentException( + "Namespace location " + namespaceLocation + " is not within an allowed location"); + } + } else if (parent.getType().equals(PolarisEntityType.NAMESPACE)) { + NamespaceEntity parentEntity = NamespaceEntity.of(parent); + LOGGER.debug( + "Validating namespace {} given parent namespace {}", + namespace.getName(), + parentEntity.getName()); + StorageLocation parentLocation = + StorageLocation.of( + resolveNamespaceLocation( + parentEntity.asNamespace(), parentEntity.getPropertiesAsMap())); + System.out.println("#### Validating " + namespace.getName()); + System.out.println("#### Parent location is " + parentLocation); + System.out.println("#### New location is " + namespaceLocation); + if (!namespaceLocation.isChildOf(parentLocation)) { + throw new IllegalArgumentException( + "Namespace location " + namespaceLocation + " is not within an allowed location"); + } + } else { + throw new IllegalArgumentException( + "Failed to validate namespace " + + namespace.getName() + + " given parent " + + parent.getName()); + } + } + /** * Validate no location overlap exists between the entity path and its sibling entities. This * resolves all siblings at the same level as the target entity (namespaces if the target entity From 386c608a9dd9a938828fb29e49147b30f5d2e4b6 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Thu, 21 Aug 2025 14:37:19 -0700 Subject: [PATCH 08/22] fix cli test --- regtests/t_cli/src/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regtests/t_cli/src/test_cli.py b/regtests/t_cli/src/test_cli.py index ee6bea7147..e719498c05 100644 --- a/regtests/t_cli/src/test_cli.py +++ b/regtests/t_cli/src/test_cli.py @@ -170,7 +170,7 @@ def test_quickstart_flow(): '--property', 'foo=bar', '--location', - 's3://custom-namespace-location' + 's3://fake-location/custom-namespace-location' ), checker=lambda s: s == '') check_output(cli(user_token)('namespaces', 'list', '--catalog', f'test_cli_catalog_{SALT}'), checker=lambda s: f'test_cli_namespace_{SALT}' in s) From b62ab2724101f60a17c5299fb90c253bdab4b5d2 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Fri, 22 Aug 2025 11:05:12 -0700 Subject: [PATCH 09/22] Yank println --- .../polaris/service/catalog/iceberg/IcebergCatalog.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index ac1964ed71..63b92b7be0 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -1111,12 +1111,6 @@ private void validateNamespaceLocation( throw new IllegalArgumentException( "Cannot create namespace without a parent storage configuration"); } - System.out.println("#### Validating " + namespace.getName()); - System.out.println("#### Parent location is a catalog"); - System.out.println("#### New location is " + namespaceLocation); - for (var l : parentEntity.getStorageConfigurationInfo().getAllowedLocations()) { - System.out.println("#### parent location -> " + l); - } boolean allowed = parentEntity.getStorageConfigurationInfo().getAllowedLocations().stream() .filter(java.util.Objects::nonNull) @@ -1136,9 +1130,6 @@ private void validateNamespaceLocation( StorageLocation.of( resolveNamespaceLocation( parentEntity.asNamespace(), parentEntity.getPropertiesAsMap())); - System.out.println("#### Validating " + namespace.getName()); - System.out.println("#### Parent location is " + parentLocation); - System.out.println("#### New location is " + namespaceLocation); if (!namespaceLocation.isChildOf(parentLocation)) { throw new IllegalArgumentException( "Namespace location " + namespaceLocation + " is not within an allowed location"); From b92c7b795c3d0ec3c177d00ce33039e68fefe8f9 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Fri, 22 Aug 2025 23:27:04 -0700 Subject: [PATCH 10/22] reworked check --- .../PolarisApplicationIntegrationTest.java | 6 ++-- .../core/config/FeatureConfiguration.java | 10 +++--- .../polaris/core/storage/StorageLocation.java | 2 +- regtests/t_cli/src/test_cli.py | 4 +-- .../catalog/iceberg/IcebergCatalog.java | 34 ++++++++++++++----- 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java index 073af0db0c..e92bb2ffa8 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java @@ -682,7 +682,7 @@ public void testNamespaceOutsideCatalog(boolean allowNamespaceLocationEscape) th .build(), catalogLocation, ImmutableMap.of( - FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_ESCAPE.catalogConfig(), + FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION.catalogConfig(), String.valueOf(allowNamespaceLocationEscape))); try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); @@ -696,7 +696,7 @@ public void testNamespaceOutsideCatalog(boolean allowNamespaceLocationEscape) th if (!allowNamespaceLocationEscape) { assertThatThrownBy(createBadNamespace) .isInstanceOf(BadRequestException.class) - .hasMessageContaining("location"); + .hasMessageContaining("custom location"); } else { assertThatCode(createBadNamespace).doesNotThrowAnyException(); } @@ -709,7 +709,7 @@ public void testNamespaceOutsideCatalog(boolean allowNamespaceLocationEscape) th if (!allowNamespaceLocationEscape) { assertThatThrownBy(createBadChildGoodParent) .isInstanceOf(BadRequestException.class) - .hasMessageContaining("location"); + .hasMessageContaining("custom location"); } else { assertThatCode(createBadChildGoodParent).doesNotThrowAnyException(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index d4d781de8e..1fb2fb28a0 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -100,13 +100,11 @@ public static void enforceFeatureEnabledOrThrow( .defaultValue(false) .buildFeatureConfiguration(); - public static final FeatureConfiguration ALLOW_NAMESPACE_LOCATION_ESCAPE = + public static final FeatureConfiguration ALLOW_NAMESPACE_CUSTOM_LOCATION = PolarisConfiguration.builder() - .key("ALLOW_NAMESPACE_LOCATION_ESCAPE") - .catalogConfig("polaris.config.namespace-location-escape.enabled") - .description( - "If set to true, allow namespaces to be created with locations outside of the parent catalog's" - + "location.") + .key("ALLOW_NAMESPACE_CUSTOM_LOCATION") + .catalogConfig("polaris.config.namespace-custom-location.enabled") + .description("If set to true, allow namespaces with custom locations.") .defaultValue(false) .buildFeatureConfiguration(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageLocation.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageLocation.java index a1774b4ad9..fab1892abb 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageLocation.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageLocation.java @@ -65,7 +65,7 @@ protected StorageLocation(@Nonnull String location) { } /** If a path doesn't end in `/`, this will add one */ - protected static String ensureTrailingSlash(String location) { + public static String ensureTrailingSlash(String location) { if (location == null || location.endsWith("/")) { return location; } else { diff --git a/regtests/t_cli/src/test_cli.py b/regtests/t_cli/src/test_cli.py index e719498c05..0227339f4c 100644 --- a/regtests/t_cli/src/test_cli.py +++ b/regtests/t_cli/src/test_cli.py @@ -170,7 +170,7 @@ def test_quickstart_flow(): '--property', 'foo=bar', '--location', - 's3://fake-location/custom-namespace-location' + f's3://fake-location-{SALT}/custom-namespace-location/' ), checker=lambda s: s == '') check_output(cli(user_token)('namespaces', 'list', '--catalog', f'test_cli_catalog_{SALT}'), checker=lambda s: f'test_cli_namespace_{SALT}' in s) @@ -180,7 +180,7 @@ def test_quickstart_flow(): '--catalog', f'test_cli_catalog_{SALT}', f'test_cli_namespace_{SALT}' - ), checker=lambda s: 's3://custom-namespace-location' in s and '"foo": "bar"' in s) + ), checker=lambda s: f's3://fake-location-{SALT}/custom-namespace-location/' in s and '"foo": "bar"' in s) check_output(cli(user_token)( 'namespaces', 'delete', diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index 63b92b7be0..5355e561a8 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -509,7 +509,7 @@ private void createNamespaceInternal( LOGGER.debug("Skipping location overlap validation for namespace '{}'", namespace); } if (!realmConfig.getConfig( - FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_ESCAPE, catalogEntity)) { + FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { LOGGER.debug("Validating that namespace {} has a location inside its parent", namespace); validateNamespaceLocation(entity, resolvedParent); } @@ -677,7 +677,7 @@ public boolean setProperties(Namespace namespace, Map properties LOGGER.debug("Skipping location overlap validation for namespace '{}'", namespace); } if (!realmConfig.getConfig( - FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_ESCAPE, catalogEntity)) { + FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { LOGGER.debug("Validating that namespace {} has a location inside its parent", namespace); validateNamespaceLocation(NamespaceEntity.of(entity), resolvedEntities); } @@ -1098,7 +1098,8 @@ private void validateNamespaceLocation( NamespaceEntity namespace, PolarisResolvedPathWrapper resolvedParent) { StorageLocation namespaceLocation = StorageLocation.of( - resolveNamespaceLocation(namespace.asNamespace(), namespace.getPropertiesAsMap())); + StorageLocation.ensureTrailingSlash( + resolveNamespaceLocation(namespace.asNamespace(), namespace.getPropertiesAsMap()))); PolarisEntity parent = resolvedParent.getResolvedLeafEntity().getEntity(); if (parent.getType().equals(PolarisEntityType.CATALOG)) { CatalogEntity parentEntity = CatalogEntity.of(parent); @@ -1115,10 +1116,20 @@ private void validateNamespaceLocation( parentEntity.getStorageConfigurationInfo().getAllowedLocations().stream() .filter(java.util.Objects::nonNull) .map(StorageLocation::of) - .anyMatch(namespaceLocation::isChildOf); + .anyMatch( + l -> { + if (namespaceLocation.isChildOf(l)) { + String defaultLocation = + StorageLocation.ensureTrailingSlash( + StorageLocation.ensureTrailingSlash(l.withoutScheme()) + + namespace.getName()); + return defaultLocation.equals(namespaceLocation.withoutScheme()); + } + return false; + }); if (!allowed) { throw new IllegalArgumentException( - "Namespace location " + namespaceLocation + " is not within an allowed location"); + "Namespace " + namespace.getName() + " has a custom location, which is not enabled"); } } else if (parent.getType().equals(PolarisEntityType.NAMESPACE)) { NamespaceEntity parentEntity = NamespaceEntity.of(parent); @@ -1130,10 +1141,17 @@ private void validateNamespaceLocation( StorageLocation.of( resolveNamespaceLocation( parentEntity.asNamespace(), parentEntity.getPropertiesAsMap())); - if (!namespaceLocation.isChildOf(parentLocation)) { - throw new IllegalArgumentException( - "Namespace location " + namespaceLocation + " is not within an allowed location"); + if (namespaceLocation.isChildOf(parentLocation)) { + String defaultLocation = + StorageLocation.ensureTrailingSlash( + StorageLocation.ensureTrailingSlash(parentLocation.withoutScheme()) + + namespace.getName()); + if (defaultLocation.equals(namespaceLocation.withoutScheme())) { + return; + } } + throw new IllegalArgumentException( + "Namespace " + namespace.getName() + " has a custom location, which is not enabled"); } else { throw new IllegalArgumentException( "Failed to validate namespace " From 6bc11ab50debf5bdc2124928907297942d6caa01 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Mon, 25 Aug 2025 16:20:42 -0700 Subject: [PATCH 11/22] better exceptions --- .../catalog/iceberg/IcebergCatalog.java | 43 +++++++++++-------- .../it/RestCatalogFileIntegrationTest.java | 2 + 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index 5355e561a8..7f64b3ed26 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -1112,24 +1112,25 @@ private void validateNamespaceLocation( throw new IllegalArgumentException( "Cannot create namespace without a parent storage configuration"); } - boolean allowed = + List defaultLocations = parentEntity.getStorageConfigurationInfo().getAllowedLocations().stream() .filter(java.util.Objects::nonNull) - .map(StorageLocation::of) - .anyMatch( + .map( l -> { - if (namespaceLocation.isChildOf(l)) { - String defaultLocation = - StorageLocation.ensureTrailingSlash( - StorageLocation.ensureTrailingSlash(l.withoutScheme()) - + namespace.getName()); - return defaultLocation.equals(namespaceLocation.withoutScheme()); - } - return false; - }); - if (!allowed) { + return StorageLocation.ensureTrailingSlash( + StorageLocation.ensureTrailingSlash(StorageLocation.of(l).withoutScheme()) + + namespace.getName()); + }) + .toList(); + if (defaultLocations.stream() + .noneMatch(l -> namespaceLocation.isChildOf(StorageLocation.of(l)))) { throw new IllegalArgumentException( - "Namespace " + namespace.getName() + " has a custom location, which is not enabled"); + "Namespace " + + namespace.getName() + + " has a custom location, " + + "which is not enabled. Expected a location in: [" + + String.join(", ", defaultLocations) + + "]"); } } else if (parent.getType().equals(PolarisEntityType.NAMESPACE)) { NamespaceEntity parentEntity = NamespaceEntity.of(parent); @@ -1141,17 +1142,21 @@ private void validateNamespaceLocation( StorageLocation.of( resolveNamespaceLocation( parentEntity.asNamespace(), parentEntity.getPropertiesAsMap())); + String defaultLocation = + StorageLocation.ensureTrailingSlash( + StorageLocation.ensureTrailingSlash(parentLocation.withoutScheme()) + + namespace.getName()); if (namespaceLocation.isChildOf(parentLocation)) { - String defaultLocation = - StorageLocation.ensureTrailingSlash( - StorageLocation.ensureTrailingSlash(parentLocation.withoutScheme()) - + namespace.getName()); if (defaultLocation.equals(namespaceLocation.withoutScheme())) { return; } } throw new IllegalArgumentException( - "Namespace " + namespace.getName() + " has a custom location, which is not enabled"); + "Namespace " + + namespace.getName() + + " has a custom location, " + + "which is not enabled. Expected location: " + + defaultLocation); } else { throw new IllegalArgumentException( "Failed to validate namespace " diff --git a/runtime/service/src/test/java/org/apache/polaris/service/it/RestCatalogFileIntegrationTest.java b/runtime/service/src/test/java/org/apache/polaris/service/it/RestCatalogFileIntegrationTest.java index e50947ca04..5aaf858dec 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/it/RestCatalogFileIntegrationTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/it/RestCatalogFileIntegrationTest.java @@ -42,6 +42,8 @@ public Map getConfigOverrides() { "polaris.features.\"SUPPORTED_CATALOG_STORAGE_TYPES\"", "[\"FILE\",\"S3\"]", "polaris.readiness.ignore-severe-issues", + "true", + "polaris.features.\"ALLOW_NAMESPACE_CUSTOM_LOCATION\"", "true"); } } From 2a708f463e5c9c910469fd15b9be90e03d88c291 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Mon, 25 Aug 2025 16:34:25 -0700 Subject: [PATCH 12/22] fixes for various tests --- .../polaris/core/storage/aws/S3Location.java | 15 +++++++ .../core/storage/azure/AzureLocation.java | 15 +++++++ regtests/t_cli/src/test_cli.py | 2 + .../catalog/iceberg/IcebergCatalog.java | 41 ++++++++----------- .../service/admin/PolarisAuthzTestBase.java | 1 + .../catalog/AbstractIcebergCatalogTest.java | 1 + 6 files changed, 52 insertions(+), 23 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/S3Location.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/S3Location.java index 2146a5aee7..a051d0600d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/S3Location.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/S3Location.java @@ -67,4 +67,19 @@ public String getScheme() { public String withoutScheme() { return locationWithoutScheme; } + + @Override + public int hashCode() { + return withoutScheme().hashCode(); + } + + /** Checks if two S3Location instances represent the same physical location. */ + @Override + public boolean equals(Object obj) { + if (obj instanceof S3Location) { + return withoutScheme().equals(((StorageLocation) obj).withoutScheme()); + } else { + return false; + } + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java index 0214ebca02..475a8da423 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java @@ -126,4 +126,19 @@ public static boolean isAzureLocation(String location) { Matcher matcher = URI_PATTERN.matcher(location); return matcher.matches(); } + + @Override + public int hashCode() { + return withoutScheme().hashCode(); + } + + /** Checks if two AzureLocation instances represent the same physical location. */ + @Override + public boolean equals(Object obj) { + if (obj instanceof AzureLocation) { + return withoutScheme().equals(((StorageLocation) obj).withoutScheme()); + } else { + return false; + } + } } diff --git a/regtests/t_cli/src/test_cli.py b/regtests/t_cli/src/test_cli.py index 0227339f4c..07de695cfd 100644 --- a/regtests/t_cli/src/test_cli.py +++ b/regtests/t_cli/src/test_cli.py @@ -110,6 +110,8 @@ def test_quickstart_flow(): ROLE_ARN, '--default-base-location', f's3://fake-location-{SALT}', + '--property', + 'polaris.config.namespace-custom-location.enabled=true', f'test_cli_catalog_{SALT}'), checker=lambda s: s == '') check_output(root_cli('catalogs', 'list'), checker=lambda s: f'test_cli_catalog_{SALT}' in s) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index 7f64b3ed26..797afe373e 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -1112,24 +1112,24 @@ private void validateNamespaceLocation( throw new IllegalArgumentException( "Cannot create namespace without a parent storage configuration"); } - List defaultLocations = + List defaultLocations = parentEntity.getStorageConfigurationInfo().getAllowedLocations().stream() .filter(java.util.Objects::nonNull) .map( l -> { return StorageLocation.ensureTrailingSlash( - StorageLocation.ensureTrailingSlash(StorageLocation.of(l).withoutScheme()) - + namespace.getName()); + StorageLocation.ensureTrailingSlash(l) + namespace.getName()); }) + .map(StorageLocation::of) .toList(); - if (defaultLocations.stream() - .noneMatch(l -> namespaceLocation.isChildOf(StorageLocation.of(l)))) { + if (!defaultLocations.contains(namespaceLocation)) { throw new IllegalArgumentException( "Namespace " + namespace.getName() + " has a custom location, " + "which is not enabled. Expected a location in: [" - + String.join(", ", defaultLocations) + + String.join( + ", ", defaultLocations.stream().map(StorageLocation::toString).toList()) + "]"); } } else if (parent.getType().equals(PolarisEntityType.NAMESPACE)) { @@ -1138,25 +1138,20 @@ private void validateNamespaceLocation( "Validating namespace {} given parent namespace {}", namespace.getName(), parentEntity.getName()); - StorageLocation parentLocation = + String parentLocation = + resolveNamespaceLocation(parentEntity.asNamespace(), parentEntity.getPropertiesAsMap()); + StorageLocation defaultLocation = StorageLocation.of( - resolveNamespaceLocation( - parentEntity.asNamespace(), parentEntity.getPropertiesAsMap())); - String defaultLocation = - StorageLocation.ensureTrailingSlash( - StorageLocation.ensureTrailingSlash(parentLocation.withoutScheme()) - + namespace.getName()); - if (namespaceLocation.isChildOf(parentLocation)) { - if (defaultLocation.equals(namespaceLocation.withoutScheme())) { - return; - } + StorageLocation.ensureTrailingSlash( + StorageLocation.ensureTrailingSlash(parentLocation) + namespace.getName())); + if (!defaultLocation.equals(namespaceLocation)) { + throw new IllegalArgumentException( + "Namespace " + + namespace.getName() + + " has a custom location, " + + "which is not enabled. Expected location: " + + defaultLocation); } - throw new IllegalArgumentException( - "Namespace " - + namespace.getName() - + " has a custom location, " - + "which is not enabled. Expected location: " - + defaultLocation); } else { throw new IllegalArgumentException( "Failed to validate namespace " diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index 2717ee71c9..5865cf8785 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -119,6 +119,7 @@ public Map getConfigOverrides() { "polaris.features.\"ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING\"", "true") .put("polaris.features.\"DROP_WITH_PURGE_ENABLED\"", "true") + .put("polaris.features.\"ALLOW_NAMESPACE_CUSTOM_LOCATION\"", "true") .build(); } } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/AbstractIcebergCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/AbstractIcebergCatalogTest.java index d10f820574..e302997a93 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/AbstractIcebergCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/AbstractIcebergCatalogTest.java @@ -198,6 +198,7 @@ public Map getConfigOverrides() { .putAll(super.getConfigOverrides()) .put("polaris.features.\"ALLOW_TABLE_LOCATION_OVERLAP\"", "true") .put("polaris.features.\"LIST_PAGINATION_ENABLED\"", "true") + .put("polaris.features.\"ALLOW_NAMESPACE_CUSTOM_LOCATION\"", "true") .build(); } } From 435d24ad9cc4fa2b42b94b292d4bfa9cad597c9c Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Tue, 26 Aug 2025 09:28:51 -0700 Subject: [PATCH 13/22] address comments --- .../core/storage/aws/S3LocationTest.java | 3 ++- .../service/catalog/iceberg/IcebergCatalog.java | 17 ++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/S3LocationTest.java b/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/S3LocationTest.java index d89720b0da..cbc4425aca 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/S3LocationTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/S3LocationTest.java @@ -47,6 +47,7 @@ public void testPrefixValidationIgnoresScheme(String parentScheme, String childS Assertions.assertThat(loc1.isChildOf(loc2)).isTrue(); StorageLocation loc3 = StorageLocation.of(childScheme + "://bucket/schema1"); - Assertions.assertThat(loc2.equals(loc3)).isFalse(); + Assertions.assertThat(loc2.toString().equals(loc3.toString())).isFalse(); + Assertions.assertThat(loc2.equals(loc3)).isTrue(); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index 797afe373e..f83fd20b45 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -510,7 +510,6 @@ private void createNamespaceInternal( } if (!realmConfig.getConfig( FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { - LOGGER.debug("Validating that namespace {} has a location inside its parent", namespace); validateNamespaceLocation(entity, resolvedParent); } PolarisEntity returnedEntity = @@ -678,7 +677,6 @@ public boolean setProperties(Namespace namespace, Map properties } if (!realmConfig.getConfig( FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { - LOGGER.debug("Validating that namespace {} has a location inside its parent", namespace); validateNamespaceLocation(NamespaceEntity.of(entity), resolvedEntities); } @@ -1101,6 +1099,9 @@ private void validateNamespaceLocation( StorageLocation.ensureTrailingSlash( resolveNamespaceLocation(namespace.asNamespace(), namespace.getPropertiesAsMap()))); PolarisEntity parent = resolvedParent.getResolvedLeafEntity().getEntity(); + Preconditions.checkArgument( + parent.getType().equals(PolarisEntityType.CATALOG) || parent.getType().equals(PolarisEntityType.NAMESPACE), + "Invalid parent type"); if (parent.getType().equals(PolarisEntityType.CATALOG)) { CatalogEntity parentEntity = CatalogEntity.of(parent); LOGGER.debug( @@ -1130,7 +1131,7 @@ private void validateNamespaceLocation( + "which is not enabled. Expected a location in: [" + String.join( ", ", defaultLocations.stream().map(StorageLocation::toString).toList()) - + "]"); + + "]. Got location: " + namespaceLocation + "]"); } } else if (parent.getType().equals(PolarisEntityType.NAMESPACE)) { NamespaceEntity parentEntity = NamespaceEntity.of(parent); @@ -1149,15 +1150,9 @@ private void validateNamespaceLocation( "Namespace " + namespace.getName() + " has a custom location, " - + "which is not enabled. Expected location: " - + defaultLocation); + + "which is not enabled. Expected location: [" + + defaultLocation + "]. Got location: [" + namespaceLocation + "]"); } - } else { - throw new IllegalArgumentException( - "Failed to validate namespace " - + namespace.getName() - + " given parent " - + parent.getName()); } } From 24ea0c35b149da5613af2646a30f619fe99a3f65 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Tue, 26 Aug 2025 09:28:54 -0700 Subject: [PATCH 14/22] autolint --- .../service/catalog/iceberg/IcebergCatalog.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index f83fd20b45..1633ef1a1a 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -1100,7 +1100,8 @@ private void validateNamespaceLocation( resolveNamespaceLocation(namespace.asNamespace(), namespace.getPropertiesAsMap()))); PolarisEntity parent = resolvedParent.getResolvedLeafEntity().getEntity(); Preconditions.checkArgument( - parent.getType().equals(PolarisEntityType.CATALOG) || parent.getType().equals(PolarisEntityType.NAMESPACE), + parent.getType().equals(PolarisEntityType.CATALOG) + || parent.getType().equals(PolarisEntityType.NAMESPACE), "Invalid parent type"); if (parent.getType().equals(PolarisEntityType.CATALOG)) { CatalogEntity parentEntity = CatalogEntity.of(parent); @@ -1131,7 +1132,9 @@ private void validateNamespaceLocation( + "which is not enabled. Expected a location in: [" + String.join( ", ", defaultLocations.stream().map(StorageLocation::toString).toList()) - + "]. Got location: " + namespaceLocation + "]"); + + "]. Got location: " + + namespaceLocation + + "]"); } } else if (parent.getType().equals(PolarisEntityType.NAMESPACE)) { NamespaceEntity parentEntity = NamespaceEntity.of(parent); @@ -1151,7 +1154,10 @@ private void validateNamespaceLocation( + namespace.getName() + " has a custom location, " + "which is not enabled. Expected location: [" - + defaultLocation + "]. Got location: [" + namespaceLocation + "]"); + + defaultLocation + + "]. Got location: [" + + namespaceLocation + + "]"); } } } From 23b331d1ac3731fc47048529e91eb8bdc43e3f41 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Tue, 26 Aug 2025 09:30:18 -0700 Subject: [PATCH 15/22] lint --- .../polaris/service/catalog/iceberg/IcebergCatalog.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index 1633ef1a1a..f797ba1b40 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -1118,10 +1118,9 @@ private void validateNamespaceLocation( parentEntity.getStorageConfigurationInfo().getAllowedLocations().stream() .filter(java.util.Objects::nonNull) .map( - l -> { - return StorageLocation.ensureTrailingSlash( - StorageLocation.ensureTrailingSlash(l) + namespace.getName()); - }) + l -> + StorageLocation.ensureTrailingSlash( + StorageLocation.ensureTrailingSlash(l) + namespace.getName())) .map(StorageLocation::of) .toList(); if (!defaultLocations.contains(namespaceLocation)) { From c0a3d2f8aeffd7679b1ac8e1e7314b231a0fea7f Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Tue, 26 Aug 2025 11:25:56 -0700 Subject: [PATCH 16/22] fix more tests --- .../service/it/test/PolarisRestCatalogIntegrationBase.java | 4 +++- .../apache/polaris/spark/quarkus/it/SparkIntegrationBase.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java index 7a1889b93e..f1271f90a5 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java @@ -173,7 +173,9 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests Date: Tue, 26 Aug 2025 11:25:58 -0700 Subject: [PATCH 17/22] autolint --- .../service/it/test/PolarisRestCatalogIntegrationBase.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java index f1271f90a5..ad72ad5ebc 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java @@ -174,8 +174,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests Date: Tue, 26 Aug 2025 11:47:09 -0700 Subject: [PATCH 18/22] clarify comment --- .../org/apache/polaris/core/config/FeatureConfiguration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index 1fb2fb28a0..ed14a4e0a8 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -104,7 +104,8 @@ public static void enforceFeatureEnabledOrThrow( PolarisConfiguration.builder() .key("ALLOW_NAMESPACE_CUSTOM_LOCATION") .catalogConfig("polaris.config.namespace-custom-location.enabled") - .description("If set to true, allow namespaces with custom locations.") + .description("If set to true, allow namespaces with arbitrary locations, even those which escape " + + "from the parent/allowed locations. This may impact credential vending when enabled.") .defaultValue(false) .buildFeatureConfiguration(); From 414fc6b7d830ff2a8e0b7b01825a010afdd59090 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Tue, 26 Aug 2025 11:47:17 -0700 Subject: [PATCH 19/22] autolint --- .../org/apache/polaris/core/config/FeatureConfiguration.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index ed14a4e0a8..6220cbeff2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -104,8 +104,9 @@ public static void enforceFeatureEnabledOrThrow( PolarisConfiguration.builder() .key("ALLOW_NAMESPACE_CUSTOM_LOCATION") .catalogConfig("polaris.config.namespace-custom-location.enabled") - .description("If set to true, allow namespaces with arbitrary locations, even those which escape " + - "from the parent/allowed locations. This may impact credential vending when enabled.") + .description( + "If set to true, allow namespaces with arbitrary locations, even those which escape " + + "from the parent/allowed locations. This may impact credential vending when enabled.") .defaultValue(false) .buildFeatureConfiguration(); From a43c7aaed4e50caadcf2b8c0ca2dd790a655f43c Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Fri, 29 Aug 2025 11:58:35 -0700 Subject: [PATCH 20/22] change wording --- .../it/test/PolarisApplicationIntegrationTest.java | 4 ++-- .../core/config/BehaviorChangeConfiguration.java | 10 ++++++++++ .../polaris/core/config/FeatureConfiguration.java | 10 ---------- .../service/storage/PolarisConfigurationStoreTest.java | 2 +- .../service/catalog/iceberg/IcebergCatalog.java | 4 ++-- .../polaris/service/admin/PolarisAuthzTestBase.java | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java index e92bb2ffa8..a7e21427ea 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java @@ -69,7 +69,7 @@ import org.apache.polaris.core.admin.model.PolarisCatalog; import org.apache.polaris.core.admin.model.PrincipalRole; import org.apache.polaris.core.admin.model.StorageConfigInfo; -import org.apache.polaris.core.config.FeatureConfiguration; +import org.apache.polaris.core.config.BehaviorChangeConfiguration; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.service.it.env.ClientPrincipal; @@ -682,7 +682,7 @@ public void testNamespaceOutsideCatalog(boolean allowNamespaceLocationEscape) th .build(), catalogLocation, ImmutableMap.of( - FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION.catalogConfig(), + BehaviorChangeConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION.catalogConfig(), String.valueOf(allowNamespaceLocationEscape))); try (RESTSessionCatalog sessionCatalog = newSessionCatalog(catalogName)) { SessionCatalog.SessionContext sessionContext = SessionCatalog.SessionContext.createEmpty(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java index db5176e7c3..c8d1ff8550 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java @@ -74,4 +74,14 @@ protected BehaviorChangeConfiguration( + " the committed metadata again.") .defaultValue(true) .buildBehaviorChangeConfiguration(); + + public static final BehaviorChangeConfiguration ALLOW_NAMESPACE_CUSTOM_LOCATION = + PolarisConfiguration.builder() + .key("ALLOW_NAMESPACE_CUSTOM_LOCATION") + .catalogConfig("polaris.config.namespace-custom-location.enabled") + .description( + "If set to true, allow namespaces with completely arbitrary locations. This should not affect" + + " credential vending.") + .defaultValue(false) + .buildBehaviorChangeConfiguration(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index 6220cbeff2..e01e065a1c 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -100,16 +100,6 @@ public static void enforceFeatureEnabledOrThrow( .defaultValue(false) .buildFeatureConfiguration(); - public static final FeatureConfiguration ALLOW_NAMESPACE_CUSTOM_LOCATION = - PolarisConfiguration.builder() - .key("ALLOW_NAMESPACE_CUSTOM_LOCATION") - .catalogConfig("polaris.config.namespace-custom-location.enabled") - .description( - "If set to true, allow namespaces with arbitrary locations, even those which escape " - + "from the parent/allowed locations. This may impact credential vending when enabled.") - .defaultValue(false) - .buildFeatureConfiguration(); - public static final FeatureConfiguration ALLOW_EXTERNAL_METADATA_FILE_LOCATION = PolarisConfiguration.builder() .key("ALLOW_EXTERNAL_METADATA_FILE_LOCATION") diff --git a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java index 612b8716bf..53ec550082 100644 --- a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java @@ -125,7 +125,7 @@ public T consumeConfiguration( } @Test - public void testBehaviorAndFeatureConfigs() { + public void gtestBehaviorAndFeatureConfigs() { PolarisConfigurationConsumer consumer = new PolarisConfigurationConsumer(testRealmContext, new PolarisConfigurationStore() {}); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index f797ba1b40..48670c392f 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -509,7 +509,7 @@ private void createNamespaceInternal( LOGGER.debug("Skipping location overlap validation for namespace '{}'", namespace); } if (!realmConfig.getConfig( - FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { + BehaviorChangeConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { validateNamespaceLocation(entity, resolvedParent); } PolarisEntity returnedEntity = @@ -676,7 +676,7 @@ public boolean setProperties(Namespace namespace, Map properties LOGGER.debug("Skipping location overlap validation for namespace '{}'", namespace); } if (!realmConfig.getConfig( - FeatureConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { + BehaviorChangeConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { validateNamespaceLocation(NamespaceEntity.of(entity), resolvedEntities); } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index 5865cf8785..e9a27c923c 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -119,7 +119,7 @@ public Map getConfigOverrides() { "polaris.features.\"ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING\"", "true") .put("polaris.features.\"DROP_WITH_PURGE_ENABLED\"", "true") - .put("polaris.features.\"ALLOW_NAMESPACE_CUSTOM_LOCATION\"", "true") + .put("polaris.behavior-changes.\"ALLOW_NAMESPACE_CUSTOM_LOCATION\"", "true") .build(); } } From 654882934121cf8d47f62ced3e861637e887189c Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Fri, 29 Aug 2025 11:59:03 -0700 Subject: [PATCH 21/22] pull main --- .../polaris/service/storage/PolarisConfigurationStoreTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java index 53ec550082..612b8716bf 100644 --- a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisConfigurationStoreTest.java @@ -125,7 +125,7 @@ public T consumeConfiguration( } @Test - public void gtestBehaviorAndFeatureConfigs() { + public void testBehaviorAndFeatureConfigs() { PolarisConfigurationConsumer consumer = new PolarisConfigurationConsumer(testRealmContext, new PolarisConfigurationStore() {}); From d623f6bb60a6ce75c8322f0166e955ac6d13cb11 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Fri, 29 Aug 2025 12:10:39 -0700 Subject: [PATCH 22/22] fix for catalog config --- .../core/config/BehaviorChangeConfiguration.java | 14 +++++++------- .../polaris/core/config/PolarisConfiguration.java | 4 ---- .../service/catalog/iceberg/IcebergCatalog.java | 4 +++- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java index c8d1ff8550..3caded0dcf 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java @@ -22,11 +22,11 @@ /** * Internal configuration flags for non-feature behavior changes in Polaris. These flags control - * subtle behavior adjustments and bug fixes, not user-facing catalog settings. They are intended - * for internal use only, are inherently unstable, and may be removed at any time. When introducing - * a new flag, consider the trade-off between maintenance burden and the risk of an unguarded - * behavior change. Flags here are generally short-lived and should either be removed or promoted to - * stable feature flags before the next release. + * subtle behavior adjustments and bug fixes, not user-facing settings. They are intended for + * internal use only, are inherently unstable, and may be removed at any time. When introducing a + * new flag, consider the trade-off between maintenance burden and the risk of an unguarded behavior + * change. Flags here are generally short-lived and should either be removed or promoted to stable + * feature flags before the next release. * * @param The type of the configuration */ @@ -80,8 +80,8 @@ protected BehaviorChangeConfiguration( .key("ALLOW_NAMESPACE_CUSTOM_LOCATION") .catalogConfig("polaris.config.namespace-custom-location.enabled") .description( - "If set to true, allow namespaces with completely arbitrary locations. This should not affect" + - " credential vending.") + "If set to true, allow namespaces with completely arbitrary locations. This should not affect" + + " credential vending.") .defaultValue(false) .buildBehaviorChangeConfiguration(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java index f9cf8192f7..0750f70265 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/PolarisConfiguration.java @@ -209,10 +209,6 @@ public FeatureConfiguration buildFeatureConfiguration() { public BehaviorChangeConfiguration buildBehaviorChangeConfiguration() { validateOrThrow(); - if (catalogConfig.isPresent() || catalogConfigUnsafe.isPresent()) { - throw new IllegalArgumentException( - "catalog configs are not valid for behavior change configs"); - } BehaviorChangeConfiguration config = new BehaviorChangeConfiguration<>( key, description, defaultValue, catalogConfig, catalogConfigUnsafe); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index 0eb3e04b1b..2b98bd613e 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -677,7 +677,9 @@ public boolean setProperties(Namespace namespace, Map properties } if (!realmConfig.getConfig( BehaviorChangeConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { - validateNamespaceLocation(NamespaceEntity.of(entity), resolvedEntities); + if (properties.containsKey(PolarisEntityConstants.ENTITY_BASE_LOCATION)) { + validateNamespaceLocation(NamespaceEntity.of(entity), resolvedEntities); + } } List parentPath = resolvedEntities.getRawFullPath();