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
This commit is contained in:
Yang Wang 2021-08-17 16:00:55 +10:00 committed by GitHub
parent c1a32447a7
commit 7bb1185806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 774 additions and 5 deletions

View File

@ -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">

View File

@ -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");

View File

@ -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;
}
}

View File

@ -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"));
}
}

View File

@ -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) + "]";
}
}

View File

@ -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);

View File

@ -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();
{

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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.

View File

@ -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[]

View File

@ -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()