浏览代码

HLRC support for query API key API (#76520)

This PR adds HLRC for the new Query API key API added with #75335 and #76144

Relates: #71023
Yang Wang 4 年之前
父节点
当前提交
7bb1185806

+ 34 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

@@ -80,6 +80,8 @@ import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserResponse;
 import org.elasticsearch.client.security.PutUserResponse;
 import org.elasticsearch.client.security.KibanaEnrollmentRequest;
 import org.elasticsearch.client.security.KibanaEnrollmentRequest;
 import org.elasticsearch.client.security.KibanaEnrollmentResponse;
 import org.elasticsearch.client.security.KibanaEnrollmentResponse;
+import org.elasticsearch.client.security.QueryApiKeyRequest;
+import org.elasticsearch.client.security.QueryApiKeyResponse;
 
 
 import java.io.IOException;
 import java.io.IOException;
 
 
@@ -1054,7 +1056,7 @@ public final class SecurityClient {
      *
      *
      * @param request the request to retrieve API key(s)
      * @param request the request to retrieve API key(s)
      * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
      * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
-     * @return the response from the create API key call
+     * @return the response from the get API key call
      * @throws IOException in case there is a problem sending the request or parsing back the response
      * @throws IOException in case there is a problem sending the request or parsing back the response
      */
      */
     public GetApiKeyResponse getApiKey(final GetApiKeyRequest request, final RequestOptions options) throws IOException {
     public GetApiKeyResponse getApiKey(final GetApiKeyRequest request, final RequestOptions options) throws IOException {
@@ -1141,6 +1143,37 @@ public final class SecurityClient {
             CreateApiKeyResponse::fromXContent, listener, emptySet());
             CreateApiKeyResponse::fromXContent, listener, emptySet());
     }
     }
 
 
+    /**
+     * Query and retrieve API Key(s) information.<br>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-query-api-key.html">
+     * the docs</a> for more.
+     *
+     * @param request the request to query and retrieve API key(s)
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response from the query API key call
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public QueryApiKeyResponse queryApiKey(final QueryApiKeyRequest request, final RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::queryApiKey, options,
+            QueryApiKeyResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Asynchronously query and retrieve API Key(s) information.<br>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-query-api-key.html">
+     * the docs</a> for more.
+     *
+     * @param request the request to query and retrieve API key(s)
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener the listener to be notified upon request completion
+     * @return cancellable that may be used to cancel the request
+     */
+    public Cancellable queryApiKeyAsync(final QueryApiKeyRequest request, final RequestOptions options,
+                                      final ActionListener<QueryApiKeyResponse> listener) {
+        return restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::queryApiKey, options,
+            QueryApiKeyResponse::fromXContent, listener, emptySet());
+    }
+
     /**
     /**
      * Get a service account, or list of service accounts synchronously.
      * Get a service account, or list of service accounts synchronously.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-service-accounts.html">
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-service-accounts.html">

+ 7 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

@@ -44,6 +44,7 @@ import org.elasticsearch.client.security.PutPrivilegesRequest;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserRequest;
+import org.elasticsearch.client.security.QueryApiKeyRequest;
 import org.elasticsearch.client.security.SetUserEnabledRequest;
 import org.elasticsearch.client.security.SetUserEnabledRequest;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.Strings;
 
 
@@ -346,6 +347,12 @@ final class SecurityRequestConverters {
         return request;
         return request;
     }
     }
 
 
+    static Request queryApiKey(final QueryApiKeyRequest queryApiKeyRequest) throws IOException {
+        final Request request = new Request(HttpGet.METHOD_NAME, "/_security/_query/api_key");
+        request.setEntity(createEntity(queryApiKeyRequest, REQUEST_BODY_CONTENT_TYPE));
+        return request;
+    }
+
     static Request getServiceAccounts(final GetServiceAccountsRequest getServiceAccountsRequest) {
     static Request getServiceAccounts(final GetServiceAccountsRequest getServiceAccountsRequest) {
         final RequestConverters.EndpointBuilder endpointBuilder = new RequestConverters.EndpointBuilder()
         final RequestConverters.EndpointBuilder endpointBuilder = new RequestConverters.EndpointBuilder()
             .addPathPartAsIs("_security/service");
             .addPathPartAsIs("_security/service");

+ 158 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/QueryApiKeyRequest.java

@@ -0,0 +1,158 @@
+/*
+ * 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.Validatable;
+import org.elasticsearch.client.ValidationException;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.core.Nullable;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.searchafter.SearchAfterBuilder;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public final class QueryApiKeyRequest implements Validatable, ToXContentObject {
+
+    @Nullable
+    private QueryBuilder queryBuilder;
+    private Integer from;
+    private Integer size;
+    @Nullable
+    private List<FieldSortBuilder> fieldSortBuilders;
+    @Nullable
+    private SearchAfterBuilder searchAfterBuilder;
+
+    public QueryApiKeyRequest() {
+        this(null, null, null, null, null);
+    }
+
+    public QueryApiKeyRequest(
+        @Nullable QueryBuilder queryBuilder,
+        @Nullable Integer from,
+        @Nullable Integer size,
+        @Nullable List<FieldSortBuilder> fieldSortBuilders,
+        @Nullable SearchAfterBuilder searchAfterBuilder) {
+        this.queryBuilder = queryBuilder;
+        this.from = from;
+        this.size = size;
+        this.fieldSortBuilders = fieldSortBuilders;
+        this.searchAfterBuilder = searchAfterBuilder;
+    }
+
+    public QueryBuilder getQueryBuilder() {
+        return queryBuilder;
+    }
+
+    public int getFrom() {
+        return from;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public List<FieldSortBuilder> getFieldSortBuilders() {
+        return fieldSortBuilders;
+    }
+
+    public SearchAfterBuilder getSearchAfterBuilder() {
+        return searchAfterBuilder;
+    }
+
+    public QueryApiKeyRequest queryBuilder(QueryBuilder queryBuilder) {
+        this.queryBuilder = queryBuilder;
+        return this;
+    }
+
+    public QueryApiKeyRequest from(int from) {
+        this.from = from;
+        return this;
+    }
+
+    public QueryApiKeyRequest size(int size) {
+        this.size = size;
+        return this;
+    }
+
+    public QueryApiKeyRequest fieldSortBuilders(List<FieldSortBuilder> fieldSortBuilders) {
+        this.fieldSortBuilders = fieldSortBuilders;
+        return this;
+    }
+
+    public QueryApiKeyRequest searchAfterBuilder(SearchAfterBuilder searchAfterBuilder) {
+        this.searchAfterBuilder = searchAfterBuilder;
+        return this;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        if (queryBuilder != null) {
+            builder.field("query");
+            queryBuilder.toXContent(builder, params);
+        }
+        if (from != null) {
+            builder.field("from", from);
+        }
+        if (size != null) {
+            builder.field("size", size);
+        }
+        if (fieldSortBuilders != null && false == fieldSortBuilders.isEmpty()) {
+            builder.field("sort", fieldSortBuilders);
+        }
+        if (searchAfterBuilder != null) {
+            builder.array(SearchAfterBuilder.SEARCH_AFTER.getPreferredName(), searchAfterBuilder.getSortValues());
+        }
+        return builder.endObject();
+    }
+
+    @Override
+    public Optional<ValidationException> validate() {
+        ValidationException validationException = null;
+        if (from != null && from < 0) {
+            validationException = addValidationError(validationException, "from must be non-negative");
+        }
+        if (size != null && size < 0) {
+            validationException = addValidationError(validationException, "size must be non-negative");
+        }
+        return validationException == null ? Optional.empty() : Optional.of(validationException);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        QueryApiKeyRequest that = (QueryApiKeyRequest) o;
+        return Objects.equals(queryBuilder, that.queryBuilder) && Objects.equals(from, that.from) && Objects.equals(
+            size,
+            that.size) && Objects.equals(fieldSortBuilders, that.fieldSortBuilders) && Objects.equals(
+            searchAfterBuilder,
+            that.searchAfterBuilder);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(queryBuilder, from, size, fieldSortBuilders, searchAfterBuilder);
+    }
+
+    private ValidationException addValidationError(ValidationException validationException, String message) {
+        if (validationException == null) {
+            validationException = new ValidationException();
+        }
+        validationException.addValidationError(message);
+        return validationException;
+    }
+}

+ 68 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/QueryApiKeyResponse.java

@@ -0,0 +1,68 @@
+/*
+ * 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.security.support.ApiKey;
+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.List;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public final class QueryApiKeyResponse {
+
+    private final long total;
+    private final List<ApiKey> apiKeys;
+
+    public QueryApiKeyResponse(long total, List<ApiKey> apiKeys) {
+        this.total = total;
+        this.apiKeys = apiKeys;
+    }
+
+    public long getTotal() {
+        return total;
+    }
+
+    public int getCount() {
+        return apiKeys.size();
+    }
+
+    public List<ApiKey> getApiKeys() {
+        return apiKeys;
+    }
+
+    public static QueryApiKeyResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    static final ConstructingObjectParser<QueryApiKeyResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "query_api_key_response",
+        args -> {
+            final long total = (long) args[0];
+            final int count = (int) args[1];
+            @SuppressWarnings("unchecked")
+            final List<ApiKey> items = (List<ApiKey>) args[2];
+            if (count != items.size()) {
+                throw new IllegalArgumentException("count [" + count + "] is not equal to number of items ["
+                    + items.size() + "]");
+            }
+            return new QueryApiKeyResponse(total, items);
+        }
+    );
+
+    static {
+        PARSER.declareLong(constructorArg(), new ParseField("total"));
+        PARSER.declareInt(constructorArg(), new ParseField("count"));
+        PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ApiKey.fromXContent(p), new ParseField("api_keys"));
+    }
+}

+ 36 - 4
client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java

@@ -12,9 +12,12 @@ import org.elasticsearch.common.xcontent.ParseField;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.core.Nullable;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.time.Instant;
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
 
 
@@ -34,9 +37,16 @@ public final class ApiKey {
     private final String username;
     private final String username;
     private final String realm;
     private final String realm;
     private final Map<String, Object> metadata;
     private final Map<String, Object> metadata;
+    @Nullable
+    private final Object[] sortValues;
 
 
     public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm,
     public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm,
                   Map<String, Object> metadata) {
                   Map<String, Object> metadata) {
+        this(name, id, creation, expiration, invalidated, username, realm, metadata, null);
+    }
+
+    public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm,
+                  Map<String, Object> metadata, @Nullable Object[] sortValues) {
         this.name = name;
         this.name = name;
         this.id = id;
         this.id = id;
         // As we do not yet support the nanosecond precision when we serialize to JSON,
         // As we do not yet support the nanosecond precision when we serialize to JSON,
@@ -48,6 +58,7 @@ public final class ApiKey {
         this.username = username;
         this.username = username;
         this.realm = realm;
         this.realm = realm;
         this.metadata = metadata;
         this.metadata = metadata;
+        this.sortValues = sortValues;
     }
     }
 
 
     public String getId() {
     public String getId() {
@@ -98,9 +109,21 @@ public final class ApiKey {
         return metadata;
         return metadata;
     }
     }
 
 
+    /**
+     * API keys can be retrieved with either {@link org.elasticsearch.client.security.GetApiKeyRequest}
+     * or {@link org.elasticsearch.client.security.QueryApiKeyRequest}. When sorting is specified for
+     * QueryApiKeyRequest, the sort values for each key is returned along with each API key.
+     *
+     * @return Sort values for this API key if it is retrieved with QueryApiKeyRequest and sorting is
+     *         required. Otherwise, it is null.
+     */
+    public Object[] getSortValues() {
+        return sortValues;
+    }
+
     @Override
     @Override
     public int hashCode() {
     public int hashCode() {
-        return Objects.hash(name, id, creation, expiration, invalidated, username, realm, metadata);
+        return Objects.hash(name, id, creation, expiration, invalidated, username, realm, metadata, Arrays.hashCode(sortValues));
     }
     }
 
 
     @Override
     @Override
@@ -122,14 +145,22 @@ public final class ApiKey {
                 && Objects.equals(invalidated, other.invalidated)
                 && Objects.equals(invalidated, other.invalidated)
                 && Objects.equals(username, other.username)
                 && Objects.equals(username, other.username)
                 && Objects.equals(realm, other.realm)
                 && Objects.equals(realm, other.realm)
-                && Objects.equals(metadata, other.metadata);
+                && Objects.equals(metadata, other.metadata)
+                && Arrays.equals(sortValues, other.sortValues);
     }
     }
 
 
     @SuppressWarnings("unchecked")
     @SuppressWarnings("unchecked")
     static final ConstructingObjectParser<ApiKey, Void> PARSER = new ConstructingObjectParser<>("api_key", args -> {
     static final ConstructingObjectParser<ApiKey, Void> PARSER = new ConstructingObjectParser<>("api_key", args -> {
+        final Object[] sortValues;
+        if (args[8] == null) {
+            sortValues = null;
+        } else {
+            final List<Object> arg8 = (List<Object>) args[8];
+            sortValues = arg8.isEmpty() ? null : arg8.toArray();
+        }
         return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]),
         return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]),
                 (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6],
                 (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6],
-                (Map<String, Object>) args[7]);
+                (Map<String, Object>) args[7], sortValues);
     });
     });
     static {
     static {
         PARSER.declareField(optionalConstructorArg(), (p, c) -> p.textOrNull(), new ParseField("name"),
         PARSER.declareField(optionalConstructorArg(), (p, c) -> p.textOrNull(), new ParseField("name"),
@@ -141,6 +172,7 @@ public final class ApiKey {
         PARSER.declareString(constructorArg(), new ParseField("username"));
         PARSER.declareString(constructorArg(), new ParseField("username"));
         PARSER.declareString(constructorArg(), new ParseField("realm"));
         PARSER.declareString(constructorArg(), new ParseField("realm"));
         PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata"));
         PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata"));
+        PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> p.objectText(), new ParseField("_sort"));
     }
     }
 
 
     public static ApiKey fromXContent(XContentParser parser) throws IOException {
     public static ApiKey fromXContent(XContentParser parser) throws IOException {
@@ -150,6 +182,6 @@ public final class ApiKey {
     @Override
     @Override
     public String toString() {
     public String toString() {
         return "ApiKey [name=" + name + ", id=" + id + ", creation=" + creation + ", expiration=" + expiration + ", invalidated="
         return "ApiKey [name=" + name + ", id=" + id + ", creation=" + creation + ", expiration=" + expiration + ", invalidated="
-                + invalidated + ", username=" + username + ", realm=" + realm + "]";
+                + invalidated + ", username=" + username + ", realm=" + realm + ", _sort=" + Arrays.toString(sortValues) + "]";
     }
     }
 }
 }

+ 15 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java

@@ -38,6 +38,8 @@ import org.elasticsearch.client.security.PutPrivilegesRequest;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserRequest;
+import org.elasticsearch.client.security.QueryApiKeyRequest;
+import org.elasticsearch.client.security.QueryApiKeyRequestTests;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression;
 import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression;
 import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression;
 import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression;
@@ -504,6 +506,19 @@ public class SecurityRequestConvertersTests extends ESTestCase {
         assertToXContentBody(invalidateApiKeyRequest, request.getEntity());
         assertToXContentBody(invalidateApiKeyRequest, request.getEntity());
     }
     }
 
 
+    public void testQueryApiKey() throws IOException {
+        final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest(
+            QueryApiKeyRequestTests.randomQueryBuilder(),
+            randomIntBetween(0, 100),
+            randomIntBetween(0, 100),
+            QueryApiKeyRequestTests.randomFieldSortBuilders(),
+            QueryApiKeyRequestTests.randomSearchAfterBuilder());
+        final Request request = SecurityRequestConverters.queryApiKey(queryApiKeyRequest);
+        assertEquals(HttpGet.METHOD_NAME, request.getMethod());
+        assertEquals("/_security/_query/api_key", request.getEndpoint());
+        assertToXContentBody(queryApiKeyRequest, request.getEntity());
+    }
+
     public void testGetServiceAccounts() throws IOException {
     public void testGetServiceAccounts() throws IOException {
         final String namespace = randomBoolean() ? randomAlphaOfLengthBetween(3, 8) : null;
         final String namespace = randomBoolean() ? randomAlphaOfLengthBetween(3, 8) : null;
         final String serviceName = namespace == null ? null : randomAlphaOfLengthBetween(3, 8);
         final String serviceName = namespace == null ? null : randomAlphaOfLengthBetween(3, 8);

+ 133 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

@@ -84,6 +84,8 @@ import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutRoleResponse;
 import org.elasticsearch.client.security.PutRoleResponse;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserResponse;
 import org.elasticsearch.client.security.PutUserResponse;
+import org.elasticsearch.client.security.QueryApiKeyRequest;
+import org.elasticsearch.client.security.QueryApiKeyResponse;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.TemplateRoleName;
 import org.elasticsearch.client.security.TemplateRoleName;
 import org.elasticsearch.client.security.support.ApiKey;
 import org.elasticsearch.client.security.support.ApiKey;
@@ -109,6 +111,10 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.common.util.set.Sets;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.searchafter.SearchAfterBuilder;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.elasticsearch.search.sort.SortOrder;
 import org.hamcrest.Matchers;
 import org.hamcrest.Matchers;
 
 
 import javax.crypto.SecretKeyFactory;
 import javax.crypto.SecretKeyFactory;
@@ -132,7 +138,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.contains;
@@ -2557,6 +2565,131 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
 
     }
     }
 
 
+    public void testQueryApiKey() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+        RestHighLevelClient client = highLevelClient();
+        final CreateApiKeyRequest createApiKeyRequest1 = new CreateApiKeyRequest("key-10000", List.of(),
+            randomBoolean() ? TimeValue.timeValueHours(24) : null,
+            RefreshPolicy.WAIT_UNTIL, Map.of("environment", "east-production"));
+        final CreateApiKeyResponse createApiKeyResponse1 = client.security().createApiKey(createApiKeyRequest1, RequestOptions.DEFAULT);
+        final CreateApiKeyRequest createApiKeyRequest2 = new CreateApiKeyRequest("key-20000", List.of(),
+            randomBoolean() ? TimeValue.timeValueHours(24) : null,
+            RefreshPolicy.WAIT_UNTIL, Map.of("environment", "east-staging"));
+        final CreateApiKeyResponse createApiKeyResponse2 = client.security().createApiKey(createApiKeyRequest2, RequestOptions.DEFAULT);
+
+        {
+            // tag::query-api-key-default-request
+            QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest();
+            // end::query-api-key-default-request
+
+            // tag::query-api-key-execute
+            QueryApiKeyResponse queryApiKeyResponse = client.security().queryApiKey(queryApiKeyRequest, RequestOptions.DEFAULT);
+            // end::query-api-key-execute
+
+            assertThat(queryApiKeyResponse.getTotal(), equalTo(2L));
+            assertThat(queryApiKeyResponse.getCount(), equalTo(2));
+            assertThat(queryApiKeyResponse.getApiKeys().stream().map(ApiKey::getName).collect(Collectors.toUnmodifiableSet()),
+                equalTo(Set.of("key-10000", "key-20000")));
+            assertThat(queryApiKeyResponse.getApiKeys().stream().map(ApiKey::getId).collect(Collectors.toUnmodifiableSet()),
+                equalTo(Set.of(createApiKeyResponse1.getId(), createApiKeyResponse2.getId())));
+        }
+
+        {
+            // tag::query-api-key-query-request
+            QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest().queryBuilder(
+                QueryBuilders.boolQuery()
+                    .must(QueryBuilders.prefixQuery("metadata.environment", "east-"))
+                    .mustNot(QueryBuilders.termQuery("name", "key-20000")));
+            // end::query-api-key-query-request
+
+            QueryApiKeyResponse queryApiKeyResponse = client.security().queryApiKey(queryApiKeyRequest, RequestOptions.DEFAULT);
+            assertThat(queryApiKeyResponse.getTotal(), equalTo(1L));
+            assertThat(queryApiKeyResponse.getCount(), equalTo(1));
+            assertThat(queryApiKeyResponse.getApiKeys().get(0).getName(), equalTo(createApiKeyResponse1.getName()));
+            assertThat(queryApiKeyResponse.getApiKeys().get(0).getId(), equalTo(createApiKeyResponse1.getId()));
+        }
+
+        {
+            // tag::query-api-key-from-size-sort-request
+            QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest()
+                .from(1)
+                .size(100)
+                .fieldSortBuilders(List.of(new FieldSortBuilder("name").order(SortOrder.DESC)));
+            // end::query-api-key-from-size-sort-request
+
+            QueryApiKeyResponse queryApiKeyResponse = client.security().queryApiKey(queryApiKeyRequest, RequestOptions.DEFAULT);
+
+            // tag::query-api-key-from-size-sort-response
+            final long total = queryApiKeyResponse.getTotal();  // <1>
+            final int count = queryApiKeyResponse.getCount();  // <2>
+            final List<ApiKey> apiKeys = queryApiKeyResponse.getApiKeys();  // <3>
+            final Object[] sortValues = apiKeys.get(apiKeys.size()-1).getSortValues();  // <4>
+            // end::query-api-key-from-size-sort-response
+
+            assertThat(total, equalTo(2L));
+            assertThat(count, equalTo(1));
+            assertThat(apiKeys.get(0).getName(), equalTo(createApiKeyResponse1.getName()));
+            assertThat(apiKeys.get(0).getId(), equalTo(createApiKeyResponse1.getId()));
+            assertThat(sortValues.length, equalTo(1));
+            assertThat(sortValues[0], equalTo(createApiKeyResponse1.getName()));
+        }
+
+        {
+            // tag::query-api-key-search-after-request
+            QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest()
+                .fieldSortBuilders(List.of(new FieldSortBuilder("name")))
+                .searchAfterBuilder(new SearchAfterBuilder().setSortValues(new String[] {"key-10000"}));
+            // end::query-api-key-search-after-request
+
+            QueryApiKeyResponse queryApiKeyResponse = client.security().queryApiKey(queryApiKeyRequest, RequestOptions.DEFAULT);
+            assertThat(queryApiKeyResponse.getTotal(), equalTo(2L));
+            assertThat(queryApiKeyResponse.getCount(), equalTo(1));
+            assertThat(queryApiKeyResponse.getApiKeys().get(0).getName(), equalTo(createApiKeyResponse2.getName()));
+            assertThat(queryApiKeyResponse.getApiKeys().get(0).getId(), equalTo(createApiKeyResponse2.getId()));
+        }
+
+        {
+            QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest();
+
+            ActionListener<QueryApiKeyResponse> listener;
+            // tag::query-api-key-execute-listener
+            listener = new ActionListener<QueryApiKeyResponse>() {
+                @Override
+                public void onResponse(QueryApiKeyResponse queryApiKeyResponse) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::query-api-key-execute-listener
+
+            // Avoid unused variable warning
+            assertNotNull(listener);
+
+            // Replace the empty listener by a blocking listener in test
+            final PlainActionFuture<QueryApiKeyResponse> future = new PlainActionFuture<>();
+            listener = future;
+
+            // tag::query-api-key-execute-async
+            client.security().queryApiKeyAsync(queryApiKeyRequest, RequestOptions.DEFAULT, listener); // <1>
+            // end::query-api-key-execute-async
+
+            final QueryApiKeyResponse queryApiKeyResponse = future.get(30, TimeUnit.SECONDS);
+            assertNotNull(queryApiKeyResponse);
+
+            assertThat(queryApiKeyResponse.getTotal(), equalTo(2L));
+            assertThat(queryApiKeyResponse.getCount(), equalTo(2));
+            assertThat(queryApiKeyResponse.getApiKeys(), is(notNullValue()));
+            assertThat(queryApiKeyResponse.getApiKeys().size(), is(2));
+            assertThat(queryApiKeyResponse.getApiKeys().stream().map(ApiKey::getName).collect(Collectors.toUnmodifiableSet()),
+                equalTo(Set.of("key-10000", "key-20000")));
+            assertThat(queryApiKeyResponse.getApiKeys().stream().map(ApiKey::getId).collect(Collectors.toUnmodifiableSet()),
+                equalTo(Set.of(createApiKeyResponse1.getId(), createApiKeyResponse2.getId())));
+        }
+    }
+
     public void testGetServiceAccounts() throws IOException {
     public void testGetServiceAccounts() throws IOException {
         RestHighLevelClient client = highLevelClient();
         RestHighLevelClient client = highLevelClient();
         {
         {

+ 144 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/QueryApiKeyRequestTests.java

@@ -0,0 +1,144 @@
+/*
+ * 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.ValidationException;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.searchafter.SearchAfterBuilder;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+public class QueryApiKeyRequestTests extends ESTestCase {
+
+    public void testNewInstance() {
+        final QueryBuilder queryBuilder = randomQueryBuilder();
+        final int from = randomIntBetween(0, 100);
+        final int size = randomIntBetween(0, 100);
+        final List<FieldSortBuilder> fieldSortBuilders = randomFieldSortBuilders();
+        final SearchAfterBuilder searchAfterBuilder = randomSearchAfterBuilder();
+        final QueryApiKeyRequest request = new QueryApiKeyRequest(queryBuilder, from, size, fieldSortBuilders, searchAfterBuilder);
+
+        assertThat(request.getQueryBuilder(), equalTo(queryBuilder));
+        assertThat(request.getFrom(), equalTo(from));
+        assertThat(request.getSize(), equalTo(size));
+        assertThat(request.getFieldSortBuilders(), equalTo(fieldSortBuilders));
+        assertThat(request.getSearchAfterBuilder(), equalTo(searchAfterBuilder));
+    }
+
+    public void testEqualsHashCode() {
+        final QueryApiKeyRequest request = new QueryApiKeyRequest(randomQueryBuilder(),
+            randomIntBetween(0, 100),
+            randomIntBetween(0, 100),
+            randomFieldSortBuilders(),
+            randomSearchAfterBuilder());
+
+        EqualsHashCodeTestUtils.checkEqualsAndHashCode(request, original -> new QueryApiKeyRequest(original.getQueryBuilder(),
+            original.getFrom(),
+            original.getSize(),
+            original.getFieldSortBuilders(),
+            original.getSearchAfterBuilder()), this::mutateInstance);
+    }
+
+    public void testValidation() {
+        final QueryApiKeyRequest request1 = new QueryApiKeyRequest(null, randomIntBetween(0, 100), randomIntBetween(0, 100), null, null);
+        final Optional<ValidationException> validationException1 = request1.validate();
+        assertThat(validationException1.isEmpty(), is(true));
+
+        final QueryApiKeyRequest request2 = new QueryApiKeyRequest(null, randomIntBetween(-100, -1), randomIntBetween(0, 100), null, null);
+        final Optional<ValidationException> validationException2 = request2.validate();
+        assertThat(validationException2.orElseThrow().getMessage(), containsString("from must be non-negative"));
+
+        final QueryApiKeyRequest request3 = new QueryApiKeyRequest(null, randomIntBetween(0, 100), randomIntBetween(-100, -1), null, null);
+        final Optional<ValidationException> validationException3 = request3.validate();
+        assertThat(validationException3.orElseThrow().getMessage(), containsString("size must be non-negative"));
+    }
+
+    private QueryApiKeyRequest mutateInstance(QueryApiKeyRequest request) {
+        switch (randomIntBetween(0, 5)) {
+            case 0:
+                return new QueryApiKeyRequest(randomValueOtherThan(request.getQueryBuilder(), QueryApiKeyRequestTests::randomQueryBuilder),
+                    request.getFrom(),
+                    request.getSize(),
+                    request.getFieldSortBuilders(),
+                    request.getSearchAfterBuilder());
+            case 1:
+                return new QueryApiKeyRequest(request.getQueryBuilder(),
+                    request.getFrom() + 1,
+                    request.getSize(),
+                    request.getFieldSortBuilders(),
+                    request.getSearchAfterBuilder());
+            case 2:
+                return new QueryApiKeyRequest(request.getQueryBuilder(),
+                    request.getFrom(),
+                    request.getSize() + 1,
+                    request.getFieldSortBuilders(),
+                    request.getSearchAfterBuilder());
+            case 3:
+                return new QueryApiKeyRequest(request.getQueryBuilder(),
+                    request.getFrom(),
+                    request.getSize(),
+                    randomValueOtherThan(request.getFieldSortBuilders(), QueryApiKeyRequestTests::randomFieldSortBuilders),
+                    request.getSearchAfterBuilder());
+            default:
+                return new QueryApiKeyRequest(request.getQueryBuilder(),
+                    request.getFrom(),
+                    request.getSize(),
+                    request.getFieldSortBuilders(),
+                    randomValueOtherThan(request.getSearchAfterBuilder(), QueryApiKeyRequestTests::randomSearchAfterBuilder));
+
+        }
+    }
+
+    public static QueryBuilder randomQueryBuilder() {
+        switch (randomIntBetween(0, 5)) {
+            case 0:
+                return QueryBuilders.matchAllQuery();
+            case 1:
+                return QueryBuilders.termQuery(randomAlphaOfLengthBetween(3, 8),
+                    randomFrom(randomAlphaOfLength(8), randomInt(), randomLong(), randomDouble(), randomFloat()));
+            case 2:
+                return QueryBuilders.idsQuery().addIds(randomArray(1, 5, String[]::new, () -> randomAlphaOfLength(20)));
+            case 3:
+                return QueryBuilders.prefixQuery(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8));
+            case 4:
+                return QueryBuilders.wildcardQuery(randomAlphaOfLengthBetween(3, 8),
+                    randomAlphaOfLengthBetween(0, 3) + "*" + randomAlphaOfLengthBetween(0, 3));
+            case 5:
+                return QueryBuilders.rangeQuery(randomAlphaOfLengthBetween(3, 8)).from(randomNonNegativeLong()).to(randomNonNegativeLong());
+            default:
+                return null;
+        }
+    }
+
+    public static List<FieldSortBuilder> randomFieldSortBuilders() {
+        if (randomBoolean()) {
+            return randomList(1, 2, () -> new FieldSortBuilder(randomAlphaOfLengthBetween(3, 8)).order(randomFrom(SortOrder.values())));
+        } else {
+            return null;
+        }
+    }
+
+    public static SearchAfterBuilder randomSearchAfterBuilder() {
+        if (randomBoolean()) {
+            return new SearchAfterBuilder().setSortValues(randomArray(1, 2, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
+        } else {
+            return null;
+        }
+    }
+}

+ 88 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/QueryApiKeyResponseTests.java

@@ -0,0 +1,88 @@
+/*
+ * 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.AbstractResponseTestCase;
+import org.elasticsearch.client.security.support.ApiKey;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class QueryApiKeyResponseTests
+    extends AbstractResponseTestCase<org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse, QueryApiKeyResponse> {
+
+    @Override
+    protected org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse createServerTestInstance(XContentType xContentType) {
+        final int count = randomIntBetween(0, 5);
+        final int total = randomIntBetween(count, count + 5);
+        final int nSortValues = randomIntBetween(0, 3);
+        return new org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse(total,
+            IntStream.range(0, count)
+                .mapToObj(i -> new org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse.Item(
+                    randomApiKeyInfo(),
+                    randSortValues(nSortValues)))
+                .collect(Collectors.toUnmodifiableList()));
+    }
+
+    @Override
+    protected QueryApiKeyResponse doParseToClientInstance(XContentParser parser) throws IOException {
+        return QueryApiKeyResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected void assertInstances(
+        org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse serverTestInstance, QueryApiKeyResponse clientInstance) {
+        assertThat(serverTestInstance.getTotal(), equalTo(clientInstance.getTotal()));
+        assertThat(serverTestInstance.getCount(), equalTo(clientInstance.getCount()));
+        for (int i = 0; i < serverTestInstance.getItems().length; i++) {
+            assertApiKeyInfo(serverTestInstance.getItems()[i], clientInstance.getApiKeys().get(i));
+        }
+    }
+
+    private void assertApiKeyInfo(
+        org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse.Item serverItem, ApiKey clientApiKeyInfo) {
+        assertThat(serverItem.getApiKey().getId(), equalTo(clientApiKeyInfo.getId()));
+        assertThat(serverItem.getApiKey().getName(), equalTo(clientApiKeyInfo.getName()));
+        assertThat(serverItem.getApiKey().getUsername(), equalTo(clientApiKeyInfo.getUsername()));
+        assertThat(serverItem.getApiKey().getRealm(), equalTo(clientApiKeyInfo.getRealm()));
+        assertThat(serverItem.getApiKey().getCreation(), equalTo(clientApiKeyInfo.getCreation()));
+        assertThat(serverItem.getApiKey().getExpiration(), equalTo(clientApiKeyInfo.getExpiration()));
+        assertThat(serverItem.getApiKey().getMetadata(), equalTo(clientApiKeyInfo.getMetadata()));
+        assertThat(serverItem.getSortValues(), equalTo(clientApiKeyInfo.getSortValues()));
+    }
+
+    private org.elasticsearch.xpack.core.security.action.ApiKey randomApiKeyInfo() {
+        final Instant creation = Instant.now();
+        return new org.elasticsearch.xpack.core.security.action.ApiKey(randomAlphaOfLengthBetween(3, 8),
+            randomAlphaOfLength(20),
+            creation,
+            randomFrom(creation.plus(randomLongBetween(1, 10), ChronoUnit.DAYS), null),
+            randomBoolean(),
+            randomAlphaOfLengthBetween(3, 8),
+            randomAlphaOfLengthBetween(3, 8),
+            CreateApiKeyRequestTests.randomMetadata()
+        );
+    }
+
+    private Object[] randSortValues(int nSortValues) {
+        if (nSortValues > 0) {
+            return randomArray(nSortValues, nSortValues, Object[]::new,
+                () -> randomFrom(randomInt(Integer.MAX_VALUE), randomAlphaOfLength(8), randomBoolean()));
+        } else {
+            return null;
+        }
+    }
+}

+ 86 - 0
docs/java-rest/high-level/security/query-api-key.asciidoc

@@ -0,0 +1,86 @@
+--
+:api: query-api-key
+:request: QueryApiKeyRequest
+:response: QueryApiKeyResponse
+--
+[role="xpack"]
+[id="{upid}-{api}"]
+=== Query API Key information API
+
+API Key(s) information can be queried and retrieved in a paginated
+fashion using this API.
+
+[id="{upid}-{api}-request"]
+==== Query API Key Request
+The +{request}+ supports query and retrieving API key information using
+Elasticsearch's {ref}/query-dsl.html[Query DSL] with
+{ref}/paginate-search-results.html[pagination].
+It supports only a subset of available query types, including:
+
+. {ref}/query-dsl-bool-query.html[Boolean query]
+
+. {ref}/query-dsl-match-all-query.html[Match all query]
+
+. {ref}/query-dsl-term-query.html[Term query]
+
+. {ref}/query-dsl-terms-query.html[Terms query]
+
+. {ref}/query-dsl-ids-query.html[IDs Query]
+
+. {ref}/query-dsl-prefix-query.html[Prefix query]
+
+. {ref}/query-dsl-wildcard-query.html[Wildcard query]
+
+. {ref}/query-dsl-range-query.html[Range query]
+
+===== Query for all API keys
+In its most basic form, the request selects all API keys that the user
+has access to.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[query-api-key-default-request]
+--------------------------------------------------
+
+===== Query API keys with Query DSL
+The following query selects API keys owned by the user and also satisfy following criteria:
+* The API key name must begin with the word `key`
+* The API key name must *not* be `key-20000`
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[query-api-key-query-request]
+--------------------------------------------------
+
+===== Retrieve API keys with explicitly configured sort and paging
+The following request sort the API keys by their names in descending order.
+It also retrieves the API keys from index 1 (zero-based) and in a page size of 100.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[query-api-key-from-size-sort-request]
+--------------------------------------------------
+
+===== Deep pagination can be achieved with search after
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[query-api-key-search-after-request]
+--------------------------------------------------
+
+include::../execution.asciidoc[]
+
+[id="{upid}-{api}-response"]
+==== Query API Key information API Response
+
+The returned +{response}+ contains the information regarding the API keys that were
+requested.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[query-api-key-from-size-sort-response]
+--------------------------------------------------
+<1> Total number of API keys matched by the query
+<2> Number of API keys returned in this response
+<3> The list of API keys
+<4> If sorting is requested, each API key in the response contains its sort values.

+ 1 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -540,6 +540,7 @@ include::security/create-api-key.asciidoc[]
 include::security/grant-api-key.asciidoc[]
 include::security/grant-api-key.asciidoc[]
 include::security/get-api-key.asciidoc[]
 include::security/get-api-key.asciidoc[]
 include::security/invalidate-api-key.asciidoc[]
 include::security/invalidate-api-key.asciidoc[]
+include::security/query-api-key.asciidoc[]
 include::security/get-service-accounts.asciidoc[]
 include::security/get-service-accounts.asciidoc[]
 include::security/create-service-account-token.asciidoc[]
 include::security/create-service-account-token.asciidoc[]
 include::security/delete-service-account-token.asciidoc[]
 include::security/delete-service-account-token.asciidoc[]

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

@@ -56,6 +56,10 @@ public final class QueryApiKeyResponse extends ActionResponse implements ToXCont
         return items;
         return items;
     }
     }
 
 
+    public int getCount() {
+        return items.length;
+    }
+
     @Override
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject()
         builder.startObject()