Skip to content

Commit 32c8559

Browse files
committed
Restrict run-as to realm and api_key authentication types
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: elastic#79809
1 parent 52ac5d1 commit 32c8559

File tree

6 files changed

+106
-63
lines changed

6 files changed

+106
-63
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ public Authentication runAs(User runAs, @Nullable RealmRef lookupRealmRef) {
178178
Objects.requireNonNull(runAs);
179179
assert false == runAs.isRunAs();
180180
assert false == getUser().isRunAs();
181+
assert AuthenticationType.REALM == getAuthenticationType() || AuthenticationType.API_KEY == getAuthenticationType();
181182
return new Authentication(
182183
new User(runAs, getUser()),
183184
getAuthenticatedBy(),

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

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -207,39 +207,27 @@ public void testRunAsAuthenticationWithDomain() {
207207
RealmRef lookupRealmRef = randomRealmRef(true);
208208
// realm/token run-as
209209
Authentication test = Authentication.newRealmAuthentication(randomUser(), authnRealmRef);
210-
if (randomBoolean()) {
211-
test = test.token();
212-
}
213210
test = test.runAs(randomUser(), lookupRealmRef);
214211
if (randomBoolean()) {
215212
test = test.token();
216213
}
217214
assertThat(test.isAssignedToDomain(), is(true));
218215
assertThat(test.getDomain(), is(lookupRealmRef.getDomain()));
219216
test = Authentication.newRealmAuthentication(randomUser(), randomRealmRef(false));
220-
if (randomBoolean()) {
221-
test = test.token();
222-
}
223217
test = test.runAs(randomUser(), lookupRealmRef);
224218
if (randomBoolean()) {
225219
test = test.token();
226220
}
227221
assertThat(test.isAssignedToDomain(), is(true));
228222
assertThat(test.getDomain(), is(lookupRealmRef.getDomain()));
229223
test = Authentication.newRealmAuthentication(randomUser(), authnRealmRef);
230-
if (randomBoolean()) {
231-
test = test.token();
232-
}
233224
test = test.runAs(randomUser(), randomRealmRef(false));
234225
if (randomBoolean()) {
235226
test = test.token();
236227
}
237228
assertThat(test.isAssignedToDomain(), is(false));
238229
assertThat(test.getDomain(), nullValue());
239230
test = Authentication.newRealmAuthentication(randomUser(), randomRealmRef(false));
240-
if (randomBoolean()) {
241-
test = test.token();
242-
}
243231
test = test.runAs(randomUser(), lookupRealmRef);
244232
if (randomBoolean()) {
245233
test = test.token();
@@ -248,9 +236,6 @@ public void testRunAsAuthenticationWithDomain() {
248236
assertThat(test.getDomain(), is(lookupRealmRef.getDomain()));
249237
// api key run-as
250238
test = randomApiKeyAuthentication(randomUser(), randomAlphaOfLengthBetween(10, 20), Version.CURRENT);
251-
if (randomBoolean()) {
252-
test = test.token();
253-
}
254239
assertThat(test.isAssignedToDomain(), is(false));
255240
assertThat(test.getDomain(), nullValue());
256241
if (randomBoolean()) {
@@ -268,25 +253,10 @@ public void testRunAsAuthenticationWithDomain() {
268253
assertThat(test.isAssignedToDomain(), is(false));
269254
assertThat(test.getDomain(), nullValue());
270255
}
271-
// service account run-as
256+
// service account cannot run-as
272257
test = randomServiceAccountAuthentication();
273258
assertThat(test.isAssignedToDomain(), is(false));
274259
assertThat(test.getDomain(), nullValue());
275-
if (randomBoolean()) {
276-
test = test.runAs(randomUser(), lookupRealmRef);
277-
if (randomBoolean()) {
278-
test = test.token();
279-
}
280-
assertThat(test.isAssignedToDomain(), is(true));
281-
assertThat(test.getDomain(), is(lookupRealmRef.getDomain()));
282-
} else {
283-
test = test.runAs(randomUser(), randomRealmRef(false));
284-
if (randomBoolean()) {
285-
test = test.token();
286-
}
287-
assertThat(test.isAssignedToDomain(), is(false));
288-
assertThat(test.getDomain(), nullValue());
289-
}
290260
}
291261

292262
public void testDomainSerialize() throws Exception {

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
@@ -29,6 +29,7 @@
2929
import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService;
3030

3131
import java.util.List;
32+
import java.util.Locale;
3233
import java.util.Map;
3334
import java.util.function.BiConsumer;
3435
import java.util.function.Consumer;
@@ -189,11 +190,8 @@ private BiConsumer<Authenticator, ActionListener<AuthenticationResult<Authentica
189190
};
190191
}
191192

192-
private void maybeLookupRunAsUser(
193-
Authenticator.Context context,
194-
Authentication authentication,
195-
ActionListener<Authentication> listener
196-
) {
193+
// Package private for test
194+
void maybeLookupRunAsUser(Authenticator.Context context, Authentication authentication, ActionListener<Authentication> listener) {
197195
if (false == runAsEnabled) {
198196
finishAuthentication(context, authentication, listener);
199197
return;
@@ -205,6 +203,19 @@ private void maybeLookupRunAsUser(
205203
return;
206204
}
207205

206+
// Run-as is supported for authentication with realm or api_key. Run-as for other authentication types is ignored.
207+
// Both realm user and api_key can create tokens. They can also run-as another user and create tokens.
208+
// In both cases, the created token will have a TOKEN authentication type and hence does not support run-as.
209+
if (Authentication.AuthenticationType.REALM != authentication.getAuthenticationType()
210+
&& Authentication.AuthenticationType.API_KEY != authentication.getAuthenticationType()) {
211+
logger.info(
212+
"ignore run-as header since it is not supported for authentication type [{}]",
213+
authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
214+
);
215+
finishAuthentication(context, authentication, listener);
216+
return;
217+
}
218+
208219
// Now we have a valid runAsUsername
209220
realmsAuthenticator.lookupRunAsUser(context, authentication, ActionListener.wrap(tuple -> {
210221
final Authentication finalAuth;

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

Lines changed: 84 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,80 @@ 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(false)
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.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomAlphaOfLength(20)).token(),
325+
AuthenticationTests.randomRealmAuthentication(false).token(),
326+
AuthenticationTests.randomServiceAccountAuthentication(),
327+
AuthenticationTests.randomAnonymousAuthentication(),
328+
AuthenticationTests.randomInternalAuthentication()
329+
);
330+
threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, "you-shall-not-pass");
331+
assertThat(
332+
authentication.getUser().principal(),
333+
not(equalTo(threadContext.getHeader(AuthenticationServiceField.RUN_AS_USER_HEADER)))
334+
);
335+
336+
final AuthenticationService.AuditableRequest auditableRequest = mock(AuthenticationService.AuditableRequest.class);
337+
final Authenticator.Context context = new Authenticator.Context(threadContext, auditableRequest, null, true, realms);
338+
339+
doAnswer(invocation -> {
340+
fail("should not reach here");
341+
return null;
342+
}).when(realmsAuthenticator).lookupRunAsUser(any(), any(), any());
343+
344+
final Logger logger = LogManager.getLogger(AuthenticatorChain.class);
345+
Loggers.setLevel(logger, Level.INFO);
346+
final MockLogAppender appender = new MockLogAppender();
347+
Loggers.addAppender(logger, appender);
348+
appender.start();
349+
350+
try {
351+
appender.addExpectation(
352+
new MockLogAppender.SeenEventExpectation(
353+
"run-as",
354+
AuthenticatorChain.class.getName(),
355+
Level.INFO,
356+
"ignore run-as header since it is not supported for authentication type ["
357+
+ authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
358+
+ "]"
359+
)
360+
);
361+
final PlainActionFuture<Authentication> future = new PlainActionFuture<>();
362+
authenticatorChain.maybeLookupRunAsUser(context, authentication, future);
363+
assertThat(future.actionGet(), equalTo(authentication));
364+
appender.assertAllExpectationsMatched();
365+
} finally {
366+
appender.stop();
367+
Loggers.setLevel(logger, Level.INFO);
368+
Loggers.removeAppender(logger, appender);
369+
}
370+
}
371+
288372
private Authenticator.Context createAuthenticatorContext() {
289373
return createAuthenticatorContext(mock(AuthenticationService.AuditableRequest.class));
290374
}

0 commit comments

Comments
 (0)