Skip to content

Commit 953b9fb

Browse files
authored
ESQL: List/get query API (#124832)
This PR adds two new REST endpoints, for listing queries and getting information on a current query. * Resolves #124827 * Related to #124828 (initial work) Changes from the API specified in the above issues: * The get API is pretty initial, as we don't have a way of fetching the memory used or number of rows processed. List queries response: ``` GET /_query/queries // returns for each of the running queries // query_id, start_time, running_time, query { "queries" : { "abc": { "id": "abc", "start_time_millis": 14585858875292, "running_time_nanos": 762794, "query": "FROM logs* | STATS BY hostname" }, "4321": { "id":"4321", "start_time_millis": 14585858823573, "running_time_nanos": 90231, "query": "FROM orders | LOOKUP country_code ON country" } } } ``` Get query response: ``` GET /_query/queries/abc { "id" : "abc", "start_time_millis": 14585858875292, "running_time_nanos": 762794, "query": "FROM logs* | STATS BY hostname" "coordinating_node": "oTUltX4IQMOUUVeiohTt8A" "data_nodes" : [ "DwrYwfytxthse49X4", "i5msnbUyWlpe86e7"] } ```
1 parent b21e325 commit 953b9fb

File tree

30 files changed

+840
-31
lines changed

30 files changed

+840
-31
lines changed

docs/changelog/124832.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 124832
2+
summary: List/get query API
3+
area: ES|QL
4+
type: feature
5+
issues:
6+
- 124827

docs/reference/elasticsearch/security-privileges.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ This section lists the privileges that you can assign to a role.
194194
`monitor_enrich`
195195
: All read-only operations related to managing and executing enrich policies.
196196

197+
`monitor_esql`
198+
: All read-only operations related to ES|QL queries.
199+
197200
`monitor_inference`
198201
: All read-only operations related to {{infer}}.
199202

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"esql.get_query": {
3+
"documentation": {
4+
"url": null,
5+
"description": "Executes a get ESQL query request"
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [],
11+
"content_type": [
12+
"application/json"
13+
]
14+
},
15+
"url": {
16+
"paths": [
17+
{
18+
"path": "/_query/queries/{id}",
19+
"methods": [
20+
"GET"
21+
],
22+
"parts": {
23+
"id": {
24+
"type": "string",
25+
"description": "The query ID"
26+
}
27+
}
28+
}
29+
]
30+
}
31+
}
32+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"esql.list_queries": {
3+
"documentation": {
4+
"url": null,
5+
"description": "Executes a list ESQL queries request"
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [],
11+
"content_type": [
12+
"application/json"
13+
]
14+
},
15+
"url": {
16+
"paths": [
17+
{
18+
"path": "/_query/queries",
19+
"methods": [
20+
"GET"
21+
]
22+
}
23+
]
24+
}
25+
}
26+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.test;
11+
12+
import org.hamcrest.BaseMatcher;
13+
import org.hamcrest.Description;
14+
import org.hamcrest.Matcher;
15+
16+
import static org.hamcrest.Matchers.anyOf;
17+
import static org.hamcrest.Matchers.equalTo;
18+
import static org.hamcrest.Matchers.isA;
19+
20+
/**
21+
* A type-agnostic way of comparing integer values, not caring if it's a long or an integer.
22+
*/
23+
public abstract sealed class IntOrLongMatcher<T> extends BaseMatcher<T> {
24+
public static IntOrLongMatcher<Integer> matches(int expected) {
25+
return new IntMatcher(expected);
26+
}
27+
28+
public static IntOrLongMatcher<Long> matches(long expected) {
29+
return new LongMatcher(expected);
30+
}
31+
32+
private static final class IntMatcher extends IntOrLongMatcher<Integer> {
33+
private final int expected;
34+
35+
private IntMatcher(int expected) {
36+
this.expected = expected;
37+
}
38+
39+
@Override
40+
public boolean matches(Object o) {
41+
return switch (o) {
42+
case Integer i -> expected == i;
43+
case Long l -> expected == l;
44+
default -> false;
45+
};
46+
}
47+
48+
@Override
49+
public void describeTo(Description description) {
50+
equalTo(expected).describeTo(description);
51+
}
52+
}
53+
54+
private static final class LongMatcher extends IntOrLongMatcher<Long> {
55+
private final long expected;
56+
57+
LongMatcher(long expected) {
58+
this.expected = expected;
59+
}
60+
61+
@Override
62+
public boolean matches(Object o) {
63+
return switch (o) {
64+
case Integer i -> expected == i;
65+
case Long l -> expected == l;
66+
default -> false;
67+
};
68+
}
69+
70+
@Override
71+
public void describeTo(Description description) {
72+
equalTo(expected).describeTo(description);
73+
}
74+
}
75+
76+
public static Matcher<Object> isIntOrLong() {
77+
return anyOf(isA(Integer.class), isA(Long.class));
78+
}
79+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ private static String maybeRewriteSingleAuthenticationHeaderForVersion(
196196
public static final String APM_ORIGIN = "apm";
197197
public static final String OTEL_ORIGIN = "otel";
198198
public static final String REINDEX_DATA_STREAM_ORIGIN = "reindex_data_stream";
199+
public static final String ESQL_ORIGIN = "esql";
199200

200201
private ClientHelper() {}
201202

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public class ClusterPrivilegeResolver {
110110
private static final Set<String> MONITOR_WATCHER_PATTERN = Set.of("cluster:monitor/xpack/watcher/*");
111111
private static final Set<String> MONITOR_ROLLUP_PATTERN = Set.of("cluster:monitor/xpack/rollup/*");
112112
private static final Set<String> MONITOR_ENRICH_PATTERN = Set.of("cluster:monitor/xpack/enrich/*", "cluster:admin/xpack/enrich/get");
113+
private static final Set<String> MONITOR_ESQL_PATTERN = Set.of("cluster:monitor/xpack/esql/*");
113114
// intentionally cluster:monitor/stats* to match cluster:monitor/stats, cluster:monitor/stats[n] and cluster:monitor/stats/remote
114115
private static final Set<String> MONITOR_STATS_PATTERN = Set.of("cluster:monitor/stats*");
115116

@@ -249,6 +250,7 @@ public class ClusterPrivilegeResolver {
249250
public static final NamedClusterPrivilege MONITOR_WATCHER = new ActionClusterPrivilege("monitor_watcher", MONITOR_WATCHER_PATTERN);
250251
public static final NamedClusterPrivilege MONITOR_ROLLUP = new ActionClusterPrivilege("monitor_rollup", MONITOR_ROLLUP_PATTERN);
251252
public static final NamedClusterPrivilege MONITOR_ENRICH = new ActionClusterPrivilege("monitor_enrich", MONITOR_ENRICH_PATTERN);
253+
public static final NamedClusterPrivilege MONITOR_ESQL = new ActionClusterPrivilege("monitor_esql", MONITOR_ESQL_PATTERN);
252254
public static final NamedClusterPrivilege MONITOR_STATS = new ActionClusterPrivilege("monitor_stats", MONITOR_STATS_PATTERN);
253255
public static final NamedClusterPrivilege MANAGE = new ActionClusterPrivilege("manage", ALL_CLUSTER_PATTERN, ALL_SECURITY_PATTERN);
254256
public static final NamedClusterPrivilege MANAGE_INFERENCE = new ActionClusterPrivilege("manage_inference", MANAGE_INFERENCE_PATTERN);
@@ -431,6 +433,7 @@ public class ClusterPrivilegeResolver {
431433
MONITOR_WATCHER,
432434
MONITOR_ROLLUP,
433435
MONITOR_ENRICH,
436+
MONITOR_ESQL,
434437
MONITOR_STATS,
435438
MANAGE,
436439
MANAGE_CONNECTOR,

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/async/AsyncTaskManagementService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ public AsyncTaskManagementService(
172172
this.threadPool = threadPool;
173173
}
174174

175+
public static String ASYNC_ACTION_SUFFIX = "[a]";
176+
175177
public void asyncExecute(
176178
Request request,
177179
TimeValue waitForCompletionTimeout,
@@ -182,7 +184,7 @@ public void asyncExecute(
182184
String nodeId = clusterService.localNode().getId();
183185
try (var ignored = threadPool.getThreadContext().newTraceContext()) {
184186
@SuppressWarnings("unchecked")
185-
T searchTask = (T) taskManager.register("transport", action + "[a]", new AsyncRequestWrapper(request, nodeId));
187+
T searchTask = (T) taskManager.register("transport", action + ASYNC_ACTION_SUFFIX, new AsyncRequestWrapper(request, nodeId));
186188
boolean operationStarted = false;
187189
try {
188190
operation.execute(

x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import static org.elasticsearch.test.MapMatcher.matchesMap;
4343
import static org.hamcrest.Matchers.containsString;
4444
import static org.hamcrest.Matchers.equalTo;
45+
import static org.hamcrest.Matchers.not;
4546

4647
public class EsqlSecurityIT extends ESRestTestCase {
4748
@ClassRule
@@ -69,6 +70,8 @@ public class EsqlSecurityIT extends ESRestTestCase {
6970
.user("logs_foo_after_2021", "x-pack-test-password", "logs_foo_after_2021", false)
7071
.user("logs_foo_after_2021_pattern", "x-pack-test-password", "logs_foo_after_2021_pattern", false)
7172
.user("logs_foo_after_2021_alias", "x-pack-test-password", "logs_foo_after_2021_alias", false)
73+
.user("user_without_monitor_privileges", "x-pack-test-password", "user_without_monitor_privileges", false)
74+
.user("user_with_monitor_privileges", "x-pack-test-password", "user_with_monitor_privileges", false)
7275
.build();
7376

7477
@Override
@@ -309,7 +312,7 @@ public void testIndexPatternErrorMessageComparison_ESQL_SearchDSL() throws Excep
309312
json.endObject();
310313
Request searchRequest = new Request("GET", "/index-user1,index-user2/_search");
311314
searchRequest.setJsonEntity(Strings.toString(json));
312-
searchRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", "metadata1_read2"));
315+
setUser(searchRequest, "metadata1_read2");
313316

314317
// ES|QL query on the same index pattern
315318
var esqlResp = expectThrows(ResponseException.class, () -> runESQLCommand("metadata1_read2", "FROM index-user1,index-user2"));
@@ -429,13 +432,13 @@ public void testFieldLevelSecurityAllow() throws Exception {
429432

430433
public void testFieldLevelSecurityAllowPartial() throws Exception {
431434
Request request = new Request("GET", "/index*/_field_caps");
432-
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", "fls_user"));
435+
setUser(request, "fls_user");
433436
request.addParameter("error_trace", "true");
434437
request.addParameter("pretty", "true");
435438
request.addParameter("fields", "*");
436439

437440
request = new Request("GET", "/index*/_search");
438-
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", "fls_user"));
441+
setUser(request, "fls_user");
439442
request.addParameter("error_trace", "true");
440443
request.addParameter("pretty", "true");
441444

@@ -761,6 +764,36 @@ public void testFromLookupIndexForbidden() throws Exception {
761764
assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST));
762765
}
763766

767+
public void testListQueryAllowed() throws Exception {
768+
Request request = new Request("GET", "_query/queries");
769+
setUser(request, "user_with_monitor_privileges");
770+
var resp = client().performRequest(request);
771+
assertOK(resp);
772+
}
773+
774+
public void testListQueryForbidden() throws Exception {
775+
Request request = new Request("GET", "_query/queries");
776+
setUser(request, "user_without_monitor_privileges");
777+
var resp = expectThrows(ResponseException.class, () -> client().performRequest(request));
778+
assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(403));
779+
assertThat(resp.getMessage(), containsString("this action is granted by the cluster privileges [monitor_esql,monitor,manage,all]"));
780+
}
781+
782+
public void testGetQueryAllowed() throws Exception {
783+
// This is a bit tricky, since there is no such running query. We just make sure it didn't fail on forbidden privileges.
784+
Request request = new Request("GET", "_query/queries/foo:1234");
785+
var resp = expectThrows(ResponseException.class, () -> client().performRequest(request));
786+
assertThat(resp.getResponse().getStatusLine().getStatusCode(), not(equalTo(404)));
787+
}
788+
789+
public void testGetQueryForbidden() throws Exception {
790+
Request request = new Request("GET", "_query/queries/foo:1234");
791+
setUser(request, "user_without_monitor_privileges");
792+
var resp = expectThrows(ResponseException.class, () -> client().performRequest(request));
793+
assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(403));
794+
assertThat(resp.getMessage(), containsString("this action is granted by the cluster privileges [monitor_esql,monitor,manage,all]"));
795+
}
796+
764797
private void createEnrichPolicy() throws Exception {
765798
createIndex("songs", Settings.EMPTY, """
766799
"properties":{"song_id": {"type": "keyword"}, "title": {"type": "keyword"}, "artist": {"type": "keyword"} }
@@ -837,11 +870,16 @@ protected Response runESQLCommand(String user, String command) throws IOExceptio
837870
json.endObject();
838871
Request request = new Request("POST", "_query");
839872
request.setJsonEntity(Strings.toString(json));
840-
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user));
873+
setUser(request, user);
841874
request.addParameter("error_trace", "true");
842875
return client().performRequest(request);
843876
}
844877

878+
private static void setUser(Request request, String user) {
879+
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user));
880+
881+
}
882+
845883
static void addRandomPragmas(XContentBuilder builder) throws IOException {
846884
if (Build.current().isSnapshot()) {
847885
Settings pragmas = randomPragmas();
@@ -853,7 +891,7 @@ static void addRandomPragmas(XContentBuilder builder) throws IOException {
853891
}
854892
}
855893

856-
static Settings randomPragmas() {
894+
private static Settings randomPragmas() {
857895
Settings.Builder settings = Settings.builder();
858896
if (randomBoolean()) {
859897
settings.put("page_size", between(1, 5));

x-pack/plugin/esql/qa/security/src/javaRestTest/resources/roles.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,10 @@ logs_foo_after_2021_alias:
193193
"@timestamp": {"gte": "2021-01-01T00:00:00"}
194194
}
195195
}
196+
197+
user_without_monitor_privileges:
198+
cluster: []
199+
200+
user_with_monitor_privileges:
201+
cluster:
202+
- monitor_esql

0 commit comments

Comments
 (0)