Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions x-pack/plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ tasks.named("yamlRestTestV7CompatTransform").configure{ task ->
task.skipTest("indices.freeze/10_basic/Basic", "#70192 -- the freeze index API is removed from 8.0")
task.skipTest("indices.freeze/10_basic/Test index options", "#70192 -- the freeze index API is removed from 8.0")
task.skipTest("ml/categorization_agg/Test categorization aggregation with poor settings", "https://github.com/elastic/elasticsearch/pull/79586")
task.skipTest("service_accounts/10_basic/Test service account tokens", "mute till we decide whether to make the changes in 7.16")

task.replaceValueInMatch("_type", "_doc")
task.addAllowedWarningRegex("\\[types removal\\].*")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ public Map<String, Object> getMetadata() {
return metadata;
}

public boolean isServiceAccount() {
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType()) && null == getLookedUpBy();
public boolean isAuthenticatedWithServiceAccount() {
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect we're going to end up with a bug at some point because this method is a now little ambiguous about what it does.
isServiceAccount is true, if the user authenticated using a service account token (or, in the future another credential), but the effective (run-as) user might not be a service account.

Anything that relies on isServiceAccount to make decisions has the potential to do the wrong thing if they don't take that into account.

I wonder if we can rename the method to make that more clear somehow.

  • isServiceAccountAuthentication() ?
  • isAuthenticatedWithServiceAccountCredential() ?
  • authenticatedAsServiceAccount()

I don't love any of those, but I think we need to do something to mitigate the risk.

}

public boolean isApiKey() {
public boolean isAuthenticatedWithApiKey() {
return AuthenticationType.API_KEY.equals(getAuthenticationType());
}

Expand Down Expand Up @@ -235,7 +235,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
builder.array(User.Fields.ROLES.getPreferredName(), user.roles());
builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName());
builder.field(User.Fields.EMAIL.getPreferredName(), user.email());
if (isServiceAccount()) {
if (isAuthenticatedWithServiceAccount()) {
final String tokenName = (String) getMetadata().get(ServiceAccountSettings.TOKEN_NAME_FIELD);
assert tokenName != null : "token name cannot be null";
final String tokenSource = (String) getMetadata().get(ServiceAccountSettings.TOKEN_SOURCE_FIELD);
Expand All @@ -261,7 +261,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
}
builder.endObject();
builder.field(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), getAuthenticationType().name().toLowerCase(Locale.ROOT));
if (isApiKey()) {
if (isAuthenticatedWithApiKey()) {
this.assertApiKeyMetadata();
final String apiKeyId = (String) this.metadata.get(AuthenticationField.API_KEY_ID_KEY);
final String apiKeyName = (String) this.metadata.get(AuthenticationField.API_KEY_NAME_KEY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ public void testIsServiceAccount() {
);
final Authentication authentication = new Authentication(user, authRealm, lookupRealm);

if (authRealmIsForServiceAccount && lookupRealm == null) {
assertThat(authentication.isServiceAccount(), is(true));
if (authRealmIsForServiceAccount) {
assertThat(authentication.isAuthenticatedWithServiceAccount(), is(true));
} else {
assertThat(authentication.isServiceAccount(), is(false));
assertThat(authentication.isAuthenticatedWithServiceAccount(), is(false));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,35 @@

import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.SecurityIntegTestCase;
import org.elasticsearch.test.SecuritySettingsSource;
import org.elasticsearch.test.XContentTestUtils;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.junit.BeforeClass;

import java.io.IOException;

import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;

public class RunAsIntegTests extends SecurityIntegTestCase {

private static final String RUN_AS_USER = "run_as_user";
private static final String CLIENT_USER = "transport_user";
private static final String ROLES = "run_as_role:\n" + " run_as: [ '" + SecuritySettingsSource.TEST_USER_NAME + "', 'idontexist' ]\n";
private static final String NO_ROLE_USER = "no_role_user";
private static final String ROLES = "run_as_role:\n"
+ " cluster: ['manage_own_api_key', 'manage_token']\n"
+ " run_as: [ '"
+ SecuritySettingsSource.TEST_USER_NAME
+ "', '"
+ NO_ROLE_USER
+ "', 'idontexist' ]\n";

// indicates whether the RUN_AS_USER that is being authenticated is also a superuser
private static boolean runAsHasSuperUserRole;
Expand Down Expand Up @@ -52,6 +66,10 @@ public String configUsers() {
+ CLIENT_USER
+ ":"
+ SecuritySettingsSource.TEST_PASSWORD_HASHED
+ "\n"
+ NO_ROLE_USER
+ ":"
+ SecuritySettingsSource.TEST_PASSWORD_HASHED
+ "\n";
}

Expand All @@ -64,6 +82,13 @@ public String configUsersRoles() {
return roles;
}

@Override
protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
final Settings.Builder builder = Settings.builder().put(super.nodeSettings(nodeOrdinal, otherSettings));
builder.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), "true");
return builder.build();
}

@Override
protected boolean transportSSLEnabled() {
return false;
Expand Down Expand Up @@ -119,6 +144,107 @@ public void testNonExistentRunAsUserUsingHttp() throws Exception {
}
}

public void testRunAsUsingApiKey() throws IOException {
final Request createApiKeyRequest = new Request("PUT", "/_security/api_key");
createApiKeyRequest.setJsonEntity("{\"name\":\"k1\"}\n");
createApiKeyRequest.setOptions(
createApiKeyRequest.getOptions()
.toBuilder()
.addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(RUN_AS_USER, TEST_PASSWORD_SECURE_STRING))
);
final Response createApiKeyResponse = getRestClient().performRequest(createApiKeyRequest);
final XContentTestUtils.JsonMapView apiKeyMapView = XContentTestUtils.createJsonMapView(
createApiKeyResponse.getEntity().getContent()
);

final boolean runAsTestUser = false;

final Request authenticateRequest = new Request("GET", "/_security/_authenticate");
authenticateRequest.setOptions(
authenticateRequest.getOptions()
.toBuilder()
.addHeader("Authorization", "ApiKey " + apiKeyMapView.get("encoded"))
.addHeader(
AuthenticationServiceField.RUN_AS_USER_HEADER,
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
)
);
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("authentication_realm.type"), equalTo("_es_api_key"));
assertThat(authenticateJsonView.get("authentication_type"), equalTo("api_key"));

final Request getUserRequest = new Request("GET", "/_security/user");
getUserRequest.setOptions(
getUserRequest.getOptions()
.toBuilder()
.addHeader("Authorization", "ApiKey " + apiKeyMapView.get("encoded"))
.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));
}
}

