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 f894489b9e..4d0c987b2b 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 static RESTCatalog restCatalog( 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 392b21f25e..7212dd804a 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 @@ */ 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 URI managementApiEndpoint() { 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 e862fc2c82..c0475f088b 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.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 Invocation.Builder request(String path, Map templateValue public Invocation.Builder request( String path, Map templateValues, Map queryParams) { + Map 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 templateValues, + Map queryParams, + Map headers) { WebTarget target = client.target(uri).path(path); for (Map.Entry entry : templateValues.entrySet()) { target = target.resolveTemplate(entry.getKey(), entry.getValue()); @@ -56,10 +70,7 @@ public Invocation.Builder request( 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 22fecf1632..4fea80070e 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 @@ * 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 e2b6c43b74..d0504002eb 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 @@ private static class Env implements CloseableResource { 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 894926fa0a..7a2961e402 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.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.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.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 @@ * * The server must also be configured to reject request body sizes larger than 1MB (1000000 * bytes). + *

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 static void setup(PolarisApiEndpoints apiEndpoints, ClientPrincipal admin 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 @@ private static RESTSessionCatalog newSessionCatalog(String catalog) { authToken, "warehouse", catalog, - "header." + REALM_HEADER, + "header." + endpoints.realmHeaderName(), realm)); return sessionCatalog; } @@ -588,7 +594,7 @@ public void testWarehouseNotSpecified() throws IOException { 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 void testRequestBodyTooLarge() throws Exception { }); } } + + @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 325982cf96..e7fa2dcece 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 @@ private SparkSession.Builder withCatalog(SparkSession.Builder builder, String ca 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 32877e585a..1ba9d9db5d 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 2a19f7d121..5f46d203f2 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 c40034f2eb..8b25cbcd5d 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/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFilterPriorities.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFilterPriorities.java new file mode 100644 index 0000000000..79d310034b --- /dev/null +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusFilterPriorities.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.service.quarkus.config; + +import org.apache.polaris.service.config.PolarisFilterPriorities; + +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 d06af60ac0..5e69b89c41 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 @@ 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.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.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 PolarisDiagnostics polarisDiagnostics() { @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 98fb2888dd..3062fabd30 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 147ebd7fd7..8388be9eba 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.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 28811ad91c..6035cceb4d 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 56f67fd959..8aaac080e5 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 static class Profile implements QuarkusTestProfile { public Map 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 void testIcebergRestApiRefreshToken( 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 38a3e3e07f..a9f16c2077 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 @@ 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 502742ee9c..27fc6d312b 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.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 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 e45c6caf00..255f9b222f 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 @@ 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 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/config/PolarisFilterPriorities.java b/service/common/src/main/java/org/apache/polaris/service/config/PolarisFilterPriorities.java new file mode 100644 index 0000000000..621da3e3cc --- /dev/null +++ b/service/common/src/main/java/org/apache/polaris/service/config/PolarisFilterPriorities.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.service.config; + +import jakarta.ws.rs.Priorities; + +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 c12cdd42b7..3318bd2f53 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 @@ 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 DefaultRealmContextResolver(RealmContextConfiguration configuration) { @Override public RealmContext resolveRealmContext( - String requestURL, String method, String path, Map headers) { - - String realm; + String requestURL, String method, String path, Function headers) { + String realm = resolveRealmIdentifier(headers); + return () -> realm; + } - if (headers.containsKey(configuration.headerName())) { - realm = headers.get(configuration.headerName()); + private String resolveRealmIdentifier(Function 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 08599e022c..ae34b1ab5a 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. + * + *

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/context/RealmContextFilter.java b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java new file mode 100644 index 0000000000..7939d05b1c --- /dev/null +++ b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java @@ -0,0 +1,51 @@ +/* + * 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 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.config.PolarisFilterPriorities; + +@PreMatching +@ApplicationScoped +@Priority(PolarisFilterPriorities.REALM_CONTEXT_FILTER) +@Provider +public class RealmContextFilter implements ContainerRequestFilter { + + public static final String REALM_CONTEXT_KEY = "realmContext"; + + @Inject RealmContextResolver realmContextResolver; + + @Override + 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 dae00f377d..e9c7536036 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 headers); + String requestURL, String method, String path, Function headers); + + default RealmContext resolveRealmContext( + String requestURL, String method, String path, Map 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 122a5436aa..1fcff6496b 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.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 TestRealmContextResolver(RealmContextConfiguration configuration) { @Override public RealmContext resolveRealmContext( - String requestURL, String method, String path, Map headers) { + String requestURL, String method, String path, Function 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 RealmContext resolveRealmContext( "Resolving RealmContext for method: {}, path: {}, headers: {}", method, path, headers); Map 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 RealmContext resolveRealmContext( * 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 parseBearerTokenAsKvPairs(Map headers) { + private static Map parseBearerTokenAsKvPairs(Function headers) { Map 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/UnresolvableRealmContextException.java b/service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java new file mode 100644 index 0000000000..4456fc602e --- /dev/null +++ b/service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java @@ -0,0 +1,34 @@ +/* + * 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 java.util.Map; +import org.apache.polaris.core.exceptions.PolarisException; + +/** + * Exception thrown when a realm context cannot be resolved. + * + * @see RealmContextResolver#resolveRealmContext(String, String, String, Map) + */ +public class UnresolvableRealmContextException extends PolarisException { + + 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 489b373510..2ba2b804f3 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 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 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 3047b52fab..28bb60589d 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 @@ 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 0000000000..a68e10ff38 --- /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 5b83bbe58b..4b243e8676 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