Skip to content

Commit 615c3cd

Browse files
authored
Restrict run-as to realm and api_key authentication types (#84336) (#84399)
This PR removes run-as support for authentication types other than realm and API key. The change essentially makes the behaviour closer to the existing one (in released versions) except for API keys. This is not to say that the existing behaviour is the best. But we need more time to agree on the new behaviour. Relates: #79809
1 parent 5a94989 commit 615c3cd

File tree

5 files changed

+152
-32
lines changed

5 files changed

+152
-32
lines changed

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.elasticsearch.xpack.core.security.authc;
99

1010
import org.elasticsearch.Version;
11+
import org.elasticsearch.common.settings.Settings;
1112
import org.elasticsearch.test.ESTestCase;
1213
import org.elasticsearch.test.VersionUtils;
1314
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
@@ -16,7 +17,12 @@
1617
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
1718
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
1819
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
20+
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
21+
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
22+
import org.elasticsearch.xpack.core.security.user.SystemUser;
1923
import org.elasticsearch.xpack.core.security.user.User;
24+
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
25+
import org.elasticsearch.xpack.core.security.user.XPackUser;
2026

2127
import java.util.Arrays;
2228
import java.util.EnumSet;
@@ -26,6 +32,12 @@
2632
import java.util.Set;
2733
import java.util.stream.Collectors;
2834

35+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_NAME;
36+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_TYPE;
37+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME;
38+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE;
39+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME;
40+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE;
2941
import static org.hamcrest.Matchers.is;
3042

3143
public class AuthenticationTests extends ESTestCase {
@@ -258,6 +270,40 @@ public static Authentication randomServiceAccountAuthentication() {
258270
);
259271
}
260272

273+
public static Authentication randomRealmAuthentication() {
274+
return new Authentication(randomUser(), randomRealm(), null);
275+
}
276+
277+
public static Authentication randomInternalAuthentication() {
278+
String nodeName = randomAlphaOfLengthBetween(3, 8);
279+
return randomFrom(
280+
new Authentication(
281+
randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, AsyncSearchUser.INSTANCE),
282+
new RealmRef(ATTACH_REALM_NAME, ATTACH_REALM_TYPE, nodeName),
283+
null
284+
),
285+
new Authentication(SystemUser.INSTANCE, new RealmRef(FALLBACK_REALM_NAME, FALLBACK_REALM_TYPE, nodeName), null)
286+
);
287+
}
288+
289+
public static Authentication randomAnonymousAuthentication() {
290+
Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anon_role").build();
291+
String nodeName = randomAlphaOfLengthBetween(3, 8);
292+
return new Authentication(new AnonymousUser(settings), new RealmRef(ANONYMOUS_REALM_NAME, ANONYMOUS_REALM_TYPE, nodeName), null);
293+
}
294+
295+
public static Authentication toToken(Authentication authentication) {
296+
final Authentication newTokenAuthentication = new Authentication(
297+
authentication.getUser(),
298+
authentication.getAuthenticatedBy(),
299+
authentication.getLookedUpBy(),
300+
Version.CURRENT,
301+
AuthenticationType.TOKEN,
302+
authentication.getMetadata()
303+
);
304+
return newTokenAuthentication;
305+
}
306+
261307
private boolean realmIsSingleton(RealmRef realmRef) {
262308
return Set.of(FileRealmSettings.TYPE, NativeRealmSettings.TYPE).contains(realmRef.getType());
263309
}

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/RunAsIntegTests.java

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ public void testRunAsUsingApiKey() throws IOException {
175175
);
176176
assertThat(authenticateJsonView.get("username"), equalTo(runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER));
177177
assertThat(authenticateJsonView.get("authentication_realm.type"), equalTo("_es_api_key"));
178+
assertThat(authenticateJsonView.get("lookup_realm.type"), equalTo("file"));
178179
assertThat(authenticateJsonView.get("authentication_type"), equalTo("api_key"));
179180

180181
final Request getUserRequest = new Request("GET", "/_security/user");
@@ -195,7 +196,7 @@ public void testRunAsUsingApiKey() throws IOException {
195196
}
196197
}
197198

