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