mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-09 19:09:03 +00:00
Add SSL bundle support to LDAP auto-configuration
See gh-51382 Signed-off-by: dhruv9b <9bdhruv@gmail.com>
This commit is contained in:
committed by
Moritz Halbritter
parent
862b763d00
commit
255677d8e7
+26
-4
@@ -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);
|
||||
|
||||
+9
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+36
@@ -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.
|
||||
*/
|
||||
|
||||
+22
-1
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+78
-6
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user