HADOOP-13105. Support timeouts in LDAP queries in LdapGroupsMapping. Contributed by Mingliang Liu.

This commit is contained in:
Chris Nauroth 2016-06-03 16:38:30 -07:00
parent 78b3a03831
commit d82bc85018
3 changed files with 176 additions and 0 deletions

View File

@ -179,6 +179,13 @@ public class LdapGroupsMapping
LDAP_CONFIG_PREFIX + ".directory.search.timeout"; LDAP_CONFIG_PREFIX + ".directory.search.timeout";
public static final int DIRECTORY_SEARCH_TIMEOUT_DEFAULT = 10000; // 10s 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 Log LOG = LogFactory.getLog(LdapGroupsMapping.class);
private static final SearchControls SEARCH_CONTROLS = new SearchControls(); 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_PRINCIPAL, bindUser);
env.put(Context.SECURITY_CREDENTIALS, bindPassword); 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); ctx = new InitialDirContext(env);
} }

View File

@ -165,6 +165,30 @@
</description> </description>
</property> </property>
<property>
<name>hadoop.security.group.mapping.ldap.connection.timeout.ms</name>
<value>60000</value>
<description>
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.
</description>
</property>
<property>
<name>hadoop.security.group.mapping.ldap.read.timeout.ms</name>
<value>60000</value>
<description>
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.
</description>
</property>
<property> <property>
<name>hadoop.security.group.mapping.ldap.url</name> <name>hadoop.security.group.mapping.ldap.url</name>
<value></value> <value></value>

View File

@ -17,8 +17,13 @@
*/ */
package org.apache.hadoop.security; 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.assertArrayEquals;
import static org.junit.Assert.assertEquals; 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.any;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -29,8 +34,11 @@ import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch;
import javax.naming.CommunicationException; import javax.naming.CommunicationException;
import javax.naming.NamingException; import javax.naming.NamingException;
@ -38,16 +46,38 @@ import javax.naming.directory.SearchControls;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path; 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.CredentialProvider;
import org.apache.hadoop.security.alias.CredentialProviderFactory; import org.apache.hadoop.security.alias.CredentialProviderFactory;
import org.apache.hadoop.security.alias.JavaKeyStoreProvider; import org.apache.hadoop.security.alias.JavaKeyStoreProvider;
import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.test.GenericTestUtils;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase { 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 @Before
public void setupMocks() throws NamingException { public void setupMocks() throws NamingException {
when(getUserSearchResult().getNameInNamespace()). when(getUserSearchResult().getNameInNamespace()).
@ -176,4 +206,114 @@ public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase {
// extract password // extract password
Assert.assertEquals("", mapping.getPassword(conf,"invalid-alias", "")); 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();
}
}
} }