diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 7e4e8577d01c2..8a9be8b5a9cc2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -118,11 +118,27 @@ public AuthenticationService(Settings settings, Realms realms, AuditTrailService * 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 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 not fallback to anonymous. + * If {@code true}, then authentication will fallback to anonymous, if this service is + * configured to allow anonymous access (see {@link #isAnonymousUserEnabled}). + */ + public void authenticate(RestRequest request, boolean allowAnonymous, ActionListener authenticationListener) { + createAuthenticator(request, allowAnonymous, authenticationListener).authenticateAsync(); } /** @@ -133,15 +149,31 @@ public void authenticate(RestRequest request, ActionListener aut * * @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 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 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 void authenticate(String action, TransportMessage message, User fallbackU */ public void authenticate(String action, TransportMessage message, AuthenticationToken token, ActionListener 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 void onSecurityIndexStateChange(SecurityIndexManager.State previousState, } // pkg private method for testing - Authenticator createAuthenticator(RestRequest request, ActionListener listener) { - return new Authenticator(request, listener); + Authenticator createAuthenticator(RestRequest request, boolean fallbackToAnonymous, ActionListener listener) { + return new Authenticator(request, shouldFallbackToAnonymous(fallbackToAnonymous), listener); } // pkg private method for testing - Authenticator createAuthenticator(String action, TransportMessage message, User fallbackUser, ActionListener listener) { + Authenticator createAuthenticator(String action, TransportMessage message, boolean fallbackToAnonymous, + ActionListener listener) { + return new Authenticator(action, message, shouldFallbackToAnonymous(fallbackToAnonymous), listener); + } + + // pkg private method for testing + Authenticator createAuthenticator(String action, TransportMessage message, User fallbackUser, + ActionListener listener) { return new Authenticator(action, message, fallbackUser, listener); } @@ -192,6 +231,31 @@ long getNumInvalidation() { return numInvalidation.get(); } + /** + * Determines whether to support anonymous access for the current request. Returns {@code true} if all of the following are true + *
    + *
  • The service has anonymous authentication enabled (see {@link #isAnonymousUserEnabled})
  • + *
  • Anonymous access is accepted for this request ({@code allowAnonymousOnThisRequest} parameter) + *
  • 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.
  • + *
+ */ + 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 @@ class Authenticator { private final AuditableRequest request; private final User fallbackUser; + private final boolean fallbackToAnonymous; private final List defaultOrderedRealmList; private final ActionListener listener; @@ -208,18 +273,25 @@ class Authenticator { private AuthenticationToken authenticationToken = null; private AuthenticationResult authenticationResult = null; - Authenticator(RestRequest request, ActionListener listener) { - this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, listener); + Authenticator(RestRequest request, boolean fallbackToAnonymous, ActionListener listener) { + this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, fallbackToAnonymous, listener); + } + + Authenticator(String action, TransportMessage message, boolean fallbackToAnonymous, ActionListener listener) { + this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message), + null, fallbackToAnonymous, listener); } Authenticator(String action, TransportMessage message, User fallbackUser, ActionListener 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 listener) { + private Authenticator(AuditableRequest auditableRequest, User fallbackUser, boolean fallbackToAnonymous, + ActionListener listener) { this.request = auditableRequest; this.fallbackUser = fallbackUser; + this.fallbackToAnonymous = fallbackToAnonymous; this.defaultOrderedRealmList = realms.asList(); this.listener = listener; } @@ -479,7 +551,7 @@ void handleNullToken() { 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 @@ void handleNullToken() { 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 diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java index af06f30d504cd..23afe9248754a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java @@ -27,7 +27,6 @@ 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 @@ requests from all the nodes are attached with a user (either a serialize } 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) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java index 80d7b4b4d00ad..9bf0b4a6ab87c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java @@ -40,6 +40,7 @@ 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 void testApply() throws Exception { 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 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(null); return Void.TYPE; }).when(authzService) @@ -116,16 +120,19 @@ public void testApplyRestoresThreadContext() throws Exception { 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 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(null); return Void.TYPE; }).when(authzService) @@ -158,9 +165,10 @@ public void testApplyAsSystemUser() throws Exception { } 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 void testApplyDestructiveOperations() throws Exception { 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 void testActionProcessException() throws Exception { 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)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 0f2a42f0d72c9..3a9399494e9b9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -102,6 +102,7 @@ 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 void testTokenFirstMissingSecondFound() throws Exception { when(secondRealm.token(threadContext)).thenReturn(token); PlainActionFuture 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 void testTokenFirstMissingSecondFound() throws Exception { public void testTokenMissing() throws Exception { final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); PlainActionFuture 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 void testAuthenticateBothSupportSecondSucceeds() throws Exception { 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 void testAuthenticateSmartRealmOrdering() { // 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 void testAuthenticateSmartRealmOrdering() { // 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 void testAuthenticateSmartRealmOrdering() { // 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 void testAuthenticateSmartRealmOrderingDisabled() { 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 void testAuthenticateSmartRealmOrderingDisabled() { 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 void testAuthenticateFirstNotSupportingSecondSucceeds() throws Exception 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 void testTokenRestMissing() throws Exception { 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 void testAuthenticateRestDisabledUser() throws Exception { 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> 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 void testAuthenticateTransportContextAndHeader() throws Exception { final SetOnce 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 void testAuthenticateTransportContextAndHeader() throws Exception { 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 void testAuthenticateTransportContextAndHeader() throws Exception { 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 void testAnonymousUserRest() throws Exception { 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 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 void testRunAsLookupSameRealm() throws Exception { // 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 void testRunAsLookupDifferentRealm() throws Exception { // 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 void testAuthenticateWithToken() throws Exception { 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 void testInvalidToken() throws Exception { 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 @@ private Authentication authenticateBlocking(RestRequest restRequest) { private Authentication authenticateBlocking(String action, TransportMessage message, User fallbackUser) { PlainActionFuture 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(); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java index d8628fe31d853..e9e80bcf9745d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java @@ -37,6 +37,7 @@ 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 void testInbound() throws Exception { 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 future = new PlainActionFuture<>(); filter.inbound("_action", request, channel, future); @@ -97,12 +99,13 @@ public void testInboundDestructiveOperations() throws Exception { 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 void testInboundDestructiveOperations() throws Exception { 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 future = new PlainActionFuture<>(); @@ -139,12 +143,13 @@ public void testInboundAuthorizationException() throws Exception { 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 future = new PlainActionFuture<>(); @@ -163,25 +168,27 @@ public void testAllowsNodeActions() throws Exception { 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); }