From cb36645429da1afd512386859abd9a92a17aa191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Wed, 22 May 2024 17:32:15 +0200 Subject: [PATCH] Add opt encrypted communication for MSSQL --- .../ContainerManagedResourceBuilder.java | 15 +- .../DockerContainerManagedResource.java | 18 ++- .../test/bootstrap/ServiceContext.java | 2 +- .../annotations/DisabledOnFipsAndJava17.java | 20 +++ .../DisabledOnFipsAndJava17Condition.java | 31 ++++ .../security/certificate/Certificate.java | 141 ++++++++++++------ .../certificate/CertificateBuilder.java | 4 + .../certificate/CertificateOptions.java | 9 ++ .../certificate/ContainerMountStrategy.java | 23 +++ .../DefaultContainerMountStrategy.java | 56 +++++++ .../FixedPathContainerMountStrategy.java | 36 +++++ quarkus-test-service-database/pom.xml | 4 + .../test/bootstrap/SqlServerService.java | 42 ++++++ .../test/services/SqlServerContainer.java | 22 +++ .../SqlServerContainerAnnotationBinding.java | 28 ++++ .../SqlServerManagedResourceBuilder.java | 58 +++++++ ...o.quarkus.test.bootstrap.AnnotationBinding | 1 + .../src/main/resources/mssql.conf | 5 + 18 files changed, 464 insertions(+), 51 deletions(-) create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17.java create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17Condition.java create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateOptions.java create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/ContainerMountStrategy.java create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/DefaultContainerMountStrategy.java create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/FixedPathContainerMountStrategy.java create mode 100644 quarkus-test-service-database/src/main/java/io/quarkus/test/services/SqlServerContainer.java create mode 100644 quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerContainerAnnotationBinding.java create mode 100644 quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerManagedResourceBuilder.java create mode 100644 quarkus-test-service-database/src/main/resources/META-INF/services/io.quarkus.test.bootstrap.AnnotationBinding create mode 100644 quarkus-test-service-database/src/main/resources/mssql.conf diff --git a/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/ContainerManagedResourceBuilder.java b/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/ContainerManagedResourceBuilder.java index 2b485ce72..cd656a1af 100644 --- a/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/ContainerManagedResourceBuilder.java +++ b/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/ContainerManagedResourceBuilder.java @@ -46,11 +46,16 @@ protected ServiceContext getContext() { @Override public void init(Annotation annotation) { Container metadata = (Container) annotation; - this.image = PropertiesUtils.resolveProperty(metadata.image()); - this.command = metadata.command(); - this.expectedLog = PropertiesUtils.resolveProperty(metadata.expectedLog()); - this.port = metadata.port(); - this.portDockerHostToLocalhost = metadata.portDockerHostToLocalhost(); + init(metadata.image(), metadata.command(), metadata.expectedLog(), metadata.port(), + metadata.portDockerHostToLocalhost()); + } + + protected void init(String image, String[] command, String expectedLog, int port, boolean portDockerHostToLocalhost) { + this.image = PropertiesUtils.resolveProperty(image); + this.command = command; + this.expectedLog = PropertiesUtils.resolveProperty(expectedLog); + this.port = port; + this.portDockerHostToLocalhost = portDockerHostToLocalhost; } @Override diff --git a/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/DockerContainerManagedResource.java b/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/DockerContainerManagedResource.java index fe0fb006c..f5528cc88 100644 --- a/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/DockerContainerManagedResource.java +++ b/quarkus-test-containers/src/main/java/io/quarkus/test/services/containers/DockerContainerManagedResource.java @@ -7,6 +7,7 @@ import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_PREFIX_MATCHER; import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_SPLIT_CHAR; import static io.quarkus.test.utils.PropertiesUtils.SECRET_PREFIX; +import static io.quarkus.test.utils.PropertiesUtils.SECRET_WITH_DESTINATION_PREFIX; import static io.quarkus.test.utils.PropertiesUtils.SLASH; import java.nio.file.Files; @@ -28,6 +29,7 @@ import io.quarkus.test.logging.TestContainersLoggingHandler; import io.quarkus.test.services.URILike; import io.quarkus.test.utils.DockerUtils; +import io.quarkus.test.utils.FileUtils; public abstract class DockerContainerManagedResource implements ManagedResource { @@ -131,6 +133,11 @@ private Map resolveProperties() { if (isResource(entry.getValue())) { value = entry.getValue().replace(RESOURCE_PREFIX, StringUtils.EMPTY); addFileToContainer(value); + } else if (isSecretWithDestinationPath(entry.getValue())) { + value = entry.getValue().replace(SECRET_WITH_DESTINATION_PREFIX, StringUtils.EMPTY); + String destinationPath = value.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[0]; + String fileName = value.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[1]; + addFileToContainer(destinationPath, fileName); } else if (isResourceWithDestinationPath(entry.getValue())) { value = entry.getValue().replace(RESOURCE_WITH_DESTINATION_PREFIX, StringUtils.EMPTY); if (!value.matches(RESOURCE_WITH_DESTINATION_PREFIX_MATCHER)) { @@ -163,8 +170,17 @@ private void addFileToContainer(String filePath) { } private void addFileToContainer(String destinationPath, String hostFilePath) { + var filePath = FileUtils.findTargetFile(Path.of("target"), hostFilePath); String containerFullPath = destinationPath + SLASH + hostFilePath; - innerContainer.withClasspathResourceMapping(hostFilePath, containerFullPath, BindMode.READ_ONLY); + if (filePath.isEmpty()) { + innerContainer.withClasspathResourceMapping(hostFilePath, containerFullPath, BindMode.READ_ONLY); + } else { + innerContainer.withCopyFileToContainer(MountableFile.forHostPath(filePath.get()), containerFullPath); + } + } + + private static boolean isSecretWithDestinationPath(String key) { + return key.startsWith(SECRET_WITH_DESTINATION_PREFIX); } private boolean isResourceWithDestinationPath(String key) { diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/ServiceContext.java b/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/ServiceContext.java index 1ad5413df..d2031df6b 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/ServiceContext.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/ServiceContext.java @@ -70,7 +70,7 @@ public ServiceContext withTestScopeConfigProperty(String key, String value) { return this; } - Map getConfigPropertiesWithTestScope() { + public Map getConfigPropertiesWithTestScope() { return configPropertiesWithTestScope; } diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17.java b/quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17.java new file mode 100644 index 000000000..7ac914538 --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17.java @@ -0,0 +1,20 @@ +package io.quarkus.test.scenarios.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +@Inherited +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(DisabledOnFipsAndJava17Condition.class) +public @interface DisabledOnFipsAndJava17 { + /** + * Why is the annotated test class or test method disabled. + */ + String reason() default ""; +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17Condition.java b/quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17Condition.java new file mode 100644 index 000000000..f9c5136c2 --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/scenarios/annotations/DisabledOnFipsAndJava17Condition.java @@ -0,0 +1,31 @@ +package io.quarkus.test.scenarios.annotations; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class DisabledOnFipsAndJava17Condition implements ExecutionCondition { + + /** + * We set environment variable "FIPS" to "fips" in our Jenkins jobs when FIPS are enabled. + */ + private static final String FIPS_ENABLED = "fips"; + private static final int JAVA_17 = 17; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + if (isFipsEnabledEnvironment() && isJava17()) { + return ConditionEvaluationResult.disabled("The test is running in FIPS enabled environment with Java 17"); + } + + return ConditionEvaluationResult.enabled("The test is not running in FIPS enabled environment with Java 17"); + } + + private static boolean isFipsEnabledEnvironment() { + return FIPS_ENABLED.equals(System.getenv().get("FIPS")); + } + + private static boolean isJava17() { + return JAVA_17 == Runtime.version().feature(); + } +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java index 59d536b1c..1e143f8a4 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/Certificate.java @@ -3,19 +3,21 @@ 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; -import static io.quarkus.test.utils.TestExecutionProperties.isKubernetesPlatform; -import static io.quarkus.test.utils.TestExecutionProperties.isOpenshiftPlatform; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -27,7 +29,6 @@ import java.util.Random; import java.util.stream.Collectors; -import io.quarkus.test.utils.FileUtils; import me.escoffier.certs.CertificateGenerator; import me.escoffier.certs.CertificateRequest; import me.escoffier.certs.JksCertificateFiles; @@ -60,27 +61,40 @@ interface PemCertificate extends Certificate { } + static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password, Path targetDir, + ContainerMountStrategy containerMountStrategy, boolean createPkcs12TsForPem) { + return of(new CertificateOptions(prefix, format, password, false, false, false, + new io.quarkus.test.services.Certificate.ClientCertificate[0], + targetDir, containerMountStrategy, createPkcs12TsForPem)); + } + static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password) { - return of(prefix, format, password, false, false, false, new io.quarkus.test.services.Certificate.ClientCertificate[0]); + return of(prefix, format, password, createCertsTempDir(prefix), new DefaultContainerMountStrategy(prefix), false); } static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password, boolean keystoreProps, boolean truststoreProps, boolean keystoreManagementInterfaceProps, io.quarkus.test.services.Certificate.ClientCertificate[] clientCertificates) { + return of(new CertificateOptions(prefix, format, password, keystoreProps, truststoreProps, + keystoreManagementInterfaceProps, + clientCertificates, createCertsTempDir(prefix), new DefaultContainerMountStrategy(prefix), false)); + } + + private static Certificate of(CertificateOptions o) { Map props = new HashMap<>(); - CertificateGenerator generator = new CertificateGenerator(createCertsTempDir(prefix), false); + CertificateGenerator generator = new CertificateGenerator(o.localTargetDir(), false); String serverTrustStoreLocation = null; String serverKeyStoreLocation = null; String keyLocation = null; String certLocation = null; List generatedClientCerts = new ArrayList<>(); - String[] cnAttrs = collectCommonNames(clientCertificates); - var unknownClientCn = getUnknownClientCnAttr(clientCertificates, cnAttrs); + String[] cnAttrs = collectCommonNames(o.clientCertificates()); + var unknownClientCn = getUnknownClientCnAttr(o.clientCertificates(), cnAttrs); // 1. GENERATE FIRST CERTIFICATE AND SERVER KEYSTORE AND TRUSTSTORE boolean withClientCerts = cnAttrs.length > 0; String cn = withClientCerts ? cnAttrs[0] : "localhost"; - final CertificateRequest request = createCertificateRequest(prefix, format, password, withClientCerts, cn); + final CertificateRequest request = createCertificateRequest(o.prefix(), o.format(), o.password(), withClientCerts, cn); try { var certFile = generator.generate(request).get(0); if (certFile instanceof Pkcs12CertificateFiles pkcs12CertFile) { @@ -96,17 +110,32 @@ static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format } else if (certFile instanceof PemCertificateFiles pemCertsFile) { keyLocation = getPathOrNull(pemCertsFile.keyFile()); certLocation = getPathOrNull(pemCertsFile.certFile()); - if (isOpenshiftPlatform() || isKubernetesPlatform()) { + if (o.createPkcs12TsForPem()) { + // PKCS12 truststore + serverTrustStoreLocation = createPkcs12TruststoreForPem(pemCertsFile.trustStore(), o.password(), cn); + } else { + // ca-cert + serverTrustStoreLocation = getPathOrNull(pemCertsFile.trustStore()); + } + if (o.containerMountStrategy().mountToContainer()) { if (certLocation != null) { - certLocation = makeFileMountPathUnique(prefix, certLocation); - // mount certificate to the pod - props.put(getRandomPropKey("crt"), toSecretProperty(certLocation)); + var containerMountPath = o.containerMountStrategy().certPath(certLocation); + if (o.containerMountStrategy().containerShareMountPathWithApp()) { + certLocation = containerMountPath; + } + + // mount certificate to the container + props.put(getRandomPropKey("crt"), toSecretProperty(containerMountPath)); } if (keyLocation != null) { - keyLocation = makeFileMountPathUnique(prefix, keyLocation); - // mount private key to the pod - props.put(getRandomPropKey("key"), toSecretProperty(keyLocation)); + var containerMountPath = o.containerMountStrategy().keyPath(keyLocation); + if (o.containerMountStrategy().containerShareMountPathWithApp()) { + keyLocation = containerMountPath; + } + + // mount private key to the container + props.put(getRandomPropKey("key"), toSecretProperty(containerMountPath)); } } } else if (certFile instanceof JksCertificateFiles jksCertFile) { @@ -126,18 +155,18 @@ static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format // 2. IF THERE IS MORE THAN ONE CLIENT CERTIFICATE, GENERATE OTHERS if (withClientCerts && cnAttrs.length > 1) { - if (format != PKCS12) { + if (o.format() != PKCS12) { throw new IllegalArgumentException( "Generation of more than one client certificate is only implemented for PKCS12."); } var correctClientTruststore = Path.of(generatedClientCerts.get(0).truststorePath()).toFile(); for (int i = 1; i < cnAttrs.length; i++) { var clientCn = cnAttrs[i]; - var clientPrefix = clientCn + "-" + prefix; - var clientRequest = createCertificateRequest(clientPrefix, format, password, true, clientCn); + var clientPrefix = clientCn + "-" + o.prefix(); + var clientRequest = createCertificateRequest(clientPrefix, o.format(), o.password(), true, clientCn); try { var clientCertFile = (Pkcs12CertificateFiles) generator.generate(clientRequest).get(0); - fixGeneratedClientCerts(clientPrefix, password, clientCertFile, correctClientTruststore, + fixGeneratedClientCerts(clientPrefix, o.password(), clientCertFile, correctClientTruststore, serverTrustStoreLocation, unknownClientCn, clientCn); generatedClientCerts .add(new ClientCertificateImpl(clientCn, getPathOrNull(clientCertFile.clientKeyStoreFile()), @@ -150,36 +179,45 @@ static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format // 3. PREPARE QUARKUS APPLICATION CONFIGURATION PROPERTIES if (serverTrustStoreLocation != null) { - if (isOpenshiftPlatform() || isKubernetesPlatform()) { - // mount truststore to the pod - props.put(getRandomPropKey("truststore"), toSecretProperty(serverTrustStoreLocation)); + if (o.containerMountStrategy().mountToContainer()) { + var containerMountPath = o.containerMountStrategy().truststorePath(serverTrustStoreLocation); + if (o.containerMountStrategy().containerShareMountPathWithApp()) { + serverTrustStoreLocation = containerMountPath; + } + + // mount truststore to the container + props.put(getRandomPropKey("truststore"), toSecretProperty(containerMountPath)); } - if (truststoreProps) { + if (o.truststoreProps()) { props.put("quarkus.http.ssl.certificate.trust-store-file", serverTrustStoreLocation); - props.put("quarkus.http.ssl.certificate.trust-store-file-type", format.toString()); - props.put("quarkus.http.ssl.certificate.trust-store-password", password); + props.put("quarkus.http.ssl.certificate.trust-store-file-type", o.format().toString()); + props.put("quarkus.http.ssl.certificate.trust-store-password", o.password()); } } if (serverKeyStoreLocation != null) { - if (isOpenshiftPlatform() || isKubernetesPlatform()) { - serverKeyStoreLocation = makeFileMountPathUnique(prefix, serverKeyStoreLocation); - // mount keystore to the pod - props.put(getRandomPropKey("keystore"), toSecretProperty(serverKeyStoreLocation)); + if (o.containerMountStrategy().mountToContainer()) { + var containerMountPath = o.containerMountStrategy().keystorePath(serverKeyStoreLocation); + if (o.containerMountStrategy().containerShareMountPathWithApp()) { + serverKeyStoreLocation = containerMountPath; + } + + // mount keystore to the container + props.put(getRandomPropKey("keystore"), toSecretProperty(containerMountPath)); } - if (keystoreProps) { + if (o.keystoreProps()) { props.put("quarkus.http.ssl.certificate.key-store-file", serverKeyStoreLocation); - props.put("quarkus.http.ssl.certificate.key-store-file-type", format.toString()); - props.put("quarkus.http.ssl.certificate.key-store-password", password); + props.put("quarkus.http.ssl.certificate.key-store-file-type", o.format().toString()); + props.put("quarkus.http.ssl.certificate.key-store-password", o.password()); } - if (keystoreManagementInterfaceProps) { + if (o.keystoreManagementInterfaceProps()) { props.put("quarkus.management.ssl.certificate.key-store-file", serverKeyStoreLocation); - props.put("quarkus.management.ssl.certificate.key-store-file-type", format.toString()); - props.put("quarkus.management.ssl.certificate.key-store-password", password); + props.put("quarkus.management.ssl.certificate.key-store-file-type", o.format().toString()); + props.put("quarkus.management.ssl.certificate.key-store-password", o.password()); } } return new CertificateImpl(serverKeyStoreLocation, serverTrustStoreLocation, Map.copyOf(props), - List.copyOf(generatedClientCerts), password, format.toString(), keyLocation, certLocation, prefix); + List.copyOf(generatedClientCerts), o.password(), o.format().toString(), keyLocation, certLocation, o.prefix()); } private static String getUnknownClientCnAttr(io.quarkus.test.services.Certificate.ClientCertificate[] clientCertificates, @@ -275,14 +313,7 @@ private static String getPathOrNull(Path file) { return null; } - private static String makeFileMountPathUnique(String prefix, String storeLocation) { - var newTempCertDir = createCertsTempDir(prefix); - var storeFile = Path.of(storeLocation).toFile(); - FileUtils.copyFileTo(storeFile, newTempCertDir); - return newTempCertDir.resolve(storeFile.getName()).toAbsolutePath().toString(); - } - - private static Path createCertsTempDir(String prefix) { + static Path createCertsTempDir(String prefix) { Path certsDir; try { certsDir = Files.createTempDirectory(prefix + "-certs"); @@ -291,4 +322,26 @@ private static Path createCertsTempDir(String prefix) { } return certsDir; } + + private static String createPkcs12TruststoreForPem(Path caCertPath, String password, String alias) { + try { + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + try (FileInputStream is = new FileInputStream(caCertPath.toFile())) { + X509Certificate cer = (X509Certificate) fact.generateCertificate(is); + + var newTruststorePath = Files.createTempFile("pem-12-truststore", ".p12"); + + try (OutputStream truststoreOs = Files.newOutputStream(newTruststorePath)) { + KeyStore truststore = KeyStore.getInstance("PKCS12"); + truststore.load(null, password.toCharArray()); + truststore.setCertificateEntry(alias, cer); + truststore.store(truststoreOs, password.toCharArray()); + } + + return newTruststorePath.toAbsolutePath().toString(); + } + } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to create PKCS12 truststore", e); + } + } } diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java index 2f42ce973..63989d0ea 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java @@ -13,6 +13,10 @@ public interface CertificateBuilder { Certificate findCertificateByPrefix(String prefix); + static CertificateBuilder of(Certificate certificate) { + return new CertificateBuilderImp(List.of(certificate)); + } + static CertificateBuilder of(io.quarkus.test.services.Certificate[] certificates) { if (certificates == null || certificates.length == 0) { return null; diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateOptions.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateOptions.java new file mode 100644 index 000000000..1a959be3e --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateOptions.java @@ -0,0 +1,9 @@ +package io.quarkus.test.security.certificate; + +import java.nio.file.Path; + +record CertificateOptions(String prefix, io.quarkus.test.services.Certificate.Format format, String password, + boolean keystoreProps, boolean truststoreProps, boolean keystoreManagementInterfaceProps, + io.quarkus.test.services.Certificate.ClientCertificate[] clientCertificates, Path localTargetDir, + ContainerMountStrategy containerMountStrategy, boolean createPkcs12TsForPem) { +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/ContainerMountStrategy.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/ContainerMountStrategy.java new file mode 100644 index 000000000..21703ff0c --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/ContainerMountStrategy.java @@ -0,0 +1,23 @@ +package io.quarkus.test.security.certificate; + +public interface ContainerMountStrategy { + + String truststorePath(String currentLocation); + + String keystorePath(String currentLocation); + + String keyPath(String currentLocation); + + String certPath(String currentLocation); + + /** + * Whether container destination path is also path used by Quarkus application when accessing these certs. + * Simply put it, if 'yes' is returned, we are probably mounting certs to the Quarkus application pod. + */ + boolean containerShareMountPathWithApp(); + + /** + * Whether certificates should be mounted to the container. + */ + boolean mountToContainer(); +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/DefaultContainerMountStrategy.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/DefaultContainerMountStrategy.java new file mode 100644 index 000000000..dd372b1a4 --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/DefaultContainerMountStrategy.java @@ -0,0 +1,56 @@ +package io.quarkus.test.security.certificate; + +import static io.quarkus.test.security.certificate.Certificate.createCertsTempDir; +import static io.quarkus.test.utils.TestExecutionProperties.isKubernetesPlatform; +import static io.quarkus.test.utils.TestExecutionProperties.isOpenshiftPlatform; + +import java.nio.file.Path; + +import io.quarkus.test.utils.FileUtils; + +class DefaultContainerMountStrategy implements ContainerMountStrategy { + + private final String prefix; + + DefaultContainerMountStrategy(String prefix) { + this.prefix = prefix; + } + + @Override + public String truststorePath(String currentLocation) { + // no point of making both keystore and truststore unique, one of them is enough + return currentLocation; + } + + @Override + public String keystorePath(String currentLocation) { + return makeFileMountPathUnique(prefix, currentLocation); + } + + @Override + public String keyPath(String currentLocation) { + return makeFileMountPathUnique(prefix, currentLocation); + } + + @Override + public String certPath(String currentLocation) { + return makeFileMountPathUnique(prefix, currentLocation); + } + + @Override + public boolean containerShareMountPathWithApp() { + return true; + } + + @Override + public boolean mountToContainer() { + return isOpenshiftPlatform() || isKubernetesPlatform(); + } + + private static String makeFileMountPathUnique(String prefix, String storeLocation) { + var newTempCertDir = createCertsTempDir(prefix); + var storeFile = Path.of(storeLocation).toFile(); + FileUtils.copyFileTo(storeFile, newTempCertDir); + return newTempCertDir.resolve(storeFile.getName()).toAbsolutePath().toString(); + } +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/FixedPathContainerMountStrategy.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/FixedPathContainerMountStrategy.java new file mode 100644 index 000000000..1daa3f28b --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/FixedPathContainerMountStrategy.java @@ -0,0 +1,36 @@ +package io.quarkus.test.security.certificate; + +public record FixedPathContainerMountStrategy(String truststorePath, String keystorePath, String keyPath, + String certPath) implements ContainerMountStrategy { + + @Override + public String truststorePath(String currentLocation) { + return truststorePath(); + } + + @Override + public String keystorePath(String currentLocation) { + return keystorePath(); + } + + @Override + public String keyPath(String currentLocation) { + return keyPath(); + } + + @Override + public String certPath(String currentLocation) { + return certPath(); + } + + @Override + public boolean containerShareMountPathWithApp() { + return false; + } + + @Override + public boolean mountToContainer() { + return true; + } + +} diff --git a/quarkus-test-service-database/pom.xml b/quarkus-test-service-database/pom.xml index 2a3462995..833d19ef8 100644 --- a/quarkus-test-service-database/pom.xml +++ b/quarkus-test-service-database/pom.xml @@ -13,5 +13,9 @@ io.quarkus.qe quarkus-test-core + + io.quarkus.qe + quarkus-test-containers + diff --git a/quarkus-test-service-database/src/main/java/io/quarkus/test/bootstrap/SqlServerService.java b/quarkus-test-service-database/src/main/java/io/quarkus/test/bootstrap/SqlServerService.java index 7a8dfdbfa..a879db06b 100644 --- a/quarkus-test-service-database/src/main/java/io/quarkus/test/bootstrap/SqlServerService.java +++ b/quarkus-test-service-database/src/main/java/io/quarkus/test/bootstrap/SqlServerService.java @@ -1,5 +1,12 @@ package io.quarkus.test.bootstrap; +import static io.quarkus.test.services.containers.SqlServerManagedResourceBuilder.CERTIFICATE_PREFIX; + +import java.util.Map; + +import io.quarkus.test.security.certificate.Certificate.PemCertificate; +import io.quarkus.test.security.certificate.CertificateBuilder; + public class SqlServerService extends DatabaseService { private static final String USER = "sa"; @@ -28,6 +35,41 @@ public String getJdbcUrl() { return "jdbc:" + getJdbcName() + "://" + host.getHost() + ":" + host.getPort() + ";databaseName=" + getDatabase(); } + /** + * @see #getTlsProperties(String) + */ + public Map getTlsProperties() { + return getTlsProperties(null); + } + + /** + * Additional JDBC extension properties configuring SQL Server driver to use encrypted communication. + * + * @param datasourceName datasource name + * @return additional JDBC properties + */ + public Map getTlsProperties(String datasourceName) { + CertificateBuilder certBuilder = getPropertyFromContext(CertificateBuilder.INSTANCE_KEY); + if (certBuilder != null && certBuilder.findCertificateByPrefix(CERTIFICATE_PREFIX) instanceof PemCertificate pemCert) { + final String additionalJdbcProperties; + if (datasourceName != null && !datasourceName.isEmpty()) { + additionalJdbcProperties = "quarkus.datasource.%s.jdbc.additional-jdbc-properties.".formatted(datasourceName); + } else { + additionalJdbcProperties = "quarkus.datasource.jdbc.additional-jdbc-properties."; + } + return Map.of( + additionalJdbcProperties + "trustStore", pemCert.truststorePath(), + additionalJdbcProperties + "trustStorePassword", pemCert.password(), + additionalJdbcProperties + "trustStoreType", "PKCS12", + additionalJdbcProperties + "trustServerCertificate", "false", + additionalJdbcProperties + "sslProtocol", "TLSv1.2", + additionalJdbcProperties + "authentication", "SqlPassword", + additionalJdbcProperties + "fips", "true", + additionalJdbcProperties + "encrypt", "true"); + } + return Map.of(); + } + @Override public SqlServerService withUser(String user) { throw new UnsupportedOperationException("You cannot configure a username for SQL Server"); diff --git a/quarkus-test-service-database/src/main/java/io/quarkus/test/services/SqlServerContainer.java b/quarkus-test-service-database/src/main/java/io/quarkus/test/services/SqlServerContainer.java new file mode 100644 index 000000000..5329ac5b0 --- /dev/null +++ b/quarkus-test-service-database/src/main/java/io/quarkus/test/services/SqlServerContainer.java @@ -0,0 +1,22 @@ +package io.quarkus.test.services; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SqlServerContainer { + + String image() default "${mssql.image}"; + + int port() default 1433; + + String expectedLog() default "Service Broker manager has started"; + + /** + * Encrypt connections to SQL Server. + */ + boolean tlsEnabled() default false; +} diff --git a/quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerContainerAnnotationBinding.java b/quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerContainerAnnotationBinding.java new file mode 100644 index 000000000..52d39e53a --- /dev/null +++ b/quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerContainerAnnotationBinding.java @@ -0,0 +1,28 @@ +package io.quarkus.test.services.containers; + +import java.lang.reflect.Field; + +import io.quarkus.test.bootstrap.AnnotationBinding; +import io.quarkus.test.bootstrap.ManagedResourceBuilder; +import io.quarkus.test.services.SqlServerContainer; + +public class SqlServerContainerAnnotationBinding implements AnnotationBinding { + + @Override + public boolean isFor(Field field) { + return field.isAnnotationPresent(SqlServerContainer.class); + } + + @Override + public ManagedResourceBuilder createBuilder(Field field) throws Exception { + SqlServerContainer metadata = field.getAnnotation(SqlServerContainer.class); + ManagedResourceBuilder builder = new SqlServerManagedResourceBuilder(); + builder.init(metadata); + return builder; + } + + @Override + public boolean requiresLinuxContainersOnBareMetal() { + return true; + } +} diff --git a/quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerManagedResourceBuilder.java b/quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerManagedResourceBuilder.java new file mode 100644 index 000000000..a0e8cd4e9 --- /dev/null +++ b/quarkus-test-service-database/src/main/java/io/quarkus/test/services/containers/SqlServerManagedResourceBuilder.java @@ -0,0 +1,58 @@ +package io.quarkus.test.services.containers; + +import static io.quarkus.test.services.Certificate.Format.PEM; +import static io.quarkus.test.utils.PropertiesUtils.DESTINATION_TO_FILENAME_SEPARATOR; +import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_PREFIX; + +import java.lang.annotation.Annotation; +import java.nio.file.Path; + +import io.quarkus.test.bootstrap.ManagedResource; +import io.quarkus.test.bootstrap.ServiceContext; +import io.quarkus.test.security.certificate.Certificate; +import io.quarkus.test.security.certificate.CertificateBuilder; +import io.quarkus.test.security.certificate.FixedPathContainerMountStrategy; +import io.quarkus.test.services.SqlServerContainer; + +public final class SqlServerManagedResourceBuilder extends ContainerManagedResourceBuilder { + + public static final String CERTIFICATE_PREFIX = "mssql"; + private boolean tlsEnabled = false; + + @Override + public void init(Annotation annotation) { + if (annotation instanceof SqlServerContainer sqlServerContainer) { + tlsEnabled = sqlServerContainer.tlsEnabled(); + init(sqlServerContainer.image(), new String[0], sqlServerContainer.expectedLog(), sqlServerContainer.port(), + tlsEnabled); + } else { + throw new IllegalStateException("Expected annotation SqlServerContainer, but got: " + annotation); + } + } + + @Override + public ManagedResource build(ServiceContext context) { + if (!tlsEnabled) { + return super.build(context); + } + + var destStrategy = new FixedPathContainerMountStrategy("/etc/ssl/ca-crt/mssql-ca.crt", null, + "/etc/ssl/private/mssql.key", "/etc/ssl/certs/mssql.crt"); + var cert = Certificate.of(CERTIFICATE_PREFIX, PEM, "password", certTargetDir(), destStrategy, true); + + cert.configProperties().forEach(context::withTestScopeConfigProperty); + context.withTestScopeConfigProperty("mssql-config", createConfigFileProperty()); + context.put(CertificateBuilder.INSTANCE_KEY, CertificateBuilder.of(cert)); + + return super.build(context); + } + + private static String createConfigFileProperty() { + return RESOURCE_WITH_DESTINATION_PREFIX + "/var/opt/mssql/" + DESTINATION_TO_FILENAME_SEPARATOR + "mssql.conf"; + } + + private static Path certTargetDir() { + // when mounting to Docker we are not searching for files recursively, so we put certs directly to the target dir + return Path.of("target"); + } +} diff --git a/quarkus-test-service-database/src/main/resources/META-INF/services/io.quarkus.test.bootstrap.AnnotationBinding b/quarkus-test-service-database/src/main/resources/META-INF/services/io.quarkus.test.bootstrap.AnnotationBinding new file mode 100644 index 000000000..e1f809b53 --- /dev/null +++ b/quarkus-test-service-database/src/main/resources/META-INF/services/io.quarkus.test.bootstrap.AnnotationBinding @@ -0,0 +1 @@ +io.quarkus.test.services.containers.SqlServerContainerAnnotationBinding diff --git a/quarkus-test-service-database/src/main/resources/mssql.conf b/quarkus-test-service-database/src/main/resources/mssql.conf new file mode 100644 index 000000000..18d325160 --- /dev/null +++ b/quarkus-test-service-database/src/main/resources/mssql.conf @@ -0,0 +1,5 @@ +[network] +tlscert = /etc/ssl/certs/mssql.crt +tlskey = /etc/ssl/private/mssql.key +tlsprotocols = 1.2 +forceencryption = 1