Skip to content

Commit

Permalink
Merge pull request #1270 from michalvavrik/feature/openshift-tls-cert…
Browse files Browse the repository at this point in the history
…ificate-serving

Support OpenShift certificate serving used with TLS registry
  • Loading branch information
michalvavrik authored Sep 1, 2024
2 parents 36a1fc9 + e878ab5 commit 6d2a583
Show file tree
Hide file tree
Showing 20 changed files with 644 additions and 20 deletions.
6 changes: 5 additions & 1 deletion examples/https/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkus.qe;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

import io.quarkus.qe.hero.Hero;
import io.quarkus.qe.hero.HeroClient;
import io.quarkus.qe.hero.HeroClientResource;
import io.quarkus.qe.hero.HeroResource;
import io.quarkus.test.bootstrap.Protocol;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.OpenShiftScenario;
import io.quarkus.test.scenarios.annotations.DisabledOnNative;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.QuarkusApplication;
import io.quarkus.test.utils.AwaitilityUtils;

/**
* Test OpenShift serving certificate support provided by our framework.
*/
@DisabledOnNative // building 2 apps is costly and point here is to test FW support not Quarkus
@OpenShiftScenario
public class OpenShiftServingCertificatesIT {

private static final String CLIENT_TLS_CONFIG_NAME = "cert-serving-test-client";
private static final String SERVER_TLS_CONFIG_NAME = "cert-serving-test-server";

@QuarkusApplication(ssl = true, certificates = @Certificate(tlsConfigName = SERVER_TLS_CONFIG_NAME, servingCertificates = @Certificate.ServingCertificates(addServiceCertificate = true)), classes = {
HeroResource.class, Hero.class })
static final RestService server = new RestService()
.withProperty("quarkus.http.ssl.client-auth", "request")
.withProperty("quarkus.http.insecure-requests", "DISABLED");

@QuarkusApplication(certificates = @Certificate(tlsConfigName = CLIENT_TLS_CONFIG_NAME, servingCertificates = @Certificate.ServingCertificates(injectCABundle = true)), classes = {
HeroClient.class, Hero.class, HeroClientResource.class })
static final RestService client = new RestService()
.withProperty("quarkus.rest-client.hero.tls-configuration-name", CLIENT_TLS_CONFIG_NAME)
.withProperty("quarkus.rest-client.hero.uri", () -> server.getURI(Protocol.HTTPS).getRestAssuredStyleUri());

@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-"));
assertTrue(hero.otherName().startsWith("Other-"));
});
}

}
4 changes: 4 additions & 0 deletions examples/https/src/test/java/io/quarkus/qe/hero/Hero.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.quarkus.qe.hero;

public record Hero(Long id, String name, String otherName, int level, String picture, String powers) {
}
15 changes: 15 additions & 0 deletions examples/https/src/test/java/io/quarkus/qe/hero/HeroClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.qe.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();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.qe.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();
}

}
17 changes: 17 additions & 0 deletions examples/https/src/test/java/io/quarkus/qe/hero/HeroResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.qe.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");
}

}
3 changes: 3 additions & 0 deletions examples/https/src/test/resources/test.properties
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
ts.app.log.enable=true

# serving certs only works for internal DNS
ts.server.openshift.use-internal-service-as-url=true
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.quarkus.test.security.certificate;

import static io.quarkus.test.security.certificate.Certificate.createCertsTempDir;
import static io.quarkus.test.services.Certificate.DEFAULT_CONFIG;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import io.quarkus.test.utils.TestExecutionProperties;

