Explorar el Código

Support authentication without anonymous user (#52094)

This change adds a new parameter to the authenticate methods in the
AuthenticationService to optionally exclude support for the anonymous
user (if an anonymous user exists).
Tim Vernum hace 5 años
padre
commit
ec1afb9f7c

+ 87 - 29
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java

@@ -118,11 +118,27 @@ public class AuthenticationService {
      * Authenticates the user that is associated with the given request. If the user was authenticated successfully (i.e.
      * a user was indeed associated with the request and the credentials were verified to be valid), the method returns
      * the user and that user is then "attached" to the request's context.
+     * This method will authenticate as the anonymous user if the service is configured to allow anonymous access.
      *
      * @param request The request to be authenticated
      */
     public void authenticate(RestRequest request, ActionListener<Authentication> authenticationListener) {
-        createAuthenticator(request, authenticationListener).authenticateAsync();
+        authenticate(request, true, authenticationListener);
+    }
+
+    /**
+     * Authenticates the user that is associated with the given request. If the user was authenticated successfully (i.e.
+     * a user was indeed associated with the request and the credentials were verified to be valid), the method returns
+     * the user and that user is then "attached" to the request's context.
+     * This method will optionally, authenticate as the anonymous user if the service is configured to allow anonymous access.
+     *
+     * @param request The request to be authenticated
+     * @param allowAnonymous If {@code false}, then authentication will <em>not</em> fallback to anonymous.
+     *                               If {@code true}, then authentication <em>will</em> fallback to anonymous, if this service is
+     *                               configured to allow anonymous access (see {@link #isAnonymousUserEnabled}).
+     */
+    public void authenticate(RestRequest request, boolean allowAnonymous, ActionListener<Authentication> authenticationListener) {
+        createAuthenticator(request, allowAnonymous, authenticationListener).authenticateAsync();
     }
 
     /**
@@ -133,15 +149,31 @@ public class AuthenticationService {
      *
      * @param action       The action of the message
      * @param message      The message to be authenticated
-     * @param fallbackUser The default user that will be assumed if no other user is attached to the message. Can be
-     *                     {@code null}, in which case there will be no fallback user and the success/failure of the
-     *                     authentication will be based on the whether there's an attached user to in the message and
-     *                     if there is, whether its credentials are valid.
+     * @param fallbackUser The default user that will be assumed if no other user is attached to the message. May not
+     *                      be {@code null}.
      */
     public void authenticate(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
+        Objects.requireNonNull(fallbackUser, "fallback user may not be null");
         createAuthenticator(action, message, fallbackUser, listener).authenticateAsync();
     }
 
+    /**
+     * Authenticates the user that is associated with the given message. If the user was authenticated successfully (i.e.
+     * a user was indeed associated with the request and the credentials were verified to be valid), the method returns
+     * the user and that user is then "attached" to the message's context.
+     * If no user or credentials are found to be attached to the given message, and the caller allows anonymous access
+     * ({@code allowAnonymous} parameter), and this service is configured for anonymous access (see {@link #isAnonymousUserEnabled} and
+     * {@link #anonymousUser}), then the anonymous user will be returned instead.
+     *
+     * @param action       The action of the message
+     * @param message      The message to be authenticated
+     * @param allowAnonymous Whether to permit anonymous access for this request (this only relevant if the service is
+     *                       {@link #isAnonymousUserEnabled configured for anonymous access}).
+     */
+    public void authenticate(String action, TransportMessage message, boolean allowAnonymous, ActionListener<Authentication> listener) {
+        createAuthenticator(action, message, allowAnonymous, listener).authenticateAsync();
+    }
+
     /**
      * Authenticates the user based on the contents of the token that is provided as parameter. This will not look at the values in the
      * ThreadContext for Authentication.
@@ -152,7 +184,7 @@ public class AuthenticationService {
      */
     public void authenticate(String action, TransportMessage message,
                              AuthenticationToken token, ActionListener<Authentication> listener) {
-        new Authenticator(action, message, null, listener).authenticateToken(token);
+        new Authenticator(action, message, shouldFallbackToAnonymous(true), listener).authenticateToken(token);
     }
 
     public void expire(String principal) {
@@ -178,12 +210,19 @@ public class AuthenticationService {
     }
 
     // pkg private method for testing
-    Authenticator createAuthenticator(RestRequest request, ActionListener<Authentication> listener) {
-        return new Authenticator(request, listener);
+    Authenticator createAuthenticator(RestRequest request, boolean fallbackToAnonymous, ActionListener<Authentication> listener) {
+        return new Authenticator(request, shouldFallbackToAnonymous(fallbackToAnonymous), listener);
     }
 
     // pkg private method for testing
-    Authenticator createAuthenticator(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
+    Authenticator createAuthenticator(String action, TransportMessage message, boolean fallbackToAnonymous,
+                                      ActionListener<Authentication> listener) {
+        return new Authenticator(action, message, shouldFallbackToAnonymous(fallbackToAnonymous), listener);
+    }
+
+    // pkg private method for testing
+    Authenticator createAuthenticator(String action, TransportMessage message, User fallbackUser,
+                                      ActionListener<Authentication> listener) {
         return new Authenticator(action, message, fallbackUser, listener);
     }
 
@@ -192,6 +231,31 @@ public class AuthenticationService {
         return numInvalidation.get();
     }
 
+    /**
+     * Determines whether to support anonymous access for the current request. Returns {@code true} if all of the following are true
+     * <ul>
+     *     <li>The service has anonymous authentication enabled (see {@link #isAnonymousUserEnabled})</li>
+     *     <li>Anonymous access is accepted for this request ({@code allowAnonymousOnThisRequest} parameter)
+     *     <li>The {@link ThreadContext} does not provide API Key or Bearer Token credentials. If these are present, we
+     *     treat the request as though it attempted to authenticate (even if that failed), and will not fall back to anonymous.</li>
+     * </ul>
+     */
+    boolean shouldFallbackToAnonymous(boolean allowAnonymousOnThisRequest) {
+        if (isAnonymousUserEnabled == false) {
+            return false;
+        }
+        if (allowAnonymousOnThisRequest == false) {
+            return false;
+        }
+        String header = threadContext.getHeader("Authorization");
+        if (Strings.hasText(header) &&
+            ((header.regionMatches(true, 0, "Bearer ", 0, "Bearer ".length()) && header.length() > "Bearer ".length()) ||
+                (header.regionMatches(true, 0, "ApiKey ", 0, "ApiKey ".length()) && header.length() > "ApiKey ".length()))) {
+            return false;
+        }
+        return true;
+    }
+
     /**
      * This class is responsible for taking a request and executing the authentication. The authentication is executed in an asynchronous
      * fashion in order to avoid blocking calls on a network thread. This class also performs the auditing necessary around authentication
@@ -200,6 +264,7 @@ public class AuthenticationService {
 
         private final AuditableRequest request;
         private final User fallbackUser;
+        private final boolean fallbackToAnonymous;
         private final List<Realm> defaultOrderedRealmList;
         private final ActionListener<Authentication> listener;
 
@@ -208,18 +273,25 @@ public class AuthenticationService {
         private AuthenticationToken authenticationToken = null;
         private AuthenticationResult authenticationResult = null;
 
-        Authenticator(RestRequest request, ActionListener<Authentication> listener) {
-            this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, listener);
+        Authenticator(RestRequest request, boolean fallbackToAnonymous, ActionListener<Authentication> listener) {
+            this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, fallbackToAnonymous, listener);
+        }
+
+        Authenticator(String action, TransportMessage message, boolean fallbackToAnonymous, ActionListener<Authentication> listener) {
+            this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message),
+                null, fallbackToAnonymous, listener);
         }
 
         Authenticator(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
-            this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message
-            ), fallbackUser, listener);
+            this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message),
+                Objects.requireNonNull(fallbackUser, "Fallback user cannot be null"), false, listener);
         }
 
-        private Authenticator(AuditableRequest auditableRequest, User fallbackUser, ActionListener<Authentication> listener) {
+        private Authenticator(AuditableRequest auditableRequest, User fallbackUser, boolean fallbackToAnonymous,
+                              ActionListener<Authentication> listener) {
             this.request = auditableRequest;
             this.fallbackUser = fallbackUser;
+            this.fallbackToAnonymous = fallbackToAnonymous;
             this.defaultOrderedRealmList = realms.asList();
             this.listener = listener;
         }
@@ -479,7 +551,7 @@ public class AuthenticationService {
                 RealmRef authenticatedBy = new RealmRef("__fallback", "__fallback", nodeName);
                 authentication = new Authentication(fallbackUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.INTERNAL,
                     Collections.emptyMap());
-            } else if (isAnonymousUserEnabled && shouldFallbackToAnonymous()) {
+            } else if (fallbackToAnonymous) {
                 logger.trace("No valid credentials found in request [{}], using anonymous [{}]", request, anonymousUser.principal());
                 RealmRef authenticatedBy = new RealmRef("__anonymous", "__anonymous", nodeName);
                 authentication = new Authentication(anonymousUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.ANONYMOUS,
@@ -503,20 +575,6 @@ public class AuthenticationService {
             action.run();
         }
 
-        /**
-         * When an API Key or an Elasticsearch Token Service token is used for authentication and authentication fails (as indicated by
-         * a null AuthenticationToken) we should not fallback to the anonymous user.
-         */
-        boolean shouldFallbackToAnonymous(){
-            String header = threadContext.getHeader("Authorization");
-            if (Strings.hasText(header) &&
-                ((header.regionMatches(true, 0, "Bearer ", 0, "Bearer ".length()) && header.length() > "Bearer ".length()) ||
-                (header.regionMatches(true, 0, "ApiKey ", 0, "ApiKey ".length()) && header.length() > "ApiKey ".length()))) {
-                return false;
-            }
-            return true;
-        }
-
         /**
          * Consumes the {@link User} that resulted from attempting to authenticate a token against the {@link Realms}. When the user is
          * {@code null}, authentication fails and does not proceed. When there is a user, the request is inspected to see if the run as

+ 1 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java

@@ -27,7 +27,6 @@ import org.elasticsearch.transport.nio.NioTcpChannel;
 import org.elasticsearch.xpack.core.security.SecurityContext;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.user.SystemUser;
-import org.elasticsearch.xpack.core.security.user.User;
 import org.elasticsearch.xpack.security.action.SecurityActionMapper;
 import org.elasticsearch.xpack.security.authc.AuthenticationService;
 import org.elasticsearch.xpack.security.authz.AuthorizationService;
@@ -100,7 +99,7 @@ final class ServerTransportFilter {
         }
 
         final Version version = transportChannel.getVersion();
-        authcService.authenticate(securityAction, request, (User)null, ActionListener.wrap((authentication) -> {
+        authcService.authenticate(securityAction, request, true, ActionListener.wrap((authentication) -> {
             if (authentication != null) {
                 if (securityAction.equals(TransportService.HANDSHAKE_ACTION_NAME) &&
                     SystemUser.is(authentication.getUser()) == false) {

+ 29 - 19
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java

@@ -40,6 +40,7 @@ import org.junit.Before;
 
 import java.util.Collections;
 
+import static org.hamcrest.Matchers.arrayWithSize;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.isA;
@@ -92,14 +93,17 @@ public class SecurityActionFilterTests extends ESTestCase {
         Task task = mock(Task.class);
         User user = new User("username", "r1", "r2");
         Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
         }).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
-        doAnswer((i) -> {
-            ActionListener<Void> callback = (ActionListener<Void>) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(null);
             return Void.TYPE;
         }).when(authzService)
@@ -116,16 +120,19 @@ public class SecurityActionFilterTests extends ESTestCase {
         Task task = mock(Task.class);
         User user = new User("username", "r1", "r2");
         Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
             threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
             callback.onResponse(authentication);
             return Void.TYPE;
         }).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
-        doAnswer((i) -> {
-            ActionListener<Void> callback = (ActionListener<Void>) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(null);
             return Void.TYPE;
         }).when(authzService)
@@ -158,9 +165,10 @@ public class SecurityActionFilterTests extends ESTestCase {
         } else {
             assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
         }
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
             return Void.TYPE;
         }).when(authcService).authenticate(eq(action), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
@@ -193,9 +201,10 @@ public class SecurityActionFilterTests extends ESTestCase {
         Task task = mock(Task.class);
         User user = new User("username", "r1", "r2");
         Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
         }).when(authcService).authenticate(eq(action), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
@@ -223,9 +232,10 @@ public class SecurityActionFilterTests extends ESTestCase {
         Task task = mock(Task.class);
         User user = new User("username", "r1", "r2");
         Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
         }).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));

+ 79 - 42
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java

@@ -102,6 +102,7 @@ import java.util.function.Consumer;
 
 import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_TERM;
 import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException;
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
 import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError;
 import static org.elasticsearch.xpack.security.authc.TokenServiceTests.mockGetTokenFromId;
 import static org.hamcrest.Matchers.arrayContaining;
@@ -251,7 +252,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         when(secondRealm.token(threadContext)).thenReturn(token);
 
         PlainActionFuture<Authentication> future = new PlainActionFuture<>();
-        Authenticator authenticator = service.createAuthenticator("_action", message, null, future);
+        Authenticator authenticator = service.createAuthenticator("_action", message, true, future);
         authenticator.extractToken((result) -> {
             assertThat(result, notNullValue());
             assertThat(result, is(token));
@@ -262,7 +263,7 @@ public class AuthenticationServiceTests extends ESTestCase {
     public void testTokenMissing() throws Exception {
         final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
         PlainActionFuture<Authentication> future = new PlainActionFuture<>();
-        Authenticator authenticator = service.createAuthenticator("_action", message, null, future);
+        Authenticator authenticator = service.createAuthenticator("_action", message, true, future);
         authenticator.extractToken((token) -> {
             assertThat(token, nullValue());
             authenticator.handleNullToken();
@@ -288,7 +289,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
 
         final AtomicBoolean completed = new AtomicBoolean(false);
-        service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+        service.authenticate("_action", message, true, ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), is(user));
             assertThat(result.getLookedUpBy(), is(nullValue()));
@@ -314,7 +315,7 @@ public class AuthenticationServiceTests extends ESTestCase {
 
         // Authenticate against the normal chain. 1st Realm will be checked (and not pass) then 2nd realm will successfully authc
         final AtomicBoolean completed = new AtomicBoolean(false);
-        service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+        service.authenticate("_action", message, true, ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), is(user));
             assertThat(result.getLookedUpBy(), is(nullValue()));
@@ -330,7 +331,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         // Authenticate against the smart chain.
         // "SecondRealm" will be at the top of the list and will successfully authc.
         // "FirstRealm" will not be used
-        service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+        service.authenticate("_action", message, true, ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), is(user));
             assertThat(result.getLookedUpBy(), is(nullValue()));
@@ -362,7 +363,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         // This will authenticate against the smart chain.
         // "SecondRealm" will be at the top of the list but will no longer authenticate the user.
         // Then "FirstRealm" will be checked.
-        service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+        service.authenticate("_action", message, true, ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), is(user));
             assertThat(result.getLookedUpBy(), is(nullValue()));
@@ -431,7 +432,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
 
         final AtomicBoolean completed = new AtomicBoolean(false);
-        service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+        service.authenticate("_action", message, true, ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), is(user));
             assertThat(result.getLookedUpBy(), is(nullValue()));
@@ -442,7 +443,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         assertTrue(completed.get());
 
         completed.set(false);
-        service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+        service.authenticate("_action", message, true, ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), is(user));
             assertThat(result.getLookedUpBy(), is(nullValue()));
@@ -473,7 +474,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
 
         final AtomicBoolean completed = new AtomicBoolean(false);
-        service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+        service.authenticate("_action", message, true, ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), is(user));
             assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM));
@@ -515,7 +516,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         when(firstRealm.token(threadContext)).thenReturn(null);
         when(secondRealm.token(threadContext)).thenReturn(null);
 
-        Authenticator authenticator = service.createAuthenticator(restRequest, mock(ActionListener.class));
+        Authenticator authenticator = service.createAuthenticator(restRequest, true, mock(ActionListener.class));
         authenticator.extractToken((token) -> {
             assertThat(token, nullValue());
         });
@@ -612,14 +613,19 @@ public class AuthenticationServiceTests extends ESTestCase {
 
     public void testAuthenticateTransportSuccess() throws Exception {
         final String reqId = AuditUtil.getOrGenerateRequestId(threadContext);
-        User user = new User("username", "r1", "r2");
-        User fallback = randomBoolean() ? SystemUser.INSTANCE : null;
+        final User user = new User("username", "r1", "r2");
+        final Consumer<ActionListener<Authentication>> authenticate;
+        if (randomBoolean()) {
+            authenticate = listener -> service.authenticate("_action", message, SystemUser.INSTANCE, listener);
+        } else {
+            authenticate = listener -> service.authenticate("_action", message, true, listener);
+        }
         when(firstRealm.token(threadContext)).thenReturn(token);
         when(firstRealm.supports(token)).thenReturn(true);
         mockAuthenticate(firstRealm, token, user);
 
         final AtomicBoolean completed = new AtomicBoolean(false);
-        service.authenticate("_action", message, fallback, ActionListener.wrap(result -> {
+        authenticate.accept(ActionListener.wrap(result -> {
             assertThat(result, notNullValue());
             assertThat(result.getUser(), sameInstance(user));
             assertThreadContextContainsAuthentication(result);
@@ -662,14 +668,14 @@ public class AuthenticationServiceTests extends ESTestCase {
         final SetOnce<String> authHeaderRef = new SetOnce<>();
         try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
             service.authenticate("_action", message, SystemUser.INSTANCE, ActionListener.wrap(authentication -> {
-                assertThat(authentication, notNullValue());
-                assertThat(authentication.getUser(), sameInstance(user1));
-                assertThat(authentication.getAuthenticationType(), is(AuthenticationType.REALM));
-                assertThreadContextContainsAuthentication(authentication);
-                authRef.set(authentication);
-                authHeaderRef.set(threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY));
-                setCompletedToTrue(completed);
-            }, this::logAndFail));
+                    assertThat(authentication, notNullValue());
+                    assertThat(authentication.getUser(), sameInstance(user1));
+                    assertThat(authentication.getAuthenticationType(), is(AuthenticationType.REALM));
+                    assertThreadContextContainsAuthentication(authentication);
+                    authRef.set(authentication);
+                    authHeaderRef.set(threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY));
+                    setCompletedToTrue(completed);
+                }, this::logAndFail));
         }
         assertTrue(completed.compareAndSet(true, false));
         reset(firstRealm);
@@ -686,10 +692,10 @@ public class AuthenticationServiceTests extends ESTestCase {
             threadContext1.putTransient(AuthenticationField.AUTHENTICATION_KEY, authRef.get());
             threadContext1.putHeader(AuthenticationField.AUTHENTICATION_KEY, authHeaderRef.get());
             service.authenticate("_action", message1, SystemUser.INSTANCE, ActionListener.wrap(ctxAuth -> {
-                assertThat(ctxAuth, sameInstance(authRef.get()));
-                assertThat(threadContext1.getHeader(AuthenticationField.AUTHENTICATION_KEY), sameInstance(authHeaderRef.get()));
-                setCompletedToTrue(completed);
-            }, this::logAndFail));
+                    assertThat(ctxAuth, sameInstance(authRef.get()));
+                    assertThat(threadContext1.getHeader(AuthenticationField.AUTHENTICATION_KEY), sameInstance(authHeaderRef.get()));
+                    setCompletedToTrue(completed);
+                }, this::logAndFail));
             assertTrue(completed.compareAndSet(true, false));
             verifyZeroInteractions(firstRealm);
             reset(firstRealm);
@@ -721,11 +727,11 @@ public class AuthenticationServiceTests extends ESTestCase {
                 new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool2, new AnonymousUser(Settings.EMPTY),
                 tokenService, apiKeyService);
             service.authenticate("_action", new InternalMessage(), SystemUser.INSTANCE, ActionListener.wrap(result -> {
-                assertThat(result, notNullValue());
-                assertThat(result.getUser(), equalTo(user1));
-                assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM));
-                setCompletedToTrue(completed);
-            }, this::logAndFail));
+                    assertThat(result, notNullValue());
+                    assertThat(result.getUser(), equalTo(user1));
+                    assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM));
+                    setCompletedToTrue(completed);
+                }, this::logAndFail));
             assertTrue(completed.get());
             verifyZeroInteractions(firstRealm);
         } finally {
@@ -824,6 +830,33 @@ public class AuthenticationServiceTests extends ESTestCase {
         verifyNoMoreInteractions(auditTrail);
     }
 
+    public void testAuthenticateRestRequestDisallowAnonymous() throws Exception {
+        final String username = randomBoolean() ? AnonymousUser.DEFAULT_ANONYMOUS_USERNAME : "_anon_" + randomAlphaOfLengthBetween(2, 6);
+        final Settings.Builder builder = Settings.builder()
+            .putList(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3");
+        if (username.equals(AnonymousUser.DEFAULT_ANONYMOUS_USERNAME) == false) {
+            builder.put(AnonymousUser.USERNAME_SETTING.getKey(), username);
+        }
+        Settings settings = builder.build();
+
+        final AnonymousUser anonymousUser = new AnonymousUser(settings);
+        service = new AuthenticationService(settings, realms, auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()),
+            threadPool, anonymousUser, tokenService, apiKeyService);
+        RestRequest request = new FakeRestRequest();
+
+        PlainActionFuture<Authentication> future = new PlainActionFuture<>();
+        service.authenticate(request, false, future);
+        final ElasticsearchSecurityException ex = expectThrows(ElasticsearchSecurityException.class, future::actionGet);
+
+        assertThat(ex, notNullValue());
+        assertThat(ex, throwableWithMessage(containsString("missing authentication credentials for REST request")));
+        assertThat(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY), nullValue());
+        assertThat(threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY), nullValue());
+        String reqId = expectAuditRequestId();
+        verify(auditTrail).anonymousAccessDenied(reqId, request);
+        verifyNoMoreInteractions(auditTrail);
+    }
+
     public void testAnonymousUserTransportNoDefaultUser() throws Exception {
         Settings settings = Settings.builder()
                 .putList(AnonymousUser.ROLES_SETTING.getKey(), "r1", "r2", "r3")
@@ -1071,7 +1104,7 @@ public class AuthenticationServiceTests extends ESTestCase {
 
         // we do not actually go async
         if (randomBoolean()) {
-            service.authenticate("_action", message, (User)null, listener);
+            service.authenticate("_action", message, true, listener);
         } else {
             service.authenticate(restRequest, listener);
         }
@@ -1110,7 +1143,7 @@ public class AuthenticationServiceTests extends ESTestCase {
 
         // call service asynchronously but it doesn't actually go async
         if (randomBoolean()) {
-            service.authenticate("_action", message, (User)null, listener);
+            service.authenticate("_action", message, true, listener);
         } else {
             service.authenticate(restRequest, listener);
         }
@@ -1220,14 +1253,14 @@ public class AuthenticationServiceTests extends ESTestCase {
         when(securityIndex.indexExists()).thenReturn(true);
         try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
             threadContext.putHeader("Authorization", "Bearer " + token);
-            service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
-                assertThat(result, notNullValue());
-                assertThat(result.getUser(), is(user));
-                assertThat(result.getLookedUpBy(), is(nullValue()));
-                assertThat(result.getAuthenticatedBy(), is(notNullValue()));
-                assertThat(result.getAuthenticationType(), is(AuthenticationType.TOKEN));
-                setCompletedToTrue(completed);
-            }, this::logAndFail));
+            service.authenticate("_action", message, true, ActionListener.wrap(result -> {
+                    assertThat(result, notNullValue());
+                    assertThat(result.getUser(), is(user));
+                    assertThat(result.getLookedUpBy(), is(nullValue()));
+                    assertThat(result.getAuthenticatedBy(), is(notNullValue()));
+                    assertThat(result.getAuthenticationType(), is(AuthenticationType.TOKEN));
+                    setCompletedToTrue(completed);
+                }, this::logAndFail));
         }
         assertTrue(completed.get());
         verify(auditTrail).authenticationSuccess(anyString(), eq("realm"), eq(user), eq("_action"), same(message));
@@ -1247,7 +1280,7 @@ public class AuthenticationServiceTests extends ESTestCase {
         AtomicBoolean success = new AtomicBoolean(false);
         try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
             threadContext.putHeader("Authorization", "Bearer " + Base64.getEncoder().encodeToString(randomBytes));
-            service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> {
+            service.authenticate("_action", message, true, ActionListener.wrap(result -> {
                 assertThat(result, notNullValue());
                 assertThat(result.getUser(), is(user));
                 assertThat(result.getLookedUpBy(), is(nullValue()));
@@ -1461,7 +1494,11 @@ public class AuthenticationServiceTests extends ESTestCase {
 
     private Authentication authenticateBlocking(String action, TransportMessage message, User fallbackUser) {
         PlainActionFuture<Authentication> future = new PlainActionFuture<>();
-        service.authenticate(action, message, fallbackUser, future);
+        if (fallbackUser == null) {
+            service.authenticate(action, message, true, future);
+        } else {
+            service.authenticate(action, message, fallbackUser, future);
+        }
         return future.actionGet();
     }
 

+ 32 - 25
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java

@@ -37,6 +37,7 @@ import java.util.Collections;
 
 import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError;
 import static org.elasticsearch.xpack.core.security.support.Exceptions.authorizationError;
+import static org.hamcrest.Matchers.arrayWithSize;
 import static org.hamcrest.Matchers.equalTo;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
@@ -76,12 +77,13 @@ public class ServerTransportFilterTests extends ESTestCase {
         Authentication authentication = mock(Authentication.class);
         when(authentication.getVersion()).thenReturn(Version.CURRENT);
         when(authentication.getUser()).thenReturn(SystemUser.INSTANCE);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
-        }).when(authcService).authenticate(eq("_action"), eq(request), eq((User)null), any(ActionListener.class));
+        }).when(authcService).authenticate(eq("_action"), eq(request), eq(true), any(ActionListener.class));
         ServerTransportFilter filter = getNodeFilter();
         PlainActionFuture<Void> future = new PlainActionFuture<>();
         filter.inbound("_action", request, channel, future);
@@ -97,12 +99,13 @@ public class ServerTransportFilterTests extends ESTestCase {
         Authentication authentication = mock(Authentication.class);
         when(authentication.getVersion()).thenReturn(Version.CURRENT);
         when(authentication.getUser()).thenReturn(SystemUser.INSTANCE);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
-        }).when(authcService).authenticate(eq(action), eq(request), eq((User)null), any(ActionListener.class));
+        }).when(authcService).authenticate(eq(action), eq(request), eq(true), any(ActionListener.class));
         ServerTransportFilter filter = getNodeFilter();
         PlainActionFuture listener = mock(PlainActionFuture.class);
         filter.inbound(action, request, channel, listener);
@@ -117,12 +120,13 @@ public class ServerTransportFilterTests extends ESTestCase {
     public void testInboundAuthenticationException() throws Exception {
         TransportRequest request = mock(TransportRequest.class);
         Exception authE = authenticationError("authc failed");
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onFailure(authE);
             return Void.TYPE;
-        }).when(authcService).authenticate(eq("_action"), eq(request), eq((User)null), any(ActionListener.class));
+        }).when(authcService).authenticate(eq("_action"), eq(request), eq(true), any(ActionListener.class));
         ServerTransportFilter filter = getNodeFilter();
         try {
             PlainActionFuture<Void> future = new PlainActionFuture<>();
@@ -139,12 +143,13 @@ public class ServerTransportFilterTests extends ESTestCase {
         ServerTransportFilter filter = getNodeFilter();
         TransportRequest request = mock(TransportRequest.class);
         Authentication authentication = mock(Authentication.class);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
-        }).when(authcService).authenticate(eq("_action"), eq(request), eq((User)null), any(ActionListener.class));
+        }).when(authcService).authenticate(eq("_action"), eq(request), eq(true), any(ActionListener.class));
         when(authentication.getVersion()).thenReturn(Version.CURRENT);
         when(authentication.getUser()).thenReturn(XPackUser.INSTANCE);
         PlainActionFuture<Void> future = new PlainActionFuture<>();
@@ -163,25 +168,27 @@ public class ServerTransportFilterTests extends ESTestCase {
         ServerTransportFilter filter = getNodeFilter();
         TransportRequest request = mock(TransportRequest.class);
         Authentication authentication = new Authentication(new User("test", "superuser"), new RealmRef("test", "test", "node1"), null);
-        doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+        doAnswer(i -> {
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
-        }).when(authcService).authenticate(eq(internalAction), eq(request), eq((User)null), any(ActionListener.class));
+        }).when(authcService).authenticate(eq(internalAction), eq(request), eq(true), any(ActionListener.class));
         doAnswer((i) -> {
-            ActionListener callback =
-                    (ActionListener) i.getArguments()[3];
+            final Object[] args = i.getArguments();
+            assertThat(args, arrayWithSize(4));
+            ActionListener callback = (ActionListener) args[args.length - 1];
             callback.onResponse(authentication);
             return Void.TYPE;
-        }).when(authcService).authenticate(eq(nodeOrShardAction), eq(request), eq((User)null), any(ActionListener.class));
+        }).when(authcService).authenticate(eq(nodeOrShardAction), eq(request), eq(true), any(ActionListener.class));
 
         filter.inbound(internalAction, request, channel, new PlainActionFuture<>());
-        verify(authcService).authenticate(eq(internalAction), eq(request), eq((User)null), any(ActionListener.class));
+        verify(authcService).authenticate(eq(internalAction), eq(request), eq(true), any(ActionListener.class));
         verify(authzService).authorize(eq(authentication), eq(internalAction), eq(request), any(ActionListener.class));
 
         filter.inbound(nodeOrShardAction, request, channel, new PlainActionFuture<>());
-        verify(authcService).authenticate(eq(nodeOrShardAction), eq(request), eq((User)null), any(ActionListener.class));
+        verify(authcService).authenticate(eq(nodeOrShardAction), eq(request), eq(true), any(ActionListener.class));
         verify(authzService).authorize(eq(authentication), eq(nodeOrShardAction), eq(request), any(ActionListener.class));
         verifyNoMoreInteractions(authcService, authzService);
     }