diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java index 51338a9babf..9c6e2bae855 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java @@ -442,14 +442,20 @@ private PasswordCompareConfigurer() { * embedded LDAP instance. * * @author Rob Winch + * @author Evgeniy Cheban * @since 3.2 */ public final class ContextSourceBuilder { + private static final String APACHEDS_CLASSNAME = "org.apache.directory.server.core.DefaultDirectoryService"; + private static final String UNBOUNDID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer"; + + private static final int DEFAULT_PORT = 33389; + private static final int RANDOM_PORT = 0; + private String ldif = "classpath*:*.ldif"; private String managerPassword; private String managerDn; private Integer port; - private static final int DEFAULT_PORT = 33389; private String root = "dc=springframework,dc=org"; private String url; @@ -540,6 +546,10 @@ public LdapAuthenticationProviderConfigurer and() { } private DefaultSpringSecurityContextSource build() throws Exception { + if (this.url == null) { + startEmbeddedLdapServer(); + } + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource( getProviderUrl()); if (managerDn != null) { @@ -551,26 +561,29 @@ private DefaultSpringSecurityContextSource build() throws Exception { contextSource.setPassword(managerPassword); } contextSource = postProcess(contextSource); - if (url != null) { - return contextSource; - } - if (ClassUtils.isPresent("org.apache.directory.server.core.DefaultDirectoryService", getClass().getClassLoader())) { - ApacheDSContainer apacheDsContainer = new ApacheDSContainer(root, ldif); + return contextSource; + } + + private void startEmbeddedLdapServer() throws Exception { + if (ClassUtils.isPresent(APACHEDS_CLASSNAME, getClass().getClassLoader())) { + ApacheDSContainer apacheDsContainer = new ApacheDSContainer(this.root, this.ldif); apacheDsContainer.setPort(getPort()); postProcess(apacheDsContainer); + this.port = apacheDsContainer.getLocalPort(); } - else if (ClassUtils.isPresent("com.unboundid.ldap.listener.InMemoryDirectoryServer", getClass().getClassLoader())) { - UnboundIdContainer unboundIdContainer = new UnboundIdContainer(root, ldif); + else if (ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif); unboundIdContainer.setPort(getPort()); postProcess(unboundIdContainer); + this.port = unboundIdContainer.getPort(); + } + else { + throw new IllegalStateException("Embedded LDAP server is not provided"); } - return contextSource; } private int getPort() { - if (port != null && port == 0) { - port = getRandomPort(); - } else if (port == null) { + if (port == null) { port = getDefaultPort(); } return port; @@ -580,15 +593,7 @@ private int getDefaultPort() { try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { return serverSocket.getLocalPort(); } catch (IOException e) { - return getRandomPort(); - } - } - - private int getRandomPort() { - try (ServerSocket serverSocket = new ServerSocket(0)) { - return serverSocket.getLocalPort(); - } catch (IOException e) { - return DEFAULT_PORT; + return RANDOM_PORT; } } diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java index 993cfa08e96..cbdda20de6f 100644 --- a/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java @@ -18,17 +18,19 @@ import java.io.IOException; import java.net.ServerSocket; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.security.config.BeanIds; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; import org.springframework.security.ldap.server.ApacheDSContainer; import org.springframework.security.ldap.server.UnboundIdContainer; import org.springframework.util.ClassUtils; @@ -37,12 +39,11 @@ /** * @author Luke Taylor * @author EddĂș MelĂ©ndez + * @author Evgeniy Cheban */ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser { private static final String CONTEXT_SOURCE_CLASS = "org.springframework.security.ldap.DefaultSpringSecurityContextSource"; - private final Log logger = LogFactory.getLog(getClass()); - /** * Defines the Url of the ldap server to use. If not specified, an embedded apache DS * instance will be created @@ -66,8 +67,8 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser { /** Defines the port the LDAP_PROVIDER server should run on */ public static final String ATT_PORT = "port"; + private static final String RANDOM_PORT = "0"; private static final int DEFAULT_PORT = 33389; - public static final String OPT_DEFAULT_PORT = String.valueOf(DEFAULT_PORT); private static final String APACHEDS_CLASSNAME = "org.apache.directory.server.core.DefaultDirectoryService"; private static final String UNBOUNID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer"; @@ -136,28 +137,22 @@ private RootBeanDefinition createEmbeddedServer(Element element, suffix = OPT_DEFAULT_ROOT_SUFFIX; } - String port = element.getAttribute(ATT_PORT); - - if ("0".equals(port)) { - port = getRandomPort(); - if (logger.isDebugEnabled()) { - logger.debug("Using default port of " + port); - } - } else if (!StringUtils.hasText(port)) { - port = getDefaultPort(); - if (logger.isDebugEnabled()) { - logger.debug("Using default port of " + port); - } - } - - String url = "ldap://127.0.0.1:" + port + "/" + suffix; - BeanDefinitionBuilder contextSource = BeanDefinitionBuilder .rootBeanDefinition(CONTEXT_SOURCE_CLASS); - contextSource.addConstructorArgValue(url); + contextSource.addConstructorArgValue(suffix); contextSource.addPropertyValue("userDn", "uid=admin,ou=system"); contextSource.addPropertyValue("password", "secret"); + BeanDefinition embeddedLdapServerConfigBean = BeanDefinitionBuilder + .rootBeanDefinition(EmbeddedLdapServerConfigBean.class).getBeanDefinition(); + String embeddedLdapServerConfigBeanName = parserContext.getReaderContext() + .generateBeanName(embeddedLdapServerConfigBean); + + parserContext.registerBeanComponent(new BeanComponentDefinition(embeddedLdapServerConfigBean, + embeddedLdapServerConfigBeanName)); + + contextSource.setFactoryMethodOnBean("createEmbeddedContextSource", embeddedLdapServerConfigBeanName); + String mode = element.getAttribute("mode"); RootBeanDefinition ldapContainer = getRootBeanDefinition(mode); ldapContainer.setSource(source); @@ -169,9 +164,7 @@ private RootBeanDefinition createEmbeddedServer(Element element, } ldapContainer.getConstructorArgumentValues().addGenericArgumentValue(ldifs); - ldapContainer.getPropertyValues().addPropertyValue("port", port); - - logger.info("Embedded LDAP server bean definition created for URL: " + url); + ldapContainer.getPropertyValues().addPropertyValue("port", getPort(element)); if (parserContext.getRegistry() .containsBeanDefinition(BeanIds.EMBEDDED_APACHE_DS) || @@ -217,19 +210,46 @@ private boolean isUnboundidEnabled(String mode) { return "unboundid".equals(mode) || ClassUtils.isPresent(UNBOUNID_CLASSNAME, getClass().getClassLoader()); } + private String getPort(Element element) { + String port = element.getAttribute(ATT_PORT); + return (StringUtils.hasText(port) ? port : getDefaultPort()); + } + private String getDefaultPort() { try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { return String.valueOf(serverSocket.getLocalPort()); } catch (IOException e) { - return getRandomPort(); + return RANDOM_PORT; } } - private String getRandomPort() { - try (ServerSocket serverSocket = new ServerSocket(0)) { - return String.valueOf(serverSocket.getLocalPort()); - } catch (IOException e) { - return String.valueOf(DEFAULT_PORT); + private static class EmbeddedLdapServerConfigBean implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @SuppressWarnings("unused") + private DefaultSpringSecurityContextSource createEmbeddedContextSource(String suffix) { + int port; + if (ClassUtils.isPresent(APACHEDS_CLASSNAME, getClass().getClassLoader())) { + ApacheDSContainer apacheDSContainer = this.applicationContext.getBean(ApacheDSContainer.class); + port = apacheDSContainer.getLocalPort(); + } + else if (ClassUtils.isPresent(UNBOUNID_CLASSNAME, getClass().getClassLoader())) { + UnboundIdContainer unboundIdContainer = this.applicationContext.getBean(UnboundIdContainer.class); + port = unboundIdContainer.getPort(); + } + else { + throw new IllegalStateException("Embedded LDAP server is not provided"); + } + + String providerUrl = "ldap://127.0.0.1:" + port + "/" + suffix; + + return new DefaultSpringSecurityContextSource(providerUrl); } } } 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 552410af8f2..add74ddf451 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-2016 the original author or authors. + * Copyright 2002-2020 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. @@ -39,6 +39,7 @@ * @author Luke Taylor * @author Rob Winch * @author Gunnar Hillert + * @author Evgeniy Cheban * @since 3.0 */ public class ApacheDSContainerTests { @@ -212,4 +213,20 @@ private List getDefaultPorts(int count) throws IOException { } } } + + @Test + public void afterPropertiesSetWhenPortIsZeroThenRandomPortIsSelected() throws Exception { + ApacheDSContainer server = new ApacheDSContainer("dc=springframework,dc=org", + "classpath:test-server.ldif"); + server.setPort(0); + try { + server.afterPropertiesSet(); + + assertThat(server.getPort()).isEqualTo(0); + assertThat(server.getLocalPort()).isNotEqualTo(0); + } + finally { + server.destroy(); + } + } } 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 9aad1749755..f02dd51edc8 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-2016 the original author or authors. + * Copyright 2002-2020 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. @@ -15,6 +15,7 @@ */ package org.springframework.security.ldap.server; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -39,6 +40,7 @@ import org.apache.directory.server.protocol.shared.transport.TcpTransport; import org.apache.directory.shared.ldap.exception.LdapNameNotFoundException; import org.apache.directory.shared.ldap.name.LdapDN; +import org.apache.mina.transport.socket.SocketAcceptor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; @@ -69,6 +71,7 @@ * @author Luke Taylor * @author Rob Winch * @author Gunnar Hillert + * @author Evgeniy Cheban * @deprecated Use {@link UnboundIdContainer} instead because ApacheDS 1.x is no longer * supported with no GA version to replace it. */ @@ -80,6 +83,7 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life final DefaultDirectoryService service; LdapServer server; + private TcpTransport transport; private ApplicationContext ctxt; private File workingDir; @@ -88,6 +92,7 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life private final JdbmPartition partition; private final String root; private int port = 53389; + private int localPort; private boolean ldapOverSslEnabled; private File keyStoreFile; @@ -143,7 +148,7 @@ public void afterPropertiesSet() throws Exception { server.setDirectoryService(service); // AbstractLdapIntegrationTests assume IPv4, so we specify the same here - TcpTransport transport = new TcpTransport(port); + this.transport = new TcpTransport(port); if (ldapOverSslEnabled) { transport.setEnableSSL(true); server.setKeystoreFile(this.keyStoreFile.getAbsolutePath()); @@ -190,6 +195,15 @@ public int getPort() { return this.port; } + /** + * Returns the port that is resolved by {@link TcpTransport}. + * + * @return the port that is resolved by {@link TcpTransport} + */ + public int getLocalPort() { + return this.localPort; + } + /** * 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. @@ -262,6 +276,10 @@ public void start() { logger.error("Lookup failed", e); } + SocketAcceptor socketAcceptor = this.server.getSocketAcceptor(this.transport); + InetSocketAddress localAddress = socketAcceptor.getLocalAddress(); + this.localPort = localAddress.getPort(); + running = true; try {