Skip to content

Commit

Permalink
Improve handling of advanced Neo4j url schemes
Browse files Browse the repository at this point in the history
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 quarkusio#19412.

Co-authored-by: Guillaume Smet <[email protected]>
  • Loading branch information
michael-simons and gsmet committed Oct 8, 2021
1 parent 41a7f69 commit 60020fb
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 60020fb

Please sign in to comment.