Skip to content

Commit e4f7132

Browse files
authored
Return file-backed service tokens from all nodes (#75200)
The Get service account credentials API now returns file-backed tokens from all nodes instead of only the local node. For each file-backed service token, we list names of the nodes where this token is found. The response for node-local credentials (currently only file-backed tokens) is place inside the "nodes_credentials.file_tokens" field. There is also a nodes_credentials._nodes field containing information about the overall request execution (it works the same way as the _nodes field of Nodes info API, etc.) Detailed response sample can be found in #74530 This PR also removes the beta label from the API's documentation page. Resolves: #74530
1 parent c6a90bb commit e4f7132

File tree

34 files changed

+928
-583
lines changed

34 files changed

+928
-583
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetServiceAccountCredentialsResponse.java

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,84 +9,83 @@
99
package org.elasticsearch.client.security;
1010

1111
import org.elasticsearch.client.security.support.ServiceTokenInfo;
12-
import org.elasticsearch.common.xcontent.ParseField;
1312
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
13+
import org.elasticsearch.common.xcontent.ParseField;
1414
import org.elasticsearch.common.xcontent.XContentParser;
1515

1616
import java.io.IOException;
17+
import java.util.ArrayList;
1718
import java.util.List;
18-
import java.util.Map;
1919
import java.util.Objects;
20-
import java.util.stream.Collectors;
21-
import java.util.stream.Stream;
2220

2321
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
22+
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
2423

2524
/**
2625
* Response when requesting credentials of a service account.
2726
*/
2827
public final class GetServiceAccountCredentialsResponse {
2928

3029
private final String principal;
31-
private final String nodeName;
32-
private final List<ServiceTokenInfo> serviceTokenInfos;
30+
private final List<ServiceTokenInfo> indexTokenInfos;
31+
private final ServiceAccountCredentialsNodesResponse nodesResponse;
3332

34-
public GetServiceAccountCredentialsResponse(
35-
String principal, String nodeName, List<ServiceTokenInfo> serviceTokenInfos) {
33+
public GetServiceAccountCredentialsResponse(String principal,
34+
List<ServiceTokenInfo> indexTokenInfos,
35+
ServiceAccountCredentialsNodesResponse nodesResponse) {
3636
this.principal = Objects.requireNonNull(principal, "principal is required");
37-
this.nodeName = Objects.requireNonNull(nodeName, "nodeName is required");
38-
this.serviceTokenInfos = List.copyOf(Objects.requireNonNull(serviceTokenInfos, "service token infos are required)"));
37+
this.indexTokenInfos = List.copyOf(Objects.requireNonNull(indexTokenInfos, "service token infos are required"));
38+
this.nodesResponse = Objects.requireNonNull(nodesResponse, "nodes response is required");
3939
}
4040

4141
public String getPrincipal() {
4242
return principal;
4343
}
4444

45-
public String getNodeName() {
46-
return nodeName;
47-
}
48-
49-
public List<ServiceTokenInfo> getServiceTokenInfos() {
50-
return serviceTokenInfos;
45+
public List<ServiceTokenInfo> getIndexTokenInfos() {
46+
return indexTokenInfos;
5147
}
5248

53-
@Override
54-
public boolean equals(Object o) {
55-
if (this == o)
56-
return true;
57-
if (o == null || getClass() != o.getClass())
58-
return false;
59-
GetServiceAccountCredentialsResponse that = (GetServiceAccountCredentialsResponse) o;
60-
return principal.equals(that.principal) && nodeName.equals(that.nodeName) && serviceTokenInfos.equals(that.serviceTokenInfos);
61-
}
62-
63-
@Override
64-
public int hashCode() {
65-
return Objects.hash(principal, nodeName, serviceTokenInfos);
49+
public ServiceAccountCredentialsNodesResponse getNodesResponse() {
50+
return nodesResponse;
6651
}
6752

53+
@SuppressWarnings("unchecked")
6854
static ConstructingObjectParser<GetServiceAccountCredentialsResponse, Void> PARSER =
6955
new ConstructingObjectParser<>("get_service_account_credentials_response",
7056
args -> {
71-
@SuppressWarnings("unchecked")
72-
final List<ServiceTokenInfo> tokenInfos = Stream.concat(
73-
((Map<String, Object>) args[3]).keySet().stream().map(name -> new ServiceTokenInfo(name, "index")),
74-
((Map<String, Object>) args[4]).keySet().stream().map(name -> new ServiceTokenInfo(name, "file")))
75-
.collect(Collectors.toList());
76-
assert tokenInfos.size() == (int) args[2] : "number of tokens do not match";
77-
return new GetServiceAccountCredentialsResponse((String) args[0], (String) args[1], tokenInfos);
57+
final int count = (int) args[1];
58+
final List<ServiceTokenInfo> indexTokenInfos = (List<ServiceTokenInfo>) args[2];
59+
final ServiceAccountCredentialsNodesResponse fileTokensResponse = (ServiceAccountCredentialsNodesResponse) args[3];
60+
if (count != indexTokenInfos.size() + fileTokensResponse.getFileTokenInfos().size()) {
61+
throw new IllegalArgumentException("number of tokens do not match");
62+
}
63+
return new GetServiceAccountCredentialsResponse((String) args[0], indexTokenInfos, fileTokensResponse);
7864
});
7965

8066
static {
8167
PARSER.declareString(constructorArg(), new ParseField("service_account"));
82-
PARSER.declareString(constructorArg(), new ParseField("node_name"));
8368
PARSER.declareInt(constructorArg(), new ParseField("count"));
84-
PARSER.declareObject(constructorArg(), (p, c) -> p.map(), new ParseField("tokens"));
85-
PARSER.declareObject(constructorArg(), (p, c) -> p.map(), new ParseField("file_tokens"));
69+
PARSER.declareObject(constructorArg(),
70+
(p, c) -> GetServiceAccountCredentialsResponse.parseIndexTokenInfos(p), new ParseField("tokens"));
71+
PARSER.declareObject(constructorArg(),
72+
(p, c) -> ServiceAccountCredentialsNodesResponse.fromXContent(p), new ParseField("nodes_credentials"));
8673
}
8774

8875
public static GetServiceAccountCredentialsResponse fromXContent(XContentParser parser) throws IOException {
8976
return PARSER.parse(parser, null);
9077
}
9178

79+
static List<ServiceTokenInfo> parseIndexTokenInfos(XContentParser parser) throws IOException {
80+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
81+
final List<ServiceTokenInfo> indexTokenInfos = new ArrayList<>();
82+
XContentParser.Token token;
83+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
84+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser);
85+
indexTokenInfos.add(new ServiceTokenInfo(parser.currentName(), "index"));
86+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
87+
ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser);
88+
}
89+
return indexTokenInfos;
90+
}
9291
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.client.security;
10+
11+
import org.elasticsearch.client.NodesResponseHeader;
12+
import org.elasticsearch.client.security.support.ServiceTokenInfo;
13+
import org.elasticsearch.common.xcontent.XContentParser;
14+
import org.elasticsearch.common.xcontent.XContentParserUtils;
15+
16+
import java.io.IOException;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
20+
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
21+
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName;
22+
23+
public class ServiceAccountCredentialsNodesResponse {
24+
25+
private final NodesResponseHeader header;
26+
private final List<ServiceTokenInfo> fileTokenInfos;
27+
28+
public ServiceAccountCredentialsNodesResponse(
29+
NodesResponseHeader header, List<ServiceTokenInfo> fileTokenInfos) {
30+
this.header = header;
31+
this.fileTokenInfos = fileTokenInfos;
32+
}
33+
34+
public NodesResponseHeader getHeader() {
35+
return header;
36+
}
37+
38+
public List<ServiceTokenInfo> getFileTokenInfos() {
39+
return fileTokenInfos;
40+
}
41+
42+
public static ServiceAccountCredentialsNodesResponse fromXContent(XContentParser parser) throws IOException {
43+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
44+
NodesResponseHeader header = null;
45+
List<ServiceTokenInfo> fileTokenInfos = List.of();
46+
XContentParser.Token token;
47+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
48+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser);
49+
if ("_nodes".equals(parser.currentName())) {
50+
if (header == null) {
51+
header = NodesResponseHeader.fromXContent(parser, null);
52+
} else {
53+
throw new IllegalArgumentException("expecting only a single [_nodes] field, multiple found");
54+
}
55+
} else if ("file_tokens".equals(parser.currentName())) {
56+
fileTokenInfos = parseFileToken(parser);
57+
} else {
58+
throw new IllegalArgumentException("expecting field of either [_nodes] or [file_tokens], found ["
59+
+ parser.currentName() + "]");
60+
}
61+
}
62+
return new ServiceAccountCredentialsNodesResponse(header, fileTokenInfos);
63+
}
64+
65+
static List<ServiceTokenInfo> parseFileToken(XContentParser parser) throws IOException {
66+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
67+
XContentParser.Token token;
68+
final ArrayList<ServiceTokenInfo> fileTokenInfos = new ArrayList<>();
69+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
70+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser);
71+
final String tokenName = parser.currentName();
72+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
73+
ensureFieldName(parser, parser.nextToken(), "nodes");
74+
parser.nextToken();
75+
final List<String> nodeNames = XContentParserUtils.parseList(parser, XContentParser::text);
76+
ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser);
77+
fileTokenInfos.add(new ServiceTokenInfo(tokenName, "file", nodeNames));
78+
}
79+
return fileTokenInfos;
80+
}
81+
}

