Skip to content

Commit

Permalink
Restrict run-as to realm and api_key authentication types (elastic#84336
Browse files Browse the repository at this point in the history
) (elastic#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: elastic#79809
  • Loading branch information
ywangd authored Feb 28, 2022
1 parent 5a94989 commit 615c3cd
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.elasticsearch.xpack.core.security.authc;

import org.elasticsearch.Version;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.VersionUtils;
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
Expand All @@ -16,7 +17,12 @@
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
import org.elasticsearch.xpack.core.security.user.SystemUser;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.core.security.user.XPackSecurityUser;
import org.elasticsearch.xpack.core.security.user.XPackUser;

import java.util.Arrays;
import java.util.EnumSet;
Expand All @@ -26,6 +32,12 @@
import java.util.Set;
import java.util.stream.Collectors;

import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_NAME;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_TYPE;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE;
import static org.hamcrest.Matchers.is;

public class AuthenticationTests extends ESTestCase {
Expand Down Expand Up @@ -258,6 +270,40 @@ public static Authentication randomServiceAccountAuthentication() {
);
}

public static Authentication randomRealmAuthentication() {
return new Authentication(randomUser(), randomRealm(), null);
}

public static Authentication randomInternalAuthentication() {
String nodeName = randomAlphaOfLengthBetween(3, 8);
return randomFrom(
new Authentication(
randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, AsyncSearchUser.INSTANCE),
new RealmRef(ATTACH_REALM_NAME, ATTACH_REALM_TYPE, nodeName),
null
),
new Authentication(SystemUser.INSTANCE, new RealmRef(FALLBACK_REALM_NAME, FALLBACK_REALM_TYPE, nodeName), null)
);
}

public static Authentication randomAnonymousAuthentication() {
Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anon_role").build();
String nodeName = randomAlphaOfLengthBetween(3, 8);
return new Authentication(new AnonymousUser(settings), new RealmRef(ANONYMOUS_REALM_NAME, ANONYMOUS_REALM_TYPE, nodeName), null);
}

public static Authentication toToken(Authentication authentication) {
final Authentication newTokenAuthentication = new Authentication(
authentication.getUser(),
authentication.getAuthenticatedBy(),
authentication.getLookedUpBy(),
Version.CURRENT,
AuthenticationType.TOKEN,
authentication.getMetadata()
);
return newTokenAuthentication;
}

private boolean realmIsSingleton(RealmRef realmRef) {
return Set.of(FileRealmSettings.TYPE, NativeRealmSettings.TYPE).contains(realmRef.getType());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public void testRunAsUsingApiKey() throws IOException {
);
assertThat(authenticateJsonView.get("username"), equalTo(runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER));
assertThat(authenticateJsonView.get("authentication_realm.type"), equalTo("_es_api_key"));
assertThat(authenticateJsonView.get("lookup_realm.type"), equalTo("file"));
assertThat(authenticateJsonView.get("authentication_type"), equalTo("api_key"));

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

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

final boolean runAsTestUser = randomBoolean();

final Request authenticateRequest = new Request("GET", "/_security/_authenticate");
authenticateRequest.setOptions(
authenticateRequest.getOptions()
.toBuilder()
.addHeader("Authorization", "Bearer " + tokenMapView.get("access_token"))
.addHeader(
AuthenticationServiceField.RUN_AS_USER_HEADER,
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
)
.addHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, SecuritySettingsSource.TEST_USER_NAME)
);
final Response authenticateResponse = getRestClient().performRequest(authenticateRequest);
final XContentTestUtils.JsonMapView authenticateJsonView = XContentTestUtils.createJsonMapView(
authenticateResponse.getEntity().getContent()
);
assertThat(authenticateJsonView.get("username"), equalTo(runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER));
assertThat(authenticateJsonView.get("username"), equalTo(RUN_AS_USER));
assertThat(authenticateJsonView.get("authentication_type"), equalTo("token"));

final Request getUserRequest = new Request("GET", "/_security/user");
getUserRequest.setOptions(
getUserRequest.getOptions()
.toBuilder()
.addHeader("Authorization", "Bearer " + tokenMapView.get("access_token"))
.addHeader(
AuthenticationServiceField.RUN_AS_USER_HEADER,
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
)
);
if (runAsTestUser) {
assertThat(getRestClient().performRequest(getUserRequest).getStatusLine().getStatusCode(), equalTo(200));
} else {
final ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(getUserRequest));
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403));
}
}

private static Request requestForUserRunAsUser(String user) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ protected void doExecute(Task task, CreateTokenRequest request, ActionListener<C
Authentication authentication = securityContext.getAuthentication();
if (authentication.isServiceAccount()) {
// Service account itself cannot create OAuth2 tokens.
// But it is possible to create an oauth2 token if the service account run-as a different user.
// In this case, the token will be created for the run-as user (not the service account).
listener.onFailure(new ElasticsearchException("OAuth2 token creation is not supported for service accounts"));
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -195,11 +196,8 @@ private BiConsumer<Authenticator, ActionListener<AuthenticationResult<Authentica
};
}

