Skip to content

Support authentication without anonymous user #52094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<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();
}

/**
Expand All @@ -133,15 +149,31 @@ public void authenticate(RestRequest request, ActionListener<Authentication> 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<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.
Expand All @@ -152,7 +184,7 @@ public void authenticate(String action, TransportMessage message, User fallbackU
*/
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) {
Expand All @@ -178,12 +210,19 @@ public void onSecurityIndexStateChange(SecurityIndexManager.State previousState,
}

// 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);
}

Expand All @@ -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
* <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
Expand All @@ -200,6 +264,7 @@ class Authenticator {

private final AuditableRequest request;
private final User fallbackUser;
private final boolean fallbackToAnonymous;
private final List<Realm> defaultOrderedRealmList;
private final ActionListener<Authentication> listener;

Expand All @@ -208,18 +273,25 @@ class Authenticator {
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;
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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)
Expand All @@ -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<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)
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down
Loading