[Security] Handle non-existent user in native realm (elastic/x-pack-elasticsearch#2044)

Since change elastic/x-pack-elasticsearch@f796949 authentication is not allowed to respond with null, it must be AuthenticationResult.notHandled()

- Fixes 1 case where the native realm would respond null if the user was not found
- Fixes some edge cases in the LDAP realm.

Original commit: elastic/x-pack-elasticsearch@bc739a1d40
This commit is contained in:
Tim Vernum 2017-07-21 22:25:50 +10:00 committed by GitHub
parent 1752104140
commit b29f7a9ddb
4 changed files with 157 additions and 41 deletions

View File

@ -68,7 +68,7 @@ import java.util.function.Consumer;
public class NativeUsersStore extends AbstractComponent {
public static final String INDEX_TYPE = "doc";
private static final String USER_DOC_TYPE = "user";
static final String USER_DOC_TYPE = "user";
public static final String RESERVED_USER_TYPE = "reserved-user";
private final Hasher hasher = Hasher.BCRYPT;
@ -511,7 +511,7 @@ public class NativeUsersStore extends AbstractComponent {
void verifyPassword(String username, final SecureString password, ActionListener<AuthenticationResult> listener) {
getUserAndPassword(username, ActionListener.wrap((userAndPassword) -> {
if (userAndPassword == null || userAndPassword.passwordHash() == null) {
listener.onResponse(null);
listener.onResponse(AuthenticationResult.notHandled());
} else if (hasher.verify(password, userAndPassword.passwordHash())) {
listener.onResponse(AuthenticationResult.success(userAndPassword.user()));
} else {

View File

@ -5,6 +5,15 @@
*/
package org.elasticsearch.xpack.security.authc.ldap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import com.unboundid.ldap.sdk.LDAPException;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
@ -37,13 +46,6 @@ import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingSt
import org.elasticsearch.xpack.security.user.User;
import org.elasticsearch.xpack.ssl.SSLService;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Authenticates username/password tokens against ldap, locates groups and maps them to roles.
@ -145,9 +147,11 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
protected void doAuthenticate(UsernamePasswordToken token, ActionListener<AuthenticationResult> listener) {
// we submit to the threadpool because authentication using LDAP will execute blocking I/O for a bind request and we don't want
// network threads stuck waiting for a socket to connect. After the bind, then all interaction with LDAP should be async
final CancellableLdapRunnable cancellableLdapRunnable = new CancellableLdapRunnable(listener,
final CancellableLdapRunnable<AuthenticationResult> cancellableLdapRunnable = new CancellableLdapRunnable<>(listener,
ex -> AuthenticationResult.unsuccessful("Authentication against realm [" + this.toString() + "] failed", ex),
() -> sessionFactory.session(token.principal(), token.credentials(),
contextPreservingListener(new LdapSessionActionListener("authenticate", token.principal(), listener))), logger);
contextPreservingListener(new LdapSessionActionListener("authenticate", token.principal(), listener))), logger
);
threadPool.generic().execute(cancellableLdapRunnable);
threadPool.schedule(executionTimeout, Names.SAME, cancellableLdapRunnable::maybeTimeout);
}
@ -159,7 +163,7 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
// network threads stuck waiting for a socket to connect. After the bind, then all interaction with LDAP should be async
final ActionListener<AuthenticationResult> sessionListener = ActionListener.wrap(AuthenticationResult::getUser,
userActionListener::onFailure);
final CancellableLdapRunnable cancellableLdapRunnable = new CancellableLdapRunnable(userActionListener,
final CancellableLdapRunnable<User> cancellableLdapRunnable = new CancellableLdapRunnable<>(userActionListener, e -> null,
() -> sessionFactory.unauthenticatedSession(username,
contextPreservingListener(new LdapSessionActionListener("lookup", username, sessionListener))), logger);
threadPool.generic().execute(cancellableLdapRunnable);
@ -193,7 +197,7 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
private static void buildUser(LdapSession session, String username, ActionListener<AuthenticationResult> listener,
UserRoleMapper roleMapper) {
if (session == null) {
listener.onResponse(null);
listener.onResponse(AuthenticationResult.notHandled());
} else {
boolean loadingGroups = false;
try {
@ -250,7 +254,7 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
@Override
public void onResponse(LdapSession session) {
if (session == null) {
resultListener.onResponse(null);
resultListener.onResponse(AuthenticationResult.notHandled());
} else {
ldapSessionAtomicReference.set(session);
buildUser(session, username, resultListener, roleMapper);
@ -275,15 +279,17 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
* be queued and not executed for a long time or ever and this causes user requests to appear
* to hang. In these cases at least we can provide a response.
*/
static class CancellableLdapRunnable extends AbstractRunnable {
static class CancellableLdapRunnable<T> extends AbstractRunnable {
private final Runnable in;
private final ActionListener<?> listener;
private final ActionListener<T> listener;
private final Function<Exception, T> defaultValue;
private final Logger logger;
private final AtomicReference<LdapRunnableState> state = new AtomicReference<>(LdapRunnableState.AWAITING_EXECUTION);
CancellableLdapRunnable(ActionListener<?> listener, Runnable in, Logger logger) {
CancellableLdapRunnable(ActionListener<T> listener, Function<Exception, T> defaultValue, Runnable in, Logger logger) {
this.listener = listener;
this.defaultValue = Objects.requireNonNull(defaultValue);
this.in = in;
this.logger = logger;
}
@ -291,9 +297,8 @@ public final class LdapRealm extends CachingUsernamePasswordRealm {
@Override
public void onFailure(Exception e) {
logger.error("execution of ldap runnable failed", e);
// this is really a exceptional state but just call the listener and maybe another realm can authenticate, otherwise
// something as simple as a down ldap server/network error takes down auth
listener.onResponse(null);
final T result = defaultValue.apply(e);
listener.onResponse(result);
}
@Override

View File

@ -5,6 +5,13 @@
*/
package org.elasticsearch.xpack.security.authc.esnative;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
@ -15,26 +22,26 @@ import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
import org.elasticsearch.xpack.security.user.User;
import org.junit.Before;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
@ -114,6 +121,73 @@ public class NativeUsersStoreTests extends ESTestCase {
assertThat(userInfo.passwordHash, equalTo(ReservedRealm.EMPTY_PASSWORD_HASH));
}
public void testVerifyUserWithCorrectPassword() throws Exception {
final NativeUsersStore nativeUsersStore = startNativeUsersStore();
final String username = randomAlphaOfLengthBetween(4, 12);
final SecureString password = new SecureString(randomAlphaOfLengthBetween(8, 16).toCharArray());
final String[] roles = generateRandomStringArray(4, 12, false, false);
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
nativeUsersStore.verifyPassword(username, password, future);
respondToGetUserRequest(username, password, roles);
final AuthenticationResult result = future.get();
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
final User user = result.getUser();
assertThat(user, notNullValue());
assertThat(user.enabled(), equalTo(true));
assertThat(user.principal(), equalTo(username));
assertThat(user.roles(), equalTo(roles));
assertThat(user.authenticatedUser(), equalTo(user));
}
public void testVerifyUserWithIncorrectPassword() throws Exception {
final NativeUsersStore nativeUsersStore = startNativeUsersStore();
final String username = randomAlphaOfLengthBetween(4, 12);
final SecureString correctPassword = new SecureString(randomAlphaOfLengthBetween(12, 16).toCharArray());
final SecureString incorrectPassword = new SecureString(randomAlphaOfLengthBetween(8, 10).toCharArray());
final String[] roles = generateRandomStringArray(4, 12, false, false);
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
nativeUsersStore.verifyPassword(username, incorrectPassword, future);
respondToGetUserRequest(username, correctPassword, roles);
final AuthenticationResult result = future.get();
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE));
assertThat(result.getUser(), nullValue());
assertThat(result.getMessage(), containsString("authentication failed"));
}
public void testVerifyNonExistentUser() throws Exception {
final NativeUsersStore nativeUsersStore = startNativeUsersStore();
final String username = randomAlphaOfLengthBetween(4, 12);
final SecureString password = new SecureString(randomAlphaOfLengthBetween(8, 16).toCharArray());
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
nativeUsersStore.verifyPassword(username, password, future);
final GetResult getResult = new GetResult(
SecurityLifecycleService.SECURITY_INDEX_NAME,
NativeUsersStore.INDEX_TYPE,
NativeUsersStore.getIdForUser(NativeUsersStore.USER_DOC_TYPE, username),
1L,
false,
null,
Collections.emptyMap());
actionRespond(GetRequest.class, new GetResponse(getResult));
final AuthenticationResult result = future.get();
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE));
assertThat(result.getUser(), nullValue());
assertThat(result.getMessage(), nullValue());
}
private <ARequest extends ActionRequest, AResponse extends ActionResponse> ARequest actionRespond(Class<ARequest> requestClass,
AResponse response) {
Tuple<ARequest, ActionListener<?>> tuple = findRequest(requestClass);
@ -129,6 +203,26 @@ public class NativeUsersStoreTests extends ESTestCase {
.findFirst().orElseThrow(() -> new RuntimeException("Cannot find request of type " + requestClass));
}
private void respondToGetUserRequest(String username, SecureString password, String[] roles) throws IOException {
final Map<String, Object> values = new HashMap<>();
values.put(User.Fields.USERNAME.getPreferredName(), username);
values.put(User.Fields.PASSWORD.getPreferredName(), String.valueOf(Hasher.BCRYPT.hash(password)));
values.put(User.Fields.ROLES.getPreferredName(), roles);
values.put(User.Fields.ENABLED.getPreferredName(), Boolean.TRUE);
values.put(User.Fields.TYPE.getPreferredName(), NativeUsersStore.USER_DOC_TYPE);
final BytesReference source = jsonBuilder().map(values).bytes();
final GetResult getResult = new GetResult(
SecurityLifecycleService.SECURITY_INDEX_NAME,
NativeUsersStore.INDEX_TYPE,
NativeUsersStore.getIdForUser(NativeUsersStore.USER_DOC_TYPE, username),
1L,
true,
source,
Collections.emptyMap());
actionRespond(GetRequest.class, new GetResponse(getResult));
}
private NativeUsersStore startNativeUsersStore() {
SecurityLifecycleService securityLifecycleService = mock(SecurityLifecycleService.class);
when(securityLifecycleService.isSecurityIndexAvailable()).thenReturn(true);

View File

@ -5,17 +5,18 @@
*/
package org.elasticsearch.xpack.security.authc.ldap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.elasticsearch.ElasticsearchTimeoutException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authc.ldap.LdapRealm.CancellableLdapRunnable;
import org.elasticsearch.xpack.security.user.User;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.sameInstance;
@ -25,10 +26,10 @@ public class CancellableLdapRunnableTests extends ESTestCase {
AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>();
final CancellableLdapRunnable runnable =
new CancellableLdapRunnable(ActionListener.wrap(user -> {
throw new AssertionError("onResponse should not be called");
}, exceptionAtomicReference::set), () -> {
throw new AssertionError("runnable should not be executed");
}, logger);
throw new AssertionError("onResponse should not be called");
}, exceptionAtomicReference::set), e -> null, () -> {
throw new AssertionError("runnable should not be executed");
}, logger);
runnable.maybeTimeout();
runnable.run();
@ -45,10 +46,10 @@ public class CancellableLdapRunnableTests extends ESTestCase {
new CancellableLdapRunnable(ActionListener.wrap(user -> {
listenerCalled.set(true);
throw new AssertionError("onResponse should not be called");
}, e -> {
}, e -> {
listenerCalled.set(true);
throw new AssertionError("onFailure should not be called");
}), () -> ran.set(ran.get() == false), logger);
}), e -> null, () -> ran.set(ran.get() == false), logger);
runnable.run();
assertTrue(ran.get());
@ -63,10 +64,10 @@ public class CancellableLdapRunnableTests extends ESTestCase {
AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>();
final CancellableLdapRunnable runnable =
new CancellableLdapRunnable(ActionListener.wrap(user -> {
throw new AssertionError("onResponse should not be called");
}, exceptionAtomicReference::set), () -> {
throw new AssertionError("runnable should not be executed");
}, logger);
throw new AssertionError("onResponse should not be called");
}, exceptionAtomicReference::set), e -> null, () -> {
throw new AssertionError("runnable should not be executed");
}, logger);
final Exception e = new RuntimeException("foo");
runnable.onRejection(e);
@ -84,7 +85,7 @@ public class CancellableLdapRunnableTests extends ESTestCase {
}, e -> {
throw new AssertionError("onFailure should not be executed");
});
final CancellableLdapRunnable runnable = new CancellableLdapRunnable(listener, () -> {
final CancellableLdapRunnable runnable = new CancellableLdapRunnable(listener, e -> null, () -> {
runningLatch.countDown();
try {
timeoutCalledLatch.await();
@ -102,4 +103,20 @@ public class CancellableLdapRunnableTests extends ESTestCase {
listenerCalledLatch.await();
t.join();
}
public void testExceptionInRunnable() {
AtomicReference<String> resultRef = new AtomicReference<>();
final ActionListener<String> listener = ActionListener.wrap(resultRef::set, e -> {
throw new AssertionError("onFailure should not be executed");
});
String defaultValue = randomAlphaOfLengthBetween(2, 10);
final CancellableLdapRunnable<String> runnable = new CancellableLdapRunnable<>(listener, e -> defaultValue,
() -> {
throw new RuntimeException("runnable intentionally failed");
}, logger);
runnable.run();
assertThat(resultRef.get(), equalTo(defaultValue));
}
}