Fix embedded LDAP SSL and reuse client auto-configuration

Replace the hand-rolled LdapContextSource of the embedded server
with an LdapConnectionDetails implementation, so that
LdapAutoConfiguration creates the context source for both the external
and the embedded case. Previously, the embedded context source derived
the URL scheme from spring.ldap.ssl and never applied an SSL bundle to
the JNDI environment, leaving an embedded LDAPS server unreachable. It
also ignored spring.ldap.anonymous-read-only, spring.ldap.referral,
spring.ldap.base-environment and any DirContextAuthenticationStrategy
bean, all of which now apply.

The embedded server provides everything that describes a connection to
it, so spring.ldap.urls, spring.ldap.username, spring.ldap.password and
spring.ldap.ssl are now ignored while it is used. A spring.ldap
configuration meant for a production server therefore no longer has to
be unset for a test to run against the embedded server. This is a
behavior change: spring.ldap.urls used to take precedence and silently
pointed the client away from the embedded server.

As spring.ldap.base-environment now applies to the embedded case, a
socket factory set there would be replaced by the one of the SSL
bundle. Startup fails instead of using either silently.

LdapProperties.determineUrls has been removed. Its local.ldap.port
handling only ever served the embedded case, which the embedded
connection details now cover, and the default URL derivation has moved
to PropertiesLdapConnectionDetails, its only caller.

