Skip to content

Commit d81347d

Browse files
authored
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.
1 parent 03ee53c commit d81347d

File tree

6 files changed

+279
-110
lines changed

6 files changed

+279
-110
lines changed

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

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
import org.elasticsearch.action.ActionResponse;
1010
import org.elasticsearch.common.io.stream.StreamInput;
1111
import org.elasticsearch.common.io.stream.StreamOutput;
12+
import org.elasticsearch.common.xcontent.ToXContentObject;
13+
import org.elasticsearch.common.xcontent.XContentBuilder;
1214

1315
import java.io.IOException;
1416
import java.util.ArrayList;
1517
import java.util.Collection;
1618
import java.util.Collections;
19+
import java.util.Comparator;
1720
import java.util.HashMap;
1821
import java.util.List;
1922
import java.util.Map;
@@ -22,24 +25,35 @@
2225
/**
2326
* Response for a {@link HasPrivilegesRequest}
2427
*/
25-
public class HasPrivilegesResponse extends ActionResponse {
28+
public class HasPrivilegesResponse extends ActionResponse implements ToXContentObject {
29+
private String username;
2630
private boolean completeMatch;
2731
private Map<String, Boolean> cluster;
2832
private List<ResourcePrivileges> index;
2933
private Map<String, List<ResourcePrivileges>> application;
3034

3135
public HasPrivilegesResponse() {
32-
this(true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap());
36+
this("", true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap());
3337
}
3438

35-
public HasPrivilegesResponse(boolean completeMatch, Map<String, Boolean> cluster, Collection<ResourcePrivileges> index,
39+
public HasPrivilegesResponse(String username, boolean completeMatch, Map<String, Boolean> cluster, Collection<ResourcePrivileges> index,
3640
Map<String, Collection<ResourcePrivileges>> application) {
3741
super();
42+
this.username = username;
3843
this.completeMatch = completeMatch;
3944
this.cluster = new HashMap<>(cluster);
40-
this.index = new ArrayList<>(index);
45+
this.index = sorted(new ArrayList<>(index));
4146
this.application = new HashMap<>();
42-
application.forEach((key, val) -> this.application.put(key, Collections.unmodifiableList(new ArrayList<>(val))));
47+
application.forEach((key, val) -> this.application.put(key, Collections.unmodifiableList(sorted(new ArrayList<>(val)))));
48+
}
49+
50+
private static List<ResourcePrivileges> sorted(List<ResourcePrivileges> resources) {
51+
Collections.sort(resources, Comparator.comparing(o -> o.resource));
52+
return resources;
53+
}
54+
55+
public String getUsername() {
56+
return username;
4357
}
4458

4559
public boolean isCompleteMatch() {
@@ -62,13 +76,40 @@ public Map<String, List<ResourcePrivileges>> getApplicationPrivileges() {
6276
return Collections.unmodifiableMap(application);
6377
}
6478

79+
@Override
80+
public boolean equals(Object o) {
81+
if (this == o) {
82+
return true;
83+
}
84+
if (o == null || getClass() != o.getClass()) {
85+
return false;
86+
}
87+
final HasPrivilegesResponse response = (HasPrivilegesResponse) o;
88+
return completeMatch == response.completeMatch
89+
&& Objects.equals(username, response.username)
90+
&& Objects.equals(cluster, response.cluster)
91+
&& Objects.equals(index, response.index)
92+
&& Objects.equals(application, response.application);
93+
}
94+
95+
@Override
96+
public int hashCode() {
97+
return Objects.hash(username, completeMatch, cluster, index, application);
98+
}
99+
65100
public void readFrom(StreamInput in) throws IOException {
66101
super.readFrom(in);
67102
completeMatch = in.readBoolean();
103+
if (in.getVersion().onOrAfter(Version.V_6_6_0)) {
104+
cluster = in.readMap(StreamInput::readString, StreamInput::readBoolean);
105+
}
68106
index = readResourcePrivileges(in);
69107
if (in.getVersion().onOrAfter(Version.V_6_4_0)) {
70108
application = in.readMap(StreamInput::readString, HasPrivilegesResponse::readResourcePrivileges);
71109
}
110+
if (in.getVersion().onOrAfter(Version.V_6_6_0)) {
111+
username = in.readString();
112+
}
72113
}
73114

74115
private static List<ResourcePrivileges> readResourcePrivileges(StreamInput in) throws IOException {
@@ -86,10 +127,16 @@ private static List<ResourcePrivileges> readResourcePrivileges(StreamInput in) t
86127
public void writeTo(StreamOutput out) throws IOException {
87128
super.writeTo(out);
88129
out.writeBoolean(completeMatch);
130+
if (out.getVersion().onOrAfter(Version.V_6_6_0)) {
131+
out.writeMap(cluster, StreamOutput::writeString, StreamOutput::writeBoolean);
132+
}
89133
writeResourcePrivileges(out, index);
90134
if (out.getVersion().onOrAfter(Version.V_6_4_0)) {
91135
out.writeMap(application, StreamOutput::writeString, HasPrivilegesResponse::writeResourcePrivileges);
92136
}
137+
if (out.getVersion().onOrAfter(Version.V_6_6_0)) {
138+
out.writeString(username);
139+
}
93140
}
94141

95142
private static void writeResourcePrivileges(StreamOutput out, List<ResourcePrivileges> privileges) throws IOException {
@@ -100,6 +147,49 @@ private static void writeResourcePrivileges(StreamOutput out, List<ResourcePrivi
100147
}
101148
}
102149

150+
@Override
151+
public String toString() {
152+
return getClass().getSimpleName() + "{"
153+
+ "username=" + username + ","
154+
+ "completeMatch=" + completeMatch + ","
155+
+ "cluster=" + cluster + ","
156+
+ "index=" + index + ","
157+
+ "application=" + application
158+
+ "}";
159+
}
160+
161+
@Override
162+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
163+
builder.startObject()
164+
.field("username", username)
165+
.field("has_all_requested", completeMatch);
166+
167+
builder.field("cluster");
168+
builder.map(cluster);
169+
170+
appendResources(builder, "index", index);
171+
172+
builder.startObject("application");
173+
for (String app : application.keySet()) {
174+
appendResources(builder, app, application.get(app));
175+
}
176+
builder.endObject();
177+
178+
builder.endObject();
179+
return builder;
180+
}
181+
182+
private void appendResources(XContentBuilder builder, String field, List<HasPrivilegesResponse.ResourcePrivileges> privileges)
183+
throws IOException {
184+
builder.startObject(field);
185+
for (HasPrivilegesResponse.ResourcePrivileges privilege : privileges) {
186+
builder.field(privilege.getResource());
187+
builder.map(privilege.getPrivileges());
188+
}
189+
builder.endObject();
190+
}
191+
192+
103193
public static class ResourcePrivileges {
104194
private final String resource;
105195
private final Map<String, Boolean> privileges;
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action.user;
8+
9+
import org.elasticsearch.Version;
10+
import org.elasticsearch.common.bytes.BytesReference;
11+
import org.elasticsearch.common.collect.MapBuilder;
12+
import org.elasticsearch.common.io.stream.BytesStreamOutput;
13+
import org.elasticsearch.common.io.stream.StreamInput;
14+
import org.elasticsearch.common.xcontent.ToXContent;
15+
import org.elasticsearch.common.xcontent.XContentBuilder;
16+
import org.elasticsearch.common.xcontent.XContentParser;
17+
import org.elasticsearch.common.xcontent.XContentType;
18+
import org.elasticsearch.protocol.AbstractHlrcStreamableXContentTestCase;
19+
import org.elasticsearch.test.VersionUtils;
20+
import org.hamcrest.Matchers;
21+
22+
import java.io.IOException;
23+
import java.util.ArrayList;
24+
import java.util.Arrays;
25+
import java.util.Collection;
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.LinkedHashMap;
29+
import java.util.List;
30+
import java.util.Locale;
31+
import java.util.Map;
32+
import java.util.stream.Collectors;
33+
34+
import static org.hamcrest.Matchers.equalTo;
35+
36+
public class HasPrivilegesResponseTests
37+
extends AbstractHlrcStreamableXContentTestCase<HasPrivilegesResponse, org.elasticsearch.client.security.HasPrivilegesResponse> {
38+
39+
public void testSerializationV64OrV65() throws IOException {
40+
final HasPrivilegesResponse original = randomResponse();
41+
final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_4_0, Version.V_6_5_1);
42+
final HasPrivilegesResponse copy = serializeAndDeserialize(original, version);
43+
44+
assertThat(copy.isCompleteMatch(), equalTo(original.isCompleteMatch()));
45+
assertThat(copy.getClusterPrivileges().entrySet(), Matchers.emptyIterable());
46+
assertThat(copy.getIndexPrivileges(), equalTo(original.getIndexPrivileges()));
47+
assertThat(copy.getApplicationPrivileges(), equalTo(original.getApplicationPrivileges()));
48+
}
49+
50+
public void testSerializationV63() throws IOException {
51+
final HasPrivilegesResponse original = randomResponse();
52+
final HasPrivilegesResponse copy = serializeAndDeserialize(original, Version.V_6_3_0);
53+
54+
assertThat(copy.isCompleteMatch(), equalTo(original.isCompleteMatch()));
55+
assertThat(copy.getClusterPrivileges().entrySet(), Matchers.emptyIterable());
56+
assertThat(copy.getIndexPrivileges(), equalTo(original.getIndexPrivileges()));
57+
assertThat(copy.getApplicationPrivileges(), equalTo(Collections.emptyMap()));
58+
}
59+
60+
public void testToXContent() throws Exception {
61+
final HasPrivilegesResponse response = new HasPrivilegesResponse("daredevil", false,
62+
Collections.singletonMap("manage", true),
63+
Arrays.asList(
64+
new HasPrivilegesResponse.ResourcePrivileges("staff",
65+
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
66+
.put("read", true).put("index", true).put("delete", false).put("manage", false).map()),
67+
new HasPrivilegesResponse.ResourcePrivileges("customers",
68+
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
69+
.put("read", true).put("index", true).put("delete", true).put("manage", false).map())
70+
), Collections.emptyMap());
71+
72+
final XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
73+
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
74+
BytesReference bytes = BytesReference.bytes(builder);
75+
76+
final String json = bytes.utf8ToString();
77+
assertThat(json, equalTo("{" +
78+
"\"username\":\"daredevil\"," +
79+
"\"has_all_requested\":false," +
80+
"\"cluster\":{\"manage\":true}," +
81+
"\"index\":{" +
82+
"\"customers\":{\"read\":true,\"index\":true,\"delete\":true,\"manage\":false}," +
83+
"\"staff\":{\"read\":true,\"index\":true,\"delete\":false,\"manage\":false}" +
84+
"}," +
85+
"\"application\":{}" +
86+
"}"));
87+
}
88+
89+
@Override
90+
protected boolean supportsUnknownFields() {
91+
// Because we have nested objects with { string : boolean }, unknown fields cause parsing problems
92+
return false;
93+
}
94+
95+
@Override
96+
protected HasPrivilegesResponse createBlankInstance() {
97+
return new HasPrivilegesResponse();
98+
}
99+
100+
@Override
101+
protected HasPrivilegesResponse createTestInstance() {
102+
return randomResponse();
103+
}
104+
105+
@Override
106+
public org.elasticsearch.client.security.HasPrivilegesResponse doHlrcParseInstance(XContentParser parser) throws IOException {
107+
return org.elasticsearch.client.security.HasPrivilegesResponse.fromXContent(parser);
108+
}
109+
110+
@Override
111+
public HasPrivilegesResponse convertHlrcToInternal(org.elasticsearch.client.security.HasPrivilegesResponse hlrc) {
112+
return new HasPrivilegesResponse(
113+
hlrc.getUsername(),
114+
hlrc.hasAllRequested(),
115+
hlrc.getClusterPrivileges(),
116+
toResourcePrivileges(hlrc.getIndexPrivileges()),
117+
hlrc.getApplicationPrivileges().entrySet().stream()
118+
.collect(Collectors.toMap(Map.Entry::getKey, e -> toResourcePrivileges(e.getValue())))
119+
);
120+
}
121+
122+
private static List<HasPrivilegesResponse.ResourcePrivileges> toResourcePrivileges(Map<String, Map<String, Boolean>> map) {
123+
return map.entrySet().stream()
124+
.map(e -> new HasPrivilegesResponse.ResourcePrivileges(e.getKey(), e.getValue()))
125+
.collect(Collectors.toList());
126+
}
127+
128+
private HasPrivilegesResponse serializeAndDeserialize(HasPrivilegesResponse original, Version version) throws IOException {
129+
logger.info("Test serialize/deserialize with version {}", version);
130+
final BytesStreamOutput out = new BytesStreamOutput();
131+
out.setVersion(version);
132+
original.writeTo(out);
133+
134+
final HasPrivilegesResponse copy = new HasPrivilegesResponse();
135+
final StreamInput in = out.bytes().streamInput();
136+
in.setVersion(version);
137+
copy.readFrom(in);
138+
assertThat(in.read(), equalTo(-1));
139+
return copy;
140+
}
141+
142+
private HasPrivilegesResponse randomResponse() {
143+
final String username = randomAlphaOfLengthBetween(4, 12);
144+
final Map<String, Boolean> cluster = new HashMap<>();
145+
for (String priv : randomArray(1, 6, String[]::new, () -> randomAlphaOfLengthBetween(3, 12))) {
146+
cluster.put(priv, randomBoolean());
147+
}
148+
final Collection<HasPrivilegesResponse.ResourcePrivileges> index = randomResourcePrivileges();
149+
final Map<String, Collection<HasPrivilegesResponse.ResourcePrivileges>> application = new HashMap<>();
150+
for (String app : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 6).toLowerCase(Locale.ROOT))) {
151+
application.put(app, randomResourcePrivileges());
152+
}
153+
return new HasPrivilegesResponse(username, randomBoolean(), cluster, index, application);
154+
}
155+
156+
private Collection<HasPrivilegesResponse.ResourcePrivileges> randomResourcePrivileges() {
157+
final Collection<HasPrivilegesResponse.ResourcePrivileges> list = new ArrayList<>();
158+
for (String resource : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(2, 6))) {
159+
final Map<String, Boolean> privileges = new HashMap<>();
160+
for (String priv : randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(3, 8))) {
161+
privileges.put(priv, randomBoolean());
162+
}
163+
list.add(new HasPrivilegesResponse.ResourcePrivileges(resource, privileges));
164+
}
165+
return list;
166+
}
167+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private void checkPrivileges(HasPrivilegesRequest request, Role userRole,
168168
privilegesByApplication.put(applicationName, appPrivilegesByResource.values());
169169
}
170170

171-
listener.onResponse(new HasPrivilegesResponse(allMatch, cluster, indices.values(), privilegesByApplication));
171+
listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, indices.values(), privilegesByApplication));
172172
}
173173

174174
private boolean testIndexMatch(String checkIndex, String checkPrivilegeName, Role userRole,

0 commit comments

Comments
 (0)