This is an automated email from the ASF dual-hosted git repository.
emaynard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new da23e669d Introduce reserved-properties setting; reserve "polaris." by
default (#1417)
da23e669d is described below
commit da23e669dcceed15d328d5e1bf324e1a641023cd
Author: Eric Maynard <[email protected]>
AuthorDate: Fri May 9 14:22:47 2025 -0700
Introduce reserved-properties setting; reserve "polaris." by default (#1417)
* initial commit
* initial commit
* try to test
* quarkus fixes
* fix a bunch of callsites
* Start applying changes
* autolint
* chase todos
* autolint
* bugfix
* stable
* add one test
* stable with more tests
* autolint
* more tests
* autolint
* stable tests
* clean up
* oops
* stabilize on main
* autolint
* more changes per review
---
.../PolarisManagementServiceIntegrationTest.java | 124 +++++++++++++++++++
.../it/test/PolarisRestCatalogIntegrationTest.java | 110 ++++++++++++++++-
.../quarkus/config/QuarkusReservedProperties.java | 31 +++++
.../admin/PolarisAdminServiceAuthzTest.java | 3 +-
.../quarkus/admin/PolarisAuthzTestBase.java | 5 +-
.../quarkus/catalog/GenericTableCatalogTest.java | 8 +-
.../catalog/IcebergCatalogHandlerAuthzTest.java | 9 +-
.../quarkus/catalog/IcebergCatalogTest.java | 51 +++++++-
.../quarkus/catalog/IcebergCatalogViewTest.java | 7 +-
.../service/quarkus/catalog/PolicyCatalogTest.java | 8 +-
.../polaris/service/admin/PolarisAdminService.java | 27 +++-
.../polaris/service/admin/PolarisServiceImpl.java | 41 ++++--
.../generic/GenericTableCatalogAdapter.java | 8 +-
.../catalog/iceberg/IcebergCatalogAdapter.java | 68 ++++++++--
.../catalog/iceberg/IcebergCatalogHandler.java | 34 +++--
.../polaris/service/config/ReservedProperties.java | 137 +++++++++++++++++++++
.../org/apache/polaris/service/TestServices.java | 9 +-
17 files changed, 628 insertions(+), 52 deletions(-)
diff --git
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
index 573623021..1b3899c31 100644
---
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
+++
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
@@ -84,6 +84,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
import org.testcontainers.shaded.org.awaitility.Awaitility;
/**
@@ -2102,6 +2103,129 @@ public class PolarisManagementServiceIntegrationTest {
}
}
+ @Test
+ public void testCreateAndUpdateCatalogRoleWithReservedProperties() {
+ String catalogName = client.newEntityName("mycatalog1");
+ Catalog catalog =
+ PolarisCatalog.builder()
+ .setType(Catalog.TypeEnum.INTERNAL)
+ .setName(catalogName)
+ .setProperties(new
CatalogProperties("s3://required/base/location"))
+ .setStorageConfigInfo(
+ new AwsStorageConfigInfo(
+ "arn:aws:iam::012345678901:role/jdoe",
StorageConfigInfo.StorageTypeEnum.S3))
+ .build();
+ managementApi.createCatalog(catalog);
+
+ CatalogRole badCatalogRole =
+ new CatalogRole("mycatalogrole", Map.of("polaris.reserved", "foo"),
0L, 0L, 1);
+ try (Response response =
+ managementApi
+ .request("v1/catalogs/{cat}/catalog-roles", Map.of("cat",
catalogName))
+ .post(Entity.json(new CreateCatalogRoleRequest(badCatalogRole)))) {
+ assertThat(response)
+ .returns(Response.Status.BAD_REQUEST.getStatusCode(),
Response::getStatus);
+ }
+
+ CatalogRole okCatalogRole = new CatalogRole("mycatalogrole", Map.of("foo",
"bar"), 0L, 0L, 1);
+ try (Response response =
+ managementApi
+ .request("v1/catalogs/{cat}/catalog-roles", Map.of("cat",
catalogName))
+ .post(Entity.json(new CreateCatalogRoleRequest(okCatalogRole)))) {
+ assertThat(response).returns(Response.Status.CREATED.getStatusCode(),
Response::getStatus);
+ }
+
+ UpdateCatalogRoleRequest updateRequest =
+ new UpdateCatalogRoleRequest(
+ okCatalogRole.getEntityVersion(), Map.of("polaris.reserved",
"true"));
+ try (Response response =
+ managementApi
+ .request("v1/catalogs/{cat}/catalog-roles/mycatalogrole",
Map.of("cat", catalogName))
+ .put(Entity.json(updateRequest))) {
+ assertThat(response)
+ .returns(Response.Status.BAD_REQUEST.getStatusCode(),
Response::getStatus);
+ }
+ }
+
+ @Test
+ public void testCreateAndUpdatePrincipalRoleWithReservedProperties() {
+ String principal =
"testCreateAndUpdatePrincipalRoleWithReservedProperties";
+ managementApi.createPrincipal(principal);
+
+ PrincipalRole badPrincipalRole =
+ new PrincipalRole(
+ client.newEntityName("myprincipalrole"),
Map.of("polaris.reserved", "foo"), 0L, 0L, 1);
+ try (Response response =
+ managementApi
+ .request("v1/principal-roles")
+ .post(Entity.json(new
CreatePrincipalRoleRequest(badPrincipalRole)))) {
+ assertThat(response)
+ .returns(Response.Status.BAD_REQUEST.getStatusCode(),
Response::getStatus);
+ }
+
+ PrincipalRole goodPrincipalRole =
+ new PrincipalRole(
+ client.newEntityName("myprincipalrole"), Map.of("not.reserved",
"foo"), 0L, 0L, 1);
+ try (Response response =
+ managementApi
+ .request("v1/principal-roles")
+ .post(Entity.json(new
CreatePrincipalRoleRequest(goodPrincipalRole)))) {
+ assertThat(response).returns(Response.Status.CREATED.getStatusCode(),
Response::getStatus);
+ }
+
+ UpdatePrincipalRoleRequest badUpdate =
+ new UpdatePrincipalRoleRequest(
+ goodPrincipalRole.getEntityVersion(),
ImmutableMap.of("polaris.reserved", "true"));
+ try (Response response =
+ managementApi
+ .request("v1/principal-roles/{pr}", Map.of("pr",
goodPrincipalRole.getName()))
+ .put(Entity.json(badUpdate))) {
+ assertThat(response)
+ .returns(Response.Status.BAD_REQUEST.getStatusCode(),
Response::getStatus);
+ }
+
+ managementApi.deletePrincipalRole(goodPrincipalRole);
+ managementApi.deletePrincipal(principal);
+ }
+
+ @Test
+ public void testCreateAndUpdatePrincipalWithReservedProperties() {
+ String principal = "testCreateAndUpdatePrincipalWithReservedProperties";
+
+ Principal badPrincipal =
+ new Principal(
+ principal, "clientId", ImmutableMap.of("polaris.reserved",
"true"), 0L, 0L, 1);
+ try (Response response =
+ managementApi
+ .request("v1/principals")
+ .post(Entity.json(new CreatePrincipalRequest(badPrincipal,
false)))) {
+ assertThat(response)
+ .returns(Response.Status.BAD_REQUEST.getStatusCode(),
Response::getStatus);
+ }
+
+ Principal goodPrincipal =
+ new Principal(principal, "clientId", ImmutableMap.of("not.reserved",
"true"), 0L, 0L, 1);
+ try (Response response =
+ managementApi
+ .request("v1/principals")
+ .post(Entity.json(new CreatePrincipalRequest(goodPrincipal,
false)))) {
+ assertThat(response).returns(Response.Status.CREATED.getStatusCode(),
Response::getStatus);
+ }
+
+ UpdatePrincipalRequest badUpdate =
+ new UpdatePrincipalRequest(
+ goodPrincipal.getEntityVersion(),
ImmutableMap.of("polaris.reserved", "true"));
+ try (Response response =
+ managementApi
+ .request("v1/principals/{p}", Map.of("p", goodPrincipal.getName()))
+ .put(Entity.json(badUpdate))) {
+ assertThat(response)
+ .returns(Response.Status.BAD_REQUEST.getStatusCode(),
Response::getStatus);
+ }
+
+ managementApi.deletePrincipal(principal);
+ }
+
public static JWTCreator.Builder defaultJwt() {
Instant now = Instant.now();
return JWT.create()
diff --git
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
index e55ddf1c5..ae42c5ae5 100644
---
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
+++
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationTest.java
@@ -19,13 +19,13 @@
package org.apache.polaris.service.it.test;
import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
-import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
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 com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
@@ -97,6 +97,7 @@ import org.apache.polaris.service.it.env.ManagementApi;
import org.apache.polaris.service.it.env.PolarisApiEndpoints;
import org.apache.polaris.service.it.env.PolarisClient;
import org.apache.polaris.service.it.ext.PolarisIntegrationTestExtension;
+import org.apache.polaris.service.types.CreateGenericTableRequest;
import org.apache.polaris.service.types.GenericTable;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.Assumptions;
@@ -1409,7 +1410,7 @@ public class PolarisRestCatalogIntegrationTest extends
CatalogTests<RESTCatalog>
assertThatCode(() -> catalogApi.loadTable(currentCatalogName,
tableIdentifier, "not-real"))
.isInstanceOf(RESTException.class)
.hasMessageContaining("Unrecognized snapshots")
- .hasMessageContaining("code=" + BAD_REQUEST.getStatusCode());
+ .hasMessageContaining("code=" +
Response.Status.BAD_REQUEST.getStatusCode());
} finally {
genericTableApi.purge(currentCatalogName, namespace);
}
@@ -1450,4 +1451,109 @@ public class PolarisRestCatalogIntegrationTest extends
CatalogTests<RESTCatalog>
genericTableApi.purge(currentCatalogName, namespace);
}
}
+
+ @Test
+ public void testCreateGenericTableWithReservedProperty() {
+ Namespace namespace = Namespace.of("ns1");
+ restCatalog.createNamespace(namespace);
+ TableIdentifier tableIdentifier = TableIdentifier.of(namespace, "tbl1");
+
+ String ns = RESTUtil.encodeNamespace(tableIdentifier.namespace());
+ try (Response res =
+ genericTableApi
+ .request(
+ "polaris/v1/{cat}/namespaces/{ns}/generic-tables/",
+ Map.of("cat", currentCatalogName, "ns", ns))
+ .post(
+ Entity.json(
+ new CreateGenericTableRequest(
+ tableIdentifier.name(),
+ "format",
+ "doc",
+ Map.of("polaris.reserved", "true"))))) {
+
Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode());
+ Assertions.assertThat(res.readEntity(String.class)).contains("reserved
prefix");
+ }
+
+ genericTableApi.purge(currentCatalogName, namespace);
+ }
+
+ @Test
+ public void testCreateNamespaceWithReservedProperty() {
+ Namespace namespace = Namespace.of("ns1");
+ assertThatCode(
+ () -> {
+ restCatalog.createNamespace(namespace,
ImmutableMap.of("polaris.reserved", "true"));
+ })
+ .isInstanceOf(org.apache.iceberg.exceptions.BadRequestException.class)
+ .hasMessageContaining("reserved prefix");
+ }
+
+ @Test
+ public void testUpdateNamespaceWithReservedProperty() {
+ Namespace namespace = Namespace.of("ns1");
+ restCatalog.createNamespace(namespace, ImmutableMap.of("a", "b"));
+ restCatalog.setProperties(namespace, ImmutableMap.of("c", "d"));
+ Assertions.assertThatCode(
+ () -> {
+ restCatalog.setProperties(namespace,
ImmutableMap.of("polaris.reserved", "true"));
+ })
+ .isInstanceOf(org.apache.iceberg.exceptions.BadRequestException.class)
+ .hasMessageContaining("reserved prefix");
+ genericTableApi.purge(currentCatalogName, namespace);
+ }
+
+ @Test
+ public void testRemoveReservedPropertyFromNamespace() {
+ Namespace namespace = Namespace.of("ns1");
+ restCatalog.createNamespace(namespace, ImmutableMap.of("a", "b"));
+ restCatalog.removeProperties(namespace, Sets.newHashSet("a"));
+ Assertions.assertThatCode(
+ () -> {
+ restCatalog.removeProperties(namespace,
Sets.newHashSet("polaris.reserved"));
+ })
+ .isInstanceOf(org.apache.iceberg.exceptions.BadRequestException.class)
+ .hasMessageContaining("reserved prefix");
+ genericTableApi.purge(currentCatalogName, namespace);
+ }
+
+ @Test
+ public void testCreateTableWithReservedProperty() {
+ Namespace namespace = Namespace.of("ns1");
+ restCatalog.createNamespace(namespace);
+ TableIdentifier identifier = TableIdentifier.of(namespace, "t1");
+ Assertions.assertThatCode(
+ () -> {
+ restCatalog.createTable(
+ identifier,
+ SCHEMA,
+ PartitionSpec.unpartitioned(),
+ ImmutableMap.of("polaris.reserved", ""));
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("reserved prefix");
+ genericTableApi.purge(currentCatalogName, namespace);
+ }
+
+ @Test
+ public void testUpdateTableWithReservedProperty() {
+ Namespace namespace = Namespace.of("ns1");
+ restCatalog.createNamespace(namespace);
+ TableIdentifier identifier = TableIdentifier.of(namespace, "t1");
+ restCatalog.createTable(identifier, SCHEMA);
+ Assertions.assertThatCode(
+ () -> {
+ var txn =
+ restCatalog.newReplaceTableTransaction(
+ identifier,
+ SCHEMA,
+ PartitionSpec.unpartitioned(),
+ ImmutableMap.of("polaris.reserved", ""),
+ false);
+ txn.commitTransaction();
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("reserved prefix");
+ genericTableApi.purge(currentCatalogName, namespace);
+ }
}
diff --git
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusReservedProperties.java
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusReservedProperties.java
new file mode 100644
index 000000000..4d45a456e
--- /dev/null
+++
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusReservedProperties.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.quarkus.config;
+
+import io.smallrye.config.ConfigMapping;
+import java.util.List;
+import org.apache.polaris.service.config.ReservedProperties;
+
+@ConfigMapping(prefix = "polaris.reserved-properties")
+public interface QuarkusReservedProperties extends ReservedProperties {
+ @Override
+ default List<String> prefixes() {
+ return List.of("polaris.");
+ }
+}
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
index 55b2d9f34..f5a6460b1 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
@@ -55,7 +55,8 @@ public class PolarisAdminServiceAuthzTest extends
PolarisAuthzTestBase {
metaStoreManager,
userSecretsManager,
securityContext(authenticatedPrincipal, activatedPrincipalRoles),
- polarisAuthorizer);
+ polarisAuthorizer,
+ reservedProperties);
}
private void doTestSufficientPrivileges(
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
index 5761812f4..bb32d9915 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java
@@ -86,6 +86,7 @@ import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.catalog.policy.PolicyCatalog;
import org.apache.polaris.service.config.DefaultConfigurationStore;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.context.CallContextCatalogFactory;
import org.apache.polaris.service.context.PolarisCallContextCatalogFactory;
import org.apache.polaris.service.events.PolarisEventListener;
@@ -181,6 +182,7 @@ public abstract class PolarisAuthzTestBase {
Map.of(
FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING.key,
true)));
+ protected final ReservedProperties reservedProperties =
ReservedProperties.NONE;
@Inject protected MetaStoreManagerFactory managerFactory;
@Inject protected RealmEntityManagerFactory realmEntityManagerFactory;
@@ -264,7 +266,8 @@ public abstract class PolarisAuthzTestBase {
metaStoreManager,
userSecretsManager,
securityContext(authenticatedRoot, Set.of()),
- polarisAuthorizer);
+ polarisAuthorizer,
+ reservedProperties);
String storageLocation = "file:///tmp/authz";
FileStorageConfigInfo storageConfigModel =
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/GenericTableCatalogTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/GenericTableCatalogTest.java
index 0b6af6e8f..a296feca6 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/GenericTableCatalogTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/GenericTableCatalogTest.java
@@ -81,6 +81,7 @@ import
org.apache.polaris.service.catalog.iceberg.IcebergCatalog;
import org.apache.polaris.service.catalog.io.DefaultFileIOFactory;
import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.events.NoOpPolarisEventListener;
import
org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl;
import org.apache.polaris.service.task.TaskExecutor;
@@ -141,6 +142,7 @@ public class GenericTableCatalogTest {
private AuthenticatedPolarisPrincipal authenticatedRoot;
private PolarisEntity catalogEntity;
private SecurityContext securityContext;
+ private ReservedProperties reservedProperties;
protected static final Schema SCHEMA =
new Schema(
@@ -195,6 +197,9 @@ public class GenericTableCatalogTest {
securityContext = Mockito.mock(SecurityContext.class);
when(securityContext.getUserPrincipal()).thenReturn(authenticatedRoot);
when(securityContext.isUserInRole(isA(String.class))).thenReturn(true);
+
+ reservedProperties = ReservedProperties.NONE;
+
adminService =
new PolarisAdminService(
callContext,
@@ -202,7 +207,8 @@ public class GenericTableCatalogTest {
metaStoreManager,
userSecretsManager,
securityContext,
- new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}));
+ new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}),
+ reservedProperties);
String storageLocation = "s3://my-bucket/path/to/data";
storageConfigModel =
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogHandlerAuthzTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogHandlerAuthzTest.java
index e74b7d641..6a95f7cdb 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogHandlerAuthzTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogHandlerAuthzTest.java
@@ -115,7 +115,8 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
securityContext(authenticatedPrincipal, activatedPrincipalRoles),
factory,
catalogName,
- polarisAuthorizer);
+ polarisAuthorizer,
+ reservedProperties);
}
/**
@@ -254,7 +255,8 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
securityContext(authenticatedPrincipal, Set.of(PRINCIPAL_ROLE1,
PRINCIPAL_ROLE2)),
callContextCatalogFactory,
CATALOG_NAME,
- polarisAuthorizer);
+ polarisAuthorizer,
+ reservedProperties);
// a variety of actions are all disallowed because the principal's
credentials must be rotated
doTestInsufficientPrivileges(
@@ -287,7 +289,8 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
securityContext(authenticatedPrincipal1, Set.of(PRINCIPAL_ROLE1,
PRINCIPAL_ROLE2)),
callContextCatalogFactory,
CATALOG_NAME,
- polarisAuthorizer);
+ polarisAuthorizer,
+ reservedProperties);
doTestSufficientPrivilegeSets(
List.of(Set.of(PolarisPrivilege.NAMESPACE_LIST)),
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogTest.java
index 5837f8822..a2d50d9a4 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogTest.java
@@ -75,6 +75,7 @@ import org.apache.polaris.core.PolarisDiagnostics;
import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
import org.apache.polaris.core.admin.model.CreateCatalogRequest;
import org.apache.polaris.core.admin.model.StorageConfigInfo;
+import org.apache.polaris.core.admin.model.UpdateCatalogRequest;
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
import org.apache.polaris.core.config.FeatureConfiguration;
@@ -116,6 +117,7 @@ import
org.apache.polaris.service.catalog.io.ExceptionMappingFileIO;
import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.catalog.io.MeasuredFileIOFactory;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.events.AfterTableCommitedEvent;
import org.apache.polaris.service.events.AfterTableRefreshedEvent;
import org.apache.polaris.service.events.BeforeTableCommitedEvent;
@@ -124,6 +126,7 @@ import
org.apache.polaris.service.events.PolarisEventListener;
import org.apache.polaris.service.events.TestPolarisEventListener;
import org.apache.polaris.service.exception.FakeAzureHttpResponse;
import org.apache.polaris.service.exception.IcebergExceptionMapper;
+import org.apache.polaris.service.quarkus.config.QuarkusReservedProperties;
import org.apache.polaris.service.quarkus.test.TestData;
import
org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl;
import org.apache.polaris.service.task.TableCleanupTaskHandler;
@@ -218,6 +221,7 @@ public abstract class IcebergCatalogTest extends
CatalogTests<IcebergCatalog> {
private PolarisEntity catalogEntity;
private SecurityContext securityContext;
private TestPolarisEventListener testPolarisEventListener;
+ private ReservedProperties reservedProperties;
@BeforeAll
public static void setUpMocks() {
@@ -270,6 +274,9 @@ public abstract class IcebergCatalogTest extends
CatalogTests<IcebergCatalog> {
securityContext = Mockito.mock(SecurityContext.class);
when(securityContext.getUserPrincipal()).thenReturn(authenticatedRoot);
when(securityContext.isUserInRole(isA(String.class))).thenReturn(true);
+
+ reservedProperties = new QuarkusReservedProperties() {};
+
adminService =
new PolarisAdminService(
callContext,
@@ -277,7 +284,8 @@ public abstract class IcebergCatalogTest extends
CatalogTests<IcebergCatalog> {
metaStoreManager,
userSecretsManager,
securityContext,
- new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}));
+ new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}),
+ reservedProperties);
String storageLocation = "s3://my-bucket/path/to/data";
AwsStorageConfigInfo storageConfigModel =
@@ -1896,6 +1904,47 @@ public abstract class IcebergCatalogTest extends
CatalogTests<IcebergCatalog> {
.hasMessageContaining("conflict_table");
}
+ @Test
+ public void createCatalogWithReservedProperty() {
+ Assertions.assertThatCode(
+ () -> {
+ adminService.createCatalog(
+ new CreateCatalogRequest(
+ new CatalogEntity.Builder()
+ .setDefaultBaseLocation("file://")
+ .setName("createCatalogWithReservedProperty")
+ .setProperties(ImmutableMap.of("polaris.reserved",
"true"))
+ .build()
+ .asCatalog()));
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("reserved prefix");
+ }
+
+ @Test
+ public void updateCatalogWithReservedProperty() {
+ adminService.createCatalog(
+ new CreateCatalogRequest(
+ new CatalogEntity.Builder()
+ .setDefaultBaseLocation("file://")
+ .setName("updateCatalogWithReservedProperty")
+ .setProperties(ImmutableMap.of("a", "b"))
+ .build()
+ .asCatalog()));
+ Assertions.assertThatCode(
+ () -> {
+ adminService.updateCatalog(
+ "updateCatalogWithReservedProperty",
+ UpdateCatalogRequest.builder()
+ .setCurrentEntityVersion(1)
+ .setProperties(ImmutableMap.of("polaris.reserved",
"true"))
+ .build());
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("reserved prefix");
+ adminService.deleteCatalog("updateCatalogWithReservedProperty");
+ }
+
@ParameterizedTest
@ValueSource(booleans = {false, true})
public void testTableOperationsDoesNotRefreshAfterCommit(boolean
updateMetadataOnCommit) {
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogViewTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogViewTest.java
index c66e88b37..a7bdec4f1 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogViewTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/IcebergCatalogViewTest.java
@@ -68,6 +68,7 @@ import
org.apache.polaris.service.catalog.iceberg.IcebergCatalog;
import org.apache.polaris.service.catalog.io.DefaultFileIOFactory;
import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.events.AfterViewCommitedEvent;
import org.apache.polaris.service.events.AfterViewRefreshedEvent;
import org.apache.polaris.service.events.BeforeViewCommitedEvent;
@@ -197,6 +198,9 @@ public class IcebergCatalogViewTest extends
ViewCatalogTests<IcebergCatalog> {
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
when(securityContext.getUserPrincipal()).thenReturn(authenticatedRoot);
when(securityContext.isUserInRole(Mockito.anyString())).thenReturn(true);
+
+ ReservedProperties reservedProperties = ReservedProperties.NONE;
+
PolarisAdminService adminService =
new PolarisAdminService(
callContext,
@@ -204,7 +208,8 @@ public class IcebergCatalogViewTest extends
ViewCatalogTests<IcebergCatalog> {
metaStoreManager,
userSecretsManager,
securityContext,
- new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}));
+ new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}),
+ reservedProperties);
adminService.createCatalog(
new CreateCatalogRequest(
new CatalogEntity.Builder()
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
index 68e2c35e0..0ea91bb19 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java
@@ -94,6 +94,7 @@ import
org.apache.polaris.service.catalog.io.DefaultFileIOFactory;
import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.catalog.policy.PolicyCatalog;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.events.NoOpPolarisEventListener;
import
org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl;
import org.apache.polaris.service.task.TaskExecutor;
@@ -170,6 +171,7 @@ public class PolicyCatalogTest {
private AuthenticatedPolarisPrincipal authenticatedRoot;
private PolarisEntity catalogEntity;
private SecurityContext securityContext;
+ private ReservedProperties reservedProperties;
@BeforeAll
public static void setUpMocks() {
@@ -219,6 +221,9 @@ public class PolicyCatalogTest {
securityContext = Mockito.mock(SecurityContext.class);
when(securityContext.getUserPrincipal()).thenReturn(authenticatedRoot);
when(securityContext.isUserInRole(isA(String.class))).thenReturn(true);
+
+ reservedProperties = ReservedProperties.NONE;
+
adminService =
new PolarisAdminService(
callContext,
@@ -226,7 +231,8 @@ public class PolicyCatalogTest {
metaStoreManager,
userSecretsManager,
securityContext,
- new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}));
+ new PolarisAuthorizerImpl(new PolarisConfigurationStore() {}),
+ reservedProperties);
String storageLocation = "s3://my-bucket/path/to/data";
storageConfigModel =
diff --git
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
index ef6a754e9..c51c9a8eb 100644
---
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
+++
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
@@ -106,6 +106,7 @@ import org.apache.polaris.core.storage.StorageLocation;
import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
import org.apache.polaris.core.storage.azure.AzureStorageConfigurationInfo;
import org.apache.polaris.service.catalog.common.CatalogHandler;
+import org.apache.polaris.service.config.ReservedProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -129,6 +130,7 @@ public class PolarisAdminService {
private final PolarisAuthorizer authorizer;
private final PolarisMetaStoreManager metaStoreManager;
private final UserSecretsManager userSecretsManager;
+ private final ReservedProperties reservedProperties;
// Initialized in the authorize methods.
private PolarisResolutionManifest resolutionManifest = null;
@@ -139,7 +141,8 @@ public class PolarisAdminService {
@NotNull PolarisMetaStoreManager metaStoreManager,
@NotNull UserSecretsManager userSecretsManager,
@NotNull SecurityContext securityContext,
- @NotNull PolarisAuthorizer authorizer) {
+ @NotNull PolarisAuthorizer authorizer,
+ @NotNull ReservedProperties reservedProperties) {
this.callContext = callContext;
this.entityManager = entityManager;
this.metaStoreManager = metaStoreManager;
@@ -156,6 +159,7 @@ public class PolarisAdminService {
(AuthenticatedPolarisPrincipal) securityContext.getUserPrincipal();
this.authorizer = authorizer;
this.userSecretsManager = userSecretsManager;
+ this.reservedProperties = reservedProperties;
}
private PolarisCallContext getCurrentPolarisContext() {
@@ -682,6 +686,7 @@ public class PolarisAdminService {
new CatalogEntity.Builder(entity)
.setId(metaStoreManager.generateNewEntityId(getCurrentPolarisContext()).getId())
.setCreateTimestamp(System.currentTimeMillis())
+
.setProperties(reservedProperties.removeReservedProperties(entity.getPropertiesAsMap()))
.build();
if (requiresSecretReferenceExtraction(catalogRequest)) {
@@ -823,7 +828,10 @@ public class PolarisAdminService {
CatalogEntity.Builder updateBuilder = new
CatalogEntity.Builder(currentCatalogEntity);
String defaultBaseLocation = currentCatalogEntity.getDefaultBaseLocation();
if (updateRequest.getProperties() != null) {
- updateBuilder.setProperties(updateRequest.getProperties());
+ Map<String, String> updateProperties =
+ reservedProperties.removeReservedPropertiesFromUpdate(
+ currentCatalogEntity.getPropertiesAsMap(),
updateRequest.getProperties());
+ updateBuilder.setProperties(updateProperties);
String newDefaultBaseLocation =
updateRequest.getProperties().get(CatalogEntity.DEFAULT_BASE_LOCATION_KEY);
// Since defaultBaseLocation is a required field during construction of
a catalog, and the
@@ -972,7 +980,10 @@ public class PolarisAdminService {
PrincipalEntity.Builder updateBuilder = new
PrincipalEntity.Builder(currentPrincipalEntity);
if (updateRequest.getProperties() != null) {
- updateBuilder.setProperties(updateRequest.getProperties());
+ Map<String, String> updateProperties =
+ reservedProperties.removeReservedPropertiesFromUpdate(
+ currentPrincipalEntity.getPropertiesAsMap(),
updateRequest.getProperties());
+ updateBuilder.setProperties(updateProperties);
}
PrincipalEntity updatedEntity = updateBuilder.build();
PrincipalEntity returnedEntity =
@@ -1138,7 +1149,10 @@ public class PolarisAdminService {
PrincipalRoleEntity.Builder updateBuilder =
new PrincipalRoleEntity.Builder(currentPrincipalRoleEntity);
if (updateRequest.getProperties() != null) {
- updateBuilder.setProperties(updateRequest.getProperties());
+ Map<String, String> updateProperties =
+ reservedProperties.removeReservedPropertiesFromUpdate(
+ currentPrincipalRoleEntity.getPropertiesAsMap(),
updateRequest.getProperties());
+ updateBuilder.setProperties(updateProperties);
}
PrincipalRoleEntity updatedEntity = updateBuilder.build();
PrincipalRoleEntity returnedEntity =
@@ -1262,7 +1276,10 @@ public class PolarisAdminService {
CatalogRoleEntity.Builder updateBuilder =
new CatalogRoleEntity.Builder(currentCatalogRoleEntity);
if (updateRequest.getProperties() != null) {
- updateBuilder.setProperties(updateRequest.getProperties());
+ Map<String, String> updateProperties =
+ reservedProperties.removeReservedPropertiesFromUpdate(
+ currentCatalogRoleEntity.getPropertiesAsMap(),
updateRequest.getProperties());
+ updateBuilder.setProperties(updateProperties);
}
CatalogRoleEntity updatedEntity = updateBuilder.build();
CatalogRoleEntity returnedEntity =
diff --git
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
index c2c53c17b..c0eba7b0a 100644
---
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
+++
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
@@ -74,6 +74,7 @@ import
org.apache.polaris.service.admin.api.PolarisCatalogsApiService;
import org.apache.polaris.service.admin.api.PolarisPrincipalRolesApiService;
import org.apache.polaris.service.admin.api.PolarisPrincipalsApiService;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
+import org.apache.polaris.service.config.ReservedProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -89,6 +90,7 @@ public class PolarisServiceImpl
private final MetaStoreManagerFactory metaStoreManagerFactory;
private final UserSecretsManagerFactory userSecretsManagerFactory;
private final CallContext callContext;
+ private final ReservedProperties reservedProperties;
@Inject
public PolarisServiceImpl(
@@ -96,12 +98,14 @@ public class PolarisServiceImpl
MetaStoreManagerFactory metaStoreManagerFactory,
UserSecretsManagerFactory userSecretsManagerFactory,
PolarisAuthorizer polarisAuthorizer,
- CallContext callContext) {
+ CallContext callContext,
+ ReservedProperties reservedProperties) {
this.entityManagerFactory = entityManagerFactory;
this.metaStoreManagerFactory = metaStoreManagerFactory;
this.userSecretsManagerFactory = userSecretsManagerFactory;
this.polarisAuthorizer = polarisAuthorizer;
this.callContext = callContext;
+ this.reservedProperties = reservedProperties;
// FIXME: This is a hack to set the current context for downstream calls.
CallContext.setCurrentContext(callContext);
}
@@ -126,7 +130,8 @@ public class PolarisServiceImpl
metaStoreManager,
userSecretsManager,
securityContext,
- polarisAuthorizer);
+ polarisAuthorizer,
+ reservedProperties);
}
/** From PolarisCatalogsApiService */
@@ -208,7 +213,13 @@ public class PolarisServiceImpl
public Response createPrincipal(
CreatePrincipalRequest request, RealmContext realmContext,
SecurityContext securityContext) {
PolarisAdminService adminService = newAdminService(realmContext,
securityContext);
- PrincipalEntity principal =
PrincipalEntity.fromPrincipal(request.getPrincipal());
+ PrincipalEntity principal =
+ new PrincipalEntity.Builder()
+ .setName(request.getPrincipal().getName())
+ .setClientId(request.getPrincipal().getClientId())
+ .setProperties(
+
reservedProperties.removeReservedProperties(request.getPrincipal().getProperties()))
+ .build();
if (Boolean.TRUE.equals(request.getCredentialRotationRequired())) {
principal =
new
PrincipalEntity.Builder(principal).setCredentialRotationRequiredState().build();
@@ -276,11 +287,15 @@ public class PolarisServiceImpl
RealmContext realmContext,
SecurityContext securityContext) {
PolarisAdminService adminService = newAdminService(realmContext,
securityContext);
+ PrincipalRoleEntity entity =
+ new PrincipalRoleEntity.Builder()
+ .setName(request.getPrincipalRole().getName())
+ .setProperties(
+ reservedProperties.removeReservedProperties(
+ request.getPrincipalRole().getProperties()))
+ .build();
PrincipalRole newPrincipalRole =
- new PrincipalRoleEntity(
- adminService.createPrincipalRole(
-
PrincipalRoleEntity.fromPrincipalRole(request.getPrincipalRole())))
- .asPrincipalRole();
+ new
PrincipalRoleEntity(adminService.createPrincipalRole(entity)).asPrincipalRole();
LOGGER.info("Created new principalRole {}", newPrincipalRole);
return Response.status(Response.Status.CREATED).build();
}
@@ -337,11 +352,15 @@ public class PolarisServiceImpl
RealmContext realmContext,
SecurityContext securityContext) {
PolarisAdminService adminService = newAdminService(realmContext,
securityContext);
+ CatalogRoleEntity entity =
+ new CatalogRoleEntity.Builder()
+ .setName(request.getCatalogRole().getName())
+ .setProperties(
+ reservedProperties.removeReservedProperties(
+ request.getCatalogRole().getProperties()))
+ .build();
CatalogRole newCatalogRole =
- new CatalogRoleEntity(
- adminService.createCatalogRole(
- catalogName,
CatalogRoleEntity.fromCatalogRole(request.getCatalogRole())))
- .asCatalogRole();
+ new CatalogRoleEntity(adminService.createCatalogRole(catalogName,
entity)).asCatalogRole();
LOGGER.info("Created new catalogRole {}", newCatalogRole);
return Response.status(Response.Status.CREATED).build();
}
diff --git
a/service/common/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
b/service/common/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
index 8d17989ef..0c40960f5 100644
---
a/service/common/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
+++
b/service/common/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java
@@ -32,6 +32,7 @@ import
org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.service.catalog.CatalogPrefixParser;
import
org.apache.polaris.service.catalog.api.PolarisCatalogGenericTableApiService;
import org.apache.polaris.service.catalog.common.CatalogAdapter;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.types.CreateGenericTableRequest;
import org.apache.polaris.service.types.ListGenericTablesResponse;
import org.apache.polaris.service.types.LoadGenericTableResponse;
@@ -49,6 +50,7 @@ public class GenericTableCatalogAdapter
private final PolarisEntityManager entityManager;
private final PolarisMetaStoreManager metaStoreManager;
private final PolarisAuthorizer polarisAuthorizer;
+ private final ReservedProperties reservedProperties;
private final CatalogPrefixParser prefixParser;
@Inject
@@ -58,13 +60,15 @@ public class GenericTableCatalogAdapter
PolarisEntityManager entityManager,
PolarisMetaStoreManager metaStoreManager,
PolarisAuthorizer polarisAuthorizer,
- CatalogPrefixParser prefixParser) {
+ CatalogPrefixParser prefixParser,
+ ReservedProperties reservedProperties) {
this.realmContext = realmContext;
this.callContext = callContext;
this.entityManager = entityManager;
this.metaStoreManager = metaStoreManager;
this.polarisAuthorizer = polarisAuthorizer;
this.prefixParser = prefixParser;
+ this.reservedProperties = reservedProperties;
// FIXME: This is a hack to set the current context for downstream calls.
CallContext.setCurrentContext(callContext);
@@ -98,7 +102,7 @@ public class GenericTableCatalogAdapter
TableIdentifier.of(decodeNamespace(namespace),
createGenericTableRequest.getName()),
createGenericTableRequest.getFormat(),
createGenericTableRequest.getDoc(),
- createGenericTableRequest.getProperties());
+
reservedProperties.removeReservedProperties(createGenericTableRequest.getProperties()));
return Response.ok(response).build();
}
diff --git
a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index 38026c0fa..8dec97168 100644
---
a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++
b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -48,10 +48,12 @@ import
org.apache.iceberg.rest.requests.CommitTransactionRequest;
import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
import org.apache.iceberg.rest.requests.RegisterTableRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.ReportMetricsRequest;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
import org.apache.iceberg.rest.responses.ConfigResponse;
import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse;
import org.apache.iceberg.rest.responses.LoadTableResponse;
@@ -72,6 +74,7 @@ import org.apache.polaris.service.catalog.CatalogPrefixParser;
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService;
import
org.apache.polaris.service.catalog.api.IcebergRestConfigurationApiService;
import org.apache.polaris.service.catalog.common.CatalogAdapter;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.context.CallContextCatalogFactory;
import org.apache.polaris.service.http.IcebergHttpUtil;
import org.apache.polaris.service.http.IfNoneMatch;
@@ -136,6 +139,7 @@ public class IcebergCatalogAdapter
private final UserSecretsManager userSecretsManager;
private final PolarisAuthorizer polarisAuthorizer;
private final CatalogPrefixParser prefixParser;
+ private final ReservedProperties reservedProperties;
@Inject
public IcebergCatalogAdapter(
@@ -146,7 +150,8 @@ public class IcebergCatalogAdapter
PolarisMetaStoreManager metaStoreManager,
UserSecretsManager userSecretsManager,
PolarisAuthorizer polarisAuthorizer,
- CatalogPrefixParser prefixParser) {
+ CatalogPrefixParser prefixParser,
+ ReservedProperties reservedProperties) {
this.realmContext = realmContext;
this.callContext = callContext;
this.catalogFactory = catalogFactory;
@@ -155,6 +160,7 @@ public class IcebergCatalogAdapter
this.userSecretsManager = userSecretsManager;
this.polarisAuthorizer = polarisAuthorizer;
this.prefixParser = prefixParser;
+ this.reservedProperties = reservedProperties;
// FIXME: This is a hack to set the current context for downstream calls.
CallContext.setCurrentContext(callContext);
@@ -192,7 +198,8 @@ public class IcebergCatalogAdapter
securityContext,
catalogFactory,
catalogName,
- polarisAuthorizer);
+ polarisAuthorizer,
+ reservedProperties);
}
@Override
@@ -293,12 +300,19 @@ public class IcebergCatalogAdapter
RealmContext realmContext,
SecurityContext securityContext) {
Namespace ns = decodeNamespace(namespace);
+ UpdateNamespacePropertiesRequest revisedRequest =
+ UpdateNamespacePropertiesRequest.builder()
+ .removeAll(
+ reservedProperties.removeReservedProperties(
+ updateNamespacePropertiesRequest.removals()))
+ .updateAll(
+ reservedProperties.removeReservedProperties(
+ updateNamespacePropertiesRequest.updates()))
+ .build();
return withCatalog(
securityContext,
prefix,
- catalog ->
- Response.ok(catalog.updateNamespaceProperties(ns,
updateNamespacePropertiesRequest))
- .build());
+ catalog -> Response.ok(catalog.updateNamespaceProperties(ns,
revisedRequest)).build());
}
private EnumSet<AccessDelegationMode> parseAccessDelegationModes(String
accessDelegationMode) {
@@ -490,18 +504,24 @@ public class IcebergCatalogAdapter
CommitTableRequest commitTableRequest,
RealmContext realmContext,
SecurityContext securityContext) {
+ UpdateTableRequest revisedRequest =
+ UpdateTableRequest.create(
+ commitTableRequest.identifier(),
+ commitTableRequest.requirements(),
+ commitTableRequest.updates().stream()
+ .map(reservedProperties::removeReservedProperties)
+ .toList());
Namespace ns = decodeNamespace(namespace);
TableIdentifier tableIdentifier = TableIdentifier.of(ns,
RESTUtil.decodeString(table));
return withCatalog(
securityContext,
prefix,
catalog -> {
- if (IcebergCatalogHandler.isCreate(commitTableRequest)) {
- return Response.ok(
- catalog.updateTableForStagedCreate(tableIdentifier,
commitTableRequest))
+ if (IcebergCatalogHandler.isCreate(revisedRequest)) {
+ return
Response.ok(catalog.updateTableForStagedCreate(tableIdentifier, revisedRequest))
.build();
} else {
- return Response.ok(catalog.updateTable(tableIdentifier,
commitTableRequest)).build();
+ return Response.ok(catalog.updateTable(tableIdentifier,
revisedRequest)).build();
}
});
}
@@ -513,11 +533,15 @@ public class IcebergCatalogAdapter
CreateViewRequest createViewRequest,
RealmContext realmContext,
SecurityContext securityContext) {
+ CreateViewRequest revisedRequest =
+ ImmutableCreateViewRequest.copyOf(createViewRequest)
+ .withProperties(
+
reservedProperties.removeReservedProperties(createViewRequest.properties()));
Namespace ns = decodeNamespace(namespace);
return withCatalog(
securityContext,
prefix,
- catalog -> Response.ok(catalog.createView(ns,
createViewRequest)).build());
+ catalog -> Response.ok(catalog.createView(ns,
revisedRequest)).build());
}
@Override
@@ -630,12 +654,19 @@ public class IcebergCatalogAdapter
CommitViewRequest commitViewRequest,
RealmContext realmContext,
SecurityContext securityContext) {
+ UpdateTableRequest revisedRequest =
+ UpdateTableRequest.create(
+ commitViewRequest.identifier(),
+ commitViewRequest.requirements(),
+ commitViewRequest.updates().stream()
+ .map(reservedProperties::removeReservedProperties)
+ .toList());
Namespace ns = decodeNamespace(namespace);
TableIdentifier tableIdentifier = TableIdentifier.of(ns,
RESTUtil.decodeString(view));
return withCatalog(
securityContext,
prefix,
- catalog -> Response.ok(catalog.replaceView(tableIdentifier,
commitViewRequest)).build());
+ catalog -> Response.ok(catalog.replaceView(tableIdentifier,
revisedRequest)).build());
}
@Override
@@ -644,11 +675,24 @@ public class IcebergCatalogAdapter
CommitTransactionRequest commitTransactionRequest,
RealmContext realmContext,
SecurityContext securityContext) {
+ CommitTransactionRequest revisedRequest =
+ new CommitTransactionRequest(
+ commitTransactionRequest.tableChanges().stream()
+ .map(
+ r -> {
+ return UpdateTableRequest.create(
+ r.identifier(),
+ r.requirements(),
+ r.updates().stream()
+
.map(reservedProperties::removeReservedProperties)
+ .toList());
+ })
+ .toList());
return withCatalog(
securityContext,
prefix,
catalog -> {
- catalog.commitTransaction(commitTransactionRequest);
+ catalog.commitTransaction(revisedRequest);
return Response.status(Response.Status.NO_CONTENT).build();
});
}
diff --git
a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 158732c32..e8912548d 100644
---
a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++
b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -96,6 +96,7 @@ import org.apache.polaris.core.storage.AccessConfig;
import org.apache.polaris.core.storage.PolarisStorageActions;
import org.apache.polaris.service.catalog.SupportsNotifications;
import org.apache.polaris.service.catalog.common.CatalogHandler;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.context.CallContextCatalogFactory;
import org.apache.polaris.service.http.IcebergHttpUtil;
import org.apache.polaris.service.http.IfNoneMatch;
@@ -124,6 +125,7 @@ public class IcebergCatalogHandler extends CatalogHandler
implements AutoCloseab
private final PolarisMetaStoreManager metaStoreManager;
private final UserSecretsManager userSecretsManager;
private final CallContextCatalogFactory catalogFactory;
+ private final ReservedProperties reservedProperties;
// Catalog instance will be initialized after authorizing resolver
successfully resolves
// the catalog entity.
@@ -142,11 +144,13 @@ public class IcebergCatalogHandler extends CatalogHandler
implements AutoCloseab
SecurityContext securityContext,
CallContextCatalogFactory catalogFactory,
String catalogName,
- PolarisAuthorizer authorizer) {
+ PolarisAuthorizer authorizer,
+ ReservedProperties reservedProperties) {
super(callContext, entityManager, securityContext, catalogName,
authorizer);
this.metaStoreManager = metaStoreManager;
this.userSecretsManager = userSecretsManager;
this.catalogFactory = catalogFactory;
+ this.reservedProperties = reservedProperties;
}
/**
@@ -265,14 +269,17 @@ public class IcebergCatalogHandler extends CatalogHandler
implements AutoCloseab
// For CreateNamespace, we consider this a special case in that the
creator is able to
// retrieve the latest namespace metadata for the duration of the
CreateNamespace
// operation, even if the entityVersion and/or grantsVersion update in
the interim.
- namespaceCatalog.createNamespace(namespace, request.properties());
- return CreateNamespaceResponse.builder()
- .withNamespace(namespace)
- .setProperties(
+ namespaceCatalog.createNamespace(
+ namespace,
reservedProperties.removeReservedProperties(request.properties()));
+ Map<String, String> filteredProperties =
+ reservedProperties.removeReservedProperties(
resolutionManifest
.getPassthroughResolvedPath(namespace)
.getRawLeafEntity()
- .getPropertiesAsMap())
+ .getPropertiesAsMap());
+ return CreateNamespaceResponse.builder()
+ .withNamespace(namespace)
+ .setProperties(filteredProperties)
.build();
} else {
return CatalogHandlers.createNamespace(namespaceCatalog, request);
@@ -366,7 +373,16 @@ public class IcebergCatalogHandler extends CatalogHandler
implements AutoCloseab
if (isStaticFacade(catalog)) {
throw new BadRequestException("Cannot create table on static-facade
external catalogs.");
}
- return CatalogHandlers.createTable(baseCatalog, namespace, request);
+ CreateTableRequest requestWithoutReservedProperties =
+ CreateTableRequest.builder()
+ .withName(request.name())
+ .withLocation(request.location())
+ .withPartitionSpec(request.spec())
+ .withSchema(request.schema())
+ .withWriteOrder(request.writeOrder())
+
.setProperties(reservedProperties.removeReservedProperties(request.properties()))
+ .build();
+ return CatalogHandlers.createTable(baseCatalog, namespace,
requestWithoutReservedProperties);
}
/**
@@ -401,7 +417,7 @@ public class IcebergCatalogHandler extends CatalogHandler
implements AutoCloseab
Map<String, String> properties = Maps.newHashMap();
properties.put("created-at",
OffsetDateTime.now(ZoneOffset.UTC).toString());
- properties.putAll(request.properties());
+
properties.putAll(reservedProperties.removeReservedProperties(request.properties()));
Table table =
baseCatalog
@@ -441,7 +457,7 @@ public class IcebergCatalogHandler extends CatalogHandler
implements AutoCloseab
Map<String, String> properties = Maps.newHashMap();
properties.put("created-at",
OffsetDateTime.now(ZoneOffset.UTC).toString());
- properties.putAll(request.properties());
+
properties.putAll(reservedProperties.removeReservedProperties(request.properties()));
String location;
if (request.location() != null) {
diff --git
a/service/common/src/main/java/org/apache/polaris/service/config/ReservedProperties.java
b/service/common/src/main/java/org/apache/polaris/service/config/ReservedProperties.java
new file mode 100644
index 000000000..3c4b108d0
--- /dev/null
+++
b/service/common/src/main/java/org/apache/polaris/service/config/ReservedProperties.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.config;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.iceberg.MetadataUpdate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Used to track entity properties reserved for use by the catalog. These
properties may not be
+ * overridden by the end user.
+ */
+public interface ReservedProperties {
+ Logger LOGGER = LoggerFactory.getLogger(ReservedProperties.class);
+
+ /**
+ * Provides a {@link ReservedProperties} implementation that reserves
nothing. Used for testing.
+ */
+ ReservedProperties NONE =
+ new ReservedProperties() {
+ @Override
+ public List<String> prefixes() {
+ return List.of();
+ }
+ };
+
+ /**
+ * A list of prefixes that are considered reserved. Any property starting
with one of these
+ * prefixes is a reserved property.
+ */
+ List<String> prefixes();
+
+ /** If true, attempts to modify a reserved property should throw an
exception. */
+ default boolean shouldThrow() {
+ return true;
+ }
+
+ /**
+ * Removes reserved properties from a planned change to an entity. If
`shouldThrow` returns true,
+ * this will throw an IllegalArgumentException.
+ *
+ * @param existingProperties The properties currently present for an entity
+ * @param updateProperties The properties present in an update to an entity
+ * @return The properties in the update, with changes to reserved properties
removed
+ */
+ default Map<String, String> removeReservedPropertiesFromUpdate(
+ Map<String, String> existingProperties, Map<String, String>
updateProperties)
+ throws IllegalArgumentException {
+ Map<String, String> updatePropertiesWithoutReservedProperties =
+ removeReservedProperties(updateProperties);
+ for (var entry : updateProperties.entrySet()) {
+ // If a key was removed from the update, we substitute back the existing
value
+ if
(!updatePropertiesWithoutReservedProperties.containsKey(entry.getKey())) {
+ if (existingProperties.containsKey(entry.getKey())) {
+ updatePropertiesWithoutReservedProperties.put(
+ entry.getKey(), existingProperties.get(entry.getKey()));
+ }
+ }
+ }
+ return updatePropertiesWithoutReservedProperties;
+ }
+
+ /**
+ * Removes reserved properties from a list of input property keys. If
`shouldThrow`returns true,
+ * this will throw an IllegalArgumentException.
+ *
+ * @param properties A map of properties to remove reserved properties from
+ * @return The keys from the input list which are not reserved properties
+ */
+ default Map<String, String> removeReservedProperties(Map<String, String>
properties)
+ throws IllegalArgumentException {
+ Map<String, String> results = new HashMap<>();
+ List<String> prefixes = prefixes();
+ for (var entry : properties.entrySet()) {
+ boolean isReserved = false;
+ for (String prefix : prefixes) {
+ if (entry.getKey().startsWith(prefix)) {
+ isReserved = true;
+ String message =
+ String.format("Property '%s' matches reserved prefix '%s'",
entry.getKey(), prefix);
+ if (shouldThrow()) {
+ throw new IllegalArgumentException(message);
+ } else {
+ LOGGER.debug(message);
+ }
+ }
+ }
+ if (!isReserved) {
+ results.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return results;
+ }
+
+ /** See {@link #removeReservedProperties(Map)} */
+ default List<String> removeReservedProperties(List<String> properties)
+ throws IllegalArgumentException {
+ Map<String, String> propertyMap =
+ properties.stream().collect(Collectors.toMap(k -> k, k -> ""));
+ Map<String, String> filteredMap = removeReservedProperties(propertyMap);
+ return filteredMap.keySet().stream().toList();
+ }
+
+ default MetadataUpdate removeReservedProperties(MetadataUpdate update) {
+ return switch (update) {
+ case MetadataUpdate.SetProperties p -> {
+ yield new
MetadataUpdate.SetProperties(removeReservedProperties(p.updated()));
+ }
+ case MetadataUpdate.RemoveProperties p -> {
+ List<String> filteredProperties =
removeReservedProperties(p.removed().stream().toList());
+ yield new MetadataUpdate.RemoveProperties(new
HashSet<>(filteredProperties));
+ }
+ default -> update;
+ };
+ }
+}
diff --git
a/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
b/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index cb48ad0a1..15a254a08 100644
---
a/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++
b/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -54,6 +54,7 @@ import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.catalog.io.MeasuredFileIOFactory;
import org.apache.polaris.service.config.DefaultConfigurationStore;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
+import org.apache.polaris.service.config.ReservedProperties;
import org.apache.polaris.service.context.CallContextCatalogFactory;
import org.apache.polaris.service.context.PolarisCallContextCatalogFactory;
import org.apache.polaris.service.events.PolarisEventListener;
@@ -188,6 +189,8 @@ public record TestServices(
fileIOFactory,
polarisEventListener);
+ ReservedProperties reservedProperties = ReservedProperties.NONE;
+
IcebergCatalogAdapter service =
new IcebergCatalogAdapter(
realmContext,
@@ -197,7 +200,8 @@ public record TestServices(
metaStoreManager,
userSecretsManager,
authorizer,
- new DefaultCatalogPrefixParser());
+ new DefaultCatalogPrefixParser(),
+ reservedProperties);
IcebergRestCatalogApi restApi = new IcebergRestCatalogApi(service);
IcebergRestConfigurationApi restConfigurationApi = new
IcebergRestConfigurationApi(service);
@@ -244,7 +248,8 @@ public record TestServices(
metaStoreManagerFactory,
userSecretsManagerFactory,
authorizer,
- callContext));
+ callContext,
+ reservedProperties));
return new TestServices(
catalogsApi,