Closes gh-51465
This commit is contained in:
Moritz Halbritter
2026-08-27 09:32:13 +02:00
parent bf1b583596
commit 99274e3143
12 changed files with 540 additions and 152 deletions
@@ -41,7 +41,6 @@ import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.env.Environment;
import org.springframework.ldap.convert.ConverterUtils;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.LdapOperations;
@@ -70,9 +69,9 @@ public final class LdapAutoConfiguration {
@Bean
@ConditionalOnMissingBean(LdapConnectionDetails.class)
PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties, Environment environment,
PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesLdapConnectionDetails(properties, environment, sslBundles.getIfAvailable());
return new PropertiesLdapConnectionDetails(properties, sslBundles.getIfAvailable());
}
@Bean
@@ -100,9 +99,10 @@ public final class LdapAutoConfiguration {
* through a {@link DirContextAuthenticationStrategy} as the strategy is not consulted
* when read-only operations use an anonymous environment.
* <p>
* A socket factory in the base environment conflicts with the bundle and is rejected,
* but only when the bundle also came from the properties. A bundle from another
* {@link LdapConnectionDetails} bean takes precedence over the properties instead.
* A socket factory in the base environment conflicts with the bundle and is rejected
* when the bundle came from properties, whether the client's or the embedded
* server's. A bundle from a user-supplied {@link LdapConnectionDetails} bean takes
* precedence over the base environment instead.
* @param connectionDetails the connection details
* @param properties the LDAP properties
* @return the base environment properties
@@ -22,11 +22,8 @@ import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.env.Environment;
import org.springframework.ldap.ReferralException;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -38,10 +35,6 @@ import org.springframework.util.StringUtils;
@ConfigurationProperties("spring.ldap")
public class LdapProperties {
private static final int DEFAULT_PORT = 389;
private static final int DEFAULT_SSL_PORT = 636;
/**
* LDAP URLs of the server.
*/
@@ -143,25 +136,6 @@ public class LdapProperties {
return this.ssl;
}
public String[] determineUrls(Environment environment) {
if (ObjectUtils.isEmpty(this.urls)) {
boolean useSsl = this.ssl.isEnabled();
String protocol = useSsl ? "ldaps" : "ldap";
int defaultPort = useSsl ? DEFAULT_SSL_PORT : DEFAULT_PORT;
return new String[] { protocol + "://localhost:" + determinePort(environment, defaultPort) };
}
return this.urls;
}
private int determinePort(Environment environment, int defaultPort) {
Assert.notNull(environment, "'environment' must not be null");
String localPort = environment.getProperty("local.ldap.port");
if (localPort != null) {
return Integer.parseInt(localPort);
}
return defaultPort;
}
/**
* {@link LdapTemplate settings}.
*/
@@ -20,27 +20,28 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Adapts {@link LdapProperties} to {@link LdapConnectionDetails}.
*
* @author Philipp Kessler
* @author Moritz Halbritter
*/
class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
private final LdapProperties properties;
private static final int DEFAULT_PORT = 389;
private final Environment environment;
private static final int DEFAULT_SSL_PORT = 636;
private final LdapProperties properties;
private final @Nullable SslBundles sslBundles;
PropertiesLdapConnectionDetails(LdapProperties properties, Environment environment,
@Nullable SslBundles sslBundles) {
PropertiesLdapConnectionDetails(LdapProperties properties, @Nullable SslBundles sslBundles) {
this.properties = properties;
this.environment = environment;
this.sslBundles = sslBundles;
registerSslBundleUpdateHandler();
}
@@ -60,7 +61,14 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
@Override
public String[] getUrls() {
return this.properties.determineUrls(this.environment);
String[] urls = this.properties.getUrls();
if (!ObjectUtils.isEmpty(urls)) {
return urls;
}
boolean useSsl = this.properties.getSsl().isEnabled();
String protocol = useSsl ? "ldaps" : "ldap";
int port = useSsl ? DEFAULT_SSL_PORT : DEFAULT_PORT;
return new String[] { protocol + "://localhost:" + port };
}
@Override
@@ -50,6 +50,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration;
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
import org.springframework.boot.ldap.autoconfigure.LdapProperties;
import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints;
import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapProperties.Ssl;
@@ -70,7 +71,6 @@ import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -80,6 +80,7 @@ import org.springframework.util.StringUtils;
* @author Eddú Meléndez
* @author Mathieu Ouellet
* @author Raja Kolli
* @author Moritz Halbritter
* @since 4.0.0
*/
@AutoConfiguration(before = LdapAutoConfiguration.class)
@@ -242,19 +243,11 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
@Bean
@DependsOn("directoryServer")
@ConditionalOnMissingBean
LdapContextSource ldapContextSource(Environment environment, LdapProperties properties,
EmbeddedLdapProperties embeddedProperties) {
LdapContextSource source = new LdapContextSource();
source.setBase(properties.getBase());
String username = embeddedProperties.getCredential().getUsername();
String password = embeddedProperties.getCredential().getPassword();
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
source.setUserDn(username);
source.setPassword(password);
}
source.setUrls(properties.determineUrls(environment));
return source;
@ConditionalOnMissingBean(LdapConnectionDetails.class)
EmbeddedLdapConnectionDetails embeddedLdapConnectionDetails(Environment environment, LdapProperties properties,
EmbeddedLdapProperties embeddedProperties, ObjectProvider<SslBundles> sslBundles) {
return new EmbeddedLdapConnectionDetails(environment, properties, embeddedProperties,
sslBundles.getIfAvailable());
}
}
@@ -0,0 +1,114 @@
/*
* Copyright 2012-present 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.ldap.autoconfigure.embedded;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
import org.springframework.boot.ldap.autoconfigure.LdapProperties;
import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapProperties.Ssl;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link LdapConnectionDetails} for the embedded LDAP server. Everything that describes
* the connection comes from {@code spring.ldap.embedded}, as only the server can decide
* what a connection to it looks like: the URL from the port it is listening on and its
* SSL configuration, the credentials from {@code spring.ldap.embedded.credential}. The
* equivalent client properties, {@code spring.ldap.urls}, {@code spring.ldap.username},
* {@code spring.ldap.password} and {@code spring.ldap.ssl}, are ignored, so a
* configuration meant for production does not have to be unset to run against the
* embedded server.
*
* @author Moritz Halbritter
*/
class EmbeddedLdapConnectionDetails implements LdapConnectionDetails {
private static final String SOCKET_FACTORY_ENV_KEY = "java.naming.ldap.factory.socket";
private final Environment environment;
private final LdapProperties properties;
private final EmbeddedLdapProperties embeddedProperties;
private final @Nullable SslBundles sslBundles;
EmbeddedLdapConnectionDetails(Environment environment, LdapProperties properties,
EmbeddedLdapProperties embeddedProperties, @Nullable SslBundles sslBundles) {
this.environment = environment;
this.properties = properties;
this.embeddedProperties = embeddedProperties;
this.sslBundles = sslBundles;
}
@Override
public String[] getUrls() {
String protocol = this.embeddedProperties.getSsl().isEnabled() ? "ldaps" : "ldap";
return new String[] { protocol + "://localhost:" + this.environment.getRequiredProperty("local.ldap.port") };
}
@Override
public @Nullable String getBase() {
return this.properties.getBase();
}
@Override
public @Nullable String getUsername() {
return hasCredential() ? this.embeddedProperties.getCredential().getUsername() : null;
}
@Override
public @Nullable String getPassword() {
return hasCredential() ? this.embeddedProperties.getCredential().getPassword() : null;
}
private boolean hasCredential() {
return StringUtils.hasText(this.embeddedProperties.getCredential().getUsername())
&& StringUtils.hasText(this.embeddedProperties.getCredential().getPassword());
}
@Override
public @Nullable SslBundle getSslBundle() {
Ssl serverSsl = this.embeddedProperties.getSsl();
if (!serverSsl.isEnabled()) {
return null;
}
String bundle = serverSsl.getBundle();
if (bundle == null) {
return null;
}
assertNoSocketFactoryInBaseEnvironment();
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return this.sslBundles.getBundle(bundle);
}
/**
* Rejects a socket factory in the base environment that the SSL bundle's socket
* factory would replace, as both come from properties and so contradict each other.
* Called only when a bundle is actually used, as nothing is replaced otherwise.
*/
private void assertNoSocketFactoryInBaseEnvironment() {
Assert.state(!this.properties.getBaseEnvironment().containsKey(SOCKET_FACTORY_ENV_KEY),
() -> "SSL bundle has been configured but '" + SOCKET_FACTORY_ENV_KEY
+ "' has also been set in the base environment. Use either an SSL bundle or your own socket factory, not both");
}
}
@@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.ldap.autoconfigure.LdapProperties.Template;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,40 +42,4 @@ class LdapPropertiesTests {
templateProperties.isIgnoreSizeLimitExceededException());
}
@Test
void determineUrlsShouldDefaultToPlainLdapWhenSslNotEnabled() {
LdapProperties properties = new LdapProperties();
assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldap://localhost:389");
}
@Test
void determineUrlsShouldDefaultToLdapsWhenSslEnabled() {
LdapProperties properties = new LdapProperties();
properties.getSsl().setEnabled(true);
assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldaps://localhost:636");
}
@Test
void determineUrlsShouldDefaultToLdapsWhenSslBundleConfigured() {
LdapProperties properties = new LdapProperties();
properties.getSsl().setBundle("example");
assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldaps://localhost:636");
}
@Test
void determineUrlsShouldPreferLocalPortOverDefaultSslPort() {
LdapProperties properties = new LdapProperties();
properties.getSsl().setEnabled(true);
MockEnvironment environment = new MockEnvironment().withProperty("local.ldap.port", "1234");
assertThat(properties.determineUrls(environment)).containsExactly("ldaps://localhost:1234");
}
@Test
void determineUrlsShouldUseConfiguredUrlsRegardlessOfSsl() {
LdapProperties properties = new LdapProperties();
properties.setUrls(new String[] { "ldap://localhost:1234" });
properties.getSsl().setEnabled(true);
assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldap://localhost:1234");
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2012-present 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.ldap.autoconfigure;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertiesLdapConnectionDetails}.
*
* @author Moritz Halbritter
*/
class PropertiesLdapConnectionDetailsTests {
private final LdapProperties properties = new LdapProperties();
@Test
void shouldUseDefaultLdapPortWhenSslIsNotEnabled() {
assertThat(createConnectionDetails().getUrls()).containsExactly("ldap://localhost:389");
}
@Test
void shouldUseDefaultLdapsPortWhenSslIsEnabled() {
this.properties.getSsl().setEnabled(true);
assertThat(createConnectionDetails().getUrls()).containsExactly("ldaps://localhost:636");
}
@Test
void shouldUseDefaultLdapsPortWhenSslBundleIsConfigured() {
this.properties.getSsl().setBundle("example");
assertThat(createConnectionDetails().getUrls()).containsExactly("ldaps://localhost:636");
}
@Test
void shouldUseConfiguredUrlsRegardlessOfSsl() {
this.properties.setUrls(new String[] { "ldap://localhost:1234" });
this.properties.getSsl().setEnabled(true);
assertThat(createConnectionDetails().getUrls()).containsExactly("ldap://localhost:1234");
}
private PropertiesLdapConnectionDetails createConnectionDetails() {
return new PropertiesLdapConnectionDetails(this.properties, null);
}
}
@@ -30,13 +30,17 @@ import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.schema.Schema;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration;
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.util.TestPropertyValues;
@@ -58,7 +62,8 @@ import static org.assertj.core.api.Assertions.assertThat;
class EmbeddedLdapAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, SslAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, LdapAutoConfiguration.class,
SslAutoConfiguration.class));
@Test
void testSetDefaultPort() {
@@ -124,13 +129,11 @@ class EmbeddedLdapAutoConfigurationTests {
@Test
@WithSchemaLdifResource
void testQueryEmbeddedLdap() {
this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org")
.withConfiguration(AutoConfigurations.of(LdapAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasSingleBean(LdapTemplate.class);
LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class);
assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4);
});
this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org").run((context) -> {
assertThat(context).hasSingleBean(LdapTemplate.class);
LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class);
assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4);
});
}
@Test
@@ -355,64 +358,44 @@ class EmbeddedLdapAutoConfigurationTests {
}
@Test
void whenSslBundleIsConfiguredLdapsListenerIsConfigured() {
List<String> propertyValues = new ArrayList<>();
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret");
propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks");
propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks");
propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2");
propertyValues.add("spring.ldap.embedded.port:0");
propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org");
propertyValues.add("spring.ldap.embedded.ssl.bundle:test");
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1);
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
assertThat(config.getListenerName()).isEqualTo("LDAPS");
assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull();
});
void shouldConfigureLdapsListenerWhenSslBundleIsConfigured() {
this.contextRunner.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test"))
.run((context) -> {
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1);
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
assertThat(config.getListenerName()).isEqualTo("LDAPS");
assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull();
});
}
@Test
void whenSslBundleIsConfiguredButSslIsDisabledLdapListenerIsConfigured() {
List<String> propertyValues = new ArrayList<>();
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret");
propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks");
propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks");
propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2");
propertyValues.add("spring.ldap.embedded.port:0");
propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org");
propertyValues.add("spring.ldap.embedded.ssl.enabled:false");
propertyValues.add("spring.ldap.embedded.ssl.bundle:test");
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1);
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
assertThat(config.getListenerName()).isEqualTo("LDAP");
});
void shouldConfigureLdapListenerWhenSslBundleIsConfiguredButSslIsDisabled() {
this.contextRunner
.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.enabled:false",
"spring.ldap.embedded.ssl.bundle:test"))
.run((context) -> {
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1);
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
assertThat(config.getListenerName()).isEqualTo("LDAP");
});
}
@Test
void whenInvalidSslBundleIsConfiguredThenStartFails() {
List<String> propertyValues = new ArrayList<>();
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret");
propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks");
propertyValues.add("spring.ldap.embedded.port:0");
propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org");
propertyValues.add("spring.ldap.embedded.ssl.enabled:true");
propertyValues.add("spring.ldap.embedded.ssl.bundle:foo");
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().hasMessageContaining("foo");
assertThat(context).getFailure().hasMessageContaining("cannot be found");
});
void shouldFailWhenInvalidSslBundleIsConfigured() {
this.contextRunner
.withPropertyValues(
sslBundleProperties("spring.ldap.embedded.ssl.enabled:true", "spring.ldap.embedded.ssl.bundle:foo"))
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().hasMessageContaining("foo");
assertThat(context).getFailure().hasMessageContaining("cannot be found");
});
}
@Test
void whenSslIsEnabledWithoutAnSslBundleThenStartFails() {
void shouldFailWhenSslIsEnabledWithoutAnSslBundle() {
this.contextRunner
.withPropertyValues("spring.ldap.embedded.port:0", "spring.ldap.embedded.base-dn:dc=spring,dc=org",
"spring.ldap.embedded.ssl.enabled:true")
@@ -422,6 +405,92 @@ class EmbeddedLdapAutoConfigurationTests {
});
}
@Test
@WithSchemaLdifResource
void shouldConnectOverLdapsWhenSslBundleIsConfigured() {
this.contextRunner.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test"))
.run((context) -> {
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource.getUrls()).allMatch((url) -> url.startsWith("ldaps://"));
LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class);
assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4);
});
}
@Test
@WithSchemaLdifResource
void shouldIgnoreClientSslPropertiesMeantForAnotherServer() {
this.contextRunner
.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test",
"spring.ldap.urls:ldaps://ldap.example.com:636", "spring.ldap.ssl.bundle:does-not-exist"))
.run((context) -> {
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource.getUrls()).allMatch((url) -> url.startsWith("ldaps://localhost:"));
LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class);
assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4);
});
}
@Test
void shouldFailWhenSslBundleIsConfiguredAndSocketFactoryIsSetInBaseEnvironment() {
this.contextRunner
.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test",
"spring.ldap.baseEnvironment.java.naming.ldap.factory.socket=com.example.MySocketFactory"))
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure()
.hasMessageContaining("Use either an SSL bundle or your own socket factory, not both");
});
}
@Test
void shouldAllowSocketFactoryInBaseEnvironmentWhenContextSourceIsUserDefined() {
this.contextRunner
.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test",
"spring.ldap.baseEnvironment.java.naming.ldap.factory.socket=com.example.MySocketFactory"))
.withBean("ldapContextSource", LdapContextSource.class, () -> {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrls(new String[] { "ldaps://localhost:636" });
return contextSource;
})
.run((context) -> assertThat(context).hasNotFailed());
}
@Test
@WithSchemaLdifResource
void shouldRegisterHintsForSchemaLdif() {
RuntimeHints runtimeHints = new RuntimeHints();
new EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints().registerHints(runtimeHints,
Thread.currentThread().getContextClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("schema.ldif")).accepts(runtimeHints);
}
@Test
void shouldApplyClientPropertiesThatTheServerDoesNotDecide() {
this.contextRunner
.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org", "spring.ldap.referral:ignore",
"spring.ldap.anonymous-read-only:true",
"spring.ldap.baseEnvironment.java.naming.security.authentication:DIGEST-MD5")
.run((context) -> {
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource).hasFieldOrPropertyWithValue("referral", "ignore");
assertThat(contextSource.isAnonymousReadOnly()).isTrue();
assertThat(contextSource).extracting("anonymousEnv", InstanceOfAssertFactories.MAP)
.containsEntry("java.naming.security.authentication", "DIGEST-MD5");
});
}
@Test
void shouldBackOffWhenLdapConnectionDetailsBeanIsDefined() {
this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org")
.withUserConfiguration(LdapConnectionDetailsConfiguration.class)
.run((context) -> {
assertThat(context).doesNotHaveBean(EmbeddedLdapConnectionDetails.class);
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource.getUrls()).containsExactly("ldap://ldap.example.com:389");
});
}
@Test
void sslIsNotEnabledWhenBundleIsEmpty() {
EmbeddedLdapProperties properties = new EmbeddedLdapProperties();
@@ -429,6 +498,32 @@ class EmbeddedLdapAutoConfigurationTests {
assertThat(properties.getSsl().isEnabled()).isFalse();
}
private String[] sslBundleProperties(String... additionalProperties) {
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
List<String> propertyValues = new ArrayList<>();
propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret");
propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "localhost.jks");
propertyValues.add("spring.ssl.bundle.jks.test.truststore.password=secret");
propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "localhost.jks");
propertyValues.add("spring.ssl.bundle.jks.test.key.alias=spring-boot");
propertyValues.add("spring.ssl.bundle.jks.test.key.password=password");
propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2");
propertyValues.add("spring.ldap.embedded.port:0");
propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org");
propertyValues.addAll(List.of(additionalProperties));
return propertyValues.toArray(String[]::new);
}
@Configuration(proxyBeanMethods = false)
static class LdapConnectionDetailsConfiguration {
@Bean
LdapConnectionDetails ldapConnectionDetails() {
return () -> new String[] { "ldap://ldap.example.com:389" };
}
}
@Configuration(proxyBeanMethods = false)
static class LdapClientConfiguration {
@@ -0,0 +1,164 @@
/*
* Copyright 2012-present 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.ldap.autoconfigure.embedded;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ldap.autoconfigure.LdapProperties;
import org.springframework.boot.ldap.autoconfigure.LdapSslSocketFactory;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link EmbeddedLdapConnectionDetails}.
*
* @author Moritz Halbritter
*/
class EmbeddedLdapConnectionDetailsTests {
private final MockEnvironment environment = new MockEnvironment().withProperty("local.ldap.port", "12345");
private final LdapProperties properties = new LdapProperties();
private final EmbeddedLdapProperties embeddedProperties = new EmbeddedLdapProperties();
@AfterEach
void clearSslBundle() {
ReflectionTestUtils.setField(LdapSslSocketFactory.class, "sslBundle", null);
}
@Test
void shouldUseLdapUrlWhenEmbeddedSslIsNotEnabled() {
assertThat(createConnectionDetails(null).getUrls()).containsExactly("ldap://localhost:12345");
}
@Test
void shouldUseLdapsUrlWhenEmbeddedSslIsEnabled() {
this.embeddedProperties.getSsl().setBundle("server");
assertThat(createConnectionDetails(null).getUrls()).containsExactly("ldaps://localhost:12345");
}
@Test
void shouldIgnoreConfiguredUrls() {
this.properties.setUrls(new String[] { "ldaps://ldap.example.com" });
assertThat(createConnectionDetails(null).getUrls()).containsExactly("ldap://localhost:12345");
}
@Test
void shouldUseEmbeddedCredentials() {
this.embeddedProperties.getCredential().setUsername("uid=root");
this.embeddedProperties.getCredential().setPassword("boot");
this.properties.setUsername("uid=other");
this.properties.setPassword("other");
EmbeddedLdapConnectionDetails connectionDetails = createConnectionDetails(null);
assertThat(connectionDetails.getUsername()).isEqualTo("uid=root");
assertThat(connectionDetails.getPassword()).isEqualTo("boot");
}
@Test
void shouldUseServerSslBundleWhenClientBundleIsNotSet() {
SslBundle serverBundle = mock(SslBundle.class);
SslBundles sslBundles = sslBundles("server", serverBundle);
this.embeddedProperties.getSsl().setBundle("server");
assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle);
}
@Test
void shouldPreferServerSslBundleOverClientSslBundle() {
SslBundle serverBundle = mock(SslBundle.class);
DefaultSslBundleRegistry sslBundles = sslBundles("server", serverBundle);
sslBundles.registerBundle("client", mock(SslBundle.class));
this.embeddedProperties.getSsl().setBundle("server");
this.properties.getSsl().setBundle("client");
assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle);
}
@Test
void shouldIgnoreClientSslBundleThatDoesNotExist() {
SslBundle serverBundle = mock(SslBundle.class);
SslBundles sslBundles = sslBundles("server", serverBundle);
this.embeddedProperties.getSsl().setBundle("server");
this.properties.getSsl().setBundle("does-not-exist");
assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle);
}
@Test
void shouldUseServerSslBundleWhenClientSslIsDisabled() {
SslBundle serverBundle = mock(SslBundle.class);
SslBundles sslBundles = sslBundles("server", serverBundle);
this.embeddedProperties.getSsl().setBundle("server");
this.properties.getSsl().setEnabled(false);
assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle);
}
@Test
void shouldNotUseSslBundleWhenEmbeddedSslIsDisabled() {
SslBundles sslBundles = sslBundles("server", mock(SslBundle.class));
this.embeddedProperties.getSsl().setBundle("server");
this.embeddedProperties.getSsl().setEnabled(false);
assertThat(createConnectionDetails(sslBundles).getSslBundle()).isNull();
}
@Test
void shouldIgnoreClientSslBundleWhenEmbeddedSslIsDisabled() {
SslBundles sslBundles = sslBundles("client", mock(SslBundle.class));
this.properties.getSsl().setBundle("client");
assertThat(createConnectionDetails(sslBundles).getSslBundle()).isNull();
}
@Test
void shouldNotTrackReloadsOfTheBundleTheEmbeddedListenerStartedWith() {
DefaultSslBundleRegistry sslBundles = sslBundles("server", mock(SslBundle.class, "original"));
this.embeddedProperties.getSsl().setBundle("server");
createConnectionDetails(sslBundles);
sslBundles.updateBundle("server", mock(SslBundle.class, "reloaded"));
assertThat(ReflectionTestUtils.getField(LdapSslSocketFactory.class, "sslBundle")).isNull();
}
@Test
void shouldUseServerSslBundleWhenUrlsAreConfigured() {
SslBundle serverBundle = mock(SslBundle.class);
SslBundles sslBundles = sslBundles("server", serverBundle);
this.embeddedProperties.getSsl().setBundle("server");
this.properties.setUrls(new String[] { "ldap://ldap.example.com" });
assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle);
}
@Test
void shouldNotUseSslBundleWhenClientSslIsEnabledWithoutABundle() {
this.properties.getSsl().setEnabled(true);
assertThat(createConnectionDetails(null).getSslBundle()).isNull();
}
private DefaultSslBundleRegistry sslBundles(String name, SslBundle bundle) {
return new DefaultSslBundleRegistry(name, bundle);
}
private EmbeddedLdapConnectionDetails createConnectionDetails(@Nullable SslBundles sslBundles) {
return new EmbeddedLdapConnectionDetails(this.environment, this.properties, this.embeddedProperties,
sslBundles);
}
}