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

dimas 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 1f3023d5 Reorder filters and let RealmContextResolver throw 
UnresolvableRealmContextException (#848)
1f3023d5 is described below

commit 1f3023d5212bad53e53a5db9ce878d1d630901ec
Author: Alexandre Dutra <[email protected]>
AuthorDate: Thu Feb 6 04:08:40 2025 +0100

    Reorder filters and let RealmContextResolver throw 
UnresolvableRealmContextException (#848)
---
 .../polaris/service/it/env/IcebergHelper.java      |  4 +-
 .../service/it/env/PolarisApiEndpoints.java        | 18 +++--
 .../org/apache/polaris/service/it/env/RestApi.java | 19 ++++-
 .../org/apache/polaris/service/it/env/Server.java  | 12 ++-
 .../it/ext/PolarisIntegrationTestExtension.java    |  3 +-
 .../it/test/PolarisApplicationIntegrationTest.java | 72 +++++++++++++++++-
 .../it/test/PolarisSparkIntegrationTest.java       |  3 +-
 .../service/it/env/PolarisApiEndpointsTest.java    |  2 +-
 .../src/main/resources/application-it.properties   |  2 +
 .../src/main/resources/application.properties      |  1 +
 .../quarkus/config/QuarkusFilterPriorities.java    | 12 ++-
 .../service/quarkus/config/QuarkusProducers.java   | 14 +---
 .../quarkus/logging/QuarkusLoggingMDCFilter.java   | 52 +++++--------
 .../quarkus/metrics/RealmIdTagContributor.java     | 16 ++--
 .../quarkus/tracing/QuarkusTracingFilter.java      | 29 +++++---
 .../it/QuarkusApplicationIntegrationTest.java      |  4 +-
 .../service/quarkus/it/QuarkusServerManager.java   |  7 --
 .../auth/PolarisPrincipalAuthenticatorFilter.java  |  4 +-
 .../auth/PolarisPrincipalRolesProviderFilter.java  |  4 +-
 .../PolarisFilterPriorities.java}                  | 14 ++--
 .../context/DefaultRealmContextResolver.java       | 23 +++---
 .../service/context/RealmContextConfiguration.java |  9 +++
 .../RealmContextFilter.java}                       | 38 ++++------
 .../service/context/RealmContextResolver.java      | 14 +++-
 .../service/context/TestRealmContextResolver.java  | 13 ++--
 ...java => UnresolvableRealmContextException.java} | 14 +++-
 .../service/exception/PolarisExceptionMapper.java  |  3 +
 .../service/ratelimiter/RateLimiterFilter.java     |  4 +-
 .../context/DefaultRealmIdResolverTest.java        | 85 ++++++++++++++++++++++
 .../configuring-polaris-for-production.md          | 18 ++++-
 30 files changed, 358 insertions(+), 155 deletions(-)

diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/IcebergHelper.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/IcebergHelper.java
index f894489b..4d0c987b 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/IcebergHelper.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/IcebergHelper.java
@@ -18,8 +18,6 @@
  */
 package org.apache.polaris.service.it.env;
 
-import static 
org.apache.polaris.service.it.env.PolarisApiEndpoints.REALM_HEADER;
-
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
 import org.apache.iceberg.catalog.SessionCatalog;
@@ -56,7 +54,7 @@ public final class IcebergHelper {
                 org.apache.iceberg.CatalogProperties.FILE_IO_IMPL,
                 "org.apache.iceberg.inmemory.InMemoryFileIO")
             .put("warehouse", catalog)
