Răsfoiți Sursa

ESQL: fix for missing indices error message (#111797)

Reverts a part of https://github.com/elastic/elasticsearch/pull/109483
by going back to the previous (more restrictive) way of dealing with
missing indices or aliases. More specifically, if an index pattern used
in a query refers to a missing index or alias name and doesn't use a
wildcard for this name, then we error out. Our lack of testing in this
area made the change in
https://github.com/elastic/elasticsearch/pull/109483 to be invisible.

Fixes https://github.com/elastic/elasticsearch/issues/111712
Andrei Stefan 1 an în urmă
părinte
comite
6d076dfa17

+ 6 - 0
docs/changelog/111797.yaml

@@ -0,0 +1,6 @@
+pr: 111797
+summary: "ESQL: fix for missing indices error message"
+area: ES|QL
+type: bug
+issues:
+ - 111712

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

@@ -18,6 +18,7 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.test.MapMatcher;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.local.distribution.DistributionType;
@@ -160,7 +161,7 @@ public class EsqlSecurityIT extends ESRestTestCase {
                 .entry("values", List.of(List.of(72.0d)));
             assertMap(entityAsMap(resp), matcher);
         }
-        for (var index : List.of("index-user2", "index-user1,index-user2", "index-user*", "index*")) {
+        for (var index : List.of("index-user2", "index-user*", "index*")) {
             Response resp = runESQLCommand("metadata1_read2", "from " + index + " | stats sum=sum(value)");
             assertOK(resp);
             MapMatcher matcher = responseMatcher().entry("columns", List.of(Map.of("name", "sum", "type", "double")))
@@ -170,7 +171,7 @@ public class EsqlSecurityIT extends ESRestTestCase {
     }
 
     public void testAliases() throws Exception {
-        for (var index : List.of("second-alias", "second-alias,index-user2", "second-*", "second-*,index*")) {
+        for (var index : List.of("second-alias", "second-*", "second-*,index*")) {
             Response resp = runESQLCommand(
                 "alias_user2",
                 "from " + index + " METADATA _index" + "| stats sum=sum(value), index=VALUES(_index)"
@@ -185,7 +186,7 @@ public class EsqlSecurityIT extends ESRestTestCase {
     }
 
     public void testAliasFilter() throws Exception {
-        for (var index : List.of("first-alias", "first-alias,index-user1", "first-alias,index-*", "first-*,index-*")) {
+        for (var index : List.of("first-alias", "first-alias,index-*", "first-*,index-*")) {
             Response resp = runESQLCommand("alias_user1", "from " + index + " METADATA _index" + "| KEEP _index, org, value | LIMIT 10");
             assertOK(resp);
             MapMatcher matcher = responseMatcher().entry(
@@ -221,19 +222,97 @@ public class EsqlSecurityIT extends ESRestTestCase {
         assertThat(error.getMessage(), containsString("Unknown index [index-user1]"));
     }
 
+    public void testIndexPatternErrorMessageComparison_ESQL_SearchDSL() throws Exception {
+        // _search match_all query on the index-user1,index-user2 index pattern
+        XContentBuilder json = JsonXContent.contentBuilder();
+        json.startObject();
+        json.field("query", QueryBuilders.matchAllQuery());
+        json.endObject();
+        Request searchRequest = new Request("GET", "/index-user1,index-user2/_search");
+        searchRequest.setJsonEntity(Strings.toString(json));
+        searchRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", "metadata1_read2"));
+
+        // ES|QL query on the same index pattern
+        var esqlResp = expectThrows(ResponseException.class, () -> runESQLCommand("metadata1_read2", "FROM index-user1,index-user2"));
+        var srchResp = expectThrows(ResponseException.class, () -> client().performRequest(searchRequest));
+
+        for (ResponseException r : List.of(esqlResp, srchResp)) {
+            assertThat(
+                EntityUtils.toString(r.getResponse().getEntity()),
+                containsString(
+                    "unauthorized for user [test-admin] run as [metadata1_read2] with effective roles [metadata1_read2] on indices [index-user1]"
+                )
+            );
+        }
+        assertThat(esqlResp.getResponse().getStatusLine().getStatusCode(), equalTo(srchResp.getResponse().getStatusLine().getStatusCode()));
+    }
+
     public void testLimitedPrivilege() throws Exception {
-        Response resp = runESQLCommand("metadata1_read2", """
-            FROM index-user1,index-user2 METADATA _index
-            | STATS sum=sum(value), index=VALUES(_index)
-            """);
-        assertOK(resp);
-        Map<String, Object> respMap = entityAsMap(resp);
+        ResponseException resp = expectThrows(
+            ResponseException.class,
+            () -> runESQLCommand(
+                "metadata1_read2",
+                "FROM index-user1,index-user2 METADATA _index | STATS sum=sum(value), index=VALUES(_index)"
+            )
+        );
         assertThat(
-            respMap.get("columns"),
-            equalTo(List.of(Map.of("name", "sum", "type", "double"), Map.of("name", "index", "type", "keyword")))
+            EntityUtils.toString(resp.getResponse().getEntity()),
+            containsString(
+                "unauthorized for user [test-admin] run as [metadata1_read2] with effective roles [metadata1_read2] on indices [index-user1]"
+            )
+        );
+        assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
+
+        resp = expectThrows(
+            ResponseException.class,
+            () -> runESQLCommand("metadata1_read2", "FROM index-user1,index-user2 METADATA _index | STATS index=VALUES(_index)")
         );
-        assertThat(respMap.get("values"), equalTo(List.of(List.of(72.0, "index-user2"))));
+        assertThat(
+            EntityUtils.toString(resp.getResponse().getEntity()),
+            containsString(
+                "unauthorized for user [test-admin] run as [metadata1_read2] with effective roles [metadata1_read2] on indices [index-user1]"
+            )
+        );
+        assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
 
+        resp = expectThrows(
+            ResponseException.class,
+            () -> runESQLCommand("metadata1_read2", "FROM index-user1,index-user2 | STATS sum=sum(value)")
+        );
+        assertThat(
+            EntityUtils.toString(resp.getResponse().getEntity()),
+            containsString(
+                "unauthorized for user [test-admin] run as [metadata1_read2] with effective roles [metadata1_read2] on indices [index-user1]"
+            )
+        );
+        assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
+
+        resp = expectThrows(
+            ResponseException.class,
+            () -> runESQLCommand("alias_user1", "FROM first-alias,index-user1 METADATA _index | KEEP _index, org, value | LIMIT 10")
+        );
+        assertThat(
+            EntityUtils.toString(resp.getResponse().getEntity()),
+            containsString(
+                "unauthorized for user [test-admin] run as [alias_user1] with effective roles [alias_user1] on indices [index-user1]"
+            )
+        );
+        assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
+
+        resp = expectThrows(
+            ResponseException.class,
+            () -> runESQLCommand(
+                "alias_user2",
+                "from second-alias,index-user2 METADATA _index | stats sum=sum(value), index=VALUES(_index)"
+            )
+        );
+        assertThat(
+            EntityUtils.toString(resp.getResponse().getEntity()),
+            containsString(
+                "unauthorized for user [test-admin] run as [alias_user2] with effective roles [alias_user2] on indices [index-user2]"
+            )
+        );
+        assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN));
     }
 
     public void testDocumentLevelSecurity() throws Exception {

+ 6 - 2
x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/Clusters.java

@@ -12,9 +12,13 @@ import org.elasticsearch.test.cluster.local.distribution.DistributionType;
 import org.elasticsearch.test.cluster.util.Version;
 
 public class Clusters {
+
+    static final String REMOTE_CLUSTER_NAME = "remote_cluster";
+    static final String LOCAL_CLUSTER_NAME = "local_cluster";
+
     public static ElasticsearchCluster remoteCluster() {
         return ElasticsearchCluster.local()
-            .name("remote_cluster")
+            .name(REMOTE_CLUSTER_NAME)
             .distribution(DistributionType.DEFAULT)
             .version(Version.fromString(System.getProperty("tests.old_cluster_version")))
             .nodes(2)
@@ -28,7 +32,7 @@ public class Clusters {
 
     public static ElasticsearchCluster localCluster(ElasticsearchCluster remoteCluster) {
         return ElasticsearchCluster.local()
-            .name("local_cluster")
+            .name(LOCAL_CLUSTER_NAME)
             .distribution(DistributionType.DEFAULT)
             .version(Version.CURRENT)
             .nodes(2)

+ 81 - 0
x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/EsqlRestValidationIT.java

@@ -0,0 +1,81 @@
+/*
+ * 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.ccq;
+
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
+
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.core.IOUtils;
+import org.elasticsearch.test.TestClustersThreadFilter;
+import org.elasticsearch.test.cluster.ElasticsearchCluster;
+import org.elasticsearch.xpack.esql.qa.rest.EsqlRestValidationTestCase;
+import org.junit.AfterClass;
+import org.junit.ClassRule;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+import java.io.IOException;
+import java.util.StringJoiner;
+
+import static org.elasticsearch.xpack.esql.ccq.Clusters.REMOTE_CLUSTER_NAME;
+
+@ThreadLeakFilters(filters = TestClustersThreadFilter.class)
+public class EsqlRestValidationIT extends EsqlRestValidationTestCase {
+    static ElasticsearchCluster remoteCluster = Clusters.remoteCluster();
+    static ElasticsearchCluster localCluster = Clusters.localCluster(remoteCluster);
+
+    @ClassRule
+    public static TestRule clusterRule = RuleChain.outerRule(remoteCluster).around(localCluster);
+    private static RestClient remoteClient;
+
+    @Override
+    protected String getTestRestCluster() {
+        return localCluster.getHttpAddresses();
+    }
+
+    @AfterClass
+    public static void closeRemoteClients() throws IOException {
+        try {
+            IOUtils.close(remoteClient);
+        } finally {
+            remoteClient = null;
+        }
+    }
+
+    @Override
+    protected String clusterSpecificIndexName(String pattern) {
+        StringJoiner sj = new StringJoiner(",");
+        for (String index : pattern.split(",")) {
+            sj.add(remoteClusterIndex(index));
+        }
+        return sj.toString();
+    }
+
+    private static String remoteClusterIndex(String indexName) {
+        return REMOTE_CLUSTER_NAME + ":" + indexName;
+    }
+
+    @Override
+    protected RestClient provisioningClient() throws IOException {
+        return remoteClusterClient();
+    }
+
+    @Override
+    protected RestClient provisioningAdminClient() throws IOException {
+        return remoteClusterClient();
+    }
+
+    private RestClient remoteClusterClient() throws IOException {
+        if (remoteClient == null) {
+            var clusterHosts = parseClusterHosts(remoteCluster.getHttpAddresses());
+            remoteClient = buildClient(restClientSettings(), clusterHosts.toArray(new HttpHost[0]));
+        }
+        return remoteClient;
+    }
+}

+ 27 - 0
x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlRestValidationIT.java

@@ -0,0 +1,27 @@
+/*
+ * 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.qa.multi_node;
+
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
+
+import org.elasticsearch.test.TestClustersThreadFilter;
+import org.elasticsearch.test.cluster.ElasticsearchCluster;
+import org.elasticsearch.xpack.esql.qa.rest.EsqlRestValidationTestCase;
+import org.junit.ClassRule;
+
+@ThreadLeakFilters(filters = TestClustersThreadFilter.class)
+public class EsqlRestValidationIT extends EsqlRestValidationTestCase {
+
+    @ClassRule
+    public static ElasticsearchCluster cluster = Clusters.testCluster(spec -> {});
+
+    @Override
+    protected String getTestRestCluster() {
+        return cluster.getHttpAddresses();
+    }
+}

+ 27 - 0
x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/EsqlRestValidationIT.java

@@ -0,0 +1,27 @@
+/*
+ * 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.qa.single_node;
+
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
+
+import org.elasticsearch.test.TestClustersThreadFilter;
+import org.elasticsearch.test.cluster.ElasticsearchCluster;
+import org.elasticsearch.xpack.esql.qa.rest.EsqlRestValidationTestCase;
+import org.junit.ClassRule;
+
+@ThreadLeakFilters(filters = TestClustersThreadFilter.class)
+public class EsqlRestValidationIT extends EsqlRestValidationTestCase {
+
+    @ClassRule
+    public static ElasticsearchCluster cluster = Clusters.testCluster();
+
+    @Override
+    protected String getTestRestCluster() {
+        return cluster.getHttpAddresses();
+    }
+}

+ 170 - 0
x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/EsqlRestValidationTestCase.java

@@ -0,0 +1,170 @@
+/*
+ * 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.qa.rest;
+
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.WarningsHandler;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.test.rest.ESRestTestCase;
+import org.elasticsearch.xcontent.json.JsonXContent;
+import org.junit.After;
+import org.junit.Before;
+
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public abstract class EsqlRestValidationTestCase extends ESRestTestCase {
+
+    private static final String indexName = "test_esql";
+    private static final String aliasName = "alias-test_esql";
+    protected static final String[] existentIndexWithWildcard = new String[] {
+        indexName + ",inexistent*",
+        indexName + "*,inexistent*",
+        "inexistent*," + indexName };
+    private static final String[] existentIndexWithoutWildcard = new String[] { indexName + ",inexistent", "inexistent," + indexName };
+    protected static final String[] existentAliasWithWildcard = new String[] {
+        aliasName + ",inexistent*",
+        aliasName + "*,inexistent*",
+        "inexistent*," + aliasName };
+    private static final String[] existentAliasWithoutWildcard = new String[] { aliasName + ",inexistent", "inexistent," + aliasName };
+    private static final String[] inexistentIndexNameWithWildcard = new String[] { "inexistent*", "inexistent1*,inexistent2*" };
+    private static final String[] inexistentIndexNameWithoutWildcard = new String[] { "inexistent", "inexistent1,inexistent2" };
+    private static final String createAlias = "{\"actions\":[{\"add\":{\"index\":\"" + indexName + "\",\"alias\":\"" + aliasName + "\"}}]}";
+    private static final String removeAlias = "{\"actions\":[{\"remove\":{\"index\":\""
+        + indexName
+        + "\",\"alias\":\""
+        + aliasName
+        + "\"}}]}";
+
+    @Before
+    @After
+    public void assertRequestBreakerEmpty() throws Exception {
+        EsqlSpecTestCase.assertRequestBreakerEmpty();
+    }
+
+    @Before
+    public void prepareIndices() throws IOException {
+        if (provisioningClient().performRequest(new Request("HEAD", "/" + indexName)).getStatusLine().getStatusCode() == 404) {
+            var request = new Request("PUT", "/" + indexName);
+            request.setJsonEntity("{\"mappings\": {\"properties\": {\"foo\":{\"type\":\"keyword\"}}}}");
+            provisioningClient().performRequest(request);
+        }
+        assertOK(provisioningAdminClient().performRequest(new Request("POST", "/" + indexName + "/_refresh")));
+    }
+
+    @After
+    public void wipeTestData() throws IOException {
+        try {
+            var response = provisioningAdminClient().performRequest(new Request("DELETE", "/" + indexName));
+            assertEquals(200, response.getStatusLine().getStatusCode());
+        } catch (ResponseException re) {
+            assertEquals(404, re.getResponse().getStatusLine().getStatusCode());
+        }
+    }
+
+    private String getInexistentIndexErrorMessage() {
+        return "\"reason\" : \"Found 1 problem\\nline 1:1: Unknown index ";
+    }
+
+    public void testInexistentIndexNameWithWildcard() throws IOException {
+        assertErrorMessages(inexistentIndexNameWithWildcard, getInexistentIndexErrorMessage(), 400);
+    }
+
+    public void testInexistentIndexNameWithoutWildcard() throws IOException {
+        assertErrorMessages(inexistentIndexNameWithoutWildcard, getInexistentIndexErrorMessage(), 400);
+    }
+
+    public void testExistentIndexWithoutWildcard() throws IOException {
+        for (String indexName : existentIndexWithoutWildcard) {
+            assertErrorMessage(indexName, "\"reason\" : \"no such index [inexistent]\"", 404);
+        }
+    }
+
+    public void testExistentIndexWithWildcard() throws IOException {
+        assertValidRequestOnIndices(existentIndexWithWildcard);
+    }
+
+    public void testAlias() throws IOException {
+        createAlias();
+
+        for (String indexName : existentAliasWithoutWildcard) {
+            assertErrorMessage(indexName, "\"reason\" : \"no such index [inexistent]\"", 404);
+        }
+        assertValidRequestOnIndices(existentAliasWithWildcard);
+
+        deleteAlias();
+    }
+
+    private void assertErrorMessages(String[] indices, String errorMessage, int statusCode) throws IOException {
+        for (String indexName : indices) {
+            assertErrorMessage(indexName, errorMessage + "[" + clusterSpecificIndexName(indexName) + "]", statusCode);
+        }
+    }
+
+    protected String clusterSpecificIndexName(String indexName) {
+        return indexName;
+    }
+
+    private void assertErrorMessage(String indexName, String errorMessage, int statusCode) throws IOException {
+        var specificName = clusterSpecificIndexName(indexName);
+        final var request = createRequest(specificName);
+        ResponseException exc = expectThrows(ResponseException.class, () -> client().performRequest(request));
+
+        assertThat(exc.getResponse().getStatusLine().getStatusCode(), equalTo(statusCode));
+        assertThat(exc.getMessage(), containsString(errorMessage));
+    }
+
+    private Request createRequest(String indexName) throws IOException {
+        final var request = new Request("POST", "/_query");
+        request.addParameter("error_trace", "true");
+        request.addParameter("pretty", "true");
+        request.setJsonEntity(
+            Strings.toString(JsonXContent.contentBuilder().startObject().field("query", "from " + indexName).endObject())
+        );
+        RequestOptions.Builder options = request.getOptions().toBuilder();
+        options.setWarningsHandler(WarningsHandler.PERMISSIVE);
+        request.setOptions(options);
+        return request;
+    }
+
+    private void assertValidRequestOnIndices(String[] indices) throws IOException {
+        for (String indexName : indices) {
+            final var request = createRequest(clusterSpecificIndexName(indexName));
+            Response response = client().performRequest(request);
+            assertOK(response);
+        }
+    }
+
+    // Returned client is used to load the test data, either in the local cluster or a remote one (for
+    // multi-clusters). The client()/adminClient() will always connect to the local cluster
+    protected RestClient provisioningClient() throws IOException {
+        return client();
+    }
+
+    protected RestClient provisioningAdminClient() throws IOException {
+        return adminClient();
+    }
+
+    private void createAlias() throws IOException {
+        var r = new Request("POST", "_aliases");
+        r.setJsonEntity(createAlias);
+        assertOK(provisioningClient().performRequest(r));
+    }
+
+    private void deleteAlias() throws IOException {
+        var r = new Request("POST", "/_aliases/");
+        r.setJsonEntity(removeAlias);
+        assertOK(provisioningAdminClient().performRequest(r));
+    }
+}

+ 3 - 6
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java

@@ -11,11 +11,11 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.ActionListenerResponseHandler;
 import org.elasticsearch.action.ActionRunnable;
 import org.elasticsearch.action.OriginalIndices;
+import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchShardsGroup;
 import org.elasticsearch.action.search.SearchShardsRequest;
 import org.elasticsearch.action.search.SearchShardsResponse;
 import org.elasticsearch.action.support.ChannelActionListener;
-import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.action.support.RefCountingListener;
 import org.elasticsearch.action.support.RefCountingRunnable;
 import org.elasticsearch.cluster.node.DiscoveryNode;
@@ -68,7 +68,6 @@ import org.elasticsearch.xpack.esql.planner.EsPhysicalOperationProviders;
 import org.elasticsearch.xpack.esql.planner.LocalExecutionPlanner;
 import org.elasticsearch.xpack.esql.planner.PlannerUtils;
 import org.elasticsearch.xpack.esql.session.Configuration;
-import org.elasticsearch.xpack.esql.session.IndexResolver;
 import org.elasticsearch.xpack.esql.session.Result;
 
 import java.util.ArrayList;
@@ -98,8 +97,6 @@ public class ComputeService {
     private final EnrichLookupService enrichLookupService;
     private final ClusterService clusterService;
 
-    private static final IndicesOptions DEFAULT_INDICES_OPTIONS = IndexResolver.FIELD_CAPS_INDICES_OPTIONS;
-
     public ComputeService(
         SearchService searchService,
         TransportService transportService,
@@ -152,7 +149,7 @@ public class ComputeService {
             return;
         }
         Map<String, OriginalIndices> clusterToConcreteIndices = transportService.getRemoteClusterService()
-            .groupIndices(DEFAULT_INDICES_OPTIONS, PlannerUtils.planConcreteIndices(physicalPlan).toArray(String[]::new));
+            .groupIndices(SearchRequest.DEFAULT_INDICES_OPTIONS, PlannerUtils.planConcreteIndices(physicalPlan).toArray(String[]::new));
         QueryPragmas queryPragmas = configuration.pragmas();
         if (dataNodePlan == null) {
             if (clusterToConcreteIndices.values().stream().allMatch(v -> v.indices().length == 0) == false) {
@@ -188,7 +185,7 @@ public class ComputeService {
             }
         }
         Map<String, OriginalIndices> clusterToOriginalIndices = transportService.getRemoteClusterService()
-            .groupIndices(DEFAULT_INDICES_OPTIONS, PlannerUtils.planOriginalIndices(physicalPlan));
+            .groupIndices(SearchRequest.DEFAULT_INDICES_OPTIONS, PlannerUtils.planOriginalIndices(physicalPlan));
         var localOriginalIndices = clusterToOriginalIndices.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
         var localConcreteIndices = clusterToConcreteIndices.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
         final var exchangeSource = new ExchangeSourceHandler(

+ 58 - 25
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java

@@ -17,6 +17,7 @@ import org.elasticsearch.common.UUIDs;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.CheckedConsumer;
 import org.elasticsearch.core.Strings;
+import org.elasticsearch.core.Tuple;
 import org.elasticsearch.test.cluster.ElasticsearchCluster;
 import org.elasticsearch.test.cluster.util.resource.Resource;
 import org.elasticsearch.test.junit.RunnableTestRuleAdapter;
@@ -347,21 +348,6 @@ public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTe
             | LIMIT 10"""));
         assertRemoteAndLocalResults(response);
 
-        // query remote cluster only - but also include employees2 which the user does not have access to
-        response = performRequestWithRemoteSearchUser(esqlRequest("""
-            FROM my_remote_cluster:employees,my_remote_cluster:employees2
-            | SORT emp_id ASC
-            | LIMIT 2
-            | KEEP emp_id, department"""));
-        assertRemoteOnlyResults(response); // same as above since the user only has access to employees
-
-        // query remote and local cluster - but also include employees2 which the user does not have access to
-        response = performRequestWithRemoteSearchUser(esqlRequest("""
-            FROM my_remote_cluster:employees,my_remote_cluster:employees2,employees,employees2
-            | SORT emp_id ASC
-            | LIMIT 10"""));
-        assertRemoteAndLocalResults(response); // same as above since the user only has access to employees
-
         // update role to include both employees and employees2 for the remote cluster
         final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE);
         putRoleRequest.setJsonEntity("""
@@ -618,6 +604,37 @@ public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTe
                     + "this action is granted by the index privileges [read,read_cross_cluster,all]"
             )
         );
+
+        // query remote cluster only - but also include employees2 which the user does not have access to
+        error = expectThrows(ResponseException.class, () -> { performRequestWithRemoteSearchUser(esqlRequest("""
+            FROM my_remote_cluster:employees,my_remote_cluster:employees2
+            | SORT emp_id ASC
+            | LIMIT 2
+            | KEEP emp_id, department""")); });
+
+        assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(403));
+        assertThat(
+            error.getMessage(),
+            containsString(
+                "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles "
+                    + "[remote_search], this action is granted by the index privileges [read,read_cross_cluster,all]"
+            )
+        );
+
+        // query remote and local cluster - but also include employees2 which the user does not have access to
+        error = expectThrows(ResponseException.class, () -> { performRequestWithRemoteSearchUser(esqlRequest("""
+            FROM my_remote_cluster:employees,my_remote_cluster:employees2,employees,employees2
+            | SORT emp_id ASC
+            | LIMIT 10""")); });
+
+        assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(403));
+        assertThat(
+            error.getMessage(),
+            containsString(
+                "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles "
+                    + "[remote_search], this action is granted by the index privileges [read,read_cross_cluster,all]"
+            )
+        );
     }
 
     @SuppressWarnings("unchecked")
@@ -841,7 +858,7 @@ public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTe
             }""");
         assertOK(adminClient().performRequest(putRoleRequest));
         // query `employees2`
-        for (String index : List.of("*:employees2", "*:employee*", "*:employee*,*:alias-employees,*:employees3")) {
+        for (String index : List.of("*:employees2", "*:employee*")) {
             Request request = esqlRequest("FROM " + index + " | KEEP emp_id | SORT emp_id | LIMIT 100");
             Response response = performRequestWithRemoteSearchUser(request);
             assertOK(response);
@@ -849,15 +866,7 @@ public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTe
             List<?> ids = (List<?>) responseAsMap.get("values");
             assertThat(ids, equalTo(List.of(List.of("11"), List.of("13"))));
         }
-        // query `alias-engineering`
-        for (var index : List.of("*:alias*", "*:alias*", "*:alias*,my*:employees1", "*:alias*,my*:employees3")) {
-            Request request = esqlRequest("FROM " + index + " | KEEP emp_id | SORT emp_id | LIMIT 100");
-            Response response = performRequestWithRemoteSearchUser(request);
-            assertOK(response);
-            Map<String, Object> responseAsMap = entityAsMap(response);
-            List<?> ids = (List<?>) responseAsMap.get("values");
-            assertThat(ids, equalTo(List.of(List.of("1"), List.of("7"))));
-        }
+
         // query `employees2` and `alias-engineering`
         for (var index : List.of("*:employees2,*:alias-engineering", "*:emp*,*:alias-engineering", "*:emp*,my*:alias*")) {
             Request request = esqlRequest("FROM " + index + " | KEEP emp_id | SORT emp_id | LIMIT 100");
@@ -874,6 +883,30 @@ public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTe
             assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(400));
             assertThat(error.getMessage(), containsString(" Unknown index [" + index + "]"));
         }
+
+        for (var index : List.of(
+            Tuple.tuple("*:employee*,*:alias-employees,*:employees3", "alias-employees,employees3"),
+            Tuple.tuple("*:alias*,my*:employees1", "employees1"),
+            Tuple.tuple("*:alias*,my*:employees3", "employees3")
+        )) {
+            Request request = esqlRequest("FROM " + index.v1() + " | KEEP emp_id | SORT emp_id | LIMIT 100");
+            ResponseException error = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(request));
+            assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(403));
+            assertThat(
+                error.getMessage(),
+                containsString("unauthorized for user [remote_search_user] with assigned roles [remote_search]")
+            );
+            assertThat(error.getMessage(), containsString("user [test_user] on indices [" + index.v2() + "]"));
+        }
+
+        // query `alias-engineering`
+        Request request = esqlRequest("FROM *:alias* | KEEP emp_id | SORT emp_id | LIMIT 100");
+        Response response = performRequestWithRemoteSearchUser(request);
+        assertOK(response);
+        Map<String, Object> responseAsMap = entityAsMap(response);
+        List<?> ids = (List<?>) responseAsMap.get("values");
+        assertThat(ids, equalTo(List.of(List.of("1"), List.of("7"))));
+
         removeAliases();
     }