diff --git a/runtime/service/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java b/runtime/service/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java index c62bc0f3ba..78558bb5d5 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java @@ -27,11 +27,15 @@ import org.apache.iceberg.rest.responses.ErrorResponse; import org.apache.polaris.service.config.FilterPriorities; import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RealmContextFilter { public static final String REALM_CONTEXT_KEY = "realmContext"; + private static final Logger LOGGER = LoggerFactory.getLogger(RealmContextFilter.class); + @Inject RealmContextResolver realmContextResolver; @ServerRequestFilter(preMatching = true, priority = FilterPriorities.REALM_CONTEXT_FILTER) @@ -46,19 +50,20 @@ public Uni resolveRealmContext(ContainerRequestContext rc) { rc.getHeaders()::getFirst)) .onItem() .invoke(realmContext -> rc.setProperty(REALM_CONTEXT_KEY, realmContext)) + // ContextLocals is used by RealmIdTagContributor to add the realm id to metrics .invoke(realmContext -> ContextLocals.put(REALM_CONTEXT_KEY, realmContext)) .onItemOrFailure() .transform((realmContext, error) -> error == null ? null : errorResponse(error)); } private static Response errorResponse(Throwable error) { + LOGGER.error("Error resolving realm context", error); return Response.status(Response.Status.NOT_FOUND) .type(MediaType.APPLICATION_JSON_TYPE) .entity( ErrorResponse.builder() .responseCode(Response.Status.NOT_FOUND.getStatusCode()) - .withMessage( - error.getMessage() != null ? error.getMessage() : "Missing or invalid realm") + .withMessage("Missing or invalid realm") .withType("MissingOrInvalidRealm") .build()) .build(); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/RealmHeaderTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/RealmHeaderTest.java deleted file mode 100644 index f5eb3eac98..0000000000 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/RealmHeaderTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.service.admin; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.QuarkusTestProfile; -import io.quarkus.test.junit.TestProfile; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MultivaluedHashMap; -import jakarta.ws.rs.core.Response; -import java.net.URI; -import java.util.Map; -import java.util.Objects; -import org.apache.iceberg.rest.responses.ErrorResponse; -import org.apache.polaris.service.it.env.PolarisApiEndpoints; -import org.apache.polaris.service.it.env.PolarisClient; -import org.junit.jupiter.api.Test; - -@QuarkusTest -@TestProfile(RealmHeaderTest.Profile.class) -public class RealmHeaderTest { - public static class Profile implements QuarkusTestProfile { - @Override - public Map getConfigOverrides() { - return Map.of( - "polaris.realm-context.header-name", - REALM_HEADER, - "polaris.realm-context.realms", - "realm1,realm2", - "polaris.bootstrap.credentials", - "realm1,client1,secret1;realm2,client2,secret2"); - } - } - - private static final String REALM_HEADER = "test-header-r123"; - - private static final URI baseUri = - URI.create( - "http://localhost:" - + Objects.requireNonNull( - Integer.getInteger("quarkus.http.test-port"), - "System property not set correctly: quarkus.http.test-port")); - - private Response request(String realm, String header, String clientId, String secret) { - try (PolarisClient client = - PolarisClient.polarisClient(new PolarisApiEndpoints(baseUri, realm, header))) { - return client - .catalogApiPlain() - .request("v1/oauth/tokens") - .post( - Entity.form( - new MultivaluedHashMap<>( - Map.of( - "grant_type", - "client_credentials", - "scope", - "PRINCIPAL_ROLE:ALL", - "client_id", - clientId, - "client_secret", - secret)))); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - public void testInvalidRealmHeaderValue() { - try (Response response = request("INVALID", REALM_HEADER, "dummy", "dummy")) { - assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); - assertThat(response.readEntity(ErrorResponse.class)) - .extracting(ErrorResponse::code, ErrorResponse::type, ErrorResponse::message) - .containsExactly( - Response.Status.NOT_FOUND.getStatusCode(), - "MissingOrInvalidRealm", - "Unknown realm: INVALID"); - } - } - - @Test - public void testNoRealmHeader() { - try (Response response = request("fake-realm", "irrelevant-header", "client2", "secret2")) { - // The default realm is "realm2" so the second pair of secrets is not valid without - // an explicit header - assertThat(response.getStatus()).isEqualTo(Response.Status.UNAUTHORIZED.getStatusCode()); - } - } - - @Test - public void testDefaultRealm() { - try (Response response = request("fake-realm", "irrelevant-header", "client1", "secret1")) { - // The default realm is "realm1", now credentials match - assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); - } - } - - @Test - public void testValidRealmHeaderDefaultRealm() { - try (Response response = request("realm2", REALM_HEADER, "client2", "secret2")) { - assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); - } - } -} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/context/RealmContextFilterTest.java b/runtime/service/src/test/java/org/apache/polaris/service/context/RealmContextFilterTest.java new file mode 100644 index 0000000000..71f34a8a02 --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/context/RealmContextFilterTest.java @@ -0,0 +1,111 @@ +/* + * 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 io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import jakarta.ws.rs.core.Response; +import java.util.Map; +import org.apache.polaris.service.catalog.api.IcebergRestOAuth2Api; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestHTTPEndpoint(IcebergRestOAuth2Api.class) +@TestProfile(RealmContextFilterTest.Profile.class) +@SuppressWarnings("UastIncorrectHttpHeaderInspection") +class RealmContextFilterTest { + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "polaris.realm-context.header-name", + REALM_HEADER, + "polaris.realm-context.realms", + "realm1,realm2", + "polaris.bootstrap.credentials", + "realm1,client1,secret1;realm2,client2,secret2"); + } + } + + private static final String REALM_HEADER = "test-header-r123"; + + @Test + public void testInvalidRealmHeaderValue() { + givenTokenRequest("client1", "secret1") + .header(REALM_HEADER, "INVALID") + .when() + .post() + .then() + .statusCode(Response.Status.NOT_FOUND.getStatusCode()) + .body("error.message", is("Missing or invalid realm")) + .body("error.type", is("MissingOrInvalidRealm")) + .body("error.code", is(Response.Status.NOT_FOUND.getStatusCode())); + } + + @Test + public void testNoRealmHeader() { + // The default realm is "realm1" so the second pair of secrets is not valid without + // an explicit header + givenTokenRequest("client2", "secret2") + .header("irrelevant-header", "fake-realm") + .when() + .post() + .then() + .statusCode(Response.Status.UNAUTHORIZED.getStatusCode()); + } + + @Test + public void testDefaultRealm() { + // The default realm is "realm1", now credentials match + givenTokenRequest("client1", "secret1") + .header("irrelevant-header", "fake-realm") + .when() + .post() + .then() + .statusCode(Response.Status.OK.getStatusCode()); + } + + @Test + public void testValidRealmHeaderDefaultRealm() { + givenTokenRequest("client2", "secret2") + .header(REALM_HEADER, "realm2") + .when() + .post() + .then() + .statusCode(Response.Status.OK.getStatusCode()); + } + + private static RequestSpecification givenTokenRequest(String clientId, String clientSecret) { + return given() + .contentType(ContentType.URLENC) + .formParam("grant_type", "client_credentials") + .formParam("scope", "PRINCIPAL_ROLE:ALL") + .formParam("client_id", clientId) + .formParam("client_secret", clientSecret); + } +}