diff --git a/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java b/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java
new file mode 100644
index 0000000000000..2d4594c05c6f1
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/FlatIndicesRequest.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", 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 java.util.List;
+
+public interface FlatIndicesRequest extends IndicesRequest {
+ boolean requiresRewrite();
+
+ void indexExpressions(List indexExpressions);
+
+ record IndexExpression(String original, List rewritten) {}
+}
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..cd5c0ad466bc7 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.FlatIndicesRequest;
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
+ FlatIndicesRequest,
+ IndicesRequest.Replaceable,
+ Rewriteable {
public static final ToXContent.Params FORMAT_PARAMS = new ToXContent.MapParams(Collections.singletonMap("pretty", "false"));
@@ -70,6 +75,9 @@ public class SearchRequest extends LegacyActionRequest implements IndicesRequest
private String[] indices = Strings.EMPTY_ARRAY;
+ @Nullable
+ private List indexExpressions;
+
@Nullable
private String routing;
@Nullable
@@ -853,4 +861,16 @@ public String toString() {
+ source
+ '}';
}
+
+ @Override
+ public boolean requiresRewrite() {
+ return indexExpressions == null;
+ }
+
+ @Override
+ public void indexExpressions(List indexExpressions) {
+ assert requiresRewrite();
+ this.indexExpressions = indexExpressions;
+ indices(indexExpressions.stream().flatMap(indexExpression -> indexExpression.rewritten().stream()).toArray(String[]::new));
+ }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java
index 2c831645d0e69..e2ffcf7480381 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java
@@ -299,6 +299,11 @@ interface AuthorizedIndices {
* Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist.
*/
boolean check(String name, IndexComponentSelector selector);
+
+ // Does not belong here
+ default boolean checkProject(String projectId) {
+ return false;
+ }
}
/**
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..e895c8be3ce69 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,9 @@
*/
package org.elasticsearch.xpack.security.authz;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.FlatIndicesRequest;
import org.elasticsearch.action.AliasesRequest;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
@@ -55,6 +58,8 @@
class IndicesAndAliasesResolver {
+ private static final Logger logger = LogManager.getLogger(IndicesAndAliasesResolver.class);
+
private final IndexNameExpressionResolver nameExpressionResolver;
private final IndexAbstractionResolver indexAbstractionResolver;
private final RemoteClusterResolver remoteClusterResolver;
@@ -103,7 +108,6 @@ class IndicesAndAliasesResolver {
* resolving wildcards.
*
*/
-
ResolvedIndices resolve(
String action,
TransportRequest request,
@@ -124,9 +128,62 @@ ResolvedIndices resolve(
if (request instanceof IndicesRequest == false) {
throw new IllegalStateException("Request [" + request + "] is not an Indices request, but should be.");
}
+
+ if (request instanceof FlatIndicesRequest flatIndicesRequest && flatIndicesRequest.requiresRewrite()) {
+ rewrite(flatIndicesRequest, authorizedIndices);
+ }
+
return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices);
}
+ void rewrite(FlatIndicesRequest request, AuthorizationEngine.AuthorizedIndices authorizedIndices) {
+ assert request.requiresRewrite();
+
+ var clusters = remoteClusterResolver.clusters();
+ logger.info("Clusters available for remote indices: {}", clusters);
+ // no remotes, nothing to rewrite...
+ if (clusters.isEmpty()) {
+ logger.info("No remote clusters linked, skipping...");
+ return;
+ }
+
+ var indices = request.indices();
+ // empty indices actually means search everything so would need to also rewrite that
+
+ var authorizedClusters = new HashSet();
+ for (var cluster : clusters) {
+ if (authorizedIndices.checkProject(cluster)) {
+ logger.info("Remote cluster [{}] authorized", cluster);
+ authorizedClusters.add(cluster);
+ }
+ }
+
+ if (authorizedClusters.isEmpty()) {
+ logger.info("No remote clusters authorized, skipping...");
+ return;
+ }
+
+ ResolvedIndices resolved = remoteClusterResolver.splitLocalAndRemoteIndexNames(indices);
+ // 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 indexExpressions = new ArrayList<>(indices.length);
+ for (var local : resolved.getLocal()) {
+ List rewritten = new ArrayList<>();
+ rewritten.add(local);
+ for (var cluster : authorizedClusters) {
+ rewritten.add(RemoteClusterAware.buildRemoteIndexName(cluster, local));
+ indexExpressions.add(new FlatIndicesRequest.IndexExpression(local, rewritten));
+ }
+ logger.info("Rewrote [{}] to [{}]", local, rewritten);
+ }
+
+ request.indexExpressions(indexExpressions);
+ }
+
/**
* Attempt to resolve requested indices without expanding any wildcards.
* @return The {@link ResolvedIndices} or null if wildcard expansion must be performed.
@@ -569,5 +626,9 @@ ResolvedIndices splitLocalAndRemoteIndexNames(String... indices) {
.toList();
return new ResolvedIndices(local == null ? List.of() : local, remote);
}
+
+ Set clusters() {
+ return Collections.unmodifiableSet(clusters);
+ }
}
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java
index 1b99bd6888c4f..9c6b8c26ae312 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java
@@ -998,6 +998,10 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole(
} // we don't support granting access to a backing index with a failure selector via the parent data stream
}
return predicate.test(indexAbstraction, selector);
+ }, name -> {
+ // just some bogus predicate that lets us differentiate between roles, not at all
+ // how this will work in the end
+ return Arrays.asList(role.names()).contains("_es_test_root");
});
}
@@ -1125,15 +1129,18 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn
private final CachedSupplier> authorizedAndAvailableDataResources;
private final CachedSupplier> authorizedAndAvailableFailuresResources;
private final BiPredicate isAuthorizedPredicate;
+ private final Predicate projectPredicate;
AuthorizedIndices(
Supplier> authorizedAndAvailableDataResources,
Supplier> authorizedAndAvailableFailuresResources,
- BiPredicate isAuthorizedPredicate
+ BiPredicate isAuthorizedPredicate,
+ Predicate projectPredicate
) {
this.authorizedAndAvailableDataResources = CachedSupplier.wrap(authorizedAndAvailableDataResources);
this.authorizedAndAvailableFailuresResources = CachedSupplier.wrap(authorizedAndAvailableFailuresResources);
this.isAuthorizedPredicate = Objects.requireNonNull(isAuthorizedPredicate);
+ this.projectPredicate = projectPredicate;
}
@Override
@@ -1149,5 +1156,11 @@ public boolean check(String name, IndexComponentSelector selector) {
Objects.requireNonNull(selector, "must specify a selector for authorization check");
return isAuthorizedPredicate.test(name, selector);
}
+
+ @Override
+ public boolean checkProject(String name) {
+ Objects.requireNonNull(name, "must specify a project name for authorization check");
+ return projectPredicate.test(name);
+ }
}
}