198-
public void testRunAsUsingOAuthToken() throws IOException {
199+
public void testRunAsIgnoredForOAuthToken() throws IOException {
199200
final Request createTokenRequest = new Request("POST", "/_security/oauth2/token");
200201
createTokenRequest.setJsonEntity("{\"grant_type\":\"client_credentials\"}");
201202
createTokenRequest.setOptions(
@@ -208,41 +209,19 @@ public void testRunAsUsingOAuthToken() throws IOException {
208209
createTokenResponse.getEntity().getContent()
209210
);
210211

211-
final boolean runAsTestUser = randomBoolean();
212-
213212
final Request authenticateRequest = new Request("GET", "/_security/_authenticate");
214213
authenticateRequest.setOptions(
215214
authenticateRequest.getOptions()
216215
.toBuilder()
217216
.addHeader("Authorization", "Bearer " + tokenMapView.get("access_token"))
218-
.addHeader(
219-
AuthenticationServiceField.RUN_AS_USER_HEADER,
220-
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
221-
)
217+
.addHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, SecuritySettingsSource.TEST_USER_NAME)
222218
);
223219
final Response authenticateResponse = getRestClient().performRequest(authenticateRequest);
224220
final XContentTestUtils.JsonMapView authenticateJsonView = XContentTestUtils.createJsonMapView(
225221
authenticateResponse.getEntity().getContent()
226222
);
227-
assertThat(authenticateJsonView.get("username"), equalTo(runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER));
223+
assertThat(authenticateJsonView.get("username"), equalTo(RUN_AS_USER));
228224
assertThat(authenticateJsonView.get("authentication_type"), equalTo("token"));
229-
230-
final Request getUserRequest = new Request("GET", "/_security/user");
231-
getUserRequest.setOptions(
232-
getUserRequest.getOptions()
233-
.toBuilder()
234-
.addHeader("Authorization", "Bearer " + tokenMapView.get("access_token"))
235-
.addHeader(
236-
AuthenticationServiceField.RUN_AS_USER_HEADER,
237-
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
238-
)
239-
);
240-
if (runAsTestUser) {
241-
assertThat(getRestClient().performRequest(getUserRequest).getStatusLine().getStatusCode(), equalTo(200));
242-
} else {
243-
final ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(getUserRequest));
244-
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403));
245-
}
246225
}
247226

248227
private static Request requestForUserRunAsUser(String user) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ protected void doExecute(Task task, CreateTokenRequest request, ActionListener<C
7474
Authentication authentication = securityContext.getAuthentication();
7575
if (authentication.isServiceAccount()) {
7676
// Service account itself cannot create OAuth2 tokens.
77-
// But it is possible to create an oauth2 token if the service account run-as a different user.
78-
// In this case, the token will be created for the run-as user (not the service account).
7977
listener.onFailure(new ElasticsearchException("OAuth2 token creation is not supported for service accounts"));
8078
return;
8179
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import java.util.Collections;
3333
import java.util.List;
34+
import java.util.Locale;
3435
import java.util.function.BiConsumer;
3536
import java.util.function.Consumer;
3637
import java.util.function.Function;
@@ -195,11 +196,8 @@ private BiConsumer<Authenticator, ActionListener<AuthenticationResult<Authentica
195196
};
196197
}
197198

