Skip to content

Commit a77d358

Browse files
authored
Local index resolution records resolved expressions (#135081)
This PR extends index resolution logic to capture the resolution mapping from original to resolution result via `ResolvedIndexExpressions` (introduced in #134783). This lets consumers such as cross-project search that require full resolution context to access it via the `IndicesRequest.Replaceable` interface. To maximize code re-use, the PR updates the existing `resolveIndexAbstractions` method to always return a `ResolvedIndexExpressions` instance. The result is only recorded on the request if a boolean flag is set to avoid unnecessary memory overhead in contexts where storing the expressions is not necessary. In a (much bigger) future refactor, we plan to move away from storing a raw indices list and always use `ResolvedIndexExpressions` as the source of truth for indices resources accessed by a request. Once that's complete, we will move to _always_ storing the full resolution result. This PR does not include the following, which we'll address in immediate follow ups: - Integrate the change into the wider implementation of cross-project search index expressions rewriting and error handling - Store resolution for `_all` index expression handling - Record full authorization exceptions on authorization failures - Record resolution for remote expressions (e.g., `p:logs -> p1:logs`) Relates: ES-12690
1 parent 7cc3f52 commit a77d358

File tree

9 files changed

+375
-68
lines changed

9 files changed

+375
-68
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ interface Replaceable extends IndicesRequest {
5353
* Record the results of index resolution. See {@link ResolvedIndexExpressions} for details.
5454
* Note: this method does not replace {@link #indices(String...)}. {@link #indices(String...)} must still be called to update
5555
* the actual list of indices the request relates to.
56+
* Note: the field is transient and not serialized.
5657
*/
5758
default void setResolvedIndexExpressions(ResolvedIndexExpressions expressions) {}
5859

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import org.elasticsearch.ElasticsearchException;
1313
import org.elasticsearch.core.Nullable;
1414

15-
import java.util.List;
15+
import java.util.Set;
1616

1717
/**
1818
* This class allows capturing context about index expression replacements performed on an {@link IndicesRequest.Replaceable} during
@@ -40,23 +40,24 @@
4040
* and failure info
4141
* @param remoteExpressions the remote expressions that replace the original
4242
*/
43-
public record ResolvedIndexExpression(String original, LocalExpressions localExpressions, List<String> remoteExpressions) {
43+
public record ResolvedIndexExpression(String original, LocalExpressions localExpressions, Set<String> remoteExpressions) {
4444
/**
4545
* Indicates if a local index resolution attempt was successful or failed.
46-
* Failures can be due to missing concrete resources or unauthorized concrete resources.
46+
* Failures can be due to concrete resources not being visible (either missing or not visible due to indices options)
47+
* or unauthorized concrete resources.
4748
* A wildcard expression resolving to nothing is still considered a successful resolution.
4849
*/
49-
enum LocalIndexResolutionResult {
50+
public enum LocalIndexResolutionResult {
5051
SUCCESS,
51-
CONCRETE_RESOURCE_MISSING,
52+
CONCRETE_RESOURCE_NOT_VISIBLE,
5253
CONCRETE_RESOURCE_UNAUTHORIZED,
5354
}
5455

5556
/**
56-
* Represents local (non-remote) resolution results, including expanded indices, and the resolution result.
57+
* Represents local (non-remote) resolution results, including expanded indices, and a {@link LocalIndexResolutionResult}.
5758
*/
5859
public record LocalExpressions(
59-
List<String> expressions,
60+
Set<String> expressions,
6061
LocalIndexResolutionResult localIndexResolutionResult,
6162
@Nullable ElasticsearchException exception
6263
) {

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

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,62 @@
99

1010
package org.elasticsearch.action;
1111

12-
import java.util.Map;
12+
import org.elasticsearch.action.ResolvedIndexExpression.LocalExpressions;
13+
14+
import java.util.ArrayList;
15+
import java.util.HashSet;
16+
import java.util.List;
17+
import java.util.Objects;
18+
import java.util.Set;
1319

1420
/**
15-
* A collection of {@link ResolvedIndexExpression}, keyed by the original expression.
16-
*
17-
* <p>An example structure is:</p>
18-
*
19-
* <pre>{@code
20-
* {
21-
* "my-index-*": {
22-
* "original": "my-index-*",
23-
* "localExpressions": {
24-
* "expressions": ["my-index-000001", "my-index-000002"],
25-
* "localIndexResolutionResult": "SUCCESS"
26-
* },
27-
* "remoteExpressions": ["remote1:my-index-*", "remote2:my-index-*"]
28-
* },
29-
* "my-index-000001": {
30-
* "original": "my-index-000001",
31-
* "localExpressions": {
32-
* "expressions": ["my-index-000001"],
33-
* "localIndexResolutionResult": "SUCCESS"
34-
* },
35-
* "remoteExpressions": ["remote1:my-index-000001", "remote2:my-index-000001"]
36-
* }
37-
* }
38-
* }</pre>
21+
* A collection of {@link ResolvedIndexExpression}.
3922
*/
40-
public record ResolvedIndexExpressions(Map<String, ResolvedIndexExpression> expressions) {}
23+
public record ResolvedIndexExpressions(List<ResolvedIndexExpression> expressions) {
24+
25+
public List<String> getLocalIndicesList() {
26+
return expressions.stream().flatMap(e -> e.localExpressions().expressions().stream()).toList();
27+
}
28+
29+
public static Builder builder() {
30+
return new Builder();
31+
}
32+
33+
public static final class Builder {
34+
private final List<ResolvedIndexExpression> expressions = new ArrayList<>();
35+
36+
/**
37+
* @param original the original expression that was resolved -- may be blank for "access all" cases
38+
* @param localExpressions is a HashSet as an optimization -- the set needs to be mutable, and we want to avoid copying it.
39+
* May be empty.
40+
*/
41+
public void addLocalExpressions(
42+
String original,
43+
HashSet<String> localExpressions,
44+
ResolvedIndexExpression.LocalIndexResolutionResult resolutionResult
45+
) {
46+
Objects.requireNonNull(original);
47+
Objects.requireNonNull(localExpressions);
48+
Objects.requireNonNull(resolutionResult);
49+
expressions.add(
50+
new ResolvedIndexExpression(original, new LocalExpressions(localExpressions, resolutionResult, null), new HashSet<>())
51+
);
52+
}
53+
54+
/**
55+
* Exclude the given expressions from the local expressions of all prior added {@link ResolvedIndexExpression}.
56+
*/
57+
public void excludeFromLocalExpressions(Set<String> expressionsToExclude) {
58+
Objects.requireNonNull(expressionsToExclude);
59+
if (expressionsToExclude.isEmpty() == false) {
60+
for (ResolvedIndexExpression prior : expressions) {
61+
prior.localExpressions().expressions().removeAll(expressionsToExclude);
62+
}
63+
}
64+
}
65+
66+
public ResolvedIndexExpressions build() {
67+
return new ResolvedIndexExpressions(expressions);
68+
}
69+
}
70+
}

server/src/main/java/org/elasticsearch/action/search/SearchRequest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.action.ActionRequestValidationException;
1515
import org.elasticsearch.action.IndicesRequest;
1616
import org.elasticsearch.action.LegacyActionRequest;
17+
import org.elasticsearch.action.ResolvedIndexExpressions;
1718
import org.elasticsearch.action.support.IndicesOptions;
1819
import org.elasticsearch.client.internal.Client;
1920
import org.elasticsearch.common.Strings;
@@ -70,6 +71,9 @@ public class SearchRequest extends LegacyActionRequest implements IndicesRequest
7071

7172
private String[] indices = Strings.EMPTY_ARRAY;
7273

74+
@Nullable
75+
private ResolvedIndexExpressions resolvedIndexExpressions = null;
76+
7377
@Nullable
7478
private String routing;
7579
@Nullable
@@ -400,6 +404,17 @@ public SearchRequest indices(String... indices) {
400404
return this;
401405
}
402406

407+
@Override
408+
public void setResolvedIndexExpressions(ResolvedIndexExpressions expressions) {
409+
this.resolvedIndexExpressions = expressions;
410+
}
411+
412+
@Override
413+
@Nullable
414+
public ResolvedIndexExpressions getResolvedIndexExpressions() {
415+
return resolvedIndexExpressions;
416+
}
417+
403418
private static void validateIndices(String... indices) {
404419
Objects.requireNonNull(indices, "indices must not be null");
405420
for (String index : indices) {

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

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
package org.elasticsearch.cluster.metadata;
1111

12+
import org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult;
13+
import org.elasticsearch.action.ResolvedIndexExpressions;
1214
import org.elasticsearch.action.support.IndexComponentSelector;
1315
import org.elasticsearch.action.support.IndicesOptions;
1416
import org.elasticsearch.action.support.UnsupportedSelectorException;
@@ -19,13 +21,16 @@
1921
import org.elasticsearch.index.IndexNotFoundException;
2022
import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel;
2123

22-
import java.util.ArrayList;
2324
import java.util.HashSet;
2425
import java.util.List;
2526
import java.util.Set;
2627
import java.util.function.BiPredicate;
2728
import java.util.function.Function;
2829

30+
import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_NOT_VISIBLE;
31+
import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_UNAUTHORIZED;
32+
import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS;
33+
2934
public class IndexAbstractionResolver {
3035

3136
private final IndexNameExpressionResolver indexNameExpressionResolver;
@@ -34,15 +39,16 @@ public IndexAbstractionResolver(IndexNameExpressionResolver indexNameExpressionR
3439
this.indexNameExpressionResolver = indexNameExpressionResolver;
3540
}
3641

37-
public List<String> resolveIndexAbstractions(
38-
Iterable<String> indices,
42+
public ResolvedIndexExpressions resolveIndexAbstractions(
43+
List<String> indices,
3944
IndicesOptions indicesOptions,
4045
ProjectMetadata projectMetadata,
4146
Function<IndexComponentSelector, Set<String>> allAuthorizedAndAvailableBySelector,
4247
BiPredicate<String, IndexComponentSelector> isAuthorized,
4348
boolean includeDataStreams
4449
) {
45-
List<String> finalIndices = new ArrayList<>();
50+
final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder = ResolvedIndexExpressions.builder();
51+
4652
boolean wildcardSeen = false;
4753
for (String index : indices) {
4854
String indexAbstraction;
@@ -55,8 +61,8 @@ public List<String> resolveIndexAbstractions(
5561
}
5662

5763
// Always check to see if there's a selector on the index expression
58-
Tuple<String, String> expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexAbstraction);
59-
String selectorString = expressionAndSelector.v2();
64+
final Tuple<String, String> expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexAbstraction);
65+
final String selectorString = expressionAndSelector.v2();
6066
if (indicesOptions.allowSelectors() == false && selectorString != null) {
6167
throw new UnsupportedSelectorException(indexAbstraction);
6268
}
@@ -68,7 +74,7 @@ public List<String> resolveIndexAbstractions(
6874

6975
if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) {
7076
wildcardSeen = true;
71-
Set<String> resolvedIndices = new HashSet<>();
77+
final HashSet<String> resolvedIndices = new HashSet<>();
7278
for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selector)) {
7379
if (Regex.simpleMatch(indexAbstraction, authorizedIndex)
7480
&& isIndexVisible(
@@ -88,27 +94,46 @@ && isIndexVisible(
8894
if (indicesOptions.allowNoIndices() == false) {
8995
throw new IndexNotFoundException(indexAbstraction);
9096
}
97+
resolvedExpressionsBuilder.addLocalExpressions(index, new HashSet<>(), SUCCESS);
9198
} else {
9299
if (minus) {
93-
finalIndices.removeAll(resolvedIndices);
100+
resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
94101
} else {
95-
finalIndices.addAll(resolvedIndices);
102+
resolvedExpressionsBuilder.addLocalExpressions(index, resolvedIndices, SUCCESS);
96103
}
97104
}
98105
} else {
99-
Set<String> resolvedIndices = new HashSet<>();
106+
final HashSet<String> resolvedIndices = new HashSet<>();
100107
resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, projectMetadata);
101108
if (minus) {
102-
finalIndices.removeAll(resolvedIndices);
103-
} else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction, selector)) {
104-
// Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently
105-
// discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action
106-
// handler, see: https://github.com/elastic/elasticsearch/issues/90215
107-
finalIndices.addAll(resolvedIndices);
109+
resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
110+
} else {
111+
final boolean authorized = isAuthorized.test(indexAbstraction, selector);
112+
if (authorized) {
113+
final boolean visible = indexExists(projectMetadata, indexAbstraction)
114+
&& isIndexVisible(
115+
indexAbstraction,
116+
selectorString,
117+
indexAbstraction,
118+
indicesOptions,
119+
projectMetadata,
120+
indexNameExpressionResolver,
121+
includeDataStreams
122+
);
123+
final LocalIndexResolutionResult result = visible ? SUCCESS : CONCRETE_RESOURCE_NOT_VISIBLE;
124+
resolvedExpressionsBuilder.addLocalExpressions(index, resolvedIndices, result);
125+
} else if (indicesOptions.ignoreUnavailable()) {
126+
// ignoreUnavailable implies that the request should not fail if an index is not authorized
127+
// so we map this expression to an empty list,
128+
resolvedExpressionsBuilder.addLocalExpressions(index, new HashSet<>(), CONCRETE_RESOURCE_UNAUTHORIZED);
129+
} else {
130+
// store the calculated expansion as unauthorized, it will be rejected later
131+
resolvedExpressionsBuilder.addLocalExpressions(index, resolvedIndices, CONCRETE_RESOURCE_UNAUTHORIZED);
132+
}
108133
}
109134
}
110135
}
111-
return finalIndices;
136+
return resolvedExpressionsBuilder.build();
112137
}
113138

114139
private static void resolveSelectorsAndCollect(
@@ -260,4 +285,8 @@ private static boolean isSystemIndexVisible(IndexNameExpressionResolver resolver
260285
private static boolean isVisibleDueToImplicitHidden(String expression, String index) {
261286
return index.startsWith(".") && expression.startsWith(".") && Regex.isSimpleMatchPattern(expression);
262287
}
288+
289+
private static boolean indexExists(ProjectMetadata projectMetadata, String indexAbstraction) {
290+
return projectMetadata.getIndicesLookup().get(indexAbstraction) != null;
291+
}
263292
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,9 @@ private List<String> resolveAbstractions(List<String> expressions, IndicesOption
369369
indicesOptions,
370370
projectMetadata,
371371
(ignored) -> mask,
372-
(ignored, nothing) -> true,
372+
(ignored1, ignored2) -> true,
373373
true
374-
);
374+
).getLocalIndicesList();
375375
}
376376

377377
private boolean isIndexVisible(String index, String selector) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public AuthorizationService(
171171
this.clusterService = clusterService;
172172
this.auditTrailService = auditTrailService;
173173
this.restrictedIndices = restrictedIndices;
174-
this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, linkedProjectConfigService, resolver);
174+
this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, linkedProjectConfigService, resolver, false);
175175
this.authcFailureHandler = authcFailureHandler;
176176
this.threadContext = threadPool.getThreadContext();
177177
this.securityContext = new SecurityContext(settings, this.threadContext);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import org.elasticsearch.action.AliasesRequest;
1010
import org.elasticsearch.action.IndicesRequest;
11+
import org.elasticsearch.action.ResolvedIndexExpressions;
1112
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
1213
import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
1314
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
@@ -57,15 +58,18 @@ class IndicesAndAliasesResolver {
5758
private final IndexNameExpressionResolver nameExpressionResolver;
5859
private final IndexAbstractionResolver indexAbstractionResolver;
5960
private final RemoteClusterResolver remoteClusterResolver;
61+
private final boolean recordResolvedIndexExpressions;
6062

6163
IndicesAndAliasesResolver(
6264
Settings settings,
6365
LinkedProjectConfigService linkedProjectConfigService,
64-
IndexNameExpressionResolver resolver
66+
IndexNameExpressionResolver resolver,
67+
boolean recordResolvedIndexExpressions
6568
) {
6669
this.nameExpressionResolver = resolver;
6770
this.indexAbstractionResolver = new IndexAbstractionResolver(resolver);
6871
this.remoteClusterResolver = new RemoteClusterResolver(settings, linkedProjectConfigService);
72+
this.recordResolvedIndexExpressions = recordResolvedIndexExpressions;
6973
}
7074

7175
/**
@@ -348,15 +352,21 @@ ResolvedIndices resolveIndicesAndAliases(
348352
} else {
349353
split = new ResolvedIndices(Arrays.asList(indicesRequest.indices()), Collections.emptyList());
350354
}
351-
List<String> replaced = indexAbstractionResolver.resolveIndexAbstractions(
355+
final ResolvedIndexExpressions resolved = indexAbstractionResolver.resolveIndexAbstractions(
352356
split.getLocal(),
353357
indicesOptions,
354358
projectMetadata,
355359
authorizedIndices::all,
356360
authorizedIndices::check,
357361
indicesRequest.includeDataStreams()
358362
);
359-
resolvedIndicesBuilder.addLocal(replaced);
363+
// only store resolved expressions if configured, to avoid unnecessary memory usage
364+
// once we've migrated from `indices()` to using resolved expressions holistically,
365+
// we will always store them
366+
if (recordResolvedIndexExpressions) {
367+
replaceable.setResolvedIndexExpressions(resolved);
368+
}
369+
resolvedIndicesBuilder.addLocal(resolved.getLocalIndicesList());
360370
resolvedIndicesBuilder.addRemote(split.getRemote());
361371
}
362372

0 commit comments

Comments
 (0)