Skip to content

Commit

Permalink
Support gRPC TLS communication and TLS registry
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Sep 3, 2024
1 parent 3dcd001 commit 3c9f345
Show file tree
Hide file tree
Showing 17 changed files with 365 additions and 21 deletions.
4 changes: 4 additions & 0 deletions examples/grpc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.qe</groupId>
<artifactId>quarkus-test-core</artifactId>
Expand Down
17 changes: 17 additions & 0 deletions examples/grpc/src/main/java/io/quarkus/qe/grpc/HelloService.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
package io.quarkus.qe.grpc;

import jakarta.inject.Inject;

import io.grpc.stub.StreamObserver;
import io.quarkus.grpc.GrpcService;
import io.quarkus.security.identity.CurrentIdentityAssociation;

@GrpcService
public class HelloService extends GreeterGrpc.GreeterImplBase {

@Inject
CurrentIdentityAssociation identityAssociation;

@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
String message = "Hello " + name;
responseObserver.onNext(HelloReply.newBuilder().setMessage(message).build());
responseObserver.onCompleted();
}

@Override
public void sayHi(HiRequest request, StreamObserver<HiReply> responseObserver) {
identityAssociation.getDeferredIdentity().subscribe().with(identity -> {
String name = request.getName();
String message = "Hello " + name;
String principalName = identity.isAnonymous() ? "" : identity.getPrincipal().getName();
responseObserver.onNext(HiReply.newBuilder().setMessage(message).setPrincipalName(principalName).build());
responseObserver.onCompleted();
});
}
}
14 changes: 13 additions & 1 deletion examples/grpc/src/main/proto/helloworld.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package io.quarkus.qe.grpc;
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHi (HiRequest) returns (HiReply) {}
}

// The request message containing the user's name.
Expand All @@ -19,4 +20,15 @@ message HelloRequest {
// The response message containing the greetings
message HelloReply {
string message = 1;
}
}

// The request message containing the user's name.
message HiRequest {
string name = 1;
}

