From 1145e04b8520ab4dac6569b213ae0710eec0d5ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemys=C5=82aw=20Witek?=
Date: Wed, 10 Jan 2024 13:06:45 +0100
Subject: [PATCH 01/75] Make `ParentTaskAssigningClient.getRemoteClusterClient`
method also return `ParentTaskAssigningClient` (#100813)
Currently, if we use `ParentTaskAssigningClient` and want to spawn a
remote request, we need to: 1. get remote client using
`.getRemoteClusterClient` method 2. call `.setParentTask` on the remote
request method manually
This PR makes it so the remote client obtained by calling
`.getRemoteClusterClient` method is also a `ParentTaskAssigningClient`
so there is no need to call `.setParentTask` on the child request
anymore.
---
docs/changelog/100813.yaml | 6 +++++
.../internal/ParentTaskAssigningClient.java | 8 ++++++
.../ParentTaskAssigningClientTests.java | 26 +++++++++++++++++++
3 files changed, 40 insertions(+)
create mode 100644 docs/changelog/100813.yaml
diff --git a/docs/changelog/100813.yaml b/docs/changelog/100813.yaml
new file mode 100644
index 0000000000000..476098b62c106
--- /dev/null
+++ b/docs/changelog/100813.yaml
@@ -0,0 +1,6 @@
+pr: 100813
+summary: Make `ParentTaskAssigningClient.getRemoteClusterClient` method also return
+ `ParentTaskAssigningClient`
+area: Infra/Transport API
+type: enhancement
+issues: []
diff --git a/server/src/main/java/org/elasticsearch/client/internal/ParentTaskAssigningClient.java b/server/src/main/java/org/elasticsearch/client/internal/ParentTaskAssigningClient.java
index 967e5c72efdd0..e6393393916b1 100644
--- a/server/src/main/java/org/elasticsearch/client/internal/ParentTaskAssigningClient.java
+++ b/server/src/main/java/org/elasticsearch/client/internal/ParentTaskAssigningClient.java
@@ -16,6 +16,8 @@
import org.elasticsearch.tasks.Task;
import org.elasticsearch.tasks.TaskId;
+import java.util.concurrent.Executor;
+
/**
* A {@linkplain Client} that sets the parent task on all requests that it makes. Use this to conveniently implement actions that cause
* many other actions.
@@ -58,4 +60,10 @@ protected void
request.setParentTask(parentTask);
super.doExecute(action, request, listener);
}
+
+ @Override
+ public ParentTaskAssigningClient getRemoteClusterClient(String clusterAlias, Executor responseExecutor) {
+ Client remoteClient = super.getRemoteClusterClient(clusterAlias, responseExecutor);
+ return new ParentTaskAssigningClient(remoteClient, parentTask);
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/client/internal/ParentTaskAssigningClientTests.java b/server/src/test/java/org/elasticsearch/client/internal/ParentTaskAssigningClientTests.java
index 2c2e131b8c5ad..0100c7cab5ba4 100644
--- a/server/src/test/java/org/elasticsearch/client/internal/ParentTaskAssigningClientTests.java
+++ b/server/src/test/java/org/elasticsearch/client/internal/ParentTaskAssigningClientTests.java
@@ -15,10 +15,17 @@
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.common.util.concurrent.EsExecutors;
import org.elasticsearch.tasks.TaskId;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.client.NoOpClient;
+import java.util.concurrent.Executor;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+
public class ParentTaskAssigningClientTests extends ESTestCase {
public void testSetsParentId() {
TaskId[] parentTaskId = new TaskId[] { new TaskId(randomAlphaOfLength(3), randomLong()) };
@@ -51,4 +58,23 @@ protected void
client.unwrap().clearScroll(new ClearScrollRequest());
}
}
+
+ public void testRemoteClientIsAlsoAParentAssigningClient() {
+ TaskId parentTaskId = new TaskId(randomAlphaOfLength(3), randomLong());
+
+ try (var threadPool = createThreadPool()) {
+ final var mockClient = new NoOpClient(threadPool) {
+ @Override
+ public Client getRemoteClusterClient(String clusterAlias, Executor responseExecutor) {
+ return mock(Client.class);
+ }
+ };
+
+ final var client = new ParentTaskAssigningClient(mockClient, parentTaskId);
+ assertThat(
+ client.getRemoteClusterClient("remote-cluster", EsExecutors.DIRECT_EXECUTOR_SERVICE),
+ is(instanceOf(ParentTaskAssigningClient.class))
+ );
+ }
+ }
}
From ba0d1a4823cdc201fa57f7d1bfd2a69eb5c00cc0 Mon Sep 17 00:00:00 2001
From: Chris Hegarty <62058229+ChrisHegarty@users.noreply.github.com>
Date: Wed, 10 Jan 2024 12:15:18 +0000
Subject: [PATCH 02/75] Add ES|QL async security tests (#104137)
This commit expands the current ES|QL security tests to cover async.
---
.../test/rest/ESRestTestCase.java | 9 +-
.../xpack/core/esql/EsqlAsyncActionNames.java | 15 ++
.../xpack/esql/EsqlAsyncSecurityIT.java | 134 ++++++++++++++++++
.../xpack/esql/EsqlSecurityIT.java | 64 +++++----
.../esql/action/EsqlAsyncGetResultAction.java | 3 +-
.../xpack/security/authz/RBACEngine.java | 2 +
6 files changed, 195 insertions(+), 32 deletions(-)
create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/esql/EsqlAsyncActionNames.java
create mode 100644 x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlAsyncSecurityIT.java
diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java
index 94b1d4ab321ee..20cd1997fd70e 100644
--- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java
+++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java
@@ -11,6 +11,7 @@
import io.netty.handler.codec.http.HttpMethod;
import org.apache.http.Header;
+import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpDelete;
@@ -157,14 +158,18 @@ public abstract class ESRestTestCase extends ESTestCase {
* Convert the entity from a {@link Response} into a map of maps.
*/
public static Map entityAsMap(Response response) throws IOException {
- XContentType xContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
+ return entityAsMap(response.getEntity());
+ }
+
+ public static Map entityAsMap(HttpEntity entity) throws IOException {
+ XContentType xContentType = XContentType.fromMediaType(entity.getContentType().getValue());
// EMPTY and THROW are fine here because `.map` doesn't use named x content or deprecation
try (
XContentParser parser = xContentType.xContent()
.createParser(
XContentParserConfiguration.EMPTY.withRegistry(NamedXContentRegistry.EMPTY)
.withDeprecationHandler(DeprecationHandler.THROW_UNSUPPORTED_OPERATION),
- response.getEntity().getContent()
+ entity.getContent()
)
) {
return parser.map();
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/esql/EsqlAsyncActionNames.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/esql/EsqlAsyncActionNames.java
new file mode 100644
index 0000000000000..81ab54fc2db5f
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/esql/EsqlAsyncActionNames.java
@@ -0,0 +1,15 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.esql;
+
+/**
+ * Exposes ES|QL async action names for RBACEngine.
+ */
+public class EsqlAsyncActionNames {
+ public static final String ESQL_ASYNC_GET_RESULT_ACTION_NAME = "indices:data/read/esql/async/get";
+}
diff --git a/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlAsyncSecurityIT.java b/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlAsyncSecurityIT.java
new file mode 100644
index 0000000000000..544eb82fb5ace
--- /dev/null
+++ b/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlAsyncSecurityIT.java
@@ -0,0 +1,134 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql;
+
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.json.JsonXContent;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import static org.elasticsearch.core.TimeValue.timeValueNanos;
+import static org.hamcrest.Matchers.either;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+public class EsqlAsyncSecurityIT extends EsqlSecurityIT {
+
+ private static final Logger LOGGER = LogManager.getLogger(EsqlAsyncSecurityIT.class);
+
+ @Override
+ protected Response runESQLCommand(String user, String command) throws IOException {
+ var response = runAsync(user, command);
+ assertOK(response);
+ var respMap = entityAsMap(response.getEntity());
+ String id = (String) respMap.get("id");
+ assertThat((boolean) respMap.get("is_running"), either(is(true)).or(is(false)));
+ var getResponse = runAsyncGet(user, id);
+ assertOK(getResponse);
+ var deleteResponse = runAsyncDelete(user, id);
+ assertOK(deleteResponse);
+ return getResponse;
+ }
+
+ @Override
+ public void testUnauthorizedIndices() throws IOException {
+ super.testUnauthorizedIndices();
+ {
+ var response = runAsync("user1", "from index-user1 | stats sum(value)");
+ assertOK(response);
+ var respMap = entityAsMap(response.getEntity());
+ String id = (String) respMap.get("id");
+ assertThat((boolean) respMap.get("is_running"), either(is(true)).or(is(false)));
+
+ var getResponse = runAsyncGet("user1", id); // sanity
+ assertOK(getResponse);
+ ResponseException error;
+ error = expectThrows(ResponseException.class, () -> runAsyncGet("user2", id));
+ // resource not found exception if the authenticated user is not the creator of the original task
+ assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(404));
+
+ error = expectThrows(ResponseException.class, () -> runAsyncDelete("user2", id));
+ // resource not found exception if the authenticated user is not the creator of the original task
+ assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(404));
+ }
+ {
+ var response = runAsync("user2", "from index-user2 | stats sum(value)");
+ assertOK(response);
+ var respMap = entityAsMap(response.getEntity());
+ String id = (String) respMap.get("id");
+ assertThat((boolean) respMap.get("is_running"), either(is(true)).or(is(false)));
+
+ var getResponse = runAsyncGet("user2", id); // sanity
+ assertOK(getResponse);
+ ResponseException error;
+ error = expectThrows(ResponseException.class, () -> runAsyncGet("user1", id));
+ assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(404));
+
+ error = expectThrows(ResponseException.class, () -> runAsyncDelete("user1", id));
+ assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(404));
+ }
+ }
+
+ // Keep_on_complete is always true, so we will always get an id
+ private Response runAsync(String user, String command) throws IOException {
+ if (command.toLowerCase(Locale.ROOT).contains("limit") == false) {
+ // add a (high) limit to avoid warnings on default limit
+ command += " | limit 10000000";
+ }
+ XContentBuilder json = JsonXContent.contentBuilder();
+ json.startObject();
+ json.field("query", command);
+ addRandomPragmas(json);
+ json.field("wait_for_completion_timeout", timeValueNanos(randomIntBetween(1, 1000)));
+ json.field("keep_on_completion", "true");
+ json.endObject();
+ Request request = new Request("POST", "_query/async");
+ request.setJsonEntity(Strings.toString(json));
+ request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user));
+ logRequest(request);
+ Response response = client().performRequest(request);
+ logResponse(response);
+ return response;
+ }
+
+ private Response runAsyncGet(String user, String id) throws IOException {
+ Request getRequest = new Request("GET", "_query/async/" + id + "?wait_for_completion_timeout=60s");
+ getRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user));
+ logRequest(getRequest);
+ var response = client().performRequest(getRequest);
+ logResponse(response);
+ return response;
+ }
+
+ private Response runAsyncDelete(String user, String id) throws IOException {
+ Request getRequest = new Request("DELETE", "_query/async/" + id);
+ getRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user));
+ logRequest(getRequest);
+ var response = client().performRequest(getRequest);
+ logResponse(response);
+ return response;
+ }
+
+ static void logRequest(Request request) throws IOException {
+ LOGGER.info("REQUEST={}", request);
+ var entity = request.getEntity();
+ if (entity != null) LOGGER.info("REQUEST body={}", entityAsMap(entity));
+ }
+
+ static void logResponse(Response response) {
+ LOGGER.info("RESPONSE={}", response);
+ }
+}
diff --git a/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java b/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java
index 98ec411569af5..e363fa64c594d 100644
--- a/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java
+++ b/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java
@@ -114,7 +114,7 @@ public void testAllowedIndices() throws Exception {
}
}
- public void testUnauthorizedIndices() {
+ public void testUnauthorizedIndices() throws IOException {
ResponseException error;
error = expectThrows(ResponseException.class, () -> runESQLCommand("user1", "from index-user2 | stats sum(value)"));
assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(400));
@@ -271,41 +271,47 @@ private void removeEnrichPolicy() throws Exception {
client().performRequest(new Request("DELETE", "_enrich/policy/songs"));
}
- private Response runESQLCommand(String user, String command) throws IOException {
+ protected Response runESQLCommand(String user, String command) throws IOException {
if (command.toLowerCase(Locale.ROOT).contains("limit") == false) {
// add a (high) limit to avoid warnings on default limit
command += " | limit 10000000";
}
- Settings pragmas = Settings.EMPTY;
- if (Build.current().isSnapshot()) {
- Settings.Builder settings = Settings.builder();
- if (randomBoolean()) {
- settings.put("page_size", between(1, 5));
- }
- if (randomBoolean()) {
- settings.put("exchange_buffer_size", between(1, 2));
- }
- if (randomBoolean()) {
- settings.put("data_partitioning", randomFrom("shard", "segment", "doc"));
- }
- if (randomBoolean()) {
- settings.put("enrich_max_workers", between(1, 5));
- }
- pragmas = settings.build();
- }
- XContentBuilder query = JsonXContent.contentBuilder();
- query.startObject();
- query.field("query", command);
- if (pragmas != Settings.EMPTY) {
- query.startObject("pragma");
- query.value(pragmas);
- query.endObject();
- }
- query.endObject();
+ XContentBuilder json = JsonXContent.contentBuilder();
+ json.startObject();
+ json.field("query", command);
+ addRandomPragmas(json);
+ json.endObject();
Request request = new Request("POST", "_query");
- request.setJsonEntity("{\"query\":\"" + command + "\"}");
+ request.setJsonEntity(Strings.toString(json));
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user));
return client().performRequest(request);
}
+ static void addRandomPragmas(XContentBuilder builder) throws IOException {
+ if (Build.current().isSnapshot()) {
+ Settings pragmas = randomPragmas();
+ if (pragmas != Settings.EMPTY) {
+ builder.startObject("pragma");
+ builder.value(pragmas);
+ builder.endObject();
+ }
+ }
+ }
+
+ static Settings randomPragmas() {
+ Settings.Builder settings = Settings.builder();
+ if (randomBoolean()) {
+ settings.put("page_size", between(1, 5));
+ }
+ if (randomBoolean()) {
+ settings.put("exchange_buffer_size", between(1, 2));
+ }
+ if (randomBoolean()) {
+ settings.put("data_partitioning", randomFrom("shard", "segment", "doc"));
+ }
+ if (randomBoolean()) {
+ settings.put("enrich_max_workers", between(1, 5));
+ }
+ return settings.build();
+ }
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlAsyncGetResultAction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlAsyncGetResultAction.java
index 1603dd8fd3746..f6593dccb9c49 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlAsyncGetResultAction.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlAsyncGetResultAction.java
@@ -8,12 +8,13 @@
package org.elasticsearch.xpack.esql.action;
import org.elasticsearch.action.ActionType;
+import org.elasticsearch.xpack.core.esql.EsqlAsyncActionNames;
public class EsqlAsyncGetResultAction extends ActionType {
public static final EsqlAsyncGetResultAction INSTANCE = new EsqlAsyncGetResultAction();
- public static final String NAME = "indices:data/read/esql/async/get";
+ public static final String NAME = EsqlAsyncActionNames.ESQL_ASYNC_GET_RESULT_ACTION_NAME;
private EsqlAsyncGetResultAction() {
super(NAME, in -> { throw new IllegalArgumentException("can't transport EsqlAsyncGetResultAction"); });
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java
index f92252ebe851c..1b1d7c789b21d 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java
@@ -43,6 +43,7 @@
import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.core.async.TransportDeleteAsyncResultAction;
import org.elasticsearch.xpack.core.eql.EqlAsyncActionNames;
+import org.elasticsearch.xpack.core.esql.EsqlAsyncActionNames;
import org.elasticsearch.xpack.core.search.action.GetAsyncSearchAction;
import org.elasticsearch.xpack.core.search.action.SubmitAsyncSearchAction;
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyAction;
@@ -963,6 +964,7 @@ private static boolean isAsyncRelatedAction(String action) {
|| action.equals(GetAsyncSearchAction.NAME)
|| action.equals(TransportDeleteAsyncResultAction.TYPE.name())
|| action.equals(EqlAsyncActionNames.EQL_ASYNC_GET_RESULT_ACTION_NAME)
+ || action.equals(EsqlAsyncActionNames.ESQL_ASYNC_GET_RESULT_ACTION_NAME)
|| action.equals(SqlAsyncActionNames.SQL_ASYNC_GET_RESULT_ACTION_NAME);
}
From 73f537170b70e0191f607902e0aa3ad124950abe Mon Sep 17 00:00:00 2001
From: Benjamin Trent
Date: Wed, 10 Jan 2024 07:46:42 -0500
Subject: [PATCH 03/75] Update nested knn search documentation about inner-hits
(#104154)
Adding a link tag for inner hits behavior and kNN search. Additionally
adding a note that if you are using multiple knn clauses, that the inner
hit name should be provided.
---
docs/reference/search/search-your-data/knn-search.asciidoc | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/docs/reference/search/search-your-data/knn-search.asciidoc b/docs/reference/search/search-your-data/knn-search.asciidoc
index 496e0cf1b9d4f..a847d9a306b7c 100644
--- a/docs/reference/search/search-your-data/knn-search.asciidoc
+++ b/docs/reference/search/search-your-data/knn-search.asciidoc
@@ -814,12 +814,19 @@ Now we have filtered based on the top level `"creation_time"` and only one docum
----
// TESTRESPONSE[s/"took": 4/"took" : "$body.took"/]
+[discrete]
+[[nested-knn-search-inner-hits]]
+==== Nested kNN Search with Inner hits
+
Additionally, if you wanted to extract the nearest passage for a matched document, you can supply <>
to the `knn` clause.
NOTE: `inner_hits` for kNN will only ever return a single hit, the nearest passage vector.
Setting `"size"` to any value greater than `1` will have no effect on the results.
+NOTE: When using `inner_hits` and multiple `knn` clauses, be sure to specify the <>
+field. Otherwise, a naming clash can occur and fail the search request.
+
[source,console]
----
POST passage_vectors/_search
From 1d7d9dc1eed2aeda08dd7a62a8a1893c5113a95b Mon Sep 17 00:00:00 2001
From: Armin Braun
Date: Wed, 10 Jan 2024 14:53:35 +0100
Subject: [PATCH 04/75] Refactor RollupResponseTranslator slightly to ease
ref-counting (#104196)
Work with the combined MSearchResponse here throughout to make
ref-counting the full response-array in one possible.
---
.../rollup/RollupResponseTranslator.java | 11 +-
.../action/TransportRollupSearchAction.java | 8 +-
.../RollupResponseTranslationTests.java | 345 ++++++++++--------
.../job/RollupIndexerIndexingTests.java | 3 +-
4 files changed, 211 insertions(+), 156 deletions(-)
diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java
index ed3a3f294c65c..f7394ec12a779 100644
--- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java
+++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java
@@ -73,10 +73,10 @@ public static SearchResponse verifyResponse(MultiSearchResponse.Item normalRespo
* on the translation conventions
*/
public static SearchResponse translateResponse(
- MultiSearchResponse.Item[] rolledMsearch,
+ MultiSearchResponse mSearchResponse,
AggregationReduceContext.Builder reduceContextBuilder
) throws Exception {
-
+ var rolledMsearch = mSearchResponse.getResponses();
assert rolledMsearch.length > 0;
List responses = new ArrayList<>();
for (MultiSearchResponse.Item item : rolledMsearch) {
@@ -199,13 +199,13 @@ public static SearchResponse translateResponse(
* so that the final product looks like a regular aggregation response, allowing it to be
* reduced/merged into the response from the un-rolled index
*
- * @param msearchResponses The responses from the msearch, where the first response is the live-index response
+ * @param mSearchResponse The response from the msearch, where the first response is the live-index response
*/
public static SearchResponse combineResponses(
- MultiSearchResponse.Item[] msearchResponses,
+ MultiSearchResponse mSearchResponse,
AggregationReduceContext.Builder reduceContextBuilder
) throws Exception {
-
+ var msearchResponses = mSearchResponse.getResponses();
assert msearchResponses.length >= 2;
boolean first = true;
@@ -242,6 +242,7 @@ public static SearchResponse combineResponses(
// If we only have a live index left, just return it directly. We know it can't be an error already
if (rolledResponses.isEmpty() && liveResponse != null) {
+ liveResponse.mustIncRef();
return liveResponse;
} else if (rolledResponses.isEmpty()) {
throw new ResourceNotFoundException("No indices (live or rollup) found during rollup search");
diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java
index ff167c5586dce..2df415fbe02dc 100644
--- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java
+++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java
@@ -154,14 +154,16 @@ static SearchResponse processResponses(
) throws Exception {
if (rollupContext.hasLiveIndices() && rollupContext.hasRollupIndices()) {
// Both
- return RollupResponseTranslator.combineResponses(msearchResponse.getResponses(), reduceContextBuilder);
+ return RollupResponseTranslator.combineResponses(msearchResponse, reduceContextBuilder);
} else if (rollupContext.hasLiveIndices()) {
// Only live
assert msearchResponse.getResponses().length == 1;
- return RollupResponseTranslator.verifyResponse(msearchResponse.getResponses()[0]);
+ var res = RollupResponseTranslator.verifyResponse(msearchResponse.getResponses()[0]);
+ res.mustIncRef();
+ return res;
} else if (rollupContext.hasRollupIndices()) {
// Only rollup
- return RollupResponseTranslator.translateResponse(msearchResponse.getResponses(), reduceContextBuilder);
+ return RollupResponseTranslator.translateResponse(msearchResponse, reduceContextBuilder);
}
throw new RuntimeException("MSearch response was empty, cannot unroll RollupSearch results");
}
diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java
index 7e814230a2223..e9f882731521f 100644
--- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java
+++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java
@@ -43,6 +43,7 @@
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.search.DocValueFormat;
+import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationReduceContext;
import org.elasticsearch.search.aggregations.Aggregations;
@@ -91,56 +92,70 @@
public class RollupResponseTranslationTests extends AggregatorTestCase {
public void testLiveFailure() {
- MultiSearchResponse.Item[] failure = new MultiSearchResponse.Item[] {
- new MultiSearchResponse.Item(null, new RuntimeException("foo")),
- new MultiSearchResponse.Item(null, null) };
-
- Exception e = expectThrows(
- RuntimeException.class,
- () -> RollupResponseTranslator.combineResponses(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
+ MultiSearchResponse failure = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] {
+ new MultiSearchResponse.Item(null, new RuntimeException("foo")),
+ new MultiSearchResponse.Item(null, null) },
+ 0L
);
- assertThat(e.getMessage(), equalTo("foo"));
+ try {
+ Exception e = expectThrows(
+ RuntimeException.class,
+ () -> RollupResponseTranslator.combineResponses(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(e.getMessage(), equalTo("foo"));
- e = expectThrows(
- RuntimeException.class,
- () -> RollupResponseTranslator.translateResponse(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
- );
- assertThat(e.getMessage(), equalTo("foo"));
+ e = expectThrows(
+ RuntimeException.class,
+ () -> RollupResponseTranslator.translateResponse(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(e.getMessage(), equalTo("foo"));
- e = expectThrows(RuntimeException.class, () -> RollupResponseTranslator.verifyResponse(failure[0]));
- assertThat(e.getMessage(), equalTo("foo"));
+ e = expectThrows(RuntimeException.class, () -> RollupResponseTranslator.verifyResponse(failure.getResponses()[0]));
+ assertThat(e.getMessage(), equalTo("foo"));
+ } finally {
+ failure.decRef();
+ }
}
public void testRollupFailure() {
- MultiSearchResponse.Item[] failure = new MultiSearchResponse.Item[] {
- new MultiSearchResponse.Item(null, new RuntimeException("rollup failure")) };
-
- Exception e = expectThrows(
- RuntimeException.class,
- () -> RollupResponseTranslator.translateResponse(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
+ MultiSearchResponse failure = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] { new MultiSearchResponse.Item(null, new RuntimeException("rollup failure")) },
+ 0L
);
- assertThat(e.getMessage(), equalTo("rollup failure"));
+ try {
+ Exception e = expectThrows(
+ RuntimeException.class,
+ () -> RollupResponseTranslator.translateResponse(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(e.getMessage(), equalTo("rollup failure"));
+ } finally {
+ failure.decRef();
+ }
}
public void testLiveMissingRollupMissing() {
- MultiSearchResponse.Item[] failure = new MultiSearchResponse.Item[] {
- new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")),
- new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")) };
-
- BigArrays bigArrays = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService());
- ScriptService scriptService = mock(ScriptService.class);
-
- ResourceNotFoundException e = expectThrows(
- ResourceNotFoundException.class,
- () -> RollupResponseTranslator.combineResponses(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
- );
- assertThat(
- e.getMessage(),
- equalTo(
- "Index [[foo]] was not found, likely because it was deleted while the request was in-flight. "
- + "Rollup does not support partial search results, please try the request again."
- )
+ MultiSearchResponse failure = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] {
+ new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")),
+ new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")) },
+ 0L
);
+ try {
+ ResourceNotFoundException e = expectThrows(
+ ResourceNotFoundException.class,
+ () -> RollupResponseTranslator.combineResponses(failure, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(
+ e.getMessage(),
+ equalTo(
+ "Index [[foo]] was not found, likely because it was deleted while the request was in-flight. "
+ + "Rollup does not support partial search results, please try the request again."
+ )
+ );
+ } finally {
+ failure.decRef();
+ }
}
public void testMissingLiveIndex() throws Exception {
@@ -175,21 +190,27 @@ public void testMissingLiveIndex() throws Exception {
Aggregations mockAggsWithout = InternalAggregations.from(aggTree);
when(responseWithout.getAggregations()).thenReturn(mockAggsWithout);
- MultiSearchResponse.Item[] msearch = new MultiSearchResponse.Item[] {
- new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")),
- new MultiSearchResponse.Item(responseWithout, null) };
-
- ResourceNotFoundException e = expectThrows(
- ResourceNotFoundException.class,
- () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
- );
- assertThat(
- e.getMessage(),
- equalTo(
- "Index [[foo]] was not found, likely because it was deleted while the request was in-flight. "
- + "Rollup does not support partial search results, please try the request again."
- )
+ MultiSearchResponse msearch = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] {
+ new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")),
+ new MultiSearchResponse.Item(responseWithout, null) },
+ 0L
);
+ try {
+ ResourceNotFoundException e = expectThrows(
+ ResourceNotFoundException.class,
+ () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(
+ e.getMessage(),
+ equalTo(
+ "Index [[foo]] was not found, likely because it was deleted while the request was in-flight. "
+ + "Rollup does not support partial search results, please try the request again."
+ )
+ );
+ } finally {
+ msearch.decRef();
+ }
}
public void testRolledMissingAggs() throws Exception {
@@ -198,43 +219,52 @@ public void testRolledMissingAggs() throws Exception {
when(responseWithout.getAggregations()).thenReturn(InternalAggregations.EMPTY);
- MultiSearchResponse.Item[] msearch = new MultiSearchResponse.Item[] { new MultiSearchResponse.Item(responseWithout, null) };
-
- SearchResponse response = RollupResponseTranslator.translateResponse(
- msearch,
- InternalAggregationTestCase.emptyReduceContextBuilder()
+ MultiSearchResponse msearch = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] { new MultiSearchResponse.Item(responseWithout, null) },
+ 0L
);
try {
- assertNotNull(response);
- Aggregations responseAggs = response.getAggregations();
- assertThat(responseAggs.asList().size(), equalTo(0));
+ SearchResponse response = RollupResponseTranslator.translateResponse(
+ msearch,
+ InternalAggregationTestCase.emptyReduceContextBuilder()
+ );
+ try {
+ assertNotNull(response);
+ Aggregations responseAggs = response.getAggregations();
+ assertThat(responseAggs.asList().size(), equalTo(0));
+ } finally {
+ // this SearchResponse is not a mock, so must be decRef'd
+ response.decRef();
+ }
} finally {
- // this SearchResponse is not a mock, so must be decRef'd
- response.decRef();
+ msearch.decRef();
}
}
public void testMissingRolledIndex() {
SearchResponse response = mock(SearchResponse.class);
- MultiSearchResponse.Item[] msearch = new MultiSearchResponse.Item[] {
- new MultiSearchResponse.Item(response, null),
- new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")) };
-
- BigArrays bigArrays = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService());
- ScriptService scriptService = mock(ScriptService.class);
-
- ResourceNotFoundException e = expectThrows(
- ResourceNotFoundException.class,
- () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
- );
- assertThat(
- e.getMessage(),
- equalTo(
- "Index [[foo]] was not found, likely because it was deleted while the request was in-flight. "
- + "Rollup does not support partial search results, please try the request again."
- )
+ MultiSearchResponse msearch = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] {
+ new MultiSearchResponse.Item(response, null),
+ new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")) },
+ 0L
);
+ try {
+ ResourceNotFoundException e = expectThrows(
+ ResourceNotFoundException.class,
+ () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(
+ e.getMessage(),
+ equalTo(
+ "Index [[foo]] was not found, likely because it was deleted while the request was in-flight. "
+ + "Rollup does not support partial search results, please try the request again."
+ )
+ );
+ } finally {
+ msearch.decRef();
+ }
}
public void testVerifyNormal() throws Exception {
@@ -283,41 +313,50 @@ public void testTranslateRollup() throws Exception {
Aggregations mockAggs = InternalAggregations.from(aggTree);
when(response.getAggregations()).thenReturn(mockAggs);
- MultiSearchResponse.Item item = new MultiSearchResponse.Item(response, null);
-
- // this is not a mock, so needs to be decRef'd
- SearchResponse finalResponse = RollupResponseTranslator.translateResponse(
- new MultiSearchResponse.Item[] { item },
- InternalAggregationTestCase.emptyReduceContextBuilder()
+ MultiSearchResponse multiSearchResponse = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] { new MultiSearchResponse.Item(response, null) },
+ 0L
);
try {
- assertNotNull(finalResponse);
- Aggregations responseAggs = finalResponse.getAggregations();
- assertNotNull(finalResponse);
- Avg avg = responseAggs.get("foo");
- assertThat(avg.getValue(), equalTo(5.0));
+ // this is not a mock, so needs to be decRef'd
+ SearchResponse finalResponse = RollupResponseTranslator.translateResponse(
+ multiSearchResponse,
+ InternalAggregationTestCase.emptyReduceContextBuilder()
+ );
+ try {
+ assertNotNull(finalResponse);
+ Aggregations responseAggs = finalResponse.getAggregations();
+ assertNotNull(finalResponse);
+ Avg avg = responseAggs.get("foo");
+ assertThat(avg.getValue(), equalTo(5.0));
+ } finally {
+ finalResponse.decRef();
+ }
} finally {
- finalResponse.decRef();
+ multiSearchResponse.decRef();
}
}
public void testTranslateMissingRollup() {
- MultiSearchResponse.Item missing = new MultiSearchResponse.Item(null, new IndexNotFoundException("foo"));
-
- ResourceNotFoundException e = expectThrows(
- ResourceNotFoundException.class,
- () -> RollupResponseTranslator.translateResponse(
- new MultiSearchResponse.Item[] { missing },
- InternalAggregationTestCase.emptyReduceContextBuilder()
- )
- );
- assertThat(
- e.getMessage(),
- equalTo(
- "Index [foo] was not found, likely because it was deleted while the request was in-flight. "
- + "Rollup does not support partial search results, please try the request again."
- )
+ MultiSearchResponse missing = new MultiSearchResponse(
+ new MultiSearchResponse.Item[] { new MultiSearchResponse.Item(null, new IndexNotFoundException("foo")) },
+ 0L
);
+ try {
+ ResourceNotFoundException e = expectThrows(
+ ResourceNotFoundException.class,
+ () -> RollupResponseTranslator.translateResponse(missing, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(
+ e.getMessage(),
+ equalTo(
+ "Index [foo] was not found, likely because it was deleted while the request was in-flight. "
+ + "Rollup does not support partial search results, please try the request again."
+ )
+ );
+ } finally {
+ missing.decRef();
+ }
}
public void testMissingFilter() {
@@ -339,13 +378,16 @@ public void testMissingFilter() {
when(responseWithout.getAggregations()).thenReturn(mockAggsWithout);
MultiSearchResponse.Item rolledResponse = new MultiSearchResponse.Item(responseWithout, null);
- MultiSearchResponse.Item[] msearch = new MultiSearchResponse.Item[] { unrolledResponse, rolledResponse };
-
- Exception e = expectThrows(
- RuntimeException.class,
- () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
- );
- assertThat(e.getMessage(), containsString("Expected [bizzbuzz] to be a FilterAggregation"));
+ MultiSearchResponse msearch = new MultiSearchResponse(new MultiSearchResponse.Item[] { unrolledResponse, rolledResponse }, 0L);
+ try {
+ Exception e = expectThrows(
+ RuntimeException.class,
+ () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(e.getMessage(), containsString("Expected [bizzbuzz] to be a FilterAggregation"));
+ } finally {
+ msearch.decRef();
+ }
}
public void testMatchingNameNotFilter() {
@@ -366,13 +408,16 @@ public void testMatchingNameNotFilter() {
when(responseWithout.getAggregations()).thenReturn(mockAggsWithout);
MultiSearchResponse.Item rolledResponse = new MultiSearchResponse.Item(responseWithout, null);
- MultiSearchResponse.Item[] msearch = new MultiSearchResponse.Item[] { unrolledResponse, rolledResponse };
-
- Exception e = expectThrows(
- RuntimeException.class,
- () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
- );
- assertThat(e.getMessage(), equalTo("Expected [filter_foo] to be a FilterAggregation, but was [Max]"));
+ MultiSearchResponse msearch = new MultiSearchResponse(new MultiSearchResponse.Item[] { unrolledResponse, rolledResponse }, 0L);
+ try {
+ Exception e = expectThrows(
+ RuntimeException.class,
+ () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(e.getMessage(), equalTo("Expected [filter_foo] to be a FilterAggregation, but was [Max]"));
+ } finally {
+ msearch.decRef();
+ }
}
public void testSimpleReduction() throws Exception {
@@ -417,24 +462,27 @@ public void testSimpleReduction() throws Exception {
when(responseWithout.getAggregations()).thenReturn(mockAggsWithout);
MultiSearchResponse.Item rolledResponse = new MultiSearchResponse.Item(responseWithout, null);
- MultiSearchResponse.Item[] msearch = new MultiSearchResponse.Item[] { unrolledResponse, rolledResponse };
-
- // this SearchResponse is not a mock, so needs a decRef
- SearchResponse response = RollupResponseTranslator.combineResponses(
- msearch,
- InternalAggregationTestCase.emptyReduceContextBuilder(
- new AggregatorFactories.Builder().addAggregator(new MaxAggregationBuilder("foo"))
- .addAggregator(new MaxAggregationBuilder("foo." + RollupField.COUNT_FIELD))
- )
- );
+ MultiSearchResponse msearch = new MultiSearchResponse(new MultiSearchResponse.Item[] { unrolledResponse, rolledResponse }, 0L);
try {
- assertNotNull(response);
- Aggregations responseAggs = response.getAggregations();
- assertNotNull(responseAggs);
- Avg avg = responseAggs.get("foo");
- assertThat(avg.getValue(), equalTo(5.0));
+ // this SearchResponse is not a mock, so needs a decRef
+ SearchResponse response = RollupResponseTranslator.combineResponses(
+ msearch,
+ InternalAggregationTestCase.emptyReduceContextBuilder(
+ new AggregatorFactories.Builder().addAggregator(new MaxAggregationBuilder("foo"))
+ .addAggregator(new MaxAggregationBuilder("foo." + RollupField.COUNT_FIELD))
+ )
+ );
+ try {
+ assertNotNull(response);
+ Aggregations responseAggs = response.getAggregations();
+ assertNotNull(responseAggs);
+ Avg avg = responseAggs.get("foo");
+ assertThat(avg.getValue(), equalTo(5.0));
+ } finally {
+ response.decRef();
+ }
} finally {
- response.decRef();
+ msearch.decRef();
}
}
@@ -515,7 +563,7 @@ public void testMismatch() throws IOException {
// TODO SearchResponse.Clusters is not public, using null for now. Should fix upstream.
MultiSearchResponse.Item unrolledItem = new MultiSearchResponse.Item(
new SearchResponse(
- null,
+ SearchHits.EMPTY_WITH_TOTAL_HITS,
InternalAggregations.from(Collections.singletonList(responses.get(0))),
null,
false,
@@ -534,7 +582,7 @@ public void testMismatch() throws IOException {
);
MultiSearchResponse.Item rolledItem = new MultiSearchResponse.Item(
new SearchResponse(
- null,
+ SearchHits.EMPTY_WITH_TOTAL_HITS,
InternalAggregations.from(Collections.singletonList(responses.get(1))),
null,
false,
@@ -552,14 +600,17 @@ public void testMismatch() throws IOException {
null
);
- MultiSearchResponse.Item[] msearch = new MultiSearchResponse.Item[] { unrolledItem, rolledItem };
-
- ClassCastException e = expectThrows(
- ClassCastException.class,
- () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
- );
- assertThat(e.getMessage(), containsString("org.elasticsearch.search.aggregations.metrics.InternalGeoBounds"));
- assertThat(e.getMessage(), containsString("org.elasticsearch.search.aggregations.InternalMultiBucketAggregation"));
+ MultiSearchResponse msearch = new MultiSearchResponse(new MultiSearchResponse.Item[] { unrolledItem, rolledItem }, 0);
+ try {
+ ClassCastException e = expectThrows(
+ ClassCastException.class,
+ () -> RollupResponseTranslator.combineResponses(msearch, InternalAggregationTestCase.emptyReduceContextBuilder())
+ );
+ assertThat(e.getMessage(), containsString("org.elasticsearch.search.aggregations.metrics.InternalGeoBounds"));
+ assertThat(e.getMessage(), containsString("org.elasticsearch.search.aggregations.InternalMultiBucketAggregation"));
+ } finally {
+ msearch.decRef();
+ }
}
public void testDateHisto() throws IOException {
diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java
index 1e6a4794b14ae..e2cb5a5bc61b0 100644
--- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java
+++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java
@@ -43,6 +43,7 @@
import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.index.query.SearchExecutionContextHelper;
import org.elasticsearch.script.ScriptCompiler;
+import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.AggregatorTestCase;
import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation;
@@ -868,7 +869,7 @@ protected void doNextSearch(long waitTimeInNanos, ActionListener
ActionListener.respondAndRelease(
listener,
new SearchResponse(
- null,
+ SearchHits.EMPTY_WITH_TOTAL_HITS,
new Aggregations(Collections.singletonList(result)),
null,
false,
From 312d4c2fa172ae8181ea2e37d09c96d051d110a8 Mon Sep 17 00:00:00 2001
From: David Turner
Date: Wed, 10 Jan 2024 13:54:07 +0000
Subject: [PATCH 05/75] Mention `IndexFormatToo{Old,New}Exception` as
corruption (#104204)
If a file header is corrupted then the exception may be reported as a
bad index format version rather than a checksum mismatch. This commit
adjusts the docs to cover this case.
---
.../troubleshooting/corruption-issues.asciidoc | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/docs/reference/troubleshooting/corruption-issues.asciidoc b/docs/reference/troubleshooting/corruption-issues.asciidoc
index 4a245daba0904..15897fe8fb3bb 100644
--- a/docs/reference/troubleshooting/corruption-issues.asciidoc
+++ b/docs/reference/troubleshooting/corruption-issues.asciidoc
@@ -38,6 +38,13 @@ well-tested, so you can be very confident that a checksum mismatch really does
indicate that the data read from disk is different from the data that {es}
previously wrote.
+If a file header is corrupted then it's possible that {es} might not be able
+to work out how to even start reading the file which can lead to an exception
+such as:
+
+- `org.apache.lucene.index.IndexFormatTooOldException`
+- `org.apache.lucene.index.IndexFormatTooNewException`
+
It is also possible that {es} reports a corruption if a file it needs is
entirely missing, with an exception such as:
@@ -50,8 +57,7 @@ system previously confirmed to {es} that this file was durably synced to disk.
On Linux this means that the `fsync()` system call returned successfully. {es}
sometimes reports that an index is corrupt because a file needed for recovery
is missing, or it exists but has been truncated or is missing its footer. This
-indicates that your storage system acknowledges durable writes incorrectly or
-that some external process has modified the data {es} previously wrote to disk.
+may indicate that your storage system acknowledges durable writes incorrectly.
There are many possible explanations for {es} detecting corruption in your
cluster. Databases like {es} generate a challenging I/O workload that may find
From 9ca3be0ea745b1a0e47e15a48fa2c41155466977 Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Wed, 10 Jan 2024 09:02:28 -0500
Subject: [PATCH 06/75] [ML] Adding internal inference input type field
(#104153)
* Adding input type
* Working tests
---
.../org/elasticsearch/TransportVersions.java | 1 +
.../elasticsearch/inference/InputType.java | 40 +++++++++++++
.../inference/action/InferenceAction.java | 24 ++++++--
.../ml/action/CoordinatedInferenceAction.java | 19 +++++-
...oordinatedInferenceActionRequestTests.java | 53 ++++++++++++++++-
.../action/InferModelActionRequestTests.java | 47 ++++++++-------
.../action/InferenceActionRequestTests.java | 58 ++++++++++++++++---
.../TransportCoordinatedInferenceAction.java | 25 +++++++-
...nsportCoordinatedInferenceActionTests.java | 37 ++++++++++++
9 files changed, 265 insertions(+), 39 deletions(-)
create mode 100644 server/src/main/java/org/elasticsearch/inference/InputType.java
create mode 100644 x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportCoordinatedInferenceActionTests.java
diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java
index 76fd9d077e2e7..f289a7a3c89a1 100644
--- a/server/src/main/java/org/elasticsearch/TransportVersions.java
+++ b/server/src/main/java/org/elasticsearch/TransportVersions.java
@@ -181,6 +181,7 @@ static TransportVersion def(int id) {
public static final TransportVersion LAZY_ROLLOVER_ADDED = def(8_569_00_0);
public static final TransportVersion ESQL_PLAN_POINT_LITERAL_WKB = def(8_570_00_0);
public static final TransportVersion HOT_THREADS_AS_BYTES = def(8_571_00_0);
+ public static final TransportVersion ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED = def(8_572_00_0);
/*
* STOP! READ THIS FIRST! No, really,
diff --git a/server/src/main/java/org/elasticsearch/inference/InputType.java b/server/src/main/java/org/elasticsearch/inference/InputType.java
new file mode 100644
index 0000000000000..f8bbea4ae121f
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/inference/InputType.java
@@ -0,0 +1,40 @@
+/*
+ * 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.inference;
+
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * Defines the type of request, whether the request is to ingest a document or search for a document.
+ */
+public enum InputType implements Writeable {
+ INGEST,
+ SEARCH;
+
+ public static String NAME = "input_type";
+
+ @Override
+ public String toString() {
+ return name().toLowerCase(Locale.ROOT);
+ }
+
+ public static InputType fromStream(StreamInput in) throws IOException {
+ return in.readEnum(InputType.class);
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeEnum(this);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java
index a1eabb682c98f..732bc3d66bedc 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java
@@ -17,6 +17,7 @@
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.inference.InferenceResults;
import org.elasticsearch.inference.InferenceServiceResults;
+import org.elasticsearch.inference.InputType;
import org.elasticsearch.inference.TaskType;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.xcontent.ObjectParser;
@@ -66,12 +67,14 @@ public static Request parseRequest(String modelId, String taskType, XContentPars
private final String modelId;
private final List input;
private final Map taskSettings;
+ private final InputType inputType;
- public Request(TaskType taskType, String modelId, List input, Map taskSettings) {
+ public Request(TaskType taskType, String modelId, List input, Map taskSettings, InputType inputType) {
this.taskType = taskType;
this.modelId = modelId;
this.input = input;
this.taskSettings = taskSettings;
+ this.inputType = inputType;
}
public Request(StreamInput in) throws IOException {
@@ -84,6 +87,11 @@ public Request(StreamInput in) throws IOException {
this.input = List.of(in.readString());
}
this.taskSettings = in.readGenericMap();
+ if (in.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED)) {
+ this.inputType = InputType.fromStream(in);
+ } else {
+ this.inputType = InputType.INGEST;
+ }
}
public TaskType getTaskType() {
@@ -102,6 +110,10 @@ public Map getTaskSettings() {
return taskSettings;
}
+ public InputType getInputType() {
+ return inputType;
+ }
+
@Override
public ActionRequestValidationException validate() {
if (input == null) {
@@ -128,6 +140,9 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeString(input.get(0));
}
out.writeGenericMap(taskSettings);
+ if (out.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED)) {
+ inputType.writeTo(out);
+ }
}
@Override
@@ -138,12 +153,13 @@ public boolean equals(Object o) {
return taskType == request.taskType
&& Objects.equals(modelId, request.modelId)
&& Objects.equals(input, request.input)
- && Objects.equals(taskSettings, request.taskSettings);
+ && Objects.equals(taskSettings, request.taskSettings)
+ && Objects.equals(inputType, request.inputType);
}
@Override
public int hashCode() {
- return Objects.hash(taskType, modelId, input, taskSettings);
+ return Objects.hash(taskType, modelId, input, taskSettings, inputType);
}
public static class Builder {
@@ -181,7 +197,7 @@ public Builder setTaskSettings(Map taskSettings) {
}
public Request build() {
- return new Request(taskType, modelId, input, taskSettings);
+ return new Request(taskType, modelId, input, taskSettings, InputType.INGEST);
}
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CoordinatedInferenceAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CoordinatedInferenceAction.java
index 03270e0dda0f7..7af3d1a150ac8 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CoordinatedInferenceAction.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CoordinatedInferenceAction.java
@@ -7,6 +7,7 @@
package org.elasticsearch.xpack.core.ml.action;
+import org.elasticsearch.TransportVersions;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.ActionType;
@@ -98,7 +99,8 @@ public static Request forMapInput(
// DFA models only
private final List
*/
@Override public T visitEnrichWithClause(EsqlBaseParser.EnrichWithClauseContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitSetting(EsqlBaseParser.SettingContext ctx) { return visitChildren(ctx); }
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java
index 6c8cd7272d8dc..89c2e39b65f8d 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java
@@ -775,4 +775,14 @@ public interface EsqlBaseParserListener extends ParseTreeListener {
* @param ctx the parse tree
*/
void exitEnrichWithClause(EsqlBaseParser.EnrichWithClauseContext ctx);
+ /**
+ * Enter a parse tree produced by {@link EsqlBaseParser#setting}.
+ * @param ctx the parse tree
+ */
+ void enterSetting(EsqlBaseParser.SettingContext ctx);
+ /**
+ * Exit a parse tree produced by {@link EsqlBaseParser#setting}.
+ * @param ctx the parse tree
+ */
+ void exitSetting(EsqlBaseParser.SettingContext ctx);
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java
index 2fe5de566dbaf..0fc4fecc4a2df 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java
@@ -465,4 +465,10 @@ public interface EsqlBaseParserVisitor extends ParseTreeVisitor {
* @return the visitor result
*/
T visitEnrichWithClause(EsqlBaseParser.EnrichWithClauseContext ctx);
+ /**
+ * Visit a parse tree produced by {@link EsqlBaseParser#setting}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitSetting(EsqlBaseParser.SettingContext ctx);
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java
index 2039dc633f6cf..7541326c172ef 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java
@@ -25,7 +25,7 @@ public String visitIdentifier(IdentifierContext ctx) {
@Override
public String visitIdentifierPattern(EsqlBaseParser.IdentifierPatternContext ctx) {
- return unquoteIdentifier(ctx.QUOTED_IDENTIFIER(), ctx.PROJECT_UNQUOTED_IDENTIFIER());
+ return unquoteIdentifier(ctx.QUOTED_IDENTIFIER(), ctx.UNQUOTED_ID_PATTERN());
}
@Override
@@ -33,7 +33,7 @@ public String visitFromIdentifier(FromIdentifierContext ctx) {
return ctx == null ? null : unquoteIdentifier(ctx.QUOTED_IDENTIFIER(), ctx.FROM_UNQUOTED_IDENTIFIER());
}
- static String unquoteIdentifier(TerminalNode quotedNode, TerminalNode unquotedNode) {
+ protected static String unquoteIdentifier(TerminalNode quotedNode, TerminalNode unquotedNode) {
String result;
if (quotedNode != null) {
String identifier = quotedNode.getText();
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java
index f9d1a252afe42..5e90f6e8e44c9 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java
@@ -54,11 +54,13 @@
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import static org.elasticsearch.common.logging.HeaderWarning.addWarning;
+import static org.elasticsearch.xpack.esql.plan.logical.Enrich.Mode;
import static org.elasticsearch.xpack.ql.parser.ParserUtils.source;
import static org.elasticsearch.xpack.ql.parser.ParserUtils.typedParsing;
import static org.elasticsearch.xpack.ql.parser.ParserUtils.visitList;
@@ -311,21 +313,20 @@ public LogicalPlan visitShowFunctions(EsqlBaseParser.ShowFunctionsContext ctx) {
@Override
public PlanFactory visitEnrichCommand(EsqlBaseParser.EnrichCommandContext ctx) {
return p -> {
- String policyName = visitFromIdentifier(ctx.policyName);
+ String policyName = ctx.policyName.getText();
var source = source(ctx);
+ Mode mode = enrichMode(ctx.setting());
+
NamedExpression matchField = ctx.ON() != null ? visitQualifiedNamePattern(ctx.matchField) : new EmptyAttribute(source);
if (matchField.name().contains("*")) {
- throw new ParsingException(
- source(ctx),
- "Using wildcards (*) in ENRICH WITH projections is not allowed [{}]",
- matchField.name()
- );
+ throw new ParsingException(source, "Using wildcards (*) in ENRICH WITH projections is not allowed [{}]", matchField.name());
}
List keepClauses = visitList(this, ctx.enrichWithClause(), NamedExpression.class);
return new Enrich(
source,
p,
+ mode,
new Literal(source(ctx.policyName), policyName, DataTypes.KEYWORD),
matchField,
null,
@@ -334,5 +335,35 @@ public PlanFactory visitEnrichCommand(EsqlBaseParser.EnrichCommandContext ctx) {
};
}
+ private Mode enrichMode(List setting) {
+ if (setting == null || setting.isEmpty()) {
+ return null;
+ }
+ var s = setting.get(0);
+ var source = source(s);
+ if (setting.size() > 1) {
+ throw new ParsingException(source, "Only one setting allowed for now in ENRICH");
+ }
+ String mode = "ccq.mode";
+
+ var nameText = s.name.getText();
+ if (mode.equals(nameText.toLowerCase(Locale.ROOT)) == false) {
+ throw new ParsingException(source(s.name), "Unsupported setting [{}], expected [{}]", nameText, mode);
+ }
+
+ var valueText = s.value.getText();
+ Enrich.Mode m = Enrich.Mode.from(valueText);
+ if (m == null) {
+ throw new ParsingException(
+ source(s.value),
+ "Unrecognized value [{}], ENRICH [{}] needs to be one of {}",
+ valueText,
+ nameText,
+ Enrich.Mode.values()
+ );
+ }
+ return m;
+ }
+
interface PlanFactory extends Function {}
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Enrich.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Enrich.java
index 1ad73be7902f7..37a0ff0fe5001 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Enrich.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Enrich.java
@@ -7,6 +7,7 @@
package org.elasticsearch.xpack.esql.plan.logical;
+import org.elasticsearch.common.util.Maps;
import org.elasticsearch.xpack.esql.enrich.EnrichPolicyResolution;
import org.elasticsearch.xpack.ql.capabilities.Resolvables;
import org.elasticsearch.xpack.ql.expression.Attribute;
@@ -19,6 +20,8 @@
import org.elasticsearch.xpack.ql.tree.Source;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import static org.elasticsearch.xpack.esql.expression.NamedExpressions.mergeOutputAttributes;
@@ -30,15 +33,39 @@ public class Enrich extends UnaryPlan {
private List enrichFields;
private List output;
+ private final Mode mode;
+
+ public enum Mode {
+ ANY,
+ COORDINATOR,
+ REMOTE;
+
+ private static final Map map;
+
+ static {
+ var values = Mode.values();
+ map = Maps.newMapWithExpectedSize(values.length);
+ for (Mode m : values) {
+ map.put(m.name(), m);
+ }
+ }
+
+ public static Mode from(String name) {
+ return name == null ? null : map.get(name.toUpperCase(Locale.ROOT));
+ }
+ }
+
public Enrich(
Source source,
LogicalPlan child,
+ Mode mode,
Expression policyName,
NamedExpression matchField,
EnrichPolicyResolution policy,
List enrichFields
) {
super(source, child);
+ this.mode = mode == null ? Mode.ANY : mode;
this.policyName = policyName;
this.matchField = matchField;
this.policy = policy;
@@ -61,6 +88,10 @@ public Expression policyName() {
return policyName;
}
+ public Mode mode() {
+ return mode;
+ }
+
@Override
public boolean expressionsResolved() {
return policyName.resolved()
@@ -71,12 +102,12 @@ public boolean expressionsResolved() {
@Override
public UnaryPlan replaceChild(LogicalPlan newChild) {
- return new Enrich(source(), newChild, policyName, matchField, policy, enrichFields);
+ return new Enrich(source(), newChild, mode, policyName, matchField, policy, enrichFields);
}
@Override
protected NodeInfo extends LogicalPlan> info() {
- return NodeInfo.create(this, Enrich::new, child(), policyName, matchField, policy, enrichFields);
+ return NodeInfo.create(this, Enrich::new, child(), mode, policyName, matchField, policy, enrichFields);
}
@Override
@@ -96,7 +127,8 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
if (super.equals(o) == false) return false;
Enrich enrich = (Enrich) o;
- return Objects.equals(policyName, enrich.policyName)
+ return Objects.equals(mode, enrich.mode)
+ && Objects.equals(policyName, enrich.policyName)
&& Objects.equals(matchField, enrich.matchField)
&& Objects.equals(policy, enrich.policy)
&& Objects.equals(enrichFields, enrich.enrichFields);
@@ -104,6 +136,6 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), policyName, matchField, policy, enrichFields);
+ return Objects.hash(super.hashCode(), mode, policyName, matchField, policy, enrichFields);
}
}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java
index b20d166beb22e..931c96a8cb8ed 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java
@@ -676,7 +676,15 @@ public void testLikeRLike() {
public void testEnrich() {
assertEquals(
- new Enrich(EMPTY, PROCESSING_CMD_INPUT, new Literal(EMPTY, "countries", KEYWORD), new EmptyAttribute(EMPTY), null, List.of()),
+ new Enrich(
+ EMPTY,
+ PROCESSING_CMD_INPUT,
+ null,
+ new Literal(EMPTY, "countries", KEYWORD),
+ new EmptyAttribute(EMPTY),
+ null,
+ List.of()
+ ),
processingCommand("enrich countries")
);
@@ -684,12 +692,27 @@ public void testEnrich() {
new Enrich(
EMPTY,
PROCESSING_CMD_INPUT,
+ null,
+ new Literal(EMPTY, "index-policy", KEYWORD),
+ new UnresolvedAttribute(EMPTY, "field_underscore"),
+ null,
+ List.of()
+ ),
+ processingCommand("enrich index-policy ON field_underscore")
+ );
+
+ Enrich.Mode mode = randomFrom(Enrich.Mode.values());
+ assertEquals(
+ new Enrich(
+ EMPTY,
+ PROCESSING_CMD_INPUT,
+ mode,
new Literal(EMPTY, "countries", KEYWORD),
new UnresolvedAttribute(EMPTY, "country_code"),
null,
List.of()
),
- processingCommand("enrich countries ON country_code")
+ processingCommand("enrich [ccq.mode :" + mode.name() + "] countries ON country_code")
);
expectError("from a | enrich countries on foo* ", "Using wildcards (*) in ENRICH WITH projections is not allowed [foo*]");
@@ -702,6 +725,10 @@ public void testEnrich() {
"from a | enrich countries on foo with x* = bar ",
"Using wildcards (*) in ENRICH WITH projections is not allowed [x*]"
);
+ expectError(
+ "from a | enrich [ccq.mode : typo] countries on foo",
+ "line 1:30: Unrecognized value [typo], ENRICH [ccq.mode] needs to be one of [ANY, COORDINATOR, REMOTE]"
+ );
}
public void testMvExpand() {
From f4aaa20f28661143535461b586ef18681561e4d1 Mon Sep 17 00:00:00 2001
From: Albert Zaharovits
Date: Thu, 11 Jan 2024 10:53:50 +0200
Subject: [PATCH 30/75] Add support for the `type` parameter to the Query API
Key API (#103695)
This adds support for the type parameter to the Query API key API.
The type for an API Key can currently be either rest or cross_cluster.
Relates: #101691
---
.../rest-api/security/query-api-key.asciidoc | 5 +
.../xpack/security/QueryApiKeyIT.java | 143 +++++++++++++++++-
.../xpack/security/apikey/ApiKeyRestIT.java | 71 +++++++++
.../apikey/TransportQueryApiKeyAction.java | 30 +++-
.../support/ApiKeyBoolQueryBuilder.java | 43 ++++--
.../support/ApiKeyFieldNameTranslators.java | 3 +
.../support/ApiKeyBoolQueryBuilderTests.java | 142 +++++++++++++----
.../ApiKeyBackwardsCompatibilityIT.java | 51 ++++++-
8 files changed, 436 insertions(+), 52 deletions(-)
diff --git a/docs/reference/rest-api/security/query-api-key.asciidoc b/docs/reference/rest-api/security/query-api-key.asciidoc
index 0e5973a010a47..67b0b7bfac58d 100644
--- a/docs/reference/rest-api/security/query-api-key.asciidoc
+++ b/docs/reference/rest-api/security/query-api-key.asciidoc
@@ -64,6 +64,11 @@ You can query the following public values associated with an API key.
`id`::
ID of the API key. Note `id` must be queried with the <> query.
+`type`::
+API keys can be of type `rest`, if created via the <> or
+the <> APIs, or of type `cross_cluster` if created via
+the <> API.
+
`name`::
Name of the API key.
diff --git a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java
index f79077ae3a550..18d9dcdc822e5 100644
--- a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java
+++ b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java
@@ -9,8 +9,10 @@
import org.apache.http.HttpHeaders;
import org.elasticsearch.client.Request;
+import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
+import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.core.Strings;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.test.XContentTestUtils;
@@ -21,6 +23,7 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -43,6 +46,8 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
private static final String API_KEY_ADMIN_AUTH_HEADER = "Basic YXBpX2tleV9hZG1pbjpzZWN1cml0eS10ZXN0LXBhc3N3b3Jk";
private static final String API_KEY_USER_AUTH_HEADER = "Basic YXBpX2tleV91c2VyOnNlY3VyaXR5LXRlc3QtcGFzc3dvcmQ=";
private static final String TEST_USER_AUTH_HEADER = "Basic c2VjdXJpdHlfdGVzdF91c2VyOnNlY3VyaXR5LXRlc3QtcGFzc3dvcmQ=";
+ private static final String SYSTEM_WRITE_ROLE_NAME = "system_write";
+ private static final String SUPERUSER_WITH_SYSTEM_WRITE = "superuser_with_system_write";
public void testQuery() throws IOException {
createApiKeys();
@@ -297,6 +302,71 @@ public void testPagination() throws IOException, InterruptedException {
assertThat(responseMap2.get("count"), equalTo(0));
}
+ public void testTypeField() throws Exception {
+ final List allApiKeyIds = new ArrayList<>(7);
+ for (int i = 0; i < 7; i++) {
+ allApiKeyIds.add(
+ createApiKey("typed_key_" + i, Map.of(), randomFrom(API_KEY_ADMIN_AUTH_HEADER, API_KEY_USER_AUTH_HEADER)).v1()
+ );
+ }
+ List apiKeyIdsSubset = randomSubsetOf(allApiKeyIds);
+ List apiKeyIdsSubsetDifference = new ArrayList<>(allApiKeyIds);
+ apiKeyIdsSubsetDifference.removeAll(apiKeyIdsSubset);
+
+ List apiKeyRestTypeQueries = List.of("""
+ {"query": {"term": {"type": "rest" }}}""", """
+ {"query": {"bool": {"must_not": [{"term": {"type": "cross_cluster"}}, {"term": {"type": "other"}}]}}}""", """
+ {"query": {"prefix": {"type": "re" }}}""", """
+ {"query": {"wildcard": {"type": "r*t" }}}""", """
+ {"query": {"range": {"type": {"gte": "raaa", "lte": "rzzz"}}}}""");
+
+ for (String query : apiKeyRestTypeQueries) {
+ assertQuery(API_KEY_ADMIN_AUTH_HEADER, query, apiKeys -> {
+ assertThat(
+ apiKeys.stream().map(k -> (String) k.get("id")).toList(),
+ containsInAnyOrder(allApiKeyIds.toArray(new String[0]))
+ );
+ });
+ }
+
+ createSystemWriteRole(SYSTEM_WRITE_ROLE_NAME);
+ String systemWriteCreds = createUser(SUPERUSER_WITH_SYSTEM_WRITE, new String[] { "superuser", SYSTEM_WRITE_ROLE_NAME });
+
+ // test keys with no "type" field are still considered of type "rest"
+ // this is so in order to accommodate pre-8.9 API keys which where all of type "rest" implicitly
+ updateApiKeys(systemWriteCreds, "ctx._source.remove('type');", apiKeyIdsSubset);
+ for (String query : apiKeyRestTypeQueries) {
+ assertQuery(API_KEY_ADMIN_AUTH_HEADER, query, apiKeys -> {
+ assertThat(
+ apiKeys.stream().map(k -> (String) k.get("id")).toList(),
+ containsInAnyOrder(allApiKeyIds.toArray(new String[0]))
+ );
+ });
+ }
+
+ // but the same keys with type "other" are NOT of type "rest"
+ updateApiKeys(systemWriteCreds, "ctx._source['type']='other';", apiKeyIdsSubset);
+ for (String query : apiKeyRestTypeQueries) {
+ assertQuery(API_KEY_ADMIN_AUTH_HEADER, query, apiKeys -> {
+ assertThat(
+ apiKeys.stream().map(k -> (String) k.get("id")).toList(),
+ containsInAnyOrder(apiKeyIdsSubsetDifference.toArray(new String[0]))
+ );
+ });
+ }
+ // the complement set is not of type "rest" if it is "cross_cluster"
+ updateApiKeys(systemWriteCreds, "ctx._source['type']='rest';", apiKeyIdsSubset);
+ updateApiKeys(systemWriteCreds, "ctx._source['type']='cross_cluster';", apiKeyIdsSubsetDifference);
+ for (String query : apiKeyRestTypeQueries) {
+ assertQuery(API_KEY_ADMIN_AUTH_HEADER, query, apiKeys -> {
+ assertThat(
+ apiKeys.stream().map(k -> (String) k.get("id")).toList(),
+ containsInAnyOrder(apiKeyIdsSubset.toArray(new String[0]))
+ );
+ });
+ }
+ }
+
@SuppressWarnings("unchecked")
public void testSort() throws IOException {
final String authHeader = randomFrom(API_KEY_ADMIN_AUTH_HEADER, API_KEY_USER_AUTH_HEADER);
@@ -598,10 +668,73 @@ private String createAndInvalidateApiKey(String name, String authHeader) throws
return tuple.v1();
}
- private void createUser(String name) throws IOException {
- final Request request = new Request("POST", "/_security/user/" + name);
- request.setJsonEntity("""
- {"password":"super-strong-password","roles":[]}""");
- assertOK(adminClient().performRequest(request));
+ private String createUser(String username) throws IOException {
+ return createUser(username, new String[0]);
+ }
+
+ private String createUser(String username, String[] roles) throws IOException {
+ final Request request = new Request("POST", "/_security/user/" + username);
+ Map body = Map.ofEntries(Map.entry("roles", roles), Map.entry("password", "super-strong-password".toString()));
+ request.setJsonEntity(XContentTestUtils.convertToXContent(body, XContentType.JSON).utf8ToString());
+ Response response = adminClient().performRequest(request);
+ assertOK(response);
+ return basicAuthHeaderValue(username, new SecureString("super-strong-password".toCharArray()));
+ }
+
+ private void createSystemWriteRole(String roleName) throws IOException {
+ final Request addRole = new Request("POST", "/_security/role/" + roleName);
+ addRole.setJsonEntity("""
+ {
+ "indices": [
+ {
+ "names": [ "*" ],
+ "privileges": ["all"],
+ "allow_restricted_indices" : true
+ }
+ ]
+ }""");
+ Response response = adminClient().performRequest(addRole);
+ assertOK(response);
+ }
+
+ private void expectWarnings(Request request, String... expectedWarnings) {
+ final Set expected = Set.of(expectedWarnings);
+ RequestOptions options = request.getOptions().toBuilder().setWarningsHandler(warnings -> {
+ final Set actual = Set.copyOf(warnings);
+ // Return true if the warnings aren't what we expected; the client will treat them as a fatal error.
+ return actual.equals(expected) == false;
+ }).build();
+ request.setOptions(options);
+ }
+
+ private void updateApiKeys(String creds, String script, Collection ids) throws IOException {
+ if (ids.isEmpty()) {
+ return;
+ }
+ final Request request = new Request("POST", "/.security/_update_by_query?refresh=true&wait_for_completion=true");
+ request.setJsonEntity(Strings.format("""
+ {
+ "script": {
+ "source": "%s",
+ "lang": "painless"
+ },
+ "query": {
+ "bool": {
+ "must": [
+ {"term": {"doc_type": "api_key"}},
+ {"ids": {"values": %s}}
+ ]
+ }
+ }
+ }
+ """, script, ids.stream().map(id -> "\"" + id + "\"").collect(Collectors.toList())));
+ request.setOptions(request.getOptions().toBuilder().addHeader(HttpHeaders.AUTHORIZATION, creds));
+ expectWarnings(
+ request,
+ "this request accesses system indices: [.security-7],"
+ + " but in a future major version, direct access to system indices will be prevented by default"
+ );
+ Response response = client().performRequest(request);
+ assertOK(response);
}
}
diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java
index 6c4aaeada74c7..0d5a757f65084 100644
--- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java
+++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java
@@ -35,8 +35,10 @@
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -54,9 +56,11 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
@@ -703,6 +707,73 @@ public void testRemoteIndicesSupportForApiKeys() throws IOException {
}
+ @SuppressWarnings("unchecked")
+ public void testQueryCrossClusterApiKeysByType() throws IOException {
+ final List apiKeyIds = new ArrayList<>(3);
+ for (int i = 0; i < randomIntBetween(3, 5); i++) {
+ Request createRequest = new Request("POST", "/_security/cross_cluster/api_key");
+ createRequest.setJsonEntity(Strings.format("""
+ {
+ "name": "test-cross-key-query-%d",
+ "access": {
+ "search": [
+ {
+ "names": [ "whatever" ]
+ }
+ ]
+ },
+ "metadata": { "tag": %d, "label": "rest" }
+ }""", i, i));
+ setUserForRequest(createRequest, MANAGE_SECURITY_USER, END_USER_PASSWORD);
+ ObjectPath createResponse = assertOKAndCreateObjectPath(client().performRequest(createRequest));
+ apiKeyIds.add(createResponse.evaluate("id"));
+ }
+ // the "cross_cluster" keys are not "rest" type
+ for (String restTypeQuery : List.of("""
+ {"query": {"term": {"type": "rest" }}}""", """
+ {"query": {"bool": {"must_not": {"term": {"type": "cross_cluster"}}}}}""", """
+ {"query": {"prefix": {"type": "re" }}}""", """
+ {"query": {"wildcard": {"type": "r*t" }}}""", """
+ {"query": {"range": {"type": {"gte": "raaa", "lte": "rzzz"}}}}""")) {
+ Request queryRequest = new Request("GET", "/_security/_query/api_key");
+ queryRequest.addParameter("with_limited_by", String.valueOf(randomBoolean()));
+ queryRequest.setJsonEntity(restTypeQuery);
+ setUserForRequest(queryRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD);
+ ObjectPath queryResponse = assertOKAndCreateObjectPath(client().performRequest(queryRequest));
+ assertThat(queryResponse.evaluate("total"), is(0));
+ assertThat(queryResponse.evaluate("count"), is(0));
+ assertThat(queryResponse.evaluate("api_keys"), iterableWithSize(0));
+ }
+ for (String crossClusterTypeQuery : List.of("""
+ {"query": {"term": {"type": "cross_cluster" }}}""", """
+ {"query": {"bool": {"must_not": {"term": {"type": "rest"}}}}}""", """
+ {"query": {"prefix": {"type": "cro" }}}""", """
+ {"query": {"wildcard": {"type": "*oss_*er" }}}""", """
+ {"query": {"range": {"type": {"gte": "cross", "lte": "zzzz"}}}}""")) {
+ Request queryRequest = new Request("GET", "/_security/_query/api_key");
+ queryRequest.addParameter("with_limited_by", String.valueOf(randomBoolean()));
+ queryRequest.setJsonEntity(crossClusterTypeQuery);
+ setUserForRequest(queryRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD);
+ ObjectPath queryResponse = assertOKAndCreateObjectPath(client().performRequest(queryRequest));
+ assertThat(queryResponse.evaluate("total"), is(apiKeyIds.size()));
+ assertThat(queryResponse.evaluate("count"), is(apiKeyIds.size()));
+ assertThat(queryResponse.evaluate("api_keys"), iterableWithSize(apiKeyIds.size()));
+ Iterator> apiKeys = ((List>) queryResponse.evaluate("api_keys")).iterator();
+ while (apiKeys.hasNext()) {
+ assertThat(apiKeyIds, hasItem((String) ((Map) apiKeys.next()).get("id")));
+ }
+ }
+ final Request queryRequest = new Request("GET", "/_security/_query/api_key");
+ queryRequest.addParameter("with_limited_by", String.valueOf(randomBoolean()));
+ queryRequest.setJsonEntity("""
+ {"query": {"bool": {"must": [{"term": {"type": "cross_cluster" }}, {"term": {"metadata.tag": 2}}]}}}""");
+ setUserForRequest(queryRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD);
+ final ObjectPath queryResponse = assertOKAndCreateObjectPath(client().performRequest(queryRequest));
+ assertThat(queryResponse.evaluate("total"), is(1));
+ assertThat(queryResponse.evaluate("count"), is(1));
+ assertThat(queryResponse.evaluate("api_keys.0.name"), is("test-cross-key-query-2"));
+ }
+
public void testCreateCrossClusterApiKey() throws IOException {
final Request createRequest = new Request("POST", "/_security/cross_cluster/api_key");
createRequest.setJsonEntity("""
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java
index 4077597a7ef16..b9961e6735c7e 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java
@@ -27,11 +27,26 @@
import org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS;
public final class TransportQueryApiKeyAction extends HandledTransportAction {
+ // API keys with no "type" field are implicitly of type "rest" (this is the case for all API Keys created before v8.9).
+ // The below runtime field ensures that the "type" field can be used by the {@link RestQueryApiKeyAction},
+ // while making the implicit "rest" type feature transparent to the caller (hence all keys are either "rest"
+ // or "cross_cluster", and the "type" is always set).
+ // This can be improved, to get rid of the runtime performance impact of the runtime field, by reindexing
+ // the api key docs and setting the "type" to "rest" if empty. But the infrastructure to run such a maintenance
+ // task on a system index (once the cluster version permits) is not currently available.
+ public static final String API_KEY_TYPE_RUNTIME_MAPPING_FIELD = "runtime_key_type";
+ private static final Map API_KEY_TYPE_RUNTIME_MAPPING = Map.of(
+ API_KEY_TYPE_RUNTIME_MAPPING_FIELD,
+ Map.of("type", "keyword", "script", Map.of("source", "emit(field('type').get(\"rest\"));"))
+ );
+
private final ApiKeyService apiKeyService;
private final SecurityContext securityContext;
@@ -66,12 +81,19 @@ protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener {
+ if (API_KEY_TYPE_RUNTIME_MAPPING_FIELD.equals(fieldName)) {
+ accessesApiKeyTypeField.set(true);
+ }
+ }, request.isFilterForCurrentUser() ? authentication : null);
searchSourceBuilder.query(apiKeyBoolQueryBuilder);
+ // only add the query-level runtime field to the search request if it's actually referring the "type" field
+ if (accessesApiKeyTypeField.get()) {
+ searchSourceBuilder.runtimeMappings(API_KEY_TYPE_RUNTIME_MAPPING);
+ }
+
if (request.getFieldSortBuilders() != null) {
translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder);
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java
index 28ecd5ffe5b57..5cb6573c8b5dc 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java
@@ -28,6 +28,9 @@
import java.io.IOException;
import java.util.Set;
+import java.util.function.Consumer;
+
+import static org.elasticsearch.xpack.security.action.apikey.TransportQueryApiKeyAction.API_KEY_TYPE_RUNTIME_MAPPING_FIELD;
public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
@@ -36,10 +39,14 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
"_id",
"doc_type",
"name",
+ "type",
+ API_KEY_TYPE_RUNTIME_MAPPING_FIELD,
"api_key_invalidated",
"invalidation_time",
"creation_time",
- "expiration_time"
+ "expiration_time",
+ "creator.principal",
+ "creator.realm"
);
private ApiKeyBoolQueryBuilder() {}
@@ -56,17 +63,23 @@ private ApiKeyBoolQueryBuilder() {}
*
* @param queryBuilder This represents the query parsed directly from the user input. It is validated
* and transformed (see above).
+ * @param fieldNameVisitor This {@code Consumer} is invoked with all the (index-level) field names referred to in the passed-in query.
* @param authentication The user's authentication object. If present, it will be used to filter the results
* to only include API keys owned by the user.
* @return A specialised query builder for API keys that is safe to run on the security index.
*/
- public static ApiKeyBoolQueryBuilder build(QueryBuilder queryBuilder, @Nullable Authentication authentication) {
+ public static ApiKeyBoolQueryBuilder build(
+ QueryBuilder queryBuilder,
+ Consumer fieldNameVisitor,
+ @Nullable Authentication authentication
+ ) {
final ApiKeyBoolQueryBuilder finalQuery = new ApiKeyBoolQueryBuilder();
if (queryBuilder != null) {
- QueryBuilder processedQuery = doProcess(queryBuilder);
+ QueryBuilder processedQuery = doProcess(queryBuilder, fieldNameVisitor);
finalQuery.must(processedQuery);
}
finalQuery.filter(QueryBuilders.termQuery("doc_type", "api_key"));
+ fieldNameVisitor.accept("doc_type");
if (authentication != null) {
if (authentication.isApiKey()) {
@@ -77,8 +90,10 @@ public static ApiKeyBoolQueryBuilder build(QueryBuilder queryBuilder, @Nullable
finalQuery.filter(QueryBuilders.idsQuery().addIds(apiKeyId));
} else {
finalQuery.filter(QueryBuilders.termQuery("creator.principal", authentication.getEffectiveSubject().getUser().principal()));
+ fieldNameVisitor.accept("creator.principal");
final String[] realms = ApiKeyService.getOwnersRealmNames(authentication);
final QueryBuilder realmsQuery = ApiKeyService.filterForRealmNames(realms);
+ fieldNameVisitor.accept("creator.realm");
assert realmsQuery != null;
finalQuery.filter(realmsQuery);
}
@@ -86,15 +101,15 @@ public static ApiKeyBoolQueryBuilder build(QueryBuilder queryBuilder, @Nullable
return finalQuery;
}
- private static QueryBuilder doProcess(QueryBuilder qb) {
+ private static QueryBuilder doProcess(QueryBuilder qb, Consumer fieldNameVisitor) {
if (qb instanceof final BoolQueryBuilder query) {
final BoolQueryBuilder newQuery = QueryBuilders.boolQuery()
.minimumShouldMatch(query.minimumShouldMatch())
.adjustPureNegative(query.adjustPureNegative());
- query.must().stream().map(ApiKeyBoolQueryBuilder::doProcess).forEach(newQuery::must);
- query.should().stream().map(ApiKeyBoolQueryBuilder::doProcess).forEach(newQuery::should);
- query.mustNot().stream().map(ApiKeyBoolQueryBuilder::doProcess).forEach(newQuery::mustNot);
- query.filter().stream().map(ApiKeyBoolQueryBuilder::doProcess).forEach(newQuery::filter);
+ query.must().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::must);
+ query.should().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::should);
+ query.mustNot().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::mustNot);
+ query.filter().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::filter);
return newQuery;
} else if (qb instanceof MatchAllQueryBuilder) {
return qb;
@@ -102,29 +117,35 @@ private static QueryBuilder doProcess(QueryBuilder qb) {
return qb;
} else if (qb instanceof final TermQueryBuilder query) {
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
+ fieldNameVisitor.accept(translatedFieldName);
return QueryBuilders.termQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive());
} else if (qb instanceof final ExistsQueryBuilder query) {
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
+ fieldNameVisitor.accept(translatedFieldName);
return QueryBuilders.existsQuery(translatedFieldName);
} else if (qb instanceof final TermsQueryBuilder query) {
if (query.termsLookup() != null) {
throw new IllegalArgumentException("terms query with terms lookup is not supported for API Key query");
}
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
+ fieldNameVisitor.accept(translatedFieldName);
return QueryBuilders.termsQuery(translatedFieldName, query.getValues());
} else if (qb instanceof final PrefixQueryBuilder query) {
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
+ fieldNameVisitor.accept(translatedFieldName);
return QueryBuilders.prefixQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive());
} else if (qb instanceof final WildcardQueryBuilder query) {
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
+ fieldNameVisitor.accept(translatedFieldName);
return QueryBuilders.wildcardQuery(translatedFieldName, query.value())
.caseInsensitive(query.caseInsensitive())
.rewrite(query.rewrite());
} else if (qb instanceof final RangeQueryBuilder query) {
- final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
if (query.relation() != null) {
throw new IllegalArgumentException("range query with relation is not supported for API Key query");
}
+ final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
+ fieldNameVisitor.accept(translatedFieldName);
final RangeQueryBuilder newQuery = QueryBuilders.rangeQuery(translatedFieldName);
if (query.format() != null) {
newQuery.format(query.format());
@@ -159,9 +180,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws
}
static boolean isIndexFieldNameAllowed(String fieldName) {
- return ALLOWED_EXACT_INDEX_FIELD_NAMES.contains(fieldName)
- || fieldName.startsWith("metadata_flattened.")
- || fieldName.startsWith("creator.");
+ return ALLOWED_EXACT_INDEX_FIELD_NAMES.contains(fieldName) || fieldName.startsWith("metadata_flattened.");
}
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java
index 4d7cc9d978cd4..c204ec031b18c 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java
@@ -10,6 +10,8 @@
import java.util.List;
import java.util.function.Function;
+import static org.elasticsearch.xpack.security.action.apikey.TransportQueryApiKeyAction.API_KEY_TYPE_RUNTIME_MAPPING_FIELD;
+
/**
* A class to translate query level field names to index level field names.
*/
@@ -21,6 +23,7 @@ public class ApiKeyFieldNameTranslators {
new ExactFieldNameTranslator(s -> "creator.principal", "username"),
new ExactFieldNameTranslator(s -> "creator.realm", "realm_name"),
new ExactFieldNameTranslator(Function.identity(), "name"),
+ new ExactFieldNameTranslator(s -> API_KEY_TYPE_RUNTIME_MAPPING_FIELD, "type"),
new ExactFieldNameTranslator(s -> "creation_time", "creation"),
new ExactFieldNameTranslator(s -> "expiration_time", "expiration"),
new ExactFieldNameTranslator(s -> "api_key_invalidated", "invalidated"),
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java
index 477409f22369f..235657a30e11f 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java
@@ -29,11 +29,13 @@
import org.elasticsearch.xpack.core.security.authc.AuthenticationTests;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.user.User;
+import org.elasticsearch.xpack.security.action.apikey.TransportQueryApiKeyAction;
import org.elasticsearch.xpack.security.authc.ApiKeyService;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@@ -57,7 +59,9 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
public void testBuildFromSimpleQuery() {
final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null;
final QueryBuilder q1 = randomSimpleQuery("name");
- final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(q1, authentication);
+ final List queryFields = new ArrayList<>();
+ final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(q1, queryFields::add, authentication);
+ assertQueryFields(queryFields, q1, authentication);
assertCommonFilterQueries(apiKeyQb1, authentication);
final List mustQueries = apiKeyQb1.must();
assertThat(mustQueries, hasSize(1));
@@ -69,7 +73,9 @@ public void testBuildFromSimpleQuery() {
public void testQueryForDomainAuthentication() {
final Authentication authentication = AuthenticationTests.randomAuthentication(null, AuthenticationTests.randomRealmRef(true));
final QueryBuilder query = randomSimpleQuery("name");
- final ApiKeyBoolQueryBuilder apiKeysQuery = ApiKeyBoolQueryBuilder.build(query, authentication);
+ final List queryFields = new ArrayList<>();
+ final ApiKeyBoolQueryBuilder apiKeysQuery = ApiKeyBoolQueryBuilder.build(query, queryFields::add, authentication);
+ assertQueryFields(queryFields, query, authentication);
assertThat(apiKeysQuery.filter().get(0), is(QueryBuilders.termQuery("doc_type", "api_key")));
assertThat(
apiKeysQuery.filter().get(1),
@@ -102,18 +108,23 @@ public void testQueryForDomainAuthentication() {
public void testBuildFromBoolQuery() {
final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null;
+ final List queryFields = new ArrayList<>();
final BoolQueryBuilder bq1 = QueryBuilders.boolQuery();
+ boolean accessesNameField = false;
if (randomBoolean()) {
bq1.must(QueryBuilders.prefixQuery("name", "prod-"));
+ accessesNameField = true;
}
if (randomBoolean()) {
bq1.should(QueryBuilders.wildcardQuery("name", "*-east-*"));
+ accessesNameField = true;
}
if (randomBoolean()) {
bq1.filter(
QueryBuilders.termsQuery("name", randomArray(3, 8, String[]::new, () -> "prod-" + randomInt() + "-east-" + randomInt()))
);
+ accessesNameField = true;
}
if (randomBoolean()) {
bq1.mustNot(QueryBuilders.idsQuery().addIds(randomArray(1, 3, String[]::new, () -> randomAlphaOfLength(22))));
@@ -121,9 +132,18 @@ public void testBuildFromBoolQuery() {
if (randomBoolean()) {
bq1.minimumShouldMatch(randomIntBetween(1, 2));
}
- final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(bq1, authentication);
+ final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(bq1, queryFields::add, authentication);
assertCommonFilterQueries(apiKeyQb1, authentication);
+ assertThat(queryFields, hasItem("doc_type"));
+ if (accessesNameField) {
+ assertThat(queryFields, hasItem("name"));
+ }
+ if (authentication != null && authentication.isApiKey() == false) {
+ assertThat(queryFields, hasItem("creator.principal"));
+ assertThat(queryFields, hasItem("creator.realm"));
+ }
+
assertThat(apiKeyQb1.must(), hasSize(1));
assertThat(apiKeyQb1.should(), empty());
assertThat(apiKeyQb1.mustNot(), empty());
@@ -141,35 +161,78 @@ public void testFieldNameTranslation() {
final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null;
// metadata
- final String metadataKey = randomAlphaOfLengthBetween(3, 8);
- final TermQueryBuilder q1 = QueryBuilders.termQuery("metadata." + metadataKey, randomAlphaOfLengthBetween(3, 8));
- final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(q1, authentication);
- assertCommonFilterQueries(apiKeyQb1, authentication);
- assertThat(apiKeyQb1.must().get(0), equalTo(QueryBuilders.termQuery("metadata_flattened." + metadataKey, q1.value())));
+ {
+ final List queryFields = new ArrayList<>();
+ final String metadataKey = randomAlphaOfLengthBetween(3, 8);
+ final TermQueryBuilder q1 = QueryBuilders.termQuery("metadata." + metadataKey, randomAlphaOfLengthBetween(3, 8));
+ final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(q1, queryFields::add, authentication);
+ assertThat(queryFields, hasItem("doc_type"));
+ assertThat(queryFields, hasItem("metadata_flattened." + metadataKey));
+ if (authentication != null && authentication.isApiKey() == false) {
+ assertThat(queryFields, hasItem("creator.principal"));
+ assertThat(queryFields, hasItem("creator.realm"));
+ }
+ assertCommonFilterQueries(apiKeyQb1, authentication);
+ assertThat(apiKeyQb1.must().get(0), equalTo(QueryBuilders.termQuery("metadata_flattened." + metadataKey, q1.value())));
+ }
// username
- final PrefixQueryBuilder q2 = QueryBuilders.prefixQuery("username", randomAlphaOfLength(3));
- final ApiKeyBoolQueryBuilder apiKeyQb2 = ApiKeyBoolQueryBuilder.build(q2, authentication);
- assertCommonFilterQueries(apiKeyQb2, authentication);
- assertThat(apiKeyQb2.must().get(0), equalTo(QueryBuilders.prefixQuery("creator.principal", q2.value())));
+ {
+ final List queryFields = new ArrayList<>();
+ final PrefixQueryBuilder q2 = QueryBuilders.prefixQuery("username", randomAlphaOfLength(3));
+ final ApiKeyBoolQueryBuilder apiKeyQb2 = ApiKeyBoolQueryBuilder.build(q2, queryFields::add, authentication);
+ assertThat(queryFields, hasItem("doc_type"));
+ assertThat(queryFields, hasItem("creator.principal"));
+ if (authentication != null && authentication.isApiKey() == false) {
+ assertThat(queryFields, hasItem("creator.realm"));
+ }
+ assertCommonFilterQueries(apiKeyQb2, authentication);
+ assertThat(apiKeyQb2.must().get(0), equalTo(QueryBuilders.prefixQuery("creator.principal", q2.value())));
+ }
// realm name
- final WildcardQueryBuilder q3 = QueryBuilders.wildcardQuery("realm_name", "*" + randomAlphaOfLength(3));
- final ApiKeyBoolQueryBuilder apiKeyQb3 = ApiKeyBoolQueryBuilder.build(q3, authentication);
- assertCommonFilterQueries(apiKeyQb3, authentication);
- assertThat(apiKeyQb3.must().get(0), equalTo(QueryBuilders.wildcardQuery("creator.realm", q3.value())));
+ {
+ final List queryFields = new ArrayList<>();
+ final WildcardQueryBuilder q3 = QueryBuilders.wildcardQuery("realm_name", "*" + randomAlphaOfLength(3));
+ final ApiKeyBoolQueryBuilder apiKeyQb3 = ApiKeyBoolQueryBuilder.build(q3, queryFields::add, authentication);
+ assertThat(queryFields, hasItem("doc_type"));
+ assertThat(queryFields, hasItem("creator.realm"));
+ if (authentication != null && authentication.isApiKey() == false) {
+ assertThat(queryFields, hasItem("creator.principal"));
+ }
+ assertCommonFilterQueries(apiKeyQb3, authentication);
+ assertThat(apiKeyQb3.must().get(0), equalTo(QueryBuilders.wildcardQuery("creator.realm", q3.value())));
+ }
// creation_time
- final TermQueryBuilder q4 = QueryBuilders.termQuery("creation", randomLongBetween(0, Long.MAX_VALUE));
- final ApiKeyBoolQueryBuilder apiKeyQb4 = ApiKeyBoolQueryBuilder.build(q4, authentication);
- assertCommonFilterQueries(apiKeyQb4, authentication);
- assertThat(apiKeyQb4.must().get(0), equalTo(QueryBuilders.termQuery("creation_time", q4.value())));
+ {
+ final List queryFields = new ArrayList<>();
+ final TermQueryBuilder q4 = QueryBuilders.termQuery("creation", randomLongBetween(0, Long.MAX_VALUE));
+ final ApiKeyBoolQueryBuilder apiKeyQb4 = ApiKeyBoolQueryBuilder.build(q4, queryFields::add, authentication);
+ assertThat(queryFields, hasItem("doc_type"));
+ assertThat(queryFields, hasItem("creation_time"));
+ if (authentication != null && authentication.isApiKey() == false) {
+ assertThat(queryFields, hasItem("creator.principal"));
+ assertThat(queryFields, hasItem("creator.realm"));
+ }
+ assertCommonFilterQueries(apiKeyQb4, authentication);
+ assertThat(apiKeyQb4.must().get(0), equalTo(QueryBuilders.termQuery("creation_time", q4.value())));
+ }
// expiration_time
- final TermQueryBuilder q5 = QueryBuilders.termQuery("expiration", randomLongBetween(0, Long.MAX_VALUE));
- final ApiKeyBoolQueryBuilder apiKeyQb5 = ApiKeyBoolQueryBuilder.build(q5, authentication);
- assertCommonFilterQueries(apiKeyQb5, authentication);
- assertThat(apiKeyQb5.must().get(0), equalTo(QueryBuilders.termQuery("expiration_time", q5.value())));
+ {
+ final List queryFields = new ArrayList<>();
+ final TermQueryBuilder q5 = QueryBuilders.termQuery("expiration", randomLongBetween(0, Long.MAX_VALUE));
+ final ApiKeyBoolQueryBuilder apiKeyQb5 = ApiKeyBoolQueryBuilder.build(q5, queryFields::add, authentication);
+ assertThat(queryFields, hasItem("doc_type"));
+ assertThat(queryFields, hasItem("expiration_time"));
+ if (authentication != null && authentication.isApiKey() == false) {
+ assertThat(queryFields, hasItem("creator.principal"));
+ assertThat(queryFields, hasItem("creator.realm"));
+ }
+ assertCommonFilterQueries(apiKeyQb5, authentication);
+ assertThat(apiKeyQb5.must().get(0), equalTo(QueryBuilders.termQuery("expiration_time", q5.value())));
+ }
}
public void testAllowListOfFieldNames() {
@@ -197,7 +260,7 @@ public void testAllowListOfFieldNames() {
);
final IllegalArgumentException e1 = expectThrows(
IllegalArgumentException.class,
- () -> ApiKeyBoolQueryBuilder.build(q1, authentication)
+ () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
);
assertThat(e1.getMessage(), containsString("Field [" + fieldName + "] is not allowed for API Key query"));
@@ -208,7 +271,7 @@ public void testTermsLookupIsNotAllowed() {
final TermsQueryBuilder q1 = QueryBuilders.termsLookupQuery("name", new TermsLookup("lookup", "1", "names"));
final IllegalArgumentException e1 = expectThrows(
IllegalArgumentException.class,
- () -> ApiKeyBoolQueryBuilder.build(q1, authentication)
+ () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
);
assertThat(e1.getMessage(), containsString("terms query with terms lookup is not supported for API Key query"));
}
@@ -218,7 +281,7 @@ public void testRangeQueryWithRelationIsNotAllowed() {
final RangeQueryBuilder q1 = QueryBuilders.rangeQuery("creation").relation("contains");
final IllegalArgumentException e1 = expectThrows(
IllegalArgumentException.class,
- () -> ApiKeyBoolQueryBuilder.build(q1, authentication)
+ () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
);
assertThat(e1.getMessage(), containsString("range query with relation is not supported for API Key query"));
}
@@ -266,7 +329,7 @@ public void testDisallowedQueryTypes() {
final IllegalArgumentException e1 = expectThrows(
IllegalArgumentException.class,
- () -> ApiKeyBoolQueryBuilder.build(q1, authentication)
+ () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
);
assertThat(e1.getMessage(), containsString("Query type [" + q1.getName() + "] is not supported for API Key query"));
}
@@ -274,6 +337,7 @@ public void testDisallowedQueryTypes() {
public void testWillSetAllowedFields() throws IOException {
final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(
randomSimpleQuery("name"),
+ ignored -> {},
randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null
);
@@ -305,7 +369,11 @@ public void testWillFilterForApiKeyId() {
new User(randomAlphaOfLengthBetween(5, 8)),
apiKeyId
);
- final ApiKeyBoolQueryBuilder apiKeyQb = ApiKeyBoolQueryBuilder.build(randomFrom(randomSimpleQuery("name"), null), authentication);
+ final ApiKeyBoolQueryBuilder apiKeyQb = ApiKeyBoolQueryBuilder.build(
+ randomFrom(randomSimpleQuery("name"), null),
+ ignored -> {},
+ authentication
+ );
assertThat(apiKeyQb.filter(), hasItem(QueryBuilders.termQuery("doc_type", "api_key")));
assertThat(apiKeyQb.filter(), hasItem(QueryBuilders.idsQuery().addIds(apiKeyId)));
}
@@ -314,11 +382,14 @@ private void testAllowedIndexFieldName(Predicate predicate) {
final String allowedField = randomFrom(
"doc_type",
"name",
+ "type",
+ TransportQueryApiKeyAction.API_KEY_TYPE_RUNTIME_MAPPING_FIELD,
"api_key_invalidated",
"creation_time",
"expiration_time",
"metadata_flattened." + randomAlphaOfLengthBetween(1, 10),
- "creator." + randomAlphaOfLengthBetween(1, 10)
+ "creator.principal",
+ "creator.realm"
);
assertThat(predicate, trueWith(allowedField));
@@ -362,4 +433,15 @@ private QueryBuilder randomSimpleQuery(String name) {
.to(Instant.now().toEpochMilli(), randomBoolean());
};
}
+
+ private void assertQueryFields(List actualQueryFields, QueryBuilder queryBuilder, Authentication authentication) {
+ assertThat(actualQueryFields, hasItem("doc_type"));
+ if ((queryBuilder instanceof IdsQueryBuilder || queryBuilder instanceof MatchAllQueryBuilder) == false) {
+ assertThat(actualQueryFields, hasItem("name"));
+ }
+ if (authentication != null && authentication.isApiKey() == false) {
+ assertThat(actualQueryFields, hasItem("creator.principal"));
+ assertThat(actualQueryFields, hasItem("creator.realm"));
+ }
+ }
}
diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/ApiKeyBackwardsCompatibilityIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/ApiKeyBackwardsCompatibilityIT.java
index 1a37f31bffe79..2bce06543f67c 100644
--- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/ApiKeyBackwardsCompatibilityIT.java
+++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/ApiKeyBackwardsCompatibilityIT.java
@@ -12,6 +12,7 @@
import org.elasticsearch.Build;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.TransportVersions;
+import org.elasticsearch.Version;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
@@ -39,19 +40,53 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.function.Consumer;
import static org.elasticsearch.transport.RemoteClusterPortSettings.TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
public class ApiKeyBackwardsCompatibilityIT extends AbstractUpgradeTestCase {
+ private static final Version UPGRADE_FROM_VERSION = Version.fromString(System.getProperty("tests.upgrade_from_version"));
+
private RestClient oldVersionClient = null;
private RestClient newVersionClient = null;
+ public void testQueryRestTypeKeys() throws IOException {
+ assumeTrue(
+ "only API keys created pre-8.9 are relevant for the rest-type query bwc case",
+ UPGRADE_FROM_VERSION.before(Version.V_8_9_0)
+ );
+ switch (CLUSTER_TYPE) {
+ case OLD -> createOrGrantApiKey(client(), "query-test-rest-key-from-old-cluster", "{}");
+ case MIXED -> createOrGrantApiKey(client(), "query-test-rest-key-from-mixed-cluster", "{}");
+ case UPGRADED -> {
+ createOrGrantApiKey(client(), "query-test-rest-key-from-upgraded-cluster", "{}");
+ for (String query : List.of("""
+ {"query": {"term": {"type": "rest" }}}""", """
+ {"query": {"prefix": {"type": "re" }}}""", """
+ {"query": {"wildcard": {"type": "r*t" }}}""", """
+ {"query": {"range": {"type": {"gte": "raaa", "lte": "rzzz"}}}}""")) {
+ assertQuery(client(), query, apiKeys -> {
+ assertThat(
+ apiKeys.stream().map(k -> (String) k.get("name")).toList(),
+ hasItems(
+ "query-test-rest-key-from-old-cluster",
+ "query-test-rest-key-from-mixed-cluster",
+ "query-test-rest-key-from-upgraded-cluster"
+ )
+ );
+ });
+ }
+ }
+ }
+ }
+
public void testCreatingAndUpdatingApiKeys() throws Exception {
assumeTrue(
"The remote_indices for API Keys are not supported before transport version "
@@ -177,7 +212,10 @@ private Tuple createOrGrantApiKey(String roles) throws IOExcepti
}
private Tuple createOrGrantApiKey(RestClient client, String roles) throws IOException {
- final String name = "test-api-key-" + randomAlphaOfLengthBetween(3, 5);
+ return createOrGrantApiKey(client, "test-api-key-" + randomAlphaOfLengthBetween(3, 5), roles);
+ }
+
+ private Tuple createOrGrantApiKey(RestClient client, String name, String roles) throws IOException {
final Request createApiKeyRequest;
String body = Strings.format("""
{
@@ -391,4 +429,15 @@ private static RoleDescriptor randomRoleDescriptor(boolean includeRemoteIndices)
null
);
}
+
+ private void assertQuery(RestClient restClient, String body, Consumer>> apiKeysVerifier) throws IOException {
+ final Request request = new Request("GET", "/_security/_query/api_key");
+ request.setJsonEntity(body);
+ final Response response = restClient.performRequest(request);
+ assertOK(response);
+ final Map responseMap = responseAsMap(response);
+ @SuppressWarnings("unchecked")
+ final List