Skip to content

Commit 7294238

Browse files
ywangdtvernum
andauthored
Service Accounts - token name in response to Authenticate API (#71382) (#71808)
This PR adds the service account token name to a new top level field, "token", to the authenticate response. For now, this field will not show unless the authenticating credential is a service account. Co-authored-by: Tim Vernum <[email protected]>
1 parent 81334c0 commit 7294238

File tree

8 files changed

+83
-47
lines changed

8 files changed

+83
-47
lines changed

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

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.common.xcontent.XContentBuilder;
1717
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
1818
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
19+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
1920
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
2021
import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper;
2122
import org.elasticsearch.xpack.core.security.user.User;
@@ -28,6 +29,7 @@
2829
import java.util.Map;
2930
import java.util.Objects;
3031

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

3335
// TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField.
@@ -111,6 +113,10 @@ public Map<String, Object> getMetadata() {
111113
return metadata;
112114
}
113115

116+
public boolean isServiceAccount() {
117+
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType()) && null == getLookedUpBy();
118+
}
119+
114120
/**
115121
* Writes the authentication to the context. There must not be an existing authentication in the context and if there is an
116122
* {@link IllegalStateException} will be thrown
@@ -226,6 +232,11 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
226232
builder.array(User.Fields.ROLES.getPreferredName(), user.roles());
227233
builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName());
228234
builder.field(User.Fields.EMAIL.getPreferredName(), user.email());
235+
if (isServiceAccount()) {
236+
final String tokenName = (String) getMetadata().get(TOKEN_NAME_FIELD);
237+
assert tokenName != null : "token name cannot be null";
238+
builder.field(User.Fields.TOKEN.getPreferredName(), org.elasticsearch.common.collect.Map.of("name", tokenName));
239+
}
229240
builder.field(User.Fields.METADATA.getPreferredName(), user.metadata());
230241
builder.field(User.Fields.ENABLED.getPreferredName(), user.enabled());
231242
builder.startObject(User.Fields.AUTHENTICATION_REALM.getPreferredName());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.authc.service;
9+
10+
public final class ServiceAccountSettings {
11+
12+
public static final String REALM_TYPE = "service_account";
13+
public static final String REALM_NAME = "service_account";
14+
public static final String TOKEN_NAME_FIELD = "_token_name";
15+
16+
private ServiceAccountSettings() {}
17+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java

+1
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ public interface Fields {
245245
ParseField REALM_TYPE = new ParseField("type");
246246
ParseField REALM_NAME = new ParseField("name");
247247
ParseField AUTHENTICATION_TYPE = new ParseField("authentication_type");
248+
ParseField TOKEN = new ParseField("token");
248249
}
249250
}
250251

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

+28
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
1515
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
1616
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
17+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
1718
import org.elasticsearch.xpack.core.security.user.User;
1819

1920
import java.util.Arrays;
@@ -22,6 +23,7 @@
2223
import java.util.stream.Collectors;
2324

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

2628
public class AuthenticationTests extends ESTestCase {
2729

@@ -91,6 +93,32 @@ public void testCanAccessResourcesOf() {
9193
randomApiKeyAuthentication(randomFrom(user1, user2), apiKeyId2));
9294
}
9395

96+
public void testIsServiceAccount() {
97+
final User user =
98+
new User(randomAlphaOfLengthBetween(3, 8), randomArray(0, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
99+
final Authentication.RealmRef authRealm;
100+
final boolean authRealmIsForServiceAccount = randomBoolean();
101+
if (authRealmIsForServiceAccount) {
102+
authRealm = new Authentication.RealmRef(
103+
ServiceAccountSettings.REALM_NAME,
104+
ServiceAccountSettings.REALM_TYPE,
105+
randomAlphaOfLengthBetween(3, 8));
106+
} else {
107+
authRealm = new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8),
108+
randomAlphaOfLengthBetween(3, 8));
109+
}
110+
final Authentication.RealmRef lookupRealm = randomFrom(
111+
new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8),
112+
randomAlphaOfLengthBetween(3, 8)), null);
113+
final Authentication authentication = new Authentication(user, authRealm, lookupRealm);
114+
115+
if (authRealmIsForServiceAccount && lookupRealm == null) {
116+
assertThat(authentication.isServiceAccount(), is(true));
117+
} else {
118+
assertThat(authentication.isServiceAccount(), is(false));
119+
}
120+
}
121+
94122
private void checkCanAccessResources(Authentication authentication0, Authentication authentication1) {
95123
if (authentication0.getAuthenticationType() == authentication1.getAuthenticationType()
96124
|| EnumSet.of(AuthenticationType.REALM, AuthenticationType.TOKEN).equals(

x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.net.URL;
2828
import java.nio.file.Path;
2929
import java.util.List;
30+
import java.util.Locale;
3031
import java.util.Map;
3132

3233
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
@@ -48,9 +49,13 @@ public class ServiceAccountIT extends ESRestTestCase {
4849
+ " \"roles\": [],\n"
4950
+ " \"full_name\": \"Service account - elastic/fleet-server\",\n"
5051
+ " \"email\": null,\n"
52+
+ " \"token\": {\n"
53+
+ " \"name\": \"%s\"\n"
54+
+ " },\n"
5155
+ " \"metadata\": {\n"
5256
+ " \"_elastic_service_account\": true\n"
53-
+ " },\n" + " \"enabled\": true,\n"
57+
+ " },\n"
58+
+ " \"enabled\": true,\n"
5459
+ " \"authentication_realm\": {\n"
5560
+ " \"name\": \"service_account\",\n"
5661
+ " \"type\": \"service_account\"\n"
@@ -161,7 +166,9 @@ public void testAuthenticate() throws IOException {
161166
final Response response = client().performRequest(request);
162167
assertOK(response);
163168
assertThat(responseAsMap(response),
164-
equalTo(XContentHelper.convertToMap(new BytesArray(AUTHENTICATE_RESPONSE), false, XContentType.JSON).v2()));
169+
equalTo(XContentHelper.convertToMap(
170+
new BytesArray(String.format(Locale.ROOT, AUTHENTICATE_RESPONSE, "token1")),
171+
false, XContentType.JSON).v2()));
165172
}
166173

167174
public void testAuthenticateShouldNotFallThroughInCaseOfFailure() throws IOException {
@@ -237,7 +244,9 @@ public void testCreateApiServiceAccountTokenAndAuthenticateWithIt() throws IOExc
237244
final Response response = client().performRequest(request);
238245
assertOK(response);
239246
assertThat(responseAsMap(response),
240-
equalTo(XContentHelper.convertToMap(new BytesArray(AUTHENTICATE_RESPONSE), false, XContentType.JSON).v2()));
247+
equalTo(XContentHelper.convertToMap(
248+
new BytesArray(String.format(Locale.ROOT, AUTHENTICATE_RESPONSE, "api-token-1")),
249+
false, XContentType.JSON).v2()));
241250
}
242251

243252
public void testFileTokenAndApiTokenCanShareTheSameNameAndBothWorks() throws IOException {

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

+6-10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.rest.RestStatus;
1717
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse;
1818
import org.elasticsearch.xpack.core.security.authc.Authentication;
19+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
1920
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
2021
import org.elasticsearch.xpack.core.security.user.User;
2122
import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
@@ -24,13 +25,11 @@
2425
import java.util.Collection;
2526
import java.util.Map;
2627

28+
import static org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings.TOKEN_NAME_FIELD;
2729
import static org.elasticsearch.xpack.security.authc.service.ElasticServiceAccounts.ACCOUNTS;
2830

2931
public class ServiceAccountService {
3032

31-
public static final String REALM_TYPE = "service_account";
32-
public static final String REALM_NAME = "service_account";
33-
3433
private static final Logger logger = LogManager.getLogger(ServiceAccountService.class);
3534

3635
private final ServiceAccountsTokenStore serviceAccountsTokenStore;
@@ -41,10 +40,6 @@ public ServiceAccountService(ServiceAccountsTokenStore serviceAccountsTokenStore
4140
this.httpTlsRuntimeCheck = httpTlsRuntimeCheck;
4241
}
4342

44-
public static boolean isServiceAccount(Authentication authentication) {
45-
return REALM_TYPE.equals(authentication.getAuthenticatedBy().getType()) && null == authentication.getLookedUpBy();
46-
}
47-
4843
public static boolean isServiceAccountPrincipal(String principal) {
4944
return ACCOUNTS.containsKey(principal);
5045
}
@@ -119,7 +114,7 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no
119114
}
120115

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

135130
private Authentication createAuthentication(ServiceAccount account, ServiceAccountToken token, String nodeName) {
136131
final User user = account.asUser();
137-
final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef(REALM_NAME, REALM_TYPE, nodeName);
132+
final Authentication.RealmRef authenticatedBy =
133+
new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName);
138134
return new Authentication(user, authenticatedBy, null, Version.CURRENT, Authentication.AuthenticationType.TOKEN,
139-
org.elasticsearch.common.collect.Map.of("_token_name", token.getTokenName()));
135+
org.elasticsearch.common.collect.Map.of(TOKEN_NAME_FIELD, token.getTokenName()));
140136
}
141137

142138
private ElasticsearchSecurityException createAuthenticationException(ServiceAccountToken serviceAccountToken) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ public void getRoles(User user, Authentication authentication, ActionListener<Ro
229229
return;
230230
}
231231

232-
if (ServiceAccountService.isServiceAccount(authentication)) {
232+
if (authentication.isServiceAccount()) {
233233
getRolesForServiceAccount(authentication, roleActionListener);
234234
} else if (ApiKeyService.isApiKeyAuthentication(authentication)) {
235235
getRolesForApiKey(authentication, roleActionListener);

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

+7-33
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.test.MockLogAppender;
2929
import org.elasticsearch.transport.Transport;
3030
import org.elasticsearch.xpack.core.security.authc.Authentication;
31+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
3132
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
3233
import org.elasticsearch.xpack.core.security.support.ValidationTests;
3334
import org.elasticsearch.xpack.core.security.user.User;
@@ -86,28 +87,6 @@ public void init() throws UnknownHostException {
8687
new HttpTlsRuntimeCheck(builder.build(), new SetOnce<>(transport)));
8788
}
8889

89-
public void testIsServiceAccount() {
90-
final User user =
91-
new User(randomAlphaOfLengthBetween(3, 8), randomArray(0, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
92-
final Authentication.RealmRef authRealm;
93-
final boolean authRealmIsForServiceAccount = randomBoolean();
94-
if (authRealmIsForServiceAccount) {
95-
authRealm = new Authentication.RealmRef(ServiceAccountService.REALM_NAME,
96-
ServiceAccountService.REALM_TYPE,
97-
randomAlphaOfLengthBetween(3, 8));
98-
} else {
99-
authRealm = randomRealmRef();
100-
}
101-
final Authentication.RealmRef lookupRealm = randomFrom(randomRealmRef(), null);
102-
final Authentication authentication = new Authentication(user, authRealm, lookupRealm);
103-
104-
if (authRealmIsForServiceAccount && lookupRealm == null) {
105-
assertThat(ServiceAccountService.isServiceAccount(authentication), is(true));
106-
} else {
107-
assertThat(ServiceAccountService.isServiceAccount(authentication), is(false));
108-
}
109-
}
110-
11190
public void testGetServiceAccountPrincipals() {
11291
assertThat(ServiceAccountService.getServiceAccountPrincipals(),
11392
equalTo(org.elasticsearch.common.collect.Set.of("elastic/fleet-server")));
@@ -266,12 +245,6 @@ public void testTryParseToken() throws IOException, IllegalAccessException {
266245
}
267246
}
268247

269-
private Authentication.RealmRef randomRealmRef() {
270-
return new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8),
271-
randomAlphaOfLengthBetween(3, 8),
272-
randomAlphaOfLengthBetween(3, 8));
273-
}
274-
275248
public void testTryAuthenticateBearerToken() throws ExecutionException, InterruptedException {
276249
// Valid token
277250
final PlainActionFuture<Authentication> future5 = new PlainActionFuture<>();
@@ -290,7 +263,7 @@ public void testTryAuthenticateBearerToken() throws ExecutionException, Interrup
290263
new Authentication(
291264
new User("elastic/fleet-server", Strings.EMPTY_ARRAY, "Service account - elastic/fleet-server", null,
292265
org.elasticsearch.common.collect.Map.of("_elastic_service_account", true), true),
293-
new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName),
266+
new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName),
294267
null, Version.CURRENT, Authentication.AuthenticationType.TOKEN,
295268
org.elasticsearch.common.collect.Map.of("_token_name", "token1")
296269
)
@@ -369,7 +342,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx
369342
"Service account - elastic/fleet-server", null,
370343
org.elasticsearch.common.collect.Map.of("_elastic_service_account", true),
371344
true),
372-
new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName),
345+
new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName),
373346
null, Version.CURRENT, Authentication.AuthenticationType.TOKEN,
374347
org.elasticsearch.common.collect.Map.of("_token_name", token3.getTokenName())
375348
)));
@@ -402,7 +375,7 @@ public void testGetRoleDescriptor() throws ExecutionException, InterruptedExcept
402375
org.elasticsearch.common.collect.Map.of("_elastic_service_account", true),
403376
true),
404377
new Authentication.RealmRef(
405-
ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
378+
ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
406379
null,
407380
Version.CURRENT,
408381
Authentication.AuthenticationType.TOKEN,
@@ -420,7 +393,7 @@ ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaO
420393
new User(username, Strings.EMPTY_ARRAY, "Service account - " + username, null,
421394
org.elasticsearch.common.collect.Map.of("_elastic_service_account", true), true),
422395
new Authentication.RealmRef(
423-
ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
396+
ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, randomAlphaOfLengthBetween(3, 8)),
424397
null,
425398
Version.CURRENT,
426399
Authentication.AuthenticationType.TOKEN,
@@ -450,7 +423,8 @@ public void testTlsRequired() {
450423

451424
final PlainActionFuture<RoleDescriptor> future2 = new PlainActionFuture<>();
452425
final Authentication authentication = new Authentication(mock(User.class),
453-
new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE,
426+
new Authentication.RealmRef(
427+
ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE,
454428
randomAlphaOfLengthBetween(3, 8)),
455429
null);
456430
service.getRoleDescriptor(authentication, future2);

0 commit comments

Comments
 (0)