public interface CertificateBuilder {

/**
Expand All @@ -16,6 +20,8 @@ public interface CertificateBuilder {

Certificate findCertificateByPrefix(String prefix);

ServingCertificateConfig servingCertificateConfig();

/**
* Regenerates certificate with {@code prefix}.
* The new certificate will be stored at the same location as the original one.
Expand All @@ -24,7 +30,7 @@ public interface CertificateBuilder {
Certificate regenerateCertificate(String prefix, CertificateRequestCustomizer... customizers);

static CertificateBuilder of(Certificate certificate) {
return new CertificateBuilderImp(List.of(certificate));
return new CertificateBuilderImpl(List.of(certificate), null);
}

static CertificateBuilder of(io.quarkus.test.services.Certificate[] certificates) {
Expand All @@ -35,18 +41,45 @@ static CertificateBuilder of(io.quarkus.test.services.Certificate[] certificates
}

private static CertificateBuilder createBuilder(io.quarkus.test.services.Certificate[] certificates) {
Certificate[] generatedCerts = new Certificate[certificates.length];
var svcCertConfigBuilder = ServingCertificateConfig.builder();
List<Certificate> generatedCerts = new ArrayList<>();
for (int i = 0; i < certificates.length; i++) {
var cert = certificates[i];
configureServingCertificates(cert, svcCertConfigBuilder);
boolean generateCerts = cert.configureHttpServer() || cert.configureManagementInterface()
|| cert.configureKeystore() || cert.configureTruststore() || cert.clientCertificates().length > 0;
if (!generateCerts) {
continue;
}
var clientCertReqs = Arrays.stream(cert.clientCertificates())
.map(cc -> new ClientCertificateRequest(cc.cnAttribute(), cc.unknownToServer()))
.toArray(ClientCertificateRequest[]::new);
generatedCerts[i] = Certificate.ofInterchangeable(new CertificateOptions(cert.prefix(), cert.format(),
generatedCerts.add(Certificate.ofInterchangeable(new CertificateOptions(cert.prefix(), cert.format(),
cert.password(), cert.configureKeystore(), cert.configureTruststore(),
cert.configureManagementInterface(),
clientCertReqs, createCertsTempDir(cert.prefix()), new DefaultContainerMountStrategy(cert.prefix()),
false, null, null, null, null, cert.useTlsRegistry(), cert.tlsConfigName(), cert.configureHttpServer()));
false, null, null, null, null, cert.useTlsRegistry(), cert.tlsConfigName(), cert.configureHttpServer())));
}
return new CertificateBuilderImpl(List.copyOf(generatedCerts), svcCertConfigBuilder.build());
}

private static void configureServingCertificates(io.quarkus.test.services.Certificate cert,
ServingCertificateConfig.ServingCertificateConfigBuilder svcCertConfigBuilder) {
if (TestExecutionProperties.isOpenshiftPlatform() && cert.useTlsRegistry()) {
boolean servingCertificatesEnabled = cert.servingCertificates().length > 0;
if (servingCertificatesEnabled) {
for (var servingCertificate : cert.servingCertificates()) {
if (servingCertificate.addServiceCertificate()) {
svcCertConfigBuilder.withAddServiceCertificate(true);
}
if (servingCertificate.injectCABundle()) {
svcCertConfigBuilder.withInjectCABundle(true);
}
}
if (!DEFAULT_CONFIG.equals(cert.tlsConfigName())) {
svcCertConfigBuilder.withTlsConfigName(cert.tlsConfigName());
}
}
}
return new CertificateBuilderImp(List.of(generatedCerts));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import java.util.List;
import java.util.Objects;

record CertificateBuilderImp(List<Certificate> certificates) implements CertificateBuilder {
record CertificateBuilderImpl(List<Certificate> certificates,
ServingCertificateConfig servingCertificateConfig) implements CertificateBuilder {
@Override
public Certificate findCertificateByPrefix(String prefix) {
Objects.requireNonNull(prefix);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.quarkus.test.security.certificate;

import io.quarkus.test.bootstrap.ServiceContext;

public record ServingCertificateConfig(boolean injectCABundle, boolean addServiceCertificate, String tlsConfigName) {

public static final String SERVING_CERTIFICATE_KEY = "serving-certificate-config-key";

public static boolean isServingCertificateScenario(ServiceContext serviceContext) {
return get(serviceContext) != null;
}

public static ServingCertificateConfig get(ServiceContext serviceContext) {
if (serviceContext.get(SERVING_CERTIFICATE_KEY) instanceof ServingCertificateConfig config) {
return config;
}
return null;
}

static ServingCertificateConfigBuilder builder() {
return new ServingCertificateConfigBuilder();
}

static final class ServingCertificateConfigBuilder {

private boolean injectCABundle = false;
private boolean addServiceCertificate = false;
private String tlsConfigName = null;

private ServingCertificateConfigBuilder() {
}

ServingCertificateConfigBuilder withInjectCABundle(boolean injectCABundle) {
this.injectCABundle = injectCABundle;
return this;
}

ServingCertificateConfigBuilder withAddServiceCertificate(boolean addServiceCertificate) {
this.addServiceCertificate = addServiceCertificate;
return this;
}

ServingCertificateConfigBuilder withTlsConfigName(String tlsConfigName) {
this.tlsConfigName = tlsConfigName;
return this;
}

ServingCertificateConfig build() {
if (injectCABundle || addServiceCertificate) {
return new ServingCertificateConfig(injectCABundle, addServiceCertificate, tlsConfigName);
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ enum Format {
*/
String tlsConfigName() default DEFAULT_CONFIG;

/**
* Facilitates support for OpenShift serving certificates.
*/
ServingCertificates[] servingCertificates() default {};

/**
* Specify client certificates that should be generated.
* Generation of more than one client certificate is only implemented for {@link Format#PKCS12}.
Expand All @@ -133,4 +138,26 @@ enum Format {
boolean unknownToServer() default false;
}

/**
* OpenShift serving certificates configuration. This only works when internal service is used as URL.
*/
@interface ServingCertificates {

/**
* Whether service CA bundle should be mounted to Quarkus pod.
* This CA bundle can be used by a REST client to communicate with pod using service certificate.
* To put it blunt, this is client side, while {@link #addServiceCertificate()} is server side.
*/
boolean injectCABundle() default false;

/**
* Whether certificate generated by OpenShift should be mounted to Quarkus pod and the TLS registry
* extension should be configured with the certificate. These certificates are only valid for the internal
* service DNS and won't work outside the OpenShift cluster. That is, use internal service DNS name
* to communicate with other pods.
*/
boolean addServiceCertificate() default false;

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.test.services.quarkus;

import static io.quarkus.test.security.certificate.ServingCertificateConfig.SERVING_CERTIFICATE_KEY;
import static io.quarkus.test.utils.PropertiesUtils.resolveProperty;
import static java.util.stream.Collectors.toSet;

Expand Down Expand Up @@ -227,6 +228,9 @@ protected void configureCertificates() {
.forEach(certificate -> certificate
.configProperties()
.forEach((k, v) -> getContext().withTestScopeConfigProperty(k, v)));
if (certificateBuilder.servingCertificateConfig() != null) {
getContext().put(SERVING_CERTIFICATE_KEY, certificateBuilder.servingCertificateConfig());
}
}
}

Expand Down
Loading

0 comments on commit 6d2a583

Please sign in to comment.