Browse Source

Handle the indices pattern `["*", "-*"]` when grouping indices by cluster name (#128610) (#128822)

The auth code injects the pattern `["*", "-*"]` to specify that it's okay to return an empty response because user's patterns did not match any remote clusters. However, we fail to recognise this specific pattern and `groupIndices()` eventually associates it with the local cluster. This causes Elasticsearch to fallback to the local cluster unknowingly and return its details back to the user even though the user hasn't requested any info about the local cluster.
Pawan Kartik 4 months ago
parent
commit
c27de5ad9d

+ 6 - 0
docs/changelog/128610.yaml

@@ -0,0 +1,6 @@
+pr: 128610
+summary: "Handle the indices pattern `[\"*\", \"-*\"]` when grouping indices by cluster\
+  \ name"
+area: CCS
+type: bug
+issues: []

+ 1 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

@@ -1289,7 +1289,7 @@ public class IndexNameExpressionResolver {
     /**
      * Identifies if this expression list is *,-* which effectively means a request that requests no indices.
      */
-    static boolean isNoneExpression(String[] expressions) {
+    public static boolean isNoneExpression(String[] expressions) {
         return expressions.length == 2 && "*".equals(expressions[0]) && "-*".equals(expressions[1]);
     }
 

+ 18 - 1
server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java

@@ -19,6 +19,7 @@ import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.action.support.PlainActionFuture;
 import org.elasticsearch.action.support.RefCountingRunnable;
 import org.elasticsearch.client.internal.RemoteClusterClient;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.node.DiscoveryNodeRole;
 import org.elasticsearch.common.Strings;
@@ -201,7 +202,23 @@ public final class RemoteClusterService extends RemoteClusterAware
         boolean returnLocalAll
     ) {
         final Map<String, OriginalIndices> originalIndicesMap = new HashMap<>();
-        final Map<String, List<String>> groupedIndices = groupClusterIndices(remoteClusterNames, indices);
+        final Map<String, List<String>> groupedIndices;
+        /*
+         * returnLocalAll is used to control whether we'd like to fallback to the local cluster.
+         * While this is acceptable in a few cases, there are cases where we should not fallback to the local
+         * cluster. Consider _resolve/cluster where the specified patterns do not match any remote clusters.
+         * Falling back to the local cluster and returning its details in such cases is not ok. This is why
+         * TransportResolveClusterAction sets returnLocalAll to false wherever it uses groupIndices().
+         *
+         * If such a fallback isn't allowed and the given indices match a pattern whose semantics mean that
+         * it's ok to return an empty result (denoted via ["*", "-*"]), empty groupIndices.
+         */
+        if (returnLocalAll == false && IndexNameExpressionResolver.isNoneExpression(indices)) {
+            groupedIndices = Map.of();
+        } else {
+            groupedIndices = groupClusterIndices(remoteClusterNames, indices);
+        }
+
         if (groupedIndices.isEmpty()) {
             if (returnLocalAll) {
                 // search on _all in the local cluster if neither local indices nor remote indices were specified

+ 9 - 15
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1ResolveClusterIT.java

@@ -281,21 +281,15 @@ public class RemoteClusterSecurityRCS1ResolveClusterIT extends AbstractRemoteClu
             );
             assertThat(exc.getMessage(), containsString(indexOptionTuple.v1()));
         }
-        // TODO: The security pathways are not using the new
-        // RemoteClusterService.groupIndices(IndicesOptions indicesOptions, String[] indices, boolean returnLocalAll) method
-        // so this use case still behaves badly - fix in follow on PR
-        // {
-        // // TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes
-        // final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
-        // Response response = performRequestWithRemoteSearchUser(remoteOnly1);
-        // assertOK(response);
-        // Map<String, Object> responseMap = responseAsMap(response);
-        // assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue());
-        // Map<String, Object> remoteMap = (Map<String, Object>) responseMap.get("my_remote_cluster");
-        // assertThat((Boolean) remoteMap.get("connected"), equalTo(true));
-        // assertThat((Boolean) remoteMap.get("matching_indices"), equalTo(false));
-        // assertThat(remoteMap.get("version"), notNullValue());
-        // }
+        {
+            // TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes should result in an
+            // empty response and not fall back to the local cluster.
+            final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
+            Response response = performRequestWithRemoteSearchUser(remoteOnly1);
+            assertOK(response);
+            Map<String, Object> responseMap = responseAsMap(response);
+            assertThat(responseMap.isEmpty(), is(true));
+        }
     }
 
     private Response performRequestWithRemoteSearchUser(final Request request) throws IOException {

+ 9 - 13
x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2ResolveClusterIT.java

@@ -376,19 +376,15 @@ public class RemoteClusterSecurityRCS2ResolveClusterIT extends AbstractRemoteClu
             );
             assertThat(exc.getMessage(), containsString(indexOptionTuple.v1()));
         }
-        // TODO: fix this in a follow-on PR
-        // {
-        // // TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes
-        // final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
-        // Response response = performRequestWithRemoteSearchUser(remoteOnly1);
-        // assertOK(response);
-        // Map<String, Object> responseMap = responseAsMap(response);
-        // assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue());
-        // Map<String, Object> remoteMap = (Map<String, Object>) responseMap.get("my_remote_cluster");
-        // assertThat((Boolean) remoteMap.get("connected"), equalTo(true));
-        // assertThat((Boolean) remoteMap.get("matching_indices"), equalTo(false));
-        // assertThat(remoteMap.get("version"), notNullValue());
-        // }
+        {
+            // TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes should result in an
+            // empty response and not fall back to the local cluster.
+            final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
+            Response response = performRequestWithRemoteSearchUser(remoteOnly1);
+            assertOK(response);
+            Map<String, Object> responseMap = responseAsMap(response);
+            assertThat(responseMap.isEmpty(), is(true));
+        }
     }
 
     private Response performRequestWithRemoteSearchUser(final Request request) throws IOException {