mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Merge pull request #51382 from dhruv9b
Closes gh-51382 * pr/51382: Polish "Add SSL bundle support to LDAP auto-configuration" Add SSL bundle support to LDAP auto-configuration
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-----
|
||||
+66
-5
@@ -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,8 +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.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;
|
||||
@@ -38,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.
|
||||
@@ -49,13 +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) {
|
||||
return new PropertiesLdapConnectionDetails(properties, environment);
|
||||
PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties, Environment environment,
|
||||
ObjectProvider<SslBundles> sslBundles) {
|
||||
return new PropertiesLdapConnectionDetails(properties, environment, sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -73,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() {
|
||||
@@ -104,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -19,6 +19,7 @@ package org.springframework.boot.ldap.autoconfigure;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
|
||||
/**
|
||||
* Details required to establish a connection to an LDAP service.
|
||||
@@ -58,4 +59,13 @@ public interface LdapConnectionDetails extends ConnectionDetails {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+49
-3
@@ -27,6 +27,7 @@ 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;
|
||||
|
||||
/**
|
||||
* Configuration properties for LDAP.
|
||||
@@ -39,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.
|
||||
*/
|
||||
@@ -78,6 +81,8 @@ public class LdapProperties {
|
||||
|
||||
private final Template template = new Template();
|
||||
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
public String @Nullable [] getUrls() {
|
||||
return this.urls;
|
||||
}
|
||||
@@ -134,20 +139,27 @@ public class LdapProperties {
|
||||
return this.template;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +211,40 @@ public class LdapProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL configuration.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
public void setBundle(@Nullable String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the methods to handle referrals.
|
||||
*/
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
}
|
||||
+36
-1
@@ -18,7 +18,11 @@ package org.springframework.boot.ldap.autoconfigure;
|
||||
|
||||
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.StringUtils;
|
||||
|
||||
/**
|
||||
* Adapts {@link LdapProperties} to {@link LdapConnectionDetails}.
|
||||
@@ -31,9 +35,27 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
PropertiesLdapConnectionDetails(LdapProperties properties, Environment environment) {
|
||||
private final @Nullable SslBundles sslBundles;
|
||||
|
||||
PropertiesLdapConnectionDetails(LdapProperties properties, Environment environment,
|
||||
@Nullable SslBundles sslBundles) {
|
||||
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
|
||||
@@ -56,4 +78,17 @@ class PropertiesLdapConnectionDetails implements LdapConnectionDetails {
|
||||
return this.properties.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SslBundle getSslBundle() {
|
||||
LdapProperties.Ssl ssl = this.properties.getSsl();
|
||||
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 SslBundle.systemDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -16,9 +16,11 @@
|
||||
|
||||
package org.springframework.boot.ldap.testcontainers;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.testcontainers.ldap.LLdapContainer;
|
||||
|
||||
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
|
||||
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
@@ -64,6 +66,11 @@ class LLdapContainerConnectionDetailsFactory
|
||||
return getContainer().getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SslBundle getSslBundle() {
|
||||
return super.getSslBundle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -20,10 +20,12 @@ import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.testcontainers.containers.Container;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
|
||||
import org.springframework.boot.ldap.autoconfigure.LdapConnectionDetails;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
|
||||
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
@@ -84,6 +86,11 @@ class OpenLdapContainerConnectionDetailsFactory
|
||||
return getContainer().getEnvMap().getOrDefault("LDAP_ADMIN_PASSWORD", "admin");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SslBundle getSslBundle() {
|
||||
return super.getSslBundle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+230
@@ -16,14 +16,26 @@
|
||||
|
||||
package org.springframework.boot.ldap.autoconfigure;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.naming.Name;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
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;
|
||||
@@ -35,8 +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.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -46,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) -> {
|
||||
@@ -209,6 +231,182 @@ class LdapAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@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) -> {
|
||||
@@ -235,6 +433,38 @@ class LdapAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ConnectionDetailsConfiguration {
|
||||
|
||||
|
||||
+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