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 createComponents( if (authorizationDenialMessages.get() == null) { authorizationDenialMessages.set(new AuthorizationDenialMessages.Default()); } + + CrossProjectTargetResolver crossProjectTargetResolver = createCrossProjectTargetResolver(extensionComponents); final AuthorizationService authzService = new AuthorizationService( settings, allRolesStore, @@ -1162,7 +1166,8 @@ Collection createComponents( operatorPrivilegesService.get(), restrictedIndices, authorizationDenialMessages.get(), - projectResolver + projectResolver, + crossProjectTargetResolver ); components.add(nativeRolesStore); // used by roles actions @@ -1181,8 +1186,17 @@ Collection createComponents( ipFilter.set(new IPFilter(settings, auditTrailService, clusterService.getClusterSettings(), getLicenseState())); components.add(ipFilter.get()); + CrossProjectRemoteServerTransportInterceptor crossProjectRemoteServerTransportInterceptor = + createCustomRemoteServerTransportInterceptor(extensionComponents); DestructiveOperations destructiveOperations = new DestructiveOperations(settings, clusterService.getClusterSettings()); - crossClusterAccessAuthcService.set(new CrossClusterAccessAuthenticationService(clusterService, apiKeyService, authcService.get())); + crossClusterAccessAuthcService.set( + new CrossClusterAccessAuthenticationService( + clusterService, + apiKeyService, + authcService.get(), + crossProjectRemoteServerTransportInterceptor.enabled() + ) + ); components.add(crossClusterAccessAuthcService.get()); securityInterceptor.set( new SecurityServerTransportInterceptor( @@ -1194,6 +1208,7 @@ Collection createComponents( securityContext.get(), destructiveOperations, crossClusterAccessAuthcService.get(), + crossProjectRemoteServerTransportInterceptor, getLicenseState() ) ); @@ -1252,6 +1267,90 @@ Collection createComponents( return components; } + private CrossProjectRemoteServerTransportInterceptor createCustomRemoteServerTransportInterceptor( + SecurityExtension.SecurityComponents extensionComponents + ) { + final Map customByExtension = new HashMap<>(); + for (final SecurityExtension extension : securityExtensions) { + final CrossProjectRemoteServerTransportInterceptor custom = extension.getCustomRemoteServerTransportInterceptor( + extensionComponents + ); + if (custom != null) { + if (false == isInternalExtension(extension)) { + throw new IllegalStateException( + "The [" + + extension.extensionName() + + "] extension tried to install a custom CustomIndicesRequestRewriter. " + + "This functionality is not available to external extensions." + ); + } + customByExtension.put(extension.extensionName(), custom); + } + } + + if (customByExtension.isEmpty()) { + logger.debug( + "No custom implementation for [{}]. Falling-back to default implementation.", + CrossProjectRemoteServerTransportInterceptor.class.getCanonicalName() + ); + return new CrossProjectRemoteServerTransportInterceptor.Default(); + } else if (customByExtension.size() > 1) { + throw new IllegalStateException( + "Multiple extensions tried to install a custom CustomRemoteServerTransportInterceptor: " + customByExtension.keySet() + ); + } else { + final var byExtensionEntry = customByExtension.entrySet().iterator().next(); + final CrossProjectRemoteServerTransportInterceptor custom = byExtensionEntry.getValue(); + final String extensionName = byExtensionEntry.getKey(); + logger.debug( + "CustomRemoteServerTransportInterceptor implementation [{}] provided by extension [{}]", + custom.getClass().getCanonicalName(), + extensionName + ); + return custom; + } + } + + private CrossProjectTargetResolver createCrossProjectTargetResolver(SecurityExtension.SecurityComponents extensionComponents) { + final Map customByExtension = new HashMap<>(); + for (final SecurityExtension extension : securityExtensions) { + final CrossProjectTargetResolver custom = extension.getCrossProjectTargetResolver(extensionComponents); + if (custom != null) { + if (false == isInternalExtension(extension)) { + throw new IllegalStateException( + "The [" + + extension.extensionName() + + "] extension tried to install a custom CrossProjectTargetResolver. " + + "This functionality is not available to external extensions." + ); + } + customByExtension.put(extension.extensionName(), custom); + } + } + + if (customByExtension.isEmpty()) { + logger.debug( + "No custom implementation for [{}]. Falling-back to default implementation.", + CrossProjectTargetResolver.class.getCanonicalName() + ); + return new CrossProjectTargetResolver.Default(); + } else if (customByExtension.size() > 1) { + throw new IllegalStateException( + "Multiple extensions tried to install a custom CrossProjectTargetResolver: " + customByExtension.keySet() + ); + } else { + final var byExtensionEntry = customByExtension.entrySet().iterator().next(); + final CrossProjectTargetResolver custom = byExtensionEntry.getValue(); + final String extensionName = byExtensionEntry.getKey(); + logger.debug( + "CrossProjectTargetResolver implementation [{}] provided by extension [{}]", + custom.getClass().getCanonicalName(), + extensionName + ); + return custom; + } + } + private CustomApiKeyAuthenticator createCustomApiKeyAuthenticator(SecurityExtension.SecurityComponents extensionComponents) { final Map customApiKeyAuthenticatorByExtension = new HashMap<>(); for (final SecurityExtension extension : securityExtensions) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java index 16f67c9077311..cc2812bca70b9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java @@ -41,15 +41,23 @@ public class CrossClusterAccessAuthenticationService { private final ClusterService clusterService; private final ApiKeyService apiKeyService; private final AuthenticationService authenticationService; + // TODO hack hack hack + private final boolean skipTransportCheck; public CrossClusterAccessAuthenticationService( ClusterService clusterService, ApiKeyService apiKeyService, - AuthenticationService authenticationService + AuthenticationService authenticationService, + boolean skipTransportCheck ) { this.clusterService = clusterService; this.apiKeyService = apiKeyService; this.authenticationService = authenticationService; + this.skipTransportCheck = skipTransportCheck; + } + + public boolean skipTransportCheck() { + return skipTransportCheck; } public void authenticate(final String action, final TransportRequest request, final ActionListener listener) { 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 f7f0f48f1c0fe..a5471eb4a59f8 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 @@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.CrossProjectAwareRequest; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchRoleRestrictionException; import org.elasticsearch.ElasticsearchSecurityException; @@ -69,6 +70,7 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; +import org.elasticsearch.xpack.core.security.authz.CrossProjectTargetResolver; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RestrictedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; @@ -143,6 +145,7 @@ public class AuthorizationService { private final RestrictedIndices restrictedIndices; private final AuthorizationDenialMessages authorizationDenialMessages; private final ProjectResolver projectResolver; + private final CrossProjectTargetResolver crossProjectTargetResolver; private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; @@ -164,12 +167,13 @@ public AuthorizationService( OperatorPrivilegesService operatorPrivilegesService, RestrictedIndices restrictedIndices, AuthorizationDenialMessages authorizationDenialMessages, - ProjectResolver projectResolver + ProjectResolver projectResolver, + CrossProjectTargetResolver crossProjectTargetResolver ) { this.clusterService = clusterService; this.auditTrailService = auditTrailService; this.restrictedIndices = restrictedIndices; - this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService, resolver); + this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService, resolver, crossProjectTargetResolver); this.authcFailureHandler = authcFailureHandler; this.threadContext = threadPool.getThreadContext(); this.securityContext = new SecurityContext(settings, this.threadContext); @@ -190,6 +194,7 @@ public AuthorizationService( this.indicesAccessControlWrapper = new DlsFlsFeatureTrackingIndicesAccessControlWrapper(settings, licenseState); this.authorizationDenialMessages = authorizationDenialMessages; this.projectResolver = projectResolver; + this.crossProjectTargetResolver = crossProjectTargetResolver; } public void checkPrivileges( @@ -485,13 +490,20 @@ private void authorizeAction( })); } else if (isIndexAction(action)) { final ProjectMetadata projectMetadata = projectResolver.getProjectMetadata(clusterService.state()); + final CrossProjectTargetResolver.ResolvedProjects resolvedProjects = request instanceof CrossProjectAwareRequest + ? crossProjectTargetResolver.resolve(securityContext) + : CrossProjectTargetResolver.ResolvedProjects.VOID; assert projectMetadata != null; final AsyncSupplier resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>(() -> { if (request instanceof SearchRequest searchRequest && searchRequest.pointInTimeBuilder() != null) { - var resolvedIndices = indicesAndAliasesResolver.resolvePITIndices(searchRequest); + var resolvedIndices = indicesAndAliasesResolver.resolvePITIndices(searchRequest, resolvedProjects); return SubscribableListener.newSucceeded(resolvedIndices); } - final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.tryResolveWithoutWildcards(action, request); + final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.tryResolveWithoutWildcards( + action, + request, + resolvedProjects + ); if (resolvedIndices != null) { return SubscribableListener.newSucceeded(resolvedIndices); } else { @@ -502,7 +514,7 @@ private void authorizeAction( projectMetadata.getIndicesLookup(), ActionListener.wrap( authorizedIndices -> resolvedIndicesListener.onResponse( - indicesAndAliasesResolver.resolve(action, request, projectMetadata, authorizedIndices) + indicesAndAliasesResolver.resolve(action, request, projectMetadata, authorizedIndices, resolvedProjects) ), e -> { if (e instanceof InvalidIndexNameException 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 ff39fd587dc3a..c376a73afe3eb 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,7 @@ */ package org.elasticsearch.xpack.security.authz; +import org.elasticsearch.CrossProjectAwareRequest; import org.elasticsearch.action.AliasesRequest; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; @@ -31,11 +32,14 @@ import org.elasticsearch.core.Tuple; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; import org.elasticsearch.transport.NoSuchRemoteClusterException; import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.transport.RemoteConnectionStrategy; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.CrossProjectTargetResolver; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; @@ -53,16 +57,25 @@ import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER; -class IndicesAndAliasesResolver { +public class IndicesAndAliasesResolver { + + private static final Logger logger = LogManager.getLogger(IndicesAndAliasesResolver.class); private final IndexNameExpressionResolver nameExpressionResolver; private final IndexAbstractionResolver indexAbstractionResolver; private final RemoteClusterResolver remoteClusterResolver; + private final CrossProjectTargetResolver crossProjectTargetResolver; - IndicesAndAliasesResolver(Settings settings, ClusterService clusterService, IndexNameExpressionResolver resolver) { + IndicesAndAliasesResolver( + Settings settings, + ClusterService clusterService, + IndexNameExpressionResolver resolver, + CrossProjectTargetResolver crossProjectTargetResolver + ) { this.nameExpressionResolver = resolver; this.indexAbstractionResolver = new IndexAbstractionResolver(resolver); this.remoteClusterResolver = new RemoteClusterResolver(settings, clusterService.getClusterSettings()); + this.crossProjectTargetResolver = crossProjectTargetResolver; } /** @@ -103,12 +116,12 @@ class IndicesAndAliasesResolver { * resolving wildcards. *

*/ - 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) {