From 60020fba75de674bfc82c35f4e93e831289a4eda Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 6 Oct 2021 15:46:07 +0200 Subject: [PATCH] Improve handling of advanced Neo4j url schemes This adds dedicated support of Neo4j `neo4j+s` and `neo4j+ssc` URL schemes by skipping all other configuration of encryption settings as the underlying driver prevents configuration of encryption settings via url schema and explicit settings at the same time. Also adds a bit of documentation. This fixes #19412. Co-authored-by: Guillaume Smet --- docs/src/main/asciidoc/neo4j.adoc | 11 +- extensions/neo4j/runtime/pom.xml | 5 + .../neo4j/runtime/Neo4jDriverRecorder.java | 29 ++++- .../neo4j/runtime/EmptyShutdownContext.java | 16 +++ .../neo4j/runtime/Neo4jConfigurationTest.java | 4 +- .../runtime/Neo4jDriverRecorderTest.java | 119 ++++++++++++++++++ 6 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/EmptyShutdownContext.java create mode 100644 extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorderTest.java diff --git a/docs/src/main/asciidoc/neo4j.adoc b/docs/src/main/asciidoc/neo4j.adoc index 62f63d64d8f5b..fc7dc3ba4db11 100644 --- a/docs/src/main/asciidoc/neo4j.adoc +++ b/docs/src/main/asciidoc/neo4j.adoc @@ -4,7 +4,7 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Neo4j -:neo4j_version: 4.0.0 +:neo4j_version: 4.3 include::./attributes.adoc[] :extension-status: preview @@ -507,4 +507,13 @@ Typically, in the previous code, the session is closed when the stream completes == Configuration Reference +NOTE: Each of the neo4j and bolt URI schemes permit variants that contain extra encryption and trust information. + The +s variants enable encryption with a full certificate check, and the +ssc variants enable encryption, + but with no certificate check. This latter variant is designed specifically for use with self-signed certificates. + The variants are basically shortcuts over explicit configuration. If you use one of them, Quarkus won't pass + `quarkus.neo4j.encrypted` and related to the driver creation as the driver prohibits this. + + + The only check applied when Quarkus detects a secure url (either of `+s` or `+ssc`) is to ensure availability of + SSL in native image and will throw `ConfigurationException` if it isn't available. + include::{generated-dir}/config/quarkus-neo4j.adoc[opts=optional, leveloffset=+1] diff --git a/extensions/neo4j/runtime/pom.xml b/extensions/neo4j/runtime/pom.xml index 25af1e951ea79..13fbf1e60fcdc 100644 --- a/extensions/neo4j/runtime/pom.xml +++ b/extensions/neo4j/runtime/pom.xml @@ -37,6 +37,11 @@ quarkus-junit5-internal test + + org.assertj + assertj-core + test + diff --git a/extensions/neo4j/runtime/src/main/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorder.java b/extensions/neo4j/runtime/src/main/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorder.java index 9bbe61e44d3b1..b266a1fde0b7f 100644 --- a/extensions/neo4j/runtime/src/main/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorder.java +++ b/extensions/neo4j/runtime/src/main/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorder.java @@ -2,7 +2,9 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; +import java.net.URI; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.logging.Level; @@ -15,11 +17,13 @@ import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Logging; +import org.neo4j.driver.internal.Scheme; import io.quarkus.arc.Arc; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.runtime.metrics.MetricsFactory; import io.quarkus.runtime.ssl.SslContextConfiguration; @@ -108,11 +112,30 @@ private static Config.ConfigBuilder createBaseConfig() { return configBuilder; } - private static void configureSsl(Config.ConfigBuilder configBuilder, - Neo4jConfiguration configuration) { + private static void configureSsl(Config.ConfigBuilder configBuilder, Neo4jConfiguration configuration) { + + var uri = URI.create(configuration.uri); + var scheme = uri.getScheme(); + + boolean isSecurityScheme = Scheme.isSecurityScheme(scheme); + boolean isNativeWithoutSslSupport = ImageInfo.inImageRuntimeCode() && !SslContextConfiguration.isSslNativeEnabled(); + + // If the URL indicates a secure / security scheme + // (either neo4j+s (encrypted with full cert checks) or neo4j+ssc (encrypted, but allows self signed certs) + // we cannot configure security settings again, so we check only if Quarkus can provide the necessary runtime + if (isSecurityScheme) { + if (isNativeWithoutSslSupport) { + throw new ConfigurationException("You cannot use " + scheme + + " because SSL support is not available in your current native image setup.", + Set.of("quarkus.neo4j.uri")); + } + + // Nothing to configure + return; + } // Disable encryption regardless of user configuration when ssl is not natively enabled. - if (ImageInfo.inImageRuntimeCode() && !SslContextConfiguration.isSslNativeEnabled()) { + if (isNativeWithoutSslSupport) { log.warn( "Native SSL is disabled, communication between this client and the Neo4j server cannot be encrypted."); configBuilder.withoutEncryption(); diff --git a/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/EmptyShutdownContext.java b/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/EmptyShutdownContext.java new file mode 100644 index 0000000000000..0262860438b39 --- /dev/null +++ b/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/EmptyShutdownContext.java @@ -0,0 +1,16 @@ +package io.quarkus.neo4j.runtime; + +import io.quarkus.runtime.ShutdownContext; + +/** + * Only for tests, avoiding mocks. + */ +final class EmptyShutdownContext implements ShutdownContext { + @Override + public void addShutdownTask(Runnable runnable) { + } + + @Override + public void addLastShutdownTask(Runnable runnable) { + } +} diff --git a/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jConfigurationTest.java b/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jConfigurationTest.java index 80d8e2c8ff674..5bab09c01761e 100644 --- a/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jConfigurationTest.java +++ b/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jConfigurationTest.java @@ -58,7 +58,7 @@ void customCaShouldRequireCertFile() { TrustSettings trustSettings = new TrustSettings(); trustSettings.strategy = TrustSettings.Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES; - String msg = assertThrows(RuntimeException.class, () -> trustSettings.toInternalRepresentation()) + String msg = assertThrows(RuntimeException.class, trustSettings::toInternalRepresentation) .getMessage(); assertEquals("Configured trust strategy requires a certificate file.", msg); } @@ -70,7 +70,7 @@ void customCaShouldRequireExistingCertFile() { trustSettings.strategy = TrustSettings.Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES; trustSettings.certFile = Optional.of(Paths.get("na")); - String msg = assertThrows(RuntimeException.class, () -> trustSettings.toInternalRepresentation()) + String msg = assertThrows(RuntimeException.class, trustSettings::toInternalRepresentation) .getMessage(); assertEquals("Configured trust strategy requires a certificate file.", msg); } diff --git a/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorderTest.java b/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorderTest.java new file mode 100644 index 0000000000000..40b6b70fa126a --- /dev/null +++ b/extensions/neo4j/runtime/src/test/java/io/quarkus/neo4j/runtime/Neo4jDriverRecorderTest.java @@ -0,0 +1,119 @@ +package io.quarkus.neo4j.runtime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.time.Duration; +import java.util.logging.LogRecord; + +import org.graalvm.nativeimage.ImageInfo; +import org.junit.jupiter.api.Test; + +import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.ssl.SslContextConfiguration; +import io.quarkus.test.InMemoryLogHandler; + +class Neo4jDriverRecorderTest { + + Neo4jConfiguration emptyConfig() { + var configShell = new Neo4jConfiguration(); + + configShell.authentication = new Neo4jConfiguration.Authentication(); + configShell.authentication.username = "Thomas"; + configShell.authentication.password = "Anderson"; + + configShell.pool = new Neo4jConfiguration.Pool(); + configShell.pool.maxConnectionPoolSize = 21; + configShell.pool.connectionAcquisitionTimeout = Duration.ZERO; + configShell.pool.idleTimeBeforeConnectionTest = Duration.ZERO; + configShell.pool.maxConnectionLifetime = Duration.ZERO; + + return configShell; + } + + @Test + void shouldThrowWhenSSLIsConfiguredViaProtocolButNotAvailable() { + + runTestPretendingToBeInNativeImageWithoutSSL(() -> { + var recorder = new Neo4jDriverRecorder(); + var config = emptyConfig(); + config.uri = "neo4j+s://somewhere"; + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(() -> recorder.initializeDriver(config, new EmptyShutdownContext())) + .withMessage( + "You cannot use neo4j+s because SSL support is not available in your current native image setup."); + }); + } + + private void runTestPretendingToBeInNativeImageWithoutSSL(Runnable test) { + var propertyImageCodeKey = ImageInfo.PROPERTY_IMAGE_CODE_KEY; + var oldImageCodeValue = System.getProperty(propertyImageCodeKey); + + try { + // This makes the test pretend to run in native image mode without SSL present + System.setProperty(propertyImageCodeKey, ImageInfo.PROPERTY_IMAGE_CODE_VALUE_RUNTIME); + SslContextConfiguration.setSslNativeEnabled(false); + + test.run(); + } finally { + if (oldImageCodeValue == null || oldImageCodeValue.isBlank()) { + System.clearProperty(propertyImageCodeKey); + } else { + System.setProperty(propertyImageCodeKey, oldImageCodeValue); + } + } + } + + @Test + void shouldNotTouchConfigIsSecuritySchemeIsUsed() { + + var recorder = new Neo4jDriverRecorder(); + var config = emptyConfig(); + config.uri = "neo4j+s://somewhere"; + + var driver = recorder.initializeDriver(config, new EmptyShutdownContext()).getValue(); + assertThat(driver.isEncrypted()).isTrue(); + } + + @Test + void shouldEncryptWhenPossible() { + + var recorder = new Neo4jDriverRecorder(); + var config = emptyConfig(); + config.uri = "neo4j://somewhere"; + config.encrypted = true; + config.trustSettings = new Neo4jConfiguration.TrustSettings(); + config.trustSettings.strategy = Neo4jConfiguration.TrustSettings.Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES; + + var driver = recorder.initializeDriver(config, new EmptyShutdownContext()).getValue(); + assertThat(driver.isEncrypted()).isTrue(); + } + + @Test + void shouldWarnWhenEncryptionIsNotPossible() { + var capturingHandler = new InMemoryLogHandler(r -> r.getLoggerName().contains("Neo4jDriverRecorder")); + InitialConfigurator.DELAYED_HANDLER.addHandler(capturingHandler); + try { + runTestPretendingToBeInNativeImageWithoutSSL(() -> { + var recorder = new Neo4jDriverRecorder(); + var config = emptyConfig(); + config.uri = "neo4j://somewhere"; + config.encrypted = true; + config.trustSettings = new Neo4jConfiguration.TrustSettings(); + config.trustSettings.strategy = Neo4jConfiguration.TrustSettings.Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES; + + var driver = recorder.initializeDriver(config, new EmptyShutdownContext()).getValue(); + assertThat(driver.isEncrypted()).isFalse(); + + assertThat(capturingHandler.getRecords()) + .extracting(LogRecord::getMessage) + .anyMatch(m -> m.startsWith( + "Native SSL is disabled, communication between this client and the Neo4j server cannot be encrypted.")); + }); + } finally { + InitialConfigurator.DELAYED_HANDLER.removeHandler(capturingHandler); + } + } +}