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 <eric.maynard+...@snowflake.com>
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,

Reply via email to