client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ServiceTokenInfo.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,25 @@
88

99
package org.elasticsearch.client.security.support;
1010

11+
import org.elasticsearch.core.Nullable;
12+
13+
import java.util.Collection;
1114
import java.util.Objects;
1215

1316
public class ServiceTokenInfo {
1417
private final String name;
1518
private final String source;
19+
@Nullable
20+
private final Collection<String> nodeNames;
1621

1722
public ServiceTokenInfo(String name, String source) {
23+
this(name, source, null);
24+
}
25+
26+
public ServiceTokenInfo(String name, String source, Collection<String> nodeNames) {
1827
this.name = Objects.requireNonNull(name, "token name is required");
1928
this.source = Objects.requireNonNull(source, "token source is required");
29+
this.nodeNames = nodeNames;
2030
}
2131

2232
public String getName() {
@@ -27,23 +37,27 @@ public String getSource() {
2737
return source;
2838
}
2939

40+
public Collection<String> getNodeNames() {
41+
return nodeNames;
42+
}
43+
3044
@Override
3145
public boolean equals(Object o) {
3246
if (this == o)
3347
return true;
3448
if (o == null || getClass() != o.getClass())
3549
return false;
3650
ServiceTokenInfo that = (ServiceTokenInfo) o;
37-
return name.equals(that.name) && source.equals(that.source);
51+
return Objects.equals(name, that.name) && Objects.equals(source, that.source) && Objects.equals(nodeNames, that.nodeNames);
3852
}
3953

4054
@Override
4155
public int hashCode() {
42-
return Objects.hash(name, source);
56+
return Objects.hash(name, source, nodeNames);
4357
}
4458

4559
@Override
4660
public String toString() {
47-
return "ServiceTokenInfo{" + "name='" + name + '\'' + ", source='" + source + '\'' + '}';
61+
return "ServiceTokenInfo{" + "name='" + name + '\'' + ", source='" + source + '\'' + ", nodeNames=" + nodeNames + '}';
4862
}
4963
}

client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.action.LatchedActionListener;
1515
import org.elasticsearch.action.support.PlainActionFuture;
1616
import org.elasticsearch.client.ESRestHighLevelClientTestCase;
17+
import org.elasticsearch.client.NodesResponseHeader;
1718
import org.elasticsearch.client.RequestOptions;
1819
import org.elasticsearch.client.RestHighLevelClient;
1920
import org.elasticsearch.client.security.AuthenticateResponse;
@@ -122,6 +123,7 @@
122123
import java.util.ArrayList;
123124
import java.util.Arrays;
124125
import java.util.Base64;
126+
import java.util.Collection;
125127
import java.util.Collections;
126128
import java.util.Comparator;
127129
import java.util.HashMap;
@@ -2745,17 +2747,23 @@ public void testGetServiceAccountCredentials() throws IOException {
27452747

27462748
// tag::get-service-account-credentials-response
27472749
final String principal = getServiceAccountCredentialsResponse.getPrincipal(); // <1>
2748-
final String nodeName = getServiceAccountCredentialsResponse.getNodeName(); // <2>
2749-
final List<ServiceTokenInfo> serviceTokenInfos = getServiceAccountCredentialsResponse.getServiceTokenInfos(); // <3>
2750-
final String tokenName = serviceTokenInfos.get(0).getName(); // <4>
2751-
final String tokenSource = serviceTokenInfos.get(0).getSource(); // <5>
2750+
final List<ServiceTokenInfo> indexTokenInfos = getServiceAccountCredentialsResponse.getIndexTokenInfos(); // <2>
2751+
final String tokenName = indexTokenInfos.get(0).getName(); // <3>
2752+
final String tokenSource = indexTokenInfos.get(0).getSource(); // <4>
2753+
final Collection<String> nodeNames = indexTokenInfos.get(0).getNodeNames(); // <5>
2754+
final List<ServiceTokenInfo> fileTokenInfos
2755+
= getServiceAccountCredentialsResponse.getNodesResponse().getFileTokenInfos(); // <6>
2756+
final NodesResponseHeader fileTokensResponseHeader
2757+
= getServiceAccountCredentialsResponse.getNodesResponse().getHeader(); // <7>
2758+
final int nSuccessful = fileTokensResponseHeader.getSuccessful(); // <8>
2759+
final int nFailed = fileTokensResponseHeader.getFailed(); // <9>
27522760
// end::get-service-account-credentials-response
27532761
assertThat(principal, equalTo("elastic/fleet-server"));
27542762
// Cannot assert exactly one token because there are rare occasions where tests overlap and it will see
27552763
// token created from other tests
2756-
assertThat(serviceTokenInfos.size(), greaterThanOrEqualTo(1));
2757-
assertThat(serviceTokenInfos.stream().map(ServiceTokenInfo::getName).collect(Collectors.toSet()), hasItem("token2"));
2758-
assertThat(serviceTokenInfos.stream().map(ServiceTokenInfo::getSource).collect(Collectors.toSet()), hasItem("index"));
2764+
assertThat(indexTokenInfos.size(), greaterThanOrEqualTo(1));
2765+
assertThat(indexTokenInfos.stream().map(ServiceTokenInfo::getName).collect(Collectors.toSet()), hasItem("token2"));
2766+
assertThat(indexTokenInfos.stream().map(ServiceTokenInfo::getSource).collect(Collectors.toSet()), hasItem("index"));
27592767
}
27602768

27612769
{
@@ -2787,8 +2795,8 @@ public void onFailure(Exception e) {
27872795

27882796
assertNotNull(future.actionGet());
27892797
assertThat(future.actionGet().getPrincipal(), equalTo("elastic/fleet-server"));
2790-
assertThat(future.actionGet().getServiceTokenInfos().size(), greaterThanOrEqualTo(1));
2791-
assertThat(future.actionGet().getServiceTokenInfos().stream().map(ServiceTokenInfo::getName).collect(Collectors.toSet()),
2798+
assertThat(future.actionGet().getIndexTokenInfos().size(), greaterThanOrEqualTo(1));
2799+
assertThat(future.actionGet().getIndexTokenInfos().stream().map(ServiceTokenInfo::getName).collect(Collectors.toSet()),
27922800
hasItem("token2"));
27932801
}
27942802
}

client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetServiceAccountCredentialsResponseTests.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@
88

99
package org.elasticsearch.client.security;
1010

11+
import org.elasticsearch.Version;
12+
import org.elasticsearch.action.FailedNodeException;
1113
import org.elasticsearch.client.AbstractResponseTestCase;
14+
import org.elasticsearch.cluster.ClusterName;
15+
import org.elasticsearch.cluster.node.DiscoveryNode;
16+
import org.elasticsearch.common.transport.TransportAddress;
1217
import org.elasticsearch.core.Tuple;
1318
import org.elasticsearch.common.xcontent.XContentParser;
1419
import org.elasticsearch.common.xcontent.XContentType;
20+
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsNodesResponse;
1521
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
1622

1723
import java.io.IOException;
24+
import java.util.List;
1825
import java.util.Locale;
1926
import java.util.stream.Collectors;
27+
import java.util.stream.Stream;
2028

2129
import static org.hamcrest.Matchers.equalTo;
2230

@@ -27,15 +35,17 @@ public class GetServiceAccountCredentialsResponseTests
2735
@Override
2836
protected org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsResponse createServerTestInstance(
2937
XContentType xContentType) {
38+
final String[] fileTokenNames = randomArray(3, 5, String[]::new, () -> randomAlphaOfLengthBetween(3, 8));
39+
final GetServiceAccountCredentialsNodesResponse nodesResponse = new GetServiceAccountCredentialsNodesResponse(
40+
new ClusterName(randomAlphaOfLength(12)),
41+
List.of(new GetServiceAccountCredentialsNodesResponse.Node(new DiscoveryNode(randomAlphaOfLength(10),
42+
new TransportAddress(TransportAddress.META_ADDRESS, 9300),
43+
Version.CURRENT), fileTokenNames)),
44+
List.of(new FailedNodeException(randomAlphaOfLength(11), "error", new NoSuchFieldError("service_tokens"))));
3045
return new org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsResponse(
3146
randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8),
32-
randomAlphaOfLengthBetween(3, 8), randomList(
33-
1,
34-
5,
35-
() -> randomBoolean() ?
36-
TokenInfo.fileToken(randomAlphaOfLengthBetween(3, 8)) :
37-
TokenInfo.indexToken(randomAlphaOfLengthBetween(3, 8)))
38-
);
47+
randomList(0, 5, () -> TokenInfo.indexToken(randomAlphaOfLengthBetween(3, 8))),
48+
nodesResponse);
3949
}
4050

