Skip to content

Commit 6134a8e

Browse files
committed
Security fixes
1 parent f786222 commit 6134a8e

File tree

6 files changed

+120
-48
lines changed

6 files changed

+120
-48
lines changed

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

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

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlGetQueryAction.java

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
package org.elasticsearch.xpack.esql.plugin;
99

1010
import org.elasticsearch.action.ActionListener;
11-
import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskRequestBuilder;
11+
import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskRequest;
1212
import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskResponse;
13-
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequestBuilder;
13+
import org.elasticsearch.action.admin.cluster.node.tasks.get.TransportGetTaskAction;
14+
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
1415
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
16+
import org.elasticsearch.action.admin.cluster.node.tasks.list.TransportListTasksAction;
1517
import org.elasticsearch.action.support.ActionFilters;
1618
import org.elasticsearch.action.support.HandledTransportAction;
1719
import org.elasticsearch.client.internal.node.NodeClient;
@@ -21,9 +23,12 @@
2123
import org.elasticsearch.tasks.Task;
2224
import org.elasticsearch.tasks.TaskInfo;
2325
import org.elasticsearch.transport.TransportService;
26+
import org.elasticsearch.xpack.core.ClientHelper;
2427
import org.elasticsearch.xpack.esql.action.EsqlGetQueryAction;
2528
import org.elasticsearch.xpack.esql.action.EsqlGetQueryRequest;
2629

