Skip to content

Commit 66ba205

Browse files
committed
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
2 parents a1ed3bf + dfd41a6 commit 66ba205

File tree

5 files changed

+154
-5
lines changed

5 files changed

+154
-5
lines changed

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,3 +736,20 @@ If you want to load the initialization script from a different resource, you can
736736
By default, a standard schema is used to validate `LDIF` files.
737737
You can turn off validation altogether by setting the configprop:spring.ldap.embedded.validation.enabled[] property.
738738
If you have custom attributes, you can use configprop:spring.ldap.embedded.validation.schema[] to define your custom attribute types or object classes.
739+
740+
741+
742+
[[data.nosql.ldap.embedded.ssl]]
743+
==== SSL
744+
745+
The in-memory LDAP server supports SSL (LDAPS).
746+
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:
747+
748+
[configprops,yaml]
749+
----
750+
spring:
751+
ldap:
752+
embedded:
753+
ssl:
754+
bundle: "example"
755+
----

module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
import java.util.List;
2323
import java.util.Map;
2424

25+
import javax.net.ssl.SSLContext;
26+
import javax.net.ssl.SSLServerSocketFactory;
27+
import javax.net.ssl.SSLSocketFactory;
28+
2529
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
2630
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
2731
import com.unboundid.ldap.listener.InMemoryListenerConfig;
@@ -33,6 +37,7 @@
3337
import org.springframework.aot.hint.RuntimeHints;
3438
import org.springframework.aot.hint.RuntimeHintsRegistrar;
3539
import org.springframework.beans.factory.DisposableBean;
40+
import org.springframework.beans.factory.ObjectProvider;
3641
import org.springframework.boot.autoconfigure.AutoConfiguration;
3742
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3843
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
@@ -47,6 +52,9 @@
4752
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration;
4853
import org.springframework.boot.ldap.autoconfigure.LdapProperties;
4954
import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints;
55+
import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapProperties.Ssl;
56+
import org.springframework.boot.ssl.SslBundle;
57+
import org.springframework.boot.ssl.SslBundles;
5058
import org.springframework.context.ApplicationContext;
5159
import org.springframework.context.ConfigurableApplicationContext;
5260
import org.springframework.context.annotation.Bean;
@@ -63,6 +71,7 @@
6371
import org.springframework.core.type.AnnotatedTypeMetadata;
6472
import org.springframework.ldap.core.ContextSource;
6573
import org.springframework.ldap.core.support.LdapContextSource;
74+
import org.springframework.util.Assert;
6675
import org.springframework.util.StringUtils;
6776

6877
/**
@@ -91,25 +100,45 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean {
91100
}
92101

93102
@Bean
94-
InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) throws LDAPException {
103+
InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext,
104+
ObjectProvider<SslBundles> sslBundles) throws LDAPException {
95105
String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn());
96106
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn);
97107
String username = this.embeddedProperties.getCredential().getUsername();
98108
String password = this.embeddedProperties.getCredential().getPassword();
99109
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
100110
config.addAdditionalBindCredentials(username, password);
101111
}
112+
config.setListenerConfigs(createListenerConfig(sslBundles));
102113
setSchema(config);
103-
InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("LDAP",
104-
this.embeddedProperties.getPort());
105-
config.setListenerConfigs(listenerConfig);
106114
this.server = new InMemoryDirectoryServer(config);
107115
importLdif(this.server, applicationContext);
108116
this.server.startListening();
109117
setPortProperty(applicationContext, this.server.getListenPort());
110118
return this.server;
111119
}
112120

121+
private InMemoryListenerConfig createListenerConfig(ObjectProvider<SslBundles> sslBundles) throws LDAPException {
122+
SslBundle sslBundle = getSslBundle(sslBundles.getIfAvailable());
123+
if (sslBundle != null) {
124+
SSLContext sslContext = sslBundle.createSslContext();
125+
SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory();
126+
SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
127+
return InMemoryListenerConfig.createLDAPSConfig("LDAPS", null, this.embeddedProperties.getPort(),
128+
serverSocketFactory, clientSocketFactory);
129+
}
130+
return InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort());
131+
}
132+
133+
private @Nullable SslBundle getSslBundle(@Nullable SslBundles sslBundles) {
134+
Ssl ssl = this.embeddedProperties.getSsl();
135+
if (ssl.isEnabled() && StringUtils.hasLength(ssl.getBundle())) {
136+
Assert.notNull(sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
137+
return sslBundles.getBundle(ssl.getBundle());
138+
}
139+
return null;
140+
}
141+
113142
private void setSchema(InMemoryDirectoryServerConfig config) {
114143
if (!this.embeddedProperties.getValidation().isEnabled()) {
115144
config.setSchema(null);

module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public class EmbeddedLdapProperties {
6262
*/
6363
private final Validation validation = new Validation();
6464

