mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Polish "Add SSL bundle support to LDAP auto-configuration"
Configure the SSL bundle through the context source's base environment rather than a DirContextAuthenticationStrategy. The strategy is not consulted when read-only operations use an anonymous environment, which is the default when no username is set, so LDAPS connections silently used the JVM's default trust material. It was also bypassed entirely when a custom strategy bean was defined. Register an SSL bundle update handler so that reloaded key and trust material is used by subsequent connections. Target LDAPS rather than StartTLS by setting the JNDI java.naming.ldap.factory.socket property, and fail at startup if a bundle is combined with a non-ldaps URL or with a socket factory that has also been set through spring.ldap.base-environment. Delegate all socket factory methods so that a configured connect timeout is applied. Add spring.ldap.ssl.enabled to use the platform's default trust and key material without a bundle, default the URL to ldaps://localhost:636 when SSL is enabled, and register the reflection hint that JNDI needs to load the socket factory by name in a native image. Cover the result with integration tests that search over LDAPS against OpenLDAP and LLDAP containers, including certificates that are untrusted or issued to a different host. See gh-51382
This commit is contained in:
+34
@@ -703,6 +703,40 @@ Make sure to flag your customized javadoc:org.springframework.ldap.core.ContextS
|
||||
|
||||
|
||||
|
||||
[[data.nosql.ldap.ssl]]
|
||||
=== SSL
|
||||
|
||||
To connect using LDAPS, configure the xref:features/ssl.adoc[SSL bundle] to use by setting the configprop:spring.ldap.ssl.bundle[] property, and use an `ldaps://` URL in the configprop:spring.ldap.urls[] property, as shown in the following example:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
ldap:
|
||||
urls: "ldaps://myserver:636"
|
||||
ssl:
|
||||
bundle: "example"
|
||||
----
|
||||
|
||||
Setting configprop:spring.ldap.ssl.bundle[] also switches the default URL to `ldaps://localhost:636`, so configprop:spring.ldap.urls[] only has to be set when connecting to a different host or port.
|
||||
|
||||
NOTE: SSL is only supported when connecting over `ldaps://`. Enabling SSL while any of the configured URLs uses a plain `ldap://` scheme results in a startup failure.
|
||||
|
||||
These properties do not configure StartTLS, which upgrades a plain `ldap://` connection rather than using LDAPS, and which therefore cannot be combined with an SSL bundle.
|
||||
To use StartTLS, define a javadoc:org.springframework.ldap.core.support.DirContextAuthenticationStrategy[] bean such as javadoc:org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy[] and set the SSL socket factory to use on it.
|
||||
|
||||
If you need SSL support without customizing the trust and key material, set the configprop:spring.ldap.ssl.enabled[] property to `true` instead of providing a bundle.
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
|
||||
[[data.nosql.ldap.repositories]]
|
||||
=== Spring Data LDAP Repositories
|
||||
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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 java.security.cert.CertPathBuilderException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.testcontainers.service.connection.PemTrustStore;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.boot.testcontainers.service.connection.Ssl;
|
||||
import org.springframework.boot.testsupport.container.OpenLdapContainer;
|
||||
import org.springframework.boot.testsupport.container.TestImage;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.ldap.core.AttributesMapper;
|
||||
import org.springframework.ldap.core.LdapTemplate;
|
||||
import org.springframework.ldap.query.LdapQueryBuilder;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
|
||||
/**
|
||||
* Integration tests for SSL bundle support in {@link LdapAutoConfiguration}, using a real
|
||||
* OpenLDAP server configured for LDAPS.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class LdapAutoConfigurationSslIntegrationTests {
|
||||
|
||||
private static final String CERTIFICATES = "org/springframework/boot/ldap/autoconfigure/ssl/";
|
||||
|
||||
private static final String CA_CERTIFICATE = "classpath:" + CERTIFICATES + "ca.crt";
|
||||
|
||||
// Certificates issued to 'other.example.com' rather than to the host the container is
|
||||
// reached on, used to check that the hostname is verified.
|
||||
private static final String OTHER_HOST_CERTIFICATES = CERTIFICATES + "otherhost/";
|
||||
|
||||
private static final String OTHER_HOST_CA_CERTIFICATE = "classpath:" + CERTIFICATES + "otherhost/ca.crt";
|
||||
|
||||
// Without a connect timeout, the SSL handshake is performed lazily on the first
|
||||
// write, and a certificate validation failure then surfaces as a generic
|
||||
// SocketException instead of the underlying cause.
|
||||
private static final String CONNECT_TIMEOUT_PROPERTY = "spring.ldap.baseEnvironment.com.sun.jndi.ldap.connect.timeout=5000";
|
||||
|
||||
private static OpenLdapContainer openLdapContainer() {
|
||||
return openLdapContainer(CERTIFICATES);
|
||||
}
|
||||
|
||||
private static OpenLdapContainer openLdapContainer(String certificates) {
|
||||
OpenLdapContainer container = TestImage.container(OpenLdapContainer.class);
|
||||
container.addExposedPorts(636);
|
||||
return container.withEnv("LDAP_TLS_VERIFY_CLIENT", "never")
|
||||
// The image restarts slapd internally once the TLS config has been applied,
|
||||
// so the default "port is listening" wait strategy resolves too early.
|
||||
.waitingFor(Wait.forLogMessage(".*slapd starting.*\\n", 1))
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource(certificates + "server.crt"),
|
||||
"/container/service/slapd/assets/certs/ldap.crt")
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource(certificates + "server.key"),
|
||||
"/container/service/slapd/assets/certs/ldap.key")
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource(certificates + "ca.crt"),
|
||||
"/container/service/slapd/assets/certs/ca.crt");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringJUnitConfig
|
||||
@TestPropertySource(properties = CONNECT_TIMEOUT_PROPERTY)
|
||||
class WhenServerCertificateIsTrusted {
|
||||
|
||||
@Ssl
|
||||
@PemTrustStore(CA_CERTIFICATE)
|
||||
@Container
|
||||
@ServiceConnection
|
||||
static final OpenLdapContainer openLdap = openLdapContainer();
|
||||
|
||||
@Autowired
|
||||
private LdapTemplate ldapTemplate;
|
||||
|
||||
@Test
|
||||
void shouldSearchOverLdaps() {
|
||||
List<String> dc = this.ldapTemplate.search(LdapQueryBuilder.query().where("objectclass").is("dcObject"),
|
||||
(AttributesMapper<String>) (attributes) -> attributes.get("dc").get().toString());
|
||||
assertThat(dc).singleElement().isEqualTo("example");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration(LdapAutoConfiguration.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WhenServerCertificateIsNotTrusted {
|
||||
|
||||
@Container
|
||||
private final OpenLdapContainer openLdap = openLdapContainer();
|
||||
|
||||
@Test
|
||||
void shouldFailToSearchOverLdaps() {
|
||||
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(LdapAutoConfiguration.class, SslAutoConfiguration.class))
|
||||
.withPropertyValues(
|
||||
"spring.ldap.urls:ldaps://" + this.openLdap.getHost() + ":" + this.openLdap.getMappedPort(636),
|
||||
"spring.ldap.base:dc=example,dc=org", "spring.ldap.username:cn=admin,dc=example,dc=org",
|
||||
"spring.ldap.password:admin", "spring.ldap.ssl.enabled:true", CONNECT_TIMEOUT_PROPERTY);
|
||||
contextRunner.run((context) -> {
|
||||
LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class);
|
||||
assertThatException()
|
||||
.isThrownBy(() -> ldapTemplate.search(LdapQueryBuilder.query().where("objectclass").is("dcObject"),
|
||||
(AttributesMapper<String>) (attributes) -> attributes.get("dc").get().toString()))
|
||||
.withRootCauseInstanceOf(CertPathBuilderException.class);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringJUnitConfig
|
||||
@TestPropertySource(properties = CONNECT_TIMEOUT_PROPERTY)
|
||||
class WhenServerCertificateIsForADifferentHost {
|
||||
|
||||
@Ssl
|
||||
@PemTrustStore(OTHER_HOST_CA_CERTIFICATE)
|
||||
@Container
|
||||
@ServiceConnection
|
||||
static final OpenLdapContainer openLdap = openLdapContainer(OTHER_HOST_CERTIFICATES);
|
||||
|
||||
@Autowired
|
||||
private LdapTemplate ldapTemplate;
|
||||
|
||||
@Test
|
||||
void shouldFailToSearchOverLdaps() {
|
||||
// The certificate chain is trusted, so reaching the server on a host the
|
||||
// certificate has not been issued to can only fail on hostname verification.
|
||||
assertThatException()
|
||||
.isThrownBy(() -> this.ldapTemplate.search(LdapQueryBuilder.query().where("objectclass").is("dcObject"),
|
||||
(AttributesMapper<String>) (attributes) -> attributes.get("dc").get().toString()))
|
||||
.withRootCauseInstanceOf(CertificateException.class)
|
||||
.havingRootCause()
|
||||
.withMessageContaining(openLdap.getHost());
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration(LdapAutoConfiguration.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.testcontainers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.ldap.LLdapContainer;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration;
|
||||
import org.springframework.boot.testcontainers.service.connection.PemTrustStore;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.boot.testcontainers.service.connection.Ssl;
|
||||
import org.springframework.boot.testsupport.container.TestImage;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.ldap.core.AttributesMapper;
|
||||
import org.springframework.ldap.core.LdapTemplate;
|
||||
import org.springframework.ldap.query.LdapQueryBuilder;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for the SSL bundle support of {@link LLdapContainerConnectionDetailsFactory},
|
||||
* using an LLDAP container configured for LDAPS.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class LLdapContainerConnectionDetailsFactorySslIntegrationTests {
|
||||
|
||||
private static final String CERTIFICATES = "org/springframework/boot/ldap/autoconfigure/ssl/";
|
||||
|
||||
private static final int LDAPS_PORT = 6360;
|
||||
|
||||
@Ssl
|
||||
@PemTrustStore("classpath:" + CERTIFICATES + "ca.crt")
|
||||
@Container
|
||||
@ServiceConnection
|
||||
static final LLdapContainer lldap = ldapsContainer();
|
||||
|
||||
@Autowired
|
||||
private LdapTemplate ldapTemplate;
|
||||
|
||||
private static LLdapContainer ldapsContainer() {
|
||||
LLdapContainer container = TestImage.container(LLdapContainer.class);
|
||||
container.addExposedPorts(LDAPS_PORT);
|
||||
return container.withEnv("LLDAP_LDAPS_OPTIONS__ENABLED", "true")
|
||||
.withEnv("LLDAP_LDAPS_OPTIONS__CERT_FILE", "/certs/server.crt")
|
||||
.withEnv("LLDAP_LDAPS_OPTIONS__KEY_FILE", "/certs/server.key")
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource(CERTIFICATES + "server.crt"),
|
||||
"/certs/server.crt")
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource(CERTIFICATES + "server.key"),
|
||||
"/certs/server.key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSearchOverLdaps() {
|
||||
List<String> cn = this.ldapTemplate.search(LdapQueryBuilder.query().where("objectClass").is("inetOrgPerson"),
|
||||
(AttributesMapper<String>) (attributes) -> attributes.get("cn").get().toString());
|
||||
assertThat(cn).singleElement().isEqualTo("Administrator");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration(LdapAutoConfiguration.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDWTCCAkGgAwIBAgIURFIQf/P6N/1OEFidlxuqbi3DOp4wDQYJKoZIhvcNAQEL
|
||||
BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm
|
||||
aWNhdGUgQXV0aG9yaXR5MCAXDTI2MDgyNTEyNDg0MFoYDzIxMjYwODAxMTI0ODQw
|
||||
WjA7MRkwFwYDVQQKDBBTcHJpbmcgQm9vdCBUZXN0MR4wHAYDVQQDDBVDZXJ0aWZp
|
||||
Y2F0ZSBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9
|
||||
IJLbjjsaH7VWjlPcjHLOEptcdlWGIChS87Y2bmWgxsYw0W6HjulSFIRHUsI6/NsJ
|
||||
WH5lodrBaKVm23zqejxnPwzijR7x6tMnNYp8Hm2uy700h5AwMS+1tDCEOPIQympZ
|
||||
h7FlVCRLkNUjPNWLcVNwlZZlzSXb9Ql58zyp15/ytkO7W0Pv/VTXXhBMQfvwnN2Y
|
||||
TJIY7Mxq61W4Jo0JnYpZslbD1sWno4cceVXrVoRmEWYd89J844ItlUmheJdy0GIP
|
||||
xVBpM1Ihe6beAx8AG83kDOi6+KM0liUg5LXORDpfKdfhxLSghbCxkW0VDrYXdsGa
|
||||
oYeOeTsM/oHHgjQ4blH9AgMBAAGjUzBRMB0GA1UdDgQWBBTK6aBvkBA3CttNNWPd
|
||||
SA2zqC3J/DAfBgNVHSMEGDAWgBTK6aBvkBA3CttNNWPdSA2zqC3J/DAPBgNVHRMB
|
||||
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBd4FBgLscxlGxQEe0TukF4yZts
|
||||
c4e/waOjUJk0eNJTFeJTQrJCMqMKI5CKunjAJ1g4+XFKc3fxw9qNXieHjL0ekKLb
|
||||
hXLzdcA75gRQ4NZwcw4GdZnOb7WaUWq9u5FCGU1NQpMu38Mv56o005iCrV8sEpij
|
||||
ZsvwAJjFP2Fl//fafhMEsk06EnKbjpfxaB1KZ6DnBrMBeh/OokVnV9Mqom+bB48y
|
||||
U38dxoNcacAmOCckaqALrCyC8pYFWQqTOGtJGezEdDIK7xmonADjdv5qvl1o3xDV
|
||||
iRsjC7XaNAvvmzxm1E9JK66FhR22U1zsJp9c72otjjF6dpe8LNYTMm6KCB6/
|
||||
-----END CERTIFICATE-----
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDWTCCAkGgAwIBAgIUNzW2vCQbExGKXWES4vt2MTJpqfwwDQYJKoZIhvcNAQEL
|
||||
BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm
|
||||
aWNhdGUgQXV0aG9yaXR5MCAXDTI2MDgyNjA3NTcwM1oYDzIxMjYwODAyMDc1NzAz
|
||||
WjA7MRkwFwYDVQQKDBBTcHJpbmcgQm9vdCBUZXN0MR4wHAYDVQQDDBVDZXJ0aWZp
|
||||
Y2F0ZSBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCU
|
||||
xr7l1jVuX1W+p1V7NMwv5gZUEBPTTeuTh2pjsuNjdmGj9jxkWwV2WLOi2Vm9oSm7
|
||||
IrkLLCMX8xaq5kCgdN90c7bbLKawfMK3gCGvmWDgOHfRvtpDBD+FBn7ZbVQKKjj6
|
||||
aKaQoB6bI+53QJzgnK5rI6niC+OFhjJqY57H3GFqTOxMZPpul9QcVO2f1YGOR9Mw
|
||||
FkRmmi9jYcWC5Aa/VZRPpeBuyFLcR40R/TVsRT9VW1opUukXSDegSCUUS1Ac/k0i
|
||||
rgGQGFWs+VG/Eyux9nIfZwUx1tE9zMUsr2uB+J8P409sXJUVsYhug8L1qBo3urJ0
|
||||
R7Ckdkq2MrSnRoQWr+AfAgMBAAGjUzBRMB0GA1UdDgQWBBR0k8tCwN80BByqjJn5
|
||||
XRcDZRGGuzAfBgNVHSMEGDAWgBR0k8tCwN80BByqjJn5XRcDZRGGuzAPBgNVHRMB
|
||||
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAxly0TuONJZO/o1Upq6jQjEWUW
|
||||
Olq7CiFyhA2xV6gv3biSd6RqJDFPnE29hR8/JAGVWI0nJZPkso5wEFusWPp74py2
|
||||
ZT5/erGHd8fTwuRgeFrFa6O+E414xDj+7cI+QgOSdy+Ut5Lg5GrQBsJ/TeyRgrjv
|
||||
CumWJv59hhj1PAAtkkkUmK24J5V4NZBba6vCmFMERIbRSduNW0+EngqNRjZMdWg0
|
||||
V9VofFE2Sal4sIn3RZ+TOAmNMyaQ/Z7+QJs9M3AcHhq/MO/0NcHKVhnkvQUIjeeQ
|
||||
Ky1IzhKvJOK6e/tzzFXpjEhSFc8fdjbiZJHnXBzTkpKDdQemEOHG5ruOb02H
|
||||
-----END CERTIFICATE-----
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDcDCCAligAwIBAgIURShLEDUt3TkSK6K64LLC2BfOH6YwDQYJKoZIhvcNAQEL
|
||||
BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm
|
||||
aWNhdGUgQXV0aG9yaXR5MCAXDTI2MDgyNjA3NTcwM1oYDzIxMjYwODAyMDc1NzAz
|
||||
WjA3MRkwFwYDVQQKDBBTcHJpbmcgQm9vdCBUZXN0MRowGAYDVQQDDBFvdGhlci5l
|
||||
eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJXF59Ww
|
||||
lWJRjo+Pun8BqDO/QChI4twNvPgjgdGNzaUy7rbsMelmjgPVfV02iqdq3UOY/vf1
|
||||
rGnB3CYXSp5dF+C7zSGoDW3lyvVsxUYkod/CcqLteNwMSutFnDE9ujBcX0kMR7Py
|
||||
RKidK0unHf96rieIPTA0PSlzf0oodNtuKoy/zn+ug43fYXW3zG5K4IwrN8Xq2or1
|
||||
EwxzazWieVm4VUKdzUtJKNlWx88yYNG5IPgBQUzFydvjwRBG+mmDPTQvggiH4zBM
|
||||
U5VqyXwcukgC6so4YSzeJ5EzPjYICjFhp0xkXUmFtHx2PSCUXwDo6uuQEuFe6oB1
|
||||
q+kGf+fJNoChdIcCAwEAAaNuMGwwHAYDVR0RBBUwE4IRb3RoZXIuZXhhbXBsZS5j
|
||||
b20wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUohpPNd2Enq4O6nabkWQPFsEmaLAw
|
||||
HwYDVR0jBBgwFoAUdJPLQsDfNAQcqoyZ+V0XA2URhrswDQYJKoZIhvcNAQELBQAD
|
||||
ggEBABhW3pHn4a2footFrUq2aSS//1NVG4xkRIrjrQ7V4gfesErzBsY1p1iStTpX
|
||||
8GZykyTIPH2y039khsuIQoIHWYs3W3Ccu+SmkxVCZzEJVX/6AiyQoEcpX/nwzzcw
|
||||
R7aptXN2l67hO2w6UnBlrDpykUSvtpsiF0GUHfn1sTD0UVNbiW7SY/KAzol5RJ70
|
||||
oRkrJBSLyci7O4wYRD0ILp5y53uZr5Oqij0z/yRZFdlPjivfeWIWm7Q8r0oHVTmJ
|
||||
NBXhlQl2HmDXTTe9nBQLo4EphU0ypiHqCOgtNN38e8XjHCQrEJRKZlXB5py7ao85
|
||||
ecI/rvCeqS6/zEC6mtUnn0Z8kI0=
|
||||
-----END CERTIFICATE-----
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVxefVsJViUY6P
|
||||
j7p/Aagzv0AoSOLcDbz4I4HRjc2lMu627DHpZo4D1X1dNoqnat1DmP739axpwdwm
|
||||
F0qeXRfgu80hqA1t5cr1bMVGJKHfwnKi7XjcDErrRZwxPbowXF9JDEez8kSonStL
|
||||
px3/eq4niD0wND0pc39KKHTbbiqMv85/roON32F1t8xuSuCMKzfF6tqK9RMMc2s1
|
||||
onlZuFVCnc1LSSjZVsfPMmDRuSD4AUFMxcnb48EQRvppgz00L4IIh+MwTFOVasl8
|
||||
HLpIAurKOGEs3ieRMz42CAoxYadMZF1JhbR8dj0glF8A6OrrkBLhXuqAdavpBn/n
|
||||
yTaAoXSHAgMBAAECggEADP4U26kf35GROzInf+qi4fcbSuNXwRzBlQnV5Y0JlxhR
|
||||
NDXoXOwnZONZc4NoIv6jbY1eJ1OwJNR+YCEkUsYv0uNq97QRkvdpbdtSGbQGOHHK
|
||||
NzwM5savBE3blaHz/uUxShYlbWswByu4AvrfIISB8XsuFuDRO9rn4rgzuvCJnGZj
|
||||
h7nM6DzuspY5x17kY/b/V3dq0ytaVQbSMRA0wdR5UXV8uBTlydqL5fyTdpDWNBvr
|
||||
pK6gmEo64mCfqdhFCrhu64m/NUAAYmIoUn5mEzboy1b/j7+nTqzXs8T/2phA6XkE
|
||||
SFoEKVJnTmC8Gxmg/1MyWUg2N9dsqZow5paR/LXbcQKBgQDTpSDGYwZpWS6EXAlt
|
||||
R3dvNsjIHMvg62qsZehjPQq9oZIRy0bvwQ1KnZQWJpNZO7dRX1YkWe5KpesXG209
|
||||
bH3C+L/56H/g6d9Oxn+6jvV9xq2CZJR+PJo9ia1HonMF4l00+4MF1xrR4TYheriq
|
||||
X/rYr5tfU+Kn2pFi9IjkNb1zUwKBgQC1KVAXrKEAM81CWACw5tpcEmdwmuA8JPsX
|
||||
eFozRXSD/9dPd6aICtE33GI6v7QS6OFRiqp4HOmFoEEH2P65YcD7fft7KowXNRyV
|
||||
7oijW75fRxh5N4b2MIN2MJ5kjj4snunYbvC8zACzrF96t5/duesRUoUVaU7IbXd3
|
||||
zVMQT+enfQKBgA4UhkGshNitXjLgDKCAiKmVc3YctFOaVdZyUSuI/BXxkc4tP+Lj
|
||||
bQbxxNBUcKkXF/MJ7KkucnP05db7tCDJ/vPhqKPvm9JSvNB41DaNHDfp59Es79QW
|
||||
JAzEBAsn+48AqELGGMlirh4YZYmEqJRtHjscM7H72rBgEhwLOLA2AwyRAoGBAKcD
|
||||
jk41HTz1hN8MCJ8ORPMrGGfssJtiMIZjsyyfdJqYy+P8V/AFoPpR19F3mMjyH3+w
|
||||
ShZv+S3zHuDgQX2gzGxOqgDTqdFfapojDZ8k3m4yOjLOaUMgWKImkm/73v/+BXI+
|
||||
XPyBSohaeq/FB8I/O6J9pCmoKzSGDuTIyzI3qEZ1AoGAOEMIJ0ZXzSTo4jA27ur2
|
||||
YNspFDlUyzNEDzJ1z6om4dYJr1onO6gQeMuQsUz7BgewLn8nw3d/KzWHOiPgZ6VL
|
||||
LP45n4GLMWNxYTprmv2cbyEvT5w0W3hcukMBbMCM7bM4skwvwVWDccsgE4WDQk97
|
||||
wbIo2v1Z9TajCQjv6ir0w7c=
|
||||
-----END PRIVATE KEY-----
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDYzCCAkugAwIBAgIUa/9qbWfUPcuXtVnmhGEIpzPRro4wDQYJKoZIhvcNAQEL
|
||||
BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm
|
||||
aWNhdGUgQXV0aG9yaXR5MCAXDTI2MDgyNTEyNDg0MFoYDzIxMjYwODAxMTI0ODQw
|
||||
WjAvMRkwFwYDVQQKDBBTcHJpbmcgQm9vdCBUZXN0MRIwEAYDVQQDDAlsb2NhbGhv
|
||||
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMnHAogOOlrX3H8YCU
|
||||
9U9ubEFhJIiABCMA7itTkXCxshsziqooNFPW037MVUCEvqfXtkHy0GJXjaYLyyHj
|
||||
6NQJJs1NYaC05okEn5f0f6+lxVNSOk9TjHxEbXdOMTZAOiAmwsRXTReb185jMOxq
|
||||
xUlLSY3ehot8geB4SjGNLQOKg+mCznt9f21SGi7o8TaNbTElDWqWIlCVDQ3AYsrm
|
||||
RRRSXY9DIdpgHwRrnzYoS57+m7BjYUyx0kgamT8FehK4fa+BhIGoIvLAsWp1nd15
|
||||
2YgQBrg82zDemT2zf5PwnOrRqTiJUubJEotjeOII5mqMdyKnrlpZ41FcK2oRFwvt
|
||||
KDdtAgMBAAGjaTBnMAkGA1UdEwQCMAAwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/
|
||||
AAABMB0GA1UdDgQWBBTIoMeEZxAks2Q5D319xxUMln2noDAfBgNVHSMEGDAWgBTK
|
||||
6aBvkBA3CttNNWPdSA2zqC3J/DANBgkqhkiG9w0BAQsFAAOCAQEAostmB27rtPXi
|
||||
0hX46UiQ84tcr0ViwK7TT4BHYr0ZntkOj1hKjpfjMORN1ANTYBLGpSsSM2xLBB2g
|
||||
VXPLCo3BGU1rjxQ7rI3OKZ910pfwf2uvX6LDLRYjKjVnr6pElS3jSVw/nVvYRMf8
|
||||
acjP7KBgGSLZyqizccSWYTsCYy+V56C4IJTanqf1sHRTqLsICcnwIpoSGwWBv9cG
|
||||
kFsFoWE+tPwDGn7QuejJ+57U6m23E1vaDNwvi/xrsNy6tfVYS7OF+DzuDR8mHab/
|
||||
Qvx0qzJkpeYziaObn43ocnd3vn5vhf54UYZkK5eieRnXubfsIYA1u2igIdIAVRk7
|
||||
NPRHOlFk+g==
|
||||
-----END CERTIFICATE-----
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMnHAogOOlrX3H
|
||||
8YCU9U9ubEFhJIiABCMA7itTkXCxshsziqooNFPW037MVUCEvqfXtkHy0GJXjaYL
|
||||
yyHj6NQJJs1NYaC05okEn5f0f6+lxVNSOk9TjHxEbXdOMTZAOiAmwsRXTReb185j
|
||||
MOxqxUlLSY3ehot8geB4SjGNLQOKg+mCznt9f21SGi7o8TaNbTElDWqWIlCVDQ3A
|
||||
YsrmRRRSXY9DIdpgHwRrnzYoS57+m7BjYUyx0kgamT8FehK4fa+BhIGoIvLAsWp1
|
||||
nd152YgQBrg82zDemT2zf5PwnOrRqTiJUubJEotjeOII5mqMdyKnrlpZ41FcK2oR
|
||||
FwvtKDdtAgMBAAECggEAJZnEBLV24C3rNPCdBAK2l6C+PQdVLE1WT2f1P1S1ZHpK
|
||||
BGVBksTW+V4BabRq+Fn/ByWfpesEf3lw367zKepR+OKQDN3ZhhTtbhOT/aLvWOPT
|
||||
xYMgq8Zf2cVSDOT/RrtHyEe3hCGPKpz0eQb3E0HxD6xTLWcFQBqv8PsVcw+oHG6A
|
||||
CFFMWDYAYGbyIpwXpqPVT1YYxOc1taq+Zr2UAN4bK9hFmNv19bAqasCPWveStFiS
|
||||
oNH4cdyW5Jbss6ZVXeDRZ7NyGpARGLxNJvAKCuQglgIsn7oV2Z3niBX/pGSs3yXI
|
||||
ryP+pL3iESZPEjGkTrRy+HxWURI/fiOtLKUK/p0vMQKBgQD11Fx/KZC+k6ZSVvMD
|
||||
nsppLZM5vqSEz4FJqmTHgSF0A2zX+gqM9w7ckhA/hi6PWFzTWBqGXI5FQ2tjKcZc
|
||||
APQLcS1BrRTa2ZZK8bmVi0X1GJnWG1mqLcYolyv2X9BULhEXM5biEvM0o46pps9s
|
||||
PVZGR0vlvcENm3JjUgL2zs3qaQKBgQDVE4XAI/iIc+vUzMhyaLt18hyxW3No7r3a
|
||||
3MKYxlTH0G9MX9AQ2RzoPf6vIx6REenRUBdxJu/XaQBHd291oC8Hq5xIqefUJFY6
|
||||
hCdCi4IJw9SvgCJshRsIJP+giH3pztj0br/RNoqqd4IPTisMdVoyKFiz/Mjm4vPQ
|
||||
hoLIQKVcZQKBgQCfuamX3hP0H89TdLdVRNlTWY9tV5dhy8m/aX0tu5NW49rWwdoV
|
||||
GXmIi0cPX0nlTY6Sq44gewbdrh6aQcxmfyASRykWoTUJtZLXgxQIPIPp6mAaI48w
|
||||
6aj1OrQP0tVzvLLSFm0U2yc2robFaGMhewERjMWdsps7EiNSsTjH0Dsu8QKBgE/H
|
||||
YXrRfQrKLHCexCZiJF5C6o9VaF7PlPJPWeUNzUyLNEDqVuMYB4TFQYido6I5jMw8
|
||||
KqrJI2AKBnq7s1XdRf6fOILUauK5QDfkpzZ5OZUiZ43Wcg0jwT1JbRTURiC2u/C5
|
||||
TsSsoTq2SFImuGOPxNem1598dFROgW+ADZOg8MkpAoGAVOEKtRblYtHBpBI1itvy
|
||||
3AOIeaExQ49TLv9/fTJW9pY4j/47getq7RklU2uJ7FyV6ORxfaoQPJwSyTM47dNW
|
||||
PPo0sbiw2iGlzcoG0A7Yf7/iffuvFj9qPZEzZNczBBmwE273zmgEN/royqKEeDSV
|
||||
Czgn7Jeanqq0SBLGiJ9TPog=
|
||||
-----END PRIVATE KEY-----
|
||||
+64
-25
@@ -16,9 +16,17 @@
|
||||
|
||||
package org.springframework.boot.ldap.autoconfigure;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
@@ -27,11 +35,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration.LdapAutoConfigurationRuntimeHints;
|
||||
import org.springframework.boot.ldap.autoconfigure.LdapProperties.Template;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
|
||||
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;
|
||||
@@ -41,6 +50,8 @@ import org.springframework.ldap.core.support.DirContextAuthenticationStrategy;
|
||||
import org.springframework.ldap.core.support.LdapContextSource;
|
||||
import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
|
||||
import org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for LDAP.
|
||||
@@ -52,16 +63,16 @@ import org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper;
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(ContextSource.class)
|
||||
@EnableConfigurationProperties(LdapProperties.class)
|
||||
@ImportRuntimeHints(LdapAutoConfigurationRuntimeHints.class)
|
||||
public final class LdapAutoConfiguration {
|
||||
|
||||
private static final String SOCKET_FACTORY_ENV_KEY = "java.naming.ldap.factory.socket";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LdapConnectionDetails.class)
|
||||
PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(
|
||||
LdapProperties properties,
|
||||
Environment environment,
|
||||
PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties, Environment environment,
|
||||
ObjectProvider<SslBundles> sslBundles) {
|
||||
return new PropertiesLdapConnectionDetails(
|
||||
properties, environment, sslBundles.getIfAvailable());
|
||||
return new PropertiesLdapConnectionDetails(properties, environment, sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -69,23 +80,7 @@ public final class LdapAutoConfiguration {
|
||||
LdapContextSource ldapContextSource(LdapConnectionDetails connectionDetails, LdapProperties properties,
|
||||
ObjectProvider<DirContextAuthenticationStrategy> dirContextAuthenticationStrategy) {
|
||||
LdapContextSource source = new LdapContextSource();
|
||||
DirContextAuthenticationStrategy uniqueStrategy = dirContextAuthenticationStrategy.getIfUnique();
|
||||
if (uniqueStrategy != null) {
|
||||
// Exactly one custom strategy bean → use it
|
||||
source.setAuthenticationStrategy(uniqueStrategy);
|
||||
}
|
||||
else if (!dirContextAuthenticationStrategy.stream().findAny().isPresent()) {
|
||||
// No custom strategy beans at all → apply SSL bundle if configured
|
||||
SslBundle sslBundle = connectionDetails.getSslBundle();
|
||||
if (sslBundle != null) {
|
||||
DefaultTlsDirContextAuthenticationStrategy tlsStrategy =
|
||||
new DefaultTlsDirContextAuthenticationStrategy();
|
||||
tlsStrategy.setSslSocketFactory(sslBundle.createSslContext().getSocketFactory());
|
||||
source.setAuthenticationStrategy(tlsStrategy);
|
||||
}
|
||||
// else: no SSL bundle → retain built-in SimpleDirContextAuthenticationStrategy from constructor
|
||||
}
|
||||
// else: multiple custom strategy beans → retain built-in SimpleDirContextAuthenticationStrategy
|
||||
dirContextAuthenticationStrategy.ifUnique(source::setAuthenticationStrategy);
|
||||
PropertyMapper propertyMapper = PropertyMapper.get();
|
||||
propertyMapper.from(connectionDetails.getUsername()).to(source::setUserDn);
|
||||
propertyMapper.from(connectionDetails.getPassword()).to(source::setPassword);
|
||||
@@ -95,11 +90,46 @@ public final class LdapAutoConfiguration {
|
||||
.to(source::setReferral);
|
||||
propertyMapper.from(connectionDetails.getBase()).to(source::setBase);
|
||||
propertyMapper.from(connectionDetails.getUrls()).to(source::setUrls);
|
||||
propertyMapper.from(properties.getBaseEnvironment())
|
||||
.to((baseEnvironment) -> source.setBaseEnvironmentProperties(Collections.unmodifiableMap(baseEnvironment)));
|
||||
source.setBaseEnvironmentProperties(baseEnvironmentProperties(connectionDetails, properties));
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JNDI environment shared by the anonymous and the authenticated
|
||||
* environment of the context source. The SSL bundle is applied here rather than
|
||||
* 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.
|
||||
* @param connectionDetails the connection details
|
||||
* @param properties the LDAP properties
|
||||
* @return the base environment properties
|
||||
*/
|
||||
private Map<String, Object> baseEnvironmentProperties(LdapConnectionDetails connectionDetails,
|
||||
LdapProperties properties) {
|
||||
Map<String, Object> baseEnvironment = new LinkedHashMap<>(properties.getBaseEnvironment());
|
||||
SslBundle sslBundle = connectionDetails.getSslBundle();
|
||||
if (sslBundle != null) {
|
||||
Assert.state(usesLdaps(connectionDetails.getUrls()),
|
||||
"SSL bundle has been configured but not all LDAP URLs use the 'ldaps' scheme");
|
||||
if (connectionDetails instanceof PropertiesLdapConnectionDetails) {
|
||||
Assert.state(!baseEnvironment.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");
|
||||
}
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle);
|
||||
baseEnvironment.put(SOCKET_FACTORY_ENV_KEY, LdapSslSocketFactory.class.getName());
|
||||
}
|
||||
return Collections.unmodifiableMap(baseEnvironment);
|
||||
}
|
||||
|
||||
private boolean usesLdaps(String[] urls) {
|
||||
return !ObjectUtils.isEmpty(urls)
|
||||
&& Arrays.stream(urls).allMatch((url) -> url.toLowerCase(Locale.ROOT).startsWith("ldaps://"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ObjectDirectoryMapper objectDirectoryMapper() {
|
||||
@@ -126,4 +156,13 @@ public final class LdapAutoConfiguration {
|
||||
return ldapTemplate;
|
||||
}
|
||||
|
||||
static class LdapAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
hints.reflection().registerType(LdapSslSocketFactory.class, MemberCategory.INVOKE_PUBLIC_METHODS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ public interface LdapConnectionDetails extends ConnectionDetails {
|
||||
/**
|
||||
* SSL bundle to use to establish the LDAP connection.
|
||||
* @return the SSL bundle to use, or {@code null} if none
|
||||
* @since 4.2.0
|
||||
*/
|
||||
default @Nullable SslBundle getSslBundle() {
|
||||
return null;
|
||||
|
||||
+22
-12
@@ -40,6 +40,8 @@ public class LdapProperties {
|
||||
|
||||
private static final int DEFAULT_PORT = 389;
|
||||
|
||||
private static final int DEFAULT_SSL_PORT = 636;
|
||||
|
||||
/**
|
||||
* LDAP URLs of the server.
|
||||
*/
|
||||
@@ -143,18 +145,21 @@ public class LdapProperties {
|
||||
|
||||
public String[] determineUrls(Environment environment) {
|
||||
if (ObjectUtils.isEmpty(this.urls)) {
|
||||
return new String[] { "ldap://localhost:" + determinePort(environment) };
|
||||
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) {
|
||||
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 DEFAULT_PORT;
|
||||
return defaultPort;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,11 +216,25 @@ public class LdapProperties {
|
||||
*/
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* Whether to enable SSL support. Enabled automatically if "bundle" is provided
|
||||
* unless specified otherwise.
|
||||
*/
|
||||
private @Nullable Boolean enabled;
|
||||
|
||||
/**
|
||||
* SSL bundle name.
|
||||
*/
|
||||
private @Nullable String bundle;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return (this.enabled != null) ? this.enabled : StringUtils.hasText(this.bundle);
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public @Nullable String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
@@ -224,15 +243,6 @@ public class LdapProperties {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether SSL is enabled. SSL is considered enabled if a bundle name
|
||||
* has been set.
|
||||
* @return whether SSL is enabled
|
||||
*/
|
||||
public boolean determineEnabled() {
|
||||
return StringUtils.hasText(this.bundle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Bridges the JNDI {@code java.naming.ldap.factory.socket} environment property to an
|
||||
* {@link SslBundle}. That property only accepts a class name, which JNDI loads and on
|
||||
* which it then calls {@link #getDefault()}, with no way of passing an actual factory
|
||||
* instance. The bundle to use is therefore held in a static field.
|
||||
* <p>
|
||||
* JNDI calls {@link #getDefault()} once per new connection and the {@link SslBundle} is
|
||||
* asked for a fresh {@code SSLContext} each time, so key and trust material of a bundle
|
||||
* that has been reloaded is picked up by connections opened from then on. Reloading is
|
||||
* only tracked for a bundle configured by name through {@code spring.ldap.ssl.bundle}; a
|
||||
* bundle supplied by a custom {@link LdapConnectionDetails} is used as given.
|
||||
* <p>
|
||||
* As the bundle is held statically, a single {@link SslBundle} applies JVM-wide. This is
|
||||
* sufficient for the auto-configured
|
||||
* {@link org.springframework.ldap.core.support.LdapContextSource}, of which there is at
|
||||
* most one per application context.
|
||||
* <p>
|
||||
* This class is referenced by name from a JNDI environment and is not intended to be used
|
||||
* directly.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public final class LdapSslSocketFactory extends SSLSocketFactory {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(LdapSslSocketFactory.class);
|
||||
|
||||
private static volatile @Nullable SslBundle sslBundle;
|
||||
|
||||
private final SSLSocketFactory delegate;
|
||||
|
||||
private LdapSslSocketFactory(SslBundle sslBundle) {
|
||||
this.delegate = sslBundle.createSslContext().getSocketFactory();
|
||||
}
|
||||
|
||||
public static SocketFactory getDefault() {
|
||||
SslBundle sslBundle = LdapSslSocketFactory.sslBundle;
|
||||
Assert.state(sslBundle != null, "No SSL bundle has been set");
|
||||
return new LdapSslSocketFactory(sslBundle);
|
||||
}
|
||||
|
||||
static void setSslBundle(@Nullable SslBundle sslBundle) {
|
||||
SslBundle previous = LdapSslSocketFactory.sslBundle;
|
||||
if (previous != null && sslBundle != null && previous != sslBundle) {
|
||||
logger.warn("A different SSL bundle has already been set for LDAP. As the bundle applies JVM-wide, "
|
||||
+ "LDAPS connections opened from now on use the key and trust material of the new bundle, "
|
||||
+ "including connections from context sources that were configured with the previous one");
|
||||
}
|
||||
LdapSslSocketFactory.sslBundle = sslBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the bundle with a reloaded version of itself. Unlike
|
||||
* {@link #setSslBundle(SslBundle)} this does not warn, as the material is being
|
||||
* reloaded for the same bundle rather than claimed by another context source.
|
||||
* @param sslBundle the reloaded SSL bundle
|
||||
*/
|
||||
static void updateSslBundle(SslBundle sslBundle) {
|
||||
LdapSslSocketFactory.sslBundle = sslBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return this.delegate.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return this.delegate.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
|
||||
return this.delegate.createSocket(socket, host, port, autoClose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket socket, InputStream consumed, boolean autoClose) throws IOException {
|
||||
return this.delegate.createSocket(socket, consumed, autoClose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unconnected socket. JNDI tries this first when a connect timeout has
|
||||
* been configured, so it has to be delegated rather than inheriting
|
||||
* {@link javax.net.SocketFactory#createSocket()}, which throws and makes JNDI fall
|
||||
* back to a connected socket that ignores the timeout.
|
||||
* @return an unconnected socket
|
||||
* @throws IOException if the socket cannot be created
|
||||
*/
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return this.delegate.createSocket();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return this.delegate.createSocket(host, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return this.delegate.createSocket(host, port, localAddress, localPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return this.delegate.createSocket(host, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
|
||||
throws IOException {
|
||||
return this.delegate.createSocket(address, port, localAddress, localPort);
|
||||
}
|
||||
|
||||
}
|
||||
+16
-2
@@ -42,6 +42,20 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
|
||||
this.properties = properties;
|
||||
this.environment = environment;
|
||||
this.sslBundles = sslBundles;
|
||||
registerSslBundleUpdateHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps {@link LdapSslSocketFactory} up-to-date when the configured bundle is
|
||||
* reloaded. Without this, the bundle resolved when the context source was created is
|
||||
* held on to and reloaded key or trust material is never used.
|
||||
*/
|
||||
private void registerSslBundleUpdateHandler() {
|
||||
LdapProperties.Ssl ssl = this.properties.getSsl();
|
||||
if (this.sslBundles == null || !ssl.isEnabled() || !StringUtils.hasLength(ssl.getBundle())) {
|
||||
return;
|
||||
}
|
||||
this.sslBundles.addBundleUpdateHandler(ssl.getBundle(), LdapSslSocketFactory::updateSslBundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,14 +81,14 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
|
||||
@Override
|
||||
public @Nullable SslBundle getSslBundle() {
|
||||
LdapProperties.Ssl ssl = this.properties.getSsl();
|
||||
if (!ssl.determineEnabled()) {
|
||||
if (!ssl.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.hasLength(ssl.getBundle())) {
|
||||
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
|
||||
return this.sslBundles.getBundle(ssl.getBundle());
|
||||
}
|
||||
return null;
|
||||
return SslBundle.systemDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-1
@@ -17,7 +17,6 @@
|
||||
package org.springframework.boot.ldap.testcontainers;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.testcontainers.ldap.LLdapContainer;
|
||||
|
||||
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
|
||||
|
||||
-1
@@ -21,7 +21,6 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.testcontainers.containers.Container;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
|
||||
|
||||
+233
-75
@@ -16,15 +16,26 @@
|
||||
|
||||
package org.springframework.boot.ldap.autoconfigure;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.naming.Name;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.assertj.core.api.MapAssert;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
@@ -36,13 +47,10 @@ import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
|
||||
import org.springframework.ldap.pool2.factory.PoolConfig;
|
||||
import org.springframework.ldap.pool2.factory.PooledContextSource;
|
||||
import org.springframework.ldap.support.LdapUtils;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -52,11 +60,19 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Stephane Nicoll
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class LdapAutoConfigurationTests {
|
||||
|
||||
private static final String SOCKET_FACTORY_ENV_KEY = "java.naming.ldap.factory.socket";
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(LdapAutoConfiguration.class));
|
||||
|
||||
@AfterEach
|
||||
void clearSslBundle() {
|
||||
LdapSslSocketFactory.setSslBundle(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextSourceWithDefaultUrl() {
|
||||
this.contextRunner.run((context) -> {
|
||||
@@ -205,55 +221,192 @@ class LdapAutoConfigurationTests {
|
||||
assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreSizeLimitExceededException", false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextSourceWithSslBundleAndCustomDirContextAuthenticationStrategyUsesCustomStrategy() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
when(sslBundles.getBundle("test")).thenReturn(sslBundle);
|
||||
|
||||
DirContextAuthenticationStrategy customStrategy = mock(DirContextAuthenticationStrategy.class);
|
||||
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.withBean(DirContextAuthenticationStrategy.class, () -> customStrategy)
|
||||
.run((context) -> {
|
||||
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
|
||||
assertThat(contextSource).extracting("authenticationStrategy").isSameAs(customStrategy);
|
||||
});
|
||||
}
|
||||
@Test
|
||||
void contextSourceWithSslBundleUsesDefaultTlsAuthenticationStrategy() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SSLContext sslContext = mock(SSLContext.class);
|
||||
SSLSocketFactory socketFactory = mock(SSLSocketFactory.class);
|
||||
|
||||
when(sslBundle.createSslContext()).thenReturn(sslContext);
|
||||
when(sslContext.getSocketFactory()).thenReturn(socketFactory);
|
||||
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
when(sslBundles.getBundle("test")).thenReturn(sslBundle);
|
||||
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> {
|
||||
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
|
||||
assertThat(contextSource).extracting("authenticationStrategy")
|
||||
.isInstanceOf(DefaultTlsDirContextAuthenticationStrategy.class)
|
||||
.extracting("sslSocketFactory")
|
||||
.isSameAs(socketFactory);
|
||||
});
|
||||
}
|
||||
@Test
|
||||
void contextSourceWithoutSslBundleDoesNotConfigureAuthenticationStrategy() {
|
||||
this.contextRunner.run((context) -> {
|
||||
void contextSourceWithUserProvidedPooledContextSource() {
|
||||
this.contextRunner.withUserConfiguration(PooledContextSourceConfig.class).run((context) -> {
|
||||
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
|
||||
assertThat(contextSource).extracting("authenticationStrategy")
|
||||
.isInstanceOf(SimpleDirContextAuthenticationStrategy.class);
|
||||
assertThat(contextSource.getUrls()).containsExactly("ldap://localhost:389");
|
||||
assertThat(contextSource.isAnonymousReadOnly()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseSslSocketFactoryWhenSslBundleConfiguredWithLdapsUrl() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
given(sslBundles.getBundle("test")).willReturn(sslBundle);
|
||||
this.contextRunner.withPropertyValues("spring.ldap.urls=ldaps://localhost:636", "spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> {
|
||||
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
|
||||
assertThat(contextSource.isAnonymousReadOnly()).isTrue();
|
||||
assertThatAnonymousEnv(context).containsEntry(SOCKET_FACTORY_ENV_KEY,
|
||||
LdapSslSocketFactory.class.getName());
|
||||
assertThat(ReflectionTestUtils.getField(LdapSslSocketFactory.class, "sslBundle")).isSameAs(sslBundle);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseSslSocketFactoryWhenSslBundleConfiguredAndReadOnlyOperationsAreAuthenticated() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
given(sslBundles.getBundle("test")).willReturn(sslBundle);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.urls=ldaps://localhost:636", "spring.ldap.ssl.bundle=test",
|
||||
"spring.ldap.username=root", "spring.ldap.password=secret")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> {
|
||||
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
|
||||
assertThat(contextSource.isAnonymousReadOnly()).isFalse();
|
||||
assertThatAuthenticatedEnv(contextSource).containsEntry(SOCKET_FACTORY_ENV_KEY,
|
||||
LdapSslSocketFactory.class.getName());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseSslSocketFactoryWhenSslBundleConfiguredWithUppercaseLdapsUrl() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
given(sslBundles.getBundle("test")).willReturn(sslBundle);
|
||||
this.contextRunner.withPropertyValues("spring.ldap.urls=LDAPS://localhost:636", "spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> assertThatAnonymousEnv(context).containsEntry(SOCKET_FACTORY_ENV_KEY,
|
||||
LdapSslSocketFactory.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepCustomBaseEnvironmentWhenSslBundleConfigured() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
given(sslBundles.getBundle("test")).willReturn(sslBundle);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.urls=ldaps://localhost:636", "spring.ldap.ssl.bundle=test",
|
||||
"spring.ldap.baseEnvironment.java.naming.security.authentication:DIGEST-MD5")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> assertThatAnonymousEnv(context)
|
||||
.containsEntry(SOCKET_FACTORY_ENV_KEY, LdapSslSocketFactory.class.getName())
|
||||
.containsEntry("java.naming.security.authentication", "DIGEST-MD5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenSslBundleConfiguredWithoutLdapsUrl() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
given(sslBundles.getBundle("test")).willReturn(sslBundle);
|
||||
this.contextRunner.withPropertyValues("spring.ldap.urls=ldap://localhost:389", "spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context).getFailure()
|
||||
.hasRootCauseInstanceOf(IllegalStateException.class)
|
||||
.hasRootCauseMessage("SSL bundle has been configured but not all LDAP URLs use the 'ldaps' scheme");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenSslBundleConfiguredWithMixedSchemeUrls() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
given(sslBundles.getBundle("test")).willReturn(sslBundle);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.urls=ldaps://localhost:636,ldap://mycompany:389",
|
||||
"spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> assertThat(context).getFailure()
|
||||
.hasRootCauseInstanceOf(IllegalStateException.class)
|
||||
.hasRootCauseMessage("SSL bundle has been configured but not all LDAP URLs use the 'ldaps' scheme"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseReloadedSslBundleWhenBundleIsUpdated(CapturedOutput output) {
|
||||
SslBundle original = mock(SslBundle.class, "original");
|
||||
SslBundle reloaded = mock(SslBundle.class, "reloaded");
|
||||
DefaultSslBundleRegistry sslBundles = new DefaultSslBundleRegistry("test", original);
|
||||
this.contextRunner.withPropertyValues("spring.ldap.urls=ldaps://localhost:636", "spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> {
|
||||
assertThat(ReflectionTestUtils.getField(LdapSslSocketFactory.class, "sslBundle")).isSameAs(original);
|
||||
sslBundles.updateBundle("test", reloaded);
|
||||
assertThat(ReflectionTestUtils.getField(LdapSslSocketFactory.class, "sslBundle")).isSameAs(reloaded);
|
||||
assertThat(output).doesNotContain("A different SSL bundle has already been set for LDAP")
|
||||
.doesNotContain("doesn't support SSL reloading");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRegisterSslBundleUpdateHandlerWhenSslDisabled() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
DefaultSslBundleRegistry sslBundles = new DefaultSslBundleRegistry("test", sslBundle);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.ssl.bundle=test", "spring.ldap.ssl.enabled=false",
|
||||
"spring.ldap.urls=ldap://localhost:389")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> {
|
||||
sslBundles.updateBundle("test", mock(SslBundle.class));
|
||||
assertThat(ReflectionTestUtils.getField(LdapSslSocketFactory.class, "sslBundle")).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenSslBundleConfiguredAndSocketFactorySetInBaseEnvironment() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
given(sslBundles.getBundle("test")).willReturn(sslBundle);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.urls=ldaps://localhost:636", "spring.ldap.ssl.bundle=test",
|
||||
"spring.ldap.baseEnvironment." + SOCKET_FACTORY_ENV_KEY + "=com.example.MySocketFactory")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> assertThat(context).getFailure()
|
||||
.hasRootCauseInstanceOf(IllegalStateException.class)
|
||||
.rootCause()
|
||||
.hasMessageContaining("Use either an SSL bundle or your own socket factory, not both"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOverrideSocketFactorySetInBaseEnvironmentWhenSslBundleComesFromConnectionDetails() {
|
||||
this.contextRunner
|
||||
.withPropertyValues(
|
||||
"spring.ldap.baseEnvironment." + SOCKET_FACTORY_ENV_KEY + "=com.example.MySocketFactory")
|
||||
.withUserConfiguration(SslBundleConnectionDetailsConfiguration.class)
|
||||
.run((context) -> assertThatAnonymousEnv(context).containsEntry(SOCKET_FACTORY_ENV_KEY,
|
||||
LdapSslSocketFactory.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepSocketFactorySetInBaseEnvironmentWhenNoSslBundleConfigured() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.urls=ldaps://localhost:636",
|
||||
"spring.ldap.baseEnvironment." + SOCKET_FACTORY_ENV_KEY + "=com.example.MySocketFactory")
|
||||
.run((context) -> assertThatAnonymousEnv(context).containsEntry(SOCKET_FACTORY_ENV_KEY,
|
||||
"com.example.MySocketFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseSslSocketFactoryWhenSslEnabledAndNoBundleConfigured() {
|
||||
this.contextRunner.withPropertyValues("spring.ldap.urls=ldaps://localhost:636", "spring.ldap.ssl.enabled=true")
|
||||
.run((context) -> {
|
||||
assertThatAnonymousEnv(context).containsEntry(SOCKET_FACTORY_ENV_KEY,
|
||||
LdapSslSocketFactory.class.getName());
|
||||
assertThat(context.getBean(PropertiesLdapConnectionDetails.class).getSslBundle()).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotUseSslSocketFactoryWhenSslNotConfigured() {
|
||||
this.contextRunner.run((context) -> assertThatAnonymousEnv(context).doesNotContainKey(SOCKET_FACTORY_ENV_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotUseSslSocketFactoryWhenSslDisabledButBundleConfigured() {
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.ssl.bundle=test", "spring.ldap.ssl.enabled=false",
|
||||
"spring.ldap.urls=ldap://localhost:389")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.run((context) -> assertThatAnonymousEnv(context).doesNotContainKey(SOCKET_FACTORY_ENV_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextSourceWithCustomUniqueDirContextAuthenticationStrategy() {
|
||||
this.contextRunner.withUserConfiguration(CustomDirContextAuthenticationStrategy.class).run((context) -> {
|
||||
@@ -280,26 +433,36 @@ class LdapAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextSourceWithSslBundleAndMultipleCustomStrategiesUsesDefault() {
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
SslBundles sslBundles = mock(SslBundles.class);
|
||||
when(sslBundles.getBundle("test")).thenReturn(sslBundle);
|
||||
private MapAssert<Object, Object> assertThatAnonymousEnv(ApplicationContext context) {
|
||||
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
|
||||
return assertThat(contextSource).extracting("anonymousEnv", InstanceOfAssertFactories.MAP);
|
||||
}
|
||||
|
||||
private MapAssert<Object, Object> assertThatAuthenticatedEnv(LdapContextSource contextSource) {
|
||||
@Nullable Map<Object, Object> env = ReflectionTestUtils.invokeMethod(contextSource, "getAuthenticatedEnv", "root",
|
||||
"secret");
|
||||
return assertThat(env);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class SslBundleConnectionDetailsConfiguration {
|
||||
|
||||
@Bean
|
||||
LdapConnectionDetails ldapConnectionDetails() {
|
||||
return new LdapConnectionDetails() {
|
||||
|
||||
@Override
|
||||
public String[] getUrls() {
|
||||
return new String[] { "ldaps://ldap.example.com" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslBundle getSslBundle() {
|
||||
return mock(SslBundle.class);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ldap.ssl.bundle=test")
|
||||
.withBean(SslBundles.class, () -> sslBundles)
|
||||
.withUserConfiguration(CustomDirContextAuthenticationStrategy.class,
|
||||
AnotherCustomDirContextAuthenticationStrategy.class)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasBean("customDirContextAuthenticationStrategy")
|
||||
.hasBean("anotherCustomDirContextAuthenticationStrategy");
|
||||
LdapContextSource contextSource = context.getBean(LdapContextSource.class);
|
||||
assertThat(contextSource).extracting("authenticationStrategy")
|
||||
.isNotSameAs(context.getBean("customDirContextAuthenticationStrategy"))
|
||||
.isNotSameAs(context.getBean("anotherCustomDirContextAuthenticationStrategy"))
|
||||
.isInstanceOf(SimpleDirContextAuthenticationStrategy.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -328,11 +491,6 @@ class LdapAutoConfigurationTests {
|
||||
public String getPassword() {
|
||||
return "ldap-password";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SslBundle getSslBundle() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+37
@@ -20,6 +20,7 @@ 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;
|
||||
|
||||
@@ -42,4 +43,40 @@ 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
/**
|
||||
* Tests for {@link LdapSslSocketFactory}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class LdapSslSocketFactoryTests {
|
||||
|
||||
@AfterEach
|
||||
void clearSslBundle() {
|
||||
LdapSslSocketFactory.setSslBundle(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenNoSslBundleHasBeenSet() {
|
||||
assertThatIllegalStateException().isThrownBy(LdapSslSocketFactory::getDefault)
|
||||
.withMessage("No SSL bundle has been set");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseSocketFactoryFromSslBundle() {
|
||||
SSLSocketFactory socketFactory = mock(SSLSocketFactory.class);
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle(socketFactory));
|
||||
assertThat(LdapSslSocketFactory.getDefault()).extracting("delegate").isSameAs(socketFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateCreationOfUnconnectedSocket() throws Exception {
|
||||
SSLSocketFactory socketFactory = mock(SSLSocketFactory.class);
|
||||
Socket socket = mock(Socket.class);
|
||||
given(socketFactory.createSocket()).willReturn(socket);
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle(socketFactory));
|
||||
assertThat(LdapSslSocketFactory.getDefault().createSocket()).isSameAs(socket);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateCreationOfSocketWithConsumedInput() throws Exception {
|
||||
SSLSocketFactory socketFactory = mock(SSLSocketFactory.class);
|
||||
Socket socket = mock(Socket.class);
|
||||
Socket wrapped = mock(Socket.class);
|
||||
InputStream consumed = InputStream.nullInputStream();
|
||||
given(socketFactory.createSocket(socket, consumed, true)).willReturn(wrapped);
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle(socketFactory));
|
||||
assertThat(((SSLSocketFactory) LdapSslSocketFactory.getDefault()).createSocket(socket, consumed, true))
|
||||
.isSameAs(wrapped);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWarnWhenReplacingBundleWithADifferentOne(CapturedOutput output) {
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle(mock(SSLSocketFactory.class)));
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle(mock(SSLSocketFactory.class)));
|
||||
assertThat(output).contains("A different SSL bundle has already been set for LDAP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWarnWhenSettingTheSameBundleAgain(CapturedOutput output) {
|
||||
SslBundle sslBundle = sslBundle(mock(SSLSocketFactory.class));
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle);
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle);
|
||||
assertThat(output).doesNotContain("A different SSL bundle has already been set for LDAP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateSslContextForEachInvocationToPickUpReloadedMaterial() {
|
||||
SslBundle sslBundle = sslBundle(mock(SSLSocketFactory.class));
|
||||
LdapSslSocketFactory.setSslBundle(sslBundle);
|
||||
LdapSslSocketFactory.getDefault();
|
||||
LdapSslSocketFactory.getDefault();
|
||||
then(sslBundle).should(times(2)).createSslContext();
|
||||
}
|
||||
|
||||
private SslBundle sslBundle(SSLSocketFactory socketFactory) {
|
||||
SSLContext sslContext = mock(SSLContext.class);
|
||||
given(sslContext.getSocketFactory()).willReturn(socketFactory);
|
||||
SslBundle sslBundle = mock(SslBundle.class);
|
||||
given(sslBundle.createSslContext()).willReturn(sslContext);
|
||||
return sslBundle;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user