-            .put("header." + REALM_HEADER, endpoints.realm())
+            .put("header." + endpoints.realmHeaderName(), endpoints.realmId())
             .putAll(extraProperties);
 
     restCatalog.initialize("polaris", propertiesBuilder.buildKeepingLast());
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisApiEndpoints.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisApiEndpoints.java
index 392b21f2..7212dd80 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisApiEndpoints.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisApiEndpoints.java
@@ -28,14 +28,14 @@ import java.net.URI;
  */
 public final class PolarisApiEndpoints implements Serializable {
 
-  public static String REALM_HEADER = "realm";
-
   private final URI baseUri;
-  private final String realm;
+  private final String realmId;
+  private final String realmHeaderName;
 
-  public PolarisApiEndpoints(URI baseUri, String realm) {
+  public PolarisApiEndpoints(URI baseUri, String realmId, String 
realmHeaderName) {
     this.baseUri = baseUri;
-    this.realm = realm;
+    this.realmId = realmId;
+    this.realmHeaderName = realmHeaderName;
   }
 
   public URI catalogApiEndpoint() {
@@ -46,7 +46,11 @@ public final class PolarisApiEndpoints implements 
Serializable {
     return baseUri.resolve(baseUri.getRawPath() + 
"/api/management").normalize();
   }
 
-  public String realm() {
-    return realm;
+  public String realmId() {
+    return realmId;
+  }
+
+  public String realmHeaderName() {
+    return realmHeaderName;
   }
 }
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/RestApi.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/RestApi.java
index e862fc2c..c0475f08 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/RestApi.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/RestApi.java
@@ -22,6 +22,7 @@ import jakarta.ws.rs.client.Client;
 import jakarta.ws.rs.client.Invocation;
 import jakarta.ws.rs.client.WebTarget;
 import java.net.URI;
+import java.util.HashMap;
 import java.util.Map;
 
 /** Base class for API helper classes. */
@@ -48,6 +49,19 @@ public class RestApi {
 
   public Invocation.Builder request(
       String path, Map<String, String> templateValues, Map<String, String> 
queryParams) {
+    Map<String, String> headers = new HashMap<>();
+    headers.put(endpoints.realmHeaderName(), endpoints.realmId());
+    if (authToken != null) {
+      headers.put("Authorization", "Bearer " + authToken);
+    }
+    return request(path, templateValues, queryParams, headers);
+  }
+
+  public Invocation.Builder request(
+      String path,
+      Map<String, String> templateValues,
+      Map<String, String> queryParams,
+      Map<String, String> headers) {
     WebTarget target = client.target(uri).path(path);
     for (Map.Entry<String, String> entry : templateValues.entrySet()) {
       target = target.resolveTemplate(entry.getKey(), entry.getValue());
@@ -56,10 +70,7 @@ public class RestApi {
       target = target.queryParam(entry.getKey(), entry.getValue());
     }
     Invocation.Builder request = target.request("application/json");
-    request = request.header(PolarisApiEndpoints.REALM_HEADER, 
endpoints.realm());
-    if (authToken != null) {
-      request = request.header("Authorization", "Bearer " + authToken);
-    }
+    headers.forEach(request::header);
     return request;
   }
 }
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/Server.java 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/Server.java
index 22fecf16..4fea8007 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/Server.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/Server.java
@@ -25,7 +25,17 @@ import java.net.URI;
  * the provided admin credentials or create new principals.
  */
 public interface Server extends AutoCloseable {
-  String realmId();
+
+  String DEFAULT_REALM_HEADER = "Polaris-Realm";
+  String DEFAULT_REALM_ID = "POLARIS";
+
+  default String realmId() {
+    return DEFAULT_REALM_ID;
+  }
+
+  default String realmHeaderName() {
+    return DEFAULT_REALM_HEADER;
+  }
 
   /**
    * The base URI to all Polaris APIs (e.g. the common base of the Iceberg 
REST API endpoints and
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/ext/PolarisIntegrationTestExtension.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/ext/PolarisIntegrationTestExtension.java
index e2b6c43b..d0504002 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/ext/PolarisIntegrationTestExtension.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/ext/PolarisIntegrationTestExtension.java
@@ -92,7 +92,8 @@ public class PolarisIntegrationTestExtension implements 
ParameterResolver {
 
     private Env(Server server) {
       this.server = server;
-      this.endpoints = new PolarisApiEndpoints(server.baseUri(), 
server.realmId());
+      this.endpoints =
+          new PolarisApiEndpoints(server.baseUri(), server.realmId(), 
server.realmHeaderName());
     }
 
     PolarisApiEndpoints endpoints() {
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
index 894926fa..7a2961e4 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.polaris.service.it.test;
 
-import static 
org.apache.polaris.service.it.env.PolarisApiEndpoints.REALM_HEADER;
 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.assertThatThrownBy;
@@ -28,6 +27,7 @@ import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.client.Invocation;
 import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -58,12 +58,14 @@ import org.apache.iceberg.hadoop.HadoopFileIO;
 import org.apache.iceberg.io.ResolvingFileIO;
 import org.apache.iceberg.rest.RESTSessionCatalog;
 import org.apache.iceberg.rest.auth.OAuth2Properties;
+import org.apache.iceberg.rest.responses.ErrorResponse;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.util.EnvironmentUtil;
 import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
 import org.apache.polaris.core.admin.model.Catalog;
 import org.apache.polaris.core.admin.model.CatalogProperties;
 import org.apache.polaris.core.admin.model.CatalogRole;
+import org.apache.polaris.core.admin.model.Catalogs;
 import org.apache.polaris.core.admin.model.ExternalCatalog;
 import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
 import org.apache.polaris.core.admin.model.PolarisCatalog;
@@ -85,6 +87,8 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * @implSpec This test expects the server to be configured with the following 
features configured:
@@ -97,6 +101,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
  *     </ul>
  *     The server must also be configured to reject request body sizes larger 
than 1MB (1000000
  *     bytes).
+ *     <p>The server must also be configured with the following realms: 
POLARIS (default), and
+ *     OTHER.
  */
 @ExtendWith(PolarisIntegrationTestExtension.class)
 public class PolarisApplicationIntegrationTest {
@@ -121,7 +127,7 @@ public class PolarisApplicationIntegrationTest {
       throws IOException {
     endpoints = apiEndpoints;
     client = polarisClient(endpoints);
-    realm = endpoints.realm();
+    realm = endpoints.realmId();
     admin = adminCredentials;
     clientCredentials = adminCredentials.credentials();
     authToken = client.obtainToken(clientCredentials);
@@ -246,7 +252,7 @@ public class PolarisApplicationIntegrationTest {
             authToken,
             "warehouse",
             catalog,
-            "header." + REALM_HEADER,
+            "header." + endpoints.realmHeaderName(),
             realm));
     return sessionCatalog;
   }
@@ -588,7 +594,7 @@ public class PolarisApplicationIntegrationTest {
                           authToken,
                           "warehouse",
                           emptyEnvironmentVariable,
-                          "header." + REALM_HEADER,
+                          "header." + endpoints.realmHeaderName(),
                           realm)))
           .isInstanceOf(BadRequestException.class)
           .hasMessage("Malformed request: Please specify a warehouse");
@@ -657,4 +663,62 @@ public class PolarisApplicationIntegrationTest {
               });
     }
   }
+
+  @Test
+  public void testNoRealmHeader() {
+    try (Response response =
+        managementApi
+            .request(
+                "v1/catalogs", Map.of(), Map.of(), Map.of("Authorization", 
"Bearer " + authToken))
+            .get()) {
+      assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
+      Catalogs roles = response.readEntity(Catalogs.class);
+      
assertThat(roles.getCatalogs()).extracting(Catalog::getName).contains(internalCatalogName);
+    }
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"POLARIS", "OTHER"})
+  public void testRealmHeaderValid(String realmId) {
+    String catalogName = client.newEntityName("testRealmHeaderValid" + 
realmId);
+    createCatalog(catalogName, Catalog.TypeEnum.INTERNAL, principalRoleName);
+    try (Response response =
+        managementApi
+            .request(
+                "v1/catalogs",
+                Map.of(),
+                Map.of(),
+                Map.of(
+                    "Authorization", "Bearer " + authToken, 
endpoints.realmHeaderName(), realmId))
+            .get()) {
+      assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
+      Catalogs catalogsList = response.readEntity(Catalogs.class);
+      if ("POLARIS".equals(realmId)) {
+        
assertThat(catalogsList.getCatalogs()).extracting(Catalog::getName).contains(catalogName);
+      } else {
+        assertThat(catalogsList.getCatalogs()).isEmpty();
+      }
+    }
+  }
+
+  @Test
+  public void testRealmHeaderInvalid() {
+    try (Response response =
+        managementApi
+            .request(
+                "v1/catalogs",
+                Map.of(),
+                Map.of(),
+                Map.of(
+                    "Authorization", "Bearer " + authToken, 
endpoints.realmHeaderName(), "INVALID"))
+            .get()) {
+      
assertThat(response.getStatus()).isEqualTo(Status.NOT_FOUND.getStatusCode());
+      assertThat(response.readEntity(ErrorResponse.class))
+          .extracting(ErrorResponse::code, ErrorResponse::type, 
ErrorResponse::message)
+          .containsExactly(
+              Status.NOT_FOUND.getStatusCode(),
+              "UnresolvableRealmContextException",
+              "Unknown realm: INVALID");
+    }
+  }
 }
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
index 325982cf..e7fa2dce 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisSparkIntegrationTest.java
@@ -204,7 +204,8 @@ public class PolarisSparkIntegrationTest {
             endpoints.catalogApiEndpoint().toString())
         .config(String.format("spark.sql.catalog.%s.warehouse", catalogName), 
catalogName)
         .config(String.format("spark.sql.catalog.%s.scope", catalogName), 
"PRINCIPAL_ROLE:ALL")
-        .config(String.format("spark.sql.catalog.%s.header.realm", 
catalogName), endpoints.realm())
+        .config(
+            String.format("spark.sql.catalog.%s.header.realm", catalogName), 
endpoints.realmId())
         .config(String.format("spark.sql.catalog.%s.token", catalogName), 
sparkToken)
         .config(String.format("spark.sql.catalog.%s.s3.access-key-id", 
catalogName), "fakekey")
         .config(
diff --git 
a/integration-tests/src/test/java/org/apache/polaris/service/it/env/PolarisApiEndpointsTest.java
 
b/integration-tests/src/test/java/org/apache/polaris/service/it/env/PolarisApiEndpointsTest.java
index 32877e58..1ba9d9db 100644
--- 
a/integration-tests/src/test/java/org/apache/polaris/service/it/env/PolarisApiEndpointsTest.java
+++ 
b/integration-tests/src/test/java/org/apache/polaris/service/it/env/PolarisApiEndpointsTest.java
@@ -27,7 +27,7 @@ public class PolarisApiEndpointsTest {
   @Test
   void testEndpointRespectsPathPrefix() {
     PolarisApiEndpoints endpoints =
-        new PolarisApiEndpoints(URI.create("http://myserver.com/polaris";), "");
+        new PolarisApiEndpoints(URI.create("http://myserver.com/polaris";), "", 
"Polaris-Realm");
     Assertions.assertEquals(
         "http://myserver.com/polaris/api/catalog";, 
endpoints.catalogApiEndpoint().toString());
     Assertions.assertEquals(
diff --git a/quarkus/defaults/src/main/resources/application-it.properties 
b/quarkus/defaults/src/main/resources/application-it.properties
index 2a19f7d1..5f46d203 100644
--- a/quarkus/defaults/src/main/resources/application-it.properties
+++ b/quarkus/defaults/src/main/resources/application-it.properties
@@ -37,6 +37,8 @@ 
polaris.features.defaults."INITIALIZE_DEFAULT_CATALOG_FILEIO_FOR_it"=true
 polaris.features.defaults."SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION"=true
 
polaris.features.defaults."SUPPORTED_CATALOG_STORAGE_TYPES"=["FILE","S3","GCS","AZURE"]
 
+polaris.realm-context.realms=POLARIS,OTHER
+
 polaris.storage.gcp.token=token
 polaris.storage.gcp.lifespan=PT1H
 
diff --git a/quarkus/defaults/src/main/resources/application.properties 
b/quarkus/defaults/src/main/resources/application.properties
index c40034f2..8b25cbcd 100644
--- a/quarkus/defaults/src/main/resources/application.properties
+++ b/quarkus/defaults/src/main/resources/application.properties
@@ -82,6 +82,7 @@ quarkus.test.integration-test-profile=it
 polaris.realm-context.type=default
 polaris.realm-context.realms=POLARIS
 polaris.realm-context.header-name=Polaris-Realm
+polaris.realm-context.require-header=false
 
 
polaris.features.defaults."ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING"=false
 
polaris.features.defaults."SUPPORTED_CATALOG_STORAGE_TYPES"=["S3","GCS","AZURE","FILE"]
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFilterPriorities.java
similarity index 70%
copy from 
service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
copy to 
quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFilterPriorities.java
index dae00f37..79d31003 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFilterPriorities.java
@@ -16,13 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.service.context;
+package org.apache.polaris.service.quarkus.config;
 
-import java.util.Map;
-import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.service.config.PolarisFilterPriorities;
 
-public interface RealmContextResolver {
-
-  RealmContext resolveRealmContext(
-      String requestURL, String method, String path, Map<String, String> 
headers);
+public final class QuarkusFilterPriorities {
+  public static final int MDC_FILTER = 
PolarisFilterPriorities.REALM_CONTEXT_FILTER + 1;
+  public static final int TRACING_FILTER = 
PolarisFilterPriorities.REALM_CONTEXT_FILTER + 2;
 }
diff --git 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
index d06af60a..5e69b89c 100644
--- 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java
@@ -21,7 +21,6 @@ package org.apache.polaris.service.quarkus.config;
 import io.quarkus.runtime.StartupEvent;
 import io.smallrye.common.annotation.Identifier;
 import io.smallrye.context.SmallRyeManagedExecutor;
-import io.vertx.core.http.HttpServerRequest;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.enterprise.context.RequestScoped;
 import jakarta.enterprise.event.Observes;
@@ -30,9 +29,9 @@ import jakarta.enterprise.inject.Disposes;
 import jakarta.enterprise.inject.Instance;
 import jakarta.enterprise.inject.Produces;
 import jakarta.inject.Singleton;
+import jakarta.ws.rs.container.ContainerRequestContext;
 import jakarta.ws.rs.core.Context;
 import java.time.Clock;
-import java.util.HashMap;
 import org.apache.polaris.core.PolarisConfigurationStore;
 import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
 import org.apache.polaris.core.PolarisDiagnostics;
@@ -53,6 +52,7 @@ import 
org.apache.polaris.service.catalog.api.IcebergRestOAuth2ApiService;
 import org.apache.polaris.service.catalog.io.FileIOFactory;
 import org.apache.polaris.service.config.RealmEntityManagerFactory;
 import org.apache.polaris.service.context.RealmContextConfiguration;
+import org.apache.polaris.service.context.RealmContextFilter;
 import org.apache.polaris.service.context.RealmContextResolver;
 import 
org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
 import 
org.apache.polaris.service.quarkus.auth.QuarkusAuthenticationConfiguration;
@@ -100,14 +100,8 @@ public class QuarkusProducers {
 
   @Produces
   @RequestScoped
-  public RealmContext realmContext(
-      @Context HttpServerRequest request, RealmContextResolver 
realmContextResolver) {
-    return realmContextResolver.resolveRealmContext(
-        request.absoluteURI(),
-        request.method().name(),
-        request.path(),
-        request.headers().entries().stream()
-            .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue()), 
HashMap::putAll));
+  public RealmContext realmContext(@Context ContainerRequestContext request) {
+    return (RealmContext) 
request.getProperty(RealmContextFilter.REALM_CONTEXT_KEY);
   }
 
   @Produces
diff --git 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java
index 98fb2888..3062fabd 100644
--- 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java
@@ -18,55 +18,43 @@
  */
 package org.apache.polaris.service.quarkus.logging;
 
-import io.quarkus.vertx.web.RouteFilter;
-import io.vertx.ext.web.RoutingContext;
+import static 
org.apache.polaris.service.context.RealmContextFilter.REALM_CONTEXT_KEY;
+
+import jakarta.annotation.Priority;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.PreMatching;
+import jakarta.ws.rs.ext.Provider;
 import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.service.quarkus.config.QuarkusFilterPriorities;
 import org.slf4j.MDC;
 
+@PreMatching
 @ApplicationScoped
-public class QuarkusLoggingMDCFilter {
-
-  public static final int PRIORITY = RouteFilter.DEFAULT_PRIORITY + 100;
+@Priority(QuarkusFilterPriorities.MDC_FILTER)
+@Provider
+public class QuarkusLoggingMDCFilter implements ContainerRequestFilter {
 
-  private static final String REQUEST_ID_KEY = "requestId";
-  private static final String REALM_ID_KEY = "realmId";
-
-  @Inject RealmContext realmContext;
+  public static final String REALM_ID_KEY = "realmId";
+  public static final String REQUEST_ID_KEY = "requestId";
 
   @Inject QuarkusLoggingConfiguration loggingConfiguration;
 
-  public static String requestId(RoutingContext rc) {
-    return rc.get(REQUEST_ID_KEY);
-  }
-
-  public static String realmId(RoutingContext rc) {
-    return rc.get(REALM_ID_KEY);
-  }
-
-  @RouteFilter(value = PRIORITY)
-  public void applyMDCContext(RoutingContext rc) {
+  @Override
+  public void filter(ContainerRequestContext rc) {
     // The request scope is active here, so any MDC values set here will be 
propagated to
     // threads handling the request.
     // Also put the MDC values in the request context for use by other filters 
and handlers
     loggingConfiguration.mdc().forEach(MDC::put);
-    loggingConfiguration.mdc().forEach(rc::put);
-    var requestId = 
rc.request().getHeader(loggingConfiguration.requestIdHeaderName());
+    loggingConfiguration.mdc().forEach(rc::setProperty);
+    var requestId = 
rc.getHeaderString(loggingConfiguration.requestIdHeaderName());
     if (requestId != null) {
       MDC.put(REQUEST_ID_KEY, requestId);
-      rc.put(REQUEST_ID_KEY, requestId);
+      rc.setProperty(REQUEST_ID_KEY, requestId);
     }
+    RealmContext realmContext = (RealmContext) 
rc.getProperty(REALM_CONTEXT_KEY);
     MDC.put(REALM_ID_KEY, realmContext.getRealmIdentifier());
-    rc.put(REALM_ID_KEY, realmContext.getRealmIdentifier());
-    // Do not explicitly remove the MDC values from the request context with 
an end handler,
-    // as this could remove MDC context still in use in TaskExecutor threads
-    //    rc.addEndHandler(
-    //        (v) -> {
-    //          MDC.remove(REQUEST_ID_MDC_KEY);
-    //          MDC.remove(REALM_ID_MDC_KEY);
-    //          loggingConfiguration.mdc().keySet().forEach(MDC::remove);
-    //        });
-    rc.next();
   }
 }
diff --git 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java
index 147ebd7f..8388be9e 100644
--- 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java
@@ -23,7 +23,6 @@ import 
io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor;
 import io.vertx.core.http.HttpServerRequest;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import java.util.HashMap;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.service.context.RealmContextResolver;
 
@@ -38,16 +37,17 @@ public class RealmIdTagContributor implements 
HttpServerMetricsTagsContributor {
   public Tags contribute(Context context) {
     // FIXME request scope does not work here, so we have to resolve the realm 
context manually
     HttpServerRequest request = context.request();
-    RealmContext realmContext = resolveRealmContext(request);
-    return Tags.of(TAG_REALM, realmContext.getRealmIdentifier());
+    try {
+      RealmContext realmContext = resolveRealmContext(request);
+      return Tags.of(TAG_REALM, realmContext.getRealmIdentifier());
+    } catch (Exception ignored) {
+      // ignore, the RealmContextFilter will handle the error
+      return Tags.empty();
+    }
   }
 
   private RealmContext resolveRealmContext(HttpServerRequest request) {
     return realmContextResolver.resolveRealmContext(
-        request.absoluteURI(),
-        request.method().name(),
-        request.path(),
-        request.headers().entries().stream()
-            .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue()), 
HashMap::putAll));
+        request.absoluteURI(), request.method().name(), request.path(), 
request.headers()::get);
   }
 }
diff --git 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java
 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java
index 28811ad9..6035cceb 100644
--- 
a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java
+++ 
b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java
@@ -19,32 +19,41 @@
 package org.apache.polaris.service.quarkus.tracing;
 
 import io.opentelemetry.api.trace.Span;
-import io.quarkus.vertx.web.RouteFilter;
-import io.vertx.ext.web.RoutingContext;
+import jakarta.annotation.Priority;
 import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.PreMatching;
+import jakarta.ws.rs.ext.Provider;
+import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.service.context.RealmContextFilter;
+import org.apache.polaris.service.quarkus.config.QuarkusFilterPriorities;
 import org.apache.polaris.service.quarkus.logging.QuarkusLoggingMDCFilter;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 
+@PreMatching
 @ApplicationScoped
-public class QuarkusTracingFilter {
+@Priority(QuarkusFilterPriorities.TRACING_FILTER)
+@Provider
+public class QuarkusTracingFilter implements ContainerRequestFilter {
 
   public static final String REQUEST_ID_ATTRIBUTE = "polaris.request.id";
-  public static final String REALM_ID_ATTRIBUTE = "polaris.realm";
+  public static final String REALM_ID_ATTRIBUTE = "polaris.realm.id";
 
   @ConfigProperty(name = "quarkus.otel.sdk.disabled")
   boolean sdkDisabled;
 
-  @RouteFilter(QuarkusLoggingMDCFilter.PRIORITY - 1)
-  public void applySpanAttributes(RoutingContext rc) {
+  @Override
+  public void filter(ContainerRequestContext rc) {
     if (!sdkDisabled) {
       Span span = Span.current();
-      String requestId = QuarkusLoggingMDCFilter.requestId(rc);
-      String realmId = QuarkusLoggingMDCFilter.realmId(rc);
+      String requestId = (String) 
rc.getProperty(QuarkusLoggingMDCFilter.REQUEST_ID_KEY);
       if (requestId != null) {
         span.setAttribute(REQUEST_ID_ATTRIBUTE, requestId);
       }
-      span.setAttribute(REALM_ID_ATTRIBUTE, realmId);
+      RealmContext realmContext =
+          (RealmContext) rc.getProperty(RealmContextFilter.REALM_CONTEXT_KEY);
+      span.setAttribute(REALM_ID_ATTRIBUTE, realmContext.getRealmIdentifier());
     }
-    rc.next();
   }
 }
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
index 56f67fd9..8aaac080 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusApplicationIntegrationTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.polaris.service.quarkus.it;
 
-import static 
org.apache.polaris.service.it.env.PolarisApiEndpoints.REALM_HEADER;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import com.auth0.jwt.JWT;
@@ -48,6 +47,7 @@ public class QuarkusApplicationIntegrationTest extends 
PolarisApplicationIntegra
     public Map<String, String> getConfigOverrides() {
       return Map.of(
           "quarkus.http.limits.max-body-size", "1000000",
+          "polaris.realm-context.realms", "POLARIS,OTHER",
           "polaris.features.defaults.\"ALLOW_OVERLAPPING_CATALOG_URLS\"", 
"true",
           
"polaris.features.defaults.\"SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION\"", "true");
     }
@@ -59,7 +59,7 @@ public class QuarkusApplicationIntegrationTest extends 
PolarisApplicationIntegra
     String path = endpoints.catalogApiEndpoint() + "/v1/oauth/tokens";
     try (RESTClient client =
         HTTPClient.builder(Map.of())
-            .withHeader(REALM_HEADER, endpoints.realm())
+            .withHeader(endpoints.realmHeaderName(), endpoints.realmId())
             .uri(path)
             .build()) {
       String credentialString =
diff --git 
a/quarkus/service/src/testFixtures/java/org/apache/polaris/service/quarkus/it/QuarkusServerManager.java
 
b/quarkus/service/src/testFixtures/java/org/apache/polaris/service/quarkus/it/QuarkusServerManager.java
index 38a3e3e0..a9f16c20 100644
--- 
a/quarkus/service/src/testFixtures/java/org/apache/polaris/service/quarkus/it/QuarkusServerManager.java
+++ 
b/quarkus/service/src/testFixtures/java/org/apache/polaris/service/quarkus/it/QuarkusServerManager.java
@@ -28,17 +28,10 @@ import org.junit.jupiter.api.extension.ExtensionContext;
 
 public class QuarkusServerManager implements PolarisServerManager {
 
-  private static final String TEST_REALM = "POLARIS";
-
   @Override
   public Server serverForContext(ExtensionContext context) {
     return new Server() {
 
-      @Override
-      public String realmId() {
-        return TEST_REALM;
-      }
-
       @Override
       public URI baseUri() {
         return URI.create(String.format("http://localhost:%d";, 
getQuarkusTestPort()));
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalAuthenticatorFilter.java
 
b/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalAuthenticatorFilter.java
index 502742ee..27fc6d31 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalAuthenticatorFilter.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalAuthenticatorFilter.java
@@ -22,7 +22,6 @@ import jakarta.annotation.Priority;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.NotAuthorizedException;
-import jakarta.ws.rs.Priorities;
 import jakarta.ws.rs.container.ContainerRequestContext;
 import jakarta.ws.rs.container.ContainerRequestFilter;
 import jakarta.ws.rs.container.PreMatching;
@@ -31,9 +30,10 @@ import jakarta.ws.rs.ext.Provider;
 import java.security.Principal;
 import java.util.Optional;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
+import org.apache.polaris.service.config.PolarisFilterPriorities;
 
 @PreMatching
-@Priority(Priorities.AUTHENTICATION)
+@Priority(PolarisFilterPriorities.AUTHENTICATOR_FILTER)
 @ApplicationScoped
 @Provider
 public class PolarisPrincipalAuthenticatorFilter implements 
ContainerRequestFilter {
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalRolesProviderFilter.java
 
b/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalRolesProviderFilter.java
index e45c6caf..255f9b22 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalRolesProviderFilter.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/auth/PolarisPrincipalRolesProviderFilter.java
@@ -21,7 +21,6 @@ package org.apache.polaris.service.auth;
 import jakarta.annotation.Priority;
 import jakarta.enterprise.context.RequestScoped;
 import jakarta.inject.Inject;
-import jakarta.ws.rs.Priorities;
 import jakarta.ws.rs.container.ContainerRequestContext;
 import jakarta.ws.rs.container.ContainerRequestFilter;
 import jakarta.ws.rs.container.PreMatching;
@@ -30,9 +29,10 @@ import jakarta.ws.rs.ext.Provider;
 import java.security.Principal;
 import java.util.Set;
 import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
+import org.apache.polaris.service.config.PolarisFilterPriorities;
 
 @PreMatching
-@Priority(Priorities.AUTHENTICATION + 1)
+@Priority(PolarisFilterPriorities.ROLES_PROVIDER_FILTER)
 @RequestScoped
 @Provider
 public class PolarisPrincipalRolesProviderFilter implements 
ContainerRequestFilter {
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
 
b/service/common/src/main/java/org/apache/polaris/service/config/PolarisFilterPriorities.java
similarity index 65%
copy from 
service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
copy to 
service/common/src/main/java/org/apache/polaris/service/config/PolarisFilterPriorities.java
index dae00f37..621da3e3 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/config/PolarisFilterPriorities.java
@@ -16,13 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.service.context;
+package org.apache.polaris.service.config;
 
-import java.util.Map;
-import org.apache.polaris.core.context.RealmContext;
+import jakarta.ws.rs.Priorities;
 
-public interface RealmContextResolver {
-
-  RealmContext resolveRealmContext(
-      String requestURL, String method, String path, Map<String, String> 
headers);
+public final class PolarisFilterPriorities {
+  public static final int REALM_CONTEXT_FILTER = Priorities.AUTHENTICATION - 
100;
+  public static final int AUTHENTICATOR_FILTER = Priorities.AUTHENTICATION;
+  public static final int ROLES_PROVIDER_FILTER = Priorities.AUTHENTICATION + 
1;
+  public static final int RATE_LIMITER_FILTER = Priorities.USER;
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java
 
b/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java
index c12cdd42..3318bd2f 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java
@@ -21,7 +21,7 @@ package org.apache.polaris.service.context;
 import io.smallrye.common.annotation.Identifier;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import java.util.Map;
+import java.util.function.Function;
 import org.apache.polaris.core.context.RealmContext;
 
 @ApplicationScoped
@@ -37,19 +37,24 @@ public class DefaultRealmContextResolver implements 
RealmContextResolver {
 
   @Override
   public RealmContext resolveRealmContext(
-      String requestURL, String method, String path, Map<String, String> 
headers) {
-
-    String realm;
+      String requestURL, String method, String path, Function<String, String> 
headers) {
+    String realm = resolveRealmIdentifier(headers);
+    return () -> realm;
+  }
 
-    if (headers.containsKey(configuration.headerName())) {
-      realm = headers.get(configuration.headerName());
+  private String resolveRealmIdentifier(Function<String, String> headers) {
+    String realm = headers.apply(configuration.headerName());
+    if (realm != null) {
       if (!configuration.realms().contains(realm)) {
-        throw new IllegalArgumentException("Unknown realm: " + realm);
+        throw new UnresolvableRealmContextException("Unknown realm: " + realm);
       }
     } else {
+      if (configuration.requireHeader()) {
+        throw new UnresolvableRealmContextException(
+            "Missing required realm header: " + configuration.headerName());
+      }
       realm = configuration.defaultRealm();
     }
-
-    return () -> realm;
+    return realm;
   }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextConfiguration.java
 
b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextConfiguration.java
index 08599e02..ae34b1ab 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextConfiguration.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextConfiguration.java
@@ -33,6 +33,15 @@ public interface RealmContextConfiguration {
   /** The header name that contains the realm identifier. */
   String headerName();
 
+  /**
+   * Whether to require the realm header to be present in the request. If this 
is true and the realm
+   * header is not present, the request will be rejected. If this is false and 
the realm header is
+   * not present, the default realm will be used.
+   *
+   * <p>Note: this is actually only enforced in production setups.
+   */
+  boolean requireHeader();
+
   /** The default realm to use when no realm is specified. */
   default String defaultRealm() {
     return realms().getFirst();
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/ratelimiter/RateLimiterFilter.java
 
b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java
similarity index 55%
copy from 
service/common/src/main/java/org/apache/polaris/service/ratelimiter/RateLimiterFilter.java
copy to 
service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java
index 3047b52f..7939d05b 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/ratelimiter/RateLimiterFilter.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java
@@ -16,42 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.service.ratelimiter;
+package org.apache.polaris.service.context;
 
 import jakarta.annotation.Priority;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import jakarta.ws.rs.Priorities;
 import jakarta.ws.rs.container.ContainerRequestContext;
 import jakarta.ws.rs.container.ContainerRequestFilter;
 import jakarta.ws.rs.container.PreMatching;
-import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.ext.Provider;
-import java.io.IOException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.service.config.PolarisFilterPriorities;
 
-/** Request filter that returns a 429 Too Many Requests if the rate limiter 
says so */
-@Provider
 @PreMatching
-@Priority(Priorities.USER)
 @ApplicationScoped
-public class RateLimiterFilter implements ContainerRequestFilter {
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(RateLimiterFilter.class);
+@Priority(PolarisFilterPriorities.REALM_CONTEXT_FILTER)
+@Provider
+public class RealmContextFilter implements ContainerRequestFilter {
 
-  private final RateLimiter rateLimiter;
+  public static final String REALM_CONTEXT_KEY = "realmContext";
 
-  @Inject
-  public RateLimiterFilter(RateLimiter rateLimiter) {
-    this.rateLimiter = rateLimiter;
-  }
+  @Inject RealmContextResolver realmContextResolver;
 
-  /** Returns a 429 if the rate limiter says so. Otherwise, forwards the 
request along. */
   @Override
-  public void filter(ContainerRequestContext ctx) throws IOException {
-    if (!rateLimiter.canProceed()) {
-      
ctx.abortWith(Response.status(Response.Status.TOO_MANY_REQUESTS).build());
-      LOGGER.atDebug().log("Rate limiting request");
-    }
+  public void filter(ContainerRequestContext rc) {
+    RealmContext realmContext =
+        realmContextResolver.resolveRealmContext(
+            rc.getUriInfo().getRequestUri().toString(),
+            rc.getMethod(),
+            rc.getUriInfo().getPath(),
+            rc.getHeaders()::getFirst);
+    rc.setProperty(REALM_CONTEXT_KEY, realmContext);
   }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
 
b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
index dae00f37..e9c75360 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
@@ -19,10 +19,22 @@
 package org.apache.polaris.service.context;
 
 import java.util.Map;
+import java.util.function.Function;
 import org.apache.polaris.core.context.RealmContext;
 
 public interface RealmContextResolver {
 
+  /**
+   * Resolves the realm context for the given request.
+   *
+   * @return the resolved realm context
+   * @throws UnresolvableRealmContextException if the realm context cannot be 
resolved
+   */
   RealmContext resolveRealmContext(
-      String requestURL, String method, String path, Map<String, String> 
headers);
+      String requestURL, String method, String path, Function<String, String> 
headers);
+
+  default RealmContext resolveRealmContext(
+      String requestURL, String method, String path, Map<String, String> 
headers) {
+    return resolveRealmContext(requestURL, method, path, headers::get);
+  }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java
 
b/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java
index 122a5436..1fcff649 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java
@@ -24,6 +24,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Function;
 import org.apache.polaris.core.context.RealmContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -50,7 +51,7 @@ public class TestRealmContextResolver implements 
RealmContextResolver {
 
   @Override
   public RealmContext resolveRealmContext(
-      String requestURL, String method, String path, Map<String, String> 
headers) {
+      String requestURL, String method, String path, Function<String, String> 
headers) {
     // Since this default resolver is strictly for use in test/dev 
environments, we'll consider
     // it safe to log all contents. Any "real" resolver used in a prod 
environment should make
     // sure to only log non-sensitive contents.
@@ -58,9 +59,9 @@ public class TestRealmContextResolver implements 
RealmContextResolver {
         "Resolving RealmContext for method: {}, path: {}, headers: {}", 
method, path, headers);
     Map<String, String> parsedProperties = parseBearerTokenAsKvPairs(headers);
 
-    if (!parsedProperties.containsKey(REALM_PROPERTY_KEY)
-        && headers.containsKey(REALM_PROPERTY_KEY)) {
-      parsedProperties.put(REALM_PROPERTY_KEY, 
headers.get(REALM_PROPERTY_KEY));
+    String realm = headers.apply(REALM_PROPERTY_KEY);
+    if (!parsedProperties.containsKey(REALM_PROPERTY_KEY) && realm != null) {
+      parsedProperties.put(REALM_PROPERTY_KEY, realm);
     }
 
     if (!parsedProperties.containsKey(REALM_PROPERTY_KEY)) {
@@ -78,10 +79,10 @@ public class TestRealmContextResolver implements 
RealmContextResolver {
    * Returns kv pairs parsed from the "Authorization: Bearer 
k1:v1;k2:v2;k3:v3" header if it exists;
    * if missing, returns empty map.
    */
-  private static Map<String, String> parseBearerTokenAsKvPairs(Map<String, 
String> headers) {
+  private static Map<String, String> 
parseBearerTokenAsKvPairs(Function<String, String> headers) {
     Map<String, String> parsedProperties = new HashMap<>();
     if (headers != null) {
-      String authHeader = headers.get("Authorization");
+      String authHeader = headers.apply("Authorization");
       if (authHeader != null) {
         String[] parts = authHeader.split(" ");
         if (parts.length == 2 && "Bearer".equalsIgnoreCase(parts[0])) {
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
 
b/service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java
similarity index 70%
copy from 
service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
copy to 
service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java
index dae00f37..4456fc60 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java
@@ -19,10 +19,16 @@
 package org.apache.polaris.service.context;
 
 import java.util.Map;
-import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.core.exceptions.PolarisException;
 
-public interface RealmContextResolver {
+/**
+ * Exception thrown when a realm context cannot be resolved.
+ *
+ * @see RealmContextResolver#resolveRealmContext(String, String, String, Map)
+ */
+public class UnresolvableRealmContextException extends PolarisException {
 
-  RealmContext resolveRealmContext(
-      String requestURL, String method, String path, Map<String, String> 
headers);
+  public UnresolvableRealmContextException(String message) {
+    super(message);
+  }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
 
b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
index 489b3735..2ba2b804 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
@@ -25,6 +25,7 @@ import jakarta.ws.rs.ext.Provider;
 import org.apache.iceberg.rest.responses.ErrorResponse;
 import org.apache.polaris.core.exceptions.AlreadyExistsException;
 import org.apache.polaris.core.exceptions.PolarisException;
+import org.apache.polaris.service.context.UnresolvableRealmContextException;
 
 /**
  * An {@link ExceptionMapper} implementation for {@link PolarisException}s 
modeled after {@link
@@ -36,6 +37,8 @@ public class PolarisExceptionMapper implements 
ExceptionMapper<PolarisException>
   private Response.Status getStatus(PolarisException exception) {
     if (exception instanceof AlreadyExistsException) {
       return Response.Status.CONFLICT;
+    } else if (exception instanceof UnresolvableRealmContextException) {
+      return Response.Status.NOT_FOUND;
     } else {
       return Response.Status.INTERNAL_SERVER_ERROR;
     }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/ratelimiter/RateLimiterFilter.java
 
b/service/common/src/main/java/org/apache/polaris/service/ratelimiter/RateLimiterFilter.java
index 3047b52f..28bb6058 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/ratelimiter/RateLimiterFilter.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/ratelimiter/RateLimiterFilter.java
@@ -21,20 +21,20 @@ package org.apache.polaris.service.ratelimiter;
 import jakarta.annotation.Priority;
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import jakarta.ws.rs.Priorities;
 import jakarta.ws.rs.container.ContainerRequestContext;
 import jakarta.ws.rs.container.ContainerRequestFilter;
 import jakarta.ws.rs.container.PreMatching;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.ext.Provider;
 import java.io.IOException;
+import org.apache.polaris.service.config.PolarisFilterPriorities;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /** Request filter that returns a 429 Too Many Requests if the rate limiter 
says so */
 @Provider
 @PreMatching
-@Priority(Priorities.USER)
+@Priority(PolarisFilterPriorities.RATE_LIMITER_FILTER)
 @ApplicationScoped
 public class RateLimiterFilter implements ContainerRequestFilter {
   private static final Logger LOGGER = 
LoggerFactory.getLogger(RateLimiterFilter.class);
diff --git 
a/service/common/src/test/java/org/apache/polaris/service/context/DefaultRealmIdResolverTest.java
 
b/service/common/src/test/java/org/apache/polaris/service/context/DefaultRealmIdResolverTest.java
new file mode 100644
index 00000000..a68e10ff
--- /dev/null
+++ 
b/service/common/src/test/java/org/apache/polaris/service/context/DefaultRealmIdResolverTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.context;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.core.context.RealmContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class DefaultRealmContextResolverTest {
+
+  private RealmContextConfiguration config;
+
+  @BeforeEach
+  void setUp() {
+    config = Mockito.mock(RealmContextConfiguration.class);
+    when(config.headerName()).thenReturn("Polaris-Header");
+    when(config.realms()).thenReturn(List.of("realm1", "realm2"));
+    when(config.defaultRealm()).thenCallRealMethod();
+  }
+
+  @Test
+  void headerPresentSuccess() {
+    DefaultRealmContextResolver resolver = new 
DefaultRealmContextResolver(config);
+    RealmContext RealmContext1 =
+        resolver.resolveRealmContext(
+            "requestURL", "method", "path", Map.of("Polaris-Header", 
"realm1"));
+    assertThat(RealmContext1.getRealmIdentifier()).isEqualTo("realm1");
+    RealmContext RealmContext2 =
+        resolver.resolveRealmContext(
+            "requestURL", "method", "path", Map.of("Polaris-Header", 
"realm2"));
+    assertThat(RealmContext2.getRealmIdentifier()).isEqualTo("realm2");
+  }
+
+  @Test
+  void headerPresentFailure() {
+    DefaultRealmContextResolver resolver = new 
DefaultRealmContextResolver(config);
+    assertThatThrownBy(
+            () ->
+                resolver.resolveRealmContext(
+                    "requestURL", "method", "path", Map.of("Polaris-Header", 
"realm3")))
+        .isInstanceOf(UnresolvableRealmContextException.class)
+        .hasMessage("Unknown realm: realm3");
+  }
+
+  @Test
+  void headerNotPresentSuccess() {
+    when(config.requireHeader()).thenReturn(false);
+    DefaultRealmContextResolver resolver = new 
DefaultRealmContextResolver(config);
+    RealmContext RealmContext1 =
+        resolver.resolveRealmContext("requestURL", "method", "path", Map.of());
+    assertThat(RealmContext1.getRealmIdentifier()).isEqualTo("realm1");
+  }
+
+  @Test
+  void headerNotPresentFailure() {
+    when(config.requireHeader()).thenReturn(true);
+    DefaultRealmContextResolver resolver = new 
DefaultRealmContextResolver(config);
+    assertThatThrownBy(() -> resolver.resolveRealmContext("requestURL", 
"method", "path", Map.of()))
+        .isInstanceOf(UnresolvableRealmContextException.class)
+        .hasMessage("Missing required realm header: Polaris-Header");
+  }
+}
diff --git 
a/site/content/in-dev/unreleased/configuring-polaris-for-production.md 
b/site/content/in-dev/unreleased/configuring-polaris-for-production.md
index 5b83bbe5..4b243e86 100644
--- a/site/content/in-dev/unreleased/configuring-polaris-for-production.md
+++ b/site/content/in-dev/unreleased/configuring-polaris-for-production.md
@@ -109,8 +109,22 @@ Where:
 - `header-name` is the name of the header used to resolve the realm; by 
default, it is
   `Polaris-Realm`.
 
-If a request does not contain the specified header, Polaris will use the first 
realm in the list as
-the default realm. In the above example, `POLARIS` is the default realm.
+If a request contains the specified header, Polaris will use the realm 
specified in the header. If
+the realm is not in the list of allowed realms, Polaris will return a `404 Not 
Found` response.
+
+If a request _does not_ contain the specified header, however, by default 
Polaris will use the first
+realm in the list as the default realm. In the above example, `POLARIS` is the 
default realm and
+would be used if the `Polaris-Realm` header is not present in the request.
+
+This is not recommended for production use, as it may lead to security 
vulnerabilities. To avoid
+this, set the following property to `true`:
+
+```properties
+polaris.realm-context.require-header=true
+```
+
+This will cause Polaris to also return a `404 Not Found` response if the realm 
header is not present
+in the request.
 
 ### Metastore Configuration
 

Reply via email to