Skip to content

Commit

Permalink
Merge pull request quarkusio#20564 from michael-simons/neo4j-fix-neo4…
Browse files Browse the repository at this point in the history
…j-advanced-protocols

Improve handling of advanced Neo4j url schemes
  • Loading branch information
geoand authored Oct 11, 2021
2 parents d2d1ca2 + 60020fb commit 1f33d46
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 6 deletions.
11 changes: 10 additions & 1 deletion docs/src/main/asciidoc/neo4j.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
5 changes: 5 additions & 0 deletions extensions/neo4j/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit 1f33d46

Please sign in to comment.