Move XContent generation to HasPrivilegesResponse (#35616)

The RestHasPrivilegesAction previously handled its own XContent
generation. This change moves that into HasPrivilegesResponse and
makes the response implement ToXContent.

This allows HasPrivilegesResponseTests to be used to test
compatibility between HLRC and X-Pack internals.

A serialization bug (cluster privs) was also fixed here.
This commit is contained in:
Tim Vernum 2018-11-21 14:33:10 +11:00 committed by GitHub
parent ff03443ab9
commit 30c5422561
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 200 additions and 118 deletions

View File

@ -9,11 +9,14 @@ import org.elasticsearch.Version;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -22,24 +25,35 @@ import java.util.Objects;
/**
* Response for a {@link HasPrivilegesRequest}
*/
public class HasPrivilegesResponse extends ActionResponse {
public class HasPrivilegesResponse extends ActionResponse implements ToXContentObject {
private String username;
private boolean completeMatch;
private Map<String, Boolean> cluster;
private List<ResourcePrivileges> index;
private Map<String, List<ResourcePrivileges>> application;
public HasPrivilegesResponse() {
this(true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap());
this("", true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap());
}
public HasPrivilegesResponse(boolean completeMatch, Map<String, Boolean> cluster, Collection<ResourcePrivileges> index,
public HasPrivilegesResponse(String username, boolean completeMatch, Map<String, Boolean> cluster, Collection<ResourcePrivileges> index,
Map<String, Collection<ResourcePrivileges>> application) {
super();
this.username = username;
this.completeMatch = completeMatch;
this.cluster = new HashMap<>(cluster);
this.index = new ArrayList<>(index);
this.index = sorted(new ArrayList<>(index));
this.application = new HashMap<>();
application.forEach((key, val) -> this.application.put(key, Collections.unmodifiableList(new ArrayList<>(val))));
application.forEach((key, val) -> this.application.put(key, Collections.unmodifiableList(sorted(new ArrayList<>(val)))));
}
private static List<ResourcePrivileges> sorted(List<ResourcePrivileges> resources) {
Collections.sort(resources, Comparator.comparing(o -> o.resource));
return resources;
}
public String getUsername() {
return username;
}
public boolean isCompleteMatch() {
@ -62,13 +76,40 @@ public class HasPrivilegesResponse extends ActionResponse {
return Collections.unmodifiableMap(application);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final HasPrivilegesResponse response = (HasPrivilegesResponse) o;
return completeMatch == response.completeMatch
&& Objects.equals(username, response.username)
&& Objects.equals(cluster, response.cluster)
&& Objects.equals(index, response.index)
&& Objects.equals(application, response.application);
}
@Override
public int hashCode() {
return Objects.hash(username, completeMatch, cluster, index, application);
}
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
completeMatch = in.readBoolean();
if (in.getVersion().onOrAfter(Version.V_7_0_0)) {
cluster = in.readMap(StreamInput::readString, StreamInput::readBoolean);
}
index = readResourcePrivileges(in);
if (in.getVersion().onOrAfter(Version.V_6_4_0)) {
application = in.readMap(StreamInput::readString, HasPrivilegesResponse::readResourcePrivileges);
}
if (in.getVersion().onOrAfter(Version.V_7_0_0)) {
username = in.readString();
}
}
private static List<ResourcePrivileges> readResourcePrivileges(StreamInput in) throws IOException {
@ -86,10 +127,16 @@ public class HasPrivilegesResponse extends ActionResponse {
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeBoolean(completeMatch);
if (out.getVersion().onOrAfter(Version.V_7_0_0)) {
out.writeMap(cluster, StreamOutput::writeString, StreamOutput::writeBoolean);
}
writeResourcePrivileges(out, index);
if (out.getVersion().onOrAfter(Version.V_6_4_0)) {
out.writeMap(application, StreamOutput::writeString, HasPrivilegesResponse::writeResourcePrivileges);
}
if (out.getVersion().onOrAfter(Version.V_7_0_0)) {
out.writeString(username);
}
}
private static void writeResourcePrivileges(StreamOutput out, List<ResourcePrivileges> privileges) throws IOException {
@ -100,6 +147,49 @@ public class HasPrivilegesResponse extends ActionResponse {
}
}
@Override
public String toString() {
return getClass().getSimpleName() + "{"
+ "username=" + username + ","
+ "completeMatch=" + completeMatch + ","
+ "cluster=" + cluster + ","
+ "index=" + index + ","
+ "application=" + application
+ "}";
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject()
.field("username", username)
.field("has_all_requested", completeMatch);
builder.field("cluster");
builder.map(cluster);
appendResources(builder, "index", index);
builder.startObject("application");
for (String app : application.keySet()) {
appendResources(builder, app, application.get(app));
}
builder.endObject();
builder.endObject();
return builder;
}
private void appendResources(XContentBuilder builder, String field, List<HasPrivilegesResponse.ResourcePrivileges> privileges)
throws IOException {
builder.startObject(field);
for (HasPrivilegesResponse.ResourcePrivileges privilege : privileges) {
builder.field(privilege.getResource());
builder.map(privilege.getPrivileges());
}
builder.endObject();
}
public static class ResourcePrivileges {
private final String resource;
private final Map<String, Boolean> privileges;

View File

@ -7,30 +7,42 @@
package org.elasticsearch.xpack.core.security.action.user;
import org.elasticsearch.Version;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.protocol.AbstractHlrcStreamableXContentTestCase;
import org.elasticsearch.test.VersionUtils;
import org.hamcrest.Matchers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.equalTo;
public class HasPrivilegesResponseTests extends ESTestCase {
public class HasPrivilegesResponseTests
extends AbstractHlrcStreamableXContentTestCase<HasPrivilegesResponse, org.elasticsearch.client.security.HasPrivilegesResponse> {
public void testSerializationV64OrLater() throws IOException {
public void testSerializationV64OrV65() throws IOException {
final HasPrivilegesResponse original = randomResponse();
final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_4_0, Version.CURRENT);
final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_4_0, Version.V_6_5_1);
final HasPrivilegesResponse copy = serializeAndDeserialize(original, version);
assertThat(copy.isCompleteMatch(), equalTo(original.isCompleteMatch()));
// assertThat(copy.getClusterPrivileges(), equalTo(original.getClusterPrivileges()));
assertThat(copy.getClusterPrivileges().entrySet(), Matchers.emptyIterable());
assertThat(copy.getIndexPrivileges(), equalTo(original.getIndexPrivileges()));
assertThat(copy.getApplicationPrivileges(), equalTo(original.getApplicationPrivileges()));
}
@ -40,11 +52,79 @@ public class HasPrivilegesResponseTests extends ESTestCase {
final HasPrivilegesResponse copy = serializeAndDeserialize(original, Version.V_6_3_0);
assertThat(copy.isCompleteMatch(), equalTo(original.isCompleteMatch()));
// assertThat(copy.getClusterPrivileges(), equalTo(original.getClusterPrivileges()));
assertThat(copy.getClusterPrivileges().entrySet(), Matchers.emptyIterable());
assertThat(copy.getIndexPrivileges(), equalTo(original.getIndexPrivileges()));
assertThat(copy.getApplicationPrivileges(), equalTo(Collections.emptyMap()));
}
public void testToXContent() throws Exception {
final HasPrivilegesResponse response = new HasPrivilegesResponse("daredevil", false,
Collections.singletonMap("manage", true),
Arrays.asList(
new HasPrivilegesResponse.ResourcePrivileges("staff",
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
.put("read", true).put("index", true).put("delete", false).put("manage", false).map()),
new HasPrivilegesResponse.ResourcePrivileges("customers",
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
.put("read", true).put("index", true).put("delete", true).put("manage", false).map())
), Collections.emptyMap());
final XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
BytesReference bytes = BytesReference.bytes(builder);
final String json = bytes.utf8ToString();
assertThat(json, equalTo("{" +
"\"username\":\"daredevil\"," +
"\"has_all_requested\":false," +
"\"cluster\":{\"manage\":true}," +
"\"index\":{" +
"\"customers\":{\"read\":true,\"index\":true,\"delete\":true,\"manage\":false}," +
"\"staff\":{\"read\":true,\"index\":true,\"delete\":false,\"manage\":false}" +
"}," +
"\"application\":{}" +
"}"));
}
@Override
protected boolean supportsUnknownFields() {
// Because we have nested objects with { string : boolean }, unknown fields cause parsing problems
return false;
}
@Override
protected HasPrivilegesResponse createBlankInstance() {
return new HasPrivilegesResponse();
}
@Override
protected HasPrivilegesResponse createTestInstance() {
return randomResponse();
}
@Override
public org.elasticsearch.client.security.HasPrivilegesResponse doHlrcParseInstance(XContentParser parser) throws IOException {
return org.elasticsearch.client.security.HasPrivilegesResponse.fromXContent(parser);
}
@Override
public HasPrivilegesResponse convertHlrcToInternal(org.elasticsearch.client.security.HasPrivilegesResponse hlrc) {
return new HasPrivilegesResponse(
hlrc.getUsername(),
hlrc.hasAllRequested(),
hlrc.getClusterPrivileges(),
toResourcePrivileges(hlrc.getIndexPrivileges()),
hlrc.getApplicationPrivileges().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> toResourcePrivileges(e.getValue())))
);
}
private static List<HasPrivilegesResponse.ResourcePrivileges> toResourcePrivileges(Map<String, Map<String, Boolean>> map) {
return map.entrySet().stream()
.map(e -> new HasPrivilegesResponse.ResourcePrivileges(e.getKey(), e.getValue()))
.collect(Collectors.toList());
}
private HasPrivilegesResponse serializeAndDeserialize(HasPrivilegesResponse original, Version version) throws IOException {
logger.info("Test serialize/deserialize with version {}", version);
final BytesStreamOutput out = new BytesStreamOutput();
@ -60,6 +140,7 @@ public class HasPrivilegesResponseTests extends ESTestCase {
}
private HasPrivilegesResponse randomResponse() {
final String username = randomAlphaOfLengthBetween(4, 12);
final Map<String, Boolean> cluster = new HashMap<>();
for (String priv : randomArray(1, 6, String[]::new, () -> randomAlphaOfLengthBetween(3, 12))) {
cluster.put(priv, randomBoolean());
@ -69,7 +150,7 @@ public class HasPrivilegesResponseTests extends ESTestCase {
for (String app : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 6).toLowerCase(Locale.ROOT))) {
application.put(app, randomResourcePrivileges());
}
return new HasPrivilegesResponse(randomBoolean(), cluster, index, application);
return new HasPrivilegesResponse(username, randomBoolean(), cluster, index, application);
}
private Collection<HasPrivilegesResponse.ResourcePrivileges> randomResourcePrivileges() {
@ -83,5 +164,4 @@ public class HasPrivilegesResponseTests extends ESTestCase {
}
return list;
}
}

View File

@ -168,7 +168,7 @@ public class TransportHasPrivilegesAction extends HandledTransportAction<HasPriv
privilegesByApplication.put(applicationName, appPrivilegesByResource.values());
}
listener.onResponse(new HasPrivilegesResponse(allMatch, cluster, indices.values(), privilegesByApplication));
listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, indices.values(), privilegesByApplication));
}
private boolean testIndexMatch(String checkIndex, String checkPrivilegeName, Role userRole,

View File

@ -10,11 +10,11 @@ import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
@ -29,8 +29,6 @@ import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static org.elasticsearch.rest.RestRequest.Method.GET;
import static org.elasticsearch.rest.RestRequest.Method.POST;
@ -66,7 +64,13 @@ public class RestHasPrivilegesAction extends SecurityBaseRestHandler {
}
final Tuple<XContentType, BytesReference> content = request.contentOrSourceParam();
HasPrivilegesRequestBuilder requestBuilder = new SecurityClient(client).prepareHasPrivileges(username, content.v2(), content.v1());
return channel -> requestBuilder.execute(new HasPrivilegesRestResponseBuilder(username, channel));
return channel -> requestBuilder.execute(new RestBuilderListener<HasPrivilegesResponse>(channel) {
@Override
public RestResponse buildResponse(HasPrivilegesResponse response, XContentBuilder builder) throws Exception {
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
return new BytesRestResponse(RestStatus.OK, builder);
}
});
}
private String getUsername(RestRequest request) {
@ -80,46 +84,4 @@ public class RestHasPrivilegesAction extends SecurityBaseRestHandler {
}
return user.principal();
}
static class HasPrivilegesRestResponseBuilder extends RestBuilderListener<HasPrivilegesResponse> {
private String username;
HasPrivilegesRestResponseBuilder(String username, RestChannel channel) {
super(channel);
this.username = username;
}
@Override
public RestResponse buildResponse(HasPrivilegesResponse response, XContentBuilder builder) throws Exception {
builder.startObject()
.field("username", username)
.field("has_all_requested", response.isCompleteMatch());
builder.field("cluster");
builder.map(response.getClusterPrivileges());
appendResources(builder, "index", response.getIndexPrivileges());
builder.startObject("application");
final Map<String, List<HasPrivilegesResponse.ResourcePrivileges>> appPrivileges = response.getApplicationPrivileges();
for (String app : appPrivileges.keySet()) {
appendResources(builder, app, appPrivileges.get(app));
}
builder.endObject();
builder.endObject();
return new BytesRestResponse(RestStatus.OK, builder);
}
private void appendResources(XContentBuilder builder, String field, List<HasPrivilegesResponse.ResourcePrivileges> privileges)
throws IOException {
builder.startObject(field);
for (HasPrivilegesResponse.ResourcePrivileges privilege : privileges) {
builder.field(privilege.getResource());
builder.map(privilege.getPrivileges());
}
builder.endObject();
}
}
}

