From e4c8851a24a30d774b7cdf469138cef7f16b5d6a Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 13 Jul 2017 14:24:08 +1000 Subject: [PATCH] Convert Realm.authenticate to provide a richer result (elastic/x-pack-elasticsearch#1932) This allows for messages to be returned, and distinguishes between 4 different results: - I have authenticated the user - I don't know how to authenticate that user. Try another realm. - I tried to authenticate the user, but failed. Try another realm. - I tried to authenticate the user, but failed. Fail the authentication attempt. Original commit: elastic/x-pack-elasticsearch@f796949cfbe1232744a346df012d181a63070ef9 --- .../security/authc/AuthenticationResult.java | 130 ++++++++++++++++++ .../security/authc/AuthenticationService.java | 59 ++++---- .../xpack/security/authc/Realm.java | 83 +++++------ .../security/authc/esnative/NativeRealm.java | 4 +- .../authc/esnative/NativeUsersStore.java | 7 +- .../authc/esnative/ReservedRealm.java | 32 ++--- .../xpack/security/authc/file/FileRealm.java | 13 +- .../authc/file/FileUserPasswdStore.java | 11 +- .../xpack/security/authc/ldap/LdapRealm.java | 38 ++--- .../xpack/security/authc/pki/PkiRealm.java | 49 +++---- .../support/CachingUsernamePasswordRealm.java | 67 +++++---- .../integration/ClearRealmsCacheTests.java | 13 +- .../authc/AuthenticationServiceTests.java | 30 ++-- .../xpack/security/authc/RealmsTests.java | 5 +- .../authc/esnative/ReservedRealmTests.java | 87 ++++++------ .../security/authc/file/FileRealmTests.java | 59 +++++--- .../authc/file/FileUserPasswdStoreTests.java | 48 ++++--- .../authc/ldap/ActiveDirectoryRealmTests.java | 29 ++-- .../security/authc/ldap/LdapRealmTests.java | 55 +++++--- .../security/authc/pki/PkiRealmTests.java | 19 +-- .../CachingUsernamePasswordRealmTests.java | 102 ++++++++------ .../example/realm/CustomRealm.java | 14 +- .../example/realm/CustomRealmTests.java | 10 +- 23 files changed, 590 insertions(+), 374 deletions(-) create mode 100644 plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationResult.java diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationResult.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationResult.java new file mode 100644 index 00000000000..f76bffe2e15 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationResult.java @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc; + +import java.util.Objects; + +import org.elasticsearch.common.inject.internal.Nullable; +import org.elasticsearch.xpack.security.user.User; + +/** + * Represents the result of an authentication attempt. + * This allows a {@link Realm} to respond in 3 different ways (without needing to + * resort to {@link org.elasticsearch.action.ActionListener#onFailure(Exception)}) + *
    + *
  1. Successful authentication of a user
  2. + *
  3. Unable to authenticate user, try another realm (optionally with a diagnostic message)
  4. + *
  5. Unable to authenticate user, terminate authentication (with an error message)
  6. + *
+ */ +public final class AuthenticationResult { + private static final AuthenticationResult NOT_HANDLED = new AuthenticationResult(Status.CONTINUE, null, null, null); + + public enum Status { + SUCCESS, + CONTINUE, + TERMINATE, + } + + private final Status status; + private final User user; + private final String message; + private final Exception exception; + + private AuthenticationResult(Status status, @Nullable User user, @Nullable String message, @Nullable Exception exception) { + this.status = status; + this.user = user; + this.message = message; + this.exception = exception; + } + + public Status getStatus() { + return status; + } + + public User getUser() { + return user; + } + + public String getMessage() { + return message; + } + + public Exception getException() { + return exception; + } + + /** + * Creates an {@code AuthenticationResult} that indicates that the supplied {@link User} + * has been successfully authenticated. + *

+ * The {@link #getStatus() status} is set to {@link Status#SUCCESS}. + *

+ * Neither the {@link #getMessage() message} nor {@link #getException() exception} are populated. + *

+ * @param user The user that was authenticated. Cannot be {@code null}. + */ + public static AuthenticationResult success(User user) { + Objects.requireNonNull(user); + return new AuthenticationResult(Status.SUCCESS, user, null, null); + } + + /** + * Creates an {@code AuthenticationResult} that indicates that the realm did not handle the + * authentication request in any way, and has no failure messages. + *

+ * The {@link #getStatus() status} is set to {@link Status#CONTINUE}. + *

+ * The {@link #getMessage() message}, {@link #getException() exception}, and {@link #getUser() user} are all set to {@code null}. + *

+ */ + public static AuthenticationResult notHandled() { + return NOT_HANDLED; + } + + /** + * Creates an {@code AuthenticationResult} that indicates that the realm attempted to handle the authentication request but was + * unsuccessful. The reason for the failure is given in the supplied message and optional exception. + *

+ * The {@link #getStatus() status} is set to {@link Status#CONTINUE}. + *

+ * The {@link #getUser() user} is not populated. + *

+ */ + public static AuthenticationResult unsuccessful(String message, @Nullable Exception cause) { + Objects.requireNonNull(message); + return new AuthenticationResult(Status.CONTINUE, null, message, cause); + } + + /** + * Creates an {@code AuthenticationResult} that indicates that the realm attempted to handle the authentication request, was + * unsuccessful and wants to terminate this authentication request. + * The reason for the failure is given in the supplied message and optional exception. + *

+ * The {@link #getStatus() status} is set to {@link Status#TERMINATE}. + *

+ * The {@link #getUser() user} is not populated. + *

+ */ + public static AuthenticationResult terminate(String message, @Nullable Exception cause) { + return new AuthenticationResult(Status.TERMINATE, null, message, cause); + } + + public boolean isAuthenticated() { + return status == Status.SUCCESS; + } + + @Override + public String toString() { + return "AuthenticationResult{" + + "status=" + status + + ", user=" + user + + ", message=" + message + + ", exception=" + exception + + '}'; + } + +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 38dae45516b..bf9c3eb1756 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -5,6 +5,13 @@ */ package org.elasticsearch.xpack.security.authc; +import java.net.InetSocketAddress; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchSecurityException; @@ -27,15 +34,10 @@ import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.security.support.Exceptions; import org.elasticsearch.xpack.security.user.AnonymousUser; import org.elasticsearch.xpack.security.user.User; -import java.net.InetSocketAddress; -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - import static org.elasticsearch.xpack.security.Security.setting; /** @@ -170,7 +172,7 @@ public class AuthenticationService extends AbstractComponent { *
  • look for a user token
  • *
  • token extraction {@link #extractToken(Consumer)}
  • *
  • token authentication {@link #consumeToken(AuthenticationToken)}
  • - *
  • user lookup for run as if necessary {@link #consumeUser(User)} and + *
  • user lookup for run as if necessary {@link #consumeUser(User, Map)} and * {@link #lookupRunAsUser(User, String, Consumer)}
  • *
  • write authentication into the context {@link #finishAuthentication(User)}
  • * @@ -255,8 +257,8 @@ public class AuthenticationService extends AbstractComponent { /** * Consumes the {@link AuthenticationToken} provided by the caller. In the case of a {@code null} token, {@link #handleNullToken()} * is called. In the case of a {@code non-null} token, the realms are iterated over and the first realm that returns a non-null - * {@link User} is the authenticating realm and iteration is stopped. This user is then passed to {@link #consumeUser(User)} if no - * exception was caught while trying to authenticate the token + * {@link User} is the authenticating realm and iteration is stopped. This user is then passed to {@link #consumeUser(User, Map)} + * if no exception was caught while trying to authenticate the token */ private void consumeToken(AuthenticationToken token) { if (token == null) { @@ -264,22 +266,33 @@ public class AuthenticationService extends AbstractComponent { } else { authenticationToken = token; final List realmsList = realms.asList(); + final Map> messages = new LinkedHashMap<>(); final BiConsumer> realmAuthenticatingConsumer = (realm, userListener) -> { if (realm.supports(authenticationToken)) { - realm.authenticate(authenticationToken, ActionListener.wrap((user) -> { - if (user == null) { - // the user was not authenticated, call this so we can audit the correct event - request.realmAuthenticationFailed(authenticationToken, realm.name()); - } else { + realm.authenticate(authenticationToken, ActionListener.wrap((result) -> { + assert result != null : "Realm " + realm + " produced a null authentication result"; + if (result.getStatus() == AuthenticationResult.Status.SUCCESS) { // user was authenticated, populate the authenticated by information authenticatedBy = new RealmRef(realm.name(), realm.type(), nodeName); + userListener.onResponse(result.getUser()); + } else { + // the user was not authenticated, call this so we can audit the correct event + request.realmAuthenticationFailed(authenticationToken, realm.name()); + if (result.getStatus() == AuthenticationResult.Status.TERMINATE) { + logger.info("Authentication of [{}] was terminated by realm [{}] - {}", + authenticationToken.principal(), realm.name(), result.getMessage()); + userListener.onFailure(Exceptions.authenticationError(result.getMessage(), result.getException())); + } else { + if (result.getMessage() != null) { + messages.put(realm, new Tuple<>(result.getMessage(), result.getException())); + } + userListener.onResponse(null); + } } - userListener.onResponse(user); }, (ex) -> { - logger.warn( - "An error occurred while attempting to authenticate [{}] against realm [{}] - {}", - authenticationToken.principal(), realm.name(), ex); - logger.debug("Authentication failed due to exception", ex); + logger.warn(new ParameterizedMessage( + "An error occurred while attempting to authenticate [{}] against realm [{}]", + authenticationToken.principal(), realm.name()), ex); userListener.onFailure(ex); }), request); } else { @@ -287,7 +300,8 @@ public class AuthenticationService extends AbstractComponent { } }; final IteratingActionListener authenticatingListener = - new IteratingActionListener<>(ActionListener.wrap(this::consumeUser, + new IteratingActionListener<>(ActionListener.wrap( + (user) -> consumeUser(user, messages), (e) -> listener.onFailure(request.exceptionProcessingRequest(e, token))), realmAuthenticatingConsumer, realmsList, threadContext); try { @@ -342,10 +356,9 @@ public class AuthenticationService extends AbstractComponent { * functionality is in use. When run as is not in use, {@link #finishAuthentication(User)} is called, otherwise we try to lookup * the run as user in {@link #lookupRunAsUser(User, String, Consumer)} */ - private void consumeUser(User user) { + private void consumeUser(User user, Map> messages) { if (user == null) { - final Map> failureDetails = Realm.getAuthenticationFailureDetails(threadContext); - failureDetails.forEach((realm, tuple) -> { + messages.forEach((realm, tuple) -> { final String message = tuple.v1(); final String cause = tuple.v2() == null ? "" : " (Caused by " + tuple.v2() + ")"; logger.warn("Authentication to realm {} failed - {}{}", realm.name(), message, cause); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Realm.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Realm.java index c3de9818c54..22ff53d50af 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Realm.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/Realm.java @@ -5,18 +5,14 @@ */ package org.elasticsearch.xpack.security.authc; +import java.util.HashMap; +import java.util.Map; + import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.inject.internal.Nullable; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.security.user.User; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - /** * An authentication mechanism to which the default authentication {@link org.elasticsearch.xpack.security.authc.AuthenticationService * service } delegates the authentication process. Different realms may be defined, each may be based on different @@ -24,8 +20,6 @@ import java.util.Map; */ public abstract class Realm implements Comparable { - private static final String AUTHENTICATION_FAILURES_KEY = "_xpack_security_auth_failures"; - protected final Logger logger; protected final String type; protected RealmConfig config; @@ -37,21 +31,21 @@ public abstract class Realm implements Comparable { } /** - * @return The type of this realm + * @return The type of this realm */ public String type() { return type; } /** - * @return The name of this realm. + * @return The name of this realm. */ public String name() { return config.name; } /** - * @return The order of this realm within the executing realm chain. + * @return The order of this realm within the executing realm chain. */ public int order() { return config.order; @@ -63,7 +57,7 @@ public abstract class Realm implements Comparable { } /** - * @return {@code true} if this realm supports the given authentication token, {@code false} otherwise. + * @return {@code true} if this realm supports the given authentication token, {@code false} otherwise. */ public abstract boolean supports(AuthenticationToken token); @@ -71,22 +65,41 @@ public abstract class Realm implements Comparable { * Attempts to extract an authentication token from the given context. If an appropriate token * is found it's returned, otherwise {@code null} is returned. * - * @param context The context that will provide information about the incoming request - * @return The authentication token or {@code null} if not found + * @param context The context that will provide information about the incoming request + * @return The authentication token or {@code null} if not found */ public abstract AuthenticationToken token(ThreadContext context); /** - * Authenticates the given token in an asynchronous fashion. A successful authentication will call the - * {@link ActionListener#onResponse} with the User associated with the given token. An unsuccessful authentication calls - * with {@code null} on the argument. + * Authenticates the given token in an asynchronous fashion. + *

    + * A successful authentication will call {@link ActionListener#onResponse} with a + * {@link AuthenticationResult#success successful} result, which includes the user associated with the given token. + *
    + * If the realm does not support, or cannot handle the token, it will call {@link ActionListener#onResponse} with a + * {@link AuthenticationResult#notHandled not-handled} result. + * This can include cases where the token identifies as user that is not known by this realm. + *
    + * If the realm can handle the token, but authentication failed it will typically call {@link ActionListener#onResponse} with a + * {@link AuthenticationResult#unsuccessful failure} result, which includes a diagnostic message regarding the failure. + * This can include cases where the token identifies a valid user, but has an invalid password. + *
    + * If the realm wishes to assert that it has the exclusive right to handle the provided token, but authentication was not successful + * it typically call {@link ActionListener#onResponse} with a + * {@link AuthenticationResult#terminate termination} result, which includes a diagnostic message regarding the failure. + * This can include cases where the token identifies a valid user, but has an invalid password and no other realm is allowed to + * authenticate that user. + *

    + *

    + * The remote address should be {@code null} if the request initiated from the local node. + *

    * - * The remote address should be null if the request initiated from the local node. - * @param token The authentication token - * @param listener The listener to pass the authentication result to + * @param token The authentication token + * @param listener The listener to pass the authentication result to * @param incomingRequest the request that is being authenticated */ - public abstract void authenticate(AuthenticationToken token, ActionListener listener, IncomingRequest incomingRequest); + public abstract void authenticate(AuthenticationToken token, ActionListener listener, + IncomingRequest incomingRequest); /** * Looks up the user identified the String identifier. A successful lookup will call the {@link ActionListener#onResponse} @@ -117,35 +130,11 @@ public abstract class Realm implements Comparable { /** * Constructs a realm which will be used for authentication. + * * @param config The configuration for the realm * @throws Exception an exception may be thrown if there was an error during realm creation */ Realm create(RealmConfig config) throws Exception; } - /** - * Provides a mechanism for a realm to report errors that were handled within a realm, but may - * provide useful diagnostics about why authentication failed. - */ - protected final void setFailedAuthenticationDetails(String message, @Nullable Exception cause) { - final ThreadContext threadContext = config.threadContext(); - Map> failures = threadContext.getTransient(AUTHENTICATION_FAILURES_KEY); - if (failures == null) { - failures = new LinkedHashMap<>(); - threadContext.putTransient(AUTHENTICATION_FAILURES_KEY, failures); - } - failures.put(this, new Tuple<>(message, cause)); - } - - /** - * Retrieves any authentication failures messages that were set using {@link #setFailedAuthenticationDetails(String, Exception)} - */ - static Map> getAuthenticationFailureDetails(ThreadContext threadContext) { - final Map> failures = threadContext.getTransient(AUTHENTICATION_FAILURES_KEY); - if (failures == null) { - return Collections.emptyMap(); - } - return failures; - } - } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java index fc99e52c33e..e44137582ee 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authc.esnative; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; @@ -35,7 +36,8 @@ public class NativeRealm extends CachingUsernamePasswordRealm { } @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { userStore.verifyPassword(token.principal(), token.credentials(), listener); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index 39585294d05..427866eadaa 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -42,6 +42,7 @@ import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheResponse; import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest; import org.elasticsearch.xpack.security.action.user.DeleteUserRequest; import org.elasticsearch.xpack.security.action.user.PutUserRequest; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.ContainerSettings; import org.elasticsearch.xpack.security.authc.support.Hasher; import org.elasticsearch.xpack.security.client.SecurityClient; @@ -512,14 +513,14 @@ public class NativeUsersStore extends AbstractComponent { * @param username username to lookup the user by * @param password the plaintext password to verify */ - void verifyPassword(String username, final SecureString password, ActionListener listener) { + void verifyPassword(String username, final SecureString password, ActionListener listener) { getUserAndPassword(username, ActionListener.wrap((userAndPassword) -> { if (userAndPassword == null || userAndPassword.passwordHash() == null) { listener.onResponse(null); } else if (hasher.verify(password, userAndPassword.passwordHash())) { - listener.onResponse(userAndPassword.user()); + listener.onResponse(AuthenticationResult.success(userAndPassword.user())); } else { - listener.onResponse(null); + listener.onResponse(AuthenticationResult.unsuccessful("Password authentication failed for " + username, null)); } }, listener::onFailure)); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 0dc3db6d839..64c857c2d21 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.XPackSettings; import org.elasticsearch.xpack.security.InternalClient; import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.SecurityLifecycleService; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.action.user.ChangePasswordAction; import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest; import org.elasticsearch.xpack.security.action.user.ChangePasswordResponse; @@ -88,7 +89,8 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { } @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { if (incomingRequest.getType() != IncomingRequest.RequestType.REST) { doAuthenticate(token, listener, false); } else { @@ -105,14 +107,14 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { } } - private void doAuthenticate(UsernamePasswordToken token, ActionListener listener, boolean acceptEmptyPassword) { + private void doAuthenticate(UsernamePasswordToken token, ActionListener listener, boolean acceptEmptyPassword) { if (realmEnabled == false) { - listener.onResponse(null); + listener.onResponse(AuthenticationResult.notHandled()); } else if (isReserved(token.principal(), config.globalSettings()) == false) { - listener.onResponse(null); + listener.onResponse(AuthenticationResult.notHandled()); } else { getUserInfo(token.principal(), ActionListener.wrap((userInfo) -> { - Runnable action; + AuthenticationResult result; if (userInfo != null) { try { if (userInfo.hasEmptyPassword) { @@ -120,21 +122,18 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { // Accepting the OLD_DEFAULT_PASSWORD_HASH is a transition step. We do not want to support // this in a release. if (isSetupMode(token.principal(), acceptEmptyPassword) == false) { - action = () -> listener.onFailure(Exceptions.authenticationError("failed to authenticate user [{}]", - token.principal())); + result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null); } else if (verifyPassword(userInfo, token) || Hasher.BCRYPT.verify(token.credentials(), OLD_DEFAULT_PASSWORD_HASH)) { - action = () -> listener.onResponse(getUser(token.principal(), userInfo)); + result = AuthenticationResult.success(getUser(token.principal(), userInfo)); } else { - action = () -> listener.onFailure(Exceptions.authenticationError("failed to authenticate user [{}]", - token.principal())); + result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null); } } else if (verifyPassword(userInfo, token)) { final User user = getUser(token.principal(), userInfo); - action = () -> listener.onResponse(user); + result = AuthenticationResult.success(user); } else { - action = () -> listener.onFailure(Exceptions.authenticationError("failed to authenticate user [{}]", - token.principal())); + result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null); } } finally { if (userInfo.passwordHash != EMPTY_PASSWORD_HASH && userInfo.passwordHash != OLD_DEFAULT_PASSWORD_HASH) { @@ -142,11 +141,10 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { } } } else { - action = () -> listener.onFailure(Exceptions.authenticationError("failed to authenticate user [{}]", - token.principal())); + result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null); } - // we want the finally block to clear out the chars before we proceed further so we execute the action here - action.run(); + // we want the finally block to clear out the chars before we proceed further so we handle the result here + listener.onResponse(result); }, listener::onFailure)); } } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java index 8aa9b4b0a2f..e12d035ada4 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java @@ -11,6 +11,7 @@ import java.util.Set; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; @@ -38,13 +39,13 @@ public class FileRealm extends CachingUsernamePasswordRealm { } @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { - if (userPasswdStore.verifyPassword(token.principal(), token.credentials())) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { + final AuthenticationResult result = userPasswdStore.verifyPassword(token.principal(), token.credentials(), () -> { String[] roles = userRolesStore.roles(token.principal()); - listener.onResponse(new User(token.principal(), roles)); - } else { - listener.onResponse(null); - } + return new User(token.principal(), roles); + }); + listener.onResponse(result); } @Override diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java index 1691836463f..3b5246e3aed 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java @@ -18,11 +18,13 @@ import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.XPackPlugin; import org.elasticsearch.xpack.XPackSettings; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.support.Hasher; import org.elasticsearch.xpack.security.support.NoOpLogger; import org.elasticsearch.xpack.security.support.Validation; import org.elasticsearch.xpack.security.support.Validation.Users; +import org.elasticsearch.xpack.security.user.User; import java.io.IOException; import java.io.PrintWriter; @@ -78,16 +80,15 @@ public class FileUserPasswdStore { return users.size(); } - public boolean verifyPassword(String username, SecureString password) { + public AuthenticationResult verifyPassword(String username, SecureString password, java.util.function.Supplier user) { char[] hash = users.get(username); if (hash == null) { - return false; + return AuthenticationResult.notHandled(); } if (hasher.verify(password, hash) == false) { - logger.debug("User [{}] exists in file but authentication failed", username); - return false; + return AuthenticationResult.unsuccessful("Password authentication failed for " + username, null); } - return true; + return AuthenticationResult.success(user.get()); } public boolean userExists(String username) { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java index fe52fece154..24eb787148d 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool.Names; import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.RealmSettings; @@ -142,7 +143,8 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { * This user will then be passed to the listener */ @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { // 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, @@ -153,17 +155,19 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { } @Override - protected void doLookupUser(String username, ActionListener listener) { + protected void doLookupUser(String username, ActionListener userActionListener) { if (sessionFactory.supportsUnauthenticatedSession()) { // 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 ActionListener sessionListener = ActionListener.wrap(AuthenticationResult::getUser, + userActionListener::onFailure); + final CancellableLdapRunnable cancellableLdapRunnable = new CancellableLdapRunnable(userActionListener, () -> sessionFactory.unauthenticatedSession(username, - contextPreservingListener(new LdapSessionActionListener("lookup", username, listener))), logger); + contextPreservingListener(new LdapSessionActionListener("lookup", username, sessionListener))), logger); threadPool.generic().execute(cancellableLdapRunnable); threadPool.schedule(executionTimeout, Names.SAME, cancellableLdapRunnable::maybeTimeout); } else { - listener.onResponse(null); + userActionListener.onResponse(null); } } @@ -188,7 +192,8 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { return usage; } - private static void buildUser(LdapSession session, String username, ActionListener listener, UserRoleMapper roleMapper) { + private static void buildUser(LdapSession session, String username, ActionListener listener, + UserRoleMapper roleMapper) { if (session == null) { listener.onResponse(null); } else { @@ -210,8 +215,8 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { roles -> { IOUtils.close(session); String[] rolesArray = roles.toArray(new String[roles.size()]); - listener.onResponse( - new User(username, rolesArray, null, null, metadata, true) + listener.onResponse(AuthenticationResult.success( + new User(username, rolesArray, null, null, metadata, true)) ); }, onFailure )); @@ -236,21 +241,21 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { private final AtomicReference ldapSessionAtomicReference = new AtomicReference<>(); private String action; private final String username; - private final ActionListener userActionListener; + private final ActionListener resultListener; - LdapSessionActionListener(String action, String username, ActionListener userActionListener) { + LdapSessionActionListener(String action, String username, ActionListener resultListener) { this.action = action; this.username = username; - this.userActionListener = userActionListener; + this.resultListener = resultListener; } @Override public void onResponse(LdapSession session) { if (session == null) { - userActionListener.onResponse(null); + resultListener.onResponse(null); } else { ldapSessionAtomicReference.set(session); - buildUser(session, username, userActionListener, roleMapper); + buildUser(session, username, resultListener, roleMapper); } } @@ -262,8 +267,7 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { if (logger.isDebugEnabled()) { logger.debug(new ParameterizedMessage("Exception occurred during {} for {}", action, LdapRealm.this), e); } - setFailedAuthenticationDetails(action + " failed", e); - userActionListener.onResponse(null); + resultListener.onResponse(AuthenticationResult.unsuccessful(action + " failed", e)); } } @@ -276,11 +280,11 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { static class CancellableLdapRunnable extends AbstractRunnable { private final Runnable in; - private final ActionListener listener; + private final ActionListener listener; private final Logger logger; private final AtomicReference state = new AtomicReference<>(LdapRunnableState.AWAITING_EXECUTION); - CancellableLdapRunnable(ActionListener listener, Runnable in, Logger logger) { + CancellableLdapRunnable(ActionListener listener, Runnable in, Logger logger) { this.listener = listener; this.in = in; this.logger = logger; diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index 32546c68cc5..50cefba90af 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -5,6 +5,18 @@ */ package org.elasticsearch.xpack.security.authc.pki; +import javax.net.ssl.X509TrustManager; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; @@ -17,29 +29,18 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.watcher.ResourceWatcherService; -import org.elasticsearch.xpack.security.authc.IncomingRequest; -import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; -import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper; -import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; -import org.elasticsearch.xpack.ssl.CertUtils; -import org.elasticsearch.xpack.ssl.SSLConfigurationSettings; -import org.elasticsearch.xpack.security.user.User; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.Realm; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.RealmSettings; - -import javax.net.ssl.X509TrustManager; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; +import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper; +import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; +import org.elasticsearch.xpack.security.user.User; +import org.elasticsearch.xpack.ssl.CertUtils; +import org.elasticsearch.xpack.ssl.SSLConfigurationSettings; public class PkiRealm extends Realm { @@ -82,17 +83,19 @@ public class PkiRealm extends Realm { } @Override - public void authenticate(AuthenticationToken authToken, ActionListener listener, IncomingRequest incomingRequest) { + public void authenticate(AuthenticationToken authToken, ActionListener listener, + IncomingRequest incomingRequest) { X509AuthenticationToken token = (X509AuthenticationToken)authToken; if (isCertificateChainTrusted(trustManager, token, logger) == false) { - listener.onResponse(null); + listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " is not trusted", null)); } else { final Map metadata = Collections.singletonMap("pki_dn", token.dn()); final UserRoleMapper.UserData user = new UserRoleMapper.UserData(token.principal(), token.dn(), Collections.emptySet(), metadata, this.config); roleMapper.resolveRoles(user, ActionListener.wrap( - roles -> listener.onResponse(new User(token.principal(), - roles.toArray(new String[roles.size()]), null, null, metadata, true)), + roles -> listener.onResponse(AuthenticationResult.success( + new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true) + )), listener::onFailure )); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java index 58a136bc70f..2589555c059 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java @@ -5,23 +5,25 @@ */ package org.elasticsearch.xpack.security.authc.support; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; + import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.AuthenticationToken; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.user.User; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutionException; - public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm implements CachingRealm { public static final Setting CACHE_HASH_ALGO_SETTING = Setting.simpleString("cache.hash_algo", Setting.Property.NodeScope); @@ -73,7 +75,8 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm * @param incomingRequest the request that is being authenticated */ @Override - public final void authenticate(AuthenticationToken authToken, ActionListener listener, IncomingRequest incomingRequest) { + public final void authenticate(AuthenticationToken authToken, ActionListener listener, + IncomingRequest incomingRequest) { UsernamePasswordToken token = (UsernamePasswordToken) authToken; try { if (cache == null) { @@ -87,69 +90,74 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm } } - private void authenticateWithCache(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + private void authenticateWithCache(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { UserWithHash userWithHash = cache.get(token.principal()); if (userWithHash == null) { if (logger.isDebugEnabled()) { logger.debug("user [{}] not found in cache for realm [{}], proceeding with normal authentication", token.principal(), name()); } - doAuthenticateAndCache(token, ActionListener.wrap((user) -> { - if (user != null) { + doAuthenticateAndCache(token, ActionListener.wrap((result) -> { + if (result.isAuthenticated()) { + final User user = result.getUser(); logger.debug("realm [{}] authenticated user [{}], with roles [{}]", name(), token.principal(), user.roles()); } - listener.onResponse(user); + listener.onResponse(result); }, listener::onFailure), incomingRequest); } else if (userWithHash.hasHash()) { if (userWithHash.verify(token.credentials())) { if (userWithHash.user.enabled()) { User user = userWithHash.user; logger.debug("realm [{}] authenticated user [{}], with roles [{}]", name(), token.principal(), user.roles()); - listener.onResponse(user); + listener.onResponse(AuthenticationResult.success(user)); } else { // We successfully authenticated, but the cached user is disabled. // Reload the primary record to check whether the user is still disabled cache.invalidate(token.principal()); - doAuthenticateAndCache(token, ActionListener.wrap((user) -> { - if (user != null) { + doAuthenticateAndCache(token, ActionListener.wrap((result) -> { + if (result.isAuthenticated()) { + final User user = result.getUser(); logger.debug("realm [{}] authenticated user [{}] (enabled:{}), with roles [{}]", name(), token.principal(), user.enabled(), user.roles()); } - listener.onResponse(user); + listener.onResponse(result); }, listener::onFailure), incomingRequest); } } else { cache.invalidate(token.principal()); - doAuthenticateAndCache(token, ActionListener.wrap((user) -> { - if (user != null) { + doAuthenticateAndCache(token, ActionListener.wrap((result) -> { + if (result.isAuthenticated()) { + final User user = result.getUser(); logger.debug("cached user's password changed. realm [{}] authenticated user [{}], with roles [{}]", name(), token.principal(), user.roles()); } - listener.onResponse(user); + listener.onResponse(result); }, listener::onFailure), incomingRequest); } } else { cache.invalidate(token.principal()); - doAuthenticateAndCache(token, ActionListener.wrap((user) -> { - if (user != null) { + doAuthenticateAndCache(token, ActionListener.wrap((result) -> { + if (result.isAuthenticated()) { + final User user = result.getUser(); logger.debug("cached user came from a lookup and could not be used for authentication. " + "realm [{}] authenticated user [{}] with roles [{}]", name(), token.principal(), user.roles()); } - listener.onResponse(user); + listener.onResponse(result); }, listener::onFailure), incomingRequest); } } - private void doAuthenticateAndCache(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { - ActionListener wrapped = ActionListener.wrap((user) -> { - if (user == null) { - listener.onResponse(null); - } else { - UserWithHash userWithHash = new UserWithHash(user, token.credentials(), hasher); + private void doAuthenticateAndCache(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { + ActionListener wrapped = ActionListener.wrap((result) -> { + Objects.requireNonNull(result, "AuthenticationResult cannot be null"); + if (result.getStatus() == AuthenticationResult.Status.SUCCESS) { + UserWithHash userWithHash = new UserWithHash(result.getUser(), token.credentials(), hasher); // it doesn't matter if we already computed it elsewhere cache.put(token.principal(), userWithHash); - listener.onResponse(user); } + listener.onResponse(result); }, listener::onFailure); doAuthenticate(token, wrapped, incomingRequest); @@ -162,7 +170,8 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm return stats; } - protected abstract void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest); + protected abstract void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest); @Override public final void lookupUser(String username, ActionListener listener) { diff --git a/plugin/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java b/plugin/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java index 026a8e63dbe..f75d5a94866 100644 --- a/plugin/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java +++ b/plugin/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.test.SecuritySettingsSource; import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheRequest; import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheResponse; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.Realm; import org.elasticsearch.xpack.security.authc.Realms; @@ -234,9 +235,9 @@ public class ClearRealmsCacheTests extends SecurityIntegTestCase { Map> users = new HashMap<>(); for (Realm realm : realms) { for (String username : usernames) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(tokens.get(username), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, notNullValue()); Map realmToUser = users.get(username); if (realmToUser == null) { @@ -251,9 +252,9 @@ public class ClearRealmsCacheTests extends SecurityIntegTestCase { for (String username : usernames) { for (Realm realm : realms) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(tokens.get(username), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, sameInstance(users.get(username).get(realm))); } } @@ -264,9 +265,9 @@ public class ClearRealmsCacheTests extends SecurityIntegTestCase { // now, user_a should have been evicted, but user_b should still be cached for (String username : usernames) { for (Realm realm : realms) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(tokens.get(username), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, notNullValue()); scenario.assertEviction(users.get(username).get(realm), user); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 65e96563636..696c46c173d 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -5,6 +5,18 @@ */ package org.elasticsearch.xpack.security.authc; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.time.Clock; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchSecurityException; @@ -46,18 +58,6 @@ import org.elasticsearch.xpack.security.user.User; import org.junit.After; import org.junit.Before; -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.time.Clock; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException; import static org.elasticsearch.xpack.security.support.Exceptions.authenticationError; import static org.hamcrest.Matchers.arrayContaining; @@ -876,7 +876,11 @@ public class AuthenticationServiceTests extends ESTestCase { private void mockAuthenticate(Realm realm, AuthenticationToken token, User user) { doAnswer((i) -> { ActionListener listener = (ActionListener) i.getArguments()[1]; - listener.onResponse(user); + if (user == null) { + listener.onResponse(AuthenticationResult.notHandled()); + } else { + listener.onResponse(AuthenticationResult.success(user)); + } return null; }).when(realm).authenticate(eq(token), any(ActionListener.class), any(IncomingRequest.class)); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java index c0ff17d6152..c941d8ee937 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java @@ -436,8 +436,9 @@ public class RealmsTests extends ESTestCase { } @Override - public void authenticate(AuthenticationToken token, ActionListener listener, IncomingRequest incomingRequest) { - listener.onResponse(null); + public void authenticate(AuthenticationToken token, ActionListener listener, + IncomingRequest incomingRequest) { + listener.onResponse(AuthenticationResult.notHandled()); } @Override diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index 58702fa6f70..92ef9af1b3f 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -5,9 +5,18 @@ */ package org.elasticsearch.xpack.security.authc.esnative; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; +import java.util.function.Predicate; + import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; -import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.SuppressForbidden; @@ -18,6 +27,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.XPackSettings; import org.elasticsearch.xpack.security.SecurityLifecycleService; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; @@ -32,17 +42,6 @@ import org.elasticsearch.xpack.security.user.User; import org.junit.Before; import org.mockito.ArgumentCaptor; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ExecutionException; -import java.util.function.Predicate; - -import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -89,12 +88,12 @@ public class ReservedRealmTests extends ESTestCase { new AnonymousUser(Settings.EMPTY), securityLifecycleService, new ThreadContext(Settings.EMPTY)); final String principal = ElasticUser.NAME; - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); InetSocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), 100); when(incomingRequest.getRemoteAddress()).thenReturn(address); when(incomingRequest.getType()).thenReturn(IncomingRequest.RequestType.REST); reservedRealm.authenticate(new UsernamePasswordToken(principal, EMPTY_PASSWORD), future, incomingRequest); - assertThat(future.get().enabled(), equalTo(false)); + assertThat(future.get().getUser().enabled(), equalTo(false)); } public void testDisableDefaultPasswordAuthentication() throws Throwable { @@ -106,19 +105,9 @@ public class ReservedRealmTests extends ESTestCase { final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, usersStore, anonymousUser, securityLifecycleService, new ThreadContext(Settings.EMPTY)); - final ActionListener listener = new ActionListener() { - @Override - public void onResponse(User user) { - fail("Authentication should have failed because default-password is not allowed"); - } - - @Override - public void onFailure(Exception e) { - assertThat(e, instanceOf(ElasticsearchSecurityException.class)); - assertThat(e.getMessage(), containsString("failed to authenticate")); - } - }; + PlainActionFuture listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(expected.principal(), EMPTY_PASSWORD), listener, incomingRequest); + assertFailedAuthentication(listener, expected.principal()); } public void testElasticEmptyPasswordAuthenticationFailsFromNonLocalhost() throws Throwable { @@ -130,15 +119,13 @@ public class ReservedRealmTests extends ESTestCase { new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), securityLifecycleService, new ThreadContext(Settings.EMPTY)); - PlainActionFuture listener = new PlainActionFuture<>(); + PlainActionFuture listener = new PlainActionFuture<>(); InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("128.9.8.1"), 100); when(incomingRequest.getRemoteAddress()).thenReturn(address); reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, EMPTY_PASSWORD), listener, incomingRequest); - - ElasticsearchSecurityException actual = expectThrows(ElasticsearchSecurityException.class, listener::actionGet); - assertThat(actual.getMessage(), containsString("failed to authenticate user [" + principal)); + assertFailedAuthentication(listener, expected.principal()); } @SuppressForbidden(reason = "allow getting localhost") @@ -151,7 +138,7 @@ public class ReservedRealmTests extends ESTestCase { new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(Settings.EMPTY), securityLifecycleService, new ThreadContext(Settings.EMPTY)); - PlainActionFuture listener = new PlainActionFuture<>(); + PlainActionFuture listener = new PlainActionFuture<>(); InetSocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), 100); @@ -159,7 +146,7 @@ public class ReservedRealmTests extends ESTestCase { when(incomingRequest.getType()).thenReturn(IncomingRequest.RequestType.REST); reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, EMPTY_PASSWORD), listener, incomingRequest); - User user = listener.actionGet(); + User user = listener.actionGet().getUser(); assertEquals(expected, user); assertNotEquals(new ElasticUser(true, false), user); @@ -177,10 +164,11 @@ public class ReservedRealmTests extends ESTestCase { final User expected = randomFrom(new ElasticUser(true), new KibanaUser(true), new LogstashSystemUser(true)); final String principal = expected.principal(); - PlainActionFuture listener = new PlainActionFuture<>(); + PlainActionFuture listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, EMPTY_PASSWORD), listener, mock(IncomingRequest.class)); - final User authenticated = listener.actionGet(); - assertNull(authenticated); + final AuthenticationResult result = listener.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.CONTINUE)); + assertNull(result.getUser()); verifyZeroInteractions(usersStore); } @@ -192,7 +180,7 @@ public class ReservedRealmTests extends ESTestCase { verifySuccessfulAuthentication(false); } - private void verifySuccessfulAuthentication(boolean enabled) { + private void verifySuccessfulAuthentication(boolean enabled) throws Exception { final Settings settings = Settings.builder().put(ACCEPT_DEFAULT_PASSWORDS, randomBoolean()).build(); final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings), securityLifecycleService, new ThreadContext(Settings.EMPTY)); @@ -207,10 +195,9 @@ public class ReservedRealmTests extends ESTestCase { }).when(usersStore).getReservedUserInfo(eq(principal), any(ActionListener.class)); // test empty password - final PlainActionFuture listener = new PlainActionFuture<>(); + final PlainActionFuture listener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, EMPTY_PASSWORD), listener, incomingRequest); - ElasticsearchSecurityException expected = expectThrows(ElasticsearchSecurityException.class, listener::actionGet); - assertThat(expected.getMessage(), containsString("failed to authenticate user [" + principal)); + assertFailedAuthentication(listener, expectedUser.principal()); // the realm assumes it owns the hashed password so it fills it with 0's doAnswer((i) -> { @@ -220,9 +207,9 @@ public class ReservedRealmTests extends ESTestCase { }).when(usersStore).getReservedUserInfo(eq(principal), any(ActionListener.class)); // test new password - final PlainActionFuture authListener = new PlainActionFuture<>(); + final PlainActionFuture authListener = new PlainActionFuture<>(); reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, newPassword), authListener, incomingRequest); - final User authenticated = authListener.actionGet(); + final User authenticated = authListener.actionGet().getUser(); assertEquals(expectedUser, authenticated); assertThat(expectedUser.enabled(), is(enabled)); @@ -349,30 +336,36 @@ public class ReservedRealmTests extends ESTestCase { } @SuppressForbidden(reason = "allow getting localhost") - public void testFailedAuthentication() throws UnknownHostException { + public void testFailedAuthentication() throws Exception { final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY), securityLifecycleService, new ThreadContext(Settings.EMPTY)); InetSocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), 100); // maybe cache a successful auth if (randomBoolean()) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); IncomingRequest r = mock(IncomingRequest.class); when(r.getRemoteAddress()).thenReturn(address); when(r.getType()).thenReturn(IncomingRequest.RequestType.REST); reservedRealm.authenticate(new UsernamePasswordToken(ElasticUser.NAME, EMPTY_PASSWORD), future, r); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertEquals(new ElasticUser(true, true), user); } - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); IncomingRequest r = mock(IncomingRequest.class); when(r.getRemoteAddress()).thenReturn(address); when(r.getType()).thenReturn(IncomingRequest.RequestType.REST); reservedRealm.authenticate(new UsernamePasswordToken(ElasticUser.NAME, new SecureString("foobar".toCharArray())), future, r); - ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future::actionGet); - assertThat(e.getMessage(), containsString("failed to authenticate")); + assertFailedAuthentication(future, ElasticUser.NAME); + } + + private void assertFailedAuthentication(PlainActionFuture future, String principal) throws Exception { + final AuthenticationResult result = future.get(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); + assertThat(result.getMessage(), containsString("failed to authenticate")); + assertThat(result.getMessage(), containsString(principal)); } @SuppressWarnings("unchecked") diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java index 5245812c4b8..2fa5df4c8ce 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java @@ -5,22 +5,25 @@ */ package org.elasticsearch.xpack.security.authc.file; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.security.user.User; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.watcher.ResourceWatcherService; import org.junit.Before; - -import java.util.Locale; -import java.util.Map; +import org.mockito.stubbing.Answer; import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.equalTo; @@ -29,6 +32,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -37,6 +42,12 @@ import static org.mockito.Mockito.when; public class FileRealmTests extends ESTestCase { + private static final Answer VERIFY_PASSWORD_ANSWER = inv -> { + assertThat(inv.getArguments().length, is(3)); + Supplier supplier = (Supplier) inv.getArguments()[2]; + return AuthenticationResult.success(supplier.get()); + }; + private FileUserPasswdStore userPasswdStore; private FileUserRolesStore userRolesStore; private Settings globalSettings; @@ -49,13 +60,16 @@ public class FileRealmTests extends ESTestCase { } public void testAuthenticate() throws Exception { - when(userPasswdStore.verifyPassword("user1", new SecureString("test123"))).thenReturn(true); + when(userPasswdStore.verifyPassword(eq("user1"), eq(new SecureString("test123")), any(Supplier.class))) + .thenAnswer(VERIFY_PASSWORD_ANSWER); when(userRolesStore.roles("user1")).thenReturn(new String[] { "role1", "role2" }); RealmConfig config = new RealmConfig("file-test", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); FileRealm realm = new FileRealm(config, userPasswdStore, userRolesStore); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + User user = result.getUser(); assertThat(user, notNullValue()); assertThat(user.principal(), equalTo("user1")); assertThat(user.roles(), notNullValue()); @@ -68,15 +82,16 @@ public class FileRealmTests extends ESTestCase { .put("cache.hash_algo", Hasher.values()[randomIntBetween(0, Hasher.values().length - 1)].name().toLowerCase(Locale.ROOT)) .build(); RealmConfig config = new RealmConfig("file-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - when(userPasswdStore.verifyPassword("user1", new SecureString("test123"))).thenReturn(true); + when(userPasswdStore.verifyPassword(eq("user1"), eq(new SecureString("test123")), any(Supplier.class))) + .thenAnswer(VERIFY_PASSWORD_ANSWER); when(userRolesStore.roles("user1")).thenReturn(new String[]{"role1", "role2"}); FileRealm realm = new FileRealm(config, userPasswdStore, userRolesStore); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user1 = future.actionGet(); + User user1 = future.actionGet().getUser(); future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user2 = future.actionGet(); + User user2 = future.actionGet().getUser(); assertThat(user1, sameInstance(user2)); } @@ -84,43 +99,45 @@ public class FileRealmTests extends ESTestCase { RealmConfig config = new RealmConfig("file-test", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); userPasswdStore = spy(new UserPasswdStore(config)); userRolesStore = spy(new UserRolesStore(config)); - doReturn(true).when(userPasswdStore).verifyPassword("user1", new SecureString("test123")); + when(userPasswdStore.verifyPassword(eq("user1"), eq(new SecureString("test123")), any(Supplier.class))) + .thenAnswer(VERIFY_PASSWORD_ANSWER); doReturn(new String[] { "role1", "role2" }).when(userRolesStore).roles("user1"); FileRealm realm = new FileRealm(config, userPasswdStore, userRolesStore); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user1 = future.actionGet(); + User user1 = future.actionGet().getUser(); future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user2 = future.actionGet(); + User user2 = future.actionGet().getUser(); assertThat(user1, sameInstance(user2)); userPasswdStore.notifyRefresh(); future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user3 = future.actionGet(); + User user3 = future.actionGet().getUser(); assertThat(user2, not(sameInstance(user3))); future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user4 = future.actionGet(); + User user4 = future.actionGet().getUser(); assertThat(user3, sameInstance(user4)); userRolesStore.notifyRefresh(); future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user5 = future.actionGet(); + User user5 = future.actionGet().getUser(); assertThat(user4, not(sameInstance(user5))); future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user1", new SecureString("test123")), future, mock(IncomingRequest.class)); - User user6 = future.actionGet(); + User user6 = future.actionGet().getUser(); assertThat(user5, sameInstance(user6)); } public void testToken() throws Exception { RealmConfig config = new RealmConfig("file-test", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - when(userPasswdStore.verifyPassword("user1", new SecureString("test123"))).thenReturn(true); + when(userPasswdStore.verifyPassword(eq("user1"), eq(new SecureString("test123")), any(Supplier.class))) + .thenAnswer(VERIFY_PASSWORD_ANSWER); when(userRolesStore.roles("user1")).thenReturn(new String[]{"role1", "role2"}); FileRealm realm = new FileRealm(config, userPasswdStore, userRolesStore); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java index 424661ef513..2a5c72b875d 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java @@ -5,22 +5,6 @@ */ package org.elasticsearch.xpack.security.authc.file; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.watcher.ResourceWatcherService; -import org.elasticsearch.xpack.security.audit.logfile.CapturingLogger; -import org.elasticsearch.xpack.security.authc.RealmConfig; -import org.elasticsearch.xpack.security.authc.support.Hasher; -import org.elasticsearch.xpack.XPackPlugin; -import org.junit.After; -import org.junit.Before; - import java.io.BufferedWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -34,6 +18,24 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.XPackPlugin; +import org.elasticsearch.xpack.security.audit.logfile.CapturingLogger; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.security.authc.RealmConfig; +import org.elasticsearch.xpack.security.authc.support.Hasher; +import org.elasticsearch.xpack.security.user.User; +import org.junit.After; +import org.junit.Before; + import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; @@ -91,8 +93,11 @@ public class FileUserPasswdStoreTests extends ESTestCase { FileUserPasswdStore store = new FileUserPasswdStore(config, watcherService, latch::countDown); + User user = new User("bcrypt"); assertThat(store.userExists("bcrypt"), is(true)); - assertThat(store.verifyPassword("bcrypt", new SecureString("test123")), is(true)); + AuthenticationResult result = store.verifyPassword("bcrypt", new SecureString("test123"), () -> user); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), is(user)); watcherService.start(); @@ -106,7 +111,9 @@ public class FileUserPasswdStoreTests extends ESTestCase { } assertThat(store.userExists("foobar"), is(true)); - assertThat(store.verifyPassword("foobar", new SecureString("barfoo")), is(true)); + result = store.verifyPassword("foobar", new SecureString("barfoo"), () -> user); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), is(user)); } public void testStore_AutoReload_WithParseFailures() throws Exception { @@ -126,7 +133,10 @@ public class FileUserPasswdStoreTests extends ESTestCase { FileUserPasswdStore store = new FileUserPasswdStore(config, watcherService, latch::countDown); - assertTrue(store.verifyPassword("bcrypt", new SecureString("test123"))); + User user = new User("bcrypt"); + final AuthenticationResult result = store.verifyPassword("bcrypt", new SecureString("test123"), () -> user); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), is(user)); watcherService.start(); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java index 56fb5df3c0f..1ecacb0eaaf 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.ldap.ActiveDirectorySessionFactory.DownLevelADAuthenticator; import org.elasticsearch.xpack.security.authc.ldap.ActiveDirectorySessionFactory.UpnADAuthenticator; @@ -139,9 +140,11 @@ public class ActiveDirectoryRealmTests extends ESTestCase { DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + final User user = result.getUser(); assertThat(user, is(notNullValue())); assertThat(user.roles(), arrayContaining(containsString("Avengers"))); } @@ -154,9 +157,9 @@ public class ActiveDirectoryRealmTests extends ESTestCase { LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); // Thor does not have a UPN of form CN=Thor@ad.test.elasticsearch.com - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=Thor", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, is(notNullValue())); assertThat(user.roles(), arrayContaining(containsString("Avengers"))); } @@ -170,7 +173,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { return urls.toArray(Strings.EMPTY_ARRAY); } - public void testAuthenticateCachesSuccesfulAuthentications() throws Exception { + public void testAuthenticateCachesSuccessfulAuthentications() throws Exception { Settings settings = settings(); RealmConfig config = new RealmConfig("testAuthenticateCachesSuccesfulAuthentications", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); ActiveDirectorySessionFactory sessionFactory = spy(new ActiveDirectorySessionFactory(config, sslService)); @@ -179,7 +182,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { int count = randomIntBetween(2, 10); for (int i = 0; i < count; i++) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); future.actionGet(); } @@ -197,7 +200,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { int count = randomIntBetween(2, 10); for (int i = 0; i < count; i++) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); future.actionGet(); } @@ -215,7 +218,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { int count = randomIntBetween(2, 10); for (int i = 0; i < count; i++) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); future.actionGet(); } @@ -227,7 +230,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { roleMapper.notifyRefresh(); for (int i = 0; i < count; i++) { - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); future.actionGet(); } @@ -244,9 +247,9 @@ public class ActiveDirectoryRealmTests extends ESTestCase { DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, is(notNullValue())); assertThat(user.roles(), arrayContaining(equalTo("group_role"))); } @@ -260,9 +263,9 @@ public class ActiveDirectoryRealmTests extends ESTestCase { DnRoleMapper roleMapper = new DnRoleMapper(LdapRealm.AD_TYPE, config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealm.AD_TYPE, config, sessionFactory, roleMapper, threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=Thor", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, is(notNullValue())); assertThat(user.roles(), arrayContainingInAnyOrder(equalTo("group_role"), equalTo("user_role"))); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java index 83037e44d49..ddf3830590b 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java @@ -5,6 +5,10 @@ */ package org.elasticsearch.xpack.security.authc.ldap; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + import com.unboundid.ldap.sdk.LDAPURL; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; @@ -12,6 +16,10 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope; @@ -21,18 +29,11 @@ import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRea import org.elasticsearch.xpack.security.authc.support.DnRoleMapper; import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.security.user.User; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.ssl.SSLService; import org.elasticsearch.xpack.ssl.VerificationMode; import org.junit.After; import org.junit.Before; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - import static org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory.URLS_SETTING; import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.contains; @@ -86,9 +87,11 @@ public class LdapRealmTests extends LdapTestCase { LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + User user = result.getUser(); assertThat(user, notNullValue()); assertThat(user.roles(), arrayContaining("HMS Victory")); assertThat(user.metadata(), notNullValue()); @@ -109,9 +112,11 @@ public class LdapRealmTests extends LdapTestCase { LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + User user = result.getUser(); assertThat(user, notNullValue()); assertThat("For roles " + Arrays.toString(user.roles()), user.roles(), arrayContaining("HMS Victory")); assertThat(user.metadata(), notNullValue()); @@ -132,12 +137,14 @@ public class LdapRealmTests extends LdapTestCase { ldapFactory = spy(ldapFactory); LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - future.actionGet(); + assertThat(future.actionGet().getStatus(), is(AuthenticationResult.Status.SUCCESS)); + future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - future.actionGet(); + assertThat(future.actionGet().getStatus(), is(AuthenticationResult.Status.SUCCESS)); //verify one and only one session -> caching is working verify(ldapFactory, times(1)).session(anyString(), any(SecureString.class), any(ActionListener.class)); @@ -155,7 +162,7 @@ public class LdapRealmTests extends LdapTestCase { DnRoleMapper roleMapper = buildGroupAsRoleMapper(resourceWatcherService); ldapFactory = spy(ldapFactory); LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, roleMapper, threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); future.actionGet(); future = new PlainActionFuture<>(); @@ -188,7 +195,7 @@ public class LdapRealmTests extends LdapTestCase { ldapFactory = spy(ldapFactory); LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); future.actionGet(); future = new PlainActionFuture<>(); @@ -282,9 +289,11 @@ public class LdapRealmTests extends LdapTestCase { LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, new DnRoleMapper(LdapRealm.LDAP_TYPE, config, resourceWatcherService), threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken("Horatio Hornblower", new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + User user = result.getUser(); assertThat(user, notNullValue()); assertThat(user.roles(), arrayContaining("avenger")); } @@ -306,10 +315,14 @@ public class LdapRealmTests extends LdapTestCase { LdapRealm ldap = new LdapRealm(LdapRealm.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future, mock(IncomingRequest.class)); - User user = future.actionGet(); - assertThat(user, nullValue()); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.CONTINUE)); + assertThat(result.getUser(), nullValue()); + assertThat(result.getMessage(), is("authenticate failed")); + assertThat(result.getException(), notNullValue()); + assertThat(result.getException().getMessage(), containsString("UnknownHostException")); } public void testUsageStats() throws Exception { diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index 86403e49b3e..fae43db964e 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; @@ -104,9 +105,11 @@ public class PkiRealmTests extends ESTestCase { return null; }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(token, future, mock(IncomingRequest.class)); - User user = future.actionGet(); + final AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + User user = result.getUser(); assertThat(user, is(notNullValue())); assertThat(user.principal(), is("Elasticsearch Test Node")); assertThat(user.roles(), is(notNullValue())); @@ -128,9 +131,9 @@ public class PkiRealmTests extends ESTestCase { threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(token, future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, is(notNullValue())); assertThat(user.principal(), is("elasticsearch")); assertThat(user.roles(), is(notNullValue())); @@ -159,9 +162,9 @@ public class PkiRealmTests extends ESTestCase { threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(token, future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, is(notNullValue())); assertThat(user.principal(), is("Elasticsearch Test Node")); assertThat(user.roles(), is(notNullValue())); @@ -190,9 +193,9 @@ public class PkiRealmTests extends ESTestCase { threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(token, future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, is(nullValue())); } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java index 43c2565532e..917322f9a48 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.SecuritySettingsSource; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.Realm; import org.elasticsearch.xpack.security.authc.RealmConfig; @@ -58,8 +59,9 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { RealmConfig config = new RealmConfig("test_realm", settings, globalSettings, new ThreadContext(Settings.EMPTY)); CachingUsernamePasswordRealm realm = new CachingUsernamePasswordRealm("test", config) { @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { - listener.onResponse(new User("username", new String[]{"r1", "r2", "r3"})); + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { + listener.onResponse(AuthenticationResult.success(new User("username", new String[]{"r1", "r2", "r3"}))); } @Override @@ -74,7 +76,7 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { public void testAuthCache() { AlwaysAuthenticateCachingRealm realm = new AlwaysAuthenticateCachingRealm(globalSettings); SecureString pass = new SecureString("pass"); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("a", pass), future, mock(IncomingRequest.class)); future.actionGet(); future = new PlainActionFuture<>(); @@ -130,33 +132,37 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { public void testLookupAndAuthCache() { AlwaysAuthenticateCachingRealm realm = new AlwaysAuthenticateCachingRealm(globalSettings); // lookup first - PlainActionFuture future = new PlainActionFuture<>(); - realm.lookupUser("a", future); - User lookedUp = future.actionGet(); + PlainActionFuture lookupFuture = new PlainActionFuture<>(); + realm.lookupUser("a", lookupFuture); + User lookedUp = lookupFuture.actionGet(); assertThat(realm.lookupInvocationCounter.intValue(), is(1)); assertThat(realm.authInvocationCounter.intValue(), is(0)); assertThat(lookedUp.roles(), arrayContaining("lookupRole1", "lookupRole2")); // now authenticate - future = new PlainActionFuture<>(); - realm.authenticate(new UsernamePasswordToken("a", new SecureString("pass")), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + PlainActionFuture authFuture = new PlainActionFuture<>(); + realm.authenticate(new UsernamePasswordToken("a", new SecureString("pass")), authFuture, mock(IncomingRequest.class)); + AuthenticationResult authResult = authFuture.actionGet(); + assertThat(authResult.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + User user = authResult.getUser(); assertThat(realm.lookupInvocationCounter.intValue(), is(1)); assertThat(realm.authInvocationCounter.intValue(), is(1)); assertThat(user.roles(), arrayContaining("testRole1", "testRole2")); assertThat(user, not(sameInstance(lookedUp))); // authenticate a different user first - future = new PlainActionFuture<>(); - realm.authenticate(new UsernamePasswordToken("b", new SecureString("pass")), future, mock(IncomingRequest.class)); - user = future.actionGet(); + authFuture = new PlainActionFuture<>(); + realm.authenticate(new UsernamePasswordToken("b", new SecureString("pass")), authFuture, mock(IncomingRequest.class)); + authResult = authFuture.actionGet(); + assertThat(authResult.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + user = authResult.getUser(); assertThat(realm.lookupInvocationCounter.intValue(), is(1)); assertThat(realm.authInvocationCounter.intValue(), is(2)); assertThat(user.roles(), arrayContaining("testRole1", "testRole2")); //now lookup b - future = new PlainActionFuture<>(); - realm.lookupUser("b", future); - lookedUp = future.actionGet(); + lookupFuture = new PlainActionFuture<>(); + realm.lookupUser("b", lookupFuture); + lookedUp = lookupFuture.actionGet(); assertThat(realm.lookupInvocationCounter.intValue(), is(1)); assertThat(realm.authInvocationCounter.intValue(), is(2)); assertThat(user, sameInstance(lookedUp)); @@ -169,7 +175,7 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { SecureString pass1 = new SecureString("pass"); SecureString pass2 = new SecureString("password"); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken(user, pass1), future, mock(IncomingRequest.class)); future.actionGet(); future = new PlainActionFuture<>(); @@ -195,9 +201,9 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { String user = "testUser"; SecureString password = new SecureString("password"); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken(user, password), future, mock(IncomingRequest.class)); - assertThat(future.actionGet().enabled(), equalTo(false)); + assertThat(future.actionGet().getUser().enabled(), equalTo(false)); assertThat(realm.authInvocationCounter.intValue(), is(1)); @@ -205,14 +211,14 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken(user, password), future, mock(IncomingRequest.class)); future.actionGet(); - assertThat(future.actionGet().enabled(), equalTo(true)); + assertThat(future.actionGet().getUser().enabled(), equalTo(true)); assertThat(realm.authInvocationCounter.intValue(), is(2)); future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken(user, password), future, mock(IncomingRequest.class)); future.actionGet(); - assertThat(future.actionGet().enabled(), equalTo(true)); + assertThat(future.actionGet().getUser().enabled(), equalTo(true)); assertThat(realm.authInvocationCounter.intValue(), is(2)); } @@ -228,9 +234,9 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { final UsernamePasswordToken authToken = new UsernamePasswordToken("the-user", new SecureString("the-password")); // authenticate - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(authToken, future, mock(IncomingRequest.class)); - final User user1 = future.actionGet(); + final User user1 = future.actionGet().getUser(); assertThat(user1.roles(), arrayContaining("testRole1", "testRole2")); assertThat(realm.authInvocationCounter.intValue(), is(1)); @@ -239,7 +245,7 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { // authenticate future = new PlainActionFuture<>(); realm.authenticate(authToken, future, mock(IncomingRequest.class)); - final User user2 = future.actionGet(); + final User user2 = future.actionGet().getUser(); assertThat(user2.roles(), arrayContaining("testRole1", "testRole2")); assertThat(user2, not(sameInstance(user1))); assertThat(realm.authInvocationCounter.intValue(), is(2)); @@ -254,12 +260,12 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { AlwaysAuthenticateCachingRealm realm = new AlwaysAuthenticateCachingRealm(config); final UsernamePasswordToken authToken = new UsernamePasswordToken("the-user", new SecureString("the-password")); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); // authenticate realm.authenticate(authToken, future, mock(IncomingRequest.class)); final long start = System.currentTimeMillis(); - final User user1 = future.actionGet(); + final User user1 = future.actionGet().getUser(); assertThat(realm.authInvocationCounter.intValue(), is(1)); // After 100 ms (from the original start time), authenticate (read from cache). We don't care about the result @@ -278,7 +284,7 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { sleepUntil(start + 300); future = new PlainActionFuture<>(); realm.authenticate(authToken, future, mock(IncomingRequest.class)); - final User user2 = future.actionGet(); + final User user2 = future.actionGet().getUser(); assertThat(user2, not(sameInstance(user1))); // Due to slow VMs etc, the cache might have expired more than once during the test, but we can accept that. // We have other tests that verify caching works - this test just checks that it expires even when there are repeated reads. @@ -294,9 +300,9 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { public void testAuthenticateContract() throws Exception { Realm realm = new FailingAuthenticationRealm(Settings.EMPTY, globalSettings); - PlainActionFuture future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("user", new SecureString("pass")), future, mock(IncomingRequest.class)); - User user = future.actionGet(); + User user = future.actionGet().getUser(); assertThat(user, nullValue()); realm = new ThrowingAuthenticationRealm(Settings.EMPTY, globalSettings); @@ -329,12 +335,13 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { RealmConfig config = new RealmConfig("test_realm", Settings.EMPTY, globalSettings, new ThreadContext(Settings.EMPTY)); final CachingUsernamePasswordRealm realm = new CachingUsernamePasswordRealm("test", config) { @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { // do something slow if (BCrypt.checkpw(token.credentials(), passwordHash)) { - listener.onResponse(new User(username, new String[]{"r1", "r2", "r3"})); + listener.onResponse(AuthenticationResult.success(new User(username, new String[]{"r1", "r2", "r3"}))); } else { - listener.onResponse(null); + listener.onResponse(AuthenticationResult.unsuccessful("Incorrect password", null)); } } @@ -359,15 +366,15 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { for (int i = 0; i < numberOfIterations; i++) { UsernamePasswordToken token = new UsernamePasswordToken(username, invalidPassword ? randomPassword : password); - realm.authenticate(token, ActionListener.wrap((user) -> { - if (invalidPassword && user != null) { - throw new RuntimeException("invalid password led to an authenticated user: " + user.toString()); - } else if (invalidPassword == false && user == null) { - throw new RuntimeException("proper password led to a null user!"); + realm.authenticate(token, ActionListener.wrap((result) -> { + if (invalidPassword && result.isAuthenticated()) { + throw new RuntimeException("invalid password led to an authenticated user: " + result); + } else if (invalidPassword == false && result.isAuthenticated() == false) { + throw new RuntimeException("proper password led to an unauthenticated result: " + result); } }, (e) -> { logger.error("caught exception", e); - fail("unexpected exception"); + fail("unexpected exception - " + e); }), mock(IncomingRequest.class)); } @@ -392,7 +399,8 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { RealmConfig config = new RealmConfig("test_realm", Settings.EMPTY, globalSettings, new ThreadContext(Settings.EMPTY)); final CachingUsernamePasswordRealm realm = new CachingUsernamePasswordRealm("test", config) { @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { listener.onFailure(new UnsupportedOperationException("authenticate should not be called!")); } @@ -446,8 +454,9 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { } @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { - listener.onResponse(null); + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { + listener.onResponse(AuthenticationResult.notHandled()); } @Override @@ -463,7 +472,8 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { } @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { listener.onFailure(new RuntimeException("whatever exception")); } @@ -493,10 +503,11 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { } @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { authInvocationCounter.incrementAndGet(); final User user = new User(token.principal(), new String[]{"testRole1", "testRole2"}, null, null, emptyMap(), usersEnabled); - listener.onResponse(user); + listener.onResponse(AuthenticationResult.success(user)); } @Override @@ -516,9 +527,10 @@ public class CachingUsernamePasswordRealmTests extends ESTestCase { } @Override - protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, IncomingRequest incomingRequest) { + protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener, + IncomingRequest incomingRequest) { authInvocationCounter.incrementAndGet(); - listener.onResponse(new User(token.principal(), new String[]{"testRole1", "testRole2"})); + listener.onResponse(AuthenticationResult.success(new User(token.principal(), new String[]{"testRole1", "testRole2"}))); } @Override diff --git a/qa/security-example-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java b/qa/security-example-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java index 3e0fb1755bf..ec9a6e26420 100644 --- a/qa/security-example-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java +++ b/qa/security-example-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java @@ -8,6 +8,7 @@ package org.elasticsearch.example.realm; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.authc.support.CharArrays; import org.elasticsearch.xpack.security.user.User; @@ -49,13 +50,18 @@ public class CustomRealm extends Realm { } @Override - public void authenticate(AuthenticationToken authToken, ActionListener listener, IncomingRequest incomingRequest) { + public void authenticate(AuthenticationToken authToken, ActionListener listener, + IncomingRequest incomingRequest) { UsernamePasswordToken token = (UsernamePasswordToken)authToken; final String actualUser = token.principal(); - if (KNOWN_USER.equals(actualUser) && CharArrays.constantTimeEquals(token.credentials().getChars(), KNOWN_PW.getChars())) { - listener.onResponse(new User(actualUser, ROLES)); + if (KNOWN_USER.equals(actualUser)) { + if (CharArrays.constantTimeEquals(token.credentials().getChars(), KNOWN_PW.getChars())) { + listener.onResponse(AuthenticationResult.success(new User(actualUser, ROLES))); + } else { + listener.onResponse(AuthenticationResult.unsuccessful("Invalid password for user " + actualUser, null)); + } } else { - listener.onResponse(null); + listener.onResponse(AuthenticationResult.notHandled()); } } diff --git a/qa/security-example-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java b/qa/security-example-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java index 0029706a57b..6c3811372c9 100644 --- a/qa/security-example-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java +++ b/qa/security-example-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.security.authc.AuthenticationResult; import org.elasticsearch.xpack.security.authc.IncomingRequest; import org.elasticsearch.xpack.security.user.User; import org.elasticsearch.xpack.security.authc.RealmConfig; @@ -27,9 +28,9 @@ public class CustomRealmTests extends ESTestCase { CustomRealm realm = new CustomRealm(new RealmConfig("test", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings))); SecureString password = CustomRealm.KNOWN_PW.clone(); UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.KNOWN_USER, password); - PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); realm.authenticate(token, plainActionFuture, mock(IncomingRequest.class)); - User user = plainActionFuture.actionGet(); + User user = plainActionFuture.actionGet().getUser(); assertThat(user, notNullValue()); assertThat(user.roles(), equalTo(CustomRealm.ROLES)); assertThat(user.principal(), equalTo(CustomRealm.KNOWN_USER)); @@ -40,8 +41,9 @@ public class CustomRealmTests extends ESTestCase { CustomRealm realm = new CustomRealm(new RealmConfig("test", Settings.EMPTY, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings))); SecureString password = CustomRealm.KNOWN_PW.clone(); UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.KNOWN_USER + "1", password); - PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); realm.authenticate(token, plainActionFuture, mock(IncomingRequest.class)); - assertThat(plainActionFuture.actionGet(), nullValue()); + final AuthenticationResult result = plainActionFuture.actionGet(); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); } }