Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.action;

import org.elasticsearch.core.Nullable;

import java.util.List;

public interface AuthorizedProjectsSupplier {
AuthorizedProjects get();

class Default implements AuthorizedProjectsSupplier {
@Override
public AuthorizedProjects get() {
return AuthorizedProjects.NOT_CROSS_PROJECT;
}
}

record AuthorizedProjects(@Nullable String origin, List<String> projects) {
public static AuthorizedProjects NOT_CROSS_PROJECT = new AuthorizedProjects(null, List.of());

public boolean isOriginOnly() {
return origin != null && projects.isEmpty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.action;

import org.elasticsearch.ResourceNotFoundException;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.transport.RemoteClusterAware;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.elasticsearch.transport.RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY;
import static org.elasticsearch.transport.RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR;

public class CrossProjectResolverUtils {
private static final Logger logger = LogManager.getLogger(CrossProjectResolverUtils.class);
private static final String WILDCARD = "*";
private static final String MATCH_ALL = "_ALL";
private static final String EXCLUSION = "-";

public static void maybeRewriteCrossProjectResolvableRequest(
RemoteClusterAware remoteClusterAware,
AuthorizedProjectsSupplier.AuthorizedProjects targetProjects,
IndicesRequest.CrossProjectReplaceable request
) throws ResourceNotFoundException {
if (targetProjects == AuthorizedProjectsSupplier.AuthorizedProjects.NOT_CROSS_PROJECT) {
logger.info("Cross-project search is disabled or not applicable, skipping request [{}]...", request);
return;
}

if (targetProjects.isOriginOnly()) {
logger.info("Cross-project search is only for the origin project [{}], skipping rewrite...", targetProjects.origin());
return;
}

if (targetProjects.projects().isEmpty()) {
throw new ResourceNotFoundException("no target projects for cross-project search request");
}

String[] indices = request.indices();
logger.info("Rewriting indices for CPS [{}]", Arrays.toString(indices));

if (indices.length == 0 || WILDCARD.equals(indices[0]) || MATCH_ALL.equalsIgnoreCase(indices[0])) {
// handling of match all cases
indices = new String[] { WILDCARD };
}
boolean atLeastOneResourceWasFound = true;
Map<String, List<String>> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
for (String indexExpression : indices) {
// TODO We will need to handle exclusions here. For now we are throwing instead if we see an exclusion.
if (EXCLUSION.equals(indexExpression)) {
throw new IllegalArgumentException(
"Exclusions are not currently supported but was found in the expression [" + indexExpression + "]"
);
}
boolean isQualified = isQualifiedIndexExpression(indexExpression);
if (isQualified) {
// TODO handle empty case here -- empty means "search all" in ES which is _not_ what we want
List<String> canonicalExpressions = rewriteQualified(indexExpression, targetProjects, remoteClusterAware);
// could fail early here in ignore_unavailable and allow_no_indices strict mode if things are empty
canonicalExpressionsMap.put(indexExpression, canonicalExpressions);
if (canonicalExpressions.isEmpty() == false) {
atLeastOneResourceWasFound = false;
}
logger.info("Rewrote qualified expression [{}] to [{}]", indexExpression, canonicalExpressions);
} else {
atLeastOneResourceWasFound = false;
// un-qualified expression, i.e. flat-world
List<String> canonicalExpressions = rewriteUnqualified(indexExpression, targetProjects.projects());
canonicalExpressionsMap.put(indexExpression, canonicalExpressions);
logger.info("Rewrote unqualified expression [{}] to [{}]", indexExpression, canonicalExpressions);
}
}
if (atLeastOneResourceWasFound) {
// Do we want to throw in this case?
throw new ResourceNotFoundException("no target projects for cross-project search request");
}
request.setCanonicalExpressions(canonicalExpressionsMap);
}

private static List<String> rewriteUnqualified(String indexExpression, List<String> projects) {
List<String> canonicalExpressions = new ArrayList<>();
canonicalExpressions.add(indexExpression);
for (String targetProject : projects) {
canonicalExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProject, indexExpression));
}
return canonicalExpressions;
}

private static List<String> rewriteQualified(
String indicesExpressions,
AuthorizedProjectsSupplier.AuthorizedProjects targetProjects,
RemoteClusterAware remoteClusterAware
) {
String[] splitExpression = RemoteClusterAware.splitIndexName(indicesExpressions);
if (targetProjects.origin() != null && targetProjects.origin().equals(splitExpression[0])) {
// handling special case where we have a qualified expression like: _origin:indexName
return List.of(splitExpression[1]);
}
final Map<String, List<String>> map = remoteClusterAware.groupClusterIndices(
Set.copyOf(targetProjects.projects()),
new String[] { indicesExpressions }
);
final List<String> local = map.remove(LOCAL_CLUSTER_GROUP_KEY);
final List<String> remote = map.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(v -> e.getKey() + REMOTE_CLUSTER_INDEX_SEPARATOR + v))
.toList();
assert local == null || local.isEmpty() : "local indices should not be present in the map, but were: " + local;
if (WILDCARD.equals(splitExpression[0])) {
// handing of special case where the original expression was: *:indexName that is a
// qualified expression that includes the origin cluster and all linked projects.
List<String> remoteIncludingOrigin = new ArrayList<>(remote.size() + 1);
remoteIncludingOrigin.addAll(remote);
remoteIncludingOrigin.add(splitExpression[1]);
return remoteIncludingOrigin;
}
return remote;
}

public static boolean isQualifiedIndexExpression(String indexExpression) {
return RemoteClusterAware.isRemoteIndexName(indexExpression);
}
}
35 changes: 35 additions & 0 deletions server/src/main/java/org/elasticsearch/action/IndicesRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
package org.elasticsearch.action;

import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.index.shard.ShardId;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Needs to be implemented by all {@link org.elasticsearch.action.ActionRequest} subclasses that relate to
Expand Down Expand Up @@ -62,6 +66,37 @@ interface Replaceable extends IndicesRequest {
default boolean allowsRemoteIndices() {
return false;
}

default void setCanonicalExpressions(@Nullable Map<String, List<String>> canonicalExpressions) {
if (false == storeCanonicalExpressions()) {
assert false : "setCanonicalExpressions should not be called when storeCanonicalExpressions is false";
throw new IllegalStateException("setCanonicalExpressions should not be called when storeCanonicalExpressions is false");
}
}

default Map<String, List<String>> getCanonicalExpressions() {
if (false == storeCanonicalExpressions()) {
assert false : "getCanonicalExpressions should not be called when storeCanonicalExpressions is false";
throw new IllegalStateException("getCanonicalExpressions should not be called when storeCanonicalExpressions is false");
}
return new LinkedHashMap<>();
}

default boolean storeCanonicalExpressions() {
return false;
}
}

interface CrossProjectReplaceable extends Replaceable {
@Override
default boolean allowsRemoteIndices() {
return true;
}

@Override
default boolean storeCanonicalExpressions() {
return true;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public static String[] splitIndexName(String indexExpression) {
*
* @return a map of grouped remote and local indices
*/
protected Map<String, List<String>> groupClusterIndices(Set<String> remoteClusterNames, String[] requestIndices) {
public Map<String, List<String>> groupClusterIndices(Set<String> remoteClusterNames, String[] requestIndices) {
Map<String, List<String>> perClusterIndices = new HashMap<>();
Set<String> clustersToRemove = new HashSet<>();
for (String index : requestIndices) {
Expand Down
Loading