diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java index 2547823cda..ae32eba0be 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -17,23 +17,37 @@ package org.springframework.security.ldap.server; import static org.assertj.core.api.Assertions.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.net.ServerSocket; +import java.security.UnrecoverableKeyException; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.FileCopyUtils; /** * Useful for debugging the container by itself. * * @author Luke Taylor * @author Rob Winch + * @author Gunnar Hillert * @since 3.0 */ public class ApacheDSContainerTests { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + // SEC-2162 @Test public void failsToStartThrowsException() throws Exception { @@ -95,6 +109,96 @@ public class ApacheDSContainerTests { } } + @Test + public void startWithLdapOverSslWithoutCertificate() throws Exception { + ApacheDSContainer server = new ApacheDSContainer("dc=springframework,dc=org", + "classpath:test-server.ldif"); + List ports = getDefaultPorts(1); + server.setPort(ports.get(0)); + server.setLdapOverSslEnabled(true); + + try { + server.afterPropertiesSet(); + } + catch (IllegalArgumentException e){ + assertEquals("When LdapOverSsl is enabled, the keyStoreFile property must be set.", e.getMessage()); + return; + } + fail("Expected an IllegalArgumentException to be thrown."); + } + + @Test + public void startWithLdapOverSslWithWrongPassword() throws Exception { + final ClassPathResource keyStoreResource = new ClassPathResource("/org/springframework/security/ldap/server/spring.keystore"); + final File temporaryKeyStoreFile = new File(temporaryFolder.getRoot(), "spring.keystore"); + FileCopyUtils.copy(keyStoreResource.getInputStream(), new FileOutputStream(temporaryKeyStoreFile)); + + assertTrue(temporaryKeyStoreFile.isFile()); + + ApacheDSContainer server = new ApacheDSContainer("dc=springframework,dc=org", + "classpath:test-server.ldif"); + + List ports = getDefaultPorts(1); + server.setPort(ports.get(0)); + + server.setLdapOverSslEnabled(true); + server.setKeyStoreFile(temporaryKeyStoreFile); + server.setCertificatePassord("incorrect-password"); + + try { + server.afterPropertiesSet(); + } + catch (RuntimeException e){ + assertEquals("Server startup failed", e.getMessage()); + assertTrue("Expected an instance of 'UnrecoverableKeyException' but got " + ExceptionUtils.getRootCause(e).getClass().getName(), ExceptionUtils.getRootCause(e) instanceof UnrecoverableKeyException); + return; + } + fail("Expected a RuntimeException to be thrown."); + } + + /** + * This test starts an LDAP server using LDAPs (LDAP over SSL). A self-signed certificate is being used, which was + * previously generated with: + * + *
+	 * {@code
+	 * keytool -genkey -alias spring -keyalg RSA -keystore spring.keystore -validity 3650 -storetype JKS \
+	 * -dname "CN=localhost, OU=Spring, O=Pivotal, L=Kailua-Kona, ST=HI, C=US" -keypass spring -storepass spring
+	 * }
+	 * 
+ * @throws Exception + */ + @Test + public void startWithLdapOverSsl() throws Exception { + + final ClassPathResource keyStoreResource = new ClassPathResource("/org/springframework/security/ldap/server/spring.keystore"); + final File temporaryKeyStoreFile = new File(temporaryFolder.getRoot(), "spring.keystore"); + FileCopyUtils.copy(keyStoreResource.getInputStream(), new FileOutputStream(temporaryKeyStoreFile)); + + assertTrue(temporaryKeyStoreFile.isFile()); + + ApacheDSContainer server = new ApacheDSContainer("dc=springframework,dc=org", + "classpath:test-server.ldif"); + + List ports = getDefaultPorts(1); + server.setPort(ports.get(0)); + + server.setLdapOverSslEnabled(true); + server.setKeyStoreFile(temporaryKeyStoreFile); + server.setCertificatePassord("spring"); + + try { + server.afterPropertiesSet(); + } + finally { + try { + server.destroy(); + } + catch (Throwable t) { + } + } + } + private List getDefaultPorts(int count) throws IOException { List connections = new ArrayList(); List availablePorts = new ArrayList(count); diff --git a/ldap/src/integration-test/resources/org/springframework/security/ldap/server/spring.keystore b/ldap/src/integration-test/resources/org/springframework/security/ldap/server/spring.keystore new file mode 100644 index 0000000000..c696785da3 Binary files /dev/null and b/ldap/src/integration-test/resources/org/springframework/security/ldap/server/spring.keystore differ diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java index a0008ec4c2..57a682ea57 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -62,11 +62,12 @@ import org.springframework.util.Assert; * application context is closed to allow the bean to be disposed of and the server * shutdown prior to attempting to start it again. *

- * This class is intended for testing and internal security namespace use and is not - * considered part of framework public API. + * This class is intended for testing and internal security namespace use, only, and is not + * considered part of the framework's public API. * * @author Luke Taylor * @author Rob Winch + * @author Gunnar Hillert */ public class ApacheDSContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { @@ -84,6 +85,10 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life private final String root; private int port = 53389; + private boolean ldapOverSslEnabled; + private File keyStoreFile; + private String certificatePassord; + public ApacheDSContainer(String root, String ldifs) throws Exception { this.ldifResources = ldifs; service = new DefaultDirectoryService(); @@ -126,11 +131,21 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life setWorkingDirectory(new File(apacheWorkDir)); } + if (this.ldapOverSslEnabled && this.keyStoreFile == null) { + throw new IllegalArgumentException("When LdapOverSsl is enabled, the keyStoreFile property must be set."); + } server = new LdapServer(); server.setDirectoryService(service); // AbstractLdapIntegrationTests assume IPv4, so we specify the same here - server.setTransports(new TcpTransport(port)); + + TcpTransport transport = new TcpTransport(port); + if (ldapOverSslEnabled) { + transport.setEnableSSL(true); + server.setKeystoreFile(this.keyStoreFile.getAbsolutePath()); + server.setCertificatePassword(this.certificatePassord); + } + server.setTransports(transport); start(); } @@ -167,6 +182,35 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life this.port = port; } + /** + * If set to {@code true} will enable LDAP over SSL (LDAPs). If set to {@code true} + * {@link ApacheDSContainer#setCertificatePassord(String)} must be set as well. + * + * @param ldapOverSslEnabled If not set, will default to false + */ + public void setLdapOverSslEnabled(boolean ldapOverSslEnabled) { + this.ldapOverSslEnabled = ldapOverSslEnabled; + } + + /** + * The keyStore must not be null and must be a valid file. Will set the keyStore file on the underlying {@link LdapServer}. + * @param keyStoreFile Mandatory if LDAPs is enabled + */ + public void setKeyStoreFile(File keyStoreFile) { + Assert.notNull(keyStoreFile, "The keyStoreFile must not be null."); + Assert.isTrue(keyStoreFile.isFile(), "The keyStoreFile must be a file."); + this.keyStoreFile = keyStoreFile; + } + + /** + * Will set the certificate password on the underlying {@link LdapServer}. + * + * @param certificatePassord May be null + */ + public void setCertificatePassord(String certificatePassord) { + this.certificatePassord = certificatePassord; + } + public DefaultDirectoryService getService() { return service; }