Polish "Add LDAPS support to embedded LDAP server"

See gh-48315
This commit is contained in:
Andy Wilkinson
2025-12-19 11:26:02 +00:00
parent 0934edf556
commit dfd41a6b9f
4 changed files with 104 additions and 330 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"
----
@@ -16,27 +16,15 @@
package org.springframework.boot.ldap.autoconfigure.embedded;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
@@ -64,6 +52,7 @@ 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;
@@ -79,8 +68,6 @@ import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.support.LdapContextSource;
@@ -106,8 +93,6 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
private final EmbeddedLdapProperties embeddedProperties;
private final ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver();
private @Nullable InMemoryDirectoryServer server;
EmbeddedLdapAutoConfiguration(EmbeddedLdapProperties embeddedProperties) {
@@ -116,8 +101,7 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
@Bean
InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext,
ObjectProvider<SslBundles> sslBundles) throws LDAPException, KeyStoreException, IOException,
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
ObjectProvider<SslBundles> sslBundles) throws LDAPException {
String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn());
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn);
String username = this.embeddedProperties.getCredential().getUsername();
@@ -125,19 +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);
if (this.embeddedProperties.getSsl().isEnabled()) {
EmbeddedLdapProperties.Ssl ssl = this.embeddedProperties.getSsl();
SSLContext sslContext = getSslContext(ssl, sslBundles.getIfAvailable());
SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory();
SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
config.setListenerConfigs(InMemoryListenerConfig.createLDAPSConfig("LDAPS", null,
this.embeddedProperties.getPort(), serverSocketFactory, clientSocketFactory));
}
else {
config
.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort()));
}
this.server = new InMemoryDirectoryServer(config);
importLdif(this.server, applicationContext);
this.server.startListening();
@@ -145,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);
@@ -216,70 +210,6 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
}
}
private SSLContext getSslContext(EmbeddedLdapProperties.Ssl ssl, @Nullable SslBundles sslBundles)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
UnrecoverableKeyException, KeyManagementException {
if (sslBundles != null && StringUtils.hasText(ssl.getBundle())) {
SslBundle sslBundle = sslBundles.getBundle(ssl.getBundle());
Assert.notNull(sslBundle, "SSL bundle name has been set but no SSL bundles found in context");
return sslBundle.createSslContext();
}
else {
SSLContext sslContext = SSLContext.getInstance(ssl.getAlgorithm());
KeyManager[] keyManagers = configureKeyManagers(ssl);
TrustManager[] trustManagers = configureTrustManagers(ssl);
sslContext.init(keyManagers, trustManagers, new SecureRandom());
return sslContext;
}
}
private KeyManager @Nullable [] configureKeyManagers(EmbeddedLdapProperties.Ssl ssl) throws KeyStoreException,
IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
String keyStoreName = ssl.getKeyStore();
String keyStorePassword = ssl.getKeyStorePassword();
String storeType = ssl.getKeyStoreType();
char[] keyPassphrase = null;
if (keyStorePassword != null) {
keyPassphrase = keyStorePassword.toCharArray();
}
KeyManager[] keyManagers = null;
if (StringUtils.hasText(keyStoreName)) {
Resource resource = this.resourceLoader.getResource(keyStoreName);
KeyStore ks = KeyStore.getInstance(storeType);
try (InputStream inputStream = resource.getInputStream()) {
ks.load(inputStream, keyPassphrase);
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(ssl.getKeyStoreAlgorithm());
kmf.init(ks, keyPassphrase);
keyManagers = kmf.getKeyManagers();
}
return keyManagers;
}
private TrustManager @Nullable [] configureTrustManagers(EmbeddedLdapProperties.Ssl ssl)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
String trustStoreName = ssl.getTrustStore();
String trustStorePassword = ssl.getTrustStorePassword();
String storeType = ssl.getTrustStoreType();
char[] trustPassphrase = null;
if (trustStorePassword != null) {
trustPassphrase = trustStorePassword.toCharArray();
}
TrustManager[] trustManagers = null;
if (StringUtils.hasText(trustStoreName)) {
Resource resource = this.resourceLoader.getResource(trustStoreName);
KeyStore tks = KeyStore.getInstance(storeType);
try (InputStream inputStream = resource.getInputStream()) {
tks.load(inputStream, trustPassphrase);
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(ssl.getTrustStoreAlgorithm());
tmf.init(tks);
trustManagers = tmf.getTrustManagers();
}
return trustManagers;
}
/**
* {@link SpringBootCondition} to determine when to apply embedded LDAP
* auto-configuration.
@@ -16,12 +16,9 @@
package org.springframework.boot.ldap.autoconfigure.embedded;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLContext;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -146,87 +143,22 @@ public class EmbeddedLdapProperties {
public static class Ssl {
private static final String SUN_X509 = "SunX509";
private static final String DEFAULT_PROTOCOL;
static {
String protocol = "TLSv1.1";
try {
String[] protocols = SSLContext.getDefault().getSupportedSSLParameters().getProtocols();
for (String prot : protocols) {
if ("TLSv1.2".equals(prot)) {
protocol = "TLSv1.2";
break;
}
}
}
catch (NoSuchAlgorithmException ex) {
// nothing
}
DEFAULT_PROTOCOL = protocol;
}
/**
* Whether to enable SSL support.
* Whether to enable SSL support. Enabled automatically if "bundle" is provided
* unless specified otherwise.
*/
private Boolean enabled = false;
private @Nullable Boolean enabled;
/**
* SSL bundle name.
*/
private @Nullable String bundle;
/**
* Path to the key store that holds the SSL certificate.
*/
private @Nullable String keyStore;
/**
* Key store type.
*/
private String keyStoreType = "PKCS12";
/**
* Password used to access the key store.
*/
private @Nullable String keyStorePassword;
/**
* Key store algorithm.
*/
private String keyStoreAlgorithm = SUN_X509;
/**
* Trust store that holds SSL certificates.
*/
private @Nullable String trustStore;
/**
* Trust store type.
*/
private String trustStoreType = "JKS";
/**
* Password used to access the trust store.
*/
private @Nullable String trustStorePassword;
/**
* Trust store algorithm.
*/
private String trustStoreAlgorithm = SUN_X509;
/**
* SSL algorithm to use.
*/
private String algorithm = DEFAULT_PROTOCOL;
public Boolean isEnabled() {
return this.enabled;
public boolean isEnabled() {
return (this.enabled != null) ? this.enabled : this.bundle != null;
}
public void setEnabled(Boolean enabled) {
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@@ -238,78 +170,6 @@ public class EmbeddedLdapProperties {
this.bundle = bundle;
}
public @Nullable String getKeyStore() {
return this.keyStore;
}
public void setKeyStore(@Nullable String keyStore) {
this.keyStore = keyStore;
}
public String getKeyStoreType() {
return this.keyStoreType;
}
public void setKeyStoreType(String keyStoreType) {
this.keyStoreType = keyStoreType;
}
public @Nullable String getKeyStorePassword() {
return this.keyStorePassword;
}
public void setKeyStorePassword(@Nullable String keyStorePassword) {
this.keyStorePassword = keyStorePassword;
}
public String getKeyStoreAlgorithm() {
return this.keyStoreAlgorithm;
}
public void setKeyStoreAlgorithm(String keyStoreAlgorithm) {
this.keyStoreAlgorithm = keyStoreAlgorithm;
}
public @Nullable String getTrustStore() {
return this.trustStore;
}
public void setTrustStore(@Nullable String trustStore) {
this.trustStore = trustStore;
}
public String getTrustStoreType() {
return this.trustStoreType;
}
public void setTrustStoreType(String trustStoreType) {
this.trustStoreType = trustStoreType;
}
public @Nullable String getTrustStorePassword() {
return this.trustStorePassword;
}
public void setTrustStorePassword(@Nullable String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}
public String getTrustStoreAlgorithm() {
return this.trustStoreAlgorithm;
}
public void setTrustStoreAlgorithm(String trustStoreAlgorithm) {
this.trustStoreAlgorithm = trustStoreAlgorithm;
}
public String getAlgorithm() {
return this.algorithm;
}
public void setAlgorithm(String sslAlgorithm) {
this.algorithm = sslAlgorithm;
}
}
public static class Validation {
@@ -67,101 +67,11 @@ class EmbeddedLdapAutoConfigurationTests {
.run((context) -> {
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
assertThat(server.getListenPort()).isEqualTo(1234);
});
}
@Test
void testServerDefaultNoSsl() {
this.contextRunner
.withPropertyValues("spring.ldap.embedded.port:1234", "spring.ldap.embedded.base-dn:dc=spring,dc=org")
.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 testServerWithSslBundle() {
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:1234");
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: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(config.getListenPort()).isEqualTo(1234);
assertThat(server.getListenPort()).isEqualTo(1234);
assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull();
});
}
@Test
void testServerWithInvalidSslBundleShouldFail() {
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:1234");
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");
});
}
@Test
void testServerWithSsl() {
List<String> propertyValues = new ArrayList<>();
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
propertyValues.add("spring.ldap.embedded.port:1234");
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.keyStorePassword=secret");
propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "test.jks");
propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret");
propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "test.jks");
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(config.getListenPort()).isEqualTo(1234);
assertThat(server.getListenPort()).isEqualTo(1234);
assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull();
});
}
@Test
void testServerWithInvalidSslShouldFail() {
List<String> propertyValues = new ArrayList<>();
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
propertyValues.add("spring.ldap.embedded.port:1234");
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.keyStorePassword=secret");
propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "foo");
propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret");
propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "foo");
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().hasMessageContaining("foo");
assertThat(context).getFailure().hasMessageContaining("does not exist");
});
}
@Test
void testRandomPortWithEnvironment() {
this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org").run((context) -> {
@@ -444,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 {