diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc index c9772280051..8b1637b1365 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc @@ -728,12 +728,12 @@ If you need SSL support without customizing the trust and key material, set the This uses the platform's default trust and key material. WARNING: These properties only apply to the auto-configured javadoc:org.springframework.ldap.core.support.LdapContextSource[]. -If you define your own javadoc:org.springframework.ldap.core.support.LdapContextSource[] bean, they are ignored and you have to configure SSL yourself. -If you define your own javadoc:org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails[] bean, including one contributed by a `@ServiceConnection`, they are ignored as well and the auto-configured context source uses the SSL bundle provided by that bean instead. +If you define your own context source, you have to configure SSL on it yourself. +If you define your own javadoc:org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails[] bean, including one contributed by a `@ServiceConnection`, the auto-configured context source uses the SSL bundle that bean provides instead. An `ldaps://` connection that has not been given any key or trust material silently uses the platform's default rather than failing. -TIP: These properties configure the LDAP client. -To enable SSL on the xref:data/nosql.adoc#data.nosql.ldap.embedded.ssl[embedded LDAP server], use the separate configprop:spring.ldap.embedded.ssl.bundle[] property. +TIP: These properties configure the LDAP client and are ignored while an embedded LDAP server is used. +To enable SSL on the xref:data/nosql.adoc#data.nosql.ldap.embedded.ssl[embedded LDAP server], use the separate configprop:spring.ldap.embedded.ssl.bundle[] property, which provides the server's certificate and private key as well as the client's trust material. @@ -784,7 +784,6 @@ spring.ldap.embedded.base-dn: ==== By default, the server starts on a random port and triggers the regular LDAP support. -There is no need to specify a configprop:spring.ldap.urls[] property. If there is a `schema.ldif` file on your classpath, it is used to initialize the server. If you want to load the initialization script from a different resource, you can also use the configprop:spring.ldap.embedded.ldif[] property. @@ -793,6 +792,19 @@ 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. +The `spring.ldap.embedded` properties configure the server, while the remaining `spring.ldap` properties configure the client that connects to it, as described in xref:data/nosql.adoc#data.nosql.ldap[LDAP]. +Everything that describes the connection itself is taken from the server, as only the server can determine what a connection to it looks like: + +* The client connects to the port and scheme that the server is listening on, so configprop:spring.ldap.urls[] is ignored. +* The client uses the credentials from configprop:spring.ldap.embedded.credential.username[] and configprop:spring.ldap.embedded.credential.password[], so configprop:spring.ldap.username[] and configprop:spring.ldap.password[] are ignored. +* The client uses the SSL bundle from configprop:spring.ldap.embedded.ssl.bundle[], so `spring.ldap.ssl.*` is ignored. + +A `spring.ldap` configuration meant for a production server therefore does not have to be unset to run a test against the embedded server. +The remaining properties, such as configprop:spring.ldap.base[], configprop:spring.ldap.referral[] and `spring.ldap.base-environment.*`, are applied as usual. + +TIP: To configure the client differently, define your own javadoc:org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails[] bean. +The auto-configured client then uses that bean and ignores the embedded server's URLs, credentials and SSL bundle. + [[data.nosql.ldap.embedded.ssl]] @@ -813,3 +825,8 @@ spring: The bundle must provide the server's certificate and private key. NOTE: An SSL bundle is required for LDAPS. Setting configprop:spring.ldap.embedded.ssl.enabled[] without configprop:spring.ldap.embedded.ssl.bundle[] fails at startup. + +The auto-configured client connects to the server over `ldaps://` using the same bundle, so the bundle's trust material must trust the server's certificate. + +NOTE: The server decides whether TLS is used at all and which bundle secures the connection. +To give the client its own SSL configuration, define your own javadoc:org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails[] bean. diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfiguration.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfiguration.java index ec9b0c9b46b..539e27e719d 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfiguration.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapAutoConfiguration.java @@ -41,7 +41,6 @@ import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ImportRuntimeHints; -import org.springframework.core.env.Environment; import org.springframework.ldap.convert.ConverterUtils; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapOperations; @@ -70,9 +69,9 @@ public final class LdapAutoConfiguration { @Bean @ConditionalOnMissingBean(LdapConnectionDetails.class) - PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties, Environment environment, + PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties, ObjectProvider sslBundles) { - return new PropertiesLdapConnectionDetails(properties, environment, sslBundles.getIfAvailable()); + return new PropertiesLdapConnectionDetails(properties, sslBundles.getIfAvailable()); } @Bean @@ -100,9 +99,10 @@ public final class LdapAutoConfiguration { * through a {@link DirContextAuthenticationStrategy} as the strategy is not consulted * when read-only operations use an anonymous environment. *

