Skip to content

Commit 92750be

Browse files
authored
Always treat dash-prefixed expression as index exclusion (elastic#138467)
Fixed a bug that dash prefix expressions, including both concrete names and wildcard patterns, are not always treated as index exclusion during index resolution process. The PR also updates relevant document to make the behaviour clear. Resolves: elastic#64752 Resolves: elastic#83435
1 parent 870facd commit 92750be

File tree

9 files changed

+273
-201
lines changed

9 files changed

+273
-201
lines changed

docs/changelog/138467.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
pr: 138467
2+
summary: |
3+
Fixed a bug where dash-prefixed expressions were not consistently excluded during index resolution.
4+
This impacted both specific index names and wildcard patterns (example: `-index, -logs-*`).
5+
area: Security
6+
type: bug
7+
issues:
8+
- 64752
9+
- 83435

docs/reference/elasticsearch/rest-apis/api-conventions.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,20 @@ Most APIs that accept a `<data-stream>`, `<index>`, or `<target>` request path p
282282

283283
In multi-target syntax, you can use a comma-separated list to run a request on multiple resources, such as data streams, indices, or aliases: `test1,test2,test3`. You can also use [glob-like](https://en.wikipedia.org/wiki/Glob_(programming)) wildcard (`*`) expressions to target resources that match a pattern: `test*` or `*test` or `te*t` or `*test*`.
284284

285-
You can exclude targets using the `-` character: `test*,-test3`.
285+
Targets can be excluded by prefixing with the `-` character. This applies to both concrete names and wildcard patterns.
286+
For example, `test*,-test3` resolves to all resources that start with `test` except for the resource named `test3`.
287+
It is possible for exclusion to exclude all resources. For example, `test*,-test*` resolves to an empty set.
288+
An exclusion affects targets listed _before_ it and has no impact on targets listed _after_ it.
289+
For example, `test3*,-test3,test*` resolves to all resources that start with `test`, including `test3`, because it is included
290+
by the last `test*` pattern.
291+
292+
{applies_to}`stack: ga 9.3` A dash-prefixed (`-`) expression is always treated as an exclusion.
293+
294+
In previous versions, dash-prefixed expressions were sometimes not treated as exclusions due to a bug. For example:
295+
- `test,-test` threw an `IndexNotFoundException` instead of excluding `test`
296+
- `test3,-test*` incorrectly resolved to `test3` instead of excluding matches to the wildcard pattern
297+
298+
This bug is fixed in 9.3.
286299

287300
::::{important}
288301
Aliases are resolved after wildcard expressions. This can result in a request that targets an excluded alias. For example, if `test3` is an index alias, the pattern `test*,-test3` still targets the indices for `test3`. To avoid this, exclude the concrete indices for the alias instead.

server/src/internalClusterTest/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,13 @@ public void testBasicWorkFlow() throws Exception {
143143
}
144144
}
145145

146-
final String[] indicesToSnapshot = { "test-idx-*", "-test-idx-3" };
146+
final String[] indicesToSnapshot;
147+
final boolean exclusionWithoutWildcard = randomBoolean();
148+
if (exclusionWithoutWildcard) {
149+
indicesToSnapshot = new String[] { "test-idx-1", "test-idx-2", "test-idx-3", "-test-idx-3" };
150+
} else {
151+
indicesToSnapshot = new String[] { "test-idx-*", "-test-idx-3" };
152+
}
147153

148154
logger.info("--> capturing history UUIDs");
149155
final Map<ShardId, String> historyUUIDs = new HashMap<>();
@@ -243,7 +249,13 @@ public void testBasicWorkFlow() throws Exception {
243249
.getSetting("test-idx-1", MetadataIndexStateService.VERIFIED_BEFORE_CLOSE_SETTING.getKey())
244250
);
245251

246-
for (ShardStats shardStats : indicesAdmin().prepareStats(indicesToSnapshot).clear().get().getShards()) {
252+
final String[] indicesToStats;
253+
if (exclusionWithoutWildcard) {
254+
indicesToStats = new String[] { "test-idx-1", "test-idx-3", "-test-idx-3" };
255+
} else {
256+
indicesToStats = indicesToSnapshot;
257+
}
258+
for (ShardStats shardStats : indicesAdmin().prepareStats(indicesToStats).clear().get().getShards()) {
247259
String historyUUID = shardStats.getCommitStats().getUserData().get(Engine.HISTORY_UUID_KEY);
248260
ShardId shardId = shardStats.getShardRouting().shardId();
249261
assertThat(shardStats.getShardRouting() + " doesn't have a history uuid", historyUUID, notNullValue());

server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,14 @@ public void addRemoteExpressions(String original, Set<String> remoteExpressions)
9393
public void excludeFromLocalExpressions(Set<String> expressionsToExclude) {
9494
Objects.requireNonNull(expressionsToExclude);
9595
if (expressionsToExclude.isEmpty() == false) {
96-
for (ResolvedIndexExpression prior : expressions) {
97-
final Set<String> localExpressions = prior.localExpressions().indices();
96+
final var iter = expressions.iterator();
97+
while (iter.hasNext()) {
98+
final ResolvedIndexExpression current = iter.next();
99+
if (expressionsToExclude.contains(current.original())) {
100+
iter.remove();
101+
continue;
102+
}
103+
final Set<String> localExpressions = current.localExpressions().indices();
98104
if (localExpressions.isEmpty()) {
99105
continue;
100106
}

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

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
4949
final boolean includeDataStreams
5050
) {
5151
final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder = ResolvedIndexExpressions.builder();
52-
boolean wildcardSeen = false;
5352
for (String originalIndexExpression : indices) {
54-
wildcardSeen = resolveIndexAbstraction(
53+
resolveIndexAbstraction(
5554
resolvedExpressionsBuilder,
5655
originalIndexExpression,
5756
originalIndexExpression, // in the case of local resolution, the local expression is always the same as the original
@@ -60,8 +59,7 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
6059
allAuthorizedAndAvailableBySelector,
6160
isAuthorized,
6261
includeDataStreams,
63-
Set.of(),
64-
wildcardSeen
62+
Set.of()
6563
);
6664
}
6765
return resolvedExpressionsBuilder.build();
@@ -86,7 +84,6 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
8684
final String originProjectAlias = targetProjects.originProjectAlias();
8785
final Set<String> linkedProjectAliases = targetProjects.allProjectAliases();
8886
final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder = ResolvedIndexExpressions.builder();
89-
boolean wildcardSeen = false;
9087
for (String originalIndexExpression : indices) {
9188
final CrossProjectIndexExpressionsRewriter.IndexRewriteResult indexRewriteResult = CrossProjectIndexExpressionsRewriter
9289
.rewriteIndexExpression(originalIndexExpression, originProjectAlias, linkedProjectAliases, projectRouting);
@@ -100,7 +97,7 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
10097
continue;
10198
}
10299

103-
wildcardSeen = resolveIndexAbstraction(
100+
resolveIndexAbstraction(
104101
resolvedExpressionsBuilder,
105102
originalIndexExpression,
106103
localIndexExpression,
@@ -109,14 +106,13 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
109106
allAuthorizedAndAvailableBySelector,
110107
isAuthorized,
111108
includeDataStreams,
112-
indexRewriteResult.remoteExpressions(),
113-
wildcardSeen
109+
indexRewriteResult.remoteExpressions()
114110
);
115111
}
116112
return resolvedExpressionsBuilder.build();
117113
}
118114

119-
private boolean resolveIndexAbstraction(
115+
private void resolveIndexAbstraction(
120116
final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder,
121117
final String originalIndexExpression,
122118
final String localIndexExpression,
@@ -125,12 +121,11 @@ private boolean resolveIndexAbstraction(
125121
final Function<IndexComponentSelector, Set<String>> allAuthorizedAndAvailableBySelector,
126122
final BiPredicate<String, IndexComponentSelector> isAuthorized,
127123
final boolean includeDataStreams,
128-
final Set<String> remoteExpressions,
129-
boolean wildcardSeen
124+
final Set<String> remoteExpressions
130125
) {
131126
String indexAbstraction;
132127
boolean minus = false;
133-
if (localIndexExpression.charAt(0) == '-' && wildcardSeen) {
128+
if (localIndexExpression.charAt(0) == '-') {
134129
indexAbstraction = localIndexExpression.substring(1);
135130
minus = true;
136131
} else {
@@ -150,7 +145,6 @@ private boolean resolveIndexAbstraction(
150145
indexAbstraction = IndexNameExpressionResolver.resolveDateMathExpression(indexAbstraction);
151146

152147
if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) {
153-
wildcardSeen = true;
154148
final HashSet<String> resolvedIndices = new HashSet<>();
155149
for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selector)) {
156150
if (Regex.simpleMatch(indexAbstraction, authorizedIndex)
@@ -167,11 +161,14 @@ && isIndexVisible(
167161
}
168162
}
169163
if (resolvedIndices.isEmpty()) {
170-
// es core honours allow_no_indices for each wildcard expression, we do the same here by throwing index not found.
171-
if (indicesOptions.allowNoIndices() == false) {
172-
throw new IndexNotFoundException(indexAbstraction);
164+
// Ignore empty result for exclusions
165+
if (minus == false) {
166+
// es core honours allow_no_indices for each wildcard expression, we do the same here by throwing index not found.
167+
if (indicesOptions.allowNoIndices() == false) {
168+
throw new IndexNotFoundException(indexAbstraction);
169+
}
170+
resolvedExpressionsBuilder.addExpressions(originalIndexExpression, new HashSet<>(), SUCCESS, remoteExpressions);
173171
}
174-
resolvedExpressionsBuilder.addExpressions(originalIndexExpression, new HashSet<>(), SUCCESS, remoteExpressions);
175172
} else {
176173
if (minus) {
177174
resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
@@ -219,7 +216,6 @@ && isIndexVisible(
219216
}
220217
}
221218
}
222-
return wildcardSeen;
223219
}
224220

225221
private static void resolveSelectorsAndCollect(

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,11 @@ protected static Collection<ResolvedExpression> resolveExpressionsToResources(Co
401401
Collection<ResolvedExpression> resources = expandWildcards && WildcardExpressionResolver.hasWildcards(expressions)
402402
? new LinkedHashSet<>()
403403
: new ArrayList<>(expressions.length);
404-
boolean wildcardSeen = false;
405404
for (int i = 0, n = expressions.length; i < n; i++) {
406405
String originalExpression = expressions[i];
407406

408-
// Resolve exclusion, a `-` prefixed expression is an exclusion only if it succeeds a wildcard.
409-
boolean isExclusion = wildcardSeen && originalExpression.startsWith("-");
407+
// Resolve exclusion, a `-` prefixed expression is an exclusion
408+
boolean isExclusion = originalExpression.startsWith("-");
410409
String baseExpression = isExclusion ? originalExpression.substring(1) : originalExpression;
411410

412411
// Parse a potential selector from the expression
@@ -422,7 +421,6 @@ protected static Collection<ResolvedExpression> resolveExpressionsToResources(Co
422421

423422
// Check if it's wildcard
424423
boolean isWildcard = expandWildcards && WildcardExpressionResolver.isWildcard(baseExpression);
425-
wildcardSeen |= isWildcard;
426424

427425
if (isWildcard) {
428426
Set<ResolvedExpression> matchingResources = WildcardExpressionResolver.matchWildcardToResources(
@@ -756,17 +754,7 @@ private static IndexNotFoundException notFoundException(String... indexExpressio
756754
infe = new IndexNotFoundException("no indices exist", Metadata.ALL);
757755
infe.setResources("index_or_alias", Metadata.ALL);
758756
} else if (indexExpressions.length == 1) {
759-
if (indexExpressions[0].startsWith("-")) {
760-
// this can arise when multi-target syntax is used with an exclusion not in the "inclusion" list, such as
761-
// "GET test1,test2,-test3"
762-
// the caller should have put "GET test*,-test3"
763-
infe = new IndexNotFoundException(
764-
"if you intended to exclude this index, ensure that you use wildcards that include it before explicitly excluding it",
765-
indexExpressions[0]
766-
);
767-
} else {
768-
infe = new IndexNotFoundException(indexExpressions[0]);
769-
}
757+
infe = new IndexNotFoundException(indexExpressions[0]);
770758
infe.setResources("index_or_alias", indexExpressions[0]);
771759
} else {
772760
infe = new IndexNotFoundException((String) null);

server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -451,18 +451,8 @@ public void testConcreteIndexNamesExpandWildcards() {
451451
);
452452
assertThat(infe.getResourceId().toString(), equalTo("[-*]"));
453453

454-
infe = expectThrows(
455-
IndexNotFoundException.class,
456-
// throws error because "-foobar" was not covered by a wildcard that included it
457-
() -> indexNameExpressionResolver.concreteIndexNames(context2, "bar", "hidden", "-foobar")
458-
);
459-
assertThat(
460-
infe.getMessage(),
461-
containsString(
462-
"if you intended to exclude this index, ensure that you use wildcards that include it " + "before explicitly excluding it"
463-
)
464-
);
465-
assertThat(infe.getResourceId().toString(), equalTo("[-foobar]"));
454+
results = indexNameExpressionResolver.concreteIndexNames(context2, "bar", "hidden", "-foobar");
455+
assertThat(results, arrayContainingInAnyOrder("bar", "hidden"));
466456

467457
// open and hidden
468458
options = IndicesOptions.fromOptions(false, true, true, false, true);
@@ -983,9 +973,6 @@ public void testConcreteIndicesWildcardWithNegation() {
983973
.put(indexBuilder("testXXX").state(State.OPEN))
984974
.put(indexBuilder("testXXY").state(State.OPEN))
985975
.put(indexBuilder("testXYY").state(State.OPEN))
986-
.put(indexBuilder("-testXYZ").state(State.OPEN))
987-
.put(indexBuilder("-testXZZ").state(State.OPEN))
988-
.put(indexBuilder("-testYYY").state(State.OPEN))
989976
.put(indexBuilder("testYYY").state(State.OPEN))
990977
.put(indexBuilder("testYYX").state(State.OPEN))
991978
.build();
@@ -1005,19 +992,13 @@ public void testConcreteIndicesWildcardWithNegation() {
1005992
equalTo(newHashSet("testYYY", "testYYX"))
1006993
);
1007994

1008-
assertThat(
1009-
newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "-testX*")),
1010-
equalTo(newHashSet("-testXYZ", "-testXZZ"))
1011-
);
995+
assertThat(newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "-testX*")), empty());
1012996

1013-
assertThat(
1014-
newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "testXXY", "-testX*")),
1015-
equalTo(newHashSet("testXXY", "-testXYZ", "-testXZZ"))
1016-
);
997+
assertThat(newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "testXXY", "-testX*")), empty());
1017998

1018999
assertThat(
1019-
newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "*", "--testX*")),
1020-
equalTo(newHashSet("testXXX", "testXXY", "testXYY", "testYYX", "testYYY", "-testYYY"))
1000+
newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "*", "-testX*")),
1001+
equalTo(newHashSet("testYYX", "testYYY"))
10211002
);
10221003

10231004
assertThat(
@@ -1032,7 +1013,7 @@ public void testConcreteIndicesWildcardWithNegation() {
10321013

10331014
assertThat(
10341015
newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "testXXX", "testXXY", "testYYY", "-testYYY")),
1035-
equalTo(newHashSet("testXXX", "testXXY", "testYYY", "-testYYY"))
1016+
equalTo(newHashSet("testXXX", "testXXY"))
10361017
);
10371018

10381019
assertThat(
@@ -1042,16 +1023,13 @@ public void testConcreteIndicesWildcardWithNegation() {
10421023

10431024
assertThat(
10441025
newHashSet(indexNameExpressionResolver.concreteIndexNames(context, "-testXXX", "*testY*", "-testYYY")),
1045-
equalTo(newHashSet("testYYX", "-testYYY"))
1026+
equalTo(newHashSet("testYYX"))
10461027
);
10471028

10481029
String[] indexNames = indexNameExpressionResolver.concreteIndexNames(project, IndicesOptions.lenientExpandOpen(), "-doesnotexist");
10491030
assertEquals(0, indexNames.length);
10501031

1051-
assertThat(
1052-
newHashSet(indexNameExpressionResolver.concreteIndexNames(project, IndicesOptions.lenientExpandOpen(), "-*")),
1053-
equalTo(newHashSet("-testXYZ", "-testXZZ", "-testYYY"))
1054-
);
1032+
assertThat(newHashSet(indexNameExpressionResolver.concreteIndexNames(project, IndicesOptions.lenientExpandOpen(), "-*")), empty());
10551033

10561034
assertThat(
10571035
newHashSet(
@@ -1064,7 +1042,7 @@ public void testConcreteIndicesWildcardWithNegation() {
10641042
"-testXXY"
10651043
)
10661044
),
1067-
equalTo(newHashSet("testXXX", "testXYY", "testXXY"))
1045+
equalTo(newHashSet("testXXX", "testXYY"))
10681046
);
10691047

10701048
indexNames = indexNameExpressionResolver.concreteIndexNames(project, IndicesOptions.lenientExpandOpen(), "*", "-*");

0 commit comments

Comments
 (0)