From 816c3f45e03a02de0be833f7b49701ee7b811307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Fri, 6 Sep 2024 15:18:53 +0200 Subject: [PATCH] Test OpenShift certificate serving with REST client --- .../src/test/java/hero/Hero.java | 4 + .../src/test/java/hero/HeroClient.java | 15 +++ .../test/java/hero/HeroClientResource.java | 19 ++++ .../src/test/java/hero/HeroResource.java | 17 ++++ .../src/test/java/hero/Villain.java | 4 + .../src/test/java/hero/VillainClient.java | 15 +++ .../test/java/hero/VillainClientResource.java | 19 ++++ .../src/test/java/hero/VillainResource.java | 17 ++++ .../OpenShiftServingCertificatesIT.java | 97 +++++++++++++++++++ .../certificate-serving-client.properties | 6 ++ .../certificate-serving-server.properties | 4 + 11 files changed, 217 insertions(+) create mode 100644 http/rest-client-reactive/src/test/java/hero/Hero.java create mode 100644 http/rest-client-reactive/src/test/java/hero/HeroClient.java create mode 100644 http/rest-client-reactive/src/test/java/hero/HeroClientResource.java create mode 100644 http/rest-client-reactive/src/test/java/hero/HeroResource.java create mode 100644 http/rest-client-reactive/src/test/java/hero/Villain.java create mode 100644 http/rest-client-reactive/src/test/java/hero/VillainClient.java create mode 100644 http/rest-client-reactive/src/test/java/hero/VillainClientResource.java create mode 100644 http/rest-client-reactive/src/test/java/hero/VillainResource.java create mode 100644 http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/OpenShiftServingCertificatesIT.java create mode 100644 http/rest-client-reactive/src/test/resources/certificate-serving-client.properties create mode 100644 http/rest-client-reactive/src/test/resources/certificate-serving-server.properties diff --git a/http/rest-client-reactive/src/test/java/hero/Hero.java b/http/rest-client-reactive/src/test/java/hero/Hero.java new file mode 100644 index 0000000000..82fc6102aa --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/Hero.java @@ -0,0 +1,4 @@ +package hero; + +public record Hero(Long id, String name, String otherName, int level, String picture, String powers) { +} diff --git a/http/rest-client-reactive/src/test/java/hero/HeroClient.java b/http/rest-client-reactive/src/test/java/hero/HeroClient.java new file mode 100644 index 0000000000..171212a6a5 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/HeroClient.java @@ -0,0 +1,15 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "hero") +public interface HeroClient { + + @GET + @Path("/api/heroes/random") + Hero getRandomHero(); + +} diff --git a/http/rest-client-reactive/src/test/java/hero/HeroClientResource.java b/http/rest-client-reactive/src/test/java/hero/HeroClientResource.java new file mode 100644 index 0000000000..726d960176 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/HeroClientResource.java @@ -0,0 +1,19 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("hero-client-resource") +public class HeroClientResource { + + @RestClient + HeroClient heroClient; + + @GET + public Hero triggerClientToServerCommunication() { + return heroClient.getRandomHero(); + } + +} diff --git a/http/rest-client-reactive/src/test/java/hero/HeroResource.java b/http/rest-client-reactive/src/test/java/hero/HeroResource.java new file mode 100644 index 0000000000..f1a51a7e29 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/HeroResource.java @@ -0,0 +1,17 @@ +package hero; + +import java.util.random.RandomGenerator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/api/heroes/random") +public class HeroResource { + + @GET + public Hero getRandomHero() { + long random = RandomGenerator.getDefault().nextLong(); + return new Hero(random, "Name-" + random, "Other-" + random, 1, "placeholder", "root"); + } + +} diff --git a/http/rest-client-reactive/src/test/java/hero/Villain.java b/http/rest-client-reactive/src/test/java/hero/Villain.java new file mode 100644 index 0000000000..448be66896 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/Villain.java @@ -0,0 +1,4 @@ +package hero; + +public record Villain(Long id, String name, String otherName, int level, String picture, String powers) { +} diff --git a/http/rest-client-reactive/src/test/java/hero/VillainClient.java b/http/rest-client-reactive/src/test/java/hero/VillainClient.java new file mode 100644 index 0000000000..aeb18810c8 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/VillainClient.java @@ -0,0 +1,15 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "villain") +public interface VillainClient { + + @GET + @Path("/api/villain/random") + Villain getRandomVillain(); + +} diff --git a/http/rest-client-reactive/src/test/java/hero/VillainClientResource.java b/http/rest-client-reactive/src/test/java/hero/VillainClientResource.java new file mode 100644 index 0000000000..2d9bf9f36f --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/VillainClientResource.java @@ -0,0 +1,19 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("villain-client-resource") +public class VillainClientResource { + + @RestClient + VillainClient villainClient; + + @GET + public Villain triggerClientToServerCommunication() { + return villainClient.getRandomVillain(); + } + +} diff --git a/http/rest-client-reactive/src/test/java/hero/VillainResource.java b/http/rest-client-reactive/src/test/java/hero/VillainResource.java new file mode 100644 index 0000000000..6acb6fc66c --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/VillainResource.java @@ -0,0 +1,17 @@ +package hero; + +import java.util.random.RandomGenerator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/api/villain/random") +public class VillainResource { + + @GET + public Villain getRandomVillain() { + long random = RandomGenerator.getDefault().nextLong(); + return new Villain(random, "Name-" + random, "Other-" + random, 1, "placeholder", "root"); + } + +} diff --git a/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/OpenShiftServingCertificatesIT.java b/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/OpenShiftServingCertificatesIT.java new file mode 100644 index 0000000000..7b3bc842fb --- /dev/null +++ b/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/OpenShiftServingCertificatesIT.java @@ -0,0 +1,97 @@ +package io.quarkus.ts.http.restclient.reactive; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import io.quarkus.test.bootstrap.Protocol; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.Certificate.ServingCertificates; +import io.quarkus.test.services.QuarkusApplication; +import io.quarkus.test.utils.AwaitilityUtils; + +import hero.Hero; +import hero.HeroClient; +import hero.HeroClientResource; +import hero.HeroResource; +import hero.Villain; +import hero.VillainClient; +import hero.VillainClientResource; +import hero.VillainResource; + +/** + * Test OpenShift serving certificate support and Quarkus REST client. + */ +@Tag("QUARKUS-4592") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@OpenShiftScenario +public class OpenShiftServingCertificatesIT { + + private static final String HERO_CLIENT = "hero-client"; + private static final String SERVER_TLS_CONFIG_NAME = "cert-serving-test-server"; + + @QuarkusApplication(ssl = true, certificates = @Certificate(tlsConfigName = SERVER_TLS_CONFIG_NAME, servingCertificates = { + @ServingCertificates(addServiceCertificate = true) + }), classes = { HeroResource.class, Hero.class, Villain.class, + VillainResource.class }, properties = "certificate-serving-server.properties") + static final RestService server = new RestService(); + + @QuarkusApplication(certificates = @Certificate(tlsConfigName = HERO_CLIENT, servingCertificates = @ServingCertificates(injectCABundle = true)), classes = { + HeroClient.class, Hero.class, HeroClientResource.class, Villain.class, VillainClient.class, + VillainClientResource.class }, properties = "certificate-serving-client.properties") + static final RestService client = new RestService() + .withProperty("quarkus.rest-client.hero.uri", () -> server.getURI(Protocol.HTTPS).getRestAssuredStyleUri()); + + @Order(1) + @Test + public void testSecuredCommunicationBetweenClientAndServer() { + // REST client use OpenShift internal CA + // server is configured with OpenShift serving certificates + // ad "untilAsserted": hopefully it's not necessary, but once I experienced unknown SAN, + // so to avoid flakiness I am adding here retry: + AwaitilityUtils.untilAsserted(() -> { + var hero = client.given() + .get("hero-client-resource") + .then() + .statusCode(200) + .extract() + .as(Hero.class); + assertNotNull(hero); + assertNotNull(hero.name()); + assertTrue(hero.name().startsWith("Name-")); + assertNotNull(hero.otherName()); + assertTrue(hero.otherName().startsWith("Other-")); + }); + } + + @Order(2) + @Test + public void testConfiguredTlsProtocolEnforced() { + // verifies that protocol version set in TLS config is obliged by both HTTP server and client + // REST client requires TLSv1.2 + // HTTP server requires TLSv1.3 + try { + client.given() + .get("villain-client-resource") + .then() + .statusCode(200) + .extract() + .as(Villain.class); + Assertions.fail( + "Expected SSL handshake failure as server requires TLS 1.3 and client requires TLS 1.2, but the failure did not occurred"); + } catch (Exception e) { + assertTrue(e.getMessage().contains("Received fatal alert: protocol_version"), + "Expected SSL handshake failure as server requires TLS 1.3 and client requires TLS 1.2, but got: " + + e.getMessage()); + } + } + +} diff --git a/http/rest-client-reactive/src/test/resources/certificate-serving-client.properties b/http/rest-client-reactive/src/test/resources/certificate-serving-client.properties new file mode 100644 index 0000000000..9cc0eb88e7 --- /dev/null +++ b/http/rest-client-reactive/src/test/resources/certificate-serving-client.properties @@ -0,0 +1,6 @@ +quarkus.rest-client.hero.tls-configuration-name=hero-client +quarkus.rest-client.villain.tls-configuration-name=villain-client +quarkus.rest-client.villain.uri=${quarkus.rest-client.hero.uri} +quarkus.tls.villain-client.trust-store.pem.certs=${quarkus.tls.hero-client.trust-store.pem.certs} +quarkus.tls.hero-client.protocols=TLSv1.3 +quarkus.tls.villain-client.protocols=TLSv1.2 diff --git a/http/rest-client-reactive/src/test/resources/certificate-serving-server.properties b/http/rest-client-reactive/src/test/resources/certificate-serving-server.properties new file mode 100644 index 0000000000..4a09cefd51 --- /dev/null +++ b/http/rest-client-reactive/src/test/resources/certificate-serving-server.properties @@ -0,0 +1,4 @@ +# the REST client use HTTPS but not mTLS +quarkus.http.ssl.client-auth=request +quarkus.http.insecure-requests=disabled +quarkus.tls.cert-serving-test-server.protocols=TLSv1.3