Skip to content

Commit 8d39904

Browse files
Handle the indices pattern ["*", "-*"] when grouping indices by cluster name (#128610) (#128843)
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. (cherry picked from commit 3f1e1b3) # Conflicts: # server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java
1 parent 2607aa5 commit 8d39904

File tree

5 files changed

+43
-30
lines changed

5 files changed

+43
-30
lines changed

docs/changelog/128610.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 128610
2+
summary: "Handle the indices pattern `[\"*\", \"-*\"]` when grouping indices by cluster\
3+
\ name"
4+
area: CCS
5+
type: bug
6+
issues: []

server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1246,7 +1246,7 @@ static <T> boolean isExplicitAllPattern(Collection<T> aliasesOrIndices, Function
12461246
/**
12471247
* Identifies if this expression list is *,-* which effectively means a request that requests no indices.
12481248
*/
1249-
static boolean isNoneExpression(String[] expressions) {
1249+
public static boolean isNoneExpression(String[] expressions) {
12501250
return expressions.length == 2 && "*".equals(expressions[0]) && "-*".equals(expressions[1]);
12511251
}
12521252

server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.elasticsearch.action.support.PlainActionFuture;
2020
import org.elasticsearch.action.support.RefCountingRunnable;
2121
import org.elasticsearch.client.internal.RemoteClusterClient;
22+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
2223
import org.elasticsearch.cluster.node.DiscoveryNode;
2324
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
2425
import org.elasticsearch.common.Strings;
@@ -195,7 +196,23 @@ boolean isRemoteNodeConnected(final String remoteCluster, final DiscoveryNode no
195196
*/
196197
public Map<String, OriginalIndices> groupIndices(IndicesOptions indicesOptions, String[] indices, boolean returnLocalAll) {
197198
final Map<String, OriginalIndices> originalIndicesMap = new HashMap<>();
198-
final Map<String, List<String>> groupedIndices = groupClusterIndices(getRemoteClusterNames(), indices);
199+
final Map<String, List<String>> groupedIndices;
200+
/*
201+
* returnLocalAll is used to control whether we'd like to fallback to the local cluster.
202+
* While this is acceptable in a few cases, there are cases where we should not fallback to the local
203+
* cluster. Consider _resolve/cluster where the specified patterns do not match any remote clusters.
204+
* Falling back to the local cluster and returning its details in such cases is not ok. This is why
205+
* TransportResolveClusterAction sets returnLocalAll to false wherever it uses groupIndices().
206+
*
207+
* If such a fallback isn't allowed and the given indices match a pattern whose semantics mean that
208+
* it's ok to return an empty result (denoted via ["*", "-*"]), empty groupIndices.
209+
*/
210+
if (returnLocalAll == false && IndexNameExpressionResolver.isNoneExpression(indices)) {
211+
groupedIndices = Map.of();
212+
} else {
213+
groupedIndices = groupClusterIndices(getRemoteClusterNames(), indices);
214+
}
215+
199216
if (groupedIndices.isEmpty()) {
200217
if (returnLocalAll) {
201218
// search on _all in the local cluster if neither local indices nor remote indices were specified

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

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -281,21 +281,15 @@ public void testResolveClusterUnderRCS1() throws Exception {
281281
);
282282
assertThat(exc.getMessage(), containsString(indexOptionTuple.v1()));
283283
}
284-
// TODO: The security pathways are not using the new
285-
// RemoteClusterService.groupIndices(IndicesOptions indicesOptions, String[] indices, boolean returnLocalAll) method
286-
// so this use case still behaves badly - fix in follow on PR
287-
// {
288-
// // TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes
289-
// final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
290-
// Response response = performRequestWithRemoteSearchUser(remoteOnly1);
291-
// assertOK(response);
292-
// Map<String, Object> responseMap = responseAsMap(response);
293-
// assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue());
294-
// Map<String, Object> remoteMap = (Map<String, Object>) responseMap.get("my_remote_cluster");
295-
// assertThat((Boolean) remoteMap.get("connected"), equalTo(true));
296-
// assertThat((Boolean) remoteMap.get("matching_indices"), equalTo(false));
297-
// assertThat(remoteMap.get("version"), notNullValue());
298-
// }
284+
{
285+
// TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes should result in an
286+
// empty response and not fall back to the local cluster.
287+
final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
288+
Response response = performRequestWithRemoteSearchUser(remoteOnly1);
289+
assertOK(response);
290+
Map<String, Object> responseMap = responseAsMap(response);
291+
assertThat(responseMap.isEmpty(), is(true));
292+
}
299293
}
300294

301295
private Response performRequestWithRemoteSearchUser(final Request request) throws IOException {

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

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -376,19 +376,15 @@ public void testResolveCluster() throws Exception {
376376
);
377377
assertThat(exc.getMessage(), containsString(indexOptionTuple.v1()));
378378
}
379-
// TODO: fix this in a follow-on PR
380-
// {
381-
// // TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes
382-
// final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
383-
// Response response = performRequestWithRemoteSearchUser(remoteOnly1);
384-
// assertOK(response);
385-
// Map<String, Object> responseMap = responseAsMap(response);
386-
// assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue());
387-
// Map<String, Object> remoteMap = (Map<String, Object>) responseMap.get("my_remote_cluster");
388-
// assertThat((Boolean) remoteMap.get("connected"), equalTo(true));
389-
// assertThat((Boolean) remoteMap.get("matching_indices"), equalTo(false));
390-
// assertThat(remoteMap.get("version"), notNullValue());
391-
// }
379+
{
380+
// TEST CASE 13: Resolution against wildcarded remote cluster expression that matches no remotes should result in an
381+
// empty response and not fall back to the local cluster.
382+
final Request remoteOnly1 = new Request("GET", "_resolve/cluster/no_such_remote*:*");
383+
Response response = performRequestWithRemoteSearchUser(remoteOnly1);
384+
assertOK(response);
385+
Map<String, Object> responseMap = responseAsMap(response);
386+
assertThat(responseMap.isEmpty(), is(true));
387+
}
392388
}
393389

394390
private Response performRequestWithRemoteSearchUser(final Request request) throws IOException {

0 commit comments

Comments
 (0)