Add SSL bundle support to LDAP auto-configuration

See gh-51382

Signed-off-by: dhruv9b <9bdhruv@gmail.com>
This commit is contained in:
dhruv9b
2026-08-26 08:23:35 +02:00
committed by Moritz Halbritter
parent 862b763d00
commit 255677d8e7
7 changed files with 187 additions and 11 deletions
@@ -28,6 +28,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.ldap.autoconfigure.LdapProperties.Template;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.ldap.convert.ConverterUtils;
@@ -53,9 +56,12 @@ public final class LdapAutoConfiguration {
@Bean
@ConditionalOnMissingBean(LdapConnectionDetails.class)
PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties,
Environment environment) {
return new PropertiesLdapConnectionDetails(properties, environment);
PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(
LdapProperties properties,
Environment environment,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesLdapConnectionDetails(
properties, environment, sslBundles.getIfAvailable());
}
@Bean
@@ -63,7 +69,23 @@ public final class LdapAutoConfiguration {
LdapContextSource ldapContextSource(LdapConnectionDetails connectionDetails, LdapProperties properties,
ObjectProvider<DirContextAuthenticationStrategy> dirContextAuthenticationStrategy) {
LdapContextSource source = new LdapContextSource();
dirContextAuthenticationStrategy.ifUnique(source::setAuthenticationStrategy);
DirContextAuthenticationStrategy uniqueStrategy = dirContextAuthenticationStrategy.getIfUnique();
if (uniqueStrategy != null) {
// Exactly one custom strategy bean → use it
source.setAuthenticationStrategy(uniqueStrategy);
}
else if (!dirContextAuthenticationStrategy.stream().findAny().isPresent()) {
// No custom strategy beans at all → apply SSL bundle if configured
SslBundle sslBundle = connectionDetails.getSslBundle();
if (sslBundle != null) {
DefaultTlsDirContextAuthenticationStrategy tlsStrategy =
new DefaultTlsDirContextAuthenticationStrategy();
tlsStrategy.setSslSocketFactory(sslBundle.createSslContext().getSocketFactory());
source.setAuthenticationStrategy(tlsStrategy);
}
// else: no SSL bundle → retain built-in SimpleDirContextAuthenticationStrategy from constructor
}
// else: multiple custom strategy beans → retain built-in SimpleDirContextAuthenticationStrategy
PropertyMapper propertyMapper = PropertyMapper.get();
propertyMapper.from(connectionDetails.getUsername()).to(source::setUserDn);
propertyMapper.from(connectionDetails.getPassword()).to(source::setPassword);
@@ -19,6 +19,7 @@ package org.springframework.boot.ldap.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
/**
* Details required to establish a connection to an LDAP service.
@@ -58,4 +59,12 @@ public interface LdapConnectionDetails extends ConnectionDetails {
return null;
}
/**
* SSL bundle to use to establish the LDAP connection.
* @return the SSL bundle to use, or {@code null} if none
*/
default @Nullable SslBundle getSslBundle() {
return null;
}
}
@@ -27,6 +27,7 @@ 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;
/**
* Configuration properties for LDAP.
@@ -78,6 +79,8 @@ public class LdapProperties {
private final Template template = new Template();
private final Ssl ssl = new Ssl();
public String @Nullable [] getUrls() {
return this.urls;
}
@@ -134,6 +137,10 @@ public class LdapProperties {
return this.template;
}
public Ssl getSsl() {
return this.ssl;
}
public String[] determineUrls(Environment environment) {
if (ObjectUtils.isEmpty(this.urls)) {
return new String[] { "ldap://localhost:" + determinePort(environment) };
@@ -199,6 +206,35 @@ public class LdapProperties {
}
/**
* SSL configuration.
*/
public static class Ssl {
/**
* SSL bundle name.
*/
private @Nullable String bundle;
public @Nullable String getBundle() {
return this.bundle;
}
public void setBundle(@Nullable String bundle) {
this.bundle = bundle;
}
/**
* Returns whether SSL is enabled. SSL is considered enabled if a bundle name
* has been set.
* @return whether SSL is enabled
*/
public boolean determineEnabled() {
return StringUtils.hasText(this.bundle);
}
}
/**
* Define the methods to handle referrals.
*/
@@ -18,7 +18,11 @@ package org.springframework.boot.ldap.autoconfigure;
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.StringUtils;
/**
* Adapts {@link LdapProperties} to {@link LdapConnectionDetails}.
@@ -31,9 +35,13 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
private final Environment environment;
PropertiesLdapConnectionDetails(LdapProperties properties, Environment environment) {
private final @Nullable SslBundles sslBundles;
PropertiesLdapConnectionDetails(LdapProperties properties, Environment environment,
@Nullable SslBundles sslBundles) {
this.properties = properties;
this.environment = environment;
this.sslBundles = sslBundles;
}
@Override
@@ -56,4 +64,17 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
return this.properties.getPassword();
}
@Override
public @Nullable SslBundle getSslBundle() {
LdapProperties.Ssl ssl = this.properties.getSsl();
if (!ssl.determineEnabled()) {
return null;
}
if (StringUtils.hasLength(ssl.getBundle())) {
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return this.sslBundles.getBundle(ssl.getBundle());
}
return null;
}
}
@@ -16,9 +16,12 @@
package org.springframework.boot.ldap.testcontainers;
import org.jspecify.annotations.Nullable;
import org.testcontainers.ldap.LLdapContainer;
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@@ -64,6 +67,11 @@ class LLdapContainerConnectionDetailsFactory
return getContainer().getPassword();
}
@Override
public @Nullable SslBundle getSslBundle() {
return super.getSslBundle();
}
}
}
@@ -20,10 +20,13 @@ import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@@ -84,6 +87,11 @@ class OpenLdapContainerConnectionDetailsFactory
return getContainer().getEnvMap().getOrDefault("LDAP_ADMIN_PASSWORD", "admin");
}
@Override
public @Nullable SslBundle getSslBundle() {
return super.getSslBundle();
}
}
}
@@ -20,6 +20,7 @@ import javax.naming.Name;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.convert.ApplicationConversionService;
@@ -35,8 +36,13 @@ import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
import org.springframework.ldap.pool2.factory.PoolConfig;
import org.springframework.ldap.pool2.factory.PooledContextSource;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;
/**
@@ -199,13 +205,52 @@ class LdapAutoConfigurationTests {
assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreSizeLimitExceededException", false);
});
}
@Test
void contextSourceWithUserProvidedPooledContextSource() {
this.contextRunner.withUserConfiguration(PooledContextSourceConfig.class).run((context) -> {
void contextSourceWithSslBundleAndCustomDirContextAuthenticationStrategyUsesCustomStrategy() {
SslBundle sslBundle = mock(SslBundle.class);
SslBundles sslBundles = mock(SslBundles.class);
when(sslBundles.getBundle("test")).thenReturn(sslBundle);
DirContextAuthenticationStrategy customStrategy = mock(DirContextAuthenticationStrategy.class);
this.contextRunner
.withPropertyValues("spring.ldap.ssl.bundle=test")
.withBean(SslBundles.class, () -> sslBundles)
.withBean(DirContextAuthenticationStrategy.class, () -> customStrategy)
.run((context) -> {
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource).extracting("authenticationStrategy").isSameAs(customStrategy);
});
}
@Test
void contextSourceWithSslBundleUsesDefaultTlsAuthenticationStrategy() {
SslBundle sslBundle = mock(SslBundle.class);
SSLContext sslContext = mock(SSLContext.class);
SSLSocketFactory socketFactory = mock(SSLSocketFactory.class);
when(sslBundle.createSslContext()).thenReturn(sslContext);
when(sslContext.getSocketFactory()).thenReturn(socketFactory);
SslBundles sslBundles = mock(SslBundles.class);
when(sslBundles.getBundle("test")).thenReturn(sslBundle);
this.contextRunner
.withPropertyValues("spring.ldap.ssl.bundle=test")
.withBean(SslBundles.class, () -> sslBundles)
.run((context) -> {
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource).extracting("authenticationStrategy")
.isInstanceOf(DefaultTlsDirContextAuthenticationStrategy.class)
.extracting("sslSocketFactory")
.isSameAs(socketFactory);
});
}
@Test
void contextSourceWithoutSslBundleDoesNotConfigureAuthenticationStrategy() {
this.contextRunner.run((context) -> {
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource.getUrls()).containsExactly("ldap://localhost:389");
assertThat(contextSource.isAnonymousReadOnly()).isTrue();
assertThat(contextSource).extracting("authenticationStrategy")
.isInstanceOf(SimpleDirContextAuthenticationStrategy.class);
});
}
@@ -235,6 +280,28 @@ class LdapAutoConfigurationTests {
});
}
@Test
void contextSourceWithSslBundleAndMultipleCustomStrategiesUsesDefault() {
SslBundle sslBundle = mock(SslBundle.class);
SslBundles sslBundles = mock(SslBundles.class);
when(sslBundles.getBundle("test")).thenReturn(sslBundle);
this.contextRunner
.withPropertyValues("spring.ldap.ssl.bundle=test")
.withBean(SslBundles.class, () -> sslBundles)
.withUserConfiguration(CustomDirContextAuthenticationStrategy.class,
AnotherCustomDirContextAuthenticationStrategy.class)
.run((context) -> {
assertThat(context).hasBean("customDirContextAuthenticationStrategy")
.hasBean("anotherCustomDirContextAuthenticationStrategy");
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
assertThat(contextSource).extracting("authenticationStrategy")
.isNotSameAs(context.getBean("customDirContextAuthenticationStrategy"))
.isNotSameAs(context.getBean("anotherCustomDirContextAuthenticationStrategy"))
.isInstanceOf(SimpleDirContextAuthenticationStrategy.class);
});
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsConfiguration {
@@ -261,6 +328,11 @@ class LdapAutoConfigurationTests {
public String getPassword() {
return "ldap-password";
}
@Override
public @Nullable SslBundle getSslBundle() {
return null;
}
};
}