Browse Source

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 years ago
parent
commit
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.KibanaEnrollmentRequest;
 import org.elasticsearch.client.security.KibanaEnrollmentResponse;
+import org.elasticsearch.client.security.QueryApiKeyRequest;
+import org.elasticsearch.client.security.QueryApiKeyResponse;
 
 import java.io.IOException;
 
@@ -1054,7 +1056,7 @@ public final class SecurityClient {
      *
      * @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
-     * @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
      */
     public GetApiKeyResponse getApiKey(final GetApiKeyRequest request, final RequestOptions options) throws IOException {
@@ -1141,6 +1143,37 @@ public final class SecurityClient {
             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.
      * 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.PutRoleRequest;
 import org.elasticsearch.client.security.PutUserRequest;
+import org.elasticsearch.client.security.QueryApiKeyRequest;
 import org.elasticsearch.client.security.SetUserEnabledRequest;
 import org.elasticsearch.common.Strings;
 
@@ -346,6 +347,12 @@ final class SecurityRequestConverters {
         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) {
         final RequestConverters.EndpointBuilder endpointBuilder = new RequestConverters.EndpointBuilder()
             .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.ObjectParser;
 import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.core.Nullable;
 
 import java.io.IOException;
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -34,9 +37,16 @@ public final class ApiKey {
     private final String username;
     private final String realm;
     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,
                   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.id = id;
         // 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.realm = realm;
         this.metadata = metadata;
+        this.sortValues = sortValues;
     }
 
     public String getId() {
@@ -98,9 +109,21 @@ public final class ApiKey {
         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
     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
@@ -122,14 +145,22 @@ public final class ApiKey {
                 && Objects.equals(invalidated, other.invalidated)
                 && Objects.equals(username, other.username)
                 && Objects.equals(realm, other.realm)
-                && Objects.equals(metadata, other.metadata);
+                && Objects.equals(metadata, other.metadata)
+                && Arrays.equals(sortValues, other.sortValues);
     }
 
     @SuppressWarnings("unchecked")
     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]),
                 (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 {
         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("realm"));
         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 {
@@ -150,6 +182,6 @@ public final class ApiKey {
     @Override
     public String toString() {
         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.PutRoleRequest;
 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.support.expressiondsl.RoleMapperExpression;
 import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression;
@@ -504,6 +506,19 @@ public class SecurityRequestConvertersTests extends ESTestCase {
         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 {
         final String namespace = randomBoolean() ? randomAlphaOfLengthBetween(3, 8) : null;
         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.PutUserRequest;
 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.TemplateRoleName;
 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.common.util.concurrent.ThreadContext;
 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 javax.crypto.SecretKeyFactory;
@@ -132,7 +138,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 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 {
         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/get-api-key.asciidoc[]
 include::security/invalidate-api-key.asciidoc[]
+include::security/query-api-key.asciidoc[]
 include::security/get-service-accounts.asciidoc[]
 include::security/create-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;
     }
 
+    public int getCount() {
+        return items.length;
+    }
+
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject()