Skip to content

Commit 6bb2318

Browse files
committed
Add sort and pagination support for QueryApiKey API (elastic#76144)
This PR adds support for sort and pagination similar to those used with regular search API. Similar to the query field, the sort field also supports only a subset of what is available for regular search.
1 parent 211a958 commit 6bb2318

File tree

15 files changed

+834
-164
lines changed

15 files changed

+834
-164
lines changed

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,28 @@ public Map<String, Object> getMetadata() {
108108

109109
@Override
110110
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
111-
builder.startObject()
112-
.field("id", id)
113-
.field("name", name)
114-
.field("creation", creation.toEpochMilli());
111+
builder.startObject();
112+
innerToXContent(builder, params);
113+
return builder.endObject();
114+
}
115+
116+
public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
117+
builder
118+
.field("id", id)
119+
.field("name", name)
120+
.field("creation", creation.toEpochMilli());
115121
if (expiration != null) {
116122
builder.field("expiration", expiration.toEpochMilli());
117123
}
118-
builder.field("invalidated", invalidated)
119-
.field("username", username)
120-
.field("realm", realm)
121-
.field("metadata", (metadata == null ? org.elasticsearch.core.Map.of() : metadata));
122-
return builder.endObject();
124+
builder
125+
.field("invalidated", invalidated)
126+
.field("username", username)
127+
.field("realm", realm)
128+
.field("metadata", (metadata == null ? org.elasticsearch.core.Map.of() : metadata));
129+
return builder;
123130
}
124131

132+
125133
@Override
126134
public void writeTo(StreamOutput out) throws IOException {
127135
if (out.getVersion().onOrAfter(Version.V_7_5_0)) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequest.java

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,83 @@
1313
import org.elasticsearch.common.io.stream.StreamOutput;
1414
import org.elasticsearch.core.Nullable;
1515
import org.elasticsearch.index.query.QueryBuilder;
16+
import org.elasticsearch.search.searchafter.SearchAfterBuilder;
17+
import org.elasticsearch.search.sort.FieldSortBuilder;
1618

1719
import java.io.IOException;
20+
import java.util.List;
21+
22+
import static org.elasticsearch.action.ValidateActions.addValidationError;
1823

1924
public final class QueryApiKeyRequest extends ActionRequest {
2025

2126
@Nullable
2227
private final QueryBuilder queryBuilder;
28+
@Nullable
29+
private final Integer from;
30+
@Nullable
31+
private final Integer size;
32+
@Nullable
33+
private final List<FieldSortBuilder> fieldSortBuilders;
34+
@Nullable
35+
private final SearchAfterBuilder searchAfterBuilder;
2336
private boolean filterForCurrentUser;
2437

2538
public QueryApiKeyRequest() {
2639
this((QueryBuilder) null);
2740
}
2841

2942
public QueryApiKeyRequest(QueryBuilder queryBuilder) {
43+
this(queryBuilder, null, null, null, null);
44+
}
45+
46+
public QueryApiKeyRequest(
47+
@Nullable QueryBuilder queryBuilder,
48+
@Nullable Integer from,
49+
@Nullable Integer size,
50+
@Nullable List<FieldSortBuilder> fieldSortBuilders,
51+
@Nullable SearchAfterBuilder searchAfterBuilder
52+
) {
3053
this.queryBuilder = queryBuilder;
54+
this.from = from;
55+
this.size = size;
56+
this.fieldSortBuilders = fieldSortBuilders;
57+
this.searchAfterBuilder = searchAfterBuilder;
3158
}
3259

3360
public QueryApiKeyRequest(StreamInput in) throws IOException {
3461
super(in);
35-
queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class);
62+
this.queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class);
63+
this.from = in.readOptionalVInt();
64+
this.size = in.readOptionalVInt();
65+
if (in.readBoolean()) {
66+
this.fieldSortBuilders = in.readList(FieldSortBuilder::new);
67+
} else {
68+
this.fieldSortBuilders = null;
69+
}
70+
this.searchAfterBuilder = in.readOptionalWriteable(SearchAfterBuilder::new);
3671
}
3772

3873
public QueryBuilder getQueryBuilder() {
3974
return queryBuilder;
4075
}
4176