198-
private void maybeLookupRunAsUser(
199-
Authenticator.Context context,
200-
Authentication authentication,
201-
ActionListener<Authentication> listener
202-
) {
199+
// Package private for test
200+
void maybeLookupRunAsUser(Authenticator.Context context, Authentication authentication, ActionListener<Authentication> listener) {
203201
if (false == runAsEnabled) {
204202
finishAuthentication(context, authentication, listener);
205203
return;
@@ -211,6 +209,19 @@ private void maybeLookupRunAsUser(
211209
return;
212210
}
213211

212+
// Run-as is supported for authentication with realm or api_key. Run-as for other authentication types is ignored.
213+
// Both realm user and api_key can create tokens. They can also run-as another user and create tokens.
214+
// In both cases, the created token will have a TOKEN authentication type and hence does not support run-as.
215+
if (Authentication.AuthenticationType.REALM != authentication.getAuthenticationType()
216+
&& Authentication.AuthenticationType.API_KEY != authentication.getAuthenticationType()) {
217+
logger.info(
218+
"ignore run-as header since it is currently not supported for authentication type [{}]",
219+
authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
220+
);
221+
finishAuthentication(context, authentication, listener);
222+
return;
223+
}
224+
214225
final User user = authentication.getUser();
215226
if (runAsUsername.isEmpty()) {
216227
logger.debug("user [{}] attempted to runAs with an empty username", user.principal());

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@
77

88
package org.elasticsearch.xpack.security.authc;
99

10+
import org.apache.logging.log4j.Level;
11+
import org.apache.logging.log4j.LogManager;
12+
import org.apache.logging.log4j.Logger;
1013
import org.elasticsearch.ElasticsearchSecurityException;
1114
import org.elasticsearch.action.ActionListener;
1215
import org.elasticsearch.action.support.PlainActionFuture;
16+
import org.elasticsearch.common.logging.Loggers;
1317
import org.elasticsearch.common.settings.SecureString;
1418
import org.elasticsearch.common.settings.Settings;
1519
import org.elasticsearch.common.util.concurrent.ThreadContext;
20+
import org.elasticsearch.core.Tuple;
1621
import org.elasticsearch.node.Node;
1722
import org.elasticsearch.test.ESTestCase;
23+
import org.elasticsearch.test.MockLogAppender;
1824
import org.elasticsearch.xpack.core.security.authc.Authentication;
1925
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
2026
import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
27+
import org.elasticsearch.xpack.core.security.authc.AuthenticationTests;
2128
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
2229
import org.elasticsearch.xpack.core.security.authc.Realm;
2330
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
@@ -31,10 +38,13 @@
3138

3239
import java.io.IOException;
3340
import java.util.List;
41+
import java.util.Locale;
3442

3543
import static org.hamcrest.Matchers.containsString;
44+
import static org.hamcrest.Matchers.equalTo;
3645
import static org.hamcrest.Matchers.hasItem;
3746
import static org.hamcrest.Matchers.is;
47+
import static org.hamcrest.Matchers.not;
3848
import static org.mockito.ArgumentMatchers.any;
3949
import static org.mockito.ArgumentMatchers.eq;
4050
import static org.mockito.Mockito.doAnswer;
@@ -285,6 +295,82 @@ public void testUnsuccessfulOAuth2TokenOrApiKeyWillNotFallToAnonymousOrReportMis
285295
);
286296
}
287297

298+
public void testMaybeLookupRunAsUser() {
299+
final Authentication authentication = randomFrom(
300+
AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomAlphaOfLength(20)),
301+
AuthenticationTests.randomRealmAuthentication()
302+
);
303+
final String runAsUsername = "your-run-as-username";
304+
threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, runAsUsername);
305+
assertThat(authentication.getUser().principal(), not(equalTo(runAsUsername)));
306+
307+
final AuthenticationService.AuditableRequest auditableRequest = mock(AuthenticationService.AuditableRequest.class);
308+
final Authenticator.Context context = new Authenticator.Context(threadContext, auditableRequest, null, true, realms);
309+
310+
doAnswer(invocation -> {
311+
@SuppressWarnings("unchecked")
312+
final ActionListener<Tuple<User, Realm>> listener = (ActionListener<Tuple<User, Realm>>) invocation.getArguments()[2];
313+
listener.onResponse(null);
314+
return null;
315+
}).when(realmsAuthenticator).lookupRunAsUser(any(), any(), any());
316+
final PlainActionFuture<Authentication> future = new PlainActionFuture<>();
317+
authenticatorChain.maybeLookupRunAsUser(context, authentication, future);
318+
future.actionGet();
319+
verify(realmsAuthenticator).lookupRunAsUser(eq(context), eq(authentication), any());
320+
}
321+
322+
public void testRunAsIsIgnoredForUnsupportedAuthenticationTypes() throws IllegalAccessException {
323+
final Authentication authentication = randomFrom(
324+
AuthenticationTests.toToken(
325+
AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomAlphaOfLength(20))
326+
),
327+
AuthenticationTests.toToken(AuthenticationTests.randomRealmAuthentication()),
328+
AuthenticationTests.randomServiceAccountAuthentication(),
329+
AuthenticationTests.randomAnonymousAuthentication(),
330+
AuthenticationTests.randomInternalAuthentication()
331+
);
332+
threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, "you-shall-not-pass");
333+
assertThat(
334+
authentication.getUser().principal(),
335+
not(equalTo(threadContext.getHeader(AuthenticationServiceField.RUN_AS_USER_HEADER)))
336+
);
337+
338+
final AuthenticationService.AuditableRequest auditableRequest = mock(AuthenticationService.AuditableRequest.class);
339+
final Authenticator.Context context = new Authenticator.Context(threadContext, auditableRequest, null, true, realms);
340+
341+
doAnswer(invocation -> {
342+
fail("should not reach here");
343+
return null;
344+
}).when(realmsAuthenticator).lookupRunAsUser(any(), any(), any());
345+
346+
final Logger logger = LogManager.getLogger(AuthenticatorChain.class);
347+
Loggers.setLevel(logger, Level.INFO);
348+
final MockLogAppender appender = new MockLogAppender();
349+
Loggers.addAppender(logger, appender);
350+
appender.start();
351+
352+
try {
353+
appender.addExpectation(
354+
new MockLogAppender.SeenEventExpectation(
355+
"run-as",
356+
AuthenticatorChain.class.getName(),
357+
Level.INFO,
358+
"ignore run-as header since it is currently not supported for authentication type ["
359+
+ authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
360+
+ "]"
361+
)
362+
);
363+
final PlainActionFuture<Authentication> future = new PlainActionFuture<>();
364+
authenticatorChain.maybeLookupRunAsUser(context, authentication, future);
365+
assertThat(future.actionGet(), equalTo(authentication));
366+
appender.assertAllExpectationsMatched();
367+
} finally {
368+
appender.stop();
369+
Loggers.setLevel(logger, Level.INFO);
370+
Loggers.removeAppender(logger, appender);
371+
}
372+
}
373+
288374
private Authenticator.Context createAuthenticatorContext() {
289375
return createAuthenticatorContext(mock(AuthenticationService.AuditableRequest.class));
290376
}

0 commit comments

Comments
 (0)