You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by di...@apache.org on 2026/07/08 19:16:27 UTC

(polaris) branch main updated: Add catalog config endpoint registry (#4926)

This is an automated email from the ASF dual-hosted git repository.

dimas-b 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 ec207753b5 Add catalog config endpoint registry (#4926)
ec207753b5 is described below

commit ec207753b53617f4b259894c53b36332fbfd3c60
Author: Adam Christian <10...@users.noreply.github.com>
AuthorDate: Wed Jul 8 15:16:23 2026 -0400

    Add catalog config endpoint registry (#4926)
    
    * Add Polaris catalog config discovery endpoint
    
    * Remove Polaris catalog config endpoint
    
    * Inject RealmConfig into catalog config handler
    
    * Share generic table endpoints with Spark
    
    * Document generic table endpoint placement
    
    * Stabilize catalog config endpoint order
---
 extensions/semantic-models/build.gradle.kts        |   4 +
 .../SemanticModelConfigEndpoints.java              |  57 ++++++++++
 .../semanticmodel/SemanticModelEndpoints.java      |  48 ++++++++
 .../SemanticModelConfigEndpointsTest.java          |  52 +++++++++
 .../apache/polaris/spark/PolarisRESTCatalog.java   |  13 ++-
 .../polaris/spark/PolarisRESTCatalogTest.java      |  14 +--
 .../rest/CatalogConfigEndpointContributor.java     |  32 ++++++
 .../polaris/core/rest/GenericTableEndpoints.java   |  47 ++++++++
 .../apache/polaris/core/rest/PolarisEndpoints.java | 126 ---------------------
 .../catalog/config/CatalogConfigHandler.java       | 104 +++++++++++++++++
 .../generic/GenericTableConfigEndpoints.java       |  55 +++++++++
 .../catalog/iceberg/IcebergCatalogAdapter.java     |  10 +-
 .../catalog/iceberg/IcebergCatalogHandler.java     |  75 ------------
 .../iceberg/IcebergRestConfigEndpoints.java        |  55 +++++++++
 .../iceberg/IcebergViewConfigEndpoints.java        |  47 ++++++++
 .../catalog/policy/PolicyConfigEndpoints.java      |  54 +++++++++
 .../service/catalog/policy/PolicyEndpoints.java    |  57 ++++++++++
 .../service/catalog/iceberg/GetConfigTest.java     |  49 ++++++--
 .../org/apache/polaris/service/TestServices.java   |  55 ++++++++-
 19 files changed, 727 insertions(+), 227 deletions(-)

diff --git a/extensions/semantic-models/build.gradle.kts b/extensions/semantic-models/build.gradle.kts
index 2644a4ed57..ff03fd9da0 100644
--- a/extensions/semantic-models/build.gradle.kts
+++ b/extensions/semantic-models/build.gradle.kts
@@ -69,6 +69,10 @@ dependencies {
 
   compileOnly(project(":polaris-immutables"))
   annotationProcessor(project(":polaris-immutables", configuration = "processor"))
+
+  testImplementation(platform(libs.junit.bom))
+  testImplementation("org.junit.jupiter:junit-jupiter")
+  testImplementation(libs.assertj.core)
 }
 
 val rootDir = rootProject.layout.projectDirectory
diff --git a/extensions/semantic-models/src/main/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelConfigEndpoints.java b/extensions/semantic-models/src/main/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelConfigEndpoints.java
new file mode 100644
index 0000000000..8922a637e0
--- /dev/null
+++ b/extensions/semantic-models/src/main/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelConfigEndpoints.java
@@ -0,0 +1,57 @@
+/*
+ * 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.catalog.semanticmodel;
+
+import com.google.common.collect.ImmutableSet;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.rest.CatalogConfigEndpointContributor;
+
+@ApplicationScoped
+@Priority(500)
+public class SemanticModelConfigEndpoints implements CatalogConfigEndpointContributor {
+  private final RealmConfig realmConfig;
+
+  @Inject
+  public SemanticModelConfigEndpoints(RealmConfig realmConfig) {
+    this.realmConfig = realmConfig;
+  }
+
+  @Override
+  public Set<Endpoint> endpoints() {
+    return getSupportedSemanticModelEndpoints(realmConfig);
+  }
+
+  /**
+   * Get the semantic model endpoints. Returns SEMANTIC_MODEL_ENDPOINTS if ENABLE_SEMANTIC_MODELS is
+   * set to true, otherwise, returns an empty set.
+   */
+  public static Set<Endpoint> getSupportedSemanticModelEndpoints(RealmConfig realmConfig) {
+    boolean semanticModelsEnabled =
+        realmConfig.getConfig(FeatureConfiguration.ENABLE_SEMANTIC_MODELS);
+    return semanticModelsEnabled
+        ? SemanticModelEndpoints.SEMANTIC_MODEL_ENDPOINTS
+        : ImmutableSet.of();
+  }
+}
diff --git a/extensions/semantic-models/src/main/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelEndpoints.java b/extensions/semantic-models/src/main/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelEndpoints.java
new file mode 100644
index 0000000000..f12e8eed97
--- /dev/null
+++ b/extensions/semantic-models/src/main/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelEndpoints.java
@@ -0,0 +1,48 @@
+/*
+ * 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.catalog.semanticmodel;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.polaris.core.rest.PolarisResourcePaths;
+
+public class SemanticModelEndpoints {
+  private SemanticModelEndpoints() {}
+
+  public static final Endpoint V1_LIST_SEMANTIC_MODELS =
+      Endpoint.create("GET", PolarisResourcePaths.V1_SEMANTIC_MODELS);
+  public static final Endpoint V1_CREATE_SEMANTIC_MODEL =
+      Endpoint.create("POST", PolarisResourcePaths.V1_SEMANTIC_MODELS);
+  public static final Endpoint V1_LOAD_SEMANTIC_MODEL =
+      Endpoint.create("GET", PolarisResourcePaths.V1_SEMANTIC_MODEL);
+  public static final Endpoint V1_UPDATE_SEMANTIC_MODEL =
+      Endpoint.create("PUT", PolarisResourcePaths.V1_SEMANTIC_MODEL);
+  public static final Endpoint V1_DROP_SEMANTIC_MODEL =
+      Endpoint.create("DELETE", PolarisResourcePaths.V1_SEMANTIC_MODEL);
+
+  public static final Set<Endpoint> SEMANTIC_MODEL_ENDPOINTS =
+      ImmutableSet.<Endpoint>builder()
+          .add(V1_LIST_SEMANTIC_MODELS)
+          .add(V1_CREATE_SEMANTIC_MODEL)
+          .add(V1_LOAD_SEMANTIC_MODEL)
+          .add(V1_UPDATE_SEMANTIC_MODEL)
+          .add(V1_DROP_SEMANTIC_MODEL)
+          .build();
+}
diff --git a/extensions/semantic-models/src/test/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelConfigEndpointsTest.java b/extensions/semantic-models/src/test/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelConfigEndpointsTest.java
new file mode 100644
index 0000000000..12a887dc9b
--- /dev/null
+++ b/extensions/semantic-models/src/test/java/org/apache/polaris/service/catalog/semanticmodel/SemanticModelConfigEndpointsTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.catalog.semanticmodel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.config.RealmConfigImpl;
+import org.junit.jupiter.api.Test;
+
+public class SemanticModelConfigEndpointsTest {
+  @Test
+  public void endpointsIncludeSemanticModelsWhenFeatureEnabled() {
+    SemanticModelConfigEndpoints endpoints = new SemanticModelConfigEndpoints(realmConfig(true));
+
+    assertThat(endpoints.endpoints())
+        .containsExactlyInAnyOrderElementsOf(SemanticModelEndpoints.SEMANTIC_MODEL_ENDPOINTS);
+  }
+
+  @Test
+  public void endpointsEmptyWhenFeatureDisabled() {
+    SemanticModelConfigEndpoints endpoints = new SemanticModelConfigEndpoints(realmConfig(false));
+
+    assertThat(endpoints.endpoints()).isEmpty();
+  }
+
+  private static RealmConfig realmConfig(boolean enableSemanticModels) {
+    return new RealmConfigImpl(
+        (realmContext, configName) ->
+            FeatureConfiguration.ENABLE_SEMANTIC_MODELS.key().equals(configName)
+                ? enableSemanticModels
+                : null,
+        () -> "test-realm");
+  }
+}
diff --git a/plugins/spark/common/src/main/java/org/apache/polaris/spark/PolarisRESTCatalog.java b/plugins/spark/common/src/main/java/org/apache/polaris/spark/PolarisRESTCatalog.java
index 5be0f6952e..36496b3fd8 100644
--- a/plugins/spark/common/src/main/java/org/apache/polaris/spark/PolarisRESTCatalog.java
+++ b/plugins/spark/common/src/main/java/org/apache/polaris/spark/PolarisRESTCatalog.java
@@ -44,7 +44,7 @@ import org.apache.iceberg.rest.auth.OAuth2Util;
 import org.apache.iceberg.rest.responses.ConfigResponse;
 import org.apache.iceberg.util.EnvironmentUtil;
 import org.apache.iceberg.util.PropertyUtil;
-import org.apache.polaris.core.rest.PolarisEndpoints;
+import org.apache.polaris.core.rest.GenericTableEndpoints;
 import org.apache.polaris.core.rest.PolarisResourcePaths;
 import org.apache.polaris.spark.rest.CreateGenericTableRESTRequest;
 import org.apache.polaris.spark.rest.CreateGenericTableRequest;
@@ -70,7 +70,8 @@ public class PolarisRESTCatalog implements PolarisCatalog, Closeable {
   private Integer pageSize = null;
 
   // the default endpoints to config if server doesn't specify the 'endpoints' configuration.
-  private static final Set<Endpoint> DEFAULT_ENDPOINTS = PolarisEndpoints.GENERIC_TABLE_ENDPOINTS;
+  private static final Set<Endpoint> DEFAULT_ENDPOINTS =
+      GenericTableEndpoints.GENERIC_TABLE_ENDPOINTS;
 
   public PolarisRESTCatalog() {
     this(config -> HTTPClient.builder(config).uri(config.get(CatalogProperties.URI)).build());
@@ -153,7 +154,7 @@ public class PolarisRESTCatalog implements PolarisCatalog, Closeable {
 
   @Override
   public List<TableIdentifier> listGenericTables(Namespace ns) {
-    Endpoint.check(endpoints, PolarisEndpoints.V1_LIST_GENERIC_TABLES);
+    Endpoint.check(endpoints, GenericTableEndpoints.V1_LIST_GENERIC_TABLES);
 
     Map<String, String> queryParams = Maps.newHashMap();
     ImmutableList.Builder<TableIdentifier> tables = ImmutableList.builder();
@@ -182,7 +183,7 @@ public class PolarisRESTCatalog implements PolarisCatalog, Closeable {
 
   @Override
   public boolean dropGenericTable(TableIdentifier identifier) {
-    Endpoint.check(endpoints, PolarisEndpoints.V1_DELETE_GENERIC_TABLE);
+    Endpoint.check(endpoints, GenericTableEndpoints.V1_DELETE_GENERIC_TABLE);
 
     try {
       restClient
@@ -205,7 +206,7 @@ public class PolarisRESTCatalog implements PolarisCatalog, Closeable {
       String baseLocation,
       String doc,
       Map<String, String> props) {
-    Endpoint.check(endpoints, PolarisEndpoints.V1_CREATE_GENERIC_TABLE);
+    Endpoint.check(endpoints, GenericTableEndpoints.V1_CREATE_GENERIC_TABLE);
     CreateGenericTableRESTRequest request =
         new CreateGenericTableRESTRequest(
             CreateGenericTableRequest.builder()
@@ -231,7 +232,7 @@ public class PolarisRESTCatalog implements PolarisCatalog, Closeable {
 
   @Override
   public GenericTable loadGenericTable(TableIdentifier identifier) {
-    Endpoint.check(endpoints, PolarisEndpoints.V1_LOAD_GENERIC_TABLE);
+    Endpoint.check(endpoints, GenericTableEndpoints.V1_LOAD_GENERIC_TABLE);
     LoadGenericTableRESTResponse response =
         restClient
             .withAuthSession(this.catalogAuth)
diff --git a/plugins/spark/common/src/test/java/org/apache/polaris/spark/PolarisRESTCatalogTest.java b/plugins/spark/common/src/test/java/org/apache/polaris/spark/PolarisRESTCatalogTest.java
index a5af3498c9..4582c36cee 100644
--- a/plugins/spark/common/src/test/java/org/apache/polaris/spark/PolarisRESTCatalogTest.java
+++ b/plugins/spark/common/src/test/java/org/apache/polaris/spark/PolarisRESTCatalogTest.java
@@ -39,7 +39,7 @@ import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.apache.iceberg.rest.RESTClient;
 import org.apache.iceberg.rest.auth.OAuth2Util;
 import org.apache.iceberg.rest.responses.ConfigResponse;
-import org.apache.polaris.core.rest.PolarisEndpoints;
+import org.apache.polaris.core.rest.GenericTableEndpoints;
 import org.apache.polaris.spark.rest.GenericTable;
 import org.apache.polaris.spark.rest.ListGenericTablesRESTResponse;
 import org.apache.polaris.spark.rest.LoadGenericTableRESTResponse;
@@ -90,8 +90,8 @@ public class PolarisRESTCatalogTest {
             .withOverrides(ImmutableMap.of())
             .withEndpoints(
                 ImmutableList.of(
-                    PolarisEndpoints.V1_LIST_GENERIC_TABLES,
-                    PolarisEndpoints.V1_CREATE_GENERIC_TABLE))
+                    GenericTableEndpoints.V1_LIST_GENERIC_TABLES,
+                    GenericTableEndpoints.V1_CREATE_GENERIC_TABLE))
             .build();
 
     when(mockClient.get(any(), anyMap(), eq(ConfigResponse.class), anyMap(), any()))
@@ -328,10 +328,10 @@ public class PolarisRESTCatalogTest {
             .withOverrides(ImmutableMap.of())
             .withEndpoints(
                 ImmutableList.of(
-                    PolarisEndpoints.V1_LIST_GENERIC_TABLES,
-                    PolarisEndpoints.V1_CREATE_GENERIC_TABLE,
-                    PolarisEndpoints.V1_LOAD_GENERIC_TABLE,
-                    PolarisEndpoints.V1_DELETE_GENERIC_TABLE))
+                    GenericTableEndpoints.V1_LIST_GENERIC_TABLES,
+                    GenericTableEndpoints.V1_CREATE_GENERIC_TABLE,
+                    GenericTableEndpoints.V1_LOAD_GENERIC_TABLE,
+                    GenericTableEndpoints.V1_DELETE_GENERIC_TABLE))
             .build();
 
     when(mockClient.get(any(), anyMap(), eq(ConfigResponse.class), anyMap(), any()))
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/CatalogConfigEndpointContributor.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/CatalogConfigEndpointContributor.java
new file mode 100644
index 0000000000..4e93f2656e
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/rest/CatalogConfigEndpointContributor.java
@@ -0,0 +1,32 @@
+/*
+ * 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.core.rest;
+
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+
+/**
+ * Contributes endpoints advertised in catalog configuration responses.
+ *
+ * <p>Implementations are discovered through CDI as {@code CatalogConfigEndpointContributor} beans.
+ */
+@FunctionalInterface
+public interface CatalogConfigEndpointContributor {
+  Set<Endpoint> endpoints();
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/GenericTableEndpoints.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/GenericTableEndpoints.java
new file mode 100644
index 0000000000..27c6cda35f
--- /dev/null
+++ b/polaris-core/src/main/java/org/apache/polaris/core/rest/GenericTableEndpoints.java
@@ -0,0 +1,47 @@
+/*
+ * 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.core.rest;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+
+// TODO: This is a temporary hack to share generic-table endpoint constants with clients.
+// Endpoint definitions should not be exposed from polaris-core; move these to a dedicated
+// shared API module when one exists.
+public class GenericTableEndpoints {
+  private GenericTableEndpoints() {}
+
+  public static final Endpoint V1_LIST_GENERIC_TABLES =
+      Endpoint.create("GET", PolarisResourcePaths.V1_GENERIC_TABLES);
+  public static final Endpoint V1_LOAD_GENERIC_TABLE =
+      Endpoint.create("GET", PolarisResourcePaths.V1_GENERIC_TABLE);
+  public static final Endpoint V1_CREATE_GENERIC_TABLE =
+      Endpoint.create("POST", PolarisResourcePaths.V1_GENERIC_TABLES);
+  public static final Endpoint V1_DELETE_GENERIC_TABLE =
+      Endpoint.create("DELETE", PolarisResourcePaths.V1_GENERIC_TABLE);
+
+  public static final Set<Endpoint> GENERIC_TABLE_ENDPOINTS =
+      ImmutableSet.<Endpoint>builder()
+          .add(V1_LIST_GENERIC_TABLES)
+          .add(V1_CREATE_GENERIC_TABLE)
+          .add(V1_DELETE_GENERIC_TABLE)
+          .add(V1_LOAD_GENERIC_TABLE)
+          .build();
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisEndpoints.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisEndpoints.java
deleted file mode 100644
index 8f2b8a3812..0000000000
--- a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisEndpoints.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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.core.rest;
-
-import com.google.common.collect.ImmutableSet;
-import java.util.Set;
-import org.apache.iceberg.rest.Endpoint;
-import org.apache.polaris.core.config.FeatureConfiguration;
-import org.apache.polaris.core.config.RealmConfig;
-
-public class PolarisEndpoints {
-  // Generic table endpoints
-  public static final Endpoint V1_LIST_GENERIC_TABLES =
-      Endpoint.create("GET", PolarisResourcePaths.V1_GENERIC_TABLES);
-  public static final Endpoint V1_LOAD_GENERIC_TABLE =
-      Endpoint.create("GET", PolarisResourcePaths.V1_GENERIC_TABLE);
-  public static final Endpoint V1_CREATE_GENERIC_TABLE =
-      Endpoint.create("POST", PolarisResourcePaths.V1_GENERIC_TABLES);
-  public static final Endpoint V1_DELETE_GENERIC_TABLE =
-      Endpoint.create("DELETE", PolarisResourcePaths.V1_GENERIC_TABLE);
-
-  public static final Set<Endpoint> GENERIC_TABLE_ENDPOINTS =
-      ImmutableSet.<Endpoint>builder()
-          .add(V1_LIST_GENERIC_TABLES)
-          .add(V1_CREATE_GENERIC_TABLE)
-          .add(V1_DELETE_GENERIC_TABLE)
-          .add(V1_LOAD_GENERIC_TABLE)
-          .build();
-
-  // Policy store endpoints
-  public static final Endpoint V1_LIST_POLICIES =
-      Endpoint.create("GET", PolarisResourcePaths.V1_POLICIES);
-  public static final Endpoint V1_CREATE_POLICY =
-      Endpoint.create("POST", PolarisResourcePaths.V1_POLICIES);
-  public static final Endpoint V1_LOAD_POLICY =
-      Endpoint.create("GET", PolarisResourcePaths.V1_POLICY);
-  public static final Endpoint V1_UPDATE_POLICY =
-      Endpoint.create("PUT", PolarisResourcePaths.V1_POLICY);
-  public static final Endpoint V1_DROP_POLICY =
-      Endpoint.create("DELETE", PolarisResourcePaths.V1_POLICY);
-  public static final Endpoint V1_ATTACH_POLICY =
-      Endpoint.create("PUT", PolarisResourcePaths.V1_POLICY_MAPPINGS);
-  public static final Endpoint V1_DETACH_POLICY =
-      Endpoint.create("POST", PolarisResourcePaths.V1_POLICY_MAPPINGS);
-  public static final Endpoint V1_GET_APPLICABLE_POLICIES =
-      Endpoint.create("GET", PolarisResourcePaths.V1_APPLICABLE_POLICIES);
-
-  public static final Set<Endpoint> POLICY_STORE_ENDPOINTS =
-      ImmutableSet.<Endpoint>builder()
-          .add(V1_LIST_POLICIES)
-          .add(V1_CREATE_POLICY)
-          .add(V1_LOAD_POLICY)
-          .add(V1_UPDATE_POLICY)
-          .add(V1_DROP_POLICY)
-          .add(V1_ATTACH_POLICY)
-          .add(V1_DETACH_POLICY)
-          .add(V1_GET_APPLICABLE_POLICIES)
-          .build();
-
-  // Semantic model endpoints
-  public static final Endpoint V1_LIST_SEMANTIC_MODELS =
-      Endpoint.create("GET", PolarisResourcePaths.V1_SEMANTIC_MODELS);
-  public static final Endpoint V1_CREATE_SEMANTIC_MODEL =
-      Endpoint.create("POST", PolarisResourcePaths.V1_SEMANTIC_MODELS);
-  public static final Endpoint V1_LOAD_SEMANTIC_MODEL =
-      Endpoint.create("GET", PolarisResourcePaths.V1_SEMANTIC_MODEL);
-  public static final Endpoint V1_UPDATE_SEMANTIC_MODEL =
-      Endpoint.create("PUT", PolarisResourcePaths.V1_SEMANTIC_MODEL);
-  public static final Endpoint V1_DROP_SEMANTIC_MODEL =
-      Endpoint.create("DELETE", PolarisResourcePaths.V1_SEMANTIC_MODEL);
-
-  public static final Set<Endpoint> SEMANTIC_MODEL_ENDPOINTS =
-      ImmutableSet.<Endpoint>builder()
-          .add(V1_LIST_SEMANTIC_MODELS)
-          .add(V1_CREATE_SEMANTIC_MODEL)
-          .add(V1_LOAD_SEMANTIC_MODEL)
-          .add(V1_UPDATE_SEMANTIC_MODEL)
-          .add(V1_DROP_SEMANTIC_MODEL)
-          .build();
-
-  /**
-   * Get the generic table endpoints. Returns GENERIC_TABLE_ENDPOINTS if ENABLE_GENERIC_TABLES is
-   * set to true, otherwise, returns an empty set.
-   */
-  public static Set<Endpoint> getSupportedGenericTableEndpoints(RealmConfig realmConfig) {
-    // add the generic table endpoints as supported endpoints if generic table feature is enabled.
-    boolean genericTableEnabled = realmConfig.getConfig(FeatureConfiguration.ENABLE_GENERIC_TABLES);
-
-    return genericTableEnabled ? GENERIC_TABLE_ENDPOINTS : ImmutableSet.of();
-  }
-
-  /**
-   * Get the policy store endpoints. Returns POLICY_ENDPOINTS if ENABLE_POLICY_STORE is set to true,
-   * otherwise, returns an empty set
-   */
-  public static Set<Endpoint> getSupportedPolicyEndpoints(RealmConfig realmConfig) {
-    boolean policyStoreEnabled = realmConfig.getConfig(FeatureConfiguration.ENABLE_POLICY_STORE);
-    return policyStoreEnabled ? POLICY_STORE_ENDPOINTS : ImmutableSet.of();
-  }
-
-  /**
-   * Get the semantic model endpoints. Returns SEMANTIC_MODEL_ENDPOINTS if ENABLE_SEMANTIC_MODELS is
-   * set to true, otherwise, returns an empty set.
-   */
-  public static Set<Endpoint> getSupportedSemanticModelEndpoints(RealmConfig realmConfig) {
-    boolean semanticModelsEnabled =
-        realmConfig.getConfig(FeatureConfiguration.ENABLE_SEMANTIC_MODELS);
-    return semanticModelsEnabled ? SEMANTIC_MODEL_ENDPOINTS : ImmutableSet.of();
-  }
-}
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/config/CatalogConfigHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/config/CatalogConfigHandler.java
new file mode 100644
index 0000000000..587c895b4d
--- /dev/null
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/config/CatalogConfigHandler.java
@@ -0,0 +1,104 @@
+/*
+ * 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.catalog.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.Instance;
+import jakarta.enterprise.inject.Instance.Handle;
+import jakarta.inject.Inject;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.iceberg.exceptions.NotFoundException;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.iceberg.rest.RESTCatalogProperties;
+import org.apache.iceberg.rest.responses.ConfigResponse;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.entity.PolarisEntity;
+import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
+import org.apache.polaris.core.persistence.resolver.Resolver;
+import org.apache.polaris.core.persistence.resolver.ResolverFactory;
+import org.apache.polaris.core.persistence.resolver.ResolverStatus;
+import org.apache.polaris.core.rest.CatalogConfigEndpointContributor;
+import org.apache.polaris.core.rest.NamespaceUtils;
+import org.apache.polaris.service.catalog.CatalogPrefixParser;
+
+@RequestScoped
+public class CatalogConfigHandler {
+  private final CatalogPrefixParser prefixParser;
+  private final ResolverFactory resolverFactory;
+  private final Instance<CatalogConfigEndpointContributor> endpointContributors;
+
+  @Inject
+  public CatalogConfigHandler(
+      CatalogPrefixParser prefixParser,
+      ResolverFactory resolverFactory,
+      @Any Instance<CatalogConfigEndpointContributor> endpointContributors) {
+    this.prefixParser = prefixParser;
+    this.resolverFactory = resolverFactory;
+    this.endpointContributors = endpointContributors;
+  }
+
+  public ConfigResponse getConfig(String catalogName, PolarisPrincipal principal) {
+    Resolver resolver = resolverFactory.createResolver(principal, catalogName);
+    ResolverStatus resolverStatus = resolver.resolveAll();
+    if (!resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) {
+      throw new NotFoundException("Unable to find warehouse %s", catalogName);
+    }
+    ResolvedPolarisEntity resolvedReferenceCatalog = resolver.getResolvedReferenceCatalog();
+    Map<String, String> properties =
+        PolarisEntity.of(resolvedReferenceCatalog.getEntity()).getPropertiesAsMap();
+
+    return ConfigResponse.builder()
+        .withDefaults(properties)
+        .withOverrides(
+            ImmutableMap.of(
+                "prefix",
+                prefixParser.catalogNameToPrefix(catalogName),
+                // Polaris does not handle custom namespace separators;
+                // always communicate the default namespace separator to clients.
+                RESTCatalogProperties.NAMESPACE_SEPARATOR,
+                NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR_ENCODED))
+        .withEndpoints(ImmutableList.copyOf(supportedEndpoints()))
+        .build();
+  }
+
+  private Set<Endpoint> supportedEndpoints() {
+    Set<Endpoint> endpoints = new LinkedHashSet<>();
+    endpointContributors
+        .handlesStream()
+        .sorted(
+            Comparator.comparingInt(CatalogConfigHandler::priority)
+                .thenComparing(handle -> handle.getBean().getBeanClass().getName()))
+        .map(Handle::get)
+        .map(CatalogConfigEndpointContributor::endpoints)
+        .forEach(endpoints::addAll);
+    return endpoints;
+  }
+
+  private static int priority(Handle<CatalogConfigEndpointContributor> handle) {
+    Priority priority = handle.getBean().getBeanClass().getAnnotation(Priority.class);
+    return priority == null ? Integer.MAX_VALUE : priority.value();
+  }
+}
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableConfigEndpoints.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableConfigEndpoints.java
new file mode 100644
index 0000000000..2d68c25e3e
--- /dev/null
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableConfigEndpoints.java
@@ -0,0 +1,55 @@
+/*
+ * 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.catalog.generic;
+
+import com.google.common.collect.ImmutableSet;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.rest.CatalogConfigEndpointContributor;
+import org.apache.polaris.core.rest.GenericTableEndpoints;
+
+@ApplicationScoped
+@Priority(300)
+public class GenericTableConfigEndpoints implements CatalogConfigEndpointContributor {
+  private final RealmConfig realmConfig;
+
+  @Inject
+  public GenericTableConfigEndpoints(RealmConfig realmConfig) {
+    this.realmConfig = realmConfig;
+  }
+
+  @Override
+  public Set<Endpoint> endpoints() {
+    return getSupportedGenericTableEndpoints(realmConfig);
+  }
+
+  /**
+   * Get the generic table endpoints. Returns GENERIC_TABLE_ENDPOINTS if ENABLE_GENERIC_TABLES is
+   * set to true, otherwise, returns an empty set.
+   */
+  public static Set<Endpoint> getSupportedGenericTableEndpoints(RealmConfig realmConfig) {
+    boolean genericTableEnabled = realmConfig.getConfig(FeatureConfiguration.ENABLE_GENERIC_TABLES);
+    return genericTableEnabled ? GenericTableEndpoints.GENERIC_TABLE_ENDPOINTS : ImmutableSet.of();
+  }
+}
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index bff88f9639..427e3d093f 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -57,6 +57,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.catalog.config.CatalogConfigHandler;
 import org.apache.polaris.service.catalog.validation.EntityNameValidator;
 import org.apache.polaris.service.config.ReservedProperties;
 import org.apache.polaris.service.http.IcebergHttpUtil;
@@ -81,17 +82,20 @@ public class IcebergCatalogAdapter
   private final CatalogPrefixParser prefixParser;
   private final ReservedProperties reservedProperties;
   private final IcebergCatalogHandlerFactory handlerFactory;
+  private final CatalogConfigHandler configHandler;
 
   @Inject
   public IcebergCatalogAdapter(
       CallContext callContext,
       CatalogPrefixParser prefixParser,
       ReservedProperties reservedProperties,
-      IcebergCatalogHandlerFactory handlerFactory) {
+      IcebergCatalogHandlerFactory handlerFactory,
+      CatalogConfigHandler configHandler) {
     this.realmConfig = callContext.getRealmConfig();
     this.prefixParser = prefixParser;
     this.reservedProperties = reservedProperties;
     this.handlerFactory = handlerFactory;
+    this.configHandler = configHandler;
   }
 
   /**
@@ -764,7 +768,7 @@ public class IcebergCatalogAdapter
     if (warehouse == null) {
       throw new BadRequestException("Please specify a warehouse");
     }
-    return withCatalogByName(
-        securityContext, warehouse, catalog -> Response.ok(catalog.getConfig()).build());
+    return Response.ok(configHandler.getConfig(warehouse, validatePrincipal(securityContext)))
+        .build();
   }
 }
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 89f68caa25..9eaef279e6 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -25,9 +25,6 @@ import static org.apache.polaris.service.catalog.common.ExceptionUtils.alreadyEx
 import static org.apache.polaris.service.catalog.common.ExceptionUtils.notFoundExceptionForTableLikeEntity;
 
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import io.smallrye.common.annotation.Identifier;
 import jakarta.enterprise.inject.Instance;
@@ -67,11 +64,8 @@ import org.apache.iceberg.exceptions.BadRequestException;
 import org.apache.iceberg.exceptions.CommitFailedException;
 import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.exceptions.NoSuchTableException;
-import org.apache.iceberg.exceptions.NotFoundException;
 import org.apache.iceberg.io.FileIO;
 import org.apache.iceberg.metrics.ScanReport;
-import org.apache.iceberg.rest.Endpoint;
-import org.apache.iceberg.rest.RESTCatalogProperties;
 import org.apache.iceberg.rest.credentials.ImmutableCredential;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
@@ -83,7 +77,6 @@ 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.CreateNamespaceResponse;
 import org.apache.iceberg.rest.responses.GetNamespaceResponse;
 import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse;
@@ -110,18 +103,13 @@ import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
-import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
 import org.apache.polaris.core.persistence.TransactionWorkspaceMetaStoreManager;
 import org.apache.polaris.core.persistence.dao.entity.EntitiesResult;
 import org.apache.polaris.core.persistence.dao.entity.EntityWithPath;
 import org.apache.polaris.core.persistence.pagination.PageToken;
 import org.apache.polaris.core.persistence.resolver.ResolvedPathKey;
-import org.apache.polaris.core.persistence.resolver.Resolver;
 import org.apache.polaris.core.persistence.resolver.ResolverFactory;
 import org.apache.polaris.core.persistence.resolver.ResolverPath;
-import org.apache.polaris.core.persistence.resolver.ResolverStatus;
-import org.apache.polaris.core.rest.NamespaceUtils;
-import org.apache.polaris.core.rest.PolarisEndpoints;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.core.storage.StorageAccessConfig;
 import org.apache.polaris.core.storage.StorageUtil;
@@ -166,38 +154,6 @@ import org.slf4j.LoggerFactory;
 public abstract class IcebergCatalogHandler extends CatalogHandler implements AutoCloseable {
   private static final Logger LOGGER = LoggerFactory.getLogger(IcebergCatalogHandler.class);
 
-  private static final Set<Endpoint> DEFAULT_ENDPOINTS =
-      ImmutableSet.<Endpoint>builder()
-          .add(Endpoint.V1_LIST_NAMESPACES)
-          .add(Endpoint.V1_LOAD_NAMESPACE)
-          .add(Endpoint.V1_NAMESPACE_EXISTS)
-          .add(Endpoint.V1_CREATE_NAMESPACE)
-          .add(Endpoint.V1_UPDATE_NAMESPACE)
-          .add(Endpoint.V1_DELETE_NAMESPACE)
-          .add(Endpoint.V1_LIST_TABLES)
-          .add(Endpoint.V1_LOAD_TABLE)
-          .add(Endpoint.V1_TABLE_EXISTS)
-          .add(Endpoint.V1_CREATE_TABLE)
-          .add(Endpoint.V1_UPDATE_TABLE)
-          .add(Endpoint.V1_DELETE_TABLE)
-          .add(Endpoint.V1_RENAME_TABLE)
-          .add(Endpoint.V1_REGISTER_TABLE)
-          .add(Endpoint.V1_REPORT_METRICS)
-          .add(Endpoint.V1_COMMIT_TRANSACTION)
-          .build();
-
-  private static final Set<Endpoint> VIEW_ENDPOINTS =
-      ImmutableSet.<Endpoint>builder()
-          .add(Endpoint.V1_LIST_VIEWS)
-          .add(Endpoint.V1_LOAD_VIEW)
-          .add(Endpoint.V1_VIEW_EXISTS)
-          .add(Endpoint.V1_CREATE_VIEW)
-          .add(Endpoint.V1_UPDATE_VIEW)
-          .add(Endpoint.V1_DELETE_VIEW)
-          .add(Endpoint.V1_RENAME_VIEW)
-          .add(Endpoint.V1_REGISTER_VIEW)
-          .build();
-
   protected abstract PolarisDiagnostics diagnostics();
 
   protected abstract PolarisCredentialManager credentialManager();
@@ -1619,37 +1575,6 @@ public abstract class IcebergCatalogHandler extends CatalogHandler implements Au
     }
   }
 
-  public ConfigResponse getConfig() {
-    Resolver resolver = resolverFactory().createResolver(polarisPrincipal(), catalogName());
-    ResolverStatus resolverStatus = resolver.resolveAll();
-    if (!resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) {
-      throw new NotFoundException("Unable to find warehouse %s", catalogName());
-    }
-    ResolvedPolarisEntity resolvedReferenceCatalog = resolver.getResolvedReferenceCatalog();
-    Map<String, String> properties =
-        PolarisEntity.of(resolvedReferenceCatalog.getEntity()).getPropertiesAsMap();
-
-    return ConfigResponse.builder()
-        .withDefaults(properties) // catalog properties are defaults
-        .withOverrides(
-            ImmutableMap.of(
-                "prefix",
-                prefixParser().catalogNameToPrefix(catalogName()),
-                // Polaris does not handle custom namespace separators;
-                // always communicate the default namespace separator to clients.
-                RESTCatalogProperties.NAMESPACE_SEPARATOR,
-                NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR_ENCODED))
-        .withEndpoints(
-            ImmutableList.<Endpoint>builder()
-                .addAll(DEFAULT_ENDPOINTS)
-                .addAll(VIEW_ENDPOINTS)
-                .addAll(PolarisEndpoints.getSupportedGenericTableEndpoints(realmConfig()))
-                .addAll(PolarisEndpoints.getSupportedPolicyEndpoints(realmConfig()))
-                .addAll(PolarisEndpoints.getSupportedSemanticModelEndpoints(realmConfig()))
-                .build())
-        .build();
-  }
-
   private StorageAccessConfig vendCredentials(
       TableIdentifier tableIdentifier,
       Set<String> tableLocations,
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRestConfigEndpoints.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRestConfigEndpoints.java
new file mode 100644
index 0000000000..f2da605c3a
--- /dev/null
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRestConfigEndpoints.java
@@ -0,0 +1,55 @@
+/*
+ * 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.catalog.iceberg;
+
+import com.google.common.collect.ImmutableSet;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.polaris.core.rest.CatalogConfigEndpointContributor;
+
+@ApplicationScoped
+@Priority(100)
+public class IcebergRestConfigEndpoints implements CatalogConfigEndpointContributor {
+  public static final Set<Endpoint> ICEBERG_REST_ENDPOINTS =
+      ImmutableSet.<Endpoint>builder()
+          .add(Endpoint.V1_LIST_NAMESPACES)
+          .add(Endpoint.V1_LOAD_NAMESPACE)
+          .add(Endpoint.V1_NAMESPACE_EXISTS)
+          .add(Endpoint.V1_CREATE_NAMESPACE)
+          .add(Endpoint.V1_UPDATE_NAMESPACE)
+          .add(Endpoint.V1_DELETE_NAMESPACE)
+          .add(Endpoint.V1_LIST_TABLES)
+          .add(Endpoint.V1_LOAD_TABLE)
+          .add(Endpoint.V1_TABLE_EXISTS)
+          .add(Endpoint.V1_CREATE_TABLE)
+          .add(Endpoint.V1_UPDATE_TABLE)
+          .add(Endpoint.V1_DELETE_TABLE)
+          .add(Endpoint.V1_RENAME_TABLE)
+          .add(Endpoint.V1_REGISTER_TABLE)
+          .add(Endpoint.V1_REPORT_METRICS)
+          .add(Endpoint.V1_COMMIT_TRANSACTION)
+          .build();
+
+  @Override
+  public Set<Endpoint> endpoints() {
+    return ICEBERG_REST_ENDPOINTS;
+  }
+}
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergViewConfigEndpoints.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergViewConfigEndpoints.java
new file mode 100644
index 0000000000..aecf88d730
--- /dev/null
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergViewConfigEndpoints.java
@@ -0,0 +1,47 @@
+/*
+ * 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.catalog.iceberg;
+
+import com.google.common.collect.ImmutableSet;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.polaris.core.rest.CatalogConfigEndpointContributor;
+
+@ApplicationScoped
+@Priority(200)
+public class IcebergViewConfigEndpoints implements CatalogConfigEndpointContributor {
+  public static final Set<Endpoint> ICEBERG_VIEW_ENDPOINTS =
+      ImmutableSet.<Endpoint>builder()
+          .add(Endpoint.V1_LIST_VIEWS)
+          .add(Endpoint.V1_LOAD_VIEW)
+          .add(Endpoint.V1_VIEW_EXISTS)
+          .add(Endpoint.V1_CREATE_VIEW)
+          .add(Endpoint.V1_UPDATE_VIEW)
+          .add(Endpoint.V1_DELETE_VIEW)
+          .add(Endpoint.V1_RENAME_VIEW)
+          .add(Endpoint.V1_REGISTER_VIEW)
+          .build();
+
+  @Override
+  public Set<Endpoint> endpoints() {
+    return ICEBERG_VIEW_ENDPOINTS;
+  }
+}
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyConfigEndpoints.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyConfigEndpoints.java
new file mode 100644
index 0000000000..f17927a40d
--- /dev/null
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyConfigEndpoints.java
@@ -0,0 +1,54 @@
+/*
+ * 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.catalog.policy;
+
+import com.google.common.collect.ImmutableSet;
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.RealmConfig;
+import org.apache.polaris.core.rest.CatalogConfigEndpointContributor;
+
+@ApplicationScoped
+@Priority(400)
+public class PolicyConfigEndpoints implements CatalogConfigEndpointContributor {
+  private final RealmConfig realmConfig;
+
+  @Inject
+  public PolicyConfigEndpoints(RealmConfig realmConfig) {
+    this.realmConfig = realmConfig;
+  }
+
+  @Override
+  public Set<Endpoint> endpoints() {
+    return getSupportedPolicyEndpoints(realmConfig);
+  }
+
+  /**
+   * Get the policy store endpoints. Returns POLICY_ENDPOINTS if ENABLE_POLICY_STORE is set to true,
+   * otherwise, returns an empty set.
+   */
+  public static Set<Endpoint> getSupportedPolicyEndpoints(RealmConfig realmConfig) {
+    boolean policyStoreEnabled = realmConfig.getConfig(FeatureConfiguration.ENABLE_POLICY_STORE);
+    return policyStoreEnabled ? PolicyEndpoints.POLICY_STORE_ENDPOINTS : ImmutableSet.of();
+  }
+}
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyEndpoints.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyEndpoints.java
new file mode 100644
index 0000000000..15d413afe3
--- /dev/null
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyEndpoints.java
@@ -0,0 +1,57 @@
+/*
+ * 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.catalog.policy;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.apache.iceberg.rest.Endpoint;
+import org.apache.polaris.core.rest.PolarisResourcePaths;
+
+public class PolicyEndpoints {
+  private PolicyEndpoints() {}
+
+  public static final Endpoint V1_LIST_POLICIES =
+      Endpoint.create("GET", PolarisResourcePaths.V1_POLICIES);
+  public static final Endpoint V1_CREATE_POLICY =
+      Endpoint.create("POST", PolarisResourcePaths.V1_POLICIES);
+  public static final Endpoint V1_LOAD_POLICY =
+      Endpoint.create("GET", PolarisResourcePaths.V1_POLICY);
+  public static final Endpoint V1_UPDATE_POLICY =
+      Endpoint.create("PUT", PolarisResourcePaths.V1_POLICY);
+  public static final Endpoint V1_DROP_POLICY =
+      Endpoint.create("DELETE", PolarisResourcePaths.V1_POLICY);
+  public static final Endpoint V1_ATTACH_POLICY =
+      Endpoint.create("PUT", PolarisResourcePaths.V1_POLICY_MAPPINGS);
+  public static final Endpoint V1_DETACH_POLICY =
+      Endpoint.create("POST", PolarisResourcePaths.V1_POLICY_MAPPINGS);
+  public static final Endpoint V1_GET_APPLICABLE_POLICIES =
+      Endpoint.create("GET", PolarisResourcePaths.V1_APPLICABLE_POLICIES);
+
+  public static final Set<Endpoint> POLICY_STORE_ENDPOINTS =
+      ImmutableSet.<Endpoint>builder()
+          .add(V1_LIST_POLICIES)
+          .add(V1_CREATE_POLICY)
+          .add(V1_LOAD_POLICY)
+          .add(V1_UPDATE_POLICY)
+          .add(V1_DROP_POLICY)
+          .add(V1_ATTACH_POLICY)
+          .add(V1_DETACH_POLICY)
+          .add(V1_GET_APPLICABLE_POLICIES)
+          .build();
+}
diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/GetConfigTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/GetConfigTest.java
index 8885fcd467..a7a0cb6fe1 100644
--- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/GetConfigTest.java
+++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/GetConfigTest.java
@@ -25,6 +25,7 @@ import jakarta.ws.rs.core.Response;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import org.apache.iceberg.rest.Endpoint;
 import org.apache.iceberg.rest.responses.ConfigResponse;
 import org.apache.polaris.core.admin.model.Catalog;
 import org.apache.polaris.core.admin.model.CatalogProperties;
@@ -32,8 +33,9 @@ import org.apache.polaris.core.admin.model.CreateCatalogRequest;
 import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
 import org.apache.polaris.core.admin.model.PolarisCatalog;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
-import org.apache.polaris.core.rest.PolarisEndpoints;
+import org.apache.polaris.core.rest.GenericTableEndpoints;
 import org.apache.polaris.service.TestServices;
+import org.apache.polaris.service.catalog.policy.PolicyEndpoints;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
@@ -80,20 +82,49 @@ public class GetConfigTest {
             .restConfigurationApi()
             .getConfig(catalogName, services.realmContext(), services.securityContext());
     ConfigResponse configResponse = response.readEntity(ConfigResponse.class);
+
     assertThat(configResponse.overrides()).contains(Map.entry("prefix", catalogName));
+    assertThat(configResponse.endpoints()).contains(PolicyEndpoints.V1_CREATE_POLICY);
+    assertEndpointOrder(configResponse, enableGenericTable);
+    assertGenericTableEndpoints(configResponse, enableGenericTable);
+  }
+
+  private static void assertEndpointOrder(
+      ConfigResponse configResponse, boolean enableGenericTable) {
     if (enableGenericTable) {
-      assertThat(configResponse.endpoints()).contains(PolarisEndpoints.V1_CREATE_GENERIC_TABLE);
-      assertThat(configResponse.endpoints()).contains(PolarisEndpoints.V1_DELETE_GENERIC_TABLE);
-      assertThat(configResponse.endpoints()).contains(PolarisEndpoints.V1_LIST_GENERIC_TABLES);
-      assertThat(configResponse.endpoints()).contains(PolarisEndpoints.V1_LOAD_GENERIC_TABLE);
+      assertThat(configResponse.endpoints())
+          .containsSubsequence(
+              Endpoint.V1_LIST_NAMESPACES,
+              Endpoint.V1_REGISTER_VIEW,
+              GenericTableEndpoints.V1_CREATE_GENERIC_TABLE,
+              PolicyEndpoints.V1_CREATE_POLICY);
     } else {
       assertThat(configResponse.endpoints())
-          .doesNotContain(PolarisEndpoints.V1_CREATE_GENERIC_TABLE);
+          .containsSubsequence(
+              Endpoint.V1_LIST_NAMESPACES,
+              Endpoint.V1_REGISTER_VIEW,
+              PolicyEndpoints.V1_CREATE_POLICY);
+    }
+  }
+
+  private static void assertGenericTableEndpoints(
+      ConfigResponse configResponse, boolean enableGenericTable) {
+    if (enableGenericTable) {
+      assertThat(configResponse.endpoints())
+          .contains(GenericTableEndpoints.V1_CREATE_GENERIC_TABLE);
+      assertThat(configResponse.endpoints())
+          .contains(GenericTableEndpoints.V1_DELETE_GENERIC_TABLE);
+      assertThat(configResponse.endpoints()).contains(GenericTableEndpoints.V1_LIST_GENERIC_TABLES);
+      assertThat(configResponse.endpoints()).contains(GenericTableEndpoints.V1_LOAD_GENERIC_TABLE);
+    } else {
+      assertThat(configResponse.endpoints())
+          .doesNotContain(GenericTableEndpoints.V1_CREATE_GENERIC_TABLE);
+      assertThat(configResponse.endpoints())
+          .doesNotContain(GenericTableEndpoints.V1_DELETE_GENERIC_TABLE);
       assertThat(configResponse.endpoints())
-          .doesNotContain(PolarisEndpoints.V1_DELETE_GENERIC_TABLE);
+          .doesNotContain(GenericTableEndpoints.V1_LIST_GENERIC_TABLES);
       assertThat(configResponse.endpoints())
-          .doesNotContain(PolarisEndpoints.V1_LIST_GENERIC_TABLES);
-      assertThat(configResponse.endpoints()).doesNotContain(PolarisEndpoints.V1_LOAD_GENERIC_TABLE);
+          .doesNotContain(GenericTableEndpoints.V1_LOAD_GENERIC_TABLE);
     }
   }
 }
diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index e0b9d5ae86..6fc92e0078 100644
--- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -24,6 +24,8 @@ import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import com.google.auth.oauth2.AccessToken;
 import com.google.auth.oauth2.GoogleCredentials;
 import jakarta.enterprise.inject.Instance;
+import jakarta.enterprise.inject.Instance.Handle;
+import jakarta.enterprise.inject.spi.Bean;
 import jakarta.ws.rs.core.SecurityContext;
 import java.security.Principal;
 import java.time.Clock;
@@ -34,6 +36,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
+import java.util.stream.Stream;
 import org.apache.polaris.core.PolarisCallContext;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
@@ -60,6 +63,7 @@ import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactoryImpl;
 import org.apache.polaris.core.persistence.resolver.Resolver;
 import org.apache.polaris.core.persistence.resolver.ResolverFactory;
+import org.apache.polaris.core.rest.CatalogConfigEndpointContributor;
 import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.core.secrets.UserSecretsManagerFactory;
 import org.apache.polaris.core.storage.cache.StorageCredentialCache;
@@ -75,21 +79,26 @@ import org.apache.polaris.service.catalog.api.IcebergRestConfigurationApi;
 import org.apache.polaris.service.catalog.api.IcebergRestConfigurationApiService;
 import org.apache.polaris.service.catalog.api.PolarisCatalogGenericTableApi;
 import org.apache.polaris.service.catalog.api.PolarisCatalogGenericTableApiService;
+import org.apache.polaris.service.catalog.config.CatalogConfigHandler;
 import org.apache.polaris.service.catalog.generic.CatalogGenericTableEventServiceDelegator;
 import org.apache.polaris.service.catalog.generic.GenericTableCatalogAdapter;
 import org.apache.polaris.service.catalog.generic.GenericTableCatalogHandler;
 import org.apache.polaris.service.catalog.generic.GenericTableCatalogHandlerFactory;
+import org.apache.polaris.service.catalog.generic.GenericTableConfigEndpoints;
 import org.apache.polaris.service.catalog.generic.ImmutableGenericTableCatalogHandler;
 import org.apache.polaris.service.catalog.iceberg.CatalogHandlerUtils;
 import org.apache.polaris.service.catalog.iceberg.IcebergCatalogAdapter;
 import org.apache.polaris.service.catalog.iceberg.IcebergCatalogHandler;
 import org.apache.polaris.service.catalog.iceberg.IcebergCatalogHandlerFactory;
 import org.apache.polaris.service.catalog.iceberg.IcebergRestCatalogEventServiceDelegator;
+import org.apache.polaris.service.catalog.iceberg.IcebergRestConfigEndpoints;
 import org.apache.polaris.service.catalog.iceberg.IcebergRestConfigurationEventServiceDelegator;
+import org.apache.polaris.service.catalog.iceberg.IcebergViewConfigEndpoints;
 import org.apache.polaris.service.catalog.iceberg.ImmutableIcebergCatalogHandler;
 import org.apache.polaris.service.catalog.io.FileIOFactory;
 import org.apache.polaris.service.catalog.io.MeasuredFileIOFactory;
 import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
+import org.apache.polaris.service.catalog.policy.PolicyConfigEndpoints;
 import org.apache.polaris.service.config.ReservedProperties;
 import org.apache.polaris.service.context.catalog.PolarisLocalCatalogFactory;
 import org.apache.polaris.service.credentials.DefaultPolarisCredentialManager;
@@ -374,6 +383,34 @@ public record TestServices(
 
       EventAttributeMap eventAttributeMap = new EventAttributeMap();
 
+      Supplier<CatalogConfigHandler> catalogConfigHandlerSupplier =
+          () -> {
+            @SuppressWarnings("unchecked")
+            Instance<CatalogConfigEndpointContributor> configEndpointContributors =
+                Mockito.mock(Instance.class);
+            CatalogConfigEndpointContributor icebergRestEndpoints =
+                new IcebergRestConfigEndpoints();
+            CatalogConfigEndpointContributor icebergViewEndpoints =
+                new IcebergViewConfigEndpoints();
+            CatalogConfigEndpointContributor genericTableEndpoints =
+                new GenericTableConfigEndpoints(realmConfig);
+            CatalogConfigEndpointContributor policyEndpoints =
+                new PolicyConfigEndpoints(realmConfig);
+            Mockito.when(configEndpointContributors.handlesStream())
+                .thenAnswer(
+                    invocation ->
+                        Stream.of(
+                            endpointContributorHandle(
+                                GenericTableConfigEndpoints.class, genericTableEndpoints),
+                            endpointContributorHandle(PolicyConfigEndpoints.class, policyEndpoints),
+                            endpointContributorHandle(
+                                IcebergRestConfigEndpoints.class, icebergRestEndpoints),
+                            endpointContributorHandle(
+                                IcebergViewConfigEndpoints.class, icebergViewEndpoints)));
+            return new CatalogConfigHandler(
+                new DefaultCatalogPrefixParser(), resolverFactory, configEndpointContributors);
+          };
+
       Supplier<IcebergCatalogAdapter> catalogAdapterSupplier =
           () -> {
             IcebergCatalogHandlerFactory handlerFactory =
@@ -407,7 +444,11 @@ public record TestServices(
                 };
 
             return new IcebergCatalogAdapter(
-                callContext, new DefaultCatalogPrefixParser(), reservedProperties, handlerFactory);
+                callContext,
+                new DefaultCatalogPrefixParser(),
+                reservedProperties,
+                handlerFactory,
+                catalogConfigHandlerSupplier.get());
           };
 
       Supplier<IcebergRestCatalogApi> restApiSupplier =
@@ -518,6 +559,18 @@ public record TestServices(
     }
   }
 
+  @SuppressWarnings("unchecked")
+  private static Handle<CatalogConfigEndpointContributor> endpointContributorHandle(
+      Class<? extends CatalogConfigEndpointContributor> beanClass,
+      CatalogConfigEndpointContributor contributor) {
+    Handle<CatalogConfigEndpointContributor> handle = Mockito.mock(Handle.class);
+    Bean<CatalogConfigEndpointContributor> bean = Mockito.mock(Bean.class);
+    Mockito.doReturn(beanClass).when(bean).getBeanClass();
+    Mockito.when(handle.getBean()).thenReturn(bean);
+    Mockito.when(handle.get()).thenReturn(contributor);
+    return handle;
+  }
+
   public PolarisCallContext newCallContext() {
     return new PolarisCallContext(
         realmContext,