diff --git a/server/src/main/java/org/elasticsearch/CrossProjectAwareRequest.java b/server/src/main/java/org/elasticsearch/CrossProjectAwareRequest.java
new file mode 100644
index 0000000000000..394f42a1f1aa7
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/CrossProjectAwareRequest.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import org.elasticsearch.action.IndicesRequest;
+import org.elasticsearch.core.Nullable;
+
+import java.util.List;
+
+public interface CrossProjectAwareRequest extends IndicesRequest {
+ /**
+ * Can be used to determine if this should be processed in cross-project mode vs. stateful CCS.
+ */
+ boolean crossProjectModeEnabled();
+
+ /**
+ * Only called if cross-project rewriting (flat-world, linked project filtering) was applied
+ */
+ void qualified(List qualifiedExpressions);
+
+ @Nullable
+ String queryRouting();
+
+ /**
+ * Used to track a mapping from original expression (potentially flat-world) to canonicalized CCS expressions.
+ * e.g. for an original index expression `logs-*`, this would be:
+ * original=logs-*
+ * qualified=[(logs-*, _local), (my-remote:logs-*, my-remote)]
+ */
+ record QualifiedExpression(String original, List qualified) {
+ public boolean hasFlatOriginalExpression() {
+ return true;
+ }
+ }
+
+ record ExpressionWithProject(String expression, String project) {}
+}
diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java
index ec30886b1acbf..cdb3a193f629b 100644
--- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java
@@ -9,6 +9,7 @@
package org.elasticsearch.action.fieldcaps;
+import org.elasticsearch.CrossProjectAwareRequest;
import org.elasticsearch.TransportVersions;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.IndicesRequest;
@@ -36,11 +37,16 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
-public final class FieldCapabilitiesRequest extends LegacyActionRequest implements IndicesRequest.Replaceable, ToXContentObject {
+public final class FieldCapabilitiesRequest extends LegacyActionRequest
+ implements
+ CrossProjectAwareRequest,
+ IndicesRequest.Replaceable,
+ ToXContentObject {
public static final String NAME = "field_caps_request";
public static final IndicesOptions DEFAULT_INDICES_OPTIONS = IndicesOptions.strictExpandOpenAndForbidClosed();
@@ -58,6 +64,7 @@ public final class FieldCapabilitiesRequest extends LegacyActionRequest implemen
private QueryBuilder indexFilter;
private Map runtimeFields = Collections.emptyMap();
private Long nowInMillis;
+ private List qualifiedExpressions;
public FieldCapabilitiesRequest(StreamInput in) throws IOException {
super(in);
@@ -373,4 +380,25 @@ public String getDescription() {
}
};
}
+
+ @Override
+ public boolean crossProjectModeEnabled() {
+ return qualifiedExpressions != null;
+ }
+
+ @Override
+ public void qualified(List qualifiedExpressions) {
+ this.qualifiedExpressions = qualifiedExpressions;
+ indices(
+ qualifiedExpressions.stream()
+ .flatMap(indexExpression -> indexExpression.qualified().stream().map(ExpressionWithProject::expression))
+ .toArray(String[]::new)
+ );
+ }
+
+ @Override
+ public String queryRouting() {
+ // TODO how would this look in ES|QL?
+ return null;
+ }
}
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 fda2df81d3f94..b06b9707d9a5f 100644
--- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
@@ -9,6 +9,7 @@
package org.elasticsearch.action.search;
+import org.elasticsearch.CrossProjectAwareRequest;
import org.elasticsearch.TransportVersions;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequestValidationException;
@@ -53,7 +54,11 @@
* @see Client#search(SearchRequest)
* @see SearchResponse
*/
-public class SearchRequest extends LegacyActionRequest implements IndicesRequest.Replaceable, Rewriteable {
+public class SearchRequest extends LegacyActionRequest
+ implements
+ CrossProjectAwareRequest,
+ IndicesRequest.Replaceable,
+ Rewriteable {
public static final ToXContent.Params FORMAT_PARAMS = new ToXContent.MapParams(Collections.singletonMap("pretty", "false"));
@@ -70,6 +75,12 @@ public class SearchRequest extends LegacyActionRequest implements IndicesRequest
private String[] indices = Strings.EMPTY_ARRAY;
+ @Nullable
+ private String queryRouting = null;
+
+ @Nullable
+ private List qualifiedExpressions;
+
@Nullable
private String routing;
@Nullable
@@ -400,6 +411,11 @@ public SearchRequest indices(String... indices) {
return this;
}
+ public SearchRequest queryRouting(String queryRouting) {
+ this.queryRouting = queryRouting;
+ return this;
+ }
+
private static void validateIndices(String... indices) {
Objects.requireNonNull(indices, "indices must not be null");
for (String index : indices) {
@@ -853,4 +869,24 @@ public String toString() {
+ source
+ '}';
}
+
+ @Override
+ public boolean crossProjectModeEnabled() {
+ return qualifiedExpressions != null;
+ }
+
+ @Override
+ public void qualified(List qualifiedExpressions) {
+ this.qualifiedExpressions = qualifiedExpressions;
+ indices(
+ qualifiedExpressions.stream()
+ .flatMap(indexExpression -> indexExpression.qualified().stream().map(ExpressionWithProject::expression))
+ .toArray(String[]::new)
+ );
+ }
+
+ @Override
+ public String queryRouting() {
+ return queryRouting;
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java
index 1fbc8993cc5aa..64575bac95f8d 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java
@@ -367,6 +367,7 @@ public void apply(Settings value, Settings current, Settings previous) {
TransportSearchAction.SHARD_COUNT_LIMIT_SETTING,
TransportSearchAction.DEFAULT_PRE_FILTER_SHARD_SIZE,
RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE,
+ RemoteClusterService.REMOTE_CLUSTER_TAGS,
SniffConnectionStrategy.REMOTE_CONNECTIONS_PER_CLUSTER,
RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING,
RemoteClusterService.REMOTE_NODE_ATTRIBUTE,
diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java
index 5eda47bc32354..3cd3644f418dc 100644
--- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java
+++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java
@@ -9,6 +9,8 @@
package org.elasticsearch.rest.action.search;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.search.SearchRequest;
@@ -63,6 +65,7 @@ public class RestSearchAction extends BaseRestHandler {
public static final String TYPED_KEYS_PARAM = "typed_keys";
public static final String INCLUDE_NAMED_QUERIES_SCORE_PARAM = "include_named_queries_score";
public static final Set RESPONSE_PARAMS = Set.of(TYPED_KEYS_PARAM, TOTAL_HITS_AS_INT_PARAM, INCLUDE_NAMED_QUERIES_SCORE_PARAM);
+ private static final Logger log = LogManager.getLogger(RestSearchAction.class);
private final SearchUsageHolder searchUsageHolder;
private final Predicate clusterSupportsFeature;
@@ -98,6 +101,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
client.threadPool().getThreadContext().setErrorTraceTransportHeader(request);
}
SearchRequest searchRequest = new SearchRequest();
+
// access the BwC param, but just drop it
// this might be set by old clients
request.param("min_compatible_shard_node");
@@ -167,6 +171,14 @@ public static void parseSearchRequest(
searchRequest.source(new SearchSourceBuilder());
}
searchRequest.indices(Strings.splitStringByCommaToArray(request.param("index")));
+
+ String queryRouting = request.param("query_routing", null);
+ if (queryRouting != null) {
+ searchRequest.queryRouting(queryRouting);
+ } else {
+ log.info("No query routing defined");
+ }
+
if (requestContentParser != null) {
if (searchUsageHolder == null) {
searchRequest.source().parseXContent(requestContentParser, true, clusterSupportsFeature);
diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java
index fdb597b47c137..e6951ec29565b 100644
--- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java
+++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java
@@ -41,6 +41,7 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -97,6 +98,33 @@ public final class RemoteClusterService extends RemoteClusterAware
(ns, key) -> boolSetting(key, true, new RemoteConnectionEnabled<>(ns, key), Setting.Property.Dynamic, Setting.Property.NodeScope)
);
+ public record Metadata(String key, String value) {
+ public static Metadata fromString(String tag) {
+ if (tag == null || tag.isEmpty()) {
+ throw new IllegalArgumentException("Remote tag must not be null or empty");
+ }
+ // - as a separator to simplify search path param parsing; won't be like this in the real implementation
+ int idx = tag.indexOf('-');
+ if (idx < 0) {
+ return new Metadata(tag, "");
+ } else {
+ return new Metadata(tag.substring(0, idx), tag.substring(idx + 1));
+ }
+ }
+ }
+
+ public static final Setting.AffixSetting> REMOTE_CLUSTER_TAGS = Setting.affixKeySetting(
+ "cluster.remote.",
+ "tags",
+ (ns, key) -> Setting.listSetting(
+ key,
+ Collections.emptyList(),
+ Metadata::fromString,
+ Setting.Property.Dynamic,
+ Setting.Property.NodeScope
+ )
+ );
+
public static final Setting.AffixSetting REMOTE_CLUSTER_PING_SCHEDULE = Setting.affixKeySetting(
"cluster.remote.",
"transport.ping_schedule",
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CrossProjectRemoteServerTransportInterceptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CrossProjectRemoteServerTransportInterceptor.java
new file mode 100644
index 0000000000000..6ba0c92e3ce0e
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CrossProjectRemoteServerTransportInterceptor.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import org.elasticsearch.transport.Transport;
+import org.elasticsearch.transport.TransportInterceptor;
+import org.elasticsearch.transport.TransportRequest;
+import org.elasticsearch.transport.TransportRequestOptions;
+import org.elasticsearch.transport.TransportResponse;
+import org.elasticsearch.transport.TransportResponseHandler;
+
+public interface CrossProjectRemoteServerTransportInterceptor {
+ // TODO probably don't want this
+ boolean enabled();
+
+ // TODO this should be a wrapper around TransportInterceptor.AsyncSender instead
+ void sendRequest(
+ TransportInterceptor.AsyncSender sender,
+ Transport.Connection connection,
+ String action,
+ TransportRequest request,
+ TransportRequestOptions options,
+ TransportResponseHandler handler
+ );
+
+ CustomServerTransportFilter getFilter();
+
+ class Default implements CrossProjectRemoteServerTransportInterceptor {
+ @Override
+ public boolean enabled() {
+ return false;
+ }
+
+ @Override
+ public void sendRequest(
+ TransportInterceptor.AsyncSender sender,
+ Transport.Connection connection,
+ String action,
+ TransportRequest request,
+ TransportRequestOptions options,
+ TransportResponseHandler handler
+ ) {
+ sender.sendRequest(connection, action, request, options, handler);
+ }
+
+ @Override
+ public CustomServerTransportFilter getFilter() {
+ return new CustomServerTransportFilter.Default();
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CustomServerTransportFilter.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CustomServerTransportFilter.java
new file mode 100644
index 0000000000000..b40e7b4ab60c7
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CustomServerTransportFilter.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;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.transport.TransportRequest;
+
+public interface CustomServerTransportFilter {
+ void filter(String securityAction, TransportRequest request, ActionListener authenticationListener);
+
+ class Default implements CustomServerTransportFilter {
+ @Override
+ public void filter(String securityAction, TransportRequest request, ActionListener listener) {
+ listener.onResponse(null);
+ }
+ }
+}
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 f41b19de95272..93dfb3d1baac6 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.CrossProjectTargetResolver;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
@@ -133,6 +134,14 @@ default CustomApiKeyAuthenticator getCustomApiKeyAuthenticator(SecurityComponent
return null;
}
+ default CrossProjectTargetResolver getCrossProjectTargetResolver(SecurityComponents components) {
+ return null;
+ }
+
+ default CrossProjectRemoteServerTransportInterceptor getCustomRemoteServerTransportInterceptor(SecurityComponents components) {
+ return null;
+ }
+
/**
* Returns a authorization engine for authorizing requests, or null to use the default authorization mechanism.
*
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectTargetResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectTargetResolver.java
new file mode 100644
index 0000000000000..40d969783f873
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/CrossProjectTargetResolver.java
@@ -0,0 +1,31 @@
+/*
+ * 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.xpack.core.security.SecurityContext;
+
+import java.util.List;
+
+/**
+ * Resolves linked and authorized projects, if running in a cross-project environment.
+ * In non-cross-project environments, `resolve` returns `ResolvedProjects.VOID`
+ */
+public interface CrossProjectTargetResolver {
+ ResolvedProjects resolve(SecurityContext securityContext);
+
+ class Default implements CrossProjectTargetResolver {
+ @Override
+ public ResolvedProjects resolve(SecurityContext securityContext) {
+ return ResolvedProjects.VOID;
+ }
+ }
+
+ record ResolvedProjects(List projects) {
+ public static ResolvedProjects VOID = new ResolvedProjects(List.of());
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java
index e9d8c93511106..95799282b0390 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/PlannerUtils.java
@@ -20,6 +20,7 @@
import org.elasticsearch.index.query.CoordinatorRewriteContext;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.transport.RemoteClusterAware;
import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
import org.elasticsearch.xpack.esql.capabilities.TranslationAware;
import org.elasticsearch.xpack.esql.core.expression.AttributeSet;
@@ -64,7 +65,6 @@
import java.util.function.Consumer;
import java.util.function.Predicate;
-import static java.util.Arrays.asList;
import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.DOC_VALUES;
import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.EXTRACT_SPATIAL_BOUNDS;
import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.NONE;
@@ -153,10 +153,26 @@ public static String[] planOriginalIndices(PhysicalPlan plan) {
return Strings.EMPTY_ARRAY;
}
var indices = new LinkedHashSet();
- forEachRelation(plan, relation -> indices.addAll(asList(Strings.commaDelimitedListToStringArray(relation.indexPattern()))));
+ forEachRelation(plan, relation -> indices.addAll(expressions(relation.indexPattern())));
return indices.toArray(String[]::new);
}
+ public static List expressions(String indexPattern) {
+ var result = new ArrayList();
+ for (var expression : Strings.commaDelimitedListToStringArray(indexPattern)) {
+ var clusterAndExpression = RemoteClusterAware.splitIndexName(expression);
+ if (clusterAndExpression[0] == null) {
+ // unqualified, convert to `index`,`*:index`
+ result.add(expression);
+ result.add("*:" + expression);
+ } else {
+ // qualified expression, keep as is
+ result.add(expression);
+ }
+ }
+ return result;
+ }
+
private static void forEachRelation(PhysicalPlan plan, Consumer action) {
plan.forEachDown(FragmentExec.class, f -> f.fragment().forEachDown(EsRelation.class, r -> {
if (r.indexMode() != IndexMode.LOOKUP) {
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java
index 39e3503b5fdd9..68a90b50046fb 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java
@@ -355,6 +355,7 @@ public void executePlan(
return;
}
}
+
Map clusterToOriginalIndices = transportService.getRemoteClusterService()
.groupIndices(SearchRequest.DEFAULT_INDICES_OPTIONS, PlannerUtils.planOriginalIndices(physicalPlan));
var localOriginalIndices = clusterToOriginalIndices.remove(LOCAL_CLUSTER);
@@ -466,6 +467,7 @@ public void executePlan(
}
}
// starts computes on remote clusters
+
final var remoteClusters = clusterComputeHandler.getRemoteClusters(clusterToConcreteIndices, clusterToOriginalIndices);
for (ClusterComputeHandler.RemoteCluster cluster : remoteClusters) {
if (execInfo.getCluster(cluster.clusterAlias()).getStatus() != EsqlExecutionInfo.Cluster.Status.RUNNING) {
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java
index 40a859e3f5b58..a140315f8971e 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java
@@ -614,7 +614,7 @@ private void initializeClusterData(List indices, EsqlExecutionInfo
Map clusterIndices = indicesExpressionGrouper.groupIndices(
configuredClusters,
IndicesOptions.DEFAULT,
- indices.getFirst().indexPattern()
+ String.join(",", PlannerUtils.expressions(indices.getFirst().indexPattern()))
);
for (Map.Entry entry : clusterIndices.entrySet()) {
final String clusterAlias = entry.getKey();
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java
index fa16de22c865c..e1a4cebaae4e6 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java
@@ -161,7 +161,7 @@ protected ChannelHandler getClientChannelInitializer(DiscoveryNode node, Connect
@Override
protected InboundPipeline getInboundPipeline(Channel channel, boolean isRemoteClusterServerChannel) {
- if (false == isRemoteClusterServerChannel) {
+ if (false == isRemoteClusterServerChannel || crossClusterAccessAuthenticationService.skipTransportCheck()) {
return super.getInboundPipeline(channel, false);
} else {
return new InboundPipeline(
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 b1e30490990ff..415164ac9145b 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
@@ -127,6 +127,7 @@
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
+import org.elasticsearch.xpack.core.security.CrossProjectRemoteServerTransportInterceptor;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.SecurityExtension;
import org.elasticsearch.xpack.core.security.SecurityField;
@@ -213,6 +214,7 @@
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
+import org.elasticsearch.xpack.core.security.authz.CrossProjectTargetResolver;
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;
@@ -1146,6 +1148,8 @@ Collection
*/
-
ResolvedIndices resolve(
String action,
TransportRequest request,
ProjectMetadata projectMetadata,
- AuthorizationEngine.AuthorizedIndices authorizedIndices
+ AuthorizationEngine.AuthorizedIndices authorizedIndices,
+ CrossProjectTargetResolver.ResolvedProjects resolvedProjects
) {
if (request instanceof IndicesAliasesRequest indicesAliasesRequest) {
ResolvedIndices.Builder resolvedIndicesBuilder = new ResolvedIndices.Builder();
@@ -124,15 +137,65 @@ ResolvedIndices resolve(
if (request instanceof IndicesRequest == false) {
throw new IllegalStateException("Request [" + request + "] is not an Indices request, but should be.");
}
+
+ if (request instanceof CrossProjectAwareRequest crossProjectAwareRequest) {
+ maybeRewriteCrossProjectRequest(resolvedProjects, crossProjectAwareRequest);
+ }
+
return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices);
}
+ void maybeRewriteCrossProjectRequest(CrossProjectTargetResolver.ResolvedProjects resolvedProjects, CrossProjectAwareRequest request) {
+ if (resolvedProjects == CrossProjectTargetResolver.ResolvedProjects.VOID) {
+ logger.info("Cross-project search is disabled or not applicable, skipping request [{}]...", request);
+ return;
+ }
+
+ if (resolvedProjects.projects().isEmpty()) {
+ // This is NOT correct
+ logger.info("No target projects available for cross-project search, skipping request [{}]...", request);
+ return;
+ }
+
+ String[] indices = request.indices();
+ ResolvedIndices resolved = remoteClusterResolver.splitLocalAndRemoteIndexNames(indices);
+ logger.info("Resolved indices: local [{}], remote [{}]", resolved.getLocal(), resolved.getRemote());
+
+ // skip handling searches where there's both qualified and flat expressions to simplify POC
+ // in the real thing, we'd also rewrite these
+ if (resolved.getRemote().isEmpty() == false) {
+ return;
+ }
+
+ List qualifiedExpressions = new ArrayList<>(indices.length);
+ for (String local : resolved.getLocal()) {
+ List expressionWithProjects = new ArrayList<>();
+ expressionWithProjects.add(new CrossProjectAwareRequest.ExpressionWithProject(local, "_local"));
+ for (String targetProject : resolvedProjects.projects()) {
+ expressionWithProjects.add(
+ new CrossProjectAwareRequest.ExpressionWithProject(
+ RemoteClusterAware.buildRemoteIndexName(targetProject, local),
+ targetProject
+ )
+ );
+ qualifiedExpressions.add(new CrossProjectAwareRequest.QualifiedExpression(local, expressionWithProjects));
+ }
+ logger.info("Rewrote [{}] to [{}]", local, expressionWithProjects);
+ }
+
+ request.qualified(qualifiedExpressions);
+ }
+
/**
* Attempt to resolve requested indices without expanding any wildcards.
* @return The {@link ResolvedIndices} or null if wildcard expansion must be performed.
*/
@Nullable
- ResolvedIndices tryResolveWithoutWildcards(String action, TransportRequest transportRequest) {
+ ResolvedIndices tryResolveWithoutWildcards(
+ String action,
+ TransportRequest transportRequest,
+ CrossProjectTargetResolver.ResolvedProjects resolvedProjects
+ ) {
// We only take care of IndicesRequest
if (false == transportRequest instanceof IndicesRequest) {
return null;
@@ -142,7 +205,7 @@ ResolvedIndices tryResolveWithoutWildcards(String action, TransportRequest trans
return null;
}
// It's safe to cast IndicesRequest since the above test guarantees it
- return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest);
+ return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest, resolvedProjects);
}
private static boolean requiresWildcardExpansion(IndicesRequest indicesRequest) {
@@ -158,6 +221,14 @@ private static boolean requiresWildcardExpansion(IndicesRequest indicesRequest)
}
ResolvedIndices resolveIndicesAndAliasesWithoutWildcards(String action, IndicesRequest indicesRequest) {
+ return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest, CrossProjectTargetResolver.ResolvedProjects.VOID);
+ }
+
+ ResolvedIndices resolveIndicesAndAliasesWithoutWildcards(
+ String action,
+ IndicesRequest indicesRequest,
+ CrossProjectTargetResolver.ResolvedProjects resolvedProjects
+ ) {
assert false == requiresWildcardExpansion(indicesRequest) : "request must not require wildcard expansion";
final String[] indices = indicesRequest.indices();
if (indices == null || indices.length == 0) {
@@ -230,7 +301,7 @@ ResolvedIndices resolveIndicesAndAliasesWithoutWildcards(String action, IndicesR
/**
* Returns the resolved indices from the {@link SearchContextId} within the provided {@link SearchRequest}.
*/
- ResolvedIndices resolvePITIndices(SearchRequest request) {
+ ResolvedIndices resolvePITIndices(SearchRequest request, CrossProjectTargetResolver.ResolvedProjects resolvedProjects) {
assert request.pointInTimeBuilder() != null;
var indices = SearchContextId.decodeIndices(request.pointInTimeBuilder().getEncodedId());
final ResolvedIndices split;
@@ -541,10 +612,11 @@ private static List indicesList(String[] list) {
return (list == null) ? null : Arrays.asList(list);
}
- private static class RemoteClusterResolver extends RemoteClusterAware {
+ static class RemoteClusterResolver extends RemoteClusterAware {
private final CopyOnWriteArraySet clusters;
+ @SuppressWarnings("this-escape")
private RemoteClusterResolver(Settings settings, ClusterSettings clusterSettings) {
super(settings);
clusters = new CopyOnWriteArraySet<>(getEnabledRemoteClusters(settings));
@@ -560,7 +632,7 @@ protected void updateRemoteCluster(String clusterAlias, Settings settings) {
}
}
- ResolvedIndices splitLocalAndRemoteIndexNames(String... indices) {
+ public ResolvedIndices splitLocalAndRemoteIndexNames(String... indices) {
final Map> map = super.groupClusterIndices(clusters, indices);
final List local = map.remove(LOCAL_CLUSTER_GROUP_KEY);
final List remote = map.entrySet()
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CustomRemoteServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CustomRemoteServerTransportFilter.java
new file mode 100644
index 0000000000000..a349264bad7e4
--- /dev/null
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CustomRemoteServerTransportFilter.java
@@ -0,0 +1,56 @@
+/*
+ * 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.transport;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.DestructiveOperations;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.transport.TransportRequest;
+import org.elasticsearch.xpack.core.security.CustomServerTransportFilter;
+import org.elasticsearch.xpack.core.security.SecurityContext;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
+import org.elasticsearch.xpack.security.authc.AuthenticationService;
+import org.elasticsearch.xpack.security.authz.AuthorizationService;
+
+final class CustomRemoteServerTransportFilter extends ServerTransportFilter {
+ private static final Logger logger = LogManager.getLogger(CustomRemoteServerTransportFilter.class);
+
+ private final CustomServerTransportFilter authenticator;
+
+ CustomRemoteServerTransportFilter(
+ CustomServerTransportFilter filter,
+ AuthenticationService authcService,
+ AuthorizationService authzService,
+ ThreadContext threadContext,
+ boolean extractClientCert,
+ DestructiveOperations destructiveOperations,
+ SecurityContext securityContext
+ ) {
+ super(authcService, authzService, threadContext, extractClientCert, destructiveOperations, securityContext);
+ this.authenticator = filter;
+ }
+
+ @Override
+ public void authenticate(String securityAction, TransportRequest request, ActionListener authenticationListener) {
+ logger.info("Custom authenticator authenticating request for action: {}", securityAction);
+ authenticator.filter(securityAction, request, new ActionListener<>() {
+ @Override
+ public void onResponse(Void unused) {
+ CustomRemoteServerTransportFilter.super.authenticate(securityAction, request, authenticationListener);
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ // TODO wrap exception
+ authenticationListener.onFailure(e);
+ }
+ });
+ }
+}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java
index 268f9e6375f0e..88e4dc4357f01 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java
@@ -39,6 +39,7 @@
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.transport.TransportService.ContextRestoreResponseHandler;
import org.elasticsearch.xpack.core.XPackSettings;
+import org.elasticsearch.xpack.core.security.CrossProjectRemoteServerTransportInterceptor;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo;
@@ -99,6 +100,7 @@ public class SecurityServerTransportInterceptor implements TransportInterceptor
private final CrossClusterAccessAuthenticationService crossClusterAccessAuthcService;
private final Function> remoteClusterCredentialsResolver;
private final XPackLicenseState licenseState;
+ private final CrossProjectRemoteServerTransportInterceptor crossProjectRemoteServerTransportInterceptor;
public SecurityServerTransportInterceptor(
Settings settings,
@@ -120,6 +122,33 @@ public SecurityServerTransportInterceptor(
securityContext,
destructiveOperations,
crossClusterAccessAuthcService,
+ new CrossProjectRemoteServerTransportInterceptor.Default(),
+ licenseState
+ );
+ }
+
+ public SecurityServerTransportInterceptor(
+ Settings settings,
+ ThreadPool threadPool,
+ AuthenticationService authcService,
+ AuthorizationService authzService,
+ SSLService sslService,
+ SecurityContext securityContext,
+ DestructiveOperations destructiveOperations,
+ CrossClusterAccessAuthenticationService crossClusterAccessAuthcService,
+ CrossProjectRemoteServerTransportInterceptor crossProjectRemoteServerTransportInterceptor,
+ XPackLicenseState licenseState
+ ) {
+ this(
+ settings,
+ threadPool,
+ authcService,
+ authzService,
+ sslService,
+ securityContext,
+ destructiveOperations,
+ crossClusterAccessAuthcService,
+ crossProjectRemoteServerTransportInterceptor,
licenseState,
RemoteConnectionManager::resolveRemoteClusterAliasWithCredentials
);
@@ -137,6 +166,35 @@ public SecurityServerTransportInterceptor(
XPackLicenseState licenseState,
// Inject for simplified testing
Function> remoteClusterCredentialsResolver
+ ) {
+ this(
+ settings,
+ threadPool,
+ authcService,
+ authzService,
+ sslService,
+ securityContext,
+ destructiveOperations,
+ crossClusterAccessAuthcService,
+ new CrossProjectRemoteServerTransportInterceptor.Default(),
+ licenseState,
+ remoteClusterCredentialsResolver
+ );
+ }
+
+ SecurityServerTransportInterceptor(
+ Settings settings,
+ ThreadPool threadPool,
+ AuthenticationService authcService,
+ AuthorizationService authzService,
+ SSLService sslService,
+ SecurityContext securityContext,
+ DestructiveOperations destructiveOperations,
+ CrossClusterAccessAuthenticationService crossClusterAccessAuthcService,
+ CrossProjectRemoteServerTransportInterceptor crossProjectRemoteServerTransportInterceptor,
+ XPackLicenseState licenseState,
+ // Inject for simplified testing
+ Function> remoteClusterCredentialsResolver
) {
this.settings = settings;
this.threadPool = threadPool;
@@ -147,6 +205,7 @@ public SecurityServerTransportInterceptor(
this.crossClusterAccessAuthcService = crossClusterAccessAuthcService;
this.licenseState = licenseState;
this.remoteClusterCredentialsResolver = remoteClusterCredentialsResolver;
+ this.crossProjectRemoteServerTransportInterceptor = crossProjectRemoteServerTransportInterceptor;
this.profileFilters = initializeProfileFilters(destructiveOperations);
}
@@ -272,14 +331,17 @@ public void sendRequest(
final Optional remoteClusterCredentials = getRemoteClusterCredentials(connection);
if (remoteClusterCredentials.isPresent()) {
sendWithCrossClusterAccessHeaders(remoteClusterCredentials.get(), connection, action, request, options, handler);
- } else {
- // Send regular request, without cross cluster access headers
- try {
- sender.sendRequest(connection, action, request, options, handler);
- } catch (Exception e) {
- handler.handleException(new SendRequestTransportException(connection.getNode(), action, e));
+ } else if (crossProjectRemoteServerTransportInterceptor.enabled()
+ && RemoteConnectionManager.resolveRemoteClusterAliasWithCredentials(connection).isPresent()) {
+ crossProjectRemoteServerTransportInterceptor.sendRequest(sender, connection, action, request, options, handler);
+ } else {
+ // Send regular request, without cross cluster access headers
+ try {
+ sender.sendRequest(connection, action, request, options, handler);
+ } catch (Exception e) {
+ handler.handleException(new SendRequestTransportException(connection.getNode(), action, e));
+ }
}
- }
}
/**
@@ -495,18 +557,34 @@ private Map initializeProfileFilters(DestructiveO
final String profileName = entry.getKey();
final boolean useRemoteClusterProfile = remoteClusterPortEnabled && profileName.equals(REMOTE_CLUSTER_PROFILE);
if (useRemoteClusterProfile) {
- profileFilters.put(
- profileName,
- new CrossClusterAccessServerTransportFilter(
- crossClusterAccessAuthcService,
- authzService,
- threadPool.getThreadContext(),
- remoteClusterServerSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration),
- destructiveOperations,
- securityContext,
- licenseState
- )
- );
+ // probably not how we want to do this but ballpark correct
+ if (crossProjectRemoteServerTransportInterceptor.enabled()) {
+ profileFilters.put(
+ profileName,
+ new CustomRemoteServerTransportFilter(
+ crossProjectRemoteServerTransportInterceptor.getFilter(),
+ authcService,
+ authzService,
+ threadPool.getThreadContext(),
+ remoteClusterServerSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration),
+ destructiveOperations,
+ securityContext
+ )
+ );
+ } else {
+ profileFilters.put(
+ profileName,
+ new CrossClusterAccessServerTransportFilter(
+ crossClusterAccessAuthcService,
+ authzService,
+ threadPool.getThreadContext(),
+ remoteClusterServerSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration),
+ destructiveOperations,
+ securityContext,
+ licenseState
+ )
+ );
+ }
} else {
profileFilters.put(
profileName,
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java
index 9eac5512520b2..b92435b3520b4 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java
@@ -27,6 +27,7 @@
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.user.SystemUser;
import org.elasticsearch.xpack.security.action.SecurityActionMapper;
+import org.elasticsearch.xpack.security.audit.AuditUtil;
import org.elasticsearch.xpack.security.authc.AuthenticationService;
import org.elasticsearch.xpack.security.authz.AuthorizationService;
@@ -66,7 +67,7 @@ class ServerTransportFilter {
* thrown by this method will stop the request from being handled and the error will
* be sent back to the sender.
*/
- void inbound(String action, TransportRequest request, TransportChannel transportChannel, ActionListener listener) {
+ public void inbound(String action, TransportRequest request, TransportChannel transportChannel, ActionListener listener) {
if (TransportCloseIndexAction.NAME.equals(action)
|| OpenIndexAction.NAME.equals(action)
|| TransportDeleteIndexAction.TYPE.name().equals(action)) {
@@ -104,13 +105,30 @@ requests from all the nodes are attached with a user (either a serialize
TransportVersion version = transportChannel.getVersion();
authenticate(securityAction, request, listener.delegateFailureAndWrap((l, authentication) -> {
if (authentication != null) {
+ // TODO why is this needed?
+ String id = AuditUtil.getOrGenerateRequestId(threadContext);
+ logger.debug("Audit id [{}] for request [{}]", id, request.getDescription());
if (securityAction.equals(TransportService.HANDSHAKE_ACTION_NAME)
&& SystemUser.is(authentication.getEffectiveSubject().getUser()) == false) {
+ logger.debug(
+ "Authenticated request [{}] for action [{}] from [{}] for hand shake",
+ authentication.getEffectiveSubject().getUser(),
+ securityAction,
+ authentication.getEffectiveSubject()
+ );
securityContext.executeAsSystemUser(version, original -> {
final Authentication replaced = securityContext.getAuthentication();
authzService.authorize(replaced, securityAction, request, l);
});
} else {
+ if (SystemUser.is(authentication.getEffectiveSubject().getUser()) == false) {
+ logger.info(
+ "Authenticated request [{}] for action [{}] from [{}]",
+ authentication.getEffectiveSubject().getUser(),
+ securityAction,
+ authentication.getEffectiveSubject()
+ );
+ }
authzService.authorize(authentication, securityAction, request, l);
}
} else {
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java
index 31c6d6f0c2341..ff681864e8864 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceTests.java
@@ -68,7 +68,8 @@ public void init() throws Exception {
crossClusterAccessAuthenticationService = new CrossClusterAccessAuthenticationService(
clusterService,
apiKeyService,
- authenticationService
+ authenticationService,
+ false
);
}
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java
index e4bb33c66d983..f2bec1021d5e3 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java
@@ -155,6 +155,7 @@
import org.elasticsearch.xpack.core.security.authc.Subject;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
+import org.elasticsearch.xpack.core.security.authz.CrossProjectTargetResolver;
import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField;
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
@@ -336,7 +337,8 @@ public void setup() {
operatorPrivilegesService,
RESTRICTED_INDICES,
new AuthorizationDenialMessages.Default(),
- projectResolver
+ projectResolver,
+ new CrossProjectTargetResolver.Default()
);
}
@@ -1769,7 +1771,8 @@ public void testDenialForAnonymousUser() {
operatorPrivilegesService,
RESTRICTED_INDICES,
new AuthorizationDenialMessages.Default(),
- projectResolver
+ projectResolver,
+ new CrossProjectTargetResolver.Default()
);
RoleDescriptor role = new RoleDescriptor(
@@ -1819,7 +1822,8 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() {
operatorPrivilegesService,
RESTRICTED_INDICES,
new AuthorizationDenialMessages.Default(),
- projectResolver
+ projectResolver,
+ new CrossProjectTargetResolver.Default()
);
RoleDescriptor role = new RoleDescriptor(
@@ -3357,7 +3361,8 @@ public void testAuthorizationEngineSelectionForCheckPrivileges() throws Exceptio
operatorPrivilegesService,
RESTRICTED_INDICES,
new AuthorizationDenialMessages.Default(),
- projectResolver
+ projectResolver,
+ new CrossProjectTargetResolver.Default()
);
Subject subject = new Subject(new User("test", "a role"), mock(RealmRef.class));
@@ -3513,7 +3518,8 @@ public void getUserPrivileges(AuthorizationInfo authorizationInfo, ActionListene
operatorPrivilegesService,
RESTRICTED_INDICES,
new AuthorizationDenialMessages.Default(),
- projectResolver
+ projectResolver,
+ new CrossProjectTargetResolver.Default()
);
Authentication authentication;
try (StoredContext ignore = threadContext.stashContext()) {
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java
index cb6db57ee5558..c9f9b55126bf7 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java
@@ -69,6 +69,7 @@
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
import org.elasticsearch.xpack.core.security.authc.Subject;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizedIndices;
+import org.elasticsearch.xpack.core.security.authz.CrossProjectTargetResolver;
import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField;
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
@@ -417,7 +418,12 @@ public void setup() {
ClusterService clusterService = mock(ClusterService.class);
when(clusterService.getClusterSettings()).thenReturn(new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS));
- defaultIndicesResolver = new IndicesAndAliasesResolver(settings, clusterService, indexNameExpressionResolver);
+ defaultIndicesResolver = new IndicesAndAliasesResolver(
+ settings,
+ clusterService,
+ indexNameExpressionResolver,
+ new CrossProjectTargetResolver.Default()
+ );
}
public void testDashIndicesAreAllowedInShardLevelRequests() {
@@ -2666,7 +2672,13 @@ private ResolvedIndices resolveIndices(TransportRequest request, AuthorizedIndic
}
private ResolvedIndices resolveIndices(String action, TransportRequest request, AuthorizedIndices authorizedIndices) {
- return defaultIndicesResolver.resolve(action, request, this.projectMetadata, authorizedIndices);
+ return defaultIndicesResolver.resolve(
+ action,
+ request,
+ this.projectMetadata,
+ authorizedIndices,
+ CrossProjectTargetResolver.ResolvedProjects.VOID
+ );
}
private static void assertNoIndices(IndicesRequest.Replaceable request, ResolvedIndices resolvedIndices) {