4151
@Override
@@ -48,14 +58,19 @@ protected void assertInstances(
4858
org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsResponse serverTestInstance,
4959
GetServiceAccountCredentialsResponse clientInstance) {
5060
assertThat(serverTestInstance.getPrincipal(), equalTo(clientInstance.getPrincipal()));
51-
assertThat(serverTestInstance.getNodeName(), equalTo(clientInstance.getNodeName()));
5261

5362
assertThat(
54-
serverTestInstance.getTokenInfos().stream()
63+
Stream.concat(serverTestInstance.getIndexTokenInfos().stream(),
64+
serverTestInstance.getNodesResponse().getFileTokenInfos().stream())
5565
.map(tokenInfo -> new Tuple<>(tokenInfo.getName(), tokenInfo.getSource().name().toLowerCase(Locale.ROOT)))
5666
.collect(Collectors.toSet()),
57-
equalTo(clientInstance.getServiceTokenInfos().stream()
67+
equalTo(Stream.concat(clientInstance.getIndexTokenInfos().stream(),
68+
clientInstance.getNodesResponse().getFileTokenInfos().stream())
5869
.map(info -> new Tuple<>(info.getName(), info.getSource()))
5970
.collect(Collectors.toSet())));
71+
72+
assertThat(
73+
serverTestInstance.getNodesResponse().failures().size(),
74+
equalTo(clientInstance.getNodesResponse().getHeader().getFailures().size()));
6075
}
6176
}

0 commit comments

Comments
 (0)