- * A socket factory in the base environment conflicts with the bundle and is rejected, - * but only when the bundle also came from the properties. A bundle from another - * {@link LdapConnectionDetails} bean takes precedence over the properties instead. + * A socket factory in the base environment conflicts with the bundle and is rejected + * when the bundle came from properties, whether the client's or the embedded + * server's. A bundle from a user-supplied {@link LdapConnectionDetails} bean takes + * precedence over the base environment instead. * @param connectionDetails the connection details * @param properties the LDAP properties * @return the base environment properties diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java index c5d2e62df72..6004664af99 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/LdapProperties.java @@ -22,11 +22,8 @@ import java.util.Map; import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.core.env.Environment; 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; /** @@ -38,10 +35,6 @@ import org.springframework.util.StringUtils; @ConfigurationProperties("spring.ldap") public class LdapProperties { - private static final int DEFAULT_PORT = 389; - - private static final int DEFAULT_SSL_PORT = 636; - /** * LDAP URLs of the server. */ @@ -143,25 +136,6 @@ public class LdapProperties { return this.ssl; } - public String[] determineUrls(Environment environment) { - if (ObjectUtils.isEmpty(this.urls)) { - boolean useSsl = this.ssl.isEnabled(); - String protocol = useSsl ? "ldaps" : "ldap"; - int defaultPort = useSsl ? DEFAULT_SSL_PORT : DEFAULT_PORT; - return new String[] { protocol + "://localhost:" + determinePort(environment, defaultPort) }; - } - return this.urls; - } - - private int determinePort(Environment environment, int defaultPort) { - Assert.notNull(environment, "'environment' must not be null"); - String localPort = environment.getProperty("local.ldap.port"); - if (localPort != null) { - return Integer.parseInt(localPort); - } - return defaultPort; - } - /** * {@link LdapTemplate settings}. */ diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/PropertiesLdapConnectionDetails.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/PropertiesLdapConnectionDetails.java index 5b2ed84f12f..547e112e77d 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/PropertiesLdapConnectionDetails.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/PropertiesLdapConnectionDetails.java @@ -20,27 +20,28 @@ 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.ObjectUtils; import org.springframework.util.StringUtils; /** * Adapts {@link LdapProperties} to {@link LdapConnectionDetails}. * * @author Philipp Kessler + * @author Moritz Halbritter */ class PropertiesLdapConnectionDetails implements LdapConnectionDetails { - private final LdapProperties properties; + private static final int DEFAULT_PORT = 389; - private final Environment environment; + private static final int DEFAULT_SSL_PORT = 636; + + private final LdapProperties properties; private final @Nullable SslBundles sslBundles; - PropertiesLdapConnectionDetails(LdapProperties properties, Environment environment, - @Nullable SslBundles sslBundles) { + PropertiesLdapConnectionDetails(LdapProperties properties, @Nullable SslBundles sslBundles) { this.properties = properties; - this.environment = environment; this.sslBundles = sslBundles; registerSslBundleUpdateHandler(); } @@ -60,7 +61,14 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails { @Override public String[] getUrls() { - return this.properties.determineUrls(this.environment); + String[] urls = this.properties.getUrls(); + if (!ObjectUtils.isEmpty(urls)) { + return urls; + } + boolean useSsl = this.properties.getSsl().isEnabled(); + String protocol = useSsl ? "ldaps" : "ldap"; + int port = useSsl ? DEFAULT_SSL_PORT : DEFAULT_PORT; + return new String[] { protocol + "://localhost:" + port }; } @Override diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java index bf42ee114a5..50c7770f31f 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java @@ -50,6 +50,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration; +import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails; 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; @@ -70,7 +71,6 @@ import org.springframework.core.env.PropertySource; 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; @@ -80,6 +80,7 @@ import org.springframework.util.StringUtils; * @author EddĂș MelĂ©ndez * @author Mathieu Ouellet * @author Raja Kolli + * @author Moritz Halbritter * @since 4.0.0 */ @AutoConfiguration(before = LdapAutoConfiguration.class) @@ -242,19 +243,11 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean { @Bean @DependsOn("directoryServer") - @ConditionalOnMissingBean - LdapContextSource ldapContextSource(Environment environment, LdapProperties properties, - EmbeddedLdapProperties embeddedProperties) { - LdapContextSource source = new LdapContextSource(); - source.setBase(properties.getBase()); - String username = embeddedProperties.getCredential().getUsername(); - String password = embeddedProperties.getCredential().getPassword(); - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - source.setUserDn(username); - source.setPassword(password); - } - source.setUrls(properties.determineUrls(environment)); - return source; + @ConditionalOnMissingBean(LdapConnectionDetails.class) + EmbeddedLdapConnectionDetails embeddedLdapConnectionDetails(Environment environment, LdapProperties properties, + EmbeddedLdapProperties embeddedProperties, ObjectProvider sslBundles) { + return new EmbeddedLdapConnectionDetails(environment, properties, embeddedProperties, + sslBundles.getIfAvailable()); } } diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapConnectionDetails.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapConnectionDetails.java new file mode 100644 index 00000000000..768e42502c1 --- /dev/null +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapConnectionDetails.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.ldap.autoconfigure.embedded; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails; +import org.springframework.boot.ldap.autoconfigure.LdapProperties; +import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapProperties.Ssl; +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; + +/** + * {@link LdapConnectionDetails} for the embedded LDAP server. Everything that describes + * the connection comes from {@code spring.ldap.embedded}, as only the server can decide + * what a connection to it looks like: the URL from the port it is listening on and its + * SSL configuration, the credentials from {@code spring.ldap.embedded.credential}. The + * equivalent client properties, {@code spring.ldap.urls}, {@code spring.ldap.username}, + * {@code spring.ldap.password} and {@code spring.ldap.ssl}, are ignored, so a + * configuration meant for production does not have to be unset to run against the + * embedded server. + * + * @author Moritz Halbritter + */ +class EmbeddedLdapConnectionDetails implements LdapConnectionDetails { + + private static final String SOCKET_FACTORY_ENV_KEY = "java.naming.ldap.factory.socket"; + + private final Environment environment; + + private final LdapProperties properties; + + private final EmbeddedLdapProperties embeddedProperties; + + private final @Nullable SslBundles sslBundles; + + EmbeddedLdapConnectionDetails(Environment environment, LdapProperties properties, + EmbeddedLdapProperties embeddedProperties, @Nullable SslBundles sslBundles) { + this.environment = environment; + this.properties = properties; + this.embeddedProperties = embeddedProperties; + this.sslBundles = sslBundles; + } + + @Override + public String[] getUrls() { + String protocol = this.embeddedProperties.getSsl().isEnabled() ? "ldaps" : "ldap"; + return new String[] { protocol + "://localhost:" + this.environment.getRequiredProperty("local.ldap.port") }; + } + + @Override + public @Nullable String getBase() { + return this.properties.getBase(); + } + + @Override + public @Nullable String getUsername() { + return hasCredential() ? this.embeddedProperties.getCredential().getUsername() : null; + } + + @Override + public @Nullable String getPassword() { + return hasCredential() ? this.embeddedProperties.getCredential().getPassword() : null; + } + + private boolean hasCredential() { + return StringUtils.hasText(this.embeddedProperties.getCredential().getUsername()) + && StringUtils.hasText(this.embeddedProperties.getCredential().getPassword()); + } + + @Override + public @Nullable SslBundle getSslBundle() { + Ssl serverSsl = this.embeddedProperties.getSsl(); + if (!serverSsl.isEnabled()) { + return null; + } + String bundle = serverSsl.getBundle(); + if (bundle == null) { + return null; + } + assertNoSocketFactoryInBaseEnvironment(); + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(bundle); + } + + /** + * Rejects a socket factory in the base environment that the SSL bundle's socket + * factory would replace, as both come from properties and so contradict each other. + * Called only when a bundle is actually used, as nothing is replaced otherwise. + */ + private void assertNoSocketFactoryInBaseEnvironment() { + Assert.state(!this.properties.getBaseEnvironment().containsKey(SOCKET_FACTORY_ENV_KEY), + () -> "SSL bundle has been configured but '" + SOCKET_FACTORY_ENV_KEY + + "' has also been set in the base environment. Use either an SSL bundle or your own socket factory, not both"); + } + +} diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapPropertiesTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapPropertiesTests.java index b0bf08fc963..af5202ea4ee 100644 --- a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapPropertiesTests.java +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/LdapPropertiesTests.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.ldap.autoconfigure.LdapProperties.Template; import org.springframework.ldap.core.LdapTemplate; -import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -43,40 +42,4 @@ class LdapPropertiesTests { templateProperties.isIgnoreSizeLimitExceededException()); } - @Test - void determineUrlsShouldDefaultToPlainLdapWhenSslNotEnabled() { - LdapProperties properties = new LdapProperties(); - assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldap://localhost:389"); - } - - @Test - void determineUrlsShouldDefaultToLdapsWhenSslEnabled() { - LdapProperties properties = new LdapProperties(); - properties.getSsl().setEnabled(true); - assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldaps://localhost:636"); - } - - @Test - void determineUrlsShouldDefaultToLdapsWhenSslBundleConfigured() { - LdapProperties properties = new LdapProperties(); - properties.getSsl().setBundle("example"); - assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldaps://localhost:636"); - } - - @Test - void determineUrlsShouldPreferLocalPortOverDefaultSslPort() { - LdapProperties properties = new LdapProperties(); - properties.getSsl().setEnabled(true); - MockEnvironment environment = new MockEnvironment().withProperty("local.ldap.port", "1234"); - assertThat(properties.determineUrls(environment)).containsExactly("ldaps://localhost:1234"); - } - - @Test - void determineUrlsShouldUseConfiguredUrlsRegardlessOfSsl() { - LdapProperties properties = new LdapProperties(); - properties.setUrls(new String[] { "ldap://localhost:1234" }); - properties.getSsl().setEnabled(true); - assertThat(properties.determineUrls(new MockEnvironment())).containsExactly("ldap://localhost:1234"); - } - } diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/PropertiesLdapConnectionDetailsTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/PropertiesLdapConnectionDetailsTests.java new file mode 100644 index 00000000000..f2fde30b925 --- /dev/null +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/PropertiesLdapConnectionDetailsTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.ldap.autoconfigure; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertiesLdapConnectionDetails}. + * + * @author Moritz Halbritter + */ +class PropertiesLdapConnectionDetailsTests { + + private final LdapProperties properties = new LdapProperties(); + + @Test + void shouldUseDefaultLdapPortWhenSslIsNotEnabled() { + assertThat(createConnectionDetails().getUrls()).containsExactly("ldap://localhost:389"); + } + + @Test + void shouldUseDefaultLdapsPortWhenSslIsEnabled() { + this.properties.getSsl().setEnabled(true); + assertThat(createConnectionDetails().getUrls()).containsExactly("ldaps://localhost:636"); + } + + @Test + void shouldUseDefaultLdapsPortWhenSslBundleIsConfigured() { + this.properties.getSsl().setBundle("example"); + assertThat(createConnectionDetails().getUrls()).containsExactly("ldaps://localhost:636"); + } + + @Test + void shouldUseConfiguredUrlsRegardlessOfSsl() { + this.properties.setUrls(new String[] { "ldap://localhost:1234" }); + this.properties.getSsl().setEnabled(true); + assertThat(createConnectionDetails().getUrls()).containsExactly("ldap://localhost:1234"); + } + + private PropertiesLdapConnectionDetails createConnectionDetails() { + return new PropertiesLdapConnectionDetails(this.properties, null); + } + +} diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java index f8fdb04d440..489039a2cc4 100644 --- a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java @@ -30,13 +30,17 @@ import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.schema.Schema; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; 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.ldap.autoconfigure.LdapConnectionDetails; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.util.TestPropertyValues; @@ -58,7 +62,8 @@ import static org.assertj.core.api.Assertions.assertThat; class EmbeddedLdapAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, SslAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, LdapAutoConfiguration.class, + SslAutoConfiguration.class)); @Test void testSetDefaultPort() { @@ -124,13 +129,11 @@ class EmbeddedLdapAutoConfigurationTests { @Test @WithSchemaLdifResource void testQueryEmbeddedLdap() { - this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org") - .withConfiguration(AutoConfigurations.of(LdapAutoConfiguration.class)) - .run((context) -> { - assertThat(context).hasSingleBean(LdapTemplate.class); - LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class); - assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4); - }); + this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org").run((context) -> { + assertThat(context).hasSingleBean(LdapTemplate.class); + LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class); + assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4); + }); } @Test @@ -355,64 +358,44 @@ class EmbeddedLdapAutoConfigurationTests { } @Test - void whenSslBundleIsConfiguredLdapsListenerIsConfigured() { - List 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(); - }); + void shouldConfigureLdapsListenerWhenSslBundleIsConfigured() { + this.contextRunner.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test")) + .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 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"); - }); + void shouldConfigureLdapListenerWhenSslBundleIsConfiguredButSslIsDisabled() { + this.contextRunner + .withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.enabled:false", + "spring.ldap.embedded.ssl.bundle:test")) + .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 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"); - }); + void shouldFailWhenInvalidSslBundleIsConfigured() { + this.contextRunner + .withPropertyValues( + sslBundleProperties("spring.ldap.embedded.ssl.enabled:true", "spring.ldap.embedded.ssl.bundle:foo")) + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("foo"); + assertThat(context).getFailure().hasMessageContaining("cannot be found"); + }); } @Test - void whenSslIsEnabledWithoutAnSslBundleThenStartFails() { + void shouldFailWhenSslIsEnabledWithoutAnSslBundle() { this.contextRunner .withPropertyValues("spring.ldap.embedded.port:0", "spring.ldap.embedded.base-dn:dc=spring,dc=org", "spring.ldap.embedded.ssl.enabled:true") @@ -422,6 +405,92 @@ class EmbeddedLdapAutoConfigurationTests { }); } + @Test + @WithSchemaLdifResource + void shouldConnectOverLdapsWhenSslBundleIsConfigured() { + this.contextRunner.withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test")) + .run((context) -> { + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource.getUrls()).allMatch((url) -> url.startsWith("ldaps://")); + LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class); + assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4); + }); + } + + @Test + @WithSchemaLdifResource + void shouldIgnoreClientSslPropertiesMeantForAnotherServer() { + this.contextRunner + .withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test", + "spring.ldap.urls:ldaps://ldap.example.com:636", "spring.ldap.ssl.bundle:does-not-exist")) + .run((context) -> { + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource.getUrls()).allMatch((url) -> url.startsWith("ldaps://localhost:")); + LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class); + assertThat(ldapTemplate.list("ou=company1,c=Sweden,dc=spring,dc=org")).hasSize(4); + }); + } + + @Test + void shouldFailWhenSslBundleIsConfiguredAndSocketFactoryIsSetInBaseEnvironment() { + this.contextRunner + .withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test", + "spring.ldap.baseEnvironment.java.naming.ldap.factory.socket=com.example.MySocketFactory")) + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure() + .hasMessageContaining("Use either an SSL bundle or your own socket factory, not both"); + }); + } + + @Test + void shouldAllowSocketFactoryInBaseEnvironmentWhenContextSourceIsUserDefined() { + this.contextRunner + .withPropertyValues(sslBundleProperties("spring.ldap.embedded.ssl.bundle:test", + "spring.ldap.baseEnvironment.java.naming.ldap.factory.socket=com.example.MySocketFactory")) + .withBean("ldapContextSource", LdapContextSource.class, () -> { + LdapContextSource contextSource = new LdapContextSource(); + contextSource.setUrls(new String[] { "ldaps://localhost:636" }); + return contextSource; + }) + .run((context) -> assertThat(context).hasNotFailed()); + } + + @Test + @WithSchemaLdifResource + void shouldRegisterHintsForSchemaLdif() { + RuntimeHints runtimeHints = new RuntimeHints(); + new EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints().registerHints(runtimeHints, + Thread.currentThread().getContextClassLoader()); + assertThat(RuntimeHintsPredicates.resource().forResource("schema.ldif")).accepts(runtimeHints); + } + + @Test + void shouldApplyClientPropertiesThatTheServerDoesNotDecide() { + this.contextRunner + .withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org", "spring.ldap.referral:ignore", + "spring.ldap.anonymous-read-only:true", + "spring.ldap.baseEnvironment.java.naming.security.authentication:DIGEST-MD5") + .run((context) -> { + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource).hasFieldOrPropertyWithValue("referral", "ignore"); + assertThat(contextSource.isAnonymousReadOnly()).isTrue(); + assertThat(contextSource).extracting("anonymousEnv", InstanceOfAssertFactories.MAP) + .containsEntry("java.naming.security.authentication", "DIGEST-MD5"); + }); + } + + @Test + void shouldBackOffWhenLdapConnectionDetailsBeanIsDefined() { + this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org") + .withUserConfiguration(LdapConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).doesNotHaveBean(EmbeddedLdapConnectionDetails.class); + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource.getUrls()).containsExactly("ldap://ldap.example.com:389"); + }); + } + @Test void sslIsNotEnabledWhenBundleIsEmpty() { EmbeddedLdapProperties properties = new EmbeddedLdapProperties(); @@ -429,6 +498,32 @@ class EmbeddedLdapAutoConfigurationTests { assertThat(properties.getSsl().isEnabled()).isFalse(); } + private String[] sslBundleProperties(String... additionalProperties) { + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + List propertyValues = new ArrayList<>(); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "localhost.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.truststore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "localhost.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.key.alias=spring-boot"); + propertyValues.add("spring.ssl.bundle.jks.test.key.password=password"); + 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.addAll(List.of(additionalProperties)); + return propertyValues.toArray(String[]::new); + } + + @Configuration(proxyBeanMethods = false) + static class LdapConnectionDetailsConfiguration { + + @Bean + LdapConnectionDetails ldapConnectionDetails() { + return () -> new String[] { "ldap://ldap.example.com:389" }; + } + + } + @Configuration(proxyBeanMethods = false) static class LdapClientConfiguration { diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapConnectionDetailsTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapConnectionDetailsTests.java new file mode 100644 index 00000000000..def75d2cece --- /dev/null +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapConnectionDetailsTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.ldap.autoconfigure.embedded; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.ldap.autoconfigure.LdapProperties; +import org.springframework.boot.ldap.autoconfigure.LdapSslSocketFactory; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EmbeddedLdapConnectionDetails}. + * + * @author Moritz Halbritter + */ +class EmbeddedLdapConnectionDetailsTests { + + private final MockEnvironment environment = new MockEnvironment().withProperty("local.ldap.port", "12345"); + + private final LdapProperties properties = new LdapProperties(); + + private final EmbeddedLdapProperties embeddedProperties = new EmbeddedLdapProperties(); + + @AfterEach + void clearSslBundle() { + ReflectionTestUtils.setField(LdapSslSocketFactory.class, "sslBundle", null); + } + + @Test + void shouldUseLdapUrlWhenEmbeddedSslIsNotEnabled() { + assertThat(createConnectionDetails(null).getUrls()).containsExactly("ldap://localhost:12345"); + } + + @Test + void shouldUseLdapsUrlWhenEmbeddedSslIsEnabled() { + this.embeddedProperties.getSsl().setBundle("server"); + assertThat(createConnectionDetails(null).getUrls()).containsExactly("ldaps://localhost:12345"); + } + + @Test + void shouldIgnoreConfiguredUrls() { + this.properties.setUrls(new String[] { "ldaps://ldap.example.com" }); + assertThat(createConnectionDetails(null).getUrls()).containsExactly("ldap://localhost:12345"); + } + + @Test + void shouldUseEmbeddedCredentials() { + this.embeddedProperties.getCredential().setUsername("uid=root"); + this.embeddedProperties.getCredential().setPassword("boot"); + this.properties.setUsername("uid=other"); + this.properties.setPassword("other"); + EmbeddedLdapConnectionDetails connectionDetails = createConnectionDetails(null); + assertThat(connectionDetails.getUsername()).isEqualTo("uid=root"); + assertThat(connectionDetails.getPassword()).isEqualTo("boot"); + } + + @Test + void shouldUseServerSslBundleWhenClientBundleIsNotSet() { + SslBundle serverBundle = mock(SslBundle.class); + SslBundles sslBundles = sslBundles("server", serverBundle); + this.embeddedProperties.getSsl().setBundle("server"); + assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle); + } + + @Test + void shouldPreferServerSslBundleOverClientSslBundle() { + SslBundle serverBundle = mock(SslBundle.class); + DefaultSslBundleRegistry sslBundles = sslBundles("server", serverBundle); + sslBundles.registerBundle("client", mock(SslBundle.class)); + this.embeddedProperties.getSsl().setBundle("server"); + this.properties.getSsl().setBundle("client"); + assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle); + } + + @Test + void shouldIgnoreClientSslBundleThatDoesNotExist() { + SslBundle serverBundle = mock(SslBundle.class); + SslBundles sslBundles = sslBundles("server", serverBundle); + this.embeddedProperties.getSsl().setBundle("server"); + this.properties.getSsl().setBundle("does-not-exist"); + assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle); + } + + @Test + void shouldUseServerSslBundleWhenClientSslIsDisabled() { + SslBundle serverBundle = mock(SslBundle.class); + SslBundles sslBundles = sslBundles("server", serverBundle); + this.embeddedProperties.getSsl().setBundle("server"); + this.properties.getSsl().setEnabled(false); + assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle); + } + + @Test + void shouldNotUseSslBundleWhenEmbeddedSslIsDisabled() { + SslBundles sslBundles = sslBundles("server", mock(SslBundle.class)); + this.embeddedProperties.getSsl().setBundle("server"); + this.embeddedProperties.getSsl().setEnabled(false); + assertThat(createConnectionDetails(sslBundles).getSslBundle()).isNull(); + } + + @Test + void shouldIgnoreClientSslBundleWhenEmbeddedSslIsDisabled() { + SslBundles sslBundles = sslBundles("client", mock(SslBundle.class)); + this.properties.getSsl().setBundle("client"); + assertThat(createConnectionDetails(sslBundles).getSslBundle()).isNull(); + } + + @Test + void shouldNotTrackReloadsOfTheBundleTheEmbeddedListenerStartedWith() { + DefaultSslBundleRegistry sslBundles = sslBundles("server", mock(SslBundle.class, "original")); + this.embeddedProperties.getSsl().setBundle("server"); + createConnectionDetails(sslBundles); + sslBundles.updateBundle("server", mock(SslBundle.class, "reloaded")); + assertThat(ReflectionTestUtils.getField(LdapSslSocketFactory.class, "sslBundle")).isNull(); + } + + @Test + void shouldUseServerSslBundleWhenUrlsAreConfigured() { + SslBundle serverBundle = mock(SslBundle.class); + SslBundles sslBundles = sslBundles("server", serverBundle); + this.embeddedProperties.getSsl().setBundle("server"); + this.properties.setUrls(new String[] { "ldap://ldap.example.com" }); + assertThat(createConnectionDetails(sslBundles).getSslBundle()).isSameAs(serverBundle); + } + + @Test + void shouldNotUseSslBundleWhenClientSslIsEnabledWithoutABundle() { + this.properties.getSsl().setEnabled(true); + assertThat(createConnectionDetails(null).getSslBundle()).isNull(); + } + + private DefaultSslBundleRegistry sslBundles(String name, SslBundle bundle) { + return new DefaultSslBundleRegistry(name, bundle); + } + + private EmbeddedLdapConnectionDetails createConnectionDetails(@Nullable SslBundles sslBundles) { + return new EmbeddedLdapConnectionDetails(this.environment, this.properties, this.embeddedProperties, + sslBundles); + } + +} diff --git a/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/localhost.jks b/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/localhost.jks new file mode 100644 index 00000000000..284a37941c2 Binary files /dev/null and b/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/localhost.jks differ diff --git a/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks b/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks deleted file mode 100644 index 8413be81095..00000000000 Binary files a/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks and /dev/null differ