public void testRunAsUsingOAuthToken() throws IOException {
final Request createTokenRequest = new Request("POST", "/_security/oauth2/token");
createTokenRequest.setJsonEntity("{\"grant_type\":\"client_credentials\"}");
createTokenRequest.setOptions(
createTokenRequest.getOptions()
.toBuilder()
.addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(RUN_AS_USER, TEST_PASSWORD_SECURE_STRING))
);
final Response createTokenResponse = getRestClient().performRequest(createTokenRequest);
final XContentTestUtils.JsonMapView tokenMapView = XContentTestUtils.createJsonMapView(
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
)
);
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("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) {
Request request = new Request("GET", "/_nodes");
RequestOptions.Builder options = request.getOptions().toBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ protected void doExecute(Task task, CreateTokenRequest request, ActionListener<C
break;
case CLIENT_CREDENTIALS:
Authentication authentication = securityContext.getAuthentication();
if (authentication.isServiceAccount()) {
if (authentication.isAuthenticatedWithServiceAccount() && false == authentication.getUser().isRunAs()) {
// 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 @@ -1492,7 +1492,7 @@ LogEntryBuilder withAuthentication(Authentication authentication) {
logEntry.with(PRINCIPAL_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName());
}
}
if (authentication.isServiceAccount()) {
if (authentication.isAuthenticatedWithServiceAccount()) {
logEntry.with(SERVICE_TOKEN_NAME_FIELD_NAME, (String) authentication.getMetadata().get(TOKEN_NAME_FIELD))
.with(
SERVICE_TOKEN_TYPE_FIELD_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,7 @@ private void maybeLookupRunAsUser(
Authentication authentication,
ActionListener<Authentication> listener
) {
// TODO: only allow run as for realm authentication to maintain the existing behaviour
if (false == runAsEnabled || authentication.getAuthenticationType() != Authentication.AuthenticationType.REALM) {
if (false == runAsEnabled) {
finishAuthentication(context, authentication, listener);
return;
}
Expand Down Expand Up @@ -227,9 +226,23 @@ private void maybeLookupRunAsUser(
if (tuple == null) {
logger.debug("Cannot find run-as user [{}] for authenticated user [{}]", runAsUsername, user.principal());
// the user does not exist, but we still create a User object, which will later be rejected by authz
finalAuth = new Authentication(new User(runAsUsername, null, user), authentication.getAuthenticatedBy(), null);
finalAuth = new Authentication(
new User(runAsUsername, null, user),
authentication.getAuthenticatedBy(),
null,
authentication.getVersion(),
authentication.getAuthenticationType(),
authentication.getMetadata()
);
} else {
finalAuth = new Authentication(new User(tuple.v1(), user), authentication.getAuthenticatedBy(), tuple.v2());
finalAuth = new Authentication(
new User(tuple.v1(), user),
authentication.getAuthenticatedBy(),
tuple.v2(),
authentication.getVersion(),
authentication.getAuthenticationType(),
authentication.getMetadata()
);
}
finishAuthentication(context, finalAuth, listener);
}, listener::onFailure));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void findTokensFor(GetServiceAccountCredentialsRequest request, ActionLis
}

public void getRoleDescriptor(Authentication authentication, ActionListener<RoleDescriptor> listener) {
assert authentication.isServiceAccount() : "authentication is not for service account: " + authentication;
assert authentication.isAuthenticatedWithServiceAccount() : "authentication is not for service account: " + authentication;
final String principal = authentication.getUser().principal();
final ServiceAccount account = ACCOUNTS.get(principal);
if (account == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ private ElasticsearchSecurityException denialException(
}
}

String userText = "user [" + authUser.principal() + "]";
String userText = (authentication.isAuthenticatedWithServiceAccount() ? "service account" : "user") + " [" + authUser.principal() + "]";
// check for run as
if (authentication.getUser().isRunAs()) {
userText = userText + " run as [" + authentication.getUser().principal() + "]";
Expand All @@ -892,9 +892,14 @@ private ElasticsearchSecurityException denialException(
final String apiKeyId = (String) authentication.getMetadata().get(AuthenticationField.API_KEY_ID_KEY);
assert apiKeyId != null : "api key id must be present in the metadata";
userText = "API key id [" + apiKeyId + "] of " + userText;
} else if (false == authentication.isServiceAccount()) {
// Don't print roles for API keys because they're not meaningful
// Also not printing roles for service accounts since they have no roles
}

// The run-as user is always from a realm. So it must have roles that can be printed.
// If the user is not run-as, we cannot print the roles if it's an API key or a service account (both do not have
// roles, but privileges)
if (authentication.getUser().isRunAs()
|| (false == authentication.isAuthenticatedWithServiceAccount()
&& AuthenticationType.API_KEY != authentication.getAuthenticationType())) {
userText = userText + " with roles [" + Strings.arrayToCommaDelimitedString(authentication.getUser().roles()) + "]";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,26 +281,37 @@ public void getRoles(User user, Authentication authentication, ActionListener<Ro
return;
}

if (authentication.isServiceAccount()) {
getRolesForServiceAccount(authentication, roleActionListener);
} else if (ApiKeyService.isApiKeyAuthentication(authentication)) {
getRolesForApiKey(authentication, roleActionListener);
if (user.isRunAs()) {
// The runas user currently must be from a realm and have regular roles
getRolesForUser(user, roleActionListener);
} else {
Set<String> roleNames = new HashSet<>(Arrays.asList(user.roles()));
if (isAnonymousEnabled && anonymousUser.equals(user) == false) {
if (anonymousUser.roles().length == 0) {
throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles");
}
Collections.addAll(roleNames, anonymousUser.roles());
// The authenticated user may not come from a realm and they need to be handled specially
if (authentication.isAuthenticatedWithServiceAccount()) {
getRolesForServiceAccount(authentication, roleActionListener);
} else if (ApiKeyService.isApiKeyAuthentication(authentication)) {
// API key role descriptors are stored in the authentication metadata
getRolesForApiKey(authentication, roleActionListener);
} else {
getRolesForUser(user, roleActionListener);
}
}
}

if (roleNames.isEmpty()) {
roleActionListener.onResponse(Role.EMPTY);
} else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) {
roleActionListener.onResponse(superuserRole);
} else {
roles(roleNames, roleActionListener);
private void getRolesForUser(User user, ActionListener<Role> roleActionListener) {
Set<String> roleNames = new HashSet<>(Arrays.asList(user.roles()));
if (isAnonymousEnabled && anonymousUser.equals(user) == false) {
if (anonymousUser.roles().length == 0) {
throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles");
}
Collections.addAll(roleNames, anonymousUser.roles());
}

if (roleNames.isEmpty()) {
roleActionListener.onResponse(Role.EMPTY);
} else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) {
roleActionListener.onResponse(superuserRole);
} else {
roles(roleNames, roleActionListener);
}
}

Expand Down
Loading