Jelajahi Sumber

Allow port=0 for ApacheDSContainer

Fixes gh-8144
Evgeniy Cheban 5 tahun lalu
induk
melakukan
0fa339f75b

+ 26 - 21
config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java

@@ -442,14 +442,20 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
 	 * 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 class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
 		}
 
 		private DefaultSpringSecurityContextSource build() throws Exception {
+			if (this.url == null) {
+				startEmbeddedLdapServer();
+			}
+
 			DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
 					getProviderUrl());
 			if (managerDn != null) {
@@ -551,26 +561,29 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
 				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 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
 			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;
 			}
 		}
 

+ 51 - 31
config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java

@@ -18,17 +18,19 @@ package org.springframework.security.config.ldap;
 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 @@ import org.springframework.util.StringUtils;
 /**
  * @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 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
 			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 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
 		}
 
 		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 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
 		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);
 		}
 	}
 }

+ 18 - 1
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 @@ import org.springframework.util.FileCopyUtils;
  * @author Luke Taylor
  * @author Rob Winch
  * @author Gunnar Hillert
+ * @author Evgeniy Cheban
  * @since 3.0
  */
 public class ApacheDSContainerTests {
@@ -212,4 +213,20 @@ public class ApacheDSContainerTests {
 			}
 		}
 	}
+
+	@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();
+		}
+	}
 }

+ 20 - 2
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.store.LdifFileLoader;
 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 @@ import org.springframework.util.Assert;
  * @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 class ApacheDSContainer implements InitializingBean, DisposableBean, Life
 		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 class ApacheDSContainer implements InitializingBean, DisposableBean, Life
 		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 class ApacheDSContainer implements InitializingBean, DisposableBean, Life
 			logger.error("Lookup failed", e);
 		}
 
+		SocketAcceptor socketAcceptor = this.server.getSocketAcceptor(this.transport);
+		InetSocketAddress localAddress = socketAcceptor.getLocalAddress();
+		this.localPort = localAddress.getPort();
+
 		running = true;
 
 		try {