private void maybeLookupRunAsUser(
Authenticator.Context context,
Authentication authentication,
ActionListener<Authentication> listener
) {
// Package private for test
void maybeLookupRunAsUser(Authenticator.Context context, Authentication authentication, ActionListener<Authentication> listener) {
if (false == runAsEnabled) {
finishAuthentication(context, authentication, listener);
return;
Expand All @@ -211,6 +209,19 @@ private void maybeLookupRunAsUser(
return;
}

// Run-as is supported for authentication with realm or api_key. Run-as for other authentication types is ignored.
// Both realm user and api_key can create tokens. They can also run-as another user and create tokens.
// In both cases, the created token will have a TOKEN authentication type and hence does not support run-as.
if (Authentication.AuthenticationType.REALM != authentication.getAuthenticationType()
&& Authentication.AuthenticationType.API_KEY != authentication.getAuthenticationType()) {
logger.info(
"ignore run-as header since it is currently not supported for authentication type [{}]",
authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
);
finishAuthentication(context, authentication, listener);
return;
}

final User user = authentication.getUser();
if (runAsUsername.isEmpty()) {
logger.debug("user [{}] attempted to runAs with an empty username", user.principal());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@

package org.elasticsearch.xpack.security.authc;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.node.Node;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.MockLogAppender;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
import org.elasticsearch.xpack.core.security.authc.AuthenticationTests;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
Expand All @@ -31,10 +38,13 @@

import java.io.IOException;
import java.util.List;
import java.util.Locale;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
Expand Down Expand Up @@ -285,6 +295,82 @@ public void testUnsuccessfulOAuth2TokenOrApiKeyWillNotFallToAnonymousOrReportMis
);
}

public void testMaybeLookupRunAsUser() {
final Authentication authentication = randomFrom(
AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomAlphaOfLength(20)),
AuthenticationTests.randomRealmAuthentication()
);
final String runAsUsername = "your-run-as-username";
threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, runAsUsername);
assertThat(authentication.getUser().principal(), not(equalTo(runAsUsername)));

final AuthenticationService.AuditableRequest auditableRequest = mock(AuthenticationService.AuditableRequest.class);
final Authenticator.Context context = new Authenticator.Context(threadContext, auditableRequest, null, true, realms);

doAnswer(invocation -> {
@SuppressWarnings("unchecked")
final ActionListener<Tuple<User, Realm>> listener = (ActionListener<Tuple<User, Realm>>) invocation.getArguments()[2];
listener.onResponse(null);
return null;
}).when(realmsAuthenticator).lookupRunAsUser(any(), any(), any());
final PlainActionFuture<Authentication> future = new PlainActionFuture<>();
authenticatorChain.maybeLookupRunAsUser(context, authentication, future);
future.actionGet();
verify(realmsAuthenticator).lookupRunAsUser(eq(context), eq(authentication), any());
}

public void testRunAsIsIgnoredForUnsupportedAuthenticationTypes() throws IllegalAccessException {
final Authentication authentication = randomFrom(
AuthenticationTests.toToken(
AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomAlphaOfLength(20))
),
AuthenticationTests.toToken(AuthenticationTests.randomRealmAuthentication()),
AuthenticationTests.randomServiceAccountAuthentication(),
AuthenticationTests.randomAnonymousAuthentication(),
AuthenticationTests.randomInternalAuthentication()
);
threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, "you-shall-not-pass");
assertThat(
authentication.getUser().principal(),
not(equalTo(threadContext.getHeader(AuthenticationServiceField.RUN_AS_USER_HEADER)))
);

final AuthenticationService.AuditableRequest auditableRequest = mock(AuthenticationService.AuditableRequest.class);
final Authenticator.Context context = new Authenticator.Context(threadContext, auditableRequest, null, true, realms);

doAnswer(invocation -> {
fail("should not reach here");
return null;
}).when(realmsAuthenticator).lookupRunAsUser(any(), any(), any());

final Logger logger = LogManager.getLogger(AuthenticatorChain.class);
Loggers.setLevel(logger, Level.INFO);
final MockLogAppender appender = new MockLogAppender();
Loggers.addAppender(logger, appender);
appender.start();

try {
appender.addExpectation(
new MockLogAppender.SeenEventExpectation(
"run-as",
AuthenticatorChain.class.getName(),
Level.INFO,
"ignore run-as header since it is currently not supported for authentication type ["
+ authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
+ "]"
)
);
final PlainActionFuture<Authentication> future = new PlainActionFuture<>();
authenticatorChain.maybeLookupRunAsUser(context, authentication, future);
assertThat(future.actionGet(), equalTo(authentication));
appender.assertAllExpectationsMatched();
} finally {
appender.stop();
Loggers.setLevel(logger, Level.INFO);
Loggers.removeAppender(logger, appender);
}
}

private Authenticator.Context createAuthenticatorContext() {
return createAuthenticatorContext(mock(AuthenticationService.AuditableRequest.class));
}
Expand Down

0 comments on commit 615c3cd

Please sign in to comment.