From 9c7e04b4dcbe24d8635d9ebaa0b9ffe1b63e53a6 Mon Sep 17 00:00:00 2001 From: Richard Dennehy Date: Wed, 29 Oct 2025 11:14:12 +0000 Subject: [PATCH 1/8] resolve indices for prefixed _all expressions --- .../authz/IndicesAndAliasesResolver.java | 17 ++++-- .../authz/IndicesAndAliasesResolverTests.java | 58 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 17d932db9f0dd..bb90237d666ed 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -335,10 +335,19 @@ ResolvedIndices resolveIndicesAndAliases( String allIndicesPatternSelector = null; if (indicesRequest.indices() != null && indicesRequest.indices().length > 0) { // Always parse selectors, but do so lazily so that we don't spend a lot of time splitting strings each resolution - isAllIndices = IndexNameExpressionResolver.isAllIndices( - indicesList(indicesRequest.indices()), - (expr) -> IndexNameExpressionResolver.splitSelectorExpression(expr).v1() - ); + isAllIndices = crossProjectModeDecider.resolvesCrossProject(replaceable) + ? IndexNameExpressionResolver.isAllIndices( + indicesList(indicesRequest.indices()), + (expr) -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( + expr, + authorizedProjects.originProjectAlias(), + authorizedProjects.allProjectAliases() + ).localExpression() + ) + : IndexNameExpressionResolver.isAllIndices( + indicesList(indicesRequest.indices()), + (expr) -> IndexNameExpressionResolver.splitSelectorExpression(expr).v1() + ); if (isAllIndices) { // This parses the single all-indices expression for a second time in this conditional branch, but this is better than // parsing a potentially big list of indices on every request. diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index e4bb9664beaee..e7001170b237b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -2956,6 +2956,64 @@ public void testCrossProjectSearchSelectorsNotAllowed() { assertThat(exception.getMessage(), equalTo("Selectors are not currently supported but was found in the expression [_all::data]")); } + public void testResolveAllWithRemotePrefix() { + when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); + + var request = new SearchRequest().indices("*:_all"); + request.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), true, true)); + var resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( + "indices:/" + randomAlphaOfLength(8), + request, + projectMetadata, + buildAuthorizedIndices(user, TransportSearchAction.TYPE.name()), + new TargetProjects( + createRandomProjectWithAlias("local"), + List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"), createRandomProjectWithAlias("P3")) + ) + ); + + var expectedIndices = new String[] { "bar", "foobarfoo", "bar-closed", "foofoobar", "foofoo-closed", "foofoo" }; + + assertThat(resolvedIndices.getLocal(), contains(expectedIndices)); + assertThat(resolvedIndices.getRemote(), containsInAnyOrder("P1:_all", "P2:_all", "P3:_all")); + + final var resolved = request.getResolvedIndexExpressions(); + assertThat(resolved, is(notNullValue())); + assertThat( + resolved.expressions(), + contains(resolvedIndexExpression("*:_all", Set.of(expectedIndices), SUCCESS, Set.of("P1:_all", "P2:_all", "P3:_all"))) + ); + } + + public void testResolveIndexWithRemotePrefix() { + when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); + + var request = new SearchRequest().indices("*:bar"); + request.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), true, true)); + var resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( + "indices:/" + randomAlphaOfLength(8), + request, + projectMetadata, + buildAuthorizedIndices(user, TransportSearchAction.TYPE.name()), + new TargetProjects( + createRandomProjectWithAlias("local"), + List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"), createRandomProjectWithAlias("P3")) + ) + ); + + var expectedIndices = new String[] { "bar" }; + + assertThat(resolvedIndices.getLocal(), contains(expectedIndices)); + assertThat(resolvedIndices.getRemote(), containsInAnyOrder("P1:bar", "P2:bar", "P3:bar")); + + final var resolved = request.getResolvedIndexExpressions(); + assertThat(resolved, is(notNullValue())); + assertThat( + resolved.expressions(), + contains(resolvedIndexExpression("*:bar", Set.of(expectedIndices), SUCCESS, Set.of("P1:bar", "P2:bar", "P3:bar"))) + ); + } + private void assertIndicesMatch(IndicesRequest.Replaceable request, String expression, List indices, String[] expectedIndices) { assertThat(indices, hasSize(expectedIndices.length)); assertThat(request.indices().length, equalTo(expectedIndices.length)); From 2ecdc40bc04dd5c60ba5b38b0b01bc34c4a445cc Mon Sep 17 00:00:00 2001 From: Richard Dennehy Date: Wed, 29 Oct 2025 16:00:43 +0000 Subject: [PATCH 2/8] add test for origin prefix _all expression --- .../authz/IndicesAndAliasesResolverTests.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index e7001170b237b..c45200309a717 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -2956,7 +2956,7 @@ public void testCrossProjectSearchSelectorsNotAllowed() { assertThat(exception.getMessage(), equalTo("Selectors are not currently supported but was found in the expression [_all::data]")); } - public void testResolveAllWithRemotePrefix() { + public void testResolveAllWithWildcardRemotePrefix() { when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); var request = new SearchRequest().indices("*:_all"); @@ -2985,6 +2985,36 @@ public void testResolveAllWithRemotePrefix() { ); } + public void testResolveAllWithRemotePrefix() { + when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); + + var expression = randomBoolean() ? "local:_all" : "_origin:_all"; + var request = new SearchRequest().indices(expression); + request.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), true, true)); + var resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( + "indices:/" + randomAlphaOfLength(8), + request, + projectMetadata, + buildAuthorizedIndices(user, TransportSearchAction.TYPE.name()), + new TargetProjects( + createRandomProjectWithAlias("local"), + List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"), createRandomProjectWithAlias("P3")) + ) + ); + + var expectedIndices = new String[] { "bar", "foobarfoo", "bar-closed", "foofoobar", "foofoo-closed", "foofoo" }; + + assertThat(resolvedIndices.getLocal(), contains(expectedIndices)); + assertThat(resolvedIndices.getRemote(), is(empty())); + + final var resolved = request.getResolvedIndexExpressions(); + assertThat(resolved, is(notNullValue())); + assertThat( + resolved.expressions(), + contains(resolvedIndexExpression(expression, Set.of(expectedIndices), SUCCESS, Set.of())) + ); + } + public void testResolveIndexWithRemotePrefix() { when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); From b19ec68bbe929ffa47c157c8583d003344889bdc Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 29 Oct 2025 16:06:37 +0000 Subject: [PATCH 3/8] [CI] Auto commit changes from spotless --- .../xpack/security/authz/IndicesAndAliasesResolverTests.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index c45200309a717..fe5650a492d64 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -3009,10 +3009,7 @@ public void testResolveAllWithRemotePrefix() { final var resolved = request.getResolvedIndexExpressions(); assertThat(resolved, is(notNullValue())); - assertThat( - resolved.expressions(), - contains(resolvedIndexExpression(expression, Set.of(expectedIndices), SUCCESS, Set.of())) - ); + assertThat(resolved.expressions(), contains(resolvedIndexExpression(expression, Set.of(expectedIndices), SUCCESS, Set.of()))); } public void testResolveIndexWithRemotePrefix() { From 84ea19d39d749a9313255c0fd23aa4f955ba5e04 Mon Sep 17 00:00:00 2001 From: Richard Dennehy Date: Thu, 30 Oct 2025 10:48:19 +0000 Subject: [PATCH 4/8] use splitIndexName instead of rewriteIndexExpression --- .../authz/IndicesAndAliasesResolver.java | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index bb90237d666ed..d0d5000d204f7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -335,19 +335,12 @@ ResolvedIndices resolveIndicesAndAliases( String allIndicesPatternSelector = null; if (indicesRequest.indices() != null && indicesRequest.indices().length > 0) { // Always parse selectors, but do so lazily so that we don't spend a lot of time splitting strings each resolution - isAllIndices = crossProjectModeDecider.resolvesCrossProject(replaceable) - ? IndexNameExpressionResolver.isAllIndices( - indicesList(indicesRequest.indices()), - (expr) -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( - expr, - authorizedProjects.originProjectAlias(), - authorizedProjects.allProjectAliases() - ).localExpression() - ) - : IndexNameExpressionResolver.isAllIndices( - indicesList(indicesRequest.indices()), - (expr) -> IndexNameExpressionResolver.splitSelectorExpression(expr).v1() - ); + isAllIndices = IndexNameExpressionResolver.isAllIndices(indicesList(indicesRequest.indices()), (expr) -> { + var unprefixed = crossProjectModeDecider.resolvesCrossProject(replaceable) + ? RemoteClusterAware.splitIndexName(expr)[1] + : expr; + return IndexNameExpressionResolver.splitSelectorExpression(unprefixed).v1(); + }); if (isAllIndices) { // This parses the single all-indices expression for a second time in this conditional branch, but this is better than // parsing a potentially big list of indices on every request. From aaea6cf5d2fac32c915081e9761509e6bb10a6e6 Mon Sep 17 00:00:00 2001 From: Richard Dennehy Date: Fri, 31 Oct 2025 14:36:34 +0000 Subject: [PATCH 5/8] use rewriteIndexExpression again --- .../authz/IndicesAndAliasesResolver.java | 8 +++-- .../authz/IndicesAndAliasesResolverTests.java | 30 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index b34a7531d4585..63745c73bd7e6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -342,9 +342,13 @@ ResolvedIndices resolveIndicesAndAliases( // Always parse selectors, but do so lazily so that we don't spend a lot of time splitting strings each resolution isAllIndices = IndexNameExpressionResolver.isAllIndices(indicesList(indicesRequest.indices()), (expr) -> { var unprefixed = crossProjectModeDecider.resolvesCrossProject(replaceable) - ? RemoteClusterAware.splitIndexName(expr)[1] + ? CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( + expr, + authorizedProjects.originProjectAlias(), + authorizedProjects.allProjectAliases() + ).localExpression() : expr; - return IndexNameExpressionResolver.splitSelectorExpression(unprefixed).v1(); + return unprefixed != null ? IndexNameExpressionResolver.splitSelectorExpression(unprefixed).v1() : null; }); if (isAllIndices) { // This parses the single all-indices expression for a second time in this conditional branch, but this is better than diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index fe5650a492d64..f875c299cfca6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -113,6 +113,7 @@ import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_NOT_VISIBLE; import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_UNAUTHORIZED; +import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.NONE; import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance; import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; @@ -2985,7 +2986,7 @@ public void testResolveAllWithWildcardRemotePrefix() { ); } - public void testResolveAllWithRemotePrefix() { + public void testResolveAllWithLocalPrefix() { when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); var expression = randomBoolean() ? "local:_all" : "_origin:_all"; @@ -3012,6 +3013,33 @@ public void testResolveAllWithRemotePrefix() { assertThat(resolved.expressions(), contains(resolvedIndexExpression(expression, Set.of(expectedIndices), SUCCESS, Set.of()))); } + public void testResolveAllWithRemotePrefix() { + when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); + + var request = new SearchRequest().indices("P*:_all"); + request.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), true, true)); + var resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( + "indices:/" + randomAlphaOfLength(8), + request, + projectMetadata, + buildAuthorizedIndices(user, TransportSearchAction.TYPE.name()), + new TargetProjects( + createRandomProjectWithAlias("local"), + List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"), createRandomProjectWithAlias("P3")) + ) + ); + + assertThat(resolvedIndices.getLocal(), is(empty())); + assertThat(resolvedIndices.getRemote(), contains("P1:_all", "P2:_all", "P3:_all")); + + final var resolved = request.getResolvedIndexExpressions(); + assertThat(resolved, is(notNullValue())); + assertThat( + resolved.expressions(), + contains(resolvedIndexExpression("P*:_all", Set.of(), NONE, Set.of("P1:_all", "P2:_all", "P3:_all"))) + ); + } + public void testResolveIndexWithRemotePrefix() { when(crossProjectModeDecider.resolvesCrossProject(any(IndicesRequest.Replaceable.class))).thenReturn(true); From ef0417de104fbe3da2d22d71ef4cc0ad0ab98c82 Mon Sep 17 00:00:00 2001 From: Richard Dennehy Date: Wed, 5 Nov 2025 15:06:31 +0000 Subject: [PATCH 6/8] extend shouldExcludeLocalResolution check --- .../security/authz/IndicesAndAliasesResolver.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 63745c73bd7e6..c77fe8484cdd7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -342,11 +342,7 @@ ResolvedIndices resolveIndicesAndAliases( // Always parse selectors, but do so lazily so that we don't spend a lot of time splitting strings each resolution isAllIndices = IndexNameExpressionResolver.isAllIndices(indicesList(indicesRequest.indices()), (expr) -> { var unprefixed = crossProjectModeDecider.resolvesCrossProject(replaceable) - ? CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( - expr, - authorizedProjects.originProjectAlias(), - authorizedProjects.allProjectAliases() - ).localExpression() + ? RemoteClusterAware.splitIndexName(expr)[1] : expr; return unprefixed != null ? IndexNameExpressionResolver.splitSelectorExpression(unprefixed).v1() : null; }); @@ -400,12 +396,13 @@ ResolvedIndices resolveIndicesAndAliases( replaceable.getProjectRouting(), authorizedProjects ); - remoteIndices = CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( + final var expr = CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( indexExpression, resolvedProjects.originProjectAlias(), resolvedProjects.allProjectAliases() - ).remoteExpressions(); - if (resolvedProjects.originProject() == null) { + ); + remoteIndices = expr.remoteExpressions(); + if (resolvedProjects.originProject() == null || expr.localExpression() == null) { shouldExcludeLocalResolution = true; } } From 13ceafc6fafb1b4d9bc8cc8c5cf44d03ddd211d4 Mon Sep 17 00:00:00 2001 From: Richard Dennehy Date: Wed, 5 Nov 2025 17:03:48 +0000 Subject: [PATCH 7/8] rename --- .../xpack/security/authz/IndicesAndAliasesResolver.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index c77fe8484cdd7..67b8b58930430 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -396,13 +396,13 @@ ResolvedIndices resolveIndicesAndAliases( replaceable.getProjectRouting(), authorizedProjects ); - final var expr = CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( + final var rewritten = CrossProjectIndexExpressionsRewriter.rewriteIndexExpression( indexExpression, resolvedProjects.originProjectAlias(), resolvedProjects.allProjectAliases() ); - remoteIndices = expr.remoteExpressions(); - if (resolvedProjects.originProject() == null || expr.localExpression() == null) { + remoteIndices = rewritten.remoteExpressions(); + if (resolvedProjects.originProject() == null || rewritten.localExpression() == null) { shouldExcludeLocalResolution = true; } } From 9730e5e61677a46dc21d0a165e03958f06756cc1 Mon Sep 17 00:00:00 2001 From: Richard Dennehy Date: Thu, 6 Nov 2025 09:30:59 +0000 Subject: [PATCH 8/8] review comment --- .../xpack/security/authz/IndicesAndAliasesResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 67b8b58930430..a85e383f2fe8b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -344,7 +344,7 @@ ResolvedIndices resolveIndicesAndAliases( var unprefixed = crossProjectModeDecider.resolvesCrossProject(replaceable) ? RemoteClusterAware.splitIndexName(expr)[1] : expr; - return unprefixed != null ? IndexNameExpressionResolver.splitSelectorExpression(unprefixed).v1() : null; + return IndexNameExpressionResolver.splitSelectorExpression(unprefixed).v1(); }); if (isAllIndices) { // This parses the single all-indices expression for a second time in this conditional branch, but this is better than