77+
public Integer getFrom() {
78+
return from;
79+
}
80+
81+
public Integer getSize() {
82+
return size;
83+
}
84+
85+
public List<FieldSortBuilder> getFieldSortBuilders() {
86+
return fieldSortBuilders;
87+
}
88+
89+
public SearchAfterBuilder getSearchAfterBuilder() {
90+
return searchAfterBuilder;
91+
}
92+
4293
public boolean isFilterForCurrentUser() {
4394
return filterForCurrentUser;
4495
}
@@ -49,12 +100,28 @@ public void setFilterForCurrentUser() {
49100

50101
@Override
51102
public ActionRequestValidationException validate() {
52-
return null;
103+
ActionRequestValidationException validationException = null;
104+
if (from != null && from < 0) {
105+
validationException = addValidationError("[from] parameter cannot be negative but was [" + from + "]", validationException);
106+
}
107+
if (size != null && size < 0) {
108+
validationException = addValidationError("[size] parameter cannot be negative but was [" + size + "]", validationException);
109+
}
110+
return validationException;
53111
}
54112

55113
@Override
56114
public void writeTo(StreamOutput out) throws IOException {
57115
super.writeTo(out);
58116
out.writeOptionalNamedWriteable(queryBuilder);
117+
out.writeOptionalVInt(from);
118+
out.writeOptionalVInt(size);
119+
if (fieldSortBuilders == null) {
120+
out.writeBoolean(false);
121+
} else {
122+
out.writeBoolean(true);
123+
out.writeList(fieldSortBuilders);
124+
}
125+
out.writeOptionalWriteable(searchAfterBuilder);
59126
}
60127
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponse.java

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import org.elasticsearch.common.io.stream.StreamInput;
1212
import org.elasticsearch.common.io.stream.StreamOutput;
1313
import org.elasticsearch.common.io.stream.Writeable;
14+
import org.elasticsearch.common.lucene.Lucene;
1415
import org.elasticsearch.common.xcontent.ToXContentObject;
1516
import org.elasticsearch.common.xcontent.XContentBuilder;
17+
import org.elasticsearch.core.Nullable;
1618
import org.elasticsearch.xpack.core.security.action.ApiKey;
1719

1820
import java.io.IOException;
@@ -27,36 +29,46 @@
2729
*/
2830
public final class QueryApiKeyResponse extends ActionResponse implements ToXContentObject, Writeable {
2931

30-
private final ApiKey[] foundApiKeysInfo;
32+
private final long total;
33+
private final Item[] items;
3134

3235
public QueryApiKeyResponse(StreamInput in) throws IOException {
3336
super(in);
34-
this.foundApiKeysInfo = in.readArray(ApiKey::new, ApiKey[]::new);
37+
this.total = in.readLong();
38+
this.items = in.readArray(Item::new, Item[]::new);
3539
}
3640

37-
public QueryApiKeyResponse(Collection<ApiKey> foundApiKeysInfo) {
38-
Objects.requireNonNull(foundApiKeysInfo, "found_api_keys_info must be provided");
39-
this.foundApiKeysInfo = foundApiKeysInfo.toArray(new ApiKey[0]);
41+
public QueryApiKeyResponse(long total, Collection<Item> items) {
42+
this.total = total;
43+
Objects.requireNonNull(items, "items must be provided");
44+
this.items = items.toArray(new Item[0]);
4045
}
4146

4247
public static QueryApiKeyResponse emptyResponse() {
43-
return new QueryApiKeyResponse(Collections.emptyList());
48+
return new QueryApiKeyResponse(0, Collections.emptyList());
4449
}
4550

46-
public ApiKey[] getApiKeyInfos() {
47-
return foundApiKeysInfo;
51+
public long getTotal() {
52+
return total;
53+
}
54+
55+
public Item[] getItems() {
56+
return items;
4857
}
4958

5059
@Override
5160
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
5261
builder.startObject()
53-
.array("api_keys", (Object[]) foundApiKeysInfo);
62+
.field("total", total)
63+
.field("count", items.length)
64+
.array("api_keys", (Object[]) items);
5465
return builder.endObject();
5566
}
5667

5768
@Override
5869
public void writeTo(StreamOutput out) throws IOException {
59-
out.writeArray(foundApiKeysInfo);
70+
out.writeLong(total);
71+
out.writeArray(items);
6072
}
6173

6274
@Override
@@ -66,17 +78,81 @@ public boolean equals(Object o) {
6678
if (o == null || getClass() != o.getClass())
6779
return false;
6880
QueryApiKeyResponse that = (QueryApiKeyResponse) o;
69-
return Arrays.equals(foundApiKeysInfo, that.foundApiKeysInfo);
81+
return total == that.total && Arrays.equals(items, that.items);
7082
}
7183

7284
@Override
7385
public int hashCode() {
74-
return Arrays.hashCode(foundApiKeysInfo);
86+
int result = Objects.hash(total);
87+
result = 31 * result + Arrays.hashCode(items);
88+
return result;
7589
}
7690

7791
@Override
7892
public String toString() {
79-
return "QueryApiKeyResponse [foundApiKeysInfo=" + foundApiKeysInfo + "]";
93+
return "QueryApiKeyResponse{" + "total=" + total + ", items=" + Arrays.toString(items) + '}';
8094
}
8195

96+
public static class Item implements ToXContentObject, Writeable {
97+
private final ApiKey apiKey;
98+
@Nullable
99+
private final Object[] sortValues;
100+
101+
public Item(ApiKey apiKey, @Nullable Object[] sortValues) {
102+
this.apiKey = apiKey;
103+
this.sortValues = sortValues;
104+
}
105+
106+
public Item(StreamInput in) throws IOException {
107+
this.apiKey = new ApiKey(in);
108+
this.sortValues = in.readOptionalArray(Lucene::readSortValue, Object[]::new);
109+
}
110+
111+
public ApiKey getApiKey() {
112+
return apiKey;
113+
}
114+
115+
public Object[] getSortValues() {
116+
return sortValues;
117+
}
118+
119+
@Override
120+
public void writeTo(StreamOutput out) throws IOException {
121+
apiKey.writeTo(out);
122+
out.writeOptionalArray(Lucene::writeSortValue, sortValues);
123+
}
124+
125+
@Override
126+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
127+
builder.startObject();
128+
apiKey.innerToXContent(builder, params);
129+
if (sortValues != null && sortValues.length > 0) {
130+
builder.array("_sort", sortValues);
131+
}
132+
builder.endObject();
133+
return builder;
134+
}
135+
136+
@Override
137+
public boolean equals(Object o) {
138+
if (this == o)
139+
return true;
140+
if (o == null || getClass() != o.getClass())
141+
return false;
142+
Item item = (Item) o;
143+
return Objects.equals(apiKey, item.apiKey) && Arrays.equals(sortValues, item.sortValues);
144+
}
145+
146+
@Override
147+
public int hashCode() {
148+
int result = Objects.hash(apiKey);
149+
result = 31 * result + Arrays.hashCode(sortValues);
150+
return result;
151+
}
152+
153+
@Override
154+
public String toString() {
155+
return "Item{" + "apiKey=" + apiKey + ", sortValues=" + Arrays.toString(sortValues) + '}';
156+
}
157+
}
82158
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequestTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414
import org.elasticsearch.common.io.stream.StreamInput;
1515
import org.elasticsearch.common.settings.Settings;
1616
import org.elasticsearch.index.query.BoolQueryBuilder;
17+
import org.elasticsearch.index.query.MatchAllQueryBuilder;
1718
import org.elasticsearch.index.query.QueryBuilders;
1819
import org.elasticsearch.search.SearchModule;
20+
import org.elasticsearch.search.searchafter.SearchAfterBuilder;
21+
import org.elasticsearch.search.sort.FieldSortBuilder;
22+
import org.elasticsearch.search.sort.SortOrder;
1923
import org.elasticsearch.test.ESTestCase;
2024

2125
import java.io.ByteArrayInputStream;
2226
import java.io.IOException;
2327

28+
import static org.hamcrest.Matchers.containsString;
2429
import static org.hamcrest.Matchers.equalTo;
2530
import static org.hamcrest.Matchers.is;
2631
import static org.hamcrest.Matchers.nullValue;
@@ -56,5 +61,39 @@ public void testReadWrite() throws IOException {
5661
assertThat((BoolQueryBuilder) deserialized.getQueryBuilder(), equalTo(boolQueryBuilder2));
5762
}
5863
}
64+
65+
final QueryApiKeyRequest request3 = new QueryApiKeyRequest(
66+
QueryBuilders.matchAllQuery(),
67+
42,
68+
20,
69+
List.of(new FieldSortBuilder("name"),
70+
new FieldSortBuilder("creation_time").setFormat("strict_date_time").order(SortOrder.DESC),
71+
new FieldSortBuilder("username")),
72+
new SearchAfterBuilder().setSortValues(new String[] { "key-2048", "2021-07-01T00:00:59.000Z" }));
73+
try (BytesStreamOutput out = new BytesStreamOutput()) {
74+
request3.writeTo(out);
75+
try (StreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), writableRegistry())) {
76+
final QueryApiKeyRequest deserialized = new QueryApiKeyRequest(in);
77+
assertThat(deserialized.getQueryBuilder().getClass(), is(MatchAllQueryBuilder.class));
78+
assertThat(deserialized.getFrom(), equalTo(request3.getFrom()));
79+
assertThat(deserialized.getSize(), equalTo(request3.getSize()));
80+
assertThat(deserialized.getFieldSortBuilders(), equalTo(request3.getFieldSortBuilders()));
81+
assertThat(deserialized.getSearchAfterBuilder(), equalTo(request3.getSearchAfterBuilder()));
82+
}
83+
}
84+
}
85+
86+
public void testValidate() {
87+
final QueryApiKeyRequest request1 =
88+
new QueryApiKeyRequest(null, randomIntBetween(0, Integer.MAX_VALUE), randomIntBetween(0, Integer.MAX_VALUE), null, null);
89+
assertThat(request1.validate(), nullValue());
90+
91+
final QueryApiKeyRequest request2 =
92+
new QueryApiKeyRequest(null, randomIntBetween(Integer.MIN_VALUE, -1), randomIntBetween(0, Integer.MAX_VALUE), null, null);
93+
assertThat(request2.validate().getMessage(), containsString("[from] parameter cannot be negative"));
94+
95+
final QueryApiKeyRequest request3 =
96+
new QueryApiKeyRequest(null, randomIntBetween(0, Integer.MAX_VALUE), randomIntBetween(Integer.MIN_VALUE, -1), null, null);
97+
assertThat(request3.validate().getMessage(), containsString("[size] parameter cannot be negative"));
5998
}
6099
}

0 commit comments

Comments
 (0)