Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
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;
Expand All @@ -53,17 +56,36 @@ 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
@ConditionalOnMissingBean
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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.
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -134,6 +137,10 @@ public Template getTemplate() {
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) };
Expand Down Expand Up @@ -199,6 +206,35 @@ public void setIgnoreSizeLimitExceededException(Boolean ignoreSizeLimitExceededE

}

/**
* 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@

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}.
Expand All @@ -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
Expand All @@ -56,4 +64,17 @@ public String[] getUrls() {
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,6 +67,11 @@ public String getPassword() {
return getContainer().getPassword();
}

@Override
public @Nullable SslBundle getSslBundle() {
return super.getSslBundle();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
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;
Expand Down Expand Up @@ -84,6 +87,11 @@ public String getPassword() {
return getContainer().getEnvMap().getOrDefault("LDAP_ADMIN_PASSWORD", "admin");
}

@Override
public @Nullable SslBundle getSslBundle() {
return super.getSslBundle();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

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;
Expand All @@ -35,8 +36,13 @@
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;

/**
Expand Down Expand Up @@ -199,13 +205,52 @@ void templateConfigurationCanBeCustomized() {
assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreSizeLimitExceededException", false);
});
}
@Test
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 contextSourceWithUserProvidedPooledContextSource() {
this.contextRunner.withUserConfiguration(PooledContextSourceConfig.class).run((context) -> {
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);
});
}

Expand Down Expand Up @@ -235,6 +280,28 @@ void contextSourceWithCustomNonUniqueDirContextAuthenticationStrategy() {
});
}

@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 {

Expand All @@ -261,6 +328,11 @@ public String getUsername() {
public String getPassword() {
return "ldap-password";
}

@Override
public @Nullable SslBundle getSslBundle() {
return null;
}
};
}

Expand Down