Skip to content

Commit 69d77e1

Browse files
ES-12295 [S2D30] Painless/execute in CPS requires qualified index expression (elastic#138435)
This change add support for cross-project requests to Painless/execute endpoint. The painless/execute API is unique in that it works cross-cluster but it only allows you to query one index at a time. In contrast to other CPS-enabled endpoints all expressions should be interpreted as "canonical" like GET _settings or GET _mappings, but also "happens" to allow to specify a remote. So logs means _origin:logs. Origin project alias is also allowed, so if the origin project has the alias p1 p1:logs also means _origin:logs. Endpoint does not support project routing.
1 parent c696cdf commit 69d77e1

File tree

7 files changed

+247
-39
lines changed

7 files changed

+247
-39
lines changed

modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public List<RestHandler> getRestHandlers(
182182
Predicate<NodeFeature> clusterSupportsFeature
183183
) {
184184
List<RestHandler> handlers = new ArrayList<>();
185-
handlers.add(new PainlessExecuteAction.RestAction());
185+
handlers.add(new PainlessExecuteAction.RestAction(settings));
186186
handlers.add(new PainlessContextAction.RestAction());
187187
return handlers;
188188
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.elasticsearch.common.io.stream.StreamOutput;
4848
import org.elasticsearch.common.io.stream.Writeable;
4949
import org.elasticsearch.common.network.NetworkAddress;
50+
import org.elasticsearch.common.settings.Settings;
5051
import org.elasticsearch.common.util.concurrent.EsExecutors;
5152
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
5253
import org.elasticsearch.common.xcontent.XContentHelper;
@@ -94,6 +95,7 @@
9495
import org.elasticsearch.script.ScriptService;
9596
import org.elasticsearch.script.ScriptType;
9697
import org.elasticsearch.script.StringFieldScript;
98+
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
9799
import org.elasticsearch.search.lookup.SearchLookup;
98100
import org.elasticsearch.tasks.Task;
99101
import org.elasticsearch.threadpool.ThreadPool;
@@ -351,6 +353,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
351353
private final Script script;
352354
private final ScriptContext<?> context;
353355
private final ContextSetup contextSetup;
356+
private transient IndicesOptions indicesOptions;
354357

355358
static Request parse(XContentParser parser) throws IOException {
356359
return PARSER.parse(parser, null);
@@ -390,6 +393,14 @@ public ContextSetup getContextSetup() {
390393
return contextSetup;
391394
}
392395

396+
@Override
397+
public void markOriginOnly() {
398+
assert contextSetup != null
399+
: "Painless/execute request without context setup can't have index, this method shouldn't be called";
400+
// strip off cluster alias from the index in this request
401+
index(contextSetup.getIndex());
402+
}
403+
393404
@Override
394405
public ActionRequestValidationException validate() {
395406
ActionRequestValidationException validationException = null;
@@ -407,6 +418,20 @@ public ActionRequestValidationException validate() {
407418
return validationException;
408419
}
409420

421+
public void enableCrossProjectMode() {
422+
this.indicesOptions = IndicesOptions.builder(super.indicesOptions())
423+
.crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true))
424+
.build();
425+
}
426+
427+
@Override
428+
public IndicesOptions indicesOptions() {
429+
if (indicesOptions == null) {
430+
return super.indicesOptions();
431+
}
432+
return indicesOptions;
433+
}
434+
410435
@Override
411436
public void writeTo(StreamOutput out) throws IOException {
412437
super.writeTo(out);
@@ -532,7 +557,11 @@ public TransportAction(
532557

533558
@Override
534559
protected void doExecute(Task task, Request request, ActionListener<Response> listener) {
535-
if (request.getContextSetup() == null || request.getContextSetup().getClusterAlias() == null) {
560+
// By this point index resolution has completed, and we should not try to resolve indices for child requests
561+
// to avoid the second attempt of project authorization in CPS
562+
request.indicesOptions = null;
563+
564+
if (isLocalIndex(request)) {
536565
super.doExecute(task, request, listener);
537566
} else {
538567
// forward to remote cluster after stripping off the clusterAlias from the index expression
@@ -547,10 +576,19 @@ protected void doExecute(Task task, Request request, ActionListener<Response> li
547576
}
548577
}
549578

579+
private boolean isLocalIndex(Request request) {
580+
if (request.getContextSetup() == null) {
581+
return true;
582+
}
583+
String index = request.index();
584+
return index == null || RemoteClusterAware.isRemoteIndexName(index) == false;
585+
}
586+
550587
// Visible for testing
551588
static void removeClusterAliasFromIndexExpression(Request request) {
552-
if (request.index() != null) {
553-
String[] split = RemoteClusterAware.splitIndexName(request.index());
589+
String index = request.index();
590+
if (index != null) {
591+
String[] split = RemoteClusterAware.splitIndexName(index);
554592
if (split[0] != null) {
555593
/*
556594
* if the cluster alias is null and the index field has a clusterAlias (clusterAlias:index notation)
@@ -854,6 +892,11 @@ private static Response prepareRamIndex(
854892

855893
@ServerlessScope(Scope.PUBLIC)
856894
public static class RestAction extends BaseRestHandler {
895+
private final CrossProjectModeDecider crossProjectModeDecider;
896+
897+
public RestAction(Settings settings) {
898+
this.crossProjectModeDecider = new CrossProjectModeDecider(settings);
899+
}
857900

858901
@Override
859902
public List<Route> routes() {
@@ -868,6 +911,10 @@ public String getName() {
868911
@Override
869912
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
870913
final Request request = Request.parse(restRequest.contentOrSourceParamParser());
914+
if (crossProjectModeDecider.crossProjectEnabled()) {
915+
request.enableCrossProjectMode();
916+
}
917+
871918
return channel -> client.executeLocally(INSTANCE, request, new RestToXContentListener<>(channel));
872919
}
873920
}

modules/lang-painless/src/test/java/org/elasticsearch/painless/action/PainlessExecuteApiTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,12 @@ public void testRemoveClusterAliasFromIndexExpression() {
561561
}
562562
}
563563

564+
public void testMarkOriginOnly() {
565+
PainlessExecuteAction.Request request = createRequest("p1:blogs");
566+
request.markOriginOnly();
567+
assertThat(request.index(), equalTo("blogs"));
568+
}
569+
564570
private PainlessExecuteAction.Request createRequest(String indexExpression) {
565571
return new PainlessExecuteAction.Request(
566572
new Script("100.0 / 1000.0"),

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ interface SingleIndexNoWildcards extends IndicesRequest, CrossProjectCandidate {
118118
default boolean allowsRemoteIndices() {
119119
return true;
120120
}
121+
122+
/**
123+
* Determines whether the request type allows cross-project processing. Cross-project processing entails cross-project search
124+
* index resolution and error handling. Note: this method only determines in the request _supports_ cross-project.
125+
* Whether cross-project processing is actually performed is determined by {@link IndicesOptions}.
126+
*/
127+
default boolean allowsCrossProject() {
128+
return true;
129+
}
130+
131+
/**
132+
* Marks request local. Local requests should be processed on the same cluster, even if they have cluster-alias prefix.
133+
*/
134+
void markOriginOnly();
121135
}
122136

123137
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ private AsyncSupplier<ResolvedIndices> makeResolvedIndicesAsyncSupplier(
573573
var resolvedIndices = indicesAndAliasesResolver.resolvePITIndices(searchRequest);
574574
return SubscribableListener.newSucceeded(resolvedIndices);
575575
}
576-
final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.tryResolveWithoutWildcards(action, request);
576+
final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.tryResolveWithoutWildcards(action, request, targetProjects);
577577
if (resolvedIndices != null) {
578578
return SubscribableListener.newSucceeded(resolvedIndices);
579579
} else {

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

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.elasticsearch.index.IndexNotFoundException;
3636
import org.elasticsearch.search.crossproject.CrossProjectIndexExpressionsRewriter;
3737
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
38+
import org.elasticsearch.search.crossproject.ProjectRoutingInfo;
3839
import org.elasticsearch.search.crossproject.ProjectRoutingResolver;
3940
import org.elasticsearch.search.crossproject.TargetProjects;
4041
import org.elasticsearch.transport.LinkedProjectConfig;
@@ -57,6 +58,7 @@
5758
import java.util.SortedMap;
5859
import java.util.concurrent.CopyOnWriteArraySet;
5960
import java.util.function.BiPredicate;
61+
import java.util.stream.Collectors;
6062

6163
import static org.elasticsearch.search.crossproject.CrossProjectIndexResolutionValidator.indicesOptionsForCrossProjectFanout;
6264
import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER;
@@ -152,7 +154,7 @@ ResolvedIndices resolve(
152154
* @return The {@link ResolvedIndices} or null if wildcard expansion must be performed.
153155
*/
154156
@Nullable
155-
ResolvedIndices tryResolveWithoutWildcards(String action, TransportRequest transportRequest) {
157+
ResolvedIndices tryResolveWithoutWildcards(String action, TransportRequest transportRequest, TargetProjects authorizedProjects) {
156158
// We only take care of IndicesRequest
157159
if (false == transportRequest instanceof IndicesRequest) {
158160
return null;
@@ -162,7 +164,7 @@ ResolvedIndices tryResolveWithoutWildcards(String action, TransportRequest trans
162164
return null;
163165
}
164166
// It's safe to cast IndicesRequest since the above test guarantees it
165-
return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest);
167+
return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest, authorizedProjects);
166168
}
167169

168170
boolean resolvesCrossProject(TransportRequest request) {
@@ -183,6 +185,14 @@ private static boolean requiresWildcardExpansion(IndicesRequest indicesRequest)
183185
}
184186

185187
ResolvedIndices resolveIndicesAndAliasesWithoutWildcards(String action, IndicesRequest indicesRequest) {
188+
return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest, TargetProjects.LOCAL_ONLY_FOR_CPS_DISABLED);
189+
}
190+
191+
ResolvedIndices resolveIndicesAndAliasesWithoutWildcards(
192+
String action,
193+
IndicesRequest indicesRequest,
194+
TargetProjects authorizedProjects
195+
) {
186196
assert false == requiresWildcardExpansion(indicesRequest) : "request must not require wildcard expansion";
187197
final String[] indices = indicesRequest.indices();
188198
if (indices == null || indices.length == 0) {
@@ -205,17 +215,17 @@ ResolvedIndices resolveIndicesAndAliasesWithoutWildcards(String action, IndicesR
205215
}
206216

207217
final ResolvedIndices split;
208-
if (indicesRequest instanceof IndicesRequest.SingleIndexNoWildcards single && single.allowsRemoteIndices()) {
209-
split = remoteClusterResolver.splitLocalAndRemoteIndexNames(indicesRequest.indices());
210-
// all indices can come back empty when the remote index expression included a cluster alias with a wildcard
211-
// and no remote clusters are configured that match it
212-
if (split.getLocal().isEmpty() && split.getRemote().isEmpty()) {
213-
for (String indexExpression : indices) {
214-
String[] clusterAndIndex = RemoteClusterAware.splitIndexName(indexExpression);
215-
if (clusterAndIndex[0] != null && clusterAndIndex[0].contains("*")) {
216-
throw new NoSuchRemoteClusterException(clusterAndIndex[0]);
217-
}
218+
if (indicesRequest instanceof IndicesRequest.SingleIndexNoWildcards single
219+
&& (single.allowsRemoteIndices() || single.allowsCrossProject())) {
220+
assert indices.length == 1 : "SingleIndexNoWildcards request must have exactly one index";
221+
222+
if (crossProjectModeDecider.resolvesCrossProject(single)) {
223+
split = remoteClusterResolver.determineLocalOrRemoteIndexCrossProject(authorizedProjects, indices[0]);
224+
if (split.getLocal().isEmpty() == false) {
225+
single.markOriginOnly();
218226
}
227+
} else {
228+
split = remoteClusterResolver.determineLocalOrRemoteIndex(indices[0]);
219229
}
220230
} else {
221231
split = new ResolvedIndices(Arrays.asList(indicesRequest.indices()), List.of());
@@ -501,7 +511,7 @@ ResolvedIndices resolveIndicesAndAliases(
501511
// That's why an assertion error is triggered here so that we can catch the erroneous usage in testing.
502512
// But we still delegate in production to avoid our (potential) programing error becoming an end-user problem.
503513
assert false : "Request [" + indicesRequest + "] is not a replaceable request, but should be.";
504-
return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest);
514+
return resolveIndicesAndAliasesWithoutWildcards(action, indicesRequest, authorizedProjects);
505515
}
506516

507517
if (indicesRequest instanceof AliasesRequest aliasesRequest) {
@@ -714,5 +724,40 @@ ResolvedIndices splitLocalAndRemoteIndexNames(String... indices) {
714724
.toList();
715725
return new ResolvedIndices(local == null ? List.of() : local, remote);
716726
}
727+
728+
ResolvedIndices determineLocalOrRemoteIndex(String indexExpression) {
729+
return determineLocalOrRemoteIndex(indexExpression, Collections.emptySet(), clusters);
730+
}
731+
732+
ResolvedIndices determineLocalOrRemoteIndexCrossProject(TargetProjects targetProjects, String indexExpression) {
733+
assert targetProjects.linkedProjects() != null : "CPS should be enabled to call this method";
734+
735+
Set<String> linkedAliases = targetProjects.linkedProjects()
736+
.stream()
737+
.map(ProjectRoutingInfo::projectAlias)
738+
.collect(Collectors.toSet());
739+
740+
ProjectRoutingInfo originRoutingInfo = targetProjects.originProject();
741+
Set<String> originAliases;
742+
if (originRoutingInfo != null) {
743+
originAliases = Set.of(originRoutingInfo.projectAlias(), ProjectRoutingResolver.ORIGIN);
744+
} else {
745+
originAliases = Collections.emptySet();
746+
}
747+
return determineLocalOrRemoteIndex(indexExpression, originAliases, linkedAliases);
748+
}
749+
750+
private ResolvedIndices determineLocalOrRemoteIndex(String indexExpression, Set<String> originAliases, Set<String> remoteAliases) {
751+
String[] split = RemoteClusterAware.splitIndexName(indexExpression);
752+
String clusterAlias = split[0];
753+
if (clusterAlias == null || originAliases.contains(clusterAlias)) {
754+
return new ResolvedIndices(List.of(split[1]), List.of());
755+
} else {
756+
if (remoteAliases.contains(clusterAlias) == false) {
757+
throw new NoSuchRemoteClusterException(clusterAlias);
758+
}
759+
return new ResolvedIndices(List.of(), List.of(indexExpression));
760+
}
761+
}
717762
}
718763
}

0 commit comments

Comments
 (0)