65+
/**
66+
* SSL configuration.
67+
*/
68+
private final Ssl ssl = new Ssl();
69+
6570
public int getPort() {
6671
return this.port;
6772
}
@@ -98,6 +103,10 @@ public Validation getValidation() {
98103
return this.validation;
99104
}
100105

106+
public Ssl getSsl() {
107+
return this.ssl;
108+
}
109+
101110
public static class Credential {
102111

103112
/**
@@ -132,6 +141,37 @@ boolean isAvailable() {
132141

133142
}
134143

144+
public static class Ssl {
145+
146+
/**
147+
* Whether to enable SSL support. Enabled automatically if "bundle" is provided
148+
* unless specified otherwise.
149+
*/
150+
private @Nullable Boolean enabled;
151+
152+
/**
153+
* SSL bundle name.
154+
*/
155+
private @Nullable String bundle;
156+
157+
public boolean isEnabled() {
158+
return (this.enabled != null) ? this.enabled : this.bundle != null;
159+
}
160+
161+
public void setEnabled(boolean enabled) {
162+
this.enabled = enabled;
163+
}
164+
165+
public @Nullable String getBundle() {
166+
return this.bundle;
167+
}
168+
169+
public void setBundle(@Nullable String bundle) {
170+
this.bundle = bundle;
171+
}
172+
173+
}
174+
135175
public static class Validation {
136176

137177
/**

module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
import java.lang.annotation.Retention;
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
23+
import java.util.ArrayList;
24+
import java.util.List;
2325

2426
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
27+
import com.unboundid.ldap.listener.InMemoryListenerConfig;
2528
import com.unboundid.ldap.sdk.BindResult;
2629
import com.unboundid.ldap.sdk.DN;
2730
import com.unboundid.ldap.sdk.LDAPConnection;
@@ -32,6 +35,7 @@
3235
import org.springframework.beans.factory.annotation.Value;
3336
import org.springframework.boot.autoconfigure.AutoConfigurations;
3437
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
38+
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
3539
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration;
3640
import org.springframework.boot.test.context.FilteredClassLoader;
3741
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -54,7 +58,7 @@
5458
class EmbeddedLdapAutoConfigurationTests {
5559

5660
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
57-
.withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class));
61+
.withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, SslAutoConfiguration.class));
5862

5963
@Test
6064
void testSetDefaultPort() {
@@ -63,6 +67,8 @@ void testSetDefaultPort() {
6367
.run((context) -> {
6468
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
6569
assertThat(server.getListenPort()).isEqualTo(1234);
70+
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
71+
assertThat(config.getListenerName()).isEqualTo("LDAP");
6672
});
6773
}
6874

@@ -348,6 +354,63 @@ void ldapContextIsCreatedWithBase() {
348354
});
349355
}
350356

357+
@Test
358+
void whenSslBundleIsConfiguredLdapsListenerIsConfigured() {
359+
List<String> propertyValues = new ArrayList<>();
360+
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
361+
propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret");
362+
propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks");
363+
propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks");
364+
propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2");
365+
propertyValues.add("spring.ldap.embedded.port:0");
366+
propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org");
367+
propertyValues.add("spring.ldap.embedded.ssl.bundle:test");
368+
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
369+
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
370+
assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1);
371+
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
372+
assertThat(config.getListenerName()).isEqualTo("LDAPS");
373+
assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull();
374+
});
375+
}
376+
377+
@Test
378+
void whenSslBundleIsConfiguredButSslIsDisabledLdapListenerIsConfigured() {
379+
List<String> propertyValues = new ArrayList<>();
380+
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
381+
propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret");
382+
propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks");
383+
propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks");
384+
propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2");
385+
propertyValues.add("spring.ldap.embedded.port:0");
386+
propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org");
387+
propertyValues.add("spring.ldap.embedded.ssl.enabled:false");
388+
propertyValues.add("spring.ldap.embedded.ssl.bundle:test");
389+
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
390+
InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class);
391+
assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1);
392+
InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0);
393+
assertThat(config.getListenerName()).isEqualTo("LDAP");
394+
});
395+
}
396+
397+
@Test
398+
void whenInvalidSslBundleIsConfiguredThenStartFails() {
399+
List<String> propertyValues = new ArrayList<>();
400+
String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/";
401+
propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret");
402+
propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks");
403+
propertyValues.add("spring.ldap.embedded.port:0");
404+
propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org");
405+
propertyValues.add("spring.ldap.embedded.ssl.enabled:true");
406+
propertyValues.add("spring.ldap.embedded.ssl.bundle:foo");
407+
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
408+
assertThat(context).hasFailed();
409+
assertThat(context).getFailure().hasMessageContaining("foo");
410+
assertThat(context).getFailure().hasMessageContaining("cannot be found");
411+
});
412+
}
413+
351414
@Configuration(proxyBeanMethods = false)
352415
static class LdapClientConfiguration {
353416

0 commit comments

Comments
 (0)