From 11e955a0c6ccdb6333cc5ddb0e3e1f191ad721ae Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Mon, 15 Sep 2025 12:09:40 +0200
Subject: [PATCH 01/89] WIP CPS authz service
---
.../main/java/org/elasticsearch/action/IndicesRequest.java | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
index 176ecbc7a2395..15ad2707ff4af 100644
--- a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
@@ -62,6 +62,10 @@ interface Replaceable extends IndicesRequest {
default boolean allowsRemoteIndices() {
return false;
}
+
+ default boolean allowsCrossProjectSearch() {
+ return false;
+ }
}
/**
From 23ed699adc6b99f64475a7806c9dffe4c9d13f6b Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Mon, 15 Sep 2025 14:44:53 +0200
Subject: [PATCH 02/89] WIP injection
---
.../core/security/SecurityExtension.java | 5 +
...ProjectSearchIndexExpressionsRewriter.java | 22 ++++
.../xpack/security/Security.java | 18 ++-
.../security/authz/AuthorizationService.java | 113 +++++++++++++-----
4 files changed, 130 insertions(+), 28 deletions(-)
create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchIndexExpressionsRewriter.java
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
index 449246fbb5c92..e2a14c05d9e98 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
@@ -21,6 +21,7 @@
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
+import org.elasticsearch.xpack.core.security.authz.CrossProjectSearchIndexExpressionsRewriter;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
@@ -148,4 +149,8 @@ default AuthorizationEngine getAuthorizationEngine(Settings settings) {
default String extensionName() {
return getClass().getName();
}
+
+ default CrossProjectSearchIndexExpressionsRewriter getCrossProjectSearchIndexExpressionsRewriter(SecurityComponents components) {
+ return null;
+ }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchIndexExpressionsRewriter.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchIndexExpressionsRewriter.java
new file mode 100644
index 0000000000000..19fb51e8d118b
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchIndexExpressionsRewriter.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.security.authz;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.IndicesRequest;
+
+public interface CrossProjectSearchIndexExpressionsRewriter {
+ void rewriteIndexExpressions(IndicesRequest.Replaceable request, ActionListener listener);
+
+ class Default implements CrossProjectSearchIndexExpressionsRewriter {
+ @Override
+ public void rewriteIndexExpressions(IndicesRequest.Replaceable request, ActionListener listener) {
+ listener.onResponse(null);
+ }
+ }
+}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
index c9f0771e93068..4b31229310326 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
@@ -208,6 +208,7 @@
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
+import org.elasticsearch.xpack.core.security.authz.CrossProjectSearchIndexExpressionsRewriter;
import org.elasticsearch.xpack.core.security.authz.RestrictedIndices;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetBitsetCache;
@@ -1131,6 +1132,9 @@ Collection
*/
-
ResolvedIndices resolve(
String action,
TransportRequest request,
From e2e6236519f17653074ab56f8bb7f58ac6afcd15 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Wed, 24 Sep 2025 14:21:16 +0200
Subject: [PATCH 29/89] Tweaks
---
.../xpack/core/security/SecurityExtension.java | 2 +-
.../elasticsearch/xpack/security/Security.java | 15 ++++++++-------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
index 008b5c7cb7697..db31445001e3b 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
@@ -150,7 +150,7 @@ default String extensionName() {
return getClass().getName();
}
- default CrossProjectSearchAuthorizationService getCrossProjectSearchIndexExpressionsRewriter(SecurityComponents components) {
+ default CrossProjectSearchAuthorizationService getCrossProjectSearchAuthorizationService(SecurityComponents components) {
return null;
}
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
index 014fcc1559087..ddddc67e6a6e5 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
@@ -1142,8 +1142,9 @@ Collection createComponents(
authorizationDenialMessages.set(new AuthorizationDenialMessages.Default());
}
- final CrossProjectSearchAuthorizationService crossProjectSearchIndexExpressionsRewriter =
- getCrossProjectSearchIndexExpressionsRewriter(extensionComponents);
+ final CrossProjectSearchAuthorizationService crossProjectSearchAuthorizationService = getCrossProjectSearchAuthorizationService(
+ extensionComponents
+ );
final AuthorizationService authzService = new AuthorizationService(
settings,
allRolesStore,
@@ -1162,9 +1163,9 @@ Collection createComponents(
authorizationDenialMessages.get(),
linkedProjectConfigService,
projectResolver,
- crossProjectSearchIndexExpressionsRewriter == null
+ crossProjectSearchAuthorizationService == null
? new CrossProjectSearchAuthorizationService.Default()
- : crossProjectSearchIndexExpressionsRewriter
+ : crossProjectSearchAuthorizationService
);
components.add(nativeRolesStore); // used by roles actions
@@ -1318,12 +1319,12 @@ private List getCustomAuthenticatorFromExtensions(SecurityE
}
}
- private CrossProjectSearchAuthorizationService getCrossProjectSearchIndexExpressionsRewriter(
+ private CrossProjectSearchAuthorizationService getCrossProjectSearchAuthorizationService(
SecurityExtension.SecurityComponents extensionComponents
) {
return findValueFromExtensions(
- "cross-project search index expressions rewriter",
- extension -> extension.getCrossProjectSearchIndexExpressionsRewriter(extensionComponents)
+ "cross-project search authorization service",
+ extension -> extension.getCrossProjectSearchAuthorizationService(extensionComponents)
);
}
From 635492823b7ec2cd0078eb30a2b4307134d013be Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Wed, 24 Sep 2025 14:26:02 +0200
Subject: [PATCH 30/89] WIP
---
.../xpack/security/authz/AuthorizationService.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index 319070c5b763a..471e990d2b72b 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -554,7 +554,10 @@ private void authorizeAction(
crossProjectSearchAuthzService.loadAuthorizedProjects(new ActionListener<>() {
@Override
public void onResponse(AuthorizedProjects authorizedProjects) {
-
+ logger.debug("Loaded authorized projects: [{}]", authorizedProjects);
+ resolvedIndicesListener.onResponse(
+ indicesAndAliasesResolver.resolve(action, request, projectMetadata, authorizedIndices)
+ );
}
@Override
From 3be77dafb645e90b172edfdde35ff18e19c311d4 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Wed, 24 Sep 2025 16:14:26 +0200
Subject: [PATCH 31/89] Rename
---
.../main/java/org/elasticsearch/action/IndicesRequest.java | 2 +-
.../java/org/elasticsearch/action/search/SearchRequest.java | 5 +++++
.../xpack/security/authz/AuthorizationService.java | 2 +-
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
index 37457cb341621..7b6535e042610 100644
--- a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
@@ -81,7 +81,7 @@ default boolean allowsRemoteIndices() {
return false;
}
- default boolean allowsCrossProjectSearch() {
+ default boolean supportsCrossProjectSearch() {
return false;
}
}
diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
index f84a3578264f8..14c10ed35a7a6 100644
--- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
@@ -158,6 +158,11 @@ public boolean allowsRemoteIndices() {
return true;
}
+ @Override
+ public boolean supportsCrossProjectSearch() {
+ return true;
+ }
+
/**
* Creates a new sub-search request starting from the original search request that is provided.
* For internal use only, allows to fork a search request into multiple search requests that will be executed independently.
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index 471e990d2b72b..a3f80f92e36e1 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -550,7 +550,7 @@ private void authorizeAction(
authzInfo,
projectMetadata.getIndicesLookup(),
ActionListener.wrap(authorizedIndices -> {
- if (request instanceof IndicesRequest.Replaceable replaceable && replaceable.allowsCrossProjectSearch()) {
+ if (request instanceof IndicesRequest.Replaceable replaceable && replaceable.supportsCrossProjectSearch()) {
crossProjectSearchAuthzService.loadAuthorizedProjects(new ActionListener<>() {
@Override
public void onResponse(AuthorizedProjects authorizedProjects) {
From 339ccea447e37c81191d3ea613f8ecaa73332255 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 25 Sep 2025 11:31:13 +0200
Subject: [PATCH 32/89] Project filterting sort of works
---
...rizedProjects.java => TargetProjects.java} | 4 +-
...rossProjectSearchAuthorizationService.java | 8 +-
.../security/authz/AuthorizationService.java | 17 +-
.../authz/IndicesAndAliasesResolver.java | 26 ++-
.../CrossProjectIndexExpressionsRewriter.java | 174 ++++++++++++++++++
.../NoMatchingProjectException.java | 21 +++
6 files changed, 237 insertions(+), 13 deletions(-)
rename server/src/main/java/org/elasticsearch/search/crossproject/{AuthorizedProjects.java => TargetProjects.java} (72%)
create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/NoMatchingProjectException.java
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/AuthorizedProjects.java b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
similarity index 72%
rename from server/src/main/java/org/elasticsearch/search/crossproject/AuthorizedProjects.java
rename to server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
index 6cd3702678632..76df1fab70a21 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/AuthorizedProjects.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
@@ -11,6 +11,6 @@
import java.util.List;
-public record AuthorizedProjects(ProjectRoutingInfo originProject, List linkedProjects) {
- public static AuthorizedProjects NOT_CROSS_PROJECT = new AuthorizedProjects(null, List.of());
+public record TargetProjects(ProjectRoutingInfo originProject, List linkedProjects) {
+ public static TargetProjects NOT_CROSS_PROJECT = new TargetProjects(null, List.of());
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
index 8aae3ac4c2e83..a8de82c857416 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
@@ -8,15 +8,15 @@
package org.elasticsearch.xpack.core.security.authz;
import org.elasticsearch.action.ActionListener;
-import org.elasticsearch.search.crossproject.AuthorizedProjects;
+import org.elasticsearch.search.crossproject.TargetProjects;
public interface CrossProjectSearchAuthorizationService {
- void loadAuthorizedProjects(ActionListener listener);
+ void loadAuthorizedProjects(ActionListener listener);
class Default implements CrossProjectSearchAuthorizationService {
@Override
- public void loadAuthorizedProjects(ActionListener listener) {
- listener.onResponse(AuthorizedProjects.NOT_CROSS_PROJECT);
+ public void loadAuthorizedProjects(ActionListener listener) {
+ listener.onResponse(TargetProjects.NOT_CROSS_PROJECT);
}
}
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index a3f80f92e36e1..13831ca0356fc 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -49,7 +49,7 @@
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.indices.InvalidIndexNameException;
import org.elasticsearch.license.XPackLicenseState;
-import org.elasticsearch.search.crossproject.AuthorizedProjects;
+import org.elasticsearch.search.crossproject.TargetProjects;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.LinkedProjectConfigService;
import org.elasticsearch.transport.TransportActionProxy;
@@ -553,15 +553,22 @@ private void authorizeAction(
if (request instanceof IndicesRequest.Replaceable replaceable && replaceable.supportsCrossProjectSearch()) {
crossProjectSearchAuthzService.loadAuthorizedProjects(new ActionListener<>() {
@Override
- public void onResponse(AuthorizedProjects authorizedProjects) {
- logger.debug("Loaded authorized projects: [{}]", authorizedProjects);
+ public void onResponse(TargetProjects authorizedProjects) {
+ logger.info("Loaded authorized projects: [{}]", authorizedProjects);
resolvedIndicesListener.onResponse(
- indicesAndAliasesResolver.resolve(action, request, projectMetadata, authorizedIndices)
+ indicesAndAliasesResolver.resolve(
+ action,
+ request,
+ projectMetadata,
+ authorizedIndices,
+ authorizedProjects
+ )
);
}
@Override
public void onFailure(Exception e) {
+ logger.info("Failed to load authorized projects", e);
resolvedIndicesListener.onFailure(e);
}
});
@@ -574,7 +581,7 @@ public void onFailure(Exception e) {
if (e instanceof InvalidIndexNameException
|| e instanceof InvalidSelectorException
|| e instanceof UnsupportedSelectorException) {
- logger.debug(
+ logger.info(
() -> Strings.format(
"failed [%s] action authorization for [%s] due to [%s] exception",
action,
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 84da1b9bf5059..6e6edbc56c6d6 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
@@ -30,6 +30,7 @@
import org.elasticsearch.core.Tuple;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexNotFoundException;
+import org.elasticsearch.search.crossproject.TargetProjects;
import org.elasticsearch.transport.LinkedProjectConfig;
import org.elasticsearch.transport.LinkedProjectConfigService;
import org.elasticsearch.transport.NoSuchRemoteClusterException;
@@ -72,6 +73,15 @@ class IndicesAndAliasesResolver {
this.recordResolvedIndexExpressions = recordResolvedIndexExpressions;
}
+ ResolvedIndices resolve(
+ String action,
+ TransportRequest request,
+ ProjectMetadata projectMetadata,
+ AuthorizationEngine.AuthorizedIndices authorizedIndices
+ ) {
+ return resolve(action, request, projectMetadata, authorizedIndices, TargetProjects.NOT_CROSS_PROJECT);
+ }
+
/**
* Resolves, and if necessary updates, the list of index names in the provided request
in accordance with the user's
* authorizedIndices
.
@@ -114,7 +124,8 @@ ResolvedIndices resolve(
String action,
TransportRequest request,
ProjectMetadata projectMetadata,
- AuthorizationEngine.AuthorizedIndices authorizedIndices
+ AuthorizationEngine.AuthorizedIndices authorizedIndices,
+ TargetProjects authorizedProjects
) {
if (request instanceof IndicesAliasesRequest indicesAliasesRequest) {
ResolvedIndices.Builder resolvedIndicesBuilder = new ResolvedIndices.Builder();
@@ -130,7 +141,7 @@ ResolvedIndices resolve(
if (request instanceof IndicesRequest == false) {
throw new IllegalStateException("Request [" + request + "] is not an Indices request, but should be.");
}
- return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices);
+ return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices, authorizedProjects);
}
/**
@@ -282,6 +293,16 @@ ResolvedIndices resolveIndicesAndAliases(
IndicesRequest indicesRequest,
ProjectMetadata projectMetadata,
AuthorizationEngine.AuthorizedIndices authorizedIndices
+ ) {
+ return resolveIndicesAndAliases(action, indicesRequest, projectMetadata, authorizedIndices, TargetProjects.NOT_CROSS_PROJECT);
+ }
+
+ ResolvedIndices resolveIndicesAndAliases(
+ String action,
+ IndicesRequest indicesRequest,
+ ProjectMetadata projectMetadata,
+ AuthorizationEngine.AuthorizedIndices authorizedIndices,
+ TargetProjects authorizedProjects
) {
final ResolvedIndices.Builder resolvedIndicesBuilder = new ResolvedIndices.Builder();
boolean indicesReplacedWithNoIndices = false;
@@ -345,6 +366,7 @@ ResolvedIndices resolveIndicesAndAliases(
// if we cannot replace wildcards the indices list stays empty. Same if there are no authorized indices.
// we honour allow_no_indices like es core does.
} else {
+
final ResolvedIndices split;
if (replaceable.allowsRemoteIndices()) {
split = remoteClusterResolver.splitLocalAndRemoteIndexNames(indicesRequest.indices());
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
new file mode 100644
index 0000000000000..b9188c0fd24e4
--- /dev/null
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security.authz.crossproject;
+
+import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.core.Nullable;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+import org.elasticsearch.search.crossproject.ProjectRoutingInfo;
+import org.elasticsearch.transport.NoSuchRemoteClusterException;
+import org.elasticsearch.transport.RemoteClusterAware;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class for rewriting cross-project index expressions.
+ * Provides methods that can rewrite qualified and unqualified index expressions to canonical CCS.
+ */
+public class CrossProjectIndexExpressionsRewriter {
+ private static final Logger logger = LogManager.getLogger(CrossProjectIndexExpressionsRewriter.class);
+ private static final String ORIGIN_PROJECT_KEY = "_origin";
+ private static final String WILDCARD = "*";
+ private static final String[] MATCH_ALL = new String[] { WILDCARD };
+ private static final String EXCLUSION = "-";
+ private static final String DATE_MATH = "<";
+
+ /**
+ * Rewrites index expressions for cross-project search requests.
+ * Handles qualified and unqualified expressions and match-all cases will also hand exclusions in the future.
+ *
+ * @param originProject the _origin project with its alias
+ * @param linkedProjects the list of linked and available projects to consider for a request
+ * @param originalIndices the array of index expressions to be rewritten to canonical CCS
+ * @return a map from original index expressions to lists of canonical index expressions
+ * @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
+ * @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
+ */
+ public static Map> rewriteIndexExpressions(
+ ProjectRoutingInfo originProject,
+ List linkedProjects,
+ final String[] originalIndices
+ ) {
+ final String[] indices;
+ if (originalIndices == null || originalIndices.length == 0) { // handling of match all cases besides _all and `*`
+ // TODO this should be Metadata.ALL
+ indices = MATCH_ALL;
+ } else {
+ indices = originalIndices;
+ }
+ assert false == IndexNameExpressionResolver.isNoneExpression(indices)
+ : "expression list is *,-* which effectively means a request that requests no indices";
+ assert originProject != null || linkedProjects.isEmpty() == false
+ : "either origin project or linked projects must be in project target set";
+
+ Set linkedProjectNames = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
+ Map> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
+ for (String resource : indices) {
+ if (canonicalExpressionsMap.containsKey(resource)) {
+ continue;
+ }
+ maybeThrowOnUnsupportedResource(resource);
+
+ boolean isQualified = RemoteClusterAware.isRemoteIndexName(resource);
+ if (isQualified) {
+ String[] splitResource = RemoteClusterAware.splitIndexName(resource);
+ assert splitResource.length == 2
+ : "Expected two strings (project and indexExpression) for a qualified resource ["
+ + resource
+ + "], but found ["
+ + splitResource.length
+ + "]";
+ String projectAlias = splitResource[0];
+ assert projectAlias != null : "Expected a project alias for a qualified resource but was null";
+ String indexExpression = splitResource[1];
+ maybeThrowOnUnsupportedResource(indexExpression);
+
+ List canonicalExpressions = rewriteQualified(projectAlias, indexExpression, originProject, linkedProjectNames);
+
+ canonicalExpressionsMap.put(resource, canonicalExpressions);
+ logger.debug("Rewrote qualified expression [{}] to [{}]", resource, canonicalExpressions);
+ } else {
+ List canonicalExpressions = rewriteUnqualified(resource, originProject, linkedProjects);
+ canonicalExpressionsMap.put(resource, canonicalExpressions);
+ logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, canonicalExpressions);
+ }
+ }
+ return canonicalExpressionsMap;
+ }
+
+ private static List rewriteUnqualified(
+ String indexExpression,
+ @Nullable ProjectRoutingInfo origin,
+ List projects
+ ) {
+ List canonicalExpressions = new ArrayList<>();
+ if (origin != null) {
+ canonicalExpressions.add(indexExpression); // adding the original indexExpression for the _origin cluster.
+ }
+ for (ProjectRoutingInfo targetProject : projects) {
+ canonicalExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProject.projectAlias(), indexExpression));
+ }
+ return canonicalExpressions;
+ }
+
+ private static List rewriteQualified(
+ String requestedProjectAlias,
+ String indexExpression,
+ @Nullable ProjectRoutingInfo originProject,
+ Set allProjectAliases
+ ) {
+ if (originProject != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
+ // handling case where we have a qualified expression like: _origin:indexName
+ return List.of(indexExpression);
+ }
+
+ if (originProject == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
+ // handling case where we have a qualified expression like: _origin:indexName but no _origin project is set
+ throw new NoMatchingProjectException(requestedProjectAlias);
+ }
+
+ try {
+ if (originProject != null) {
+ allProjectAliases.add(originProject.projectAlias());
+ }
+ List resourcesMatchingAliases = new ArrayList<>();
+ List allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames(
+ allProjectAliases,
+ requestedProjectAlias
+ );
+
+ if (allProjectsMatchingAlias.isEmpty()) {
+ throw new NoMatchingProjectException(requestedProjectAlias);
+ }
+
+ for (String project : allProjectsMatchingAlias) {
+ if (originProject != null && project.equals(originProject.projectAlias())) {
+ resourcesMatchingAliases.add(indexExpression);
+ } else {
+ resourcesMatchingAliases.add(RemoteClusterAware.buildRemoteIndexName(project, indexExpression));
+ }
+ }
+
+ return resourcesMatchingAliases;
+ } catch (NoSuchRemoteClusterException ex) {
+ logger.debug(ex.getMessage(), ex);
+ throw new NoMatchingProjectException(requestedProjectAlias);
+ }
+ }
+
+ private static void maybeThrowOnUnsupportedResource(String resource) {
+ // TODO To be handled in future PR.
+ if (resource.startsWith(EXCLUSION)) {
+ throw new IllegalArgumentException("Exclusions are not currently supported but was found in the expression [" + resource + "]");
+ }
+ if (resource.startsWith(DATE_MATH)) {
+ throw new IllegalArgumentException("Date math are not currently supported but was found in the expression [" + resource + "]");
+ }
+ if (IndexNameExpressionResolver.hasSelectorSuffix(resource)) {
+ throw new IllegalArgumentException("Selectors are not currently supported but was found in the expression [" + resource + "]");
+
+ }
+ }
+}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/NoMatchingProjectException.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/NoMatchingProjectException.java
new file mode 100644
index 0000000000000..411c519156197
--- /dev/null
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/NoMatchingProjectException.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security.authz.crossproject;
+
+import org.elasticsearch.ResourceNotFoundException;
+
+/**
+ * An exception that a project is missing
+ */
+public final class NoMatchingProjectException extends ResourceNotFoundException {
+
+ public NoMatchingProjectException(String projectName) {
+ super("No such project: [" + projectName + "]");
+ }
+
+}
From eb3e45a8bce4767561e0b334cf754ea65f11eda0 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 25 Sep 2025 12:21:43 +0200
Subject: [PATCH 33/89] WIP resolve index
---
.../action/ResolvedIndexExpression.java | 37 +++++++++++-
.../action/ResolvedIndexExpressions.java | 15 ++++-
.../ResponseWithResolvedIndexExpressions.java | 15 +++++
.../indices/resolve/ResolveIndexAction.java | 56 ++++++++++++++++++-
.../search/crossproject/TargetProjects.java | 4 ++
5 files changed, 123 insertions(+), 4 deletions(-)
create mode 100644 server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java
diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
index 5180af78d22b2..9fbe7f5103cfa 100644
--- a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
+++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
@@ -10,8 +10,12 @@
package org.elasticsearch.action;
import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.core.Nullable;
+import java.io.IOException;
import java.util.Set;
/**
@@ -40,7 +44,21 @@
* and failure info
* @param remoteExpressions the remote expressions that replace the original
*/
-public record ResolvedIndexExpression(String original, LocalExpressions localExpressions, Set remoteExpressions) {
+public record ResolvedIndexExpression(String original, LocalExpressions localExpressions, Set remoteExpressions)
+ implements
+ Writeable {
+
+ public ResolvedIndexExpression(StreamInput in) throws IOException {
+ this(in.readString(), new LocalExpressions(in), in.readCollectionAsSet(StreamInput::readString));
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(original);
+ localExpressions.writeTo(out);
+ out.writeStringCollection(remoteExpressions);
+ }
+
/**
* Indicates if a local index resolution attempt was successful or failed.
* Failures can be due to concrete resources not being visible (either missing or not visible due to indices options)
@@ -60,10 +78,25 @@ public record LocalExpressions(
Set expressions,
LocalIndexResolutionResult localIndexResolutionResult,
@Nullable ElasticsearchException exception
- ) {
+ ) implements Writeable {
public LocalExpressions {
assert localIndexResolutionResult != LocalIndexResolutionResult.SUCCESS || exception == null
: "If the local resolution result is SUCCESS, exception must be null";
}
+
+ public LocalExpressions(StreamInput in) throws IOException {
+ this(
+ in.readCollectionAsSet(StreamInput::readString),
+ in.readEnum(LocalIndexResolutionResult.class),
+ ElasticsearchException.readException(in)
+ );
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeStringCollection(expressions);
+ out.writeEnum(localIndexResolutionResult);
+ ElasticsearchException.writeException(exception, out);
+ }
}
}
diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
index c02f8dfca3b24..bcd0442fe463d 100644
--- a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
+++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
@@ -10,7 +10,11 @@
package org.elasticsearch.action;
import org.elasticsearch.action.ResolvedIndexExpression.LocalExpressions;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -20,7 +24,11 @@
/**
* A collection of {@link ResolvedIndexExpression}.
*/
-public record ResolvedIndexExpressions(List expressions) {
+public record ResolvedIndexExpressions(List expressions) implements Writeable {
+
+ public ResolvedIndexExpressions(StreamInput in) throws IOException {
+ this(in.readCollectionAsList(ResolvedIndexExpression::new));
+ }
public List getLocalIndicesList() {
return expressions.stream().flatMap(e -> e.localExpressions().expressions().stream()).toList();
@@ -30,6 +38,11 @@ public static Builder builder() {
return new Builder();
}
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeCollection(expressions);
+ }
+
public static final class Builder {
private final List expressions = new ArrayList<>();
diff --git a/server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java b/server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java
new file mode 100644
index 0000000000000..c7ab869d139c4
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.action;
+
+public interface ResponseWithResolvedIndexExpressions {
+
+ ResolvedIndexExpressions getResolvedIndexExpressions();
+}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index 58ee074a0b951..a1ca30822da18 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -18,6 +18,8 @@
import org.elasticsearch.action.LegacyActionRequest;
import org.elasticsearch.action.OriginalIndices;
import org.elasticsearch.action.RemoteClusterActionType;
+import org.elasticsearch.action.ResolvedIndexExpressions;
+import org.elasticsearch.action.ResponseWithResolvedIndexExpressions;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.action.support.IndicesOptions;
@@ -78,6 +80,9 @@ public class ResolveIndexAction extends ActionType
private static final TransportVersion RESOLVE_INDEX_MODE_ADDED = TransportVersion.fromName("resolve_index_mode_added");
private static final TransportVersion RESOLVE_INDEX_MODE_FILTER = TransportVersion.fromName("resolve_index_mode_filter");
+ private static final TransportVersion RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG = TransportVersion.fromName(
+ "resolve_index_include_resolved_flag"
+ );
private ResolveIndexAction() {
super(NAME);
@@ -90,6 +95,8 @@ public static class Request extends LegacyActionRequest implements IndicesReques
private String[] names;
private IndicesOptions indicesOptions = DEFAULT_INDICES_OPTIONS;
private EnumSet indexModes = EnumSet.noneOf(IndexMode.class);
+ private ResolvedIndexExpressions resolvedIndexExpressions = null;
+ private boolean includeResolvedExpressions = false;
public Request(String[] names) {
this.names = names;
@@ -108,6 +115,20 @@ public Request(String[] names, IndicesOptions indicesOptions, @Nullable EnumSet<
}
}
+ public Request(
+ String[] names,
+ IndicesOptions indicesOptions,
+ @Nullable EnumSet indexModes,
+ boolean includeResolvedExpressions
+ ) {
+ this.names = names;
+ this.indicesOptions = indicesOptions;
+ if (indexModes != null) {
+ this.indexModes = indexModes;
+ }
+ this.includeResolvedExpressions = includeResolvedExpressions;
+ }
+
@Override
public ActionRequestValidationException validate() {
return null;
@@ -122,6 +143,11 @@ public Request(StreamInput in) throws IOException {
} else {
this.indexModes = EnumSet.noneOf(IndexMode.class);
}
+ if (in.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
+ this.includeResolvedExpressions = in.readBoolean();
+ } else {
+ this.includeResolvedExpressions = false;
+ }
}
@Override
@@ -132,6 +158,9 @@ public void writeTo(StreamOutput out) throws IOException {
if (out.getTransportVersion().supports(RESOLVE_INDEX_MODE_FILTER)) {
out.writeEnumSet(indexModes);
}
+ if (out.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
+ out.writeBoolean(includeResolvedExpressions);
+ }
}
@Override
@@ -168,6 +197,21 @@ public boolean allowsRemoteIndices() {
return true;
}
+ @Override
+ public boolean supportsCrossProjectSearch() {
+ return true;
+ }
+
+ @Override
+ public void setResolvedIndexExpressions(ResolvedIndexExpressions expressions) {
+ this.resolvedIndexExpressions = expressions;
+ }
+
+ @Override
+ public ResolvedIndexExpressions getResolvedIndexExpressions() {
+ return resolvedIndexExpressions;
+ }
+
@Override
public boolean includeDataStreams() {
// request must allow data streams because the index name expression resolver for the action handler assumes it
@@ -452,7 +496,7 @@ public String toString() {
}
}
- public static class Response extends ActionResponse implements ToXContentObject {
+ public static class Response extends ActionResponse implements ToXContentObject, ResponseWithResolvedIndexExpressions {
static final ParseField INDICES_FIELD = new ParseField("indices");
static final ParseField ALIASES_FIELD = new ParseField("aliases");
@@ -461,17 +505,21 @@ public static class Response extends ActionResponse implements ToXContentObject
private final List indices;
private final List aliases;
private final List dataStreams;
+ @Nullable
+ private final ResolvedIndexExpressions resolvedIndexExpressions;
public Response(List indices, List aliases, List dataStreams) {
this.indices = indices;
this.aliases = aliases;
this.dataStreams = dataStreams;
+ this.resolvedIndexExpressions = null;
}
public Response(StreamInput in) throws IOException {
this.indices = in.readCollectionAsList(ResolvedIndex::new);
this.aliases = in.readCollectionAsList(ResolvedAlias::new);
this.dataStreams = in.readCollectionAsList(ResolvedDataStream::new);
+ this.resolvedIndexExpressions = in.readOptionalWriteable(ResolvedIndexExpressions::new);
}
public List getIndices() {
@@ -491,6 +539,7 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeCollection(indices);
out.writeCollection(aliases);
out.writeCollection(dataStreams);
+ out.writeOptionalWriteable(resolvedIndexExpressions);
}
@Override
@@ -515,6 +564,11 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(indices, aliases, dataStreams);
}
+
+ @Override
+ public ResolvedIndexExpressions getResolvedIndexExpressions() {
+ return resolvedIndexExpressions;
+ }
}
public static class TransportAction extends HandledTransportAction {
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
index 76df1fab70a21..78aa4b321b5cd 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
@@ -13,4 +13,8 @@
public record TargetProjects(ProjectRoutingInfo originProject, List linkedProjects) {
public static TargetProjects NOT_CROSS_PROJECT = new TargetProjects(null, List.of());
+
+ public TargetProjects(ProjectRoutingInfo originProject) {
+ this(originProject, List.of());
+ }
}
From cfe91efd6442f87e8394535825f948c2e3b1f489 Mon Sep 17 00:00:00 2001
From: elasticsearchmachine
Date: Thu, 25 Sep 2025 10:43:52 +0000
Subject: [PATCH 34/89] [CI] Update transport version definitions
---
.../referable/resolve_index_include_resolved_flag.csv | 1 +
server/src/main/resources/transport/upper_bounds/9.2.csv | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
diff --git a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
new file mode 100644
index 0000000000000..833feabb55a10
--- /dev/null
+++ b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
@@ -0,0 +1 @@
+9171000
diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv
index 2c15e0254cbe8..2672f98fc63ee 100644
--- a/server/src/main/resources/transport/upper_bounds/9.2.csv
+++ b/server/src/main/resources/transport/upper_bounds/9.2.csv
@@ -1 +1 @@
-transform_check_for_dangling_tasks,9170000
+resolve_index_include_resolved_flag,9171000
From d39b249aef16f507970317cb25eb3823c9d8baa2 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 25 Sep 2025 12:48:09 +0200
Subject: [PATCH 35/89] More
---
.../elasticsearch/action/IndicesRequest.java | 2 +-
.../indices/resolve/ResolveIndexAction.java | 17 +++++--
.../action/search/SearchRequest.java | 5 --
.../admin/indices/RestResolveIndexAction.java | 7 ++-
.../resolve_index_include_resolved_flag.csv | 1 +
.../resources/transport/upper_bounds/9.2.csv | 2 +-
.../security/authz/AuthorizationService.java | 46 +++++++++----------
.../authz/IndicesAndAliasesResolver.java | 1 -
8 files changed, 45 insertions(+), 36 deletions(-)
create mode 100644 server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
diff --git a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
index 7b6535e042610..81ecdf176b565 100644
--- a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
@@ -81,7 +81,7 @@ default boolean allowsRemoteIndices() {
return false;
}
- default boolean supportsCrossProjectSearch() {
+ default boolean resolveCrossProject() {
return false;
}
}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index a1ca30822da18..261f4bf252e00 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -97,6 +97,7 @@ public static class Request extends LegacyActionRequest implements IndicesReques
private EnumSet indexModes = EnumSet.noneOf(IndexMode.class);
private ResolvedIndexExpressions resolvedIndexExpressions = null;
private boolean includeResolvedExpressions = false;
+ private boolean resolveCrossProject = false;
public Request(String[] names) {
this.names = names;
@@ -119,7 +120,8 @@ public Request(
String[] names,
IndicesOptions indicesOptions,
@Nullable EnumSet indexModes,
- boolean includeResolvedExpressions
+ boolean includeResolvedExpressions,
+ boolean resolveCrossProject
) {
this.names = names;
this.indicesOptions = indicesOptions;
@@ -127,6 +129,7 @@ public Request(
this.indexModes = indexModes;
}
this.includeResolvedExpressions = includeResolvedExpressions;
+ this.resolveCrossProject = resolveCrossProject;
}
@Override
@@ -148,6 +151,11 @@ public Request(StreamInput in) throws IOException {
} else {
this.includeResolvedExpressions = false;
}
+ if (in.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
+ this.resolveCrossProject = in.readBoolean();
+ } else {
+ this.resolveCrossProject = false;
+ }
}
@Override
@@ -161,6 +169,9 @@ public void writeTo(StreamOutput out) throws IOException {
if (out.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
out.writeBoolean(includeResolvedExpressions);
}
+ if (out.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
+ out.writeBoolean(resolveCrossProject);
+ }
}
@Override
@@ -198,8 +209,8 @@ public boolean allowsRemoteIndices() {
}
@Override
- public boolean supportsCrossProjectSearch() {
- return true;
+ public boolean resolveCrossProject() {
+ return resolveCrossProject;
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
index 14c10ed35a7a6..f84a3578264f8 100644
--- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
@@ -158,11 +158,6 @@ public boolean allowsRemoteIndices() {
return true;
}
- @Override
- public boolean supportsCrossProjectSearch() {
- return true;
- }
-
/**
* Creates a new sub-search request starting from the original search request that is provided.
* For internal use only, allows to fork a search request into multiple search requests that will be executed independently.
diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResolveIndexAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResolveIndexAction.java
index 0146fe6697ae0..09308d7034dff 100644
--- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResolveIndexAction.java
@@ -57,7 +57,8 @@ public Set supportedCapabilities() {
protected BaseRestHandler.RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
String[] indices = Strings.splitStringByCommaToArray(request.param("name"));
String modeParam = request.param("mode");
- if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) {
+ boolean crossProjectMode = settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false);
+ if (crossProjectMode) {
// accept but drop project_routing param until fully supported
request.param("project_routing");
}
@@ -68,7 +69,9 @@ protected BaseRestHandler.RestChannelConsumer prepareRequest(RestRequest request
? null
: Arrays.stream(modeParam.split(","))
.map(IndexMode::fromString)
- .collect(() -> EnumSet.noneOf(IndexMode.class), EnumSet::add, EnumSet::addAll)
+ .collect(() -> EnumSet.noneOf(IndexMode.class), EnumSet::add, EnumSet::addAll),
+ false,
+ crossProjectMode
);
return channel -> client.admin().indices().resolveIndex(resolveRequest, new RestToXContentListener<>(channel));
}
diff --git a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
new file mode 100644
index 0000000000000..833feabb55a10
--- /dev/null
+++ b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
@@ -0,0 +1 @@
+9171000
diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv
index 2c15e0254cbe8..2672f98fc63ee 100644
--- a/server/src/main/resources/transport/upper_bounds/9.2.csv
+++ b/server/src/main/resources/transport/upper_bounds/9.2.csv
@@ -1 +1 @@
-transform_check_for_dangling_tasks,9170000
+resolve_index_include_resolved_flag,9171000
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index 13831ca0356fc..5c811c56b1060 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -550,31 +550,31 @@ private void authorizeAction(
authzInfo,
projectMetadata.getIndicesLookup(),
ActionListener.wrap(authorizedIndices -> {
- if (request instanceof IndicesRequest.Replaceable replaceable && replaceable.supportsCrossProjectSearch()) {
- crossProjectSearchAuthzService.loadAuthorizedProjects(new ActionListener<>() {
- @Override
- public void onResponse(TargetProjects authorizedProjects) {
- logger.info("Loaded authorized projects: [{}]", authorizedProjects);
- resolvedIndicesListener.onResponse(
- indicesAndAliasesResolver.resolve(
- action,
- request,
- projectMetadata,
- authorizedIndices,
- authorizedProjects
- )
- );
- }
-
- @Override
- public void onFailure(Exception e) {
- logger.info("Failed to load authorized projects", e);
- resolvedIndicesListener.onFailure(e);
- }
- });
+ if (request instanceof IndicesRequest.Replaceable replaceable && replaceable.resolveCrossProject()) {
+ crossProjectSearchAuthzService.loadAuthorizedProjects(ActionListener.wrap(authorizedProjects -> {
+ logger.info("Loaded authorized projects: [{}]", authorizedProjects);
+ resolvedIndicesListener.onResponse(
+ indicesAndAliasesResolver.resolve(
+ action,
+ request,
+ projectMetadata,
+ authorizedIndices,
+ authorizedProjects
+ )
+ );
+ }, e -> {
+ logger.error("Failed to load authorized projects", e);
+ resolvedIndicesListener.onFailure(e);
+ }));
} else {
resolvedIndicesListener.onResponse(
- indicesAndAliasesResolver.resolve(action, request, projectMetadata, authorizedIndices)
+ indicesAndAliasesResolver.resolve(
+ action,
+ request,
+ projectMetadata,
+ authorizedIndices,
+ TargetProjects.NOT_CROSS_PROJECT
+ )
);
}
}, e -> {
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 6e6edbc56c6d6..b141103e51d01 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
@@ -366,7 +366,6 @@ ResolvedIndices resolveIndicesAndAliases(
// if we cannot replace wildcards the indices list stays empty. Same if there are no authorized indices.
// we honour allow_no_indices like es core does.
} else {
-
final ResolvedIndices split;
if (replaceable.allowsRemoteIndices()) {
split = remoteClusterResolver.splitLocalAndRemoteIndexNames(indicesRequest.indices());
From f1be6b4c24b6234cca50fae175914a8f8c7e44f5 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 25 Sep 2025 14:13:16 +0200
Subject: [PATCH 36/89] Moar
---
.../ResponseWithResolvedIndexExpressions.java | 15 ---------------
.../admin/indices/resolve/ResolveIndexAction.java | 4 +---
2 files changed, 1 insertion(+), 18 deletions(-)
delete mode 100644 server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java
diff --git a/server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java b/server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java
deleted file mode 100644
index c7ab869d139c4..0000000000000
--- a/server/src/main/java/org/elasticsearch/action/ResponseWithResolvedIndexExpressions.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-package org.elasticsearch.action;
-
-public interface ResponseWithResolvedIndexExpressions {
-
- ResolvedIndexExpressions getResolvedIndexExpressions();
-}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index 261f4bf252e00..2fb8fea6366fd 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -19,7 +19,6 @@
import org.elasticsearch.action.OriginalIndices;
import org.elasticsearch.action.RemoteClusterActionType;
import org.elasticsearch.action.ResolvedIndexExpressions;
-import org.elasticsearch.action.ResponseWithResolvedIndexExpressions;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.action.support.IndicesOptions;
@@ -507,7 +506,7 @@ public String toString() {
}
}
- public static class Response extends ActionResponse implements ToXContentObject, ResponseWithResolvedIndexExpressions {
+ public static class Response extends ActionResponse implements ToXContentObject {
static final ParseField INDICES_FIELD = new ParseField("indices");
static final ParseField ALIASES_FIELD = new ParseField("aliases");
@@ -576,7 +575,6 @@ public int hashCode() {
return Objects.hash(indices, aliases, dataStreams);
}
- @Override
public ResolvedIndexExpressions getResolvedIndexExpressions() {
return resolvedIndexExpressions;
}
From 515eb37860b1d694566cd4f25fbdc7a33ac4aa94 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 25 Sep 2025 15:26:58 +0200
Subject: [PATCH 37/89] WIP plug in rewriting
---
.../authz/IndicesAndAliasesResolver.java | 10 ++++++++
.../CrossProjectIndexExpressionsRewriter.java | 25 +++++++++----------
2 files changed, 22 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 b141103e51d01..c02ff8ae344b6 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
@@ -39,6 +39,7 @@
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField;
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
+import org.elasticsearch.xpack.security.authz.crossproject.CrossProjectIndexExpressionsRewriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -366,6 +367,15 @@ ResolvedIndices resolveIndicesAndAliases(
// if we cannot replace wildcards the indices list stays empty. Same if there are no authorized indices.
// we honour allow_no_indices like es core does.
} else {
+ if (replaceable.resolveCrossProject() && authorizedProjects != TargetProjects.NOT_CROSS_PROJECT) {
+ assert replaceable.allowsRemoteIndices() : "cross-project requests must allow remote indices";
+ Map> rewritten = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
+ authorizedProjects.originProject(),
+ authorizedProjects.linkedProjects(),
+ replaceable.indices()
+ );
+ }
+
final ResolvedIndices split;
if (replaceable.allowsRemoteIndices()) {
split = remoteClusterResolver.splitLocalAndRemoteIndexNames(indicesRequest.indices());
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
index b9188c0fd24e4..3d2d5f221f400 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
@@ -9,6 +9,7 @@
import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
@@ -16,8 +17,8 @@
import org.elasticsearch.transport.NoSuchRemoteClusterException;
import org.elasticsearch.transport.RemoteClusterAware;
-import java.util.ArrayList;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -30,8 +31,7 @@
public class CrossProjectIndexExpressionsRewriter {
private static final Logger logger = LogManager.getLogger(CrossProjectIndexExpressionsRewriter.class);
private static final String ORIGIN_PROJECT_KEY = "_origin";
- private static final String WILDCARD = "*";
- private static final String[] MATCH_ALL = new String[] { WILDCARD };
+ private static final String[] MATCH_ALL = new String[] { Metadata.ALL };
private static final String EXCLUSION = "-";
private static final String DATE_MATH = "<";
@@ -46,14 +46,13 @@ public class CrossProjectIndexExpressionsRewriter {
* @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
* @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
*/
- public static Map> rewriteIndexExpressions(
+ public static Map> rewriteIndexExpressions(
ProjectRoutingInfo originProject,
List linkedProjects,
final String[] originalIndices
) {
final String[] indices;
if (originalIndices == null || originalIndices.length == 0) { // handling of match all cases besides _all and `*`
- // TODO this should be Metadata.ALL
indices = MATCH_ALL;
} else {
indices = originalIndices;
@@ -64,7 +63,7 @@ public static Map> rewriteIndexExpressions(
: "either origin project or linked projects must be in project target set";
Set linkedProjectNames = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
- Map> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
+ Map> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
for (String resource : indices) {
if (canonicalExpressionsMap.containsKey(resource)) {
continue;
@@ -85,12 +84,12 @@ public static Map> rewriteIndexExpressions(
String indexExpression = splitResource[1];
maybeThrowOnUnsupportedResource(indexExpression);
- List canonicalExpressions = rewriteQualified(projectAlias, indexExpression, originProject, linkedProjectNames);
+ Set canonicalExpressions = rewriteQualified(projectAlias, indexExpression, originProject, linkedProjectNames);
canonicalExpressionsMap.put(resource, canonicalExpressions);
logger.debug("Rewrote qualified expression [{}] to [{}]", resource, canonicalExpressions);
} else {
- List canonicalExpressions = rewriteUnqualified(resource, originProject, linkedProjects);
+ Set canonicalExpressions = rewriteUnqualified(resource, originProject, linkedProjects);
canonicalExpressionsMap.put(resource, canonicalExpressions);
logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, canonicalExpressions);
}
@@ -98,12 +97,12 @@ public static Map> rewriteIndexExpressions(
return canonicalExpressionsMap;
}
- private static List rewriteUnqualified(
+ private static Set rewriteUnqualified(
String indexExpression,
@Nullable ProjectRoutingInfo origin,
List projects
) {
- List canonicalExpressions = new ArrayList<>();
+ Set canonicalExpressions = new LinkedHashSet<>();
if (origin != null) {
canonicalExpressions.add(indexExpression); // adding the original indexExpression for the _origin cluster.
}
@@ -113,7 +112,7 @@ private static List rewriteUnqualified(
return canonicalExpressions;
}
- private static List rewriteQualified(
+ private static Set rewriteQualified(
String requestedProjectAlias,
String indexExpression,
@Nullable ProjectRoutingInfo originProject,
@@ -121,7 +120,7 @@ private static List rewriteQualified(
) {
if (originProject != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
// handling case where we have a qualified expression like: _origin:indexName
- return List.of(indexExpression);
+ return Set.of(indexExpression);
}
if (originProject == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
@@ -133,7 +132,7 @@ private static List rewriteQualified(
if (originProject != null) {
allProjectAliases.add(originProject.projectAlias());
}
- List resourcesMatchingAliases = new ArrayList<>();
+ Set resourcesMatchingAliases = new LinkedHashSet<>();
List allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames(
allProjectAliases,
requestedProjectAlias
From 6f79ff0e655e73155108759d0c723a280d119d44 Mon Sep 17 00:00:00 2001
From: elasticsearchmachine
Date: Thu, 25 Sep 2025 13:44:20 +0000
Subject: [PATCH 38/89] [CI] Update transport version definitions
---
.../referable/resolve_index_include_resolved_flag.csv | 2 +-
server/src/main/resources/transport/upper_bounds/9.2.csv | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
index 833feabb55a10..42d1df167e3b9 100644
--- a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
+++ b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
@@ -1 +1 @@
-9171000
+9172000
diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv
index 2672f98fc63ee..c561d237e6a78 100644
--- a/server/src/main/resources/transport/upper_bounds/9.2.csv
+++ b/server/src/main/resources/transport/upper_bounds/9.2.csv
@@ -1 +1 @@
-resolve_index_include_resolved_flag,9171000
+resolve_index_include_resolved_flag,9172000
From cf76a8f5ca037e71922c4078e299b214a46b64ba Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 09:48:30 +0200
Subject: [PATCH 39/89] Plug in CPS rewrites
---
server/src/main/java/module-info.java | 1 +
.../action/ResolvedIndexExpression.java | 1 +
.../action/ResolvedIndexExpressions.java | 25 ++-
.../metadata/IndexAbstractionResolver.java | 202 ++++++++++++------
.../CrossProjectIndexExpressionsRewriter.java | 145 +++++++------
.../NoMatchingProjectException.java | 11 +-
.../search/crossproject/TargetProjects.java | 10 +
...rossProjectSearchAuthorizationService.java | 7 +
.../security/authz/AuthorizationService.java | 11 +-
.../authz/IndicesAndAliasesResolver.java | 63 +++---
10 files changed, 302 insertions(+), 174 deletions(-)
rename {x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz => server/src/main/java/org/elasticsearch/search}/crossproject/CrossProjectIndexExpressionsRewriter.java (51%)
rename {x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz => server/src/main/java/org/elasticsearch/search}/crossproject/NoMatchingProjectException.java (52%)
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index 68dc8da3b7d15..856633f0f8350 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -55,6 +55,7 @@
requires org.apache.lucene.queryparser;
requires org.apache.lucene.sandbox;
requires org.apache.lucene.suggest;
+ requires org.jruby.jcodings;
exports org.elasticsearch;
exports org.elasticsearch.action;
diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
index 9fbe7f5103cfa..e0624d9370e46 100644
--- a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
+++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
@@ -66,6 +66,7 @@ public void writeTo(StreamOutput out) throws IOException {
* A wildcard expression resolving to nothing is still considered a successful resolution.
*/
public enum LocalIndexResolutionResult {
+ NONE,
SUCCESS,
CONCRETE_RESOURCE_NOT_VISIBLE,
CONCRETE_RESOURCE_UNAUTHORIZED,
diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
index bcd0442fe463d..c149c0d0649b1 100644
--- a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
+++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
@@ -34,6 +34,10 @@ public List getLocalIndicesList() {
return expressions.stream().flatMap(e -> e.localExpressions().expressions().stream()).toList();
}
+ public List getRemoteIndicesList() {
+ return expressions.stream().flatMap(e -> e.remoteExpressions().stream()).toList();
+ }
+
public static Builder builder() {
return new Builder();
}
@@ -47,20 +51,35 @@ public static final class Builder {
private final List expressions = new ArrayList<>();
/**
+ * Add a new resolved expression.
* @param original the original expression that was resolved -- may be blank for "access all" cases
* @param localExpressions is a HashSet as an optimization -- the set needs to be mutable, and we want to avoid copying it.
* May be empty.
*/
- public void addLocalExpressions(
+ public void addExpressions(
String original,
HashSet localExpressions,
- ResolvedIndexExpression.LocalIndexResolutionResult resolutionResult
+ ResolvedIndexExpression.LocalIndexResolutionResult resolutionResult,
+ HashSet remoteExpressions
) {
Objects.requireNonNull(original);
Objects.requireNonNull(localExpressions);
Objects.requireNonNull(resolutionResult);
+ Objects.requireNonNull(remoteExpressions);
+ expressions.add(
+ new ResolvedIndexExpression(original, new LocalExpressions(localExpressions, resolutionResult, null), remoteExpressions)
+ );
+ }
+
+ public void addRemoteExpressions(String original, HashSet remoteExpressions) {
+ Objects.requireNonNull(original);
+ Objects.requireNonNull(remoteExpressions);
expressions.add(
- new ResolvedIndexExpression(original, new LocalExpressions(localExpressions, resolutionResult, null), new HashSet<>())
+ new ResolvedIndexExpression(
+ original,
+ new LocalExpressions(new HashSet<>(), ResolvedIndexExpression.LocalIndexResolutionResult.NONE, null),
+ remoteExpressions
+ )
);
}
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
index 6e6d9844c1154..52cde7b95eab8 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
@@ -20,6 +20,8 @@
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel;
+import org.elasticsearch.search.crossproject.CrossProjectIndexExpressionsRewriter;
+import org.elasticsearch.search.crossproject.TargetProjects;
import java.util.HashSet;
import java.util.List;
@@ -48,92 +50,156 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
boolean includeDataStreams
) {
final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder = ResolvedIndexExpressions.builder();
+ boolean wildcardSeen = false;
+ for (String index : indices) {
+ wildcardSeen = resolveIndexAbstraction(
+ index,
+ indicesOptions,
+ projectMetadata,
+ allAuthorizedAndAvailableBySelector,
+ isAuthorized,
+ includeDataStreams,
+ wildcardSeen,
+ resolvedExpressionsBuilder,
+ new HashSet<>()
+ );
+ }
+ return resolvedExpressionsBuilder.build();
+ }
+ public ResolvedIndexExpressions resolveIndexAbstractions(
+ List indices,
+ IndicesOptions indicesOptions,
+ ProjectMetadata projectMetadata,
+ Function> allAuthorizedAndAvailableBySelector,
+ BiPredicate isAuthorized,
+ TargetProjects targetProjects,
+ boolean includeDataStreams
+ ) {
+ final String originProjectAlias = targetProjects.originProjectAlias();
+ final Set linkedProjectAliases = targetProjects.linkedProjectAliases();
+ final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder = ResolvedIndexExpressions.builder();
boolean wildcardSeen = false;
for (String index : indices) {
- String indexAbstraction;
- boolean minus = false;
- if (index.charAt(0) == '-' && wildcardSeen) {
- indexAbstraction = index.substring(1);
- minus = true;
- } else {
- indexAbstraction = index;
+ CrossProjectIndexExpressionsRewriter.Result rewritten = CrossProjectIndexExpressionsRewriter.rewrite(
+ index,
+ originProjectAlias,
+ linkedProjectAliases
+ );
+ if (rewritten.local() == null) {
+ resolvedExpressionsBuilder.addRemoteExpressions(index, rewritten.remote());
+ continue;
}
- // Always check to see if there's a selector on the index expression
- final Tuple expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexAbstraction);
- final String selectorString = expressionAndSelector.v2();
- if (indicesOptions.allowSelectors() == false && selectorString != null) {
- throw new UnsupportedSelectorException(indexAbstraction);
- }
- indexAbstraction = expressionAndSelector.v1();
- IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString);
+ wildcardSeen = resolveIndexAbstraction(
+ index,
+ indicesOptions,
+ projectMetadata,
+ allAuthorizedAndAvailableBySelector,
+ isAuthorized,
+ includeDataStreams,
+ wildcardSeen,
+ resolvedExpressionsBuilder,
+ rewritten.remote()
+ );
+ }
- // we always need to check for date math expressions
- indexAbstraction = IndexNameExpressionResolver.resolveDateMathExpression(indexAbstraction);
+ return resolvedExpressionsBuilder.build();
+ }
+
+ private boolean resolveIndexAbstraction(
+ String index,
+ IndicesOptions indicesOptions,
+ ProjectMetadata projectMetadata,
+ Function> allAuthorizedAndAvailableBySelector,
+ BiPredicate isAuthorized,
+ boolean includeDataStreams,
+ boolean wildcardSeen,
+ ResolvedIndexExpressions.Builder resolvedExpressionsBuilder,
+ HashSet remoteIndices
+ ) {
+ String indexAbstraction;
+ boolean minus = false;
+ if (index.charAt(0) == '-' && wildcardSeen) {
+ indexAbstraction = index.substring(1);
+ minus = true;
+ } else {
+ indexAbstraction = index;
+ }
- if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) {
- wildcardSeen = true;
- final HashSet resolvedIndices = new HashSet<>();
- for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selector)) {
- if (Regex.simpleMatch(indexAbstraction, authorizedIndex)
+ // Always check to see if there's a selector on the index expression
+ final Tuple expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexAbstraction);
+ final String selectorString = expressionAndSelector.v2();
+ if (indicesOptions.allowSelectors() == false && selectorString != null) {
+ throw new UnsupportedSelectorException(indexAbstraction);
+ }
+ indexAbstraction = expressionAndSelector.v1();
+ IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString);
+
+ // we always need to check for date math expressions
+ indexAbstraction = IndexNameExpressionResolver.resolveDateMathExpression(indexAbstraction);
+
+ if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) {
+ wildcardSeen = true;
+ final HashSet resolvedIndices = new HashSet<>();
+ for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selector)) {
+ if (Regex.simpleMatch(indexAbstraction, authorizedIndex)
+ && isIndexVisible(
+ indexAbstraction,
+ selectorString,
+ authorizedIndex,
+ indicesOptions,
+ projectMetadata,
+ indexNameExpressionResolver,
+ includeDataStreams
+ )) {
+ resolveSelectorsAndCollect(authorizedIndex, selectorString, indicesOptions, resolvedIndices, projectMetadata);
+ }
+ }
+ if (resolvedIndices.isEmpty()) {
+ // es core honours allow_no_indices for each wildcard expression, we do the same here by throwing index not found.
+ if (indicesOptions.allowNoIndices() == false) {
+ throw new IndexNotFoundException(indexAbstraction);
+ }
+ resolvedExpressionsBuilder.addExpressions(index, new HashSet<>(), SUCCESS, remoteIndices);
+ } else {
+ if (minus) {
+ resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
+ } else {
+ resolvedExpressionsBuilder.addExpressions(index, resolvedIndices, SUCCESS, remoteIndices);
+ }
+ }
+ } else {
+ final HashSet resolvedIndices = new HashSet<>();
+ resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, projectMetadata);
+ if (minus) {
+ resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
+ } else {
+ final boolean authorized = isAuthorized.test(indexAbstraction, selector);
+ if (authorized) {
+ final boolean visible = indexExists(projectMetadata, indexAbstraction)
&& isIndexVisible(
indexAbstraction,
selectorString,
- authorizedIndex,
+ indexAbstraction,
indicesOptions,
projectMetadata,
indexNameExpressionResolver,
includeDataStreams
- )) {
- resolveSelectorsAndCollect(authorizedIndex, selectorString, indicesOptions, resolvedIndices, projectMetadata);
- }
- }
- if (resolvedIndices.isEmpty()) {
- // es core honours allow_no_indices for each wildcard expression, we do the same here by throwing index not found.
- if (indicesOptions.allowNoIndices() == false) {
- throw new IndexNotFoundException(indexAbstraction);
- }
- resolvedExpressionsBuilder.addLocalExpressions(index, new HashSet<>(), SUCCESS);
- } else {
- if (minus) {
- resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
- } else {
- resolvedExpressionsBuilder.addLocalExpressions(index, resolvedIndices, SUCCESS);
- }
- }
- } else {
- final HashSet resolvedIndices = new HashSet<>();
- resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, projectMetadata);
- if (minus) {
- resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
+ );
+ final LocalIndexResolutionResult result = visible ? SUCCESS : CONCRETE_RESOURCE_NOT_VISIBLE;
+ resolvedExpressionsBuilder.addExpressions(index, resolvedIndices, result, remoteIndices);
+ } else if (indicesOptions.ignoreUnavailable()) {
+ // ignoreUnavailable implies that the request should not fail if an index is not authorized
+ // so we map this expression to an empty list,
+ resolvedExpressionsBuilder.addExpressions(index, new HashSet<>(), CONCRETE_RESOURCE_UNAUTHORIZED, remoteIndices);
} else {
- final boolean authorized = isAuthorized.test(indexAbstraction, selector);
- if (authorized) {
- final boolean visible = indexExists(projectMetadata, indexAbstraction)
- && isIndexVisible(
- indexAbstraction,
- selectorString,
- indexAbstraction,
- indicesOptions,
- projectMetadata,
- indexNameExpressionResolver,
- includeDataStreams
- );
- final LocalIndexResolutionResult result = visible ? SUCCESS : CONCRETE_RESOURCE_NOT_VISIBLE;
- resolvedExpressionsBuilder.addLocalExpressions(index, resolvedIndices, result);
- } else if (indicesOptions.ignoreUnavailable()) {
- // ignoreUnavailable implies that the request should not fail if an index is not authorized
- // so we map this expression to an empty list,
- resolvedExpressionsBuilder.addLocalExpressions(index, new HashSet<>(), CONCRETE_RESOURCE_UNAUTHORIZED);
- } else {
- // store the calculated expansion as unauthorized, it will be rejected later
- resolvedExpressionsBuilder.addLocalExpressions(index, resolvedIndices, CONCRETE_RESOURCE_UNAUTHORIZED);
- }
+ // store the calculated expansion as unauthorized, it will be rejected later
+ resolvedExpressionsBuilder.addExpressions(index, resolvedIndices, CONCRETE_RESOURCE_UNAUTHORIZED, remoteIndices);
}
}
}
- return resolvedExpressionsBuilder.build();
+ return wildcardSeen;
}
private static void resolveSelectorsAndCollect(
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
similarity index 51%
rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
rename to server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
index 3d2d5f221f400..f472ab9060fb6 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/CrossProjectIndexExpressionsRewriter.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
@@ -1,11 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
*/
-package org.elasticsearch.xpack.security.authz.crossproject;
+package org.elasticsearch.search.crossproject;
import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
@@ -13,14 +15,13 @@
import org.elasticsearch.core.Nullable;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
-import org.elasticsearch.search.crossproject.ProjectRoutingInfo;
import org.elasticsearch.transport.NoSuchRemoteClusterException;
import org.elasticsearch.transport.RemoteClusterAware;
-import java.util.LinkedHashMap;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -35,6 +36,8 @@ public class CrossProjectIndexExpressionsRewriter {
private static final String EXCLUSION = "-";
private static final String DATE_MATH = "<";
+ public record Result(@Nullable String local, HashSet remote) {}
+
/**
* Rewrites index expressions for cross-project search requests.
* Handles qualified and unqualified expressions and match-all cases will also hand exclusions in the future.
@@ -46,7 +49,7 @@ public class CrossProjectIndexExpressionsRewriter {
* @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
* @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
*/
- public static Map> rewriteIndexExpressions(
+ public static List rewriteIndexExpressions(
ProjectRoutingInfo originProject,
List linkedProjects,
final String[] originalIndices
@@ -63,100 +66,97 @@ public static Map> rewriteIndexExpressions(
: "either origin project or linked projects must be in project target set";
Set linkedProjectNames = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
- Map> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
+ List results = new ArrayList<>(indices.length);
for (String resource : indices) {
- if (canonicalExpressionsMap.containsKey(resource)) {
- continue;
- }
- maybeThrowOnUnsupportedResource(resource);
-
- boolean isQualified = RemoteClusterAware.isRemoteIndexName(resource);
- if (isQualified) {
- String[] splitResource = RemoteClusterAware.splitIndexName(resource);
- assert splitResource.length == 2
- : "Expected two strings (project and indexExpression) for a qualified resource ["
- + resource
- + "], but found ["
- + splitResource.length
- + "]";
- String projectAlias = splitResource[0];
- assert projectAlias != null : "Expected a project alias for a qualified resource but was null";
- String indexExpression = splitResource[1];
- maybeThrowOnUnsupportedResource(indexExpression);
-
- Set canonicalExpressions = rewriteQualified(projectAlias, indexExpression, originProject, linkedProjectNames);
-
- canonicalExpressionsMap.put(resource, canonicalExpressions);
- logger.debug("Rewrote qualified expression [{}] to [{}]", resource, canonicalExpressions);
- } else {
- Set canonicalExpressions = rewriteUnqualified(resource, originProject, linkedProjects);
- canonicalExpressionsMap.put(resource, canonicalExpressions);
- logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, canonicalExpressions);
- }
+ results.add(rewrite(resource, originProject == null ? null : originProject.projectAlias(), linkedProjectNames));
}
- return canonicalExpressionsMap;
+ return results;
}
- private static Set rewriteUnqualified(
- String indexExpression,
- @Nullable ProjectRoutingInfo origin,
- List projects
- ) {
- Set canonicalExpressions = new LinkedHashSet<>();
- if (origin != null) {
- canonicalExpressions.add(indexExpression); // adding the original indexExpression for the _origin cluster.
- }
- for (ProjectRoutingInfo targetProject : projects) {
- canonicalExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProject.projectAlias(), indexExpression));
+ /**
+ * Rewrites a single index expression for cross-project search requests.
+ * Handles qualified and unqualified expressions and match-all cases will also hand exclusions in the future.
+ *
+ * @param originProjectAlias the _origin project with its alias
+ * @return a map from original index expressions to lists of canonical index expressions
+ * @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
+ * @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
+ */
+ public static Result rewrite(String resource, @Nullable String originProjectAlias, Set linkedProjectAliases) {
+ maybeThrowOnUnsupportedResource(resource);
+
+ boolean isQualified = RemoteClusterAware.isRemoteIndexName(resource);
+
+ Result result;
+ if (isQualified) {
+ result = rewriteQualified(resource, originProjectAlias, linkedProjectAliases);
+ logger.debug("Rewrote qualified expression [{}] to [{}]", resource, result);
+ } else {
+ result = rewriteUnqualified(resource, originProjectAlias, linkedProjectAliases);
+ logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, result);
}
- return canonicalExpressions;
+ return result;
}
- private static Set rewriteQualified(
- String requestedProjectAlias,
- String indexExpression,
- @Nullable ProjectRoutingInfo originProject,
- Set allProjectAliases
- ) {
- if (originProject != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
+ public static Result rewriteQualified(String resource, @Nullable String originProjectAlias, Set linkedProjectNames) {
+ String[] splitResource = RemoteClusterAware.splitIndexName(resource);
+ assert splitResource.length == 2
+ : "Expected two strings (project and indexExpression) for a qualified resource ["
+ + resource
+ + "], but found ["
+ + splitResource.length
+ + "]";
+ String projectAlias = splitResource[0];
+ assert projectAlias != null : "Expected a project alias for a qualified resource but was null";
+ String indexExpression = splitResource[1];
+ maybeThrowOnUnsupportedResource(indexExpression);
+
+ if (originProjectAlias != null && ORIGIN_PROJECT_KEY.equals(projectAlias)) {
// handling case where we have a qualified expression like: _origin:indexName
- return Set.of(indexExpression);
+ return new Result(indexExpression, new HashSet<>());
}
- if (originProject == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
+ if (originProjectAlias == null && ORIGIN_PROJECT_KEY.equals(projectAlias)) {
// handling case where we have a qualified expression like: _origin:indexName but no _origin project is set
- throw new NoMatchingProjectException(requestedProjectAlias);
+ throw new NoMatchingProjectException(projectAlias);
}
try {
- if (originProject != null) {
- allProjectAliases.add(originProject.projectAlias());
+ if (originProjectAlias != null) {
+ linkedProjectNames.add(originProjectAlias);
}
- Set resourcesMatchingAliases = new LinkedHashSet<>();
- List allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames(
- allProjectAliases,
- requestedProjectAlias
- );
+ HashSet resourcesMatchingAliases = new LinkedHashSet<>();
+ List allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames(linkedProjectNames, projectAlias);
if (allProjectsMatchingAlias.isEmpty()) {
- throw new NoMatchingProjectException(requestedProjectAlias);
+ throw new NoMatchingProjectException(projectAlias);
}
+ boolean includeOrigin = false;
for (String project : allProjectsMatchingAlias) {
- if (originProject != null && project.equals(originProject.projectAlias())) {
- resourcesMatchingAliases.add(indexExpression);
+ if (project.equals(originProjectAlias)) {
+ includeOrigin = true;
} else {
resourcesMatchingAliases.add(RemoteClusterAware.buildRemoteIndexName(project, indexExpression));
}
}
- return resourcesMatchingAliases;
+ return new Result(includeOrigin ? indexExpression : null, resourcesMatchingAliases);
} catch (NoSuchRemoteClusterException ex) {
logger.debug(ex.getMessage(), ex);
- throw new NoMatchingProjectException(requestedProjectAlias);
+ throw new NoMatchingProjectException(projectAlias);
}
}
+ public static Result rewriteUnqualified(String indexExpression, @Nullable String originProjectAlias, Set linkedProjects) {
+ HashSet remoteExpressions = new LinkedHashSet<>();
+ for (String targetProject : linkedProjects) {
+ remoteExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProject, indexExpression));
+ }
+ boolean includeOrigin = originProjectAlias != null;
+ return new Result(includeOrigin ? indexExpression : null, remoteExpressions);
+ }
+
private static void maybeThrowOnUnsupportedResource(String resource) {
// TODO To be handled in future PR.
if (resource.startsWith(EXCLUSION)) {
@@ -167,7 +167,6 @@ private static void maybeThrowOnUnsupportedResource(String resource) {
}
if (IndexNameExpressionResolver.hasSelectorSuffix(resource)) {
throw new IllegalArgumentException("Selectors are not currently supported but was found in the expression [" + resource + "]");
-
}
}
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/NoMatchingProjectException.java b/server/src/main/java/org/elasticsearch/search/crossproject/NoMatchingProjectException.java
similarity index 52%
rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/NoMatchingProjectException.java
rename to server/src/main/java/org/elasticsearch/search/crossproject/NoMatchingProjectException.java
index 411c519156197..7b3932caf0f0f 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/crossproject/NoMatchingProjectException.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/NoMatchingProjectException.java
@@ -1,3 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
@@ -5,7 +14,7 @@
* 2.0.
*/
-package org.elasticsearch.xpack.security.authz.crossproject;
+package org.elasticsearch.search.crossproject;
import org.elasticsearch.ResourceNotFoundException;
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
index 78aa4b321b5cd..536dac2f2a17f 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
@@ -10,6 +10,8 @@
package org.elasticsearch.search.crossproject;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
public record TargetProjects(ProjectRoutingInfo originProject, List linkedProjects) {
public static TargetProjects NOT_CROSS_PROJECT = new TargetProjects(null, List.of());
@@ -17,4 +19,12 @@ public record TargetProjects(ProjectRoutingInfo originProject, List linkedProjectAliases() {
+ return linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
+ }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
index a8de82c857416..746ac2196815e 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
@@ -13,10 +13,17 @@
public interface CrossProjectSearchAuthorizationService {
void loadAuthorizedProjects(ActionListener listener);
+ boolean enabled();
+
class Default implements CrossProjectSearchAuthorizationService {
@Override
public void loadAuthorizedProjects(ActionListener listener) {
listener.onResponse(TargetProjects.NOT_CROSS_PROJECT);
}
+
+ @Override
+ public boolean enabled() {
+ return false;
+ }
}
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index 5c811c56b1060..424d18febf4c6 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -212,12 +212,17 @@ public AuthorizationService(
AuthorizationDenialMessages authorizationDenialMessages,
LinkedProjectConfigService linkedProjectConfigService,
ProjectResolver projectResolver,
- CrossProjectSearchAuthorizationService crossProjectSearchIndexExpressionsRewriter
+ CrossProjectSearchAuthorizationService crossProjectSearchAuthorizationService
) {
this.clusterService = clusterService;
this.auditTrailService = auditTrailService;
this.restrictedIndices = restrictedIndices;
- this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, linkedProjectConfigService, resolver, false);
+ this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(
+ settings,
+ linkedProjectConfigService,
+ resolver,
+ crossProjectSearchAuthorizationService.enabled()
+ );
this.authcFailureHandler = authcFailureHandler;
this.threadContext = threadPool.getThreadContext();
this.securityContext = new SecurityContext(settings, this.threadContext);
@@ -238,7 +243,7 @@ public AuthorizationService(
this.indicesAccessControlWrapper = new DlsFlsFeatureTrackingIndicesAccessControlWrapper(settings, licenseState);
this.authorizationDenialMessages = authorizationDenialMessages;
this.projectResolver = projectResolver;
- this.crossProjectSearchAuthzService = crossProjectSearchIndexExpressionsRewriter;
+ this.crossProjectSearchAuthzService = crossProjectSearchAuthorizationService;
}
public void checkPrivileges(
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 c02ff8ae344b6..0744c5f0c2d0e 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
@@ -39,7 +39,6 @@
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField;
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
-import org.elasticsearch.xpack.security.authz.crossproject.CrossProjectIndexExpressionsRewriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -369,35 +368,47 @@ ResolvedIndices resolveIndicesAndAliases(
} else {
if (replaceable.resolveCrossProject() && authorizedProjects != TargetProjects.NOT_CROSS_PROJECT) {
assert replaceable.allowsRemoteIndices() : "cross-project requests must allow remote indices";
- Map> rewritten = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- authorizedProjects.originProject(),
- authorizedProjects.linkedProjects(),
- replaceable.indices()
+ assert recordResolvedIndexExpressions : "cross-project requests must record resolved index expressions";
+
+ final ResolvedIndexExpressions resolved = indexAbstractionResolver.resolveIndexAbstractions(
+ Arrays.asList(replaceable.indices()),
+ // TODO make lenient
+ indicesOptions,
+ projectMetadata,
+ authorizedIndices::all,
+ authorizedIndices::check,
+ authorizedProjects,
+ indicesRequest.includeDataStreams()
);
- }
- final ResolvedIndices split;
- if (replaceable.allowsRemoteIndices()) {
- split = remoteClusterResolver.splitLocalAndRemoteIndexNames(indicesRequest.indices());
- } else {
- split = new ResolvedIndices(Arrays.asList(indicesRequest.indices()), Collections.emptyList());
- }
- final ResolvedIndexExpressions resolved = indexAbstractionResolver.resolveIndexAbstractions(
- split.getLocal(),
- indicesOptions,
- projectMetadata,
- authorizedIndices::all,
- authorizedIndices::check,
- indicesRequest.includeDataStreams()
- );
- // only store resolved expressions if configured, to avoid unnecessary memory usage
- // once we've migrated from `indices()` to using resolved expressions holistically,
- // we will always store them
- if (recordResolvedIndexExpressions) {
replaceable.setResolvedIndexExpressions(resolved);
+
+ resolvedIndicesBuilder.addLocal(resolved.getLocalIndicesList());
+ resolvedIndicesBuilder.addRemote(resolved.getRemoteIndicesList());
+ } else {
+ final ResolvedIndices split;
+ if (replaceable.allowsRemoteIndices()) {
+ split = remoteClusterResolver.splitLocalAndRemoteIndexNames(indicesRequest.indices());
+ } else {
+ split = new ResolvedIndices(Arrays.asList(indicesRequest.indices()), Collections.emptyList());
+ }
+ final ResolvedIndexExpressions resolved = indexAbstractionResolver.resolveIndexAbstractions(
+ split.getLocal(),
+ indicesOptions,
+ projectMetadata,
+ authorizedIndices::all,
+ authorizedIndices::check,
+ indicesRequest.includeDataStreams()
+ );
+ // only store resolved expressions if configured, to avoid unnecessary memory usage
+ // once we've migrated from `indices()` to using resolved expressions holistically,
+ // we will always store them
+ if (recordResolvedIndexExpressions) {
+ replaceable.setResolvedIndexExpressions(resolved);
+ }
+ resolvedIndicesBuilder.addLocal(resolved.getLocalIndicesList());
+ resolvedIndicesBuilder.addRemote(split.getRemote());
}
- resolvedIndicesBuilder.addLocal(resolved.getLocalIndicesList());
- resolvedIndicesBuilder.addRemote(split.getRemote());
}
if (resolvedIndicesBuilder.isEmpty()) {
From c41eebce76030dec2d9d5c58231f31fe2a4ef337 Mon Sep 17 00:00:00 2001
From: elasticsearchmachine
Date: Fri, 26 Sep 2025 08:03:32 +0000
Subject: [PATCH 40/89] [CI] Update transport version definitions
---
.../referable/resolve_index_include_resolved_flag.csv | 4 ----
server/src/main/resources/transport/upper_bounds/9.2.csv | 4 ----
2 files changed, 8 deletions(-)
diff --git a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
index 807d5c5b1548f..a4676dc2fe444 100644
--- a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
+++ b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
@@ -1,5 +1 @@
-<<<<<<< HEAD
9175000
-=======
-9172000
->>>>>>> 6f79ff0e655e73155108759d0c723a280d119d44
diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv
index 2232f2fe44f86..2a4fb1db74d97 100644
--- a/server/src/main/resources/transport/upper_bounds/9.2.csv
+++ b/server/src/main/resources/transport/upper_bounds/9.2.csv
@@ -1,5 +1 @@
-<<<<<<< HEAD
resolve_index_include_resolved_flag,9175000
-=======
-resolve_index_include_resolved_flag,9172000
->>>>>>> 6f79ff0e655e73155108759d0c723a280d119d44
From ae36d5594e59d3ca2a16ea196a744c3bc67c08f2 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 10:14:19 +0200
Subject: [PATCH 41/89] Transport will not go away
---
.../referable/resolve_index_include_resolved_flag.csv | 4 ----
server/src/main/resources/transport/upper_bounds/9.2.csv | 4 ----
2 files changed, 8 deletions(-)
diff --git a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
index 807d5c5b1548f..a4676dc2fe444 100644
--- a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
+++ b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
@@ -1,5 +1 @@
-<<<<<<< HEAD
9175000
-=======
-9172000
->>>>>>> 6f79ff0e655e73155108759d0c723a280d119d44
diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv
index 2232f2fe44f86..2a4fb1db74d97 100644
--- a/server/src/main/resources/transport/upper_bounds/9.2.csv
+++ b/server/src/main/resources/transport/upper_bounds/9.2.csv
@@ -1,5 +1 @@
-<<<<<<< HEAD
resolve_index_include_resolved_flag,9175000
-=======
-resolve_index_include_resolved_flag,9172000
->>>>>>> 6f79ff0e655e73155108759d0c723a280d119d44
From 0fe6504edc6101857c23ada90ae98e612b6ab344 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 10:17:02 +0200
Subject: [PATCH 42/89] Module info
---
server/src/main/java/module-info.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index 856633f0f8350..c5f69f4a5b701 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -55,8 +55,7 @@
requires org.apache.lucene.queryparser;
requires org.apache.lucene.sandbox;
requires org.apache.lucene.suggest;
- requires org.jruby.jcodings;
-
+
exports org.elasticsearch;
exports org.elasticsearch.action;
exports org.elasticsearch.action.admin.cluster.allocation;
From 9be4eba6f450ffa91f5691a46817a699b8ec1307 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 10:18:34 +0200
Subject: [PATCH 43/89] Still module info
---
server/src/main/java/module-info.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index c5f69f4a5b701..b94fa28157d13 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -55,7 +55,6 @@
requires org.apache.lucene.queryparser;
requires org.apache.lucene.sandbox;
requires org.apache.lucene.suggest;
-
exports org.elasticsearch;
exports org.elasticsearch.action;
exports org.elasticsearch.action.admin.cluster.allocation;
From dcd3169c6762fb782725115b0be649c506a7d6db Mon Sep 17 00:00:00 2001
From: elasticsearchmachine
Date: Fri, 26 Sep 2025 08:27:10 +0000
Subject: [PATCH 44/89] [CI] Auto commit changes from spotless
---
server/src/main/java/module-info.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index b94fa28157d13..68dc8da3b7d15 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -55,6 +55,7 @@
requires org.apache.lucene.queryparser;
requires org.apache.lucene.sandbox;
requires org.apache.lucene.suggest;
+
exports org.elasticsearch;
exports org.elasticsearch.action;
exports org.elasticsearch.action.admin.cluster.allocation;
From 0338477a80617a0d88087bbce2e8b866c84adc88 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 11:34:11 +0200
Subject: [PATCH 45/89] Fix
---
server/src/main/java/module-info.java | 1 +
.../indices/resolve/ResolveIndexAction.java | 41 +++++++++++++++----
.../NoMatchingProjectException.java | 7 ----
3 files changed, 33 insertions(+), 16 deletions(-)
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index b94fa28157d13..68dc8da3b7d15 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -55,6 +55,7 @@
requires org.apache.lucene.queryparser;
requires org.apache.lucene.sandbox;
requires org.apache.lucene.suggest;
+
exports org.elasticsearch;
exports org.elasticsearch.action;
exports org.elasticsearch.action.admin.cluster.allocation;
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index 2fb8fea6366fd..aecce6a17ba69 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -147,12 +147,9 @@ public Request(StreamInput in) throws IOException {
}
if (in.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
this.includeResolvedExpressions = in.readBoolean();
- } else {
- this.includeResolvedExpressions = false;
- }
- if (in.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
this.resolveCrossProject = in.readBoolean();
} else {
+ this.includeResolvedExpressions = false;
this.resolveCrossProject = false;
}
}
@@ -167,8 +164,6 @@ public void writeTo(StreamOutput out) throws IOException {
}
if (out.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
out.writeBoolean(includeResolvedExpressions);
- }
- if (out.getTransportVersion().supports(RESOLVE_INDEX_INCLUDE_RESOLVED_FLAG)) {
out.writeBoolean(resolveCrossProject);
}
}
@@ -519,10 +514,19 @@ public static class Response extends ActionResponse implements ToXContentObject
private final ResolvedIndexExpressions resolvedIndexExpressions;
public Response(List indices, List aliases, List dataStreams) {
+ this(indices, aliases, dataStreams, null);
+ }
+
+ public Response(
+ List indices,
+ List aliases,
+ List dataStreams,
+ ResolvedIndexExpressions resolvedIndexExpressions
+ ) {
this.indices = indices;
this.aliases = aliases;
this.dataStreams = dataStreams;
- this.resolvedIndexExpressions = null;
+ this.resolvedIndexExpressions = resolvedIndexExpressions;
}
public Response(StreamInput in) throws IOException {
@@ -575,6 +579,7 @@ public int hashCode() {
return Objects.hash(indices, aliases, dataStreams);
}
+ @Nullable
public ResolvedIndexExpressions getResolvedIndexExpressions() {
return resolvedIndexExpressions;
}
@@ -620,12 +625,16 @@ protected void doExecute(Task task, Request request, final ActionListener dataStreams = new ArrayList<>();
resolveIndices(localIndices, projectState, indexNameExpressionResolver, indices, aliases, dataStreams, request.indexModes);
+ final ResolvedIndexExpressions resolvedExpressions = request.getResolvedIndexExpressions();
if (remoteClusterIndices.size() > 0) {
final int remoteRequests = remoteClusterIndices.size();
final CountDown completionCounter = new CountDown(remoteRequests);
final SortedMap remoteResponses = Collections.synchronizedSortedMap(new TreeMap<>());
final Runnable terminalHandler = () -> {
if (completionCounter.countDown()) {
+ if (request.resolveCrossProject) {
+ // TODO error handling
+ }
mergeResults(remoteResponses, indices, aliases, dataStreams, request.indexModes);
listener.onResponse(new Response(indices, aliases, dataStreams));
}
@@ -640,14 +649,28 @@ protected void doExecute(Task task, Request request, final ActionListener {
remoteResponses.put(clusterAlias, response);
terminalHandler.run();
}, failure -> terminalHandler.run()));
}
} else {
- listener.onResponse(new Response(indices, aliases, dataStreams));
+ listener.onResponse(
+ new Response(
+ indices,
+ aliases,
+ dataStreams,
+ request.includeResolvedExpressions ? request.getResolvedIndexExpressions() : null
+ )
+ );
}
}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/NoMatchingProjectException.java b/server/src/main/java/org/elasticsearch/search/crossproject/NoMatchingProjectException.java
index 7b3932caf0f0f..c9be3a517c37c 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/NoMatchingProjectException.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/NoMatchingProjectException.java
@@ -7,13 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
package org.elasticsearch.search.crossproject;
import org.elasticsearch.ResourceNotFoundException;
From 6f91def6a446bcbd1e54ff4894fa33b8d5980c02 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 14:15:33 +0200
Subject: [PATCH 46/89] Add error handling
---
.../elasticsearch/ElasticsearchException.java | 1 +
.../indices/resolve/ResolveIndexAction.java | 28 ++-
.../metadata/IndexAbstractionResolver.java | 2 +-
.../CrossProjectIndexExpressionsRewriter.java | 4 +-
.../CrossProjectSearchErrorHandler.java | 215 ++++++++++++++++++
.../LinkedProjectExpressions.java | 47 ++++
.../crossproject/RemoteIndexExpressions.java | 40 ++++
.../search/crossproject/TargetProjects.java | 7 +-
8 files changed, 336 insertions(+), 8 deletions(-)
create mode 100644 server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
create mode 100644 server/src/main/java/org/elasticsearch/search/crossproject/LinkedProjectExpressions.java
create mode 100644 server/src/main/java/org/elasticsearch/search/crossproject/RemoteIndexExpressions.java
diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
index 79d1df6a09be4..9d2c2d58ba4ad 100644
--- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java
+++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
@@ -2023,6 +2023,7 @@ private enum ElasticsearchExceptionHandle {
TransportVersions.REMOTE_EXCEPTION,
TransportVersions.REMOTE_EXCEPTION_8_19
);
+ // TODO register NoMatchingProjectException
final Class extends ElasticsearchException> exceptionClass;
final CheckedFunction constructor;
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index aecce6a17ba69..d8f7ef827e60f 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -44,6 +44,9 @@
import org.elasticsearch.index.IndexMode;
import org.elasticsearch.injection.guice.Inject;
import org.elasticsearch.search.SearchService;
+import org.elasticsearch.search.crossproject.CrossProjectSearchErrorHandler;
+import org.elasticsearch.search.crossproject.LinkedProjectExpressions;
+import org.elasticsearch.search.crossproject.RemoteIndexExpressions;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.RemoteClusterAware;
import org.elasticsearch.transport.RemoteClusterService;
@@ -70,6 +73,7 @@
import java.util.stream.Stream;
import static org.elasticsearch.action.search.TransportSearchHelper.checkCCSVersionCompatibility;
+import static org.elasticsearch.search.crossproject.CrossProjectSearchErrorHandler.lenientIndicesOptionsForFanout;
public class ResolveIndexAction extends ActionType {
@@ -614,15 +618,16 @@ protected void doExecute(Task task, Request request, final ActionListener remoteClusterIndices = remoteClusterService.groupIndices(
- request.indicesOptions(),
+ request.resolveCrossProject ? lenientIndicesOptionsForFanout(originalIndicesOptions) : originalIndicesOptions,
request.indices()
);
final OriginalIndices localIndices = remoteClusterIndices.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
List indices = new ArrayList<>();
List aliases = new ArrayList<>();
List dataStreams = new ArrayList<>();
+ final ProjectState projectState = projectResolver.getProjectState(clusterService.state());
resolveIndices(localIndices, projectState, indexNameExpressionResolver, indices, aliases, dataStreams, request.indexModes);
final ResolvedIndexExpressions resolvedExpressions = request.getResolvedIndexExpressions();
@@ -633,7 +638,24 @@ protected void doExecute(Task task, Request request, final ActionListener {
if (completionCounter.countDown()) {
if (request.resolveCrossProject) {
- // TODO error handling
+ Map linkedProjectExpressions = remoteResponses.entrySet()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ Map.Entry::getKey,
+ e -> LinkedProjectExpressions.fromResolvedExpressions(e.getValue().getResolvedIndexExpressions())
+ )
+ );
+ try {
+ CrossProjectSearchErrorHandler.crossProjectFanoutErrorHandling(
+ request.indicesOptions,
+ resolvedExpressions,
+ new RemoteIndexExpressions(linkedProjectExpressions)
+ );
+ } catch (Exception ex) {
+ listener.onFailure(ex);
+ return;
+ }
}
mergeResults(remoteResponses, indices, aliases, dataStreams, request.indexModes);
listener.onResponse(new Response(indices, aliases, dataStreams));
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
index b8e7e5543b74b..68b63a1b2ee0c 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
@@ -82,7 +82,7 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder = ResolvedIndexExpressions.builder();
boolean wildcardSeen = false;
for (String index : indices) {
- CrossProjectIndexExpressionsRewriter.Result rewritten = CrossProjectIndexExpressionsRewriter.rewrite(
+ CrossProjectIndexExpressionsRewriter.Result rewritten = CrossProjectIndexExpressionsRewriter.rewriteIndexExpression(
index,
originProjectAlias,
linkedProjectAliases
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
index f472ab9060fb6..a9a8537ceeb2b 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
@@ -68,7 +68,7 @@ public static List rewriteIndexExpressions(
Set linkedProjectNames = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
List results = new ArrayList<>(indices.length);
for (String resource : indices) {
- results.add(rewrite(resource, originProject == null ? null : originProject.projectAlias(), linkedProjectNames));
+ results.add(rewriteIndexExpression(resource, originProject == null ? null : originProject.projectAlias(), linkedProjectNames));
}
return results;
}
@@ -82,7 +82,7 @@ public static List rewriteIndexExpressions(
* @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
* @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
*/
- public static Result rewrite(String resource, @Nullable String originProjectAlias, Set linkedProjectAliases) {
+ public static Result rewriteIndexExpression(String resource, @Nullable String originProjectAlias, Set linkedProjectAliases) {
maybeThrowOnUnsupportedResource(resource);
boolean isQualified = RemoteClusterAware.isRemoteIndexName(resource);
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
new file mode 100644
index 0000000000000..e3e759b6a2d4e
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.search.crossproject;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.action.ResolvedIndexExpression;
+import org.elasticsearch.action.ResolvedIndexExpressions;
+import org.elasticsearch.action.support.IndicesOptions;
+import org.elasticsearch.index.IndexNotFoundException;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.transport.RemoteClusterAware;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS;
+
+/**
+ * Utility class for handling errors in cross-project index operations.
+ *
+ * This class provides consistent error handling for scenarios where index resolution
+ * spans multiple projects, taking into account the provided {@link IndicesOptions}.
+ * It handles:
+ *
+ * - Validation of index existence in the merged project view based on IndicesOptions (ignoreUnavailable,
+ * allowNoIndices)
+ * - Authorization issues during cross-project index resolution
+ * - Both flat (unqualified) and qualified index expressions
+ * - Wildcard index patterns that may resolve differently across projects
+ *
+ *
+ * The utility examines both local and remote resolution results to determine the appropriate
+ * error response, throwing {@link IndexNotFoundException} for missing indices or
+ * {@link ElasticsearchSecurityException} for authorization failures.
+ */
+public class CrossProjectSearchErrorHandler {
+ private static final Logger logger = LogManager.getLogger(CrossProjectSearchErrorHandler.class);
+ private static final String WILDCARD = "*";
+
+ /**
+ * Validates the results of cross-project index resolution and throws appropriate exceptions based on the provided
+ * {@link IndicesOptions}.
+ *
+ * This method handles error scenarios when resolving indices across multiple projects:
+ *
+ * - If both {@code ignoreUnavailable} and {@code allowNoIndices} are true, the method returns without validation
+ * (lenient mode)
+ * - For wildcard patterns that resolve to no indices, validates against {@code allowNoIndices}
+ * - For concrete indices that don't exist, validates against {@code ignoreUnavailable}
+ * - For indices with authorization issues, throws security exceptions
+ *
+ *
+ * The method considers both flat (unqualified) and qualified index expressions, as well as
+ * local and linked project resolution results when determining whether to throw exceptions.
+ *
+ * @param indicesOptions Controls error behavior for missing indices
+ * @param localResolvedExpressions Resolution results from the origin project
+ * @param remoteResolvedExpressions Resolution results from linked projects
+ * @throws IndexNotFoundException If indices are missing and the {@code IndicesOptions} do not allow it
+ * @throws ElasticsearchSecurityException If authorization errors occurred during index resolution
+ */
+ public static void crossProjectFanoutErrorHandling(
+ IndicesOptions indicesOptions,
+ ResolvedIndexExpressions localResolvedExpressions,
+ RemoteIndexExpressions remoteResolvedExpressions
+ ) {
+ if (indicesOptions.allowNoIndices() && indicesOptions.ignoreUnavailable()) {
+ // nothing to do since we're in lenient mode
+ logger.debug("Skipping index existence check in lenient mode");
+ return;
+ }
+
+ logger.info(
+ "Checking index existence for [{}] and [{}] with indices options [{}]",
+ localResolvedExpressions,
+ remoteResolvedExpressions,
+ indicesOptions
+ );
+
+ for (ResolvedIndexExpression localResolvedIndices : localResolvedExpressions.expressions()) {
+ String originalExpression = localResolvedIndices.original();
+
+ logger.info("Checking replaced expression for original expression [{}]", originalExpression);
+
+ String resource = originalExpression;
+ boolean isQualifiedResource = RemoteClusterAware.isRemoteIndexName(resource);
+ if (isQualifiedResource) {
+ // handle qualified resource eg. P1:logs*
+ String[] splitResource = RemoteClusterAware.splitIndexName(resource);
+ assert splitResource.length == 2
+ : "Expected two strings (project and indexExpression) for a qualified resource ["
+ + resource
+ + "], but found ["
+ + splitResource.length
+ + "]";
+ resource = splitResource[1];
+ }
+ if (false == indicesOptions.allowNoIndices()) {
+ checkAllowNoIndices(
+ resource,
+ originalExpression,
+ localResolvedIndices,
+ remoteResolvedExpressions,
+ isQualifiedResource == false
+ );
+ } else if (false == indicesOptions.ignoreUnavailable()) {
+ checkIndicesOptions(originalExpression, localResolvedIndices, remoteResolvedExpressions, isQualifiedResource == false);
+ }
+ }
+ }
+
+ public static IndicesOptions lenientIndicesOptionsForFanout(IndicesOptions indicesOptions) {
+ return IndicesOptions.builder(indicesOptions)
+ .concreteTargetOptions(new IndicesOptions.ConcreteTargetOptions(true))
+ .wildcardOptions(IndicesOptions.WildcardOptions.builder(indicesOptions.wildcardOptions()).allowEmptyExpressions(true).build())
+ .build();
+ }
+
+ private static void checkAllowNoIndices(
+ String indexAlias,
+ String originalExpression,
+ ResolvedIndexExpression localResolvedIndices,
+ RemoteIndexExpressions remoteResolvedExpressions,
+ boolean isFlatWorldResource
+ ) {
+ // strict behaviour of allowNoIndices checks if a wildcard expression resolves to no concrete indices.
+ if (false == indexAlias.contains(WILDCARD)) {
+ return;
+ }
+ checkIndicesOptions(originalExpression, localResolvedIndices, remoteResolvedExpressions, isFlatWorldResource);
+ }
+
+ private static void checkIndicesOptions(
+ String originalExpression,
+ ResolvedIndexExpression localResolvedIndices,
+ RemoteIndexExpressions remoteResolvedExpressions,
+ boolean isFlatWorldResource
+ ) {
+ ResolvedIndexExpression.LocalExpressions localExpressions = localResolvedIndices.localExpressions();
+ boolean resourceFound = false == localExpressions.expressions().isEmpty()
+ && localExpressions.localIndexResolutionResult() == SUCCESS;
+
+ if (resourceFound && (isFlatWorldResource || localExpressions.expressions().size() == 1)) {
+ // a concrete index locally and either was a flat expression or was the only thing we needed to search
+ logger.info(
+ "Local cluster has canonical expression for original expression [{}], skipping remote existence check",
+ originalExpression
+ );
+ return;
+ }
+ List exceptions = new ArrayList<>();
+ ElasticsearchException localException = localExpressions.exception();
+ if (localException != null) {
+ exceptions.add(localException);
+ }
+ int numberOfQualifiedFound = resourceFound ? 1 : 0;
+ for (var linkedProjectExpressions : remoteResolvedExpressions.expressions().values()) {
+ // for each linked project we check if the resolved expressions contains the original expression and check for resolution status
+ ResolvedIndexExpression.LocalExpressions resolvedRemoteExpression = linkedProjectExpressions.resolvedExpressions()
+ .get(originalExpression);
+ assert resolvedRemoteExpression != null : "we should always have resolved expressions from remote";
+
+ Set remoteExpressions = resolvedRemoteExpression.expressions();
+ assert remoteExpressions != null : "we should always have replaced expressions";
+
+ logger.debug("Replaced indices from remote response resolved: [{}]", remoteExpressions);
+ boolean existsRemotely = false == remoteExpressions.isEmpty()
+ && resolvedRemoteExpression.localIndexResolutionResult() == SUCCESS;
+ if (existsRemotely) {
+ if (isFlatWorldResource) {
+ logger.debug(
+ "Remote project has resolved entries for [{}], skipping further remote existence check",
+ originalExpression
+ );
+ resourceFound = true;
+ break;
+ } else {
+ numberOfQualifiedFound++;
+ }
+ } else if (resolvedRemoteExpression.exception() != null) {
+ exceptions.add(resolvedRemoteExpression.exception());
+ }
+ }
+ boolean missingFlatResource = isFlatWorldResource && false == resourceFound;
+ boolean missingQualifiedResource = false == isFlatWorldResource
+ && numberOfQualifiedFound < localResolvedIndices.remoteExpressions().size();
+
+ if (missingFlatResource || missingQualifiedResource) {
+ if (false == exceptions.isEmpty()) {
+ // we only ever get exceptions if they are security related
+ // back and forth on whether a mix or security and non-security (missing indices) exceptions should report
+ // as 403 or 404
+ ElasticsearchSecurityException e = new ElasticsearchSecurityException(
+ "authorization errors while resolving [" + originalExpression + "]",
+ RestStatus.FORBIDDEN
+ );
+ exceptions.forEach(e::addSuppressed);
+ throw e;
+ } else {
+ throw new IndexNotFoundException(originalExpression);
+ }
+ }
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/LinkedProjectExpressions.java b/server/src/main/java/org/elasticsearch/search/crossproject/LinkedProjectExpressions.java
new file mode 100644
index 0000000000000..ac01f33db6c69
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/LinkedProjectExpressions.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.search.crossproject;
+
+import org.elasticsearch.action.ResolvedIndexExpression;
+import org.elasticsearch.action.ResolvedIndexExpressions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class allows capturing context about index expression replacements performed on a linked project.
+ *
+ * The replacements are keyed by the original index expression and have as value {@link ResolvedIndexExpression.LocalExpressions} that
+ * contains the set of expression (if any) was found on the remote, the result of the resolution and possibly the exception thrown.
+ *
+ *
An example structure is:
+ *
+ * {@code
+ * {
+ * "P*:my-index-*": {
+ * "expressions": ["my-index-000001", "my-index-000002"],
+ * "localIndexResolutionResult": "SUCCESS"
+ * }
+ * }
+ * }
+ *
+ * @param resolvedExpressions a map keyed by the original expression and having as value the remote resolution for that expression.
+ */
+public record LinkedProjectExpressions(Map resolvedExpressions) {
+ public static LinkedProjectExpressions fromResolvedExpressions(ResolvedIndexExpressions resolvedExpressions) {
+ Map map = new HashMap<>();
+ for (ResolvedIndexExpression e : resolvedExpressions.expressions()) {
+ if (map.put(e.original(), e.localExpressions()) != null) {
+ throw new IllegalStateException("duplicate key");
+ }
+ }
+ return new LinkedProjectExpressions(map);
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/RemoteIndexExpressions.java b/server/src/main/java/org/elasticsearch/search/crossproject/RemoteIndexExpressions.java
new file mode 100644
index 0000000000000..6b5908d2ab20b
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/RemoteIndexExpressions.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.search.crossproject;
+
+import java.util.Map;
+
+/**
+ * A set of {@link LinkedProjectExpressions}, keyed by the project alias.
+ *
+ * An example structure is:
+ *
+ * {@code
+ * {
+ * "P1": {
+ * "P1:my-index-*": { //example qualified
+ * "expressions": ["my-index-000001", "my-index-000002"],
+ * "localIndexResolutionResult": "SUCCESS"
+ * },
+ * "my-metrics-*": { //example flat
+ * "expressions": ["my-metrics-000001", "my-metrics-000002"],
+ * "localIndexResolutionResult": "SUCCESS"
+ * }
+ * },
+ * "P2": {
+ * "my-index-*": {
+ * "expressions": ["my-index-000001", "my-index-000002"],
+ * "localIndexResolutionResult": "SUCCESS"
+ * }
+ * }
+ * }
+ * }
+ */
+public record RemoteIndexExpressions(Map expressions) {}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
index 536dac2f2a17f..9ecdbcbb41eaf 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/TargetProjects.java
@@ -9,17 +9,20 @@
package org.elasticsearch.search.crossproject;
+import org.elasticsearch.core.Nullable;
+
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
-public record TargetProjects(ProjectRoutingInfo originProject, List linkedProjects) {
- public static TargetProjects NOT_CROSS_PROJECT = new TargetProjects(null, List.of());
+public record TargetProjects(@Nullable ProjectRoutingInfo originProject, List linkedProjects) {
+ public static final TargetProjects NOT_CROSS_PROJECT = new TargetProjects(null, List.of());
public TargetProjects(ProjectRoutingInfo originProject) {
this(originProject, List.of());
}
+ @Nullable
public String originProjectAlias() {
return originProject != null ? originProject.projectAlias() : null;
}
From ddc9e8570387aacd74785b08d1a611356fb6278c Mon Sep 17 00:00:00 2001
From: elasticsearchmachine
Date: Fri, 26 Sep 2025 12:23:24 +0000
Subject: [PATCH 47/89] [CI] Update transport version definitions
---
.../referable/resolve_index_include_resolved_flag.csv | 2 +-
server/src/main/resources/transport/upper_bounds/9.2.csv | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
index a4676dc2fe444..958f4e9c30d79 100644
--- a/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
+++ b/server/src/main/resources/transport/definitions/referable/resolve_index_include_resolved_flag.csv
@@ -1 +1 @@
-9175000
+9176000
diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv
index 2a4fb1db74d97..9064e6faf610d 100644
--- a/server/src/main/resources/transport/upper_bounds/9.2.csv
+++ b/server/src/main/resources/transport/upper_bounds/9.2.csv
@@ -1 +1 @@
-resolve_index_include_resolved_flag,9175000
+resolve_index_include_resolved_flag,9176000
From 6506e485d7e7f19bb9e5f4640636058b42dc6ad9 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 14:43:42 +0200
Subject: [PATCH 48/89] Tweak error handling
---
.../search/crossproject/CrossProjectSearchErrorHandler.java | 5 +++--
.../xpack/security/authz/IndicesAndAliasesResolver.java | 4 ++--
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
index e3e759b6a2d4e..c6592bfa44c3e 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
@@ -115,7 +115,7 @@ public static void crossProjectFanoutErrorHandling(
isQualifiedResource == false
);
} else if (false == indicesOptions.ignoreUnavailable()) {
- checkIndicesOptions(originalExpression, localResolvedIndices, remoteResolvedExpressions, isQualifiedResource == false);
+ checkIndicesOptions(resource, localResolvedIndices, remoteResolvedExpressions, isQualifiedResource == false);
}
}
}
@@ -169,7 +169,8 @@ private static void checkIndicesOptions(
// for each linked project we check if the resolved expressions contains the original expression and check for resolution status
ResolvedIndexExpression.LocalExpressions resolvedRemoteExpression = linkedProjectExpressions.resolvedExpressions()
.get(originalExpression);
- assert resolvedRemoteExpression != null : "we should always have resolved expressions from remote";
+ assert resolvedRemoteExpression != null
+ : "we should always have resolved expressions from remote. missing resolved expressions for [" + originalExpression + "]";
Set remoteExpressions = resolvedRemoteExpression.expressions();
assert remoteExpressions != null : "we should always have replaced expressions";
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 d86e097e648fe..7c1850895ef53 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
@@ -52,6 +52,7 @@
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiPredicate;
+import static org.elasticsearch.search.crossproject.CrossProjectSearchErrorHandler.lenientIndicesOptionsForFanout;
import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER;
class IndicesAndAliasesResolver {
@@ -372,8 +373,7 @@ ResolvedIndices resolveIndicesAndAliases(
final ResolvedIndexExpressions resolved = indexAbstractionResolver.resolveIndexAbstractions(
Arrays.asList(replaceable.indices()),
- // TODO make lenient
- indicesOptions,
+ lenientIndicesOptionsForFanout(indicesOptions),
projectMetadata,
authorizedIndices::all,
authorizedIndices::check,
From 55dc1c9bc5a060c5e7fdef2d6ae4701c9f6285df Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Fri, 26 Sep 2025 17:59:25 +0200
Subject: [PATCH 49/89] Fix up local index resolution
---
.../metadata/IndexAbstractionResolver.java | 53 ++++++--
.../CrossProjectIndexExpressionsRewriter.java | 4 +-
.../CrossProjectSearchErrorHandler.java | 120 +++++++++---------
3 files changed, 108 insertions(+), 69 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
index 68b63a1b2ee0c..06ba6270738c5 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
@@ -94,6 +94,7 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
wildcardSeen = resolveIndexAbstraction(
index,
+ rewritten.local(),
indicesOptions,
projectMetadata,
allAuthorizedAndAvailableBySelector,
@@ -118,14 +119,40 @@ private boolean resolveIndexAbstraction(
boolean wildcardSeen,
ResolvedIndexExpressions.Builder resolvedExpressionsBuilder,
HashSet remoteIndices
+ ) {
+ return resolveIndexAbstraction(
+ index,
+ index,
+ indicesOptions,
+ projectMetadata,
+ allAuthorizedAndAvailableBySelector,
+ isAuthorized,
+ includeDataStreams,
+ wildcardSeen,
+ resolvedExpressionsBuilder,
+ remoteIndices
+ );
+ }
+
+ private boolean resolveIndexAbstraction(
+ String originalExpression,
+ String localIndex,
+ IndicesOptions indicesOptions,
+ ProjectMetadata projectMetadata,
+ Function> allAuthorizedAndAvailableBySelector,
+ BiPredicate isAuthorized,
+ boolean includeDataStreams,
+ boolean wildcardSeen,
+ ResolvedIndexExpressions.Builder resolvedExpressionsBuilder,
+ HashSet remoteIndices
) {
String indexAbstraction;
boolean minus = false;
- if (index.charAt(0) == '-' && wildcardSeen) {
- indexAbstraction = index.substring(1);
+ if (localIndex.charAt(0) == '-' && wildcardSeen) {
+ indexAbstraction = localIndex.substring(1);
minus = true;
} else {
- indexAbstraction = index;
+ indexAbstraction = localIndex;
}
// Always check to see if there's a selector on the index expression
@@ -162,12 +189,12 @@ && isIndexVisible(
if (indicesOptions.allowNoIndices() == false) {
throw new IndexNotFoundException(indexAbstraction);
}
- resolvedExpressionsBuilder.addExpressions(index, new HashSet<>(), SUCCESS, remoteIndices);
+ resolvedExpressionsBuilder.addExpressions(originalExpression, new HashSet<>(), SUCCESS, remoteIndices);
} else {
if (minus) {
resolvedExpressionsBuilder.excludeFromLocalExpressions(resolvedIndices);
} else {
- resolvedExpressionsBuilder.addExpressions(index, resolvedIndices, SUCCESS, remoteIndices);
+ resolvedExpressionsBuilder.addExpressions(originalExpression, resolvedIndices, SUCCESS, remoteIndices);
}
}
} else {
@@ -189,14 +216,24 @@ && isIndexVisible(
includeDataStreams
);
final LocalIndexResolutionResult result = visible ? SUCCESS : CONCRETE_RESOURCE_NOT_VISIBLE;
- resolvedExpressionsBuilder.addExpressions(index, resolvedIndices, result, remoteIndices);
+ resolvedExpressionsBuilder.addExpressions(originalExpression, resolvedIndices, result, remoteIndices);
} else if (indicesOptions.ignoreUnavailable()) {
// ignoreUnavailable implies that the request should not fail if an index is not authorized
// so we map this expression to an empty list,
- resolvedExpressionsBuilder.addExpressions(index, new HashSet<>(), CONCRETE_RESOURCE_UNAUTHORIZED, remoteIndices);
+ resolvedExpressionsBuilder.addExpressions(
+ originalExpression,
+ new HashSet<>(),
+ CONCRETE_RESOURCE_UNAUTHORIZED,
+ remoteIndices
+ );
} else {
// store the calculated expansion as unauthorized, it will be rejected later
- resolvedExpressionsBuilder.addExpressions(index, resolvedIndices, CONCRETE_RESOURCE_UNAUTHORIZED, remoteIndices);
+ resolvedExpressionsBuilder.addExpressions(
+ originalExpression,
+ resolvedIndices,
+ CONCRETE_RESOURCE_UNAUTHORIZED,
+ remoteIndices
+ );
}
}
}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
index a9a8537ceeb2b..6fe2017e4fb7d 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
@@ -90,10 +90,10 @@ public static Result rewriteIndexExpression(String resource, @Nullable String or
Result result;
if (isQualified) {
result = rewriteQualified(resource, originProjectAlias, linkedProjectAliases);
- logger.debug("Rewrote qualified expression [{}] to [{}]", resource, result);
+ logger.info("Rewrote qualified expression [{}] to [{}]", resource, result);
} else {
result = rewriteUnqualified(resource, originProjectAlias, linkedProjectAliases);
- logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, result);
+ logger.info("Rewrote unqualified expression [{}] to [{}]", resource, result);
}
return result;
}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
index c6592bfa44c3e..ff16a458f19df 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
@@ -22,8 +22,10 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
+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;
/**
@@ -64,10 +66,10 @@ public class CrossProjectSearchErrorHandler {
* The method considers both flat (unqualified) and qualified index expressions, as well as
* local and linked project resolution results when determining whether to throw exceptions.
*
- * @param indicesOptions Controls error behavior for missing indices
- * @param localResolvedExpressions Resolution results from the origin project
+ * @param indicesOptions Controls error behavior for missing indices
+ * @param localResolvedExpressions Resolution results from the origin project
* @param remoteResolvedExpressions Resolution results from linked projects
- * @throws IndexNotFoundException If indices are missing and the {@code IndicesOptions} do not allow it
+ * @throws IndexNotFoundException If indices are missing and the {@code IndicesOptions} do not allow it
* @throws ElasticsearchSecurityException If authorization errors occurred during index resolution
*/
public static void crossProjectFanoutErrorHandling(
@@ -75,23 +77,22 @@ public static void crossProjectFanoutErrorHandling(
ResolvedIndexExpressions localResolvedExpressions,
RemoteIndexExpressions remoteResolvedExpressions
) {
+ logger.info(
+ "Checking cross-project index resolution results with indices options [{}] for [{}] and [{}]",
+ indicesOptions,
+ localResolvedExpressions,
+ remoteResolvedExpressions
+ );
+
if (indicesOptions.allowNoIndices() && indicesOptions.ignoreUnavailable()) {
- // nothing to do since we're in lenient mode
logger.debug("Skipping index existence check in lenient mode");
return;
}
- logger.info(
- "Checking index existence for [{}] and [{}] with indices options [{}]",
- localResolvedExpressions,
- remoteResolvedExpressions,
- indicesOptions
- );
-
for (ResolvedIndexExpression localResolvedIndices : localResolvedExpressions.expressions()) {
String originalExpression = localResolvedIndices.original();
- logger.info("Checking replaced expression for original expression [{}]", originalExpression);
+ logger.debug("Checking replaced expression for original expression [{}]", originalExpression);
String resource = originalExpression;
boolean isQualifiedResource = RemoteClusterAware.isRemoteIndexName(resource);
@@ -115,7 +116,7 @@ public static void crossProjectFanoutErrorHandling(
isQualifiedResource == false
);
} else if (false == indicesOptions.ignoreUnavailable()) {
- checkIndicesOptions(resource, localResolvedIndices, remoteResolvedExpressions, isQualifiedResource == false);
+ checkIndicesOptions(originalExpression, localResolvedIndices, remoteResolvedExpressions, isQualifiedResource == false);
}
}
}
@@ -149,10 +150,9 @@ private static void checkIndicesOptions(
) {
ResolvedIndexExpression.LocalExpressions localExpressions = localResolvedIndices.localExpressions();
boolean resourceFound = false == localExpressions.expressions().isEmpty()
- && localExpressions.localIndexResolutionResult() == SUCCESS;
+ && (localExpressions.localIndexResolutionResult() == SUCCESS || localExpressions.localIndexResolutionResult() == NONE);
if (resourceFound && (isFlatWorldResource || localExpressions.expressions().size() == 1)) {
- // a concrete index locally and either was a flat expression or was the only thing we needed to search
logger.info(
"Local cluster has canonical expression for original expression [{}], skipping remote existence check",
originalExpression
@@ -164,52 +164,54 @@ private static void checkIndicesOptions(
if (localException != null) {
exceptions.add(localException);
}
- int numberOfQualifiedFound = resourceFound ? 1 : 0;
- for (var linkedProjectExpressions : remoteResolvedExpressions.expressions().values()) {
- // for each linked project we check if the resolved expressions contains the original expression and check for resolution status
- ResolvedIndexExpression.LocalExpressions resolvedRemoteExpression = linkedProjectExpressions.resolvedExpressions()
- .get(originalExpression);
- assert resolvedRemoteExpression != null
- : "we should always have resolved expressions from remote. missing resolved expressions for [" + originalExpression + "]";
-
- Set remoteExpressions = resolvedRemoteExpression.expressions();
- assert remoteExpressions != null : "we should always have replaced expressions";
-
- logger.debug("Replaced indices from remote response resolved: [{}]", remoteExpressions);
- boolean existsRemotely = false == remoteExpressions.isEmpty()
- && resolvedRemoteExpression.localIndexResolutionResult() == SUCCESS;
- if (existsRemotely) {
- if (isFlatWorldResource) {
- logger.debug(
- "Remote project has resolved entries for [{}], skipping further remote existence check",
- originalExpression
+
+ for (String remoteExpression : localResolvedIndices.remoteExpressions()) {
+ boolean isQualifiedResource = RemoteClusterAware.isRemoteIndexName(remoteExpression);
+ if (isQualifiedResource) {
+ // handle qualified resource eg. P1:logs*
+ String[] splitResource = RemoteClusterAware.splitIndexName(remoteExpression);
+ assert splitResource.length == 2
+ : "Expected two strings (project and indexExpression) for a qualified resource ["
+ + remoteExpression
+ + "], but found ["
+ + splitResource.length
+ + "]";
+ String projectAlias = splitResource[0];
+ String resource = splitResource[1];
+ LinkedProjectExpressions linkedProjectExpressions = remoteResolvedExpressions.expressions().get(projectAlias);
+ assert linkedProjectExpressions != null : "we should always have linked expressions from remote";
+
+ ResolvedIndexExpression.LocalExpressions resolvedRemoteExpression = linkedProjectExpressions.resolvedExpressions()
+ .get(resource);
+ assert resolvedRemoteExpression != null : "we should always have resolved expressions from remote";
+
+ if (resolvedRemoteExpression.localIndexResolutionResult() == CONCRETE_RESOURCE_NOT_VISIBLE) {
+ throw new IndexNotFoundException(remoteExpression);
+ }
+ if (resolvedRemoteExpression.localIndexResolutionResult() == CONCRETE_RESOURCE_UNAUTHORIZED) {
+ // we only ever get exceptions if they are security related
+ // back and forth on whether a mix or security and non-security (missing indices) exceptions should report
+ // as 403 or 404
+ ElasticsearchSecurityException e = new ElasticsearchSecurityException(
+ "authorization errors while resolving [" + remoteExpression + "]",
+ RestStatus.FORBIDDEN
);
- resourceFound = true;
- break;
- } else {
- numberOfQualifiedFound++;
+ exceptions.forEach(e::addSuppressed);
+ throw e;
}
- } else if (resolvedRemoteExpression.exception() != null) {
- exceptions.add(resolvedRemoteExpression.exception());
- }
- }
- boolean missingFlatResource = isFlatWorldResource && false == resourceFound;
- boolean missingQualifiedResource = false == isFlatWorldResource
- && numberOfQualifiedFound < localResolvedIndices.remoteExpressions().size();
-
- if (missingFlatResource || missingQualifiedResource) {
- if (false == exceptions.isEmpty()) {
- // we only ever get exceptions if they are security related
- // back and forth on whether a mix or security and non-security (missing indices) exceptions should report
- // as 403 or 404
- ElasticsearchSecurityException e = new ElasticsearchSecurityException(
- "authorization errors while resolving [" + originalExpression + "]",
- RestStatus.FORBIDDEN
- );
- exceptions.forEach(e::addSuppressed);
- throw e;
} else {
- throw new IndexNotFoundException(originalExpression);
+ boolean foundFlat = false;
+ for (var linkedProjectExpressions : remoteResolvedExpressions.expressions().values()) {
+ ResolvedIndexExpression.LocalExpressions resolvedRemoteExpression = linkedProjectExpressions.resolvedExpressions()
+ .get(remoteExpression);
+ if (resolvedRemoteExpression != null) {
+ foundFlat = true;
+ break;
+ }
+ }
+ if (false == foundFlat) {
+ throw new IndexNotFoundException(remoteExpression);
+ }
}
}
}
From 4e656681566a5a1f1e93679fee75c3859d29e427 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Mon, 29 Sep 2025 14:09:43 +0200
Subject: [PATCH 50/89] Tweaks
---
.../src/main/java/org/elasticsearch/ElasticsearchException.java | 1 -
.../src/main/java/org/elasticsearch/action/IndicesRequest.java | 2 +-
.../action/admin/indices/resolve/ResolveIndexAction.java | 2 +-
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
index 1a9713e0dd5eb..98a0846ad9fd1 100644
--- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java
+++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
@@ -2031,7 +2031,6 @@ private enum ElasticsearchExceptionHandle {
185,
NO_MATCHING_PROJECT_EXCEPTION_VERSION
);
- // TODO register NoMatchingProjectException
final Class extends ElasticsearchException> exceptionClass;
final CheckedFunction constructor;
diff --git a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
index d43a36b71d35e..2562c13030e01 100644
--- a/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/IndicesRequest.java
@@ -81,7 +81,7 @@ default boolean allowsRemoteIndices() {
return false;
}
- default boolean allowCrossProjectResolution() {
+ default boolean allowsCrossProjectResolution() {
return false;
}
}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index be68f4bed6486..dacb4d2a9eeb9 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -207,7 +207,7 @@ public boolean allowsRemoteIndices() {
}
@Override
- public boolean allowCrossProjectResolution() {
+ public boolean allowsCrossProjectResolution() {
return resolveCrossProject;
}
From f75d46519698df87984bfd05b7e325a7a2ca83c9 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Mon, 29 Sep 2025 14:09:56 +0200
Subject: [PATCH 51/89] Also xpack
---
.../xpack/security/authz/AuthorizationService.java | 2 +-
.../xpack/security/authz/IndicesAndAliasesResolver.java | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index ab7ff385bb91d..ca1522a6ae6b5 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -555,7 +555,7 @@ private void authorizeAction(
authzInfo,
projectMetadata.getIndicesLookup(),
ActionListener.wrap(authorizedIndices -> {
- if (request instanceof IndicesRequest.Replaceable replaceable && replaceable.allowCrossProjectResolution()) {
+ if (request instanceof IndicesRequest.Replaceable replaceable && replaceable.allowsCrossProjectResolution()) {
crossProjectSearchAuthzService.loadAuthorizedProjects(ActionListener.wrap(authorizedProjects -> {
logger.info("Loaded authorized projects: [{}]", authorizedProjects);
resolvedIndicesListener.onResponse(
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 4c68ce88ca011..507e364179d1c 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
@@ -367,7 +367,7 @@ ResolvedIndices resolveIndicesAndAliases(
// if we cannot replace wildcards the indices list stays empty. Same if there are no authorized indices.
// we honour allow_no_indices like es core does.
} else {
- if (replaceable.allowCrossProjectResolution() && authorizedProjects != TargetProjects.NOT_CROSS_PROJECT) {
+ if (replaceable.allowsCrossProjectResolution() && authorizedProjects != TargetProjects.NOT_CROSS_PROJECT) {
assert replaceable.allowsRemoteIndices() : "cross-project requests must allow remote indices";
assert recordResolvedIndexExpressions : "cross-project requests must record resolved index expressions";
assert false == IndexNameExpressionResolver.isNoneExpression(replaceable.indices())
From a3bd85a507afd66b36afecc8a72d7c632b60e056 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Mon, 29 Sep 2025 16:36:30 +0200
Subject: [PATCH 52/89] WIP debug wildcard weirdness
---
.../indices/resolve/ResolveIndexAction.java | 1 +
.../metadata/IndexAbstractionResolver.java | 28 ++++++++++++-----
.../CrossProjectIndexExpressionsRewriter.java | 4 +--
.../CrossProjectSearchErrorHandler.java | 3 +-
.../security/authz/AuthorizationService.java | 31 ++++++++++++++++---
5 files changed, 51 insertions(+), 16 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index dacb4d2a9eeb9..cf176843cd1ee 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -631,6 +631,7 @@ protected void doExecute(Task task, Request request, final ActionListener 0) {
final int remoteRequests = remoteClusterIndices.size();
final CountDown completionCounter = new CountDown(remoteRequests);
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
index 282bca01d0e4a..172e14ef59cab 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
@@ -9,6 +9,8 @@
package org.elasticsearch.cluster.metadata;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult;
import org.elasticsearch.action.ResolvedIndexExpressions;
import org.elasticsearch.action.support.IndexComponentSelector;
@@ -36,6 +38,8 @@
public class IndexAbstractionResolver {
+ private static final Logger logger = LogManager.getLogger(IndexAbstractionResolver.class);
+
private final IndexNameExpressionResolver indexNameExpressionResolver;
public IndexAbstractionResolver(IndexNameExpressionResolver indexNameExpressionResolver) {
@@ -89,6 +93,14 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
originProjectAlias,
linkedProjectAliases
);
+ logger.info(
+ "[{}] rewritten index expression [{}] to local [{}] and remote [{}]",
+ originProjectAlias,
+ index,
+ rewrittenIndexExpression.localExpression(),
+ rewrittenIndexExpression.remoteExpressions()
+ );
+
if (rewrittenIndexExpression.localExpression() == null) {
resolvedExpressionsBuilder.addRemoteExpressions(index, new HashSet<>(rewrittenIndexExpression.remoteExpressions()));
continue;
@@ -112,14 +124,14 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
}
private boolean resolveIndexAbstraction(
- ResolvedIndexExpressions.Builder resolvedExpressionsBuilder,
- String originalIndexExpression,
- String localIndexExpression,
- IndicesOptions indicesOptions,
- ProjectMetadata projectMetadata,
- Function> allAuthorizedAndAvailableBySelector,
- BiPredicate isAuthorized,
- boolean includeDataStreams,
+ final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder,
+ final String originalIndexExpression,
+ final String localIndexExpression,
+ final IndicesOptions indicesOptions,
+ final ProjectMetadata projectMetadata,
+ final Function> allAuthorizedAndAvailableBySelector,
+ final BiPredicate isAuthorized,
+ final boolean includeDataStreams,
boolean wildcardSeen,
HashSet remoteExpressions
) {
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
index b27711463ceda..c648a47209467 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
@@ -105,10 +105,10 @@ public static LocalWithRemoteExpressions rewriteIndexExpression(
final LocalWithRemoteExpressions rewrittenExpression;
if (isQualified) {
rewrittenExpression = rewriteQualifiedExpression(indexExpression, originProjectAlias, allProjectAliases);
- logger.debug("Rewrote qualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
+ logger.info("Rewrote qualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
} else {
rewrittenExpression = rewriteUnqualifiedExpression(indexExpression, originProjectAlias, allProjectAliases);
- logger.debug("Rewrote unqualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
+ logger.info("Rewrote unqualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
}
return rewrittenExpression;
}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
index ff16a458f19df..d28b3e858d2a8 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
@@ -78,8 +78,7 @@ public static void crossProjectFanoutErrorHandling(
RemoteIndexExpressions remoteResolvedExpressions
) {
logger.info(
- "Checking cross-project index resolution results with indices options [{}] for [{}] and [{}]",
- indicesOptions,
+ "Checking cross-project index resolution results for [{}] and [{}]",
localResolvedExpressions,
remoteResolvedExpressions
);
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index ca1522a6ae6b5..893f220ed659a 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -49,6 +49,7 @@
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.indices.InvalidIndexNameException;
import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.search.crossproject.NoMatchingProjectException;
import org.elasticsearch.search.crossproject.TargetProjects;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.LinkedProjectConfigService;
@@ -568,8 +569,28 @@ private void authorizeAction(
)
);
}, e -> {
- logger.error("Failed to load authorized projects", e);
- resolvedIndicesListener.onFailure(e);
+ // TODO avoid duplicating this
+ if (e instanceof InvalidIndexNameException
+ || e instanceof InvalidSelectorException
+ || e instanceof UnsupportedSelectorException) {
+ logger.info(
+ () -> Strings.format(
+ "failed [%s] action authorization for [%s] due to [%s] exception",
+ action,
+ authentication,
+ e.getClass().getSimpleName()
+ ),
+ e
+ );
+ listener.onFailure(e);
+ return;
+ }
+ auditTrail.accessDenied(requestId, authentication, action, request, authzInfo);
+ if (e instanceof IndexNotFoundException || e instanceof NoMatchingProjectException) {
+ listener.onFailure(e);
+ } else {
+ listener.onFailure(actionDenied(authentication, authzInfo, action, request, e));
+ }
}));
} else {
resolvedIndicesListener.onResponse(
@@ -599,7 +620,7 @@ private void authorizeAction(
return;
}
auditTrail.accessDenied(requestId, authentication, action, request, authzInfo);
- if (e instanceof IndexNotFoundException) {
+ if (e instanceof IndexNotFoundException || e instanceof NoMatchingProjectException) {
listener.onFailure(e);
} else {
listener.onFailure(actionDenied(authentication, authzInfo, action, request, e));
@@ -631,7 +652,9 @@ private void authorizeAction(
threadContext
)
);
- } else {
+ } else
+
+ {
logger.warn("denying access for [{}] as action [{}] is not an index or cluster action", authentication, action);
auditTrail.accessDenied(requestId, authentication, action, request, authzInfo);
listener.onFailure(actionDenied(authentication, authzInfo, action, request));
From 426832c30cdba308dbfffba0d6b28cba96b8865b Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Mon, 29 Sep 2025 17:59:06 +0200
Subject: [PATCH 53/89] Dont overlog
---
.../indices/resolve/ResolveIndexAction.java | 28 +++++++++++++------
.../security/authz/AuthorizationService.java | 4 +--
.../authz/IndicesAndAliasesResolver.java | 6 ++++
3 files changed, 26 insertions(+), 12 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index cf176843cd1ee..0f6bdf4028c0f 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -9,6 +9,8 @@
package org.elasticsearch.action.admin.indices.resolve;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequestValidationException;
@@ -77,6 +79,8 @@
public class ResolveIndexAction extends ActionType {
+ private static final Logger LOGGER = LogManager.getLogger(ResolveIndexAction.class);
+
public static final ResolveIndexAction INSTANCE = new ResolveIndexAction();
public static final String NAME = "indices:admin/resolve/index";
public static final RemoteClusterActionType REMOTE_TYPE = new RemoteClusterActionType<>(NAME, Response::new);
@@ -213,7 +217,10 @@ public boolean allowsCrossProjectResolution() {
@Override
public void setResolvedIndexExpressions(ResolvedIndexExpressions expressions) {
- this.resolvedIndexExpressions = expressions;
+ // TODO this is obviously not right
+ if (resolvedIndexExpressions == null) {
+ this.resolvedIndexExpressions = expressions;
+ }
}
@Override
@@ -631,7 +638,7 @@ protected void doExecute(Task task, Request request, final ActionListener 0) {
final int remoteRequests = remoteClusterIndices.size();
final CountDown completionCounter = new CountDown(remoteRequests);
@@ -672,6 +679,11 @@ protected void doExecute(Task task, Request request, final ActionListener {
remoteResponses.put(clusterAlias, response);
terminalHandler.run();
- }, failure -> terminalHandler.run()));
+ }, failure -> {
+ LOGGER.error("failed to resolve indices on remote cluster [{}]", clusterAlias, failure);
+ terminalHandler.run();
+ }));
}
} else {
listener.onResponse(
- new Response(
- indices,
- aliases,
- dataStreams,
- request.includeResolvedExpressions ? request.getResolvedIndexExpressions() : null
- )
+ new Response(indices, aliases, dataStreams, request.includeResolvedExpressions ? resolvedExpressions : null)
);
}
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
index 893f220ed659a..1ff97d002a66a 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java
@@ -652,9 +652,7 @@ private void authorizeAction(
threadContext
)
);
- } else
-
- {
+ } else {
logger.warn("denying access for [{}] as action [{}] is not an index or cluster action", authentication, action);
auditTrail.accessDenied(requestId, authentication, action, request, authzInfo);
listener.onFailure(actionDenied(authentication, authzInfo, action, request));
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 507e364179d1c..10cb63a089385 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
@@ -6,6 +6,8 @@
*/
package org.elasticsearch.xpack.security.authz;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.AliasesRequest;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.ResolvedIndexExpressions;
@@ -57,6 +59,8 @@
class IndicesAndAliasesResolver {
+ private static final Logger logger = LogManager.getLogger(IndicesAndAliasesResolver.class);
+
private final IndexNameExpressionResolver nameExpressionResolver;
private final IndexAbstractionResolver indexAbstractionResolver;
private final RemoteClusterResolver remoteClusterResolver;
@@ -400,10 +404,12 @@ ResolvedIndices resolveIndicesAndAliases(
authorizedIndices::check,
indicesRequest.includeDataStreams()
);
+ logger.debug("resolved indices for request [{}]: [{}]", action, resolved);
// only store resolved expressions if configured, to avoid unnecessary memory usage
// once we've migrated from `indices()` to using resolved expressions holistically,
// we will always store them
if (recordResolvedIndexExpressions) {
+ logger.debug("setting resolved expressions [{}]: [{}]", action, resolved);
replaceable.setResolvedIndexExpressions(resolved);
}
resolvedIndicesBuilder.addLocal(resolved.getLocalIndicesList());
From 6b8fc99caf372af0e4c33b32dfe0190dc0f24a12 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Mon, 29 Sep 2025 23:26:07 +0200
Subject: [PATCH 54/89] Set once
---
.../indices/resolve/ResolveIndexAction.java | 17 +++++++++++++----
.../authz/IndicesAndAliasesResolver.java | 19 +++++++++++++++----
2 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index 0f6bdf4028c0f..ddbf9700f66c0 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -217,10 +217,7 @@ public boolean allowsCrossProjectResolution() {
@Override
public void setResolvedIndexExpressions(ResolvedIndexExpressions expressions) {
- // TODO this is obviously not right
- if (resolvedIndexExpressions == null) {
- this.resolvedIndexExpressions = expressions;
- }
+ this.resolvedIndexExpressions = expressions;
}
@Override
@@ -701,6 +698,18 @@ protected void doExecute(Task task, Request request, final ActionListener
Date: Mon, 29 Sep 2025 23:39:40 +0200
Subject: [PATCH 55/89] Nit
---
.../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 ecece14e7912e..7a9904b3055c6 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
@@ -485,7 +485,7 @@ private static void setResolvedIfNull(String action, IndicesRequest.Replaceable
replaceable.setResolvedIndexExpressions(resolved);
} else {
logger.info(
- "resolved index expressions {} already set on request [{}], not overwriting with {}",
+ "resolved index expressions [{}] already set on request [{}], not overwriting with [{}]",
replaceable.getResolvedIndexExpressions(),
action,
resolved
From 0390eca689a23f3fd62896e28b40984b42efd17d Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Tue, 30 Sep 2025 12:04:22 +0200
Subject: [PATCH 56/89] Tweaks and fixes
---
.../elasticsearch/ElasticsearchException.java | 2 +-
.../action/ResolvedIndexExpression.java | 2 +
.../action/ResolvedIndexExpressions.java | 18 +-
.../metadata/IndexAbstractionResolver.java | 19 +-
.../CrossProjectSearchErrorHandler.java | 29 ++-
...ter.java => IndexExpressionsRewriter.java} | 40 +--
.../LocalWithRemoteExpressions.java | 34 ---
...ava => IndexExpressionsRewriterTests.java} | 230 ++++++++----------
8 files changed, 164 insertions(+), 210 deletions(-)
rename server/src/main/java/org/elasticsearch/search/crossproject/{CrossProjectIndexExpressionsRewriter.java => IndexExpressionsRewriter.java} (86%)
delete mode 100644 server/src/main/java/org/elasticsearch/search/crossproject/LocalWithRemoteExpressions.java
rename server/src/test/java/org/elasticsearch/search/crossproject/{CrossProjectIndexExpressionsRewriterTests.java => IndexExpressionsRewriterTests.java} (59%)
diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
index 98a0846ad9fd1..a46eac0c31a74 100644
--- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java
+++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
@@ -80,7 +80,7 @@
import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_UUID_NA_VALUE;
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName;
-import static org.elasticsearch.search.crossproject.CrossProjectIndexExpressionsRewriter.NO_MATCHING_PROJECT_EXCEPTION_VERSION;
+import static org.elasticsearch.search.crossproject.IndexExpressionsRewriter.NO_MATCHING_PROJECT_EXCEPTION_VERSION;
/**
* A base class for all elasticsearch exceptions.
diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
index e0624d9370e46..d2afda08954cb 100644
--- a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
+++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpression.java
@@ -85,6 +85,8 @@ public record LocalExpressions(
: "If the local resolution result is SUCCESS, exception must be null";
}
+ public static final LocalExpressions NONE = new LocalExpressions(Set.of(), LocalIndexResolutionResult.NONE, null);
+
public LocalExpressions(StreamInput in) throws IOException {
this(
in.readCollectionAsSet(StreamInput::readString),
diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
index c149c0d0649b1..2715d6705653a 100644
--- a/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
+++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndexExpressions.java
@@ -60,7 +60,7 @@ public void addExpressions(
String original,
HashSet localExpressions,
ResolvedIndexExpression.LocalIndexResolutionResult resolutionResult,
- HashSet remoteExpressions
+ Set remoteExpressions
) {
Objects.requireNonNull(original);
Objects.requireNonNull(localExpressions);
@@ -71,16 +71,10 @@ public void addExpressions(
);
}
- public void addRemoteExpressions(String original, HashSet remoteExpressions) {
+ public void addRemoteExpressions(String original, Set remoteExpressions) {
Objects.requireNonNull(original);
Objects.requireNonNull(remoteExpressions);
- expressions.add(
- new ResolvedIndexExpression(
- original,
- new LocalExpressions(new HashSet<>(), ResolvedIndexExpression.LocalIndexResolutionResult.NONE, null),
- remoteExpressions
- )
- );
+ expressions.add(new ResolvedIndexExpression(original, LocalExpressions.NONE, remoteExpressions));
}
/**
@@ -90,7 +84,11 @@ public void excludeFromLocalExpressions(Set expressionsToExclude) {
Objects.requireNonNull(expressionsToExclude);
if (expressionsToExclude.isEmpty() == false) {
for (ResolvedIndexExpression prior : expressions) {
- prior.localExpressions().expressions().removeAll(expressionsToExclude);
+ final Set localExpressions = prior.localExpressions().expressions();
+ if (localExpressions.isEmpty()) {
+ continue;
+ }
+ localExpressions.removeAll(expressionsToExclude);
}
}
}
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
index 172e14ef59cab..f949581a709f1 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java
@@ -22,8 +22,7 @@
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel;
-import org.elasticsearch.search.crossproject.CrossProjectIndexExpressionsRewriter;
-import org.elasticsearch.search.crossproject.LocalWithRemoteExpressions;
+import org.elasticsearch.search.crossproject.IndexExpressionsRewriter;
import org.elasticsearch.search.crossproject.TargetProjects;
import java.util.HashSet;
@@ -88,7 +87,7 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
final ResolvedIndexExpressions.Builder resolvedExpressionsBuilder = ResolvedIndexExpressions.builder();
boolean wildcardSeen = false;
for (String index : indices) {
- final LocalWithRemoteExpressions rewrittenIndexExpression = CrossProjectIndexExpressionsRewriter.rewriteIndexExpression(
+ final IndexExpressionsRewriter.IndexRewriteResult indexRewriteResult = IndexExpressionsRewriter.rewriteIndexExpression(
index,
originProjectAlias,
linkedProjectAliases
@@ -97,26 +96,26 @@ public ResolvedIndexExpressions resolveIndexAbstractions(
"[{}] rewritten index expression [{}] to local [{}] and remote [{}]",
originProjectAlias,
index,
- rewrittenIndexExpression.localExpression(),
- rewrittenIndexExpression.remoteExpressions()
+ indexRewriteResult.localExpression(),
+ indexRewriteResult.remoteExpressions()
);
- if (rewrittenIndexExpression.localExpression() == null) {
- resolvedExpressionsBuilder.addRemoteExpressions(index, new HashSet<>(rewrittenIndexExpression.remoteExpressions()));
+ if (indexRewriteResult.localExpression() == null) {
+ resolvedExpressionsBuilder.addRemoteExpressions(index, Set.copyOf(indexRewriteResult.remoteExpressions()));
continue;
}
wildcardSeen = resolveIndexAbstraction(
resolvedExpressionsBuilder,
index,
- rewrittenIndexExpression.localExpression(),
+ indexRewriteResult.localExpression(),
indicesOptions,
projectMetadata,
allAuthorizedAndAvailableBySelector,
isAuthorized,
includeDataStreams,
wildcardSeen,
- new HashSet<>(rewrittenIndexExpression.remoteExpressions())
+ Set.copyOf(indexRewriteResult.remoteExpressions())
);
}
@@ -133,7 +132,7 @@ private boolean resolveIndexAbstraction(
final BiPredicate isAuthorized,
final boolean includeDataStreams,
boolean wildcardSeen,
- HashSet remoteExpressions
+ Set remoteExpressions
) {
String indexAbstraction;
boolean minus = false;
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
index d28b3e858d2a8..5103bfa554180 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectSearchErrorHandler.java
@@ -164,6 +164,23 @@ private static void checkIndicesOptions(
exceptions.add(localException);
}
+ if (localResolvedIndices.remoteExpressions().isEmpty()) {
+ if (localExpressions.localIndexResolutionResult() == CONCRETE_RESOURCE_NOT_VISIBLE) {
+ throw new IndexNotFoundException(originalExpression);
+ }
+ if (localExpressions.localIndexResolutionResult() == CONCRETE_RESOURCE_UNAUTHORIZED) {
+ // we only ever get exceptions if they are security related
+ // back and forth on whether a mix or security and non-security (missing indices) exceptions should report
+ // as 403 or 404
+ ElasticsearchSecurityException e = new ElasticsearchSecurityException(
+ "authorization errors while resolving [" + originalExpression + "]",
+ RestStatus.FORBIDDEN
+ );
+ exceptions.forEach(e::addSuppressed);
+ throw e;
+ }
+ }
+
for (String remoteExpression : localResolvedIndices.remoteExpressions()) {
boolean isQualifiedResource = RemoteClusterAware.isRemoteIndexName(remoteExpression);
if (isQualifiedResource) {
@@ -184,8 +201,12 @@ private static void checkIndicesOptions(
.get(resource);
assert resolvedRemoteExpression != null : "we should always have resolved expressions from remote";
+ // TODO in wildcard case?
+ if (resolvedRemoteExpression.expressions().isEmpty()) {
+ throw new IndexNotFoundException(originalExpression);
+ }
if (resolvedRemoteExpression.localIndexResolutionResult() == CONCRETE_RESOURCE_NOT_VISIBLE) {
- throw new IndexNotFoundException(remoteExpression);
+ throw new IndexNotFoundException(originalExpression);
}
if (resolvedRemoteExpression.localIndexResolutionResult() == CONCRETE_RESOURCE_UNAUTHORIZED) {
// we only ever get exceptions if they are security related
@@ -203,13 +224,15 @@ private static void checkIndicesOptions(
for (var linkedProjectExpressions : remoteResolvedExpressions.expressions().values()) {
ResolvedIndexExpression.LocalExpressions resolvedRemoteExpression = linkedProjectExpressions.resolvedExpressions()
.get(remoteExpression);
- if (resolvedRemoteExpression != null) {
+ if (resolvedRemoteExpression != null
+ && resolvedRemoteExpression.expressions().isEmpty() == false
+ && resolvedRemoteExpression.localIndexResolutionResult() == SUCCESS) {
foundFlat = true;
break;
}
}
if (false == foundFlat) {
- throw new IndexNotFoundException(remoteExpression);
+ throw new IndexNotFoundException(originalExpression);
}
}
}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java b/server/src/main/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriter.java
similarity index 86%
rename from server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
rename to server/src/main/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriter.java
index c648a47209467..9d21cb01d0255 100644
--- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java
+++ b/server/src/main/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriter.java
@@ -31,10 +31,10 @@
* Utility class for rewriting cross-project index expressions.
* Provides methods that can rewrite qualified and unqualified index expressions to canonical CCS.
*/
-public class CrossProjectIndexExpressionsRewriter {
+public class IndexExpressionsRewriter {
public static TransportVersion NO_MATCHING_PROJECT_EXCEPTION_VERSION = TransportVersion.fromName("no_matching_project_exception");
- private static final Logger logger = LogManager.getLogger(CrossProjectIndexExpressionsRewriter.class);
+ private static final Logger logger = LogManager.getLogger(IndexExpressionsRewriter.class);
private static final String ORIGIN_PROJECT_KEY = "_origin";
private static final String[] MATCH_ALL = new String[] { Metadata.ALL };
private static final String EXCLUSION = "-";
@@ -51,7 +51,7 @@ public class CrossProjectIndexExpressionsRewriter {
* @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
* @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
*/
- public static Map> rewriteIndexExpressions(
+ public static Map rewriteIndexExpressions(
ProjectRoutingInfo originProject,
List linkedProjects,
final String[] originalIndices
@@ -67,15 +67,12 @@ public static Map> rewriteIndexExpressions(
final Set allProjectAliases = getAllProjectAliases(originProject, linkedProjects);
final String originProjectAlias = originProject != null ? originProject.projectAlias() : null;
- final Map> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
+ final Map canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
for (String indexExpression : indices) {
if (canonicalExpressionsMap.containsKey(indexExpression)) {
continue;
}
- canonicalExpressionsMap.put(
- indexExpression,
- rewriteIndexExpression(indexExpression, originProjectAlias, allProjectAliases).all()
- );
+ canonicalExpressionsMap.put(indexExpression, rewriteIndexExpression(indexExpression, originProjectAlias, allProjectAliases));
}
return canonicalExpressionsMap;
}
@@ -91,7 +88,7 @@ public static Map> rewriteIndexExpressions(
* @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
* @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
*/
- public static LocalWithRemoteExpressions rewriteIndexExpression(
+ public static IndexRewriteResult rewriteIndexExpression(
String indexExpression,
@Nullable String originProjectAlias,
Set allProjectAliases
@@ -102,13 +99,13 @@ public static LocalWithRemoteExpressions rewriteIndexExpression(
maybeThrowOnUnsupportedResource(indexExpression);
final boolean isQualified = RemoteClusterAware.isRemoteIndexName(indexExpression);
- final LocalWithRemoteExpressions rewrittenExpression;
+ final IndexRewriteResult rewrittenExpression;
if (isQualified) {
rewrittenExpression = rewriteQualifiedExpression(indexExpression, originProjectAlias, allProjectAliases);
- logger.info("Rewrote qualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
+ logger.debug("Rewrote qualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
} else {
rewrittenExpression = rewriteUnqualifiedExpression(indexExpression, originProjectAlias, allProjectAliases);
- logger.info("Rewrote unqualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
+ logger.debug("Rewrote unqualified expression [{}] to [{}]", indexExpression, rewrittenExpression);
}
return rewrittenExpression;
}
@@ -124,7 +121,7 @@ private static Set getAllProjectAliases(@Nullable ProjectRoutingInfo ori
return Collections.unmodifiableSet(allProjectAliases);
}
- private static LocalWithRemoteExpressions rewriteUnqualifiedExpression(
+ private static IndexRewriteResult rewriteUnqualifiedExpression(
String indexExpression,
@Nullable String originAlias,
Set allProjectAliases
@@ -139,10 +136,10 @@ private static LocalWithRemoteExpressions rewriteUnqualifiedExpression(
rewrittenExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProjectAlias, indexExpression));
}
}
- return new LocalWithRemoteExpressions(localExpression, rewrittenExpressions);
+ return new IndexRewriteResult(localExpression, rewrittenExpressions);
}
- private static LocalWithRemoteExpressions rewriteQualifiedExpression(
+ private static IndexRewriteResult rewriteQualifiedExpression(
String resource,
@Nullable String originProjectAlias,
Set allProjectAliases
@@ -161,7 +158,7 @@ private static LocalWithRemoteExpressions rewriteQualifiedExpression(
if (originProjectAlias != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
// handling case where we have a qualified expression like: _origin:indexName
- return new LocalWithRemoteExpressions(indexExpression);
+ return new IndexRewriteResult(indexExpression);
}
if (originProjectAlias == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
@@ -189,7 +186,7 @@ private static LocalWithRemoteExpressions rewriteQualifiedExpression(
}
}
- return new LocalWithRemoteExpressions(localExpression, resourcesMatchingLinkedProjectAliases);
+ return new IndexRewriteResult(localExpression, resourcesMatchingLinkedProjectAliases);
} catch (NoSuchRemoteClusterException ex) {
logger.debug(ex.getMessage(), ex);
throw new NoMatchingProjectException(requestedProjectAlias);
@@ -208,4 +205,13 @@ private static void maybeThrowOnUnsupportedResource(String resource) {
throw new IllegalArgumentException("Selectors are not currently supported but was found in the expression [" + resource + "]");
}
}
+
+ /**
+ * A container for a local expression and a list of remote expressions.
+ */
+ public record IndexRewriteResult(@Nullable String localExpression, List remoteExpressions) {
+ public IndexRewriteResult(String localExpression) {
+ this(localExpression, List.of());
+ }
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/LocalWithRemoteExpressions.java b/server/src/main/java/org/elasticsearch/search/crossproject/LocalWithRemoteExpressions.java
deleted file mode 100644
index faa77fa9aee02..0000000000000
--- a/server/src/main/java/org/elasticsearch/search/crossproject/LocalWithRemoteExpressions.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-package org.elasticsearch.search.crossproject;
-
-import org.elasticsearch.core.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A container for a local expression and a list of remote expressions.
- */
-public record LocalWithRemoteExpressions(@Nullable String localExpression, List remoteExpressions) {
- public LocalWithRemoteExpressions(String localExpression) {
- this(localExpression, List.of());
- }
-
- List all() {
- if (localExpression == null) {
- return remoteExpressions;
- }
- List all = new ArrayList<>();
- all.add(localExpression);
- all.addAll(remoteExpressions);
- return List.copyOf(all);
- }
-}
diff --git a/server/src/test/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriterTests.java b/server/src/test/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriterTests.java
similarity index 59%
rename from server/src/test/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriterTests.java
rename to server/src/test/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriterTests.java
index 95285c06d1a4f..41f0d02f66af5 100644
--- a/server/src/test/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriterTests.java
+++ b/server/src/test/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriterTests.java
@@ -12,13 +12,15 @@
import org.elasticsearch.ResourceNotFoundException;
import org.elasticsearch.cluster.metadata.ProjectId;
import org.elasticsearch.test.ESTestCase;
+import org.hamcrest.Matcher;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.containsInAnyOrder;
-public class CrossProjectIndexExpressionsRewriterTests extends ESTestCase {
+public class IndexExpressionsRewriterTests extends ESTestCase {
public void testFlatOnlyRewrite() {
ProjectRoutingInfo origin = createRandomProjectWithAlias("P0");
@@ -29,15 +31,14 @@ public void testFlatOnlyRewrite() {
);
String[] requestedResources = new String[] { "logs*", "metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("logs*", "metrics*"));
- assertThat(canonical.get("logs*"), containsInAnyOrder("logs*", "P1:logs*", "P2:logs*", "P3:logs*"));
- assertThat(canonical.get("metrics*"), containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("logs*", "metrics*"));
+ assertIndexRewriteResultsContains(actual.get("logs*"), containsInAnyOrder("logs*", "P1:logs*", "P2:logs*", "P3:logs*"));
+ assertIndexRewriteResultsContains(
+ actual.get("metrics*"),
+ containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*")
+ );
}
public void testFlatAndQualifiedRewrite() {
@@ -49,15 +50,14 @@ public void testFlatAndQualifiedRewrite() {
);
String[] requestedResources = new String[] { "P1:logs*", "metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("P1:logs*", "metrics*"));
- assertThat(canonical.get("P1:logs*"), containsInAnyOrder("P1:logs*"));
- assertThat(canonical.get("metrics*"), containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("P1:logs*", "metrics*"));
+ assertIndexRewriteResultsContains(actual.get("P1:logs*"), containsInAnyOrder("P1:logs*"));
+ assertIndexRewriteResultsContains(
+ actual.get("metrics*"),
+ containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*")
+ );
}
public void testQualifiedOnlyRewrite() {
@@ -69,15 +69,11 @@ public void testQualifiedOnlyRewrite() {
);
String[] requestedResources = new String[] { "P1:logs*", "P2:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("P1:logs*", "P2:metrics*"));
- assertThat(canonical.get("P1:logs*"), containsInAnyOrder("P1:logs*"));
- assertThat(canonical.get("P2:metrics*"), containsInAnyOrder("P2:metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("P1:logs*", "P2:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("P1:logs*"), containsInAnyOrder("P1:logs*"));
+ assertIndexRewriteResultsContains(actual.get("P2:metrics*"), containsInAnyOrder("P2:metrics*"));
}
public void testOriginQualifiedOnlyRewrite() {
@@ -89,15 +85,11 @@ public void testOriginQualifiedOnlyRewrite() {
);
String[] requestedResources = new String[] { "_origin:logs*", "_origin:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*"));
- assertThat(canonical.get("_origin:logs*"), containsInAnyOrder("logs*"));
- assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("_origin:logs*"), containsInAnyOrder("logs*"));
+ assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
}
public void testOriginQualifiedOnlyRewriteWithNoLikedProjects() {
@@ -105,15 +97,11 @@ public void testOriginQualifiedOnlyRewriteWithNoLikedProjects() {
List linked = List.of();
String[] requestedResources = new String[] { "_origin:logs*", "_origin:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*"));
- assertThat(canonical.get("_origin:logs*"), containsInAnyOrder("logs*"));
- assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("_origin:logs*"), containsInAnyOrder("logs*"));
+ assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
}
public void testOriginWithDifferentAliasQualifiedOnlyRewrite() {
@@ -130,15 +118,11 @@ public void testOriginWithDifferentAliasQualifiedOnlyRewrite() {
String metricResource = aliasForOrigin + ":" + metricsIndexAlias;
String[] requestedResources = new String[] { logResource, metricResource };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder(logResource, metricResource));
- assertThat(canonical.get(logResource), containsInAnyOrder(logIndexAlias));
- assertThat(canonical.get(metricResource), containsInAnyOrder(metricsIndexAlias));
+ assertThat(actual.keySet(), containsInAnyOrder(logResource, metricResource));
+ assertIndexRewriteResultsContains(actual.get(logResource), containsInAnyOrder(logIndexAlias));
+ assertIndexRewriteResultsContains(actual.get(metricResource), containsInAnyOrder(metricsIndexAlias));
}
public void testQualifiedLinkedAndOriginRewrite() {
@@ -150,15 +134,11 @@ public void testQualifiedLinkedAndOriginRewrite() {
);
String[] requestedResources = new String[] { "P1:logs*", "_origin:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("P1:logs*", "_origin:metrics*"));
- assertThat(canonical.get("P1:logs*"), containsInAnyOrder("P1:logs*"));
- assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("P1:logs*", "_origin:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("P1:logs*"), containsInAnyOrder("P1:logs*"));
+ assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
}
public void testQualifiedStartsWithProjectWildcardRewrite() {
@@ -171,14 +151,10 @@ public void testQualifiedStartsWithProjectWildcardRewrite() {
);
String[] requestedResources = new String[] { "Q*:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("Q*:metrics*"));
- assertThat(canonical.get("Q*:metrics*"), containsInAnyOrder("Q1:metrics*", "Q2:metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("Q*:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("Q*:metrics*"), containsInAnyOrder("Q1:metrics*", "Q2:metrics*"));
}
public void testQualifiedEndsWithProjectWildcardRewrite() {
@@ -191,14 +167,10 @@ public void testQualifiedEndsWithProjectWildcardRewrite() {
);
String[] requestedResources = new String[] { "*1:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("*1:metrics*"));
- assertThat(canonical.get("*1:metrics*"), containsInAnyOrder("P1:metrics*", "Q1:metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("*1:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("*1:metrics*"), containsInAnyOrder("P1:metrics*", "Q1:metrics*"));
}
public void testOriginProjectMatchingTwice() {
@@ -206,15 +178,11 @@ public void testOriginProjectMatchingTwice() {
List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"));
String[] requestedResources = new String[] { "P0:metrics*", "_origin:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("P0:metrics*", "_origin:metrics*"));
- assertThat(canonical.get("P0:metrics*"), containsInAnyOrder("metrics*"));
- assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("P0:metrics*", "_origin:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("P0:metrics*"), containsInAnyOrder("metrics*"));
+ assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*"));
}
public void testUnderscoreWildcardShouldNotMatchOrigin() {
@@ -222,14 +190,10 @@ public void testUnderscoreWildcardShouldNotMatchOrigin() {
List linked = List.of(createRandomProjectWithAlias("_P1"), createRandomProjectWithAlias("_P2"));
String[] requestedResources = new String[] { "_*:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("_*:metrics*"));
- assertThat(canonical.get("_*:metrics*"), containsInAnyOrder("_P1:metrics*", "_P2:metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("_*:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("_*:metrics*"), containsInAnyOrder("_P1:metrics*", "_P2:metrics*"));
}
public void testDuplicateInputShouldProduceSingleOutput() {
@@ -243,14 +207,10 @@ public void testDuplicateInputShouldProduceSingleOutput() {
String indexPattern = "Q*:metrics*";
String[] requestedResources = new String[] { indexPattern, indexPattern };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder(indexPattern));
- assertThat(canonical.get(indexPattern), containsInAnyOrder("Q1:metrics*", "Q2:metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder(indexPattern));
+ assertIndexRewriteResultsContains(actual.get(indexPattern), containsInAnyOrder("Q1:metrics*", "Q2:metrics*"));
}
public void testProjectWildcardNotMatchingAnythingShouldThrow() {
@@ -265,7 +225,7 @@ public void testProjectWildcardNotMatchingAnythingShouldThrow() {
expectThrows(
ResourceNotFoundException.class,
- () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
+ () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
);
}
@@ -282,7 +242,7 @@ public void testRewritingShouldThrowOnIndexExclusions() {
expectThrows(
IllegalArgumentException.class,
- () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
+ () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
);
}
@@ -299,7 +259,7 @@ public void testRewritingShouldThrowOnIndexSelectors() {
expectThrows(
IllegalArgumentException.class,
- () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
+ () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
);
}
@@ -313,14 +273,13 @@ public void testWildcardOnlyProjectRewrite() {
);
String[] requestedResources = new String[] { "*:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("*:metrics*"));
- assertThat(canonical.get("*:metrics*"), containsInAnyOrder("P1:metrics*", "P2:metrics*", "Q1:metrics*", "Q2:metrics*", "metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("*:metrics*"));
+ assertIndexRewriteResultsContains(
+ actual.get("*:metrics*"),
+ containsInAnyOrder("P1:metrics*", "P2:metrics*", "Q1:metrics*", "Q2:metrics*", "metrics*")
+ );
}
public void testWildcardMatchesOnlyOriginProject() {
@@ -333,14 +292,10 @@ public void testWildcardMatchesOnlyOriginProject() {
);
String[] requestedResources = new String[] { "alias*:metrics*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("alias*:metrics*"));
- assertThat(canonical.get("alias*:metrics*"), containsInAnyOrder("metrics*"));
+ assertThat(actual.keySet(), containsInAnyOrder("alias*:metrics*"));
+ assertIndexRewriteResultsContains(actual.get("alias*:metrics*"), containsInAnyOrder("metrics*"));
}
public void testEmptyExpressionShouldMatchAll() {
@@ -348,24 +303,20 @@ public void testEmptyExpressionShouldMatchAll() {
List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"));
String[] requestedResources = new String[] {};
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("*"));
- assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*"));
+ assertThat(actual.keySet(), containsInAnyOrder("_all"));
+ assertIndexRewriteResultsContains(actual.get("_all"), containsInAnyOrder("P1:_all", "P2:_all", "_all"));
}
public void testNullExpressionShouldMatchAll() {
ProjectRoutingInfo origin = createRandomProjectWithAlias("P0");
List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"));
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, null);
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, null);
- assertThat(canonical.keySet(), containsInAnyOrder("*"));
- assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*"));
+ assertThat(actual.keySet(), containsInAnyOrder("_all"));
+ assertIndexRewriteResultsContains(actual.get("_all"), containsInAnyOrder("P1:_all", "P2:_all", "_all"));
}
public void testWildcardExpressionShouldMatchAll() {
@@ -373,14 +324,10 @@ public void testWildcardExpressionShouldMatchAll() {
List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2"));
String[] requestedResources = new String[] { "*" };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder("*"));
- assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*"));
+ assertThat(actual.keySet(), containsInAnyOrder("*"));
+ assertIndexRewriteResultsContains(actual.get("*"), containsInAnyOrder("P1:*", "P2:*", "*"));
}
public void test_ALLExpressionShouldMatchAll() {
@@ -389,14 +336,10 @@ public void test_ALLExpressionShouldMatchAll() {
String all = randomBoolean() ? "_ALL" : "_all";
String[] requestedResources = new String[] { all };
- Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(
- origin,
- linked,
- requestedResources
- );
+ var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources);
- assertThat(canonical.keySet(), containsInAnyOrder(all));
- assertThat(canonical.get(all), containsInAnyOrder("P1:" + all, "P2:" + all, all));
+ assertThat(actual.keySet(), containsInAnyOrder(all));
+ assertIndexRewriteResultsContains(actual.get(all), containsInAnyOrder("P1:" + all, "P2:" + all, all));
}
public void testRewritingShouldThrowIfNotProjectMatchExpression() {
@@ -411,7 +354,7 @@ public void testRewritingShouldThrowIfNotProjectMatchExpression() {
expectThrows(
NoMatchingProjectException.class,
- () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
+ () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources)
);
}
@@ -424,4 +367,21 @@ private ProjectRoutingInfo createRandomProjectWithAlias(String alias) {
ProjectTags projectTags = new ProjectTags(tags);
return new ProjectRoutingInfo(projectId, type, alias, org, projectTags);
}
+
+ private static void assertIndexRewriteResultsContains(
+ IndexExpressionsRewriter.IndexRewriteResult actual,
+ Matcher> iterableMatcher
+ ) {
+ assertThat(resultAsList(actual), iterableMatcher);
+ }
+
+ private static List resultAsList(IndexExpressionsRewriter.IndexRewriteResult result) {
+ if (result.localExpression() == null) {
+ return result.remoteExpressions();
+ }
+ List all = new ArrayList<>();
+ all.add(result.localExpression());
+ all.addAll(result.remoteExpressions());
+ return List.copyOf(all);
+ }
}
From 607f46594f6d358e0c7a9a5163b09fd5abda2c5c Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Tue, 30 Sep 2025 20:31:48 +0200
Subject: [PATCH 57/89] Renames
---
.../indices/resolve/ResolveIndexAction.java | 57 ++++++++++++-------
.../core/security/SecurityExtension.java | 4 +-
...e.java => AuthorizedProjectsSupplier.java} | 8 +--
.../xpack/security/Security.java | 12 ++--
.../security/authz/AuthorizationService.java | 14 ++---
5 files changed, 56 insertions(+), 39 deletions(-)
rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/{CrossProjectSearchAuthorizationService.java => AuthorizedProjectsSupplier.java} (68%)
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
index ddbf9700f66c0..14a10f7f27d68 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java
@@ -622,9 +622,10 @@ protected void doExecute(Task task, Request request, final ActionListener remoteClusterIndices = remoteClusterService.groupIndices(
- request.resolveCrossProject ? lenientIndicesOptionsForFanout(originalIndicesOptions) : originalIndicesOptions,
+ resolveCrossProject ? lenientIndicesOptionsForFanout(originalIndicesOptions) : originalIndicesOptions,
request.indices()
);
final OriginalIndices localIndices = remoteClusterIndices.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
@@ -642,7 +643,7 @@ protected void doExecute(Task task, Request request, final ActionListener remoteResponses = Collections.synchronizedSortedMap(new TreeMap<>());
final Runnable terminalHandler = () -> {
if (completionCounter.countDown()) {
- if (request.resolveCrossProject) {
+ if (resolveCrossProject) {
Map linkedProjectExpressions = remoteResponses.entrySet()
.stream()
.collect(
@@ -686,36 +687,52 @@ protected void doExecute(Task task, Request request, final ActionListener {
remoteResponses.put(clusterAlias, response);
terminalHandler.run();
- }, failure -> {
- LOGGER.error("failed to resolve indices on remote cluster [{}]", clusterAlias, failure);
- terminalHandler.run();
- }));
+ }, failure -> terminalHandler.run()));
}
} else {
- if (request.resolveCrossProject) {
- try {
- CrossProjectSearchErrorHandler.crossProjectFanoutErrorHandling(
- request.indicesOptions,
- resolvedExpressions,
- new RemoteIndexExpressions(Map.of())
- );
- } catch (Exception ex) {
- listener.onFailure(ex);
- return;
- }
- }
- listener.onResponse(
+ onResponse(
+ request,
+ listener,
+ resolveCrossProject,
+ resolvedExpressions,
new Response(indices, aliases, dataStreams, request.includeResolvedExpressions ? resolvedExpressions : null)
);
}
}
+ private void onResponse(
+ Request request,
+ ActionListener listener,
+ boolean resolveCrossProject,
+ ResolvedIndexExpressions resolvedExpressions,
+ Response response
+ ) {
+ if (resolveCrossProject) {
+ try {
+ CrossProjectSearchErrorHandler.crossProjectFanoutErrorHandling(
+ request.indicesOptions,
+ resolvedExpressions,
+ new RemoteIndexExpressions(Map.of())
+ );
+ } catch (Exception ex) {
+ listener.onFailure(ex);
+ return;
+ }
+ }
+ listener.onResponse(response);
+ }
+
+ private boolean resolveCrossProject(Request request) {
+ // TODO use IndicesOptions instead
+ return request.resolveCrossProject;
+ }
+
/**
* Resolves the specified names and/or wildcard expressions to index abstractions. Returns results in the supplied lists.
*
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
index db31445001e3b..1863c4de716f0 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
@@ -21,7 +21,7 @@
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
-import org.elasticsearch.xpack.core.security.authz.CrossProjectSearchAuthorizationService;
+import org.elasticsearch.xpack.core.security.authz.AuthorizedProjectsSupplier;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
@@ -150,7 +150,7 @@ default String extensionName() {
return getClass().getName();
}
- default CrossProjectSearchAuthorizationService getCrossProjectSearchAuthorizationService(SecurityComponents components) {
+ default AuthorizedProjectsSupplier getCrossProjectSearchAuthorizationService(SecurityComponents components) {
return null;
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizedProjectsSupplier.java
similarity index 68%
rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizedProjectsSupplier.java
index 746ac2196815e..a9d12115d5d57 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectSearchAuthorizationService.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizedProjectsSupplier.java
@@ -10,14 +10,14 @@
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.search.crossproject.TargetProjects;
-public interface CrossProjectSearchAuthorizationService {
- void loadAuthorizedProjects(ActionListener