From de8fb04814c12d2e73b01612a9c8747c699b3268 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Mar 2023 23:22:45 -0700 Subject: [PATCH] Add ConnectionDetail support to Neo4J auto-configuration Update Neo4J auto-configuration so that `Neo4jConnectionDetails` beans may be optionally used to provide connection details. See gh-34657 Co-Authored-By: Mortitz Halbritter Co-Authored-By: Phillip Webb --- .../neo4j/Neo4jAutoConfiguration.java | 92 +++++++++++-------- .../neo4j/Neo4jConnectionDetails.java | 52 +++++++++++ .../neo4j/Neo4jAutoConfigurationTests.java | 75 +++++++++------ 3 files changed, 152 insertions(+), 67 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java index 298a093f81..39b0d334f4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java @@ -36,12 +36,14 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Authentication; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Pool; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Security; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -49,6 +51,9 @@ import org.springframework.util.StringUtils; * * @author Michael J. Simons * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.4.0 */ @AutoConfiguration @@ -56,50 +61,24 @@ import org.springframework.util.StringUtils; @EnableConfigurationProperties(Neo4jProperties.class) public class Neo4jAutoConfiguration { - private static final URI DEFAULT_SERVER_URI = URI.create("bolt://localhost:7687"); - @Bean @ConditionalOnMissingBean public Driver neo4jDriver(Neo4jProperties properties, Environment environment, - ObjectProvider configBuilderCustomizers) { - AuthToken authToken = mapAuthToken(properties.getAuthentication(), environment); - Config config = mapDriverConfig(properties, configBuilderCustomizers.orderedStream().toList()); - URI serverUri = determineServerUri(properties, environment); - return GraphDatabase.driver(serverUri, authToken, config); + ObjectProvider configBuilderCustomizers, + ObjectProvider connectionDetailsProvider) { + Neo4jConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesNeo4jConnectionDetails(properties)); + AuthToken authToken = connectionDetails.getAuthToken(); + Config config = mapDriverConfig(properties, connectionDetails, + configBuilderCustomizers.orderedStream().toList()); + return GraphDatabase.driver(connectionDetails.getUri(), authToken, config); } - URI determineServerUri(Neo4jProperties properties, Environment environment) { - URI uri = properties.getUri(); - return (uri != null) ? uri : DEFAULT_SERVER_URI; - } - - AuthToken mapAuthToken(Neo4jProperties.Authentication authentication, Environment environment) { - String username = authentication.getUsername(); - String password = authentication.getPassword(); - String kerberosTicket = authentication.getKerberosTicket(); - String realm = authentication.getRealm(); - - boolean hasUsername = StringUtils.hasText(username); - boolean hasPassword = StringUtils.hasText(password); - boolean hasKerberosTicket = StringUtils.hasText(kerberosTicket); - - if (hasUsername && hasKerberosTicket) { - throw new IllegalStateException(String - .format("Cannot specify both username ('%s') and kerberos ticket ('%s')", username, kerberosTicket)); - } - if (hasUsername && hasPassword) { - return AuthTokens.basic(username, password, realm); - } - if (hasKerberosTicket) { - return AuthTokens.kerberos(kerberosTicket); - } - return AuthTokens.none(); - } - - Config mapDriverConfig(Neo4jProperties properties, List customizers) { + Config mapDriverConfig(Neo4jProperties properties, Neo4jConnectionDetails connectionDetails, + List customizers) { Config.ConfigBuilder builder = Config.builder(); configurePoolSettings(builder, properties.getPool()); - URI uri = properties.getUri(); + URI uri = connectionDetails.getUri(); String scheme = (uri != null) ? uri.getScheme() : "bolt"; configureDriverSettings(builder, properties, isSimpleScheme(scheme)); builder.withLogging(new Neo4jSpringJclLogging()); @@ -191,4 +170,43 @@ public class Neo4jAutoConfiguration { } } + /** + * Adapts {@link Neo4jProperties} to {@link Neo4jConnectionDetails}. + */ + static class PropertiesNeo4jConnectionDetails implements Neo4jConnectionDetails { + + private final Neo4jProperties properties; + + PropertiesNeo4jConnectionDetails(Neo4jProperties properties) { + this.properties = properties; + } + + @Override + public URI getUri() { + URI uri = this.properties.getUri(); + return (uri != null) ? uri : Neo4jConnectionDetails.super.getUri(); + } + + @Override + public AuthToken getAuthToken() { + Authentication authentication = this.properties.getAuthentication(); + String username = authentication.getUsername(); + String kerberosTicket = authentication.getKerberosTicket(); + boolean hasUsername = StringUtils.hasText(username); + boolean hasKerberosTicket = StringUtils.hasText(kerberosTicket); + Assert.state(!(hasUsername && hasKerberosTicket), + () -> "Cannot specify both username ('%s') and kerberos ticket ('%s')".formatted(username, + kerberosTicket)); + String password = authentication.getPassword(); + if (hasUsername && StringUtils.hasText(password)) { + return AuthTokens.basic(username, password, authentication.getRealm()); + } + if (hasKerberosTicket) { + return AuthTokens.kerberos(kerberosTicket); + } + return AuthTokens.none(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java new file mode 100644 index 0000000000..17a950ebd8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.net.URI; + +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Neo4j service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface Neo4jConnectionDetails extends ConnectionDetails { + + /** + * Returns the URI of the Neo4j server. Defaults to {@code bolt://localhost:7687"}. + * @return the Neo4j server URI + */ + default URI getUri() { + return URI.create("bolt://localhost:7687"); + } + + /** + * Returns the token to use for authentication. Defaults to {@link AuthTokens#none()}. + * @return the auth token + */ + default AuthToken getAuthToken() { + return AuthTokens.none(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java index 81cb723237..d9b73ce74b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java @@ -26,20 +26,19 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Config.ConfigBuilder; import org.neo4j.driver.Driver; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration.PropertiesNeo4jConnectionDetails; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Authentication; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Security.TrustStrategy; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.core.env.Environment; -import org.springframework.mock.env.MockEnvironment; +import org.springframework.context.annotation.Bean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -50,6 +49,9 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * * @author Michael J. Simons * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class Neo4jAutoConfigurationTests { @@ -103,6 +105,22 @@ class Neo4jAutoConfigurationTests { .hasMessageContaining("'%s' is not a supported scheme.", invalidScheme)); } + @Bean + void usesCustomConnectionDetails() { + this.contextRunner.withBean(Neo4jConnectionDetails.class, () -> new Neo4jConnectionDetails() { + + @Override + public URI getUri() { + return URI.create("bolt+ssc://localhost:12345"); + } + + }).run((context) -> { + assertThat(context).hasSingleBean(Driver.class); + Driver driver = context.getBean(Driver.class); + assertThat(driver.isEncrypted()).isTrue(); + }); + } + @Test void connectionTimeout() { Neo4jProperties properties = new Neo4jProperties(); @@ -118,8 +136,8 @@ class Neo4jAutoConfigurationTests { } @Test - void determineServerUriShouldDefaultToLocalhost() { - assertThat(determineServerUri(new Neo4jProperties(), new MockEnvironment())) + void uriShouldDefaultToLocalhost() { + assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties()).getUri()) .isEqualTo(URI.create("bolt://localhost:7687")); } @@ -128,44 +146,52 @@ class Neo4jAutoConfigurationTests { URI customUri = URI.create("bolt://localhost:4242"); Neo4jProperties properties = new Neo4jProperties(); properties.setUri(customUri); - assertThat(determineServerUri(properties, new MockEnvironment())).isEqualTo(customUri); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getUri()).isEqualTo(customUri); } @Test void authenticationShouldDefaultToNone() { - assertThat(mapAuthToken(new Authentication())).isEqualTo(AuthTokens.none()); + assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties()).getAuthToken()) + .isEqualTo(AuthTokens.none()); } @Test void authenticationWithUsernameShouldEnableBasicAuth() { - Authentication authentication = new Authentication(); - authentication.setUsername("Farin"); - authentication.setPassword("Urlaub"); - assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.basic("Farin", "Urlaub")); + Neo4jProperties properties = new Neo4jProperties(); + properties.getAuthentication().setUsername("Farin"); + properties.getAuthentication().setPassword("Urlaub"); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + .isEqualTo(AuthTokens.basic("Farin", "Urlaub")); } @Test void authenticationWithUsernameAndRealmShouldEnableBasicAuth() { - Authentication authentication = new Authentication(); + Neo4jProperties properties = new Neo4jProperties(); + Authentication authentication = properties.getAuthentication(); authentication.setUsername("Farin"); authentication.setPassword("Urlaub"); authentication.setRealm("Test Realm"); - assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Test Realm")); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + .isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Test Realm")); } @Test void authenticationWithKerberosTicketShouldEnableKerberos() { - Authentication authentication = new Authentication(); + Neo4jProperties properties = new Neo4jProperties(); + Authentication authentication = properties.getAuthentication(); authentication.setKerberosTicket("AABBCCDDEE"); - assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.kerberos("AABBCCDDEE")); + assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + .isEqualTo(AuthTokens.kerberos("AABBCCDDEE")); } @Test void authenticationWithBothUsernameAndKerberosShouldNotBeAllowed() { - Authentication authentication = new Authentication(); + Neo4jProperties properties = new Neo4jProperties(); + Authentication authentication = properties.getAuthentication(); authentication.setUsername("Farin"); authentication.setKerberosTicket("AABBCCDDEE"); - assertThatIllegalStateException().isThrownBy(() -> mapAuthToken(authentication)) + assertThatIllegalStateException() + .isThrownBy(() -> new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) .withMessage("Cannot specify both username ('Farin') and kerberos ticket ('AABBCCDDEE')"); } @@ -279,20 +305,9 @@ class Neo4jAutoConfigurationTests { assertThat(mapDriverConfig(new Neo4jProperties()).logging()).isInstanceOf(Neo4jSpringJclLogging.class); } - private URI determineServerUri(Neo4jProperties properties, Environment environment) { - return new Neo4jAutoConfiguration().determineServerUri(properties, environment); - } - - private AuthToken mapAuthToken(Authentication authentication, Environment environment) { - return new Neo4jAutoConfiguration().mapAuthToken(authentication, environment); - } - - private AuthToken mapAuthToken(Authentication authentication) { - return mapAuthToken(authentication, new MockEnvironment()); - } - private Config mapDriverConfig(Neo4jProperties properties, ConfigBuilderCustomizer... customizers) { - return new Neo4jAutoConfiguration().mapDriverConfig(properties, Arrays.asList(customizers)); + return new Neo4jAutoConfiguration().mapDriverConfig(properties, + new PropertiesNeo4jConnectionDetails(properties), Arrays.asList(customizers)); } }