mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Fix embedded LDAP SSL and reuse client auto-configuration
Replace the hand-rolled LdapContextSource of the embedded server with an LdapConnectionDetails implementation, so that LdapAutoConfiguration creates the context source for both the external and the embedded case. Previously, the embedded context source derived the URL scheme from spring.ldap.ssl and never applied an SSL bundle to the JNDI environment, leaving an embedded LDAPS server unreachable. It also ignored spring.ldap.anonymous-read-only, spring.ldap.referral, spring.ldap.base-environment and any DirContextAuthenticationStrategy bean, all of which now apply. The embedded server provides everything that describes a connection to it, so spring.ldap.urls, spring.ldap.username, spring.ldap.password and spring.ldap.ssl are now ignored while it is used. A spring.ldap configuration meant for a production server therefore no longer has to be unset for a test to run against the embedded server. This is a behavior change: spring.ldap.urls used to take precedence and silently pointed the client away from the embedded server. As spring.ldap.base-environment now applies to the embedded case, a socket factory set there would be replaced by the one of the SSL bundle. Startup fails instead of using either silently. LdapProperties.determineUrls has been removed. Its local.ldap.port handling only ever served the embedded case, which the embedded connection details now cover, and the default URL derivation has moved to PropertiesLdapConnectionDetails, its only caller. Closes gh-51465
This commit is contained in:
+6
-6
@@ -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> 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.
|
||||
* <p>
|
||||
* 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
|
||||
|
||||
-26
@@ -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}.
|
||||
*/
|
||||
|
||||
+15
-7
@@ -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
|
||||
|
||||
+7
-14
@@ -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> sslBundles) {
|
||||
return new EmbeddedLdapConnectionDetails(environment, properties, embeddedProperties,
|
||||
sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+114
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
-37
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+60
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+152
-57
@@ -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<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();
|
||||
});
|
||||
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<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");
|
||||
});
|
||||
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<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");
|
||||
});
|
||||
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<String> 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 {
|
||||
|
||||
|
||||
+164
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user