diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java index 8cd8b510c64..0942ef053a8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java @@ -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 cluster; private List index; private Map> application; public HasPrivilegesResponse() { - this(true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap()); + this("", true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap()); } - public HasPrivilegesResponse(boolean completeMatch, Map cluster, Collection index, + public HasPrivilegesResponse(String username, boolean completeMatch, Map cluster, Collection index, Map> 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 sorted(List 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 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 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 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 privileges; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java index 89c58945bad..f9289113dc8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java @@ -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 { - 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.newMapBuilder(new LinkedHashMap<>()) + .put("read", true).put("index", true).put("delete", false).put("manage", false).map()), + new HasPrivilegesResponse.ResourcePrivileges("customers", + MapBuilder.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 toResourcePrivileges(Map> 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 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 randomResourcePrivileges() { @@ -83,5 +164,4 @@ public class HasPrivilegesResponseTests extends ESTestCase { } return list; } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java index 37cd3478aa5..20fa9f522e7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java @@ -168,7 +168,7 @@ public class TransportHasPrivilegesAction extends HandledTransportAction 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(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 { - 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> 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 privileges) - throws IOException { - builder.startObject(field); - for (HasPrivilegesResponse.ResourcePrivileges privilege : privileges) { - builder.field(privilege.getResource()); - builder.map(privilege.getPrivileges()); - } - builder.endObject(); - } - - } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java index 804412339e8..2da066b56ee 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java @@ -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( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/HasPrivilegesRestResponseTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/HasPrivilegesRestResponseTests.java deleted file mode 100644 index 645abbc8f1a..00000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/HasPrivilegesRestResponseTests.java +++ /dev/null @@ -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.newMapBuilder(new LinkedHashMap<>()) - .put("read", true).put("index", true).put("delete", false).put("manage", false).map()), - new HasPrivilegesResponse.ResourcePrivileges("customers", - MapBuilder.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\":{}" + - "}")); - } -} \ No newline at end of file