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 <mkammerer@vmware.com>
Co-Authored-By: Phillip Webb <pwebb@vmware.com>
pull/34759/head
Andy Wilkinson 2 years ago
parent 2ef33dc81f
commit de8fb04814

@ -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<ConfigBuilderCustomizer> 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);
}
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();
ObjectProvider<ConfigBuilderCustomizer> configBuilderCustomizers,
ObjectProvider<Neo4jConnectionDetails> 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);
}
Config mapDriverConfig(Neo4jProperties properties, List<ConfigBuilderCustomizer> customizers) {
Config mapDriverConfig(Neo4jProperties properties, Neo4jConnectionDetails connectionDetails,
List<ConfigBuilderCustomizer> 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();
}
}
}

@ -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();
}
}

@ -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));
}
}

Loading…
Cancel
Save