diff --git a/ldap/ldap.gradle b/ldap/ldap.gradle index 4555b36c37..a02d6934ff 100644 --- a/ldap/ldap.gradle +++ b/ldap/ldap.gradle @@ -13,7 +13,8 @@ dependencies { "org.apache.directory.server:apacheds-protocol-ldap:$apacheDsVersion", "org.apache.directory.server:apacheds-server-jndi:$apacheDsVersion", 'org.apache.directory.shared:shared-ldap:0.9.15', - 'ldapsdk:ldapsdk:4.1' + 'ldapsdk:ldapsdk:4.1', + 'com.unboundid:unboundid-ldapsdk:4.0.8' compile ("org.springframework.ldap:spring-ldap-core:$springLdapVersion") { exclude(group: 'commons-logging', module: 'commons-logging') @@ -28,7 +29,8 @@ dependencies { } integrationTest { - include('**/ApacheDSServerIntegrationTests.class', '**/ApacheDSEmbeddedLdifTests.class') + include('**/ApacheDSServerIntegrationTests.class', '**/ApacheDSEmbeddedLdifTests.class', + '**/LdapUserDetailsManagerModifyPasswordTests.class') // exclude('**/OpenLDAPIntegrationTestSuite.class') maxParallelForks = 1 } diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/server/UnboundIdContainerTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/server/UnboundIdContainerTests.java new file mode 100644 index 0000000000..a36d7273fa --- /dev/null +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/server/UnboundIdContainerTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.security.ldap.server; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Eddú Meléndez + */ +public class UnboundIdContainerTests { + + @Test + public void startLdapServer() throws IOException { + UnboundIdContainer server = new UnboundIdContainer("dc=springframework,dc=org", + "classpath:test-server.ldif"); + List ports = getDefaultPorts(1); + server.setPort(ports.get(0)); + + try { + server.afterPropertiesSet(); + fail("Expected a RuntimeException to be thrown."); + } catch (Exception ex) { + assertThat(ex).hasMessage("Server startup failed"); + } + } + + @Test + public void afterPropertiesSetWhenPortIsZeroThenRandomPortIsSelected() throws Exception { + UnboundIdContainer server = new UnboundIdContainer("dc=springframework,dc=org", null); + server.setPort(0); + + try { + server.afterPropertiesSet(); + assertThat(server.getPort()).isNotEqualTo(0); + } finally { + server.destroy(); + } + } + + private List getDefaultPorts(int count) throws IOException { + List connections = new ArrayList(); + List availablePorts = new ArrayList(count); + try { + for (int i = 0; i < count; i++) { + ServerSocket socket = new ServerSocket(0); + connections.add(socket); + availablePorts.add(socket.getLocalPort()); + } + return availablePorts; + } finally { + for (ServerSocket conn : connections) { + conn.close(); + } + } + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java new file mode 100644 index 0000000000..78ae4c256c --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.security.ldap.server; + +import java.io.IOException; +import java.io.InputStream; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldap.sdk.DN; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldif.LDIFReader; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.Lifecycle; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +/** + * @author Eddú Meléndez + */ +public class UnboundIdContainer implements InitializingBean, DisposableBean, Lifecycle, + ApplicationContextAware { + + private InMemoryDirectoryServer directoryServer; + + private String defaultPartitionSuffix; + + private int port = 53389; + + private ApplicationContext context; + + private boolean running; + + private String ldif; + + public UnboundIdContainer(String defaultPartitionSuffix, String ldif) { + this.defaultPartitionSuffix = defaultPartitionSuffix; + this.ldif = ldif; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + @Override + public void destroy() throws Exception { + stop(); + } + + @Override + public void afterPropertiesSet() throws Exception { + start(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } + + @Override + public void start() { + if (isRunning()) { + return; + } + + try { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(this.defaultPartitionSuffix); + config.addAdditionalBindCredentials("uid=admin,ou=system", "secret"); + + config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", this.port)); + config.setEnforceSingleStructuralObjectClass(false); + config.setEnforceAttributeSyntaxCompliance(true); + + DN dn = new DN(this.defaultPartitionSuffix); + Entry entry = new Entry(dn); + entry.addAttribute("objectClass", "top", "domain", "extensibleObject"); + entry.addAttribute("dc", dn.getRDN().getAttributeValues()[0]); + + InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config); + directoryServer.add(entry); + importLdif(directoryServer); + directoryServer.startListening(); + this.port = directoryServer.getListenPort(); + this.directoryServer = directoryServer; + this.running = true; + } catch (LDAPException ex) { + throw new RuntimeException("Server startup failed", ex); + } + + } + + private void importLdif(InMemoryDirectoryServer directoryServer) { + if (StringUtils.hasText(this.ldif)) { + try { + Resource resource = this.context.getResource(this.ldif); + if (resource.exists()) { + InputStream inputStream = null; + try { + inputStream = resource.getInputStream(); + directoryServer.importFromLDIF(false, new LDIFReader(inputStream)); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ex) { + throw new IllegalStateException("Unable to load LDIF " + this.ldif, ex); + } + } + } + } + } catch (Exception ex) { + throw new IllegalStateException("Unable to load LDIF " + this.ldif, ex); + } + } + } + + @Override + public void stop() { + this.directoryServer.shutDown(true); + } + + @Override + public boolean isRunning() { + return this.running; + } +}