// The response message containing the greetings
message HiReply {
string message = 1;
string principalName = 2;
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ public class DevModeGrpcServiceIT {

@Test
public void shouldHelloWorldServiceWork() {
HelloRequest request = HelloRequest.newBuilder().setName(NAME).build();
HelloReply response = GreeterGrpc.newBlockingStub(app.grpcChannel()).sayHello(request);
try (var channel = app.grpcChannel()) {
HelloRequest request = HelloRequest.newBuilder().setName(NAME).build();
HelloReply response = GreeterGrpc.newBlockingStub(channel).sayHello(request);

assertEquals("Hello " + NAME, response.getMessage());
assertEquals("Hello " + NAME, response.getMessage());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkus.qe.grpc;

import static io.quarkus.test.services.Certificate.Format.PEM;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

import io.quarkus.test.bootstrap.GrpcService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.Certificate.ClientCertificate;
import io.quarkus.test.services.QuarkusApplication;

@QuarkusScenario
public class GrpcMtlsTlsRegistryIT {

private static final String CN = "Hagrid";
private static final String NAME = "Albus";

@QuarkusApplication(grpc = true, ssl = true, certificates = @Certificate(format = PEM, configureHttpServer = true, configureKeystore = true, configureTruststore = true, tlsConfigName = "grpc-tls", clientCertificates = @ClientCertificate(cnAttribute = CN)))
static final GrpcService app = (GrpcService) new GrpcService()
.withProperty("quarkus.grpc.server.use-separate-server", "false")
.withProperty("quarkus.http.insecure-requests", "disabled")
.withProperty("quarkus.http.ssl.client-auth", "request")
.withProperty("quarkus.http.auth.permission.perm-1.policy", "authenticated")
.withProperty("quarkus.http.auth.permission.perm-1.paths", "*")
.withProperty("quarkus.http.auth.permission.perm-1.auth-mechanism", "X509");

@Test
public void testMutualTlsCommunicationWithHelloService() {
try (var channel = app.securedGrpcChannel()) {
// here both server and client certificates are generated and used
HiRequest request = HiRequest.newBuilder().setName(NAME).build();
HiReply response = GreeterGrpc.newBlockingStub(channel).sayHi(request);

assertEquals("Hello " + NAME, response.getMessage());
assertEquals("CN=Hagrid", response.getPrincipalName());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ public class GrpcServiceIT {

@Test
public void shouldHelloWorldServiceWork() {
HelloRequest request = HelloRequest.newBuilder().setName(NAME).build();
HelloReply response = GreeterGrpc.newBlockingStub(app.grpcChannel()).sayHello(request);
try (var channel = app.grpcChannel()) {
HelloRequest request = HelloRequest.newBuilder().setName(NAME).build();
HelloReply response = GreeterGrpc.newBlockingStub(channel).sayHello(request);

assertEquals("Hello " + NAME, response.getMessage());
assertEquals("Hello " + NAME, response.getMessage());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.quarkus.qe.grpc;

import static io.quarkus.test.services.Certificate.Format.PEM;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.grpc.StatusRuntimeException;
import io.quarkus.test.bootstrap.GrpcService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.QuarkusApplication;

@QuarkusScenario
public class GrpcTlsRegistryIT {

private static final String NAME = "Albus";

@QuarkusApplication(grpc = true, ssl = true, certificates = @Certificate(format = PEM, configureHttpServer = true, configureKeystore = true, configureTruststore = true, tlsConfigName = "grpc-tls"))
static final GrpcService app = (GrpcService) new GrpcService()
.withProperty("quarkus.grpc.server.use-separate-server", "false")
.withProperty("quarkus.http.insecure-requests", "disabled");

@Test
public void testGrpcServiceUsingTls() {
try (var channel = app.securedGrpcChannel()) {
HiRequest request = HiRequest.newBuilder().setName(NAME).build();
HiReply response = GreeterGrpc.newBlockingStub(channel).sayHi(request);

assertEquals("Hello " + NAME, response.getMessage());
// no authentication
assertEquals("", response.getPrincipalName());
}
}

@Test
public void testUsingTlsIsRequired() {
try (var channel = app.grpcChannel()) {
var greeterGrpcStub = GreeterGrpc.newBlockingStub(channel);
HiRequest request = HiRequest.newBuilder().setName(NAME).build();
Assertions.assertThrows(StatusRuntimeException.class, () -> greeterGrpcStub.sayHi(request),
"Secured channel should be required but isn't");
}
}

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

import static io.quarkus.test.services.Certificate.Format.PEM;
import static io.quarkus.test.services.Certificate.Format.PKCS12;
import static io.quarkus.test.utils.PropertiesUtils.DESTINATION_TO_FILENAME_SEPARATOR;
import static io.quarkus.test.utils.PropertiesUtils.SECRET_WITH_DESTINATION_PREFIX;
Expand Down Expand Up @@ -29,6 +30,8 @@
import java.util.Random;
import java.util.stream.Collectors;

import org.junit.jupiter.api.condition.OS;

import io.quarkus.test.utils.FileUtils;
import io.quarkus.test.utils.TestExecutionProperties;
import io.smallrye.certs.CertificateGenerator;
Expand Down Expand Up @@ -127,8 +130,18 @@ private static Certificate.PemCertificate of(CertificateOptions o) {
// PKCS12 truststore
serverTrustStoreLocation = createPkcs12TruststoreForPem(pemCertsFile.trustStore(), o.password(), cn);
} else {
// ca-cert
serverTrustStoreLocation = getPathOrNull(pemCertsFile.trustStore());
if (withClientCerts) {
serverTrustStoreLocation = getPathOrNull(pemCertsFile.serverTrustFile());
var clientCertLocation = getPathOrNull(pemCertsFile.clientCertFile());
var clientKeyLocation = getPathOrNull(pemCertsFile.clientKeyFile());
var clientTrustStore = getPathOrNull(pemCertsFile.trustFile());
generatedClientCerts
.add(new ClientCertificateImpl(cn, null, clientTrustStore, clientKeyLocation,
clientCertLocation));
} else {
// ca-cert
serverTrustStoreLocation = getPathOrNull(pemCertsFile.trustStore());
}
}
if (o.containerMountStrategy().mountToContainer()) {
if (certLocation != null) {
Expand Down Expand Up @@ -217,11 +230,40 @@ private static Certificate.PemCertificate of(CertificateOptions o) {
}
configureManagementInterfaceProps(o, props, serverKeyStoreLocation);
configureHttpServerProps(o, props);
configurePemConfigurationProperties(o, props, keyLocation, certLocation, serverTrustStoreLocation);
doubleBackSlashesOnWin(props);

return createCertificate(serverKeyStoreLocation, serverTrustStoreLocation, Map.copyOf(props),
List.copyOf(generatedClientCerts), keyLocation, certLocation, o);
}

private static void doubleBackSlashesOnWin(Map<String, String> props) {
if (OS.WINDOWS.isCurrentOs()) {
// we need to quote forward slashes passed as command lines in Windows as they have special meaning
// TODO: this must be done for all config properties, but I do not dare to change it now
// now is not the good time to break test suite as this PR is going to be backported and we have pressing
// matters; let's do it later: https://github.com/quarkus-qe/quarkus-test-framework/issues/1275
props.replaceAll((key, value) -> value.replace("\\", "\\\\"));
}
}

private static void configurePemConfigurationProperties(CertificateOptions options, Map<String, String> props,
String keyLocation, String certLocation, String serverTrustStoreLocation) {
if (options.format() == PEM) {
var keyStorePropertyPrefix = tlsConfigPropPrefix(options, "key-store");
if (keyLocation != null) {
props.put(keyStorePropertyPrefix + "pem-1.key", keyLocation);
}
if (certLocation != null) {
props.put(keyStorePropertyPrefix + "pem-1.cert", certLocation);
}
var trustStorePropertyPrefix = tlsConfigPropPrefix(options, "trust-store");
if (serverTrustStoreLocation != null) {
props.put(trustStorePropertyPrefix + "certs", serverTrustStoreLocation);
}
}
}

private static void configureManagementInterfaceProps(CertificateOptions o, Map<String, String> props,
String serverKeyStoreLocation) {
if (o.configureManagementInterface()) {
Expand Down Expand Up @@ -259,9 +301,11 @@ private static void configureServerKeyStoreProps(CertificateOptions o, Map<Strin
String serverKeyStoreLocation) {
if (o.keystoreProps()) {
if (o.tlsRegistryEnabled()) {
var propPrefix = tlsConfigPropPrefix(o, "key-store");
props.put(propPrefix + "path", serverKeyStoreLocation);
props.put(propPrefix + "password", o.password());
if (o.format() != PEM) {
var propPrefix = tlsConfigPropPrefix(o, "key-store");
props.put(propPrefix + "path", serverKeyStoreLocation);
props.put(propPrefix + "password", o.password());
}
} else {
props.put("quarkus.http.ssl.certificate.key-store-file", serverKeyStoreLocation);
props.put("quarkus.http.ssl.certificate.key-store-file-type", o.format().toString());
Expand All @@ -274,9 +318,11 @@ private static void configureServerTrustStoreProps(CertificateOptions o, Map<Str
String serverTrustStoreLocation) {
if (o.truststoreProps()) {
if (o.tlsRegistryEnabled()) {
var propPrefix = tlsConfigPropPrefix(o, "trust-store");
props.put(propPrefix + "path", serverTrustStoreLocation);
props.put(propPrefix + "password", o.password());
if (o.format() != PEM) {
var propPrefix = tlsConfigPropPrefix(o, "trust-store");
props.put(propPrefix + "path", serverTrustStoreLocation);
props.put(propPrefix + "password", o.password());
}
} else {
props.put("quarkus.http.ssl.certificate.trust-store-file", serverTrustStoreLocation);
props.put("quarkus.http.ssl.certificate.trust-store-file-type", o.format().toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
package io.quarkus.test.security.certificate;

record ClientCertificateImpl(String commonName, String keystorePath, String truststorePath) implements ClientCertificate {
record ClientCertificateImpl(String commonName, String keystorePath, String truststorePath, String keyPath,
String certPath) implements PemClientCertificate {

ClientCertificateImpl(String commonName, String keystorePath, String truststorePath) {
this(commonName, keystorePath, truststorePath, null, null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.quarkus.test.security.certificate;

public interface PemClientCertificate extends ClientCertificate {

String keyPath();

String certPath();

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.quarkus.test.logging.Log;
import io.quarkus.test.logging.LoggingHandler;
import io.quarkus.test.services.URILike;
import io.quarkus.test.services.quarkus.model.QuarkusProperties;
import io.quarkus.test.utils.ProcessBuilderProvider;
import io.quarkus.test.utils.ProcessUtils;
import io.quarkus.test.utils.PropertiesUtils;
Expand Down Expand Up @@ -150,7 +151,11 @@ private void assignPorts() {
}

if (model.isGrpcEnabled()) {
assignedGrpcPort = getOrAssignPortByProperty(QUARKUS_GRPC_SERVER_PORT_PROPERTY);
if (QuarkusProperties.useSeparateGrpcServer(getContext())) {
assignedGrpcPort = getOrAssignPortByProperty(QUARKUS_GRPC_SERVER_PORT_PROPERTY);
} else {
assignedGrpcPort = assignedHttpPort;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

public final class QuarkusProperties {

public static final PropertyLookup USE_SEPARATE_GRPC_SERVER = new PropertyLookup("quarkus.grpc.server.use-separate-server",
"true");
public static final PropertyLookup PLATFORM_GROUP_ID = new PropertyLookup("quarkus.platform.group-id", "io.quarkus");
public static final PropertyLookup PLATFORM_VERSION = new PropertyLookup("quarkus.platform.version");
public static final PropertyLookup PLUGIN_VERSION = new PropertyLookup("quarkus-plugin.version");
Expand Down Expand Up @@ -60,6 +62,10 @@ public static boolean isJvmPackageType(ServiceContext context) {
return !isNativeEnabled() && PACKAGE_TYPE_JVM_VALUES.contains(PACKAGE_TYPE.get(context));
}

public static boolean useSeparateGrpcServer(ServiceContext context) {
return Boolean.parseBoolean(USE_SEPARATE_GRPC_SERVER.get(context));
}

private static String defaultVersionIfEmpty(String version) {
if (StringUtils.isEmpty(version)) {
version = Version.getVersion();
Expand Down
4 changes: 4 additions & 0 deletions quarkus-test-service-grpc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
</dependency>
</dependencies>
</project>
Loading

0 comments on commit 3c9f345

Please sign in to comment.