Skip to content

Commit bac1740

Browse files
authored
Support authentication without anonymous user (#53528)
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). Backport of: #52094
1 parent 9dfcc07 commit bac1740

File tree

5 files changed

+228
-117
lines changed

5 files changed

+228
-117
lines changed

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

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,27 @@ public AuthenticationService(Settings settings, Realms realms, AuditTrailService
118118
* Authenticates the user that is associated with the given request. If the user was authenticated successfully (i.e.
119119
* a user was indeed associated with the request and the credentials were verified to be valid), the method returns
120120
* the user and that user is then "attached" to the request's context.
121+
* This method will authenticate as the anonymous user if the service is configured to allow anonymous access.
121122
*
122123
* @param request The request to be authenticated
123124
*/
124125
public void authenticate(RestRequest request, ActionListener<Authentication> authenticationListener) {
125-
createAuthenticator(request, authenticationListener).authenticateAsync();
126+
authenticate(request, true, authenticationListener);
127+
}
128+
129+
/**
130+
* Authenticates the user that is associated with the given request. If the user was authenticated successfully (i.e.
131+
* a user was indeed associated with the request and the credentials were verified to be valid), the method returns
132+
* the user and that user is then "attached" to the request's context.
133+
* This method will optionally, authenticate as the anonymous user if the service is configured to allow anonymous access.
134+
*
135+
* @param request The request to be authenticated
136+
* @param allowAnonymous If {@code false}, then authentication will <em>not</em> fallback to anonymous.
137+
* If {@code true}, then authentication <em>will</em> fallback to anonymous, if this service is
138+
* configured to allow anonymous access (see {@link #isAnonymousUserEnabled}).
139+
*/
140+
public void authenticate(RestRequest request, boolean allowAnonymous, ActionListener<Authentication> authenticationListener) {
141+
createAuthenticator(request, allowAnonymous, authenticationListener).authenticateAsync();
126142
}
127143

128144
/**
@@ -133,15 +149,31 @@ public void authenticate(RestRequest request, ActionListener<Authentication> aut
133149
*
134150
* @param action The action of the message
135151
* @param message The message to be authenticated
136-
* @param fallbackUser The default user that will be assumed if no other user is attached to the message. Can be
137-
* {@code null}, in which case there will be no fallback user and the success/failure of the
138-
* authentication will be based on the whether there's an attached user to in the message and
139-
* if there is, whether its credentials are valid.
152+
* @param fallbackUser The default user that will be assumed if no other user is attached to the message. May not
153+
* be {@code null}.
140154
*/
141155
public void authenticate(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
156+
Objects.requireNonNull(fallbackUser, "fallback user may not be null");
142157
createAuthenticator(action, message, fallbackUser, listener).authenticateAsync();
143158
}
144159

160+
/**
161+
* Authenticates the user that is associated with the given message. If the user was authenticated successfully (i.e.
162+
* a user was indeed associated with the request and the credentials were verified to be valid), the method returns
163+
* the user and that user is then "attached" to the message's context.
164+
* If no user or credentials are found to be attached to the given message, and the caller allows anonymous access
165+
* ({@code allowAnonymous} parameter), and this service is configured for anonymous access (see {@link #isAnonymousUserEnabled} and
166+
* {@link #anonymousUser}), then the anonymous user will be returned instead.
167+
*
168+
* @param action The action of the message
169+
* @param message The message to be authenticated
170+
* @param allowAnonymous Whether to permit anonymous access for this request (this only relevant if the service is
171+
* {@link #isAnonymousUserEnabled configured for anonymous access}).
172+
*/
173+
public void authenticate(String action, TransportMessage message, boolean allowAnonymous, ActionListener<Authentication> listener) {
174+
createAuthenticator(action, message, allowAnonymous, listener).authenticateAsync();
175+
}
176+
145177
/**
146178
* Authenticates the user based on the contents of the token that is provided as parameter. This will not look at the values in the
147179
* ThreadContext for Authentication.
@@ -152,7 +184,7 @@ public void authenticate(String action, TransportMessage message, User fallbackU
152184
*/
153185
public void authenticate(String action, TransportMessage message,
154186
AuthenticationToken token, ActionListener<Authentication> listener) {
155-
new Authenticator(action, message, null, listener).authenticateToken(token);
187+
new Authenticator(action, message, shouldFallbackToAnonymous(true), listener).authenticateToken(token);
156188
}
157189

158190
public void expire(String principal) {
@@ -178,12 +210,19 @@ public void onSecurityIndexStateChange(SecurityIndexManager.State previousState,
178210
}
179211

180212
// pkg private method for testing
181-
Authenticator createAuthenticator(RestRequest request, ActionListener<Authentication> listener) {
182-
return new Authenticator(request, listener);
213+
Authenticator createAuthenticator(RestRequest request, boolean fallbackToAnonymous, ActionListener<Authentication> listener) {
214+
return new Authenticator(request, shouldFallbackToAnonymous(fallbackToAnonymous), listener);
183215
}
184216

185217
// pkg private method for testing
186-
Authenticator createAuthenticator(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
218+
Authenticator createAuthenticator(String action, TransportMessage message, boolean fallbackToAnonymous,
219+
ActionListener<Authentication> listener) {
220+
return new Authenticator(action, message, shouldFallbackToAnonymous(fallbackToAnonymous), listener);
221+
}
222+
223+
// pkg private method for testing
224+
Authenticator createAuthenticator(String action, TransportMessage message, User fallbackUser,
225+
ActionListener<Authentication> listener) {
187226
return new Authenticator(action, message, fallbackUser, listener);
188227
}
189228

@@ -192,6 +231,31 @@ long getNumInvalidation() {
192231
return numInvalidation.get();
193232
}
194233

234+
/**
235+
* Determines whether to support anonymous access for the current request. Returns {@code true} if all of the following are true
236+
* <ul>
237+
* <li>The service has anonymous authentication enabled (see {@link #isAnonymousUserEnabled})</li>
238+
* <li>Anonymous access is accepted for this request ({@code allowAnonymousOnThisRequest} parameter)
239+
* <li>The {@link ThreadContext} does not provide API Key or Bearer Token credentials. If these are present, we
240+
* treat the request as though it attempted to authenticate (even if that failed), and will not fall back to anonymous.</li>
241+
* </ul>
242+
*/
243+
boolean shouldFallbackToAnonymous(boolean allowAnonymousOnThisRequest) {
244+
if (isAnonymousUserEnabled == false) {
245+
return false;
246+
}
247+
if (allowAnonymousOnThisRequest == false) {
248+
return false;
249+
}
250+
String header = threadContext.getHeader("Authorization");
251+
if (Strings.hasText(header) &&
252+
((header.regionMatches(true, 0, "Bearer ", 0, "Bearer ".length()) && header.length() > "Bearer ".length()) ||
253+
(header.regionMatches(true, 0, "ApiKey ", 0, "ApiKey ".length()) && header.length() > "ApiKey ".length()))) {
254+
return false;
255+
}
256+
return true;
257+
}
258+
195259
/**
196260
* This class is responsible for taking a request and executing the authentication. The authentication is executed in an asynchronous
197261
* 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 {
200264

201265
private final AuditableRequest request;
202266
private final User fallbackUser;
267+
private final boolean fallbackToAnonymous;
203268
private final List<Realm> defaultOrderedRealmList;
204269
private final ActionListener<Authentication> listener;
205270

@@ -208,18 +273,25 @@ class Authenticator {
208273
private AuthenticationToken authenticationToken = null;
209274
private AuthenticationResult authenticationResult = null;
210275

211-
Authenticator(RestRequest request, ActionListener<Authentication> listener) {
212-
this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, listener);
276+
Authenticator(RestRequest request, boolean fallbackToAnonymous, ActionListener<Authentication> listener) {
277+
this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, fallbackToAnonymous, listener);
278+
}
279+
280+
Authenticator(String action, TransportMessage message, boolean fallbackToAnonymous, ActionListener<Authentication> listener) {
281+
this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message),
282+
null, fallbackToAnonymous, listener);
213283
}
214284

215285
Authenticator(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
216-
this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message
217-
), fallbackUser, listener);
286+
this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message),
287+
Objects.requireNonNull(fallbackUser, "Fallback user cannot be null"), false, listener);
218288
}
219289

220-
private Authenticator(AuditableRequest auditableRequest, User fallbackUser, ActionListener<Authentication> listener) {
290+
private Authenticator(AuditableRequest auditableRequest, User fallbackUser, boolean fallbackToAnonymous,
291+
ActionListener<Authentication> listener) {
221292
this.request = auditableRequest;
222293
this.fallbackUser = fallbackUser;
294+
this.fallbackToAnonymous = fallbackToAnonymous;
223295
this.defaultOrderedRealmList = realms.asList();
224296
this.listener = listener;
225297
}
@@ -479,7 +551,7 @@ void handleNullToken() {
479551
RealmRef authenticatedBy = new RealmRef("__fallback", "__fallback", nodeName);
480552
authentication = new Authentication(fallbackUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.INTERNAL,
481553
Collections.emptyMap());
482-
} else if (isAnonymousUserEnabled && shouldFallbackToAnonymous()) {
554+
} else if (fallbackToAnonymous) {
483555
logger.trace("No valid credentials found in request [{}], using anonymous [{}]", request, anonymousUser.principal());
484556
RealmRef authenticatedBy = new RealmRef("__anonymous", "__anonymous", nodeName);
485557
authentication = new Authentication(anonymousUser, authenticatedBy, null, Version.CURRENT, AuthenticationType.ANONYMOUS,
@@ -503,20 +575,6 @@ void handleNullToken() {
503575
action.run();
504576
}
505577

506-
/**
507-
* When an API Key or an Elasticsearch Token Service token is used for authentication and authentication fails (as indicated by
508-
* a null AuthenticationToken) we should not fallback to the anonymous user.
509-
*/
510-
boolean shouldFallbackToAnonymous(){
511-
String header = threadContext.getHeader("Authorization");
512-
if (Strings.hasText(header) &&
513-
((header.regionMatches(true, 0, "Bearer ", 0, "Bearer ".length()) && header.length() > "Bearer ".length()) ||
514-
(header.regionMatches(true, 0, "ApiKey ", 0, "ApiKey ".length()) && header.length() > "ApiKey ".length()))) {
515-
return false;
516-
}
517-
return true;
518-
}
519-
520578
/**
521579
* Consumes the {@link User} that resulted from attempting to authenticate a token against the {@link Realms}. When the user is
522580
* {@code null}, authentication fails and does not proceed. When there is a user, the request is inspected to see if the run as

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.elasticsearch.xpack.core.security.SecurityContext;
2828
import org.elasticsearch.xpack.core.security.authc.Authentication;
2929
import org.elasticsearch.xpack.core.security.user.SystemUser;
30-
import org.elasticsearch.xpack.core.security.user.User;
3130
import org.elasticsearch.xpack.security.action.SecurityActionMapper;
3231
import org.elasticsearch.xpack.security.authc.AuthenticationService;
3332
import org.elasticsearch.xpack.security.authz.AuthorizationService;
@@ -118,7 +117,7 @@ requests from all the nodes are attached with a user (either a serialize
118117
}
119118

120119
final Version version = transportChannel.getVersion();
121-
authcService.authenticate(securityAction, request, (User)null, ActionListener.wrap((authentication) -> {
120+
authcService.authenticate(securityAction, request, true, ActionListener.wrap((authentication) -> {
122121
if (authentication != null) {
123122
if (securityAction.equals(TransportService.HANDSHAKE_ACTION_NAME) &&
124123
SystemUser.is(authentication.getUser()) == false) {

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

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import java.util.Collections;
4242

43+
import static org.hamcrest.Matchers.arrayWithSize;
4344
import static org.mockito.Matchers.any;
4445
import static org.mockito.Matchers.eq;
4546
import static org.mockito.Matchers.isA;
@@ -92,14 +93,17 @@ public void testApply() throws Exception {
9293
Task task = mock(Task.class);
9394
User user = new User("username", "r1", "r2");
9495
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
95-
doAnswer((i) -> {
96-
ActionListener callback =
97-
(ActionListener) i.getArguments()[3];
96+
doAnswer(i -> {
97+
final Object[] args = i.getArguments();
98+
assertThat(args, arrayWithSize(4));
99+
ActionListener callback = (ActionListener) args[args.length - 1];
98100
callback.onResponse(authentication);
99101
return Void.TYPE;
100102
}).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
101-
doAnswer((i) -> {
102-
ActionListener<Void> callback = (ActionListener<Void>) i.getArguments()[3];
103+
doAnswer(i -> {
104+
final Object[] args = i.getArguments();
105+
assertThat(args, arrayWithSize(4));
106+
ActionListener callback = (ActionListener) args[args.length - 1];
103107
callback.onResponse(null);
104108
return Void.TYPE;
105109
}).when(authzService)
@@ -116,16 +120,19 @@ public void testApplyRestoresThreadContext() throws Exception {
116120
Task task = mock(Task.class);
117121
User user = new User("username", "r1", "r2");
118122
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
119-
doAnswer((i) -> {
120-
ActionListener callback =
121-
(ActionListener) i.getArguments()[3];
123+
doAnswer(i -> {
124+
final Object[] args = i.getArguments();
125+
assertThat(args, arrayWithSize(4));
126+
ActionListener callback = (ActionListener) args[args.length - 1];
122127
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
123128
threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication);
124129
callback.onResponse(authentication);
125130
return Void.TYPE;
126131
}).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
127-
doAnswer((i) -> {
128-
ActionListener<Void> callback = (ActionListener<Void>) i.getArguments()[3];
132+
doAnswer(i -> {
133+
final Object[] args = i.getArguments();
134+
assertThat(args, arrayWithSize(4));
135+
ActionListener callback = (ActionListener) args[args.length - 1];
129136
callback.onResponse(null);
130137
return Void.TYPE;
131138
}).when(authzService)
@@ -158,9 +165,10 @@ public void testApplyAsSystemUser() throws Exception {
158165
} else {
159166
assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
160167
}
161-
doAnswer((i) -> {
162-
ActionListener callback =
163-
(ActionListener) i.getArguments()[3];
168+
doAnswer(i -> {
169+
final Object[] args = i.getArguments();
170+
assertThat(args, arrayWithSize(4));
171+
ActionListener callback = (ActionListener) args[args.length - 1];
164172
callback.onResponse(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY));
165173
return Void.TYPE;
166174
}).when(authcService).authenticate(eq(action), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
@@ -193,9 +201,10 @@ public void testApplyDestructiveOperations() throws Exception {
193201
Task task = mock(Task.class);
194202
User user = new User("username", "r1", "r2");
195203
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
196-
doAnswer((i) -> {
197-
ActionListener callback =
198-
(ActionListener) i.getArguments()[3];
204+
doAnswer(i -> {
205+
final Object[] args = i.getArguments();
206+
assertThat(args, arrayWithSize(4));
207+
ActionListener callback = (ActionListener) args[args.length - 1];
199208
callback.onResponse(authentication);
200209
return Void.TYPE;
201210
}).when(authcService).authenticate(eq(action), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));
@@ -223,9 +232,10 @@ public void testActionProcessException() throws Exception {
223232
Task task = mock(Task.class);
224233
User user = new User("username", "r1", "r2");
225234
Authentication authentication = new Authentication(user, new RealmRef("test", "test", "foo"), null);
226-
doAnswer((i) -> {
227-
ActionListener callback =
228-
(ActionListener) i.getArguments()[3];
235+
doAnswer(i -> {
236+
final Object[] args = i.getArguments();
237+
assertThat(args, arrayWithSize(4));
238+
ActionListener callback = (ActionListener) args[args.length - 1];
229239
callback.onResponse(authentication);
230240
return Void.TYPE;
231241
}).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class));

0 commit comments

Comments
 (0)