Merge pull request #48315 from CatiaCorreia

* gh-48315:
  Polish "Add LDAPS support to embedded LDAP server"
  Add LDAPS support to embedded LDAP server

Closes gh-48315
This commit is contained in:
Andy Wilkinson
2025-12-19 11:26:09 +00:00
5 changed files with 154 additions and 5 deletions
@@ -736,3 +736,20 @@ If you want to load the initialization script from a different resource, you can
By default, a standard schema is used to validate `LDIF` files.
You can turn off validation altogether by setting the configprop:spring.ldap.embedded.validation.enabled[] property.
If you have custom attributes, you can use configprop:spring.ldap.embedded.validation.schema[] to define your custom attribute types or object classes.
[[data.nosql.ldap.embedded.ssl]]
==== SSL
The in-memory LDAP server supports SSL (LDAPS).
To enable SSL, configure the xref:features/ssl.adoc[SSL bundle] to use by setting the configprop:spring.ldap.embedded.ssl.bundle[] property, as shown in the following example:
[configprops,yaml]
----
spring:
ldap:
embedded:
ssl:
bundle: "example"
----
@@ -22,6 +22,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
@@ -33,6 +37,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
@@ -47,6 +52,9 @@ import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration;
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;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -63,6 +71,7 @@ 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;
/**
@@ -91,7 +100,8 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
}
@Bean
InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) throws LDAPException {
InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext,
ObjectProvider<SslBundles> sslBundles) throws LDAPException {
String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn());
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn);
String username = this.embeddedProperties.getCredential().getUsername();
@@ -99,10 +109,8 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
config.addAdditionalBindCredentials(username, password);
}
config.setListenerConfigs(createListenerConfig(sslBundles));
setSchema(config);
InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("LDAP",
this.embeddedProperties.getPort());
config.setListenerConfigs(listenerConfig);
this.server = new InMemoryDirectoryServer(config);
importLdif(this.server, applicationContext);
this.server.startListening();
@@ -110,6 +118,27 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
return this.server;
}
private InMemoryListenerConfig createListenerConfig(ObjectProvider<SslBundles> sslBundles) throws LDAPException {
SslBundle sslBundle = getSslBundle(sslBundles.getIfAvailable());
if (sslBundle != null) {
SSLContext sslContext = sslBundle.createSslContext();
SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory();
SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
return InMemoryListenerConfig.createLDAPSConfig("LDAPS", null, this.embeddedProperties.getPort(),
serverSocketFactory, clientSocketFactory);
}
return InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort());
}
private @Nullable SslBundle getSslBundle(@Nullable SslBundles sslBundles) {
Ssl ssl = this.embeddedProperties.getSsl();
if (ssl.isEnabled() && StringUtils.hasLength(ssl.getBundle())) {
Assert.notNull(sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return sslBundles.getBundle(ssl.getBundle());
}
return null;
}
private void setSchema(InMemoryDirectoryServerConfig config) {
if (!this.embeddedProperties.getValidation().isEnabled()) {
config.setSchema(null);
@@ -62,6 +62,11 @@ public class EmbeddedLdapProperties {
*/
private final Validation validation = new Validation();
/**
* SSL configuration.
*/
private final Ssl ssl = new Ssl();
public int getPort() {
return this.port;
}
@@ -98,6 +103,10 @@ public class EmbeddedLdapProperties {
return this.validation;
}
public Ssl getSsl() {
return this.ssl;
}
public static class Credential {
/**
@@ -132,6 +141,37 @@ public class EmbeddedLdapProperties {
}
public static class Ssl {
/**
* Whether to enable SSL support. Enabled automatically if "bundle" is provided
* unless specified otherwise.
*/
private @Nullable Boolean enabled;
/**
* SSL bundle name.
*/
private @Nullable String bundle;
public boolean isEnabled() {
return (this.enabled != null) ? this.enabled : this.bundle != null;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public @Nullable String getBundle() {
return this.bundle;
}
public void setBundle(@Nullable String bundle) {
this.bundle = bundle;
}
}
public static class Validation {
/**
@@ -20,8 +20,11 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.LDAPConnection;
@@ -32,6 +35,7 @@ import org.junit.jupiter.api.Test;
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.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -54,7 +58,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class EmbeddedLdapAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void testSetDefaultPort() {
@@ -63,6 +67,8 @@ class EmbeddedLdapAutoConfigurationTests {
.run((context) -> {
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
assertThat(server.getListenPort()).isEqualTo(1234);
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
assertThat(config.getListenerName()).isEqualTo("LDAP");
});
}
@@ -348,6 +354,63 @@ 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();
});
}
@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");
});
}
@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");
});
}
@Configuration(proxyBeanMethods = false)
static class LdapClientConfiguration {