View File

@ -111,7 +111,10 @@ public class TransportHasPrivilegesActionTests extends ESTestCase {
* (in this case that {@link DeleteAction} and {@link IndexAction} are satisfied by {@link IndexPrivilege#WRITE}).
*/
public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception {
role = Role.builder("test1").cluster(ClusterPrivilege.ALL).add(IndexPrivilege.WRITE, "academy").build();
role = Role.builder("test1")
.cluster(Collections.singleton("all"), Collections.emptyList())
.add(IndexPrivilege.WRITE, "academy")
.build();
final HasPrivilegesRequest request = new HasPrivilegesRequest();
request.username(user.principal());
@ -126,6 +129,7 @@ public class TransportHasPrivilegesActionTests extends ESTestCase {
final HasPrivilegesResponse response = future.get();
assertThat(response, notNullValue());
assertThat(response.getUsername(), is(user.principal()));
assertThat(response.isCompleteMatch(), is(true));
assertThat(response.getClusterPrivileges().size(), equalTo(1));
@ -163,6 +167,7 @@ public class TransportHasPrivilegesActionTests extends ESTestCase {
final HasPrivilegesResponse response = future.get();
assertThat(response, notNullValue());
assertThat(response.getUsername(), is(user.principal()));
assertThat(response.isCompleteMatch(), is(false));
assertThat(response.getClusterPrivileges().size(), equalTo(2));
assertThat(response.getClusterPrivileges().get("monitor"), equalTo(true));
@ -205,6 +210,7 @@ public class TransportHasPrivilegesActionTests extends ESTestCase {
.indices("academy")
.privileges("read", "write")
.build(), Strings.EMPTY_ARRAY);
assertThat(response.getUsername(), is(user.principal()));
assertThat(response.isCompleteMatch(), is(false));
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1));
final ResourcePrivileges result = response.getIndexPrivileges().get(0);
@ -289,6 +295,7 @@ public class TransportHasPrivilegesActionTests extends ESTestCase {
final HasPrivilegesResponse response = future.get();
assertThat(response, notNullValue());
assertThat(response.getUsername(), is(user.principal()));
assertThat(response.isCompleteMatch(), is(false));
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(8));
assertThat(response.getIndexPrivileges(), containsInAnyOrder(

View File

@ -1,57 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.rest.action.user;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
import org.elasticsearch.xpack.security.rest.action.user.RestHasPrivilegesAction.HasPrivilegesRestResponseBuilder;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.mockito.Mockito.mock;
public class HasPrivilegesRestResponseTests extends ESTestCase {
public void testBuildValidJsonResponse() throws Exception {
final HasPrivilegesRestResponseBuilder response = new HasPrivilegesRestResponseBuilder("daredevil", mock(RestChannel.class));
final HasPrivilegesResponse actionResponse = new HasPrivilegesResponse(false,
Collections.singletonMap("manage", true),
Arrays.asList(
new HasPrivilegesResponse.ResourcePrivileges("staff",
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
.put("read", true).put("index", true).put("delete", false).put("manage", false).map()),
new HasPrivilegesResponse.ResourcePrivileges("customers",
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
.put("read", true).put("index", true).put("delete", true).put("manage", false).map())
), Collections.emptyMap());
final XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
final RestResponse rest = response.buildResponse(actionResponse, builder);
assertThat(rest, instanceOf(BytesRestResponse.class));
final String json = rest.content().utf8ToString();
assertThat(json, equalTo("{" +
"\"username\":\"daredevil\"," +
"\"has_all_requested\":false," +
"\"cluster\":{\"manage\":true}," +
"\"index\":{" +
"\"staff\":{\"read\":true,\"index\":true,\"delete\":false,\"manage\":false}," +
"\"customers\":{\"read\":true,\"index\":true,\"delete\":true,\"manage\":false}" +
"}," +
"\"application\":{}" +
"}"));
}
}