From 5e6ee5aafb9b9f200d906444e4731cfc60ac6ebb Mon Sep 17 00:00:00 2001 From: Chris Nauroth Date: Fri, 3 Jun 2016 16:38:30 -0700 Subject: [PATCH] HADOOP-13105. Support timeouts in LDAP queries in LdapGroupsMapping. Contributed by Mingliang Liu. (cherry picked from commit d82bc8501869be78780fc09752dbf7af918c14af) --- .../hadoop/security/LdapGroupsMapping.java | 12 ++ .../src/main/resources/core-default.xml | 24 +++ .../security/TestLdapGroupsMapping.java | 140 ++++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java index 498b92e3c52..da87369bbb4 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java @@ -179,6 +179,13 @@ public class LdapGroupsMapping LDAP_CONFIG_PREFIX + ".directory.search.timeout"; public static final int DIRECTORY_SEARCH_TIMEOUT_DEFAULT = 10000; // 10s + public static final String CONNECTION_TIMEOUT = + LDAP_CONFIG_PREFIX + ".connection.timeout.ms"; + public static final int CONNECTION_TIMEOUT_DEFAULT = 60 * 1000; // 60 seconds + public static final String READ_TIMEOUT = + LDAP_CONFIG_PREFIX + ".read.timeout.ms"; + public static final int READ_TIMEOUT_DEFAULT = 60 * 1000; // 60 seconds + private static final Log LOG = LogFactory.getLog(LdapGroupsMapping.class); private static final SearchControls SEARCH_CONTROLS = new SearchControls(); @@ -432,6 +439,11 @@ public class LdapGroupsMapping env.put(Context.SECURITY_PRINCIPAL, bindUser); env.put(Context.SECURITY_CREDENTIALS, bindPassword); + env.put("com.sun.jndi.ldap.connect.timeout", conf.get(CONNECTION_TIMEOUT, + String.valueOf(CONNECTION_TIMEOUT_DEFAULT))); + env.put("com.sun.jndi.ldap.read.timeout", conf.get(READ_TIMEOUT, + String.valueOf(READ_TIMEOUT_DEFAULT))); + ctx = new InitialDirContext(env); } diff --git a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml index 3f279d65ceb..b84741a8864 100644 --- a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml +++ b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml @@ -174,6 +174,30 @@ + + hadoop.security.group.mapping.ldap.connection.timeout.ms + 60000 + + This property is the connection timeout (in milliseconds) for LDAP + operations. If the LDAP provider doesn't establish a connection within the + specified period, it will abort the connect attempt. Non-positive value + means no LDAP connection timeout is specified in which case it waits for the + connection to establish until the underlying network times out. + + + + + hadoop.security.group.mapping.ldap.read.timeout.ms + 60000 + + This property is the read timeout (in milliseconds) for LDAP + operations. If the LDAP provider doesn't get a LDAP response within the + specified period, it will abort the read attempt. Non-positive value + means no read timeout is specified in which case it waits for the response + infinitely. + + + hadoop.security.group.mapping.ldap.url diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java index 931901679f7..9f9f9943cae 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java @@ -17,8 +17,13 @@ */ package org.apache.hadoop.security; +import static org.apache.hadoop.security.LdapGroupsMapping.CONNECTION_TIMEOUT; +import static org.apache.hadoop.security.LdapGroupsMapping.READ_TIMEOUT; +import static org.apache.hadoop.test.GenericTestUtils.assertExceptionContains; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.times; @@ -29,8 +34,11 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; +import java.net.ServerSocket; +import java.net.Socket; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CountDownLatch; import javax.naming.CommunicationException; import javax.naming.NamingException; @@ -38,16 +46,38 @@ import javax.naming.directory.SearchControls; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.security.alias.CredentialProvider; import org.apache.hadoop.security.alias.CredentialProviderFactory; import org.apache.hadoop.security.alias.JavaKeyStoreProvider; import org.apache.hadoop.test.GenericTestUtils; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @SuppressWarnings("unchecked") public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase { + + private static final Logger LOG = LoggerFactory.getLogger( + TestLdapGroupsMapping.class); + + /** + * To construct a LDAP InitialDirContext object, it will firstly initiate a + * protocol session to server for authentication. After a session is + * established, a method of authentication is negotiated between the server + * and the client. When the client is authenticated, the LDAP server will send + * a bind response, whose message contents are bytes as the + * {@link #AUTHENTICATE_SUCCESS_MSG}. After receiving this bind response + * message, the LDAP context is considered connected to the server and thus + * can issue query requests for determining group membership. + */ + private static final byte[] AUTHENTICATE_SUCCESS_MSG = + {48, 12, 2, 1, 1, 97, 7, 10, 1, 0, 4, 0, 4, 0}; + @Before public void setupMocks() throws NamingException { when(getUserSearchResult().getNameInNamespace()). @@ -176,4 +206,114 @@ public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase { // extract password Assert.assertEquals("", mapping.getPassword(conf,"invalid-alias", "")); } + + /** + * Test that if the {@link LdapGroupsMapping#CONNECTION_TIMEOUT} is set in the + * configuration, the LdapGroupsMapping connection will timeout by this value + * if it does not get a LDAP response from the server. + * @throws IOException + * @throws InterruptedException + */ + @Test (timeout = 30000) + public void testLdapConnectionTimeout() + throws IOException, InterruptedException { + final int connectionTimeoutMs = 3 * 1000; // 3s + try (ServerSocket serverSock = new ServerSocket(0)) { + final CountDownLatch finLatch = new CountDownLatch(1); + + // Below we create a LDAP server which will accept a client request; + // but it will never reply to the bind (connect) request. + // Client of this LDAP server is expected to get a connection timeout. + final Thread ldapServer = new Thread(new Runnable() { + @Override + public void run() { + try { + try (Socket ignored = serverSock.accept()) { + finLatch.await(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + ldapServer.start(); + + final LdapGroupsMapping mapping = new LdapGroupsMapping(); + final Configuration conf = new Configuration(); + conf.set(LdapGroupsMapping.LDAP_URL_KEY, + "ldap://localhost:" + serverSock.getLocalPort()); + conf.setInt(CONNECTION_TIMEOUT, connectionTimeoutMs); + mapping.setConf(conf); + + try { + mapping.doGetGroups("hadoop"); + fail("The LDAP query should have timed out!"); + } catch (NamingException ne) { + LOG.debug("Got the exception while LDAP querying: ", ne); + assertExceptionContains("LDAP response read timed out, timeout used:" + + connectionTimeoutMs + "ms", ne); + assertFalse(ne.getMessage().contains("remaining name")); + } finally { + finLatch.countDown(); + } + ldapServer.join(); + } + } + + /** + * Test that if the {@link LdapGroupsMapping#READ_TIMEOUT} is set in the + * configuration, the LdapGroupsMapping query will timeout by this value if + * it does not get a LDAP response from the server. + * + * @throws IOException + * @throws InterruptedException + */ + @Test(timeout = 30000) + public void testLdapReadTimeout() throws IOException, InterruptedException { + final int readTimeoutMs = 4 * 1000; // 4s + try (ServerSocket serverSock = new ServerSocket(0)) { + final CountDownLatch finLatch = new CountDownLatch(1); + + // Below we create a LDAP server which will accept a client request, + // authenticate it successfully; but it will never reply to the following + // query request. + // Client of this LDAP server is expected to get a read timeout. + final Thread ldapServer = new Thread(new Runnable() { + @Override + public void run() { + try { + try (Socket clientSock = serverSock.accept()) { + IOUtils.skipFully(clientSock.getInputStream(), 1); + clientSock.getOutputStream().write(AUTHENTICATE_SUCCESS_MSG); + finLatch.await(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + ldapServer.start(); + + final LdapGroupsMapping mapping = new LdapGroupsMapping(); + final Configuration conf = new Configuration(); + conf.set(LdapGroupsMapping.LDAP_URL_KEY, + "ldap://localhost:" + serverSock.getLocalPort()); + conf.setInt(READ_TIMEOUT, readTimeoutMs); + mapping.setConf(conf); + + try { + mapping.doGetGroups("hadoop"); + fail("The LDAP query should have timed out!"); + } catch (NamingException ne) { + LOG.debug("Got the exception while LDAP querying: ", ne); + assertExceptionContains("LDAP response read timed out, timeout used:" + + readTimeoutMs + "ms", ne); + assertExceptionContains("remaining name", ne); + } finally { + finLatch.countDown(); + } + ldapServer.join(); + } + } + }