LdapUserSearch rebind with bind DN after user bind (elastic/x-pack-elasticsearch#4209)

Fixes an inconsistency bug in which `LdapSession`s built by
`LdapUserSearchSessionFactory` are different if the factory is
configured to use a connection pool or not. The bind status of the
connection, or the connection(s) from the pool, passed through to
the newly minted `LdapSession` are now identical. Connections are
bind to the bind_dn configuration entry in the realm config.

Original commit: elastic/x-pack-elasticsearch@094af063ea
This commit is contained in:
Albert Zaharovits 2018-03-28 09:36:02 +03:00 committed by GitHub
parent d1ed4e0bff
commit b7515f03cf
6 changed files with 124 additions and 56 deletions

View File

@ -285,7 +285,7 @@ class ActiveDirectorySessionFactory extends PoolingSessionFactory {
ActionListener<LdapSession> listener) { ActionListener<LdapSession> listener) {
final byte[] passwordBytes = CharArrays.toUtf8Bytes(password.getChars()); final byte[] passwordBytes = CharArrays.toUtf8Bytes(password.getChars());
final SimpleBindRequest bind = new SimpleBindRequest(bindUsername(username), passwordBytes); final SimpleBindRequest bind = new SimpleBindRequest(bindUsername(username), passwordBytes);
LdapUtils.maybeForkThenBind(pool, bind, threadPool, new ActionRunnable<LdapSession>(listener) { LdapUtils.maybeForkThenBindAndRevert(pool, bind, threadPool, new ActionRunnable<LdapSession>(listener) {
@Override @Override
protected void doRun() throws Exception { protected void doRun() throws Exception {
searchForDN(pool, username, password, Math.toIntExact(timeout.seconds()), ActionListener.wrap((entry) -> { searchForDN(pool, username, password, Math.toIntExact(timeout.seconds()), ActionListener.wrap((entry) -> {

View File

@ -90,18 +90,13 @@ class LdapUserSearchSessionFactory extends PoolingSessionFactory {
} else { } else {
final String dn = entry.getDN(); final String dn = entry.getDN();
final byte[] passwordBytes = CharArrays.toUtf8Bytes(password.getChars()); final byte[] passwordBytes = CharArrays.toUtf8Bytes(password.getChars());
SimpleBindRequest bind = new SimpleBindRequest(dn, passwordBytes); final SimpleBindRequest bind = new SimpleBindRequest(dn, passwordBytes);
LdapUtils.maybeForkThenBind(connectionPool, bind, threadPool, new ActionRunnable<LdapSession>(listener) { LdapUtils.maybeForkThenBindAndRevert(connectionPool, bind, threadPool, new ActionRunnable<LdapSession>(listener) {
@Override @Override
protected void doRun() throws Exception { protected void doRun() throws Exception {
listener.onResponse(new LdapSession(logger, config, connectionPool, dn, groupResolver, metaDataResolver, timeout, listener.onResponse(new LdapSession(logger, config, connectionPool, dn, groupResolver, metaDataResolver, timeout,
entry.getAttributes())); entry.getAttributes()));
} }
@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
}); });
} }
}, listener::onFailure)); }, listener::onFailure));
@ -138,8 +133,20 @@ class LdapUserSearchSessionFactory extends PoolingSessionFactory {
LdapUtils.maybeForkThenBind(connection, userBind, threadPool, new AbstractRunnable() { LdapUtils.maybeForkThenBind(connection, userBind, threadPool, new AbstractRunnable() {
@Override @Override
protected void doRun() throws Exception { protected void doRun() throws Exception {
listener.onResponse(new LdapSession(logger, config, connection, dn, groupResolver, metaDataResolver, LdapUtils.maybeForkThenBind(connection, bind, threadPool, new AbstractRunnable() {
timeout, entry.getAttributes()));
@Override
protected void doRun() throws Exception {
listener.onResponse(new LdapSession(logger, config, connection, dn, groupResolver,
metaDataResolver, timeout, entry.getAttributes()));
}
@Override
public void onFailure(Exception e) {
IOUtils.closeWhileHandlingException(connection);
listener.onFailure(e);
}
});
} }
@Override @Override

View File

@ -88,22 +88,35 @@ public final class LdapUtils {
} }
/** /**
* This method submits the {@code bind} request over the provided {@code ldap} * If necessary, fork before executing the runnable. A deadlock will happen if
* connection or connection pool. The connection authentication status changes, * the same thread which handles bind responses blocks on the bind call, waiting
* see {@code LDAPConnection#bind}; in case of a connection pool the bind * for the response which he itself should handle.
* authentication status is reverted, so that the connection can be safely */
* returned to the pool, see: private static void maybeForkAndRun(ThreadPool threadPool, Runnable runnable) {
* {@code LDAPConnectionPool#bindAndRevertAuthentication}. if (isLdapConnectionThread(Thread.currentThread())) {
// only fork if binding on the LDAPConnectionReader thread
threadPool.executor(ThreadPool.Names.GENERIC).execute(runnable);
} else {
// avoids repeated forking
runnable.run();
}
}
/**
* This method submits the {@code bind} request over one connection from the
* pool. The bind authentication is then reverted and the connection is returned
* to the pool, so that the connection can be safely reused, see
* {@code LDAPConnectionPool#bindAndRevertAuthentication}. This validates the
* bind credentials.
* *
* Bind calls are blocking and if a bind is executed on the LDAP Connection * Bind calls are blocking and if a bind is executed on the LDAP Connection
* Reader thread (as returned by {@code LdapUtils#isLdapConnectionThread}), * Reader thread (as returned by {@code LdapUtils#isLdapConnectionThread}), the
* the thread will be blocked until it is interrupted by something else * thread will be blocked until it is interrupted by something else such as a
* such as a timeout timer. * timeout timer. <b>Do not call bind</b> outside this method or
* <b>Do not call bind</b> outside of this method. * {@link LdapUtils#maybeForkThenBind(LDAPConnection, BindRequest, ThreadPool, AbstractRunnable)}
* *
* @param ldap * @param ldapPool
* The LDAP connection or connection pool on which to submit the bind * The LDAP connection pool on which to submit the bind operation.
* operation.
* @param bind * @param bind
* The request object of the bind operation. * The request object of the bind operation.
* @param threadPool * @param threadPool
@ -114,19 +127,13 @@ public final class LdapUtils {
* The runnable that continues the program flow after the bind * The runnable that continues the program flow after the bind
* operation. It is executed on the same thread as the prior bind. * operation. It is executed on the same thread as the prior bind.
*/ */
public static void maybeForkThenBind(LDAPInterface ldap, BindRequest bind, ThreadPool threadPool, public static void maybeForkThenBindAndRevert(LDAPConnectionPool ldapPool, BindRequest bind, ThreadPool threadPool,
AbstractRunnable runnable) { AbstractRunnable runnable) {
Runnable bindRunnable = new AbstractRunnable() { final Runnable bindRunnable = new AbstractRunnable() {
@Override @Override
@SuppressForbidden(reason = "Bind allowed if forking of the LDAP Connection Reader Thread.") @SuppressForbidden(reason = "Bind allowed if forking of the LDAP Connection Reader Thread.")
protected void doRun() throws Exception { protected void doRun() throws Exception {
if (ldap instanceof LDAPConnectionPool) { privilegedConnect(() -> ldapPool.bindAndRevertAuthentication(bind.duplicate()));
privilegedConnect(() -> ((LDAPConnectionPool) ldap).bindAndRevertAuthentication(bind));
} else if (ldap instanceof LDAPConnection) {
((LDAPConnection) ldap).bind(bind);
} else {
throw new IllegalArgumentException("unsupported LDAPInterface implementation: " + ldap);
}
runnable.run(); runnable.run();
} }
@ -140,14 +147,52 @@ public final class LdapUtils {
runnable.onAfter(); runnable.onAfter();
} }
}; };
maybeForkAndRun(threadPool, bindRunnable);
if (isLdapConnectionThread(Thread.currentThread())) {
// only fork if binding on the LDAPConnectionReader thread
threadPool.executor(ThreadPool.Names.GENERIC).execute(bindRunnable);
} else {
// avoids repeated forking
bindRunnable.run();
} }
/**
* This method submits the {@code bind} request over the ldap connection. Its
* authentication status changes. The connection can be subsequently reused.
* This validates the bind credentials.
*
* Bind calls are blocking and if a bind is executed on the LDAP Connection
* Reader thread (as returned by {@code LdapUtils#isLdapConnectionThread}), the
* thread will be blocked until it is interrupted by something else such as a
* timeout timer. <b>Do not call bind</b> outside this method or
* {@link LdapUtils#maybeForkThenBind(LDAPConnection, BindRequest, ThreadPool, AbstractRunnable)}
*
* @param ldap
* The LDAP connection on which to submit the bind operation.
* @param bind
* The request object of the bind operation.
* @param threadPool
* The threads that will call the blocking bind operation, in case
* the calling thread is a connection reader, see:
* {@code LdapUtils#isLdapConnectionThread}.
* @param runnable
* The runnable that continues the program flow after the bind
* operation. It is executed on the same thread as the prior bind.
*/
public static void maybeForkThenBind(LDAPConnection ldap, BindRequest bind, ThreadPool threadPool, AbstractRunnable runnable) {
final Runnable bindRunnable = new AbstractRunnable() {
@Override
@SuppressForbidden(reason = "Bind allowed if forking of the LDAP Connection Reader Thread.")
protected void doRun() throws Exception {
privilegedConnect(() -> ldap.bind(bind.duplicate()));
runnable.run();
}
@Override
public void onFailure(Exception e) {
runnable.onFailure(e);
}
@Override
public void onAfter() {
runnable.onAfter();
}
};
maybeForkAndRun(threadPool, bindRunnable);
} }
/** /**

View File

@ -6,8 +6,11 @@
package org.elasticsearch.xpack.security.authc.ldap; package org.elasticsearch.xpack.security.authc.ldap;
import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPURL; import com.unboundid.ldap.sdk.LDAPURL;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.concurrent.ThreadContext;
@ -92,9 +95,10 @@ public class LdapSessionFactoryTests extends LdapTestCase {
String user = "Horatio Hornblower"; String user = "Horatio Hornblower";
SecureString userPass = new SecureString("pass"); SecureString userPass = new SecureString("pass");
final SimpleBindRequest bindRequest = new SimpleBindRequest("cn=Horatio Hornblower,ou=people,o=sevenSeas", "pass");
try (LdapSession ldap = session(sessionFactory, user, userPass)) { try (LdapSession ldap = session(sessionFactory, user, userPass)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString(user)); assertThat(dn, containsString(user));
} }
@ -132,9 +136,10 @@ public class LdapSessionFactoryTests extends LdapTestCase {
String user = "Horatio Hornblower"; String user = "Horatio Hornblower";
SecureString userPass = new SecureString("pass"); SecureString userPass = new SecureString("pass");
final SimpleBindRequest bindRequest = new SimpleBindRequest("cn=Horatio Hornblower,ou=people,o=sevenSeas", "pass");
try (LdapSession ldap = session(ldapFac, user, userPass)) { try (LdapSession ldap = session(ldapFac, user, userPass)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindRequest);
List<String> groups = groups(ldap); List<String> groups = groups(ldap);
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas")); assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
} }
@ -149,8 +154,10 @@ public class LdapSessionFactoryTests extends LdapTestCase {
LdapSessionFactory ldapFac = new LdapSessionFactory(config, sslService, threadPool); LdapSessionFactory ldapFac = new LdapSessionFactory(config, sslService, threadPool);
String user = "Horatio Hornblower"; String user = "Horatio Hornblower";
final SimpleBindRequest bindRequest = new SimpleBindRequest("cn=Horatio Hornblower,ou=people,o=sevenSeas", "pass");
try (LdapSession ldap = session(ldapFac, user, new SecureString("pass"))) { try (LdapSession ldap = session(ldapFac, user, new SecureString("pass"))) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindRequest);
List<String> groups = groups(ldap); List<String> groups = groups(ldap);
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas")); assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
} }
@ -166,9 +173,10 @@ public class LdapSessionFactoryTests extends LdapTestCase {
String user = "Horatio Hornblower"; String user = "Horatio Hornblower";
SecureString userPass = new SecureString("pass"); SecureString userPass = new SecureString("pass");
final SimpleBindRequest bindRequest = new SimpleBindRequest("cn=Horatio Hornblower,ou=people,o=sevenSeas", "pass");
try (LdapSession ldap = session(ldapFac, user, userPass)) { try (LdapSession ldap = session(ldapFac, user, userPass)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindRequest);
List<String> groups = groups(ldap); List<String> groups = groups(ldap);
assertThat(groups.size(), is(1)); assertThat(groups.size(), is(1));
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas")); assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));

View File

@ -24,7 +24,6 @@ import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.test.junit.annotations.Network;
import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.TestThreadPool;
import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmConfig;
@ -40,9 +39,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isEmptyString; import static org.hamcrest.Matchers.isEmptyString;
@ -137,18 +134,19 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase {
String user = "William Bush"; String user = "William Bush";
SecureString userPass = new SecureString("pass"); SecureString userPass = new SecureString("pass");
final SimpleBindRequest bindDNBindRequest = LdapUserSearchSessionFactory.bindRequest(config.settings());
try { try {
// auth // auth
try (LdapSession ldap = session(sessionFactory, user, userPass)) { try (LdapSession ldap = session(sessionFactory, user, userPass)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString(user)); assertThat(dn, containsString(user));
} }
//lookup //lookup
try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) { try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString(user)); assertThat(dn, containsString(user));
} }
@ -222,18 +220,19 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase {
String user = "William Bush"; String user = "William Bush";
SecureString userPass = new SecureString("pass"); SecureString userPass = new SecureString("pass");
final SimpleBindRequest bindDNBindRequest = LdapUserSearchSessionFactory.bindRequest(config.settings());
try { try {
// auth // auth
try (LdapSession ldap = session(sessionFactory, user, userPass)) { try (LdapSession ldap = session(sessionFactory, user, userPass)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString(user)); assertThat(dn, containsString(user));
} }
//lookup //lookup
try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) { try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString(user)); assertThat(dn, containsString(user));
} }
@ -307,18 +306,19 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase {
String user = "William Bush"; String user = "William Bush";
SecureString userPass = new SecureString("pass"); SecureString userPass = new SecureString("pass");
final SimpleBindRequest bindDNBindRequest = LdapUserSearchSessionFactory.bindRequest(config.settings());
try { try {
//auth //auth
try (LdapSession ldap = session(sessionFactory, user, userPass)) { try (LdapSession ldap = session(sessionFactory, user, userPass)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString(user)); assertThat(dn, containsString(user));
} }
//lookup //lookup
try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) { try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString(user)); assertThat(dn, containsString(user));
} }
@ -383,18 +383,19 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase {
String user = "wbush"; String user = "wbush";
SecureString userPass = new SecureString("pass"); SecureString userPass = new SecureString("pass");
final SimpleBindRequest bindDNBindRequest = LdapUserSearchSessionFactory.bindRequest(config.settings());
try { try {
//auth //auth
try (LdapSession ldap = session(sessionFactory, user, userPass)) { try (LdapSession ldap = session(sessionFactory, user, userPass)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString("William Bush")); assertThat(dn, containsString("William Bush"));
} }
//lookup //lookup
try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) { try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) {
assertConnectionCanReconnect(ldap.getConnection()); assertConnectionValid(ldap.getConnection(), bindDNBindRequest);
String dn = ldap.userDn(); String dn = ldap.userDn();
assertThat(dn, containsString("William Bush")); assertThat(dn, containsString("William Bush"));
} }

View File

@ -7,11 +7,13 @@ package org.elasticsearch.xpack.security.authc.ldap.support;
import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.sdk.Attribute; import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionPool; import com.unboundid.ldap.sdk.LDAPConnectionPool;
import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPInterface; import com.unboundid.ldap.sdk.LDAPInterface;
import com.unboundid.ldap.sdk.LDAPURL; import com.unboundid.ldap.sdk.LDAPURL;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.PlainActionFuture;
@ -170,15 +172,20 @@ public abstract class LdapTestCase extends ESTestCase {
return future.actionGet(); return future.actionGet();
} }
protected static void assertConnectionCanReconnect(LDAPInterface conn) { protected static void assertConnectionValid(LDAPInterface conn, SimpleBindRequest bindRequest) {
AccessController.doPrivileged(new PrivilegedAction<Void>() { AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override @Override
public Void run() { public Void run() {
try { try {
if (conn instanceof LDAPConnection) { if (conn instanceof LDAPConnection) {
assertTrue(((LDAPConnection) conn).isConnected());
assertEquals(bindRequest.getBindDN(),
((SimpleBindRequest)((LDAPConnection) conn).getLastBindRequest()).getBindDN());
((LDAPConnection) conn).reconnect(); ((LDAPConnection) conn).reconnect();
} else if (conn instanceof LDAPConnectionPool) { } else if (conn instanceof LDAPConnectionPool) {
try (LDAPConnection c = ((LDAPConnectionPool) conn).getConnection()) { try (LDAPConnection c = ((LDAPConnectionPool) conn).getConnection()) {
assertTrue(c.isConnected());
assertEquals(bindRequest.getBindDN(), ((SimpleBindRequest)c.getLastBindRequest()).getBindDN());
c.reconnect(); c.reconnect();
} }
} }