Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service Accounts - token name in response to Authenticate API #71382

Merged
merged 11 commits into from
Apr 19, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.elasticsearch.common.xcontent.XContentBuilder;
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.authc.support.AuthenticationContextSerializer;
import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper;
import org.elasticsearch.xpack.core.security.user.User;
Expand All @@ -28,6 +29,7 @@
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings.TOKEN_NAME_FIELD;
import static org.elasticsearch.xpack.core.security.authz.privilege.ManageOwnApiKeyClusterPrivilege.API_KEY_ID_KEY;

// TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField.
Expand Down Expand Up @@ -106,6 +108,10 @@ public Map<String, Object> getMetadata() {
return metadata;
}

public boolean isServiceAccount() {
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType()) && null == getLookedUpBy();
}

/**
* Writes the authentication to the context. There must not be an existing authentication in the context and if there is an
* {@link IllegalStateException} will be thrown
Expand Down Expand Up @@ -219,6 +225,11 @@ 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()) {
final String tokenName = (String) getMetadata().get(TOKEN_NAME_FIELD);
assert tokenName != null : "token name cannot be null";
builder.field(User.Fields.TOKEN.getPreferredName(), Map.of("name", tokenName));
}
builder.field(User.Fields.METADATA.getPreferredName(), user.metadata());
builder.field(User.Fields.ENABLED.getPreferredName(), user.enabled());
builder.startObject(User.Fields.AUTHENTICATION_REALM.getPreferredName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.core.security.authc.service;

public final class ServiceAccountSettings {

public static final String REALM_TYPE = "service_account";
public static final String REALM_NAME = "service_account";
public static final String TOKEN_NAME_FIELD = "_token_name";

private ServiceAccountSettings() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ public interface Fields {
ParseField REALM_TYPE = new ParseField("type");
ParseField REALM_NAME = new ParseField("name");
ParseField AUTHENTICATION_TYPE = new ParseField("authentication_type");
ParseField TOKEN = new ParseField("token");
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
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.User;

import java.util.Arrays;
Expand All @@ -23,6 +24,7 @@
import java.util.stream.Collectors;

import static org.elasticsearch.xpack.core.security.authz.privilege.ManageOwnApiKeyClusterPrivilege.API_KEY_ID_KEY;
import static org.hamcrest.Matchers.is;

public class AuthenticationTests extends ESTestCase {

Expand Down Expand Up @@ -92,6 +94,32 @@ public void testCanAccessResourcesOf() {
randomApiKeyAuthentication(randomFrom(user1, user2), apiKeyId2));
}

public void testIsServiceAccount() {
final User user =
new User(randomAlphaOfLengthBetween(3, 8), randomArray(0, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
final Authentication.RealmRef authRealm;
final boolean authRealmIsForServiceAccount = randomBoolean();
if (authRealmIsForServiceAccount) {
authRealm = new Authentication.RealmRef(
ServiceAccountSettings.REALM_NAME,
ServiceAccountSettings.REALM_TYPE,
randomAlphaOfLengthBetween(3, 8));
} else {
authRealm = new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8),
randomAlphaOfLengthBetween(3, 8));
}
final Authentication.RealmRef lookupRealm = randomFrom(
new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8),
randomAlphaOfLengthBetween(3, 8)), null);
final Authentication authentication = new Authentication(user, authRealm, lookupRealm);

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

private void checkCanAccessResources(Authentication authentication0, Authentication authentication1) {
if (authentication0.getAuthenticationType() == authentication1.getAuthenticationType()
|| EnumSet.of(AuthenticationType.REALM, AuthenticationType.TOKEN).equals(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.net.URL;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
Expand All @@ -48,9 +49,13 @@ public class ServiceAccountIT extends ESRestTestCase {
+ " \"roles\": [],\n"
+ " \"full_name\": \"Service account - elastic/fleet-server\",\n"
+ " \"email\": null,\n"
+ " \"token\": {\n"
+ " \"name\": \"%s\"\n"
+ " },\n"
+ " \"metadata\": {\n"
+ " \"_elastic_service_account\": true\n"
+ " },\n" + " \"enabled\": true,\n"
+ " },\n"
+ " \"enabled\": true,\n"
+ " \"authentication_realm\": {\n"
+ " \"name\": \"service_account\",\n"
+ " \"type\": \"service_account\"\n"
Expand Down Expand Up @@ -161,7 +166,9 @@ public void testAuthenticate() throws IOException {
final Response response = client().performRequest(request);
assertOK(response);
assertThat(responseAsMap(response),
equalTo(XContentHelper.convertToMap(new BytesArray(AUTHENTICATE_RESPONSE), false, XContentType.JSON).v2()));
equalTo(XContentHelper.convertToMap(
new BytesArray(String.format(Locale.ROOT, AUTHENTICATE_RESPONSE, "token1")),
false, XContentType.JSON).v2()));
}

public void testAuthenticateShouldNotFallThroughInCaseOfFailure() throws IOException {
Expand Down Expand Up @@ -237,7 +244,9 @@ public void testCreateApiServiceAccountTokenAndAuthenticateWithIt() throws IOExc
final Response response = client().performRequest(request);
assertOK(response);
assertThat(responseAsMap(response),
equalTo(XContentHelper.convertToMap(new BytesArray(AUTHENTICATE_RESPONSE), false, XContentType.JSON).v2()));
equalTo(XContentHelper.convertToMap(
new BytesArray(String.format(Locale.ROOT, AUTHENTICATE_RESPONSE, "api-token-1")),
false, XContentType.JSON).v2()));
}

public void testFileTokenAndApiTokenCanShareTheSameNameAndBothWorks() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
Expand All @@ -24,13 +25,11 @@
import java.util.Collection;
import java.util.Map;

import static org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings.TOKEN_NAME_FIELD;
import static org.elasticsearch.xpack.security.authc.service.ElasticServiceAccounts.ACCOUNTS;

public class ServiceAccountService {

public static final String REALM_TYPE = "service_account";
public static final String REALM_NAME = "service_account";

private static final Logger logger = LogManager.getLogger(ServiceAccountService.class);

private final ServiceAccountsTokenStore serviceAccountsTokenStore;
Expand All @@ -41,10 +40,6 @@ public ServiceAccountService(ServiceAccountsTokenStore serviceAccountsTokenStore
this.httpTlsRuntimeCheck = httpTlsRuntimeCheck;
}

public static boolean isServiceAccount(Authentication authentication) {
return REALM_TYPE.equals(authentication.getAuthenticatedBy().getType()) && null == authentication.getLookedUpBy();
}

public static boolean isServiceAccountPrincipal(String principal) {
return ACCOUNTS.containsKey(principal);
}
Expand Down Expand Up @@ -119,7 +114,7 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no
}

public void getRoleDescriptor(Authentication authentication, ActionListener<RoleDescriptor> listener) {
assert isServiceAccount(authentication) : "authentication is not for service account: " + authentication;
assert authentication.isServiceAccount() : "authentication is not for service account: " + authentication;
httpTlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "service account role descriptor resolving", () -> {
final String principal = authentication.getUser().principal();
final ServiceAccount account = ACCOUNTS.get(principal);
Expand All @@ -134,9 +129,10 @@ public void getRoleDescriptor(Authentication authentication, ActionListener<Role

private Authentication createAuthentication(ServiceAccount account, ServiceAccountToken token, String nodeName) {
final User user = account.asUser();
final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef(REALM_NAME, REALM_TYPE, nodeName);
final Authentication.RealmRef authenticatedBy =
new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName);
return new Authentication(user, authenticatedBy, null, Version.CURRENT, Authentication.AuthenticationType.TOKEN,
Map.of("_token_name", token.getTokenName()));
Map.of(TOKEN_NAME_FIELD, token.getTokenName()));
}

private ElasticsearchSecurityException createAuthenticationException(ServiceAccountToken serviceAccountToken) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public void getRoles(User user, Authentication authentication, ActionListener<Ro
return;
}

if (ServiceAccountService.isServiceAccount(authentication)) {
if (authentication.isServiceAccount()) {
getRolesForServiceAccount(authentication, roleActionListener);
} else if (ApiKeyService.isApiKeyAuthentication(authentication)) {
getRolesForApiKey(authentication, roleActionListener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.elasticsearch.test.MockLogAppender;
import org.elasticsearch.transport.Transport;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.support.ValidationTests;
import org.elasticsearch.xpack.core.security.user.User;
Expand Down Expand Up @@ -88,28 +89,6 @@ public void init() throws UnknownHostException {
new HttpTlsRuntimeCheck(builder.build(), new SetOnce<>(transport)));
}

public void testIsServiceAccount() {
final User user =
new User(randomAlphaOfLengthBetween(3, 8), randomArray(0, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
final Authentication.RealmRef authRealm;
final boolean authRealmIsForServiceAccount = randomBoolean();
if (authRealmIsForServiceAccount) {
authRealm = new Authentication.RealmRef(ServiceAccountService.REALM_NAME,
ServiceAccountService.REALM_TYPE,
randomAlphaOfLengthBetween(3, 8));
} else {
authRealm = randomRealmRef();
}
final Authentication.RealmRef lookupRealm = randomFrom(randomRealmRef(), null);
final Authentication authentication = new Authentication(user, authRealm, lookupRealm);

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

public void testGetServiceAccountPrincipals() {
assertThat(ServiceAccountService.getServiceAccountPrincipals(), equalTo(Set.of("elastic/fleet-server")));
}
Expand Down Expand Up @@ -266,12 +245,6 @@ public void testTryParseToken() throws IOException, IllegalAccessException {
}
}

private Authentication.RealmRef randomRealmRef() {
return new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8),
randomAlphaOfLengthBetween(3, 8),
randomAlphaOfLengthBetween(3, 8));
}

public void testTryAuthenticateBearerToken() throws ExecutionException, InterruptedException {
// Valid token
final PlainActionFuture<Authentication> future5 = new PlainActionFuture<>();
Expand All @@ -290,7 +263,7 @@ public void testTryAuthenticateBearerToken() throws ExecutionException, Interrup
new Authentication(
new User("elastic/fleet-server", Strings.EMPTY_ARRAY, "Service account - elastic/fleet-server", null,
Map.of("_elastic_service_account", true), true),
new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName),
new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName),
null, Version.CURRENT, Authentication.AuthenticationType.TOKEN,
Map.of("_token_name", "token1")
)
Expand Down Expand Up @@ -366,9 +339,10 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx
final Authentication authentication = future3.get();
assertThat(authentication, equalTo(new Authentication(
new User("elastic/fleet-server", Strings.EMPTY_ARRAY,
"Service account - elastic/fleet-server", null, Map.of("_elastic_service_account", true),
"Service account - elastic/fleet-server", null,
Map.of("_elastic_service_account", true),
true),
new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName),
new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName),
null, Version.CURRENT, Authentication.AuthenticationType.TOKEN,
Map.of("_token_name", token3.getTokenName())
)));
Expand Down Expand Up @@ -401,7 +375,7 @@ public void testGetRoleDescriptor() throws ExecutionException, InterruptedExcept
Map.of("_elastic_service_account", true),
true),
new Authentication.RealmRef(
ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
null,
Version.CURRENT,
Authentication.AuthenticationType.TOKEN,
Expand All @@ -419,7 +393,7 @@ ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaO
new User(username, Strings.EMPTY_ARRAY, "Service account - " + username, null,
Map.of("_elastic_service_account", true), true),
new Authentication.RealmRef(
ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
null,
Version.CURRENT,
Authentication.AuthenticationType.TOKEN,
Expand Down Expand Up @@ -449,7 +423,8 @@ public void testTlsRequired() {

final PlainActionFuture<RoleDescriptor> future2 = new PlainActionFuture<>();
final Authentication authentication = new Authentication(mock(User.class),
new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE,
new Authentication.RealmRef(
ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE,
randomAlphaOfLengthBetween(3, 8)),
null);
service.getRoleDescriptor(authentication, future2);
Expand Down