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

Return file-backed service tokens from all nodes #75200

Merged
merged 21 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from 19 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,84 +9,83 @@
package org.elasticsearch.client.security;

import org.elasticsearch.client.security.support.ServiceTokenInfo;
import org.elasticsearch.common.xcontent.ParseField;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ParseField;
import org.elasticsearch.common.xcontent.XContentParser;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;

/**
* Response when requesting credentials of a service account.
*/
public final class GetServiceAccountCredentialsResponse {

private final String principal;
private final String nodeName;
private final List<ServiceTokenInfo> serviceTokenInfos;
private final List<ServiceTokenInfo> indexTokenInfos;
private final ServiceAccountCredentialsNodesResponse nodesResponse;

public GetServiceAccountCredentialsResponse(
String principal, String nodeName, List<ServiceTokenInfo> serviceTokenInfos) {
public GetServiceAccountCredentialsResponse(String principal,
List<ServiceTokenInfo> indexTokenInfos,
ServiceAccountCredentialsNodesResponse nodesResponse) {
this.principal = Objects.requireNonNull(principal, "principal is required");
this.nodeName = Objects.requireNonNull(nodeName, "nodeName is required");
this.serviceTokenInfos = List.copyOf(Objects.requireNonNull(serviceTokenInfos, "service token infos are required)"));
this.indexTokenInfos = List.copyOf(Objects.requireNonNull(indexTokenInfos, "service token infos are required"));
this.nodesResponse = Objects.requireNonNull(nodesResponse, "nodes response is required");
}

public String getPrincipal() {
return principal;
}

public String getNodeName() {
return nodeName;
}

public List<ServiceTokenInfo> getServiceTokenInfos() {
return serviceTokenInfos;
public List<ServiceTokenInfo> getIndexTokenInfos() {
return indexTokenInfos;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
GetServiceAccountCredentialsResponse that = (GetServiceAccountCredentialsResponse) o;
return principal.equals(that.principal) && nodeName.equals(that.nodeName) && serviceTokenInfos.equals(that.serviceTokenInfos);
}

@Override
public int hashCode() {
return Objects.hash(principal, nodeName, serviceTokenInfos);
public ServiceAccountCredentialsNodesResponse getNodesResponse() {
return nodesResponse;
}

@SuppressWarnings("unchecked")
static ConstructingObjectParser<GetServiceAccountCredentialsResponse, Void> PARSER =
new ConstructingObjectParser<>("get_service_account_credentials_response",
args -> {
@SuppressWarnings("unchecked")
final List<ServiceTokenInfo> tokenInfos = Stream.concat(
((Map<String, Object>) args[3]).keySet().stream().map(name -> new ServiceTokenInfo(name, "index")),
((Map<String, Object>) args[4]).keySet().stream().map(name -> new ServiceTokenInfo(name, "file")))
.collect(Collectors.toList());
assert tokenInfos.size() == (int) args[2] : "number of tokens do not match";
return new GetServiceAccountCredentialsResponse((String) args[0], (String) args[1], tokenInfos);
final int count = (int) args[1];
final List<ServiceTokenInfo> indexTokenInfos = (List<ServiceTokenInfo>) args[2];
final ServiceAccountCredentialsNodesResponse fileTokensResponse = (ServiceAccountCredentialsNodesResponse) args[3];
if (count != indexTokenInfos.size() + fileTokensResponse.getFileTokenInfos().size()) {
throw new IllegalArgumentException("number of tokens do not match");
}
return new GetServiceAccountCredentialsResponse((String) args[0], indexTokenInfos, fileTokensResponse);
});

static {
PARSER.declareString(constructorArg(), new ParseField("service_account"));
PARSER.declareString(constructorArg(), new ParseField("node_name"));
PARSER.declareInt(constructorArg(), new ParseField("count"));
PARSER.declareObject(constructorArg(), (p, c) -> p.map(), new ParseField("tokens"));
PARSER.declareObject(constructorArg(), (p, c) -> p.map(), new ParseField("file_tokens"));
PARSER.declareObject(constructorArg(),
(p, c) -> GetServiceAccountCredentialsResponse.parseIndexTokenInfos(p), new ParseField("tokens"));
PARSER.declareObject(constructorArg(),
(p, c) -> ServiceAccountCredentialsNodesResponse.fromXContent(p), new ParseField("nodes_credentials"));
}

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

static List<ServiceTokenInfo> parseIndexTokenInfos(XContentParser parser) throws IOException {
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
final List<ServiceTokenInfo> indexTokenInfos = new ArrayList<>();
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser);
indexTokenInfos.add(new ServiceTokenInfo(parser.currentName(), "index"));
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser);
}
return indexTokenInfos;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.client.security;

import org.elasticsearch.client.NodesResponseHeader;
import org.elasticsearch.client.security.support.ServiceTokenInfo;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParserUtils;

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

import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName;

public class ServiceAccountCredentialsNodesResponse {

private final NodesResponseHeader header;
private final List<ServiceTokenInfo> fileTokenInfos;

public ServiceAccountCredentialsNodesResponse(
NodesResponseHeader header, List<ServiceTokenInfo> fileTokenInfos) {
this.header = header;
this.fileTokenInfos = fileTokenInfos;
}

public NodesResponseHeader getHeader() {
return header;
}

public List<ServiceTokenInfo> getFileTokenInfos() {
return fileTokenInfos;
}

public static ServiceAccountCredentialsNodesResponse fromXContent(XContentParser parser) throws IOException {
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
NodesResponseHeader header = null;
List<ServiceTokenInfo> fileTokenInfos = List.of();
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser);
if ("_nodes".equals(parser.currentName())) {
if (header == null) {
header = NodesResponseHeader.fromXContent(parser, null);
} else {
throw new IllegalArgumentException("expecting only a single [_nodes] field, multiple found");
}
} else if ("file_tokens".equals(parser.currentName())) {
fileTokenInfos = parseFileToken(parser);
} else {
throw new IllegalArgumentException("expecting field of either [_nodes] or [file_tokens], found ["
+ parser.currentName() + "]");
}
}
return new ServiceAccountCredentialsNodesResponse(header, fileTokenInfos);
}

static List<ServiceTokenInfo> parseFileToken(XContentParser parser) throws IOException {
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
XContentParser.Token token;
final ArrayList<ServiceTokenInfo> fileTokenInfos = new ArrayList<>();
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser);
final String tokenName = parser.currentName();
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser);
ensureFieldName(parser, parser.nextToken(), "nodes");
parser.nextToken();
final List<String> nodeNames = XContentParserUtils.parseList(parser, XContentParser::text);
ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser);
fileTokenInfos.add(new ServiceTokenInfo(tokenName, "file", nodeNames));
}
return fileTokenInfos;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@

package org.elasticsearch.client.security.support;

import org.elasticsearch.core.Nullable;

import java.util.Collection;
import java.util.Objects;

public class ServiceTokenInfo {
private final String name;
private final String source;
@Nullable
private final Collection<String> nodeNames;

public ServiceTokenInfo(String name, String source) {
this(name, source, null);
}

public ServiceTokenInfo(String name, String source, Collection<String> nodeNames) {
this.name = Objects.requireNonNull(name, "token name is required");
this.source = Objects.requireNonNull(source, "token source is required");
this.nodeNames = nodeNames;
}

public String getName() {
Expand All @@ -27,23 +37,27 @@ public String getSource() {
return source;
}

public Collection<String> getNodeNames() {
return nodeNames;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ServiceTokenInfo that = (ServiceTokenInfo) o;
return name.equals(that.name) && source.equals(that.source);
return Objects.equals(name, that.name) && Objects.equals(source, that.source) && Objects.equals(nodeNames, that.nodeNames);
}

@Override
public int hashCode() {
return Objects.hash(name, source);
return Objects.hash(name, source, nodeNames);
}

@Override
public String toString() {
return "ServiceTokenInfo{" + "name='" + name + '\'' + ", source='" + source + '\'' + '}';
return "ServiceTokenInfo{" + "name='" + name + '\'' + ", source='" + source + '\'' + ", nodeNames=" + nodeNames + '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.action.LatchedActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.client.ESRestHighLevelClientTestCase;
import org.elasticsearch.client.NodesResponseHeader;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.security.AuthenticateResponse;
Expand Down Expand Up @@ -122,6 +123,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
Expand Down Expand Up @@ -2745,17 +2747,23 @@ public void testGetServiceAccountCredentials() throws IOException {

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

{
Expand Down Expand Up @@ -2787,8 +2795,8 @@ public void onFailure(Exception e) {

assertNotNull(future.actionGet());
assertThat(future.actionGet().getPrincipal(), equalTo("elastic/fleet-server"));
assertThat(future.actionGet().getServiceTokenInfos().size(), greaterThanOrEqualTo(1));
assertThat(future.actionGet().getServiceTokenInfos().stream().map(ServiceTokenInfo::getName).collect(Collectors.toSet()),
assertThat(future.actionGet().getIndexTokenInfos().size(), greaterThanOrEqualTo(1));
assertThat(future.actionGet().getIndexTokenInfos().stream().map(ServiceTokenInfo::getName).collect(Collectors.toSet()),
hasItem("token2"));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@

package org.elasticsearch.client.security;

import org.elasticsearch.Version;
import org.elasticsearch.action.FailedNodeException;
import org.elasticsearch.client.AbstractResponseTestCase;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsNodesResponse;
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;

import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.hamcrest.Matchers.equalTo;

Expand All @@ -27,15 +35,17 @@ public class GetServiceAccountCredentialsResponseTests
@Override
protected org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsResponse createServerTestInstance(
XContentType xContentType) {
final String[] fileTokenNames = randomArray(3, 5, String[]::new, () -> randomAlphaOfLengthBetween(3, 8));
final GetServiceAccountCredentialsNodesResponse nodesResponse = new GetServiceAccountCredentialsNodesResponse(
new ClusterName(randomAlphaOfLength(12)),
List.of(new GetServiceAccountCredentialsNodesResponse.Node(new DiscoveryNode(randomAlphaOfLength(10),
new TransportAddress(TransportAddress.META_ADDRESS, 9300),
Version.CURRENT), fileTokenNames)),
List.of(new FailedNodeException(randomAlphaOfLength(11), "error", new NoSuchFieldError("service_tokens"))));
return new org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsResponse(
randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8),
randomAlphaOfLengthBetween(3, 8), randomList(
1,
5,
() -> randomBoolean() ?
TokenInfo.fileToken(randomAlphaOfLengthBetween(3, 8)) :
TokenInfo.indexToken(randomAlphaOfLengthBetween(3, 8)))
);
randomList(0, 5, () -> TokenInfo.indexToken(randomAlphaOfLengthBetween(3, 8))),
nodesResponse);
}

@Override
Expand All @@ -48,14 +58,19 @@ protected void assertInstances(
org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsResponse serverTestInstance,
GetServiceAccountCredentialsResponse clientInstance) {
assertThat(serverTestInstance.getPrincipal(), equalTo(clientInstance.getPrincipal()));
assertThat(serverTestInstance.getNodeName(), equalTo(clientInstance.getNodeName()));

assertThat(
serverTestInstance.getTokenInfos().stream()
Stream.concat(serverTestInstance.getIndexTokenInfos().stream(),
serverTestInstance.getNodesResponse().getFileTokenInfos().stream())
.map(tokenInfo -> new Tuple<>(tokenInfo.getName(), tokenInfo.getSource().name().toLowerCase(Locale.ROOT)))
.collect(Collectors.toSet()),
equalTo(clientInstance.getServiceTokenInfos().stream()
equalTo(Stream.concat(clientInstance.getIndexTokenInfos().stream(),
clientInstance.getNodesResponse().getFileTokenInfos().stream())
.map(info -> new Tuple<>(info.getName(), info.getSource()))
.collect(Collectors.toSet())));

assertThat(
serverTestInstance.getNodesResponse().failures().size(),
equalTo(clientInstance.getNodesResponse().getHeader().getFailures().size()));
}
}
Loading