30+
import static org.elasticsearch.xpack.core.ClientHelper.ESQL_ORIGIN;
31+
2732
public class TransportEsqlGetQueryAction extends HandledTransportAction<EsqlGetQueryRequest, EsqlGetQueryResponse> {
2833
private final NodeClient nodeClient;
2934

@@ -35,33 +40,44 @@ public TransportEsqlGetQueryAction(TransportService transportService, NodeClient
3540

3641
@Override
3742
protected void doExecute(Task task, EsqlGetQueryRequest request, ActionListener<EsqlGetQueryResponse> listener) {
38-
new GetTaskRequestBuilder(nodeClient).setTaskId(request.id()).execute(new ActionListener<>() {
39-
@Override
40-
public void onResponse(GetTaskResponse response) {
41-
TaskInfo task = response.getTask().getTask();
42-
new ListTasksRequestBuilder(nodeClient).setDetailed(true)
43-
.setActions(DriverTaskRunner.ACTION_NAME)
44-
.setTargetParentTaskId(request.id())
45-
.execute(new ActionListener<>() {
46-
@Override
47-
public void onResponse(ListTasksResponse response) {
48-
listener.onResponse(new EsqlGetQueryResponse(toDetailedQuery(task, response)));
49-
}
43+
ClientHelper.executeAsyncWithOrigin(
44+
nodeClient,
45+
ESQL_ORIGIN,
46+
TransportGetTaskAction.TYPE,
47+
new GetTaskRequest().setTaskId(request.id()),
48+
new ActionListener<>() {
49+
@Override
50+
public void onResponse(GetTaskResponse response) {
51+
TaskInfo task = response.getTask().getTask();
52+
ClientHelper.executeAsyncWithOrigin(
53+
nodeClient,
54+
ESQL_ORIGIN,
55+
TransportListTasksAction.TYPE,
56+
new ListTasksRequest().setDetailed(true)
57+
.setActions(DriverTaskRunner.ACTION_NAME)
58+
.setTargetParentTaskId(request.id()),
59+
new ActionListener<>() {
60+
@Override
61+
public void onResponse(ListTasksResponse response) {
62+
listener.onResponse(new EsqlGetQueryResponse(toDetailedQuery(task, response)));
63+
}
5064

51-
@Override
52-
public void onFailure(Exception e) {
53-
listener.onFailure(e);
65+
@Override
66+
public void onFailure(Exception e) {
67+
listener.onFailure(e);
68+
}
5469
}
55-
});
56-
}
70+
);
71+
}
5772

58-
@Override
59-
public void onFailure(Exception e) {
60-
// The underlying root cause is meaningless to the user, but that is what will be shown, so we remove it.
61-
var withoutCause = new Exception(e.getMessage());
62-
listener.onFailure(withoutCause);
73+
@Override
74+
public void onFailure(Exception e) {
75+
// The underlying root cause is meaningless to the user, but that is what will be shown, so we remove it.
76+
var withoutCause = new Exception(e.getMessage());
77+
listener.onFailure(withoutCause);
78+
}
6379
}
64-
});
80+
);
6581
}
6682

6783
private static EsqlGetQueryResponse.DetailedQuery toDetailedQuery(TaskInfo task, ListTasksResponse response) {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlListQueriesAction.java

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
package org.elasticsearch.xpack.esql.plugin;
99

1010
import org.elasticsearch.action.ActionListener;
11-
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequestBuilder;
11+
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
1212
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
13+
import org.elasticsearch.action.admin.cluster.node.tasks.list.TransportListTasksAction;
1314
import org.elasticsearch.action.support.ActionFilters;
1415
import org.elasticsearch.action.support.HandledTransportAction;
1516
import org.elasticsearch.client.internal.node.NodeClient;
@@ -18,13 +19,16 @@
1819
import org.elasticsearch.tasks.Task;
1920
import org.elasticsearch.tasks.TaskInfo;
2021
import org.elasticsearch.transport.TransportService;
22+
import org.elasticsearch.xpack.core.ClientHelper;
2123
import org.elasticsearch.xpack.esql.action.EsqlListQueriesAction;
2224
import org.elasticsearch.xpack.esql.action.EsqlListQueriesRequest;
2325
import org.elasticsearch.xpack.esql.action.EsqlQueryAction;
2426
import org.elasticsearch.xpack.esql.core.async.AsyncTaskManagementService;
2527

2628
import java.util.List;
2729

30+
import static org.elasticsearch.xpack.core.ClientHelper.ESQL_ORIGIN;
31+
2832
public class TransportEsqlListQueriesAction extends HandledTransportAction<EsqlListQueriesRequest, EsqlListQueriesResponse> {
2933
private final NodeClient nodeClient;
3034

@@ -42,24 +46,28 @@ public TransportEsqlListQueriesAction(TransportService transportService, NodeCli
4246

4347
@Override
4448
protected void doExecute(Task task, EsqlListQueriesRequest request, ActionListener<EsqlListQueriesResponse> listener) {
45-
new ListTasksRequestBuilder(nodeClient).setActions(
46-
EsqlQueryAction.NAME,
47-
EsqlQueryAction.NAME + AsyncTaskManagementService.ASYNC_ACTION_SUFFIX
48-
).setDetailed(true).execute(new ActionListener<>() {
49-
@Override
50-
public void onResponse(ListTasksResponse response) {
51-
List<EsqlListQueriesResponse.Query> queries = response.getTasks()
52-
.stream()
53-
.map(TransportEsqlListQueriesAction::toQuery)
54-
.toList();
55-
listener.onResponse(new EsqlListQueriesResponse(queries));
56-
}
49+
ClientHelper.executeAsyncWithOrigin(
50+
nodeClient,
51+
ESQL_ORIGIN,
52+
TransportListTasksAction.TYPE,
53+
new ListTasksRequest().setActions(EsqlQueryAction.NAME, EsqlQueryAction.NAME + AsyncTaskManagementService.ASYNC_ACTION_SUFFIX)
54+
.setDetailed(true),
55+
new ActionListener<>() {
56+
@Override
57+
public void onResponse(ListTasksResponse response) {
58+
List<EsqlListQueriesResponse.Query> queries = response.getTasks()
59+
.stream()
60+
.map(TransportEsqlListQueriesAction::toQuery)
61+
.toList();
62+
listener.onResponse(new EsqlListQueriesResponse(queries));
63+
}
5764

58-
@Override
59-
public void onFailure(Exception e) {
60-
listener.onFailure(e);
65+
@Override
66+
public void onFailure(Exception e) {
67+
listener.onFailure(e);
68+
}
6169
}
62-
});
70+
);
6371
}
6472

6573
private static EsqlListQueriesResponse.Query toQuery(TaskInfo taskInfo) {

x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ setup:
1515
# This is fragile - it needs to be updated every time we add a new cluster/index privilege
1616
# I would much prefer we could just check that specific entries are in the array, but we don't have
1717
# an assertion for that
18-
- length: { "cluster" : 62 }
18+
- length: { "cluster" : 63 }
1919
- length: { "index" : 24 }

0 commit comments

Comments
 (0)