From 980723ef83556263ff2fe00fab0ae0d6af24e94a Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Tue, 24 Jun 2025 22:49:43 +0200
Subject: [PATCH 1/4] poc
---
.../org/elasticsearch/FlatIndicesRequest.java | 18 +++
.../action/search/SearchRequest.java | 12 +-
.../security/authz/AuthorizationEngine.java | 5 +
.../core/security/SerializationDemoTests.java | 136 ++++++++++++++++++
.../authz/IndicesAndAliasesResolver.java | 53 ++++++-
.../xpack/security/authz/RBACEngine.java | 14 +-
6 files changed, 235 insertions(+), 3 deletions(-)
create mode 100644 server/src/main/java/org/elasticsearch/FlatIndicesRequest.java
create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java
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..8e9785d3f82fa
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java
@@ -0,0 +1,18 @@
+/*
+ * 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 {
+ void indices(List indices);
+}
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..60b01bda53b28 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"));
@@ -853,4 +858,9 @@ public String toString() {
+ source
+ '}';
}
+
+ @Override
+ public void indices(List indices) {
+ indices(indices.toArray(Strings.EMPTY_ARRAY));
+ }
}
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/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java
new file mode 100644
index 0000000000000..caabd1d1ee0da
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.security;
+
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
+import org.elasticsearch.core.Nullable;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.ParseField;
+import org.elasticsearch.xcontent.ToXContent;
+import org.elasticsearch.xcontent.ToXContentObject;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xcontent.XContentType;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.TransportVersions.PARTIAL_DATA_DEMO;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public class SerializationDemoTests extends ESTestCase {
+
+ record SearchResult(boolean success, @Nullable List results, @Nullable List failures)
+ implements
+ Writeable,
+ // ToXContentFragment also exists
+ ToXContentObject {
+
+ private static final ConstructingObjectParser PARSER = buildParser();
+
+ @SuppressWarnings("unchecked")
+ private static ConstructingObjectParser buildParser() {
+ final ConstructingObjectParser parser = new ConstructingObjectParser<>(
+ "search_result",
+ true,
+ a -> new SearchResult((boolean) a[0], (List) a[1], (List) a[2])
+ );
+ parser.declareBoolean(constructorArg(), new ParseField("success"));
+ parser.declareStringArray(optionalConstructorArg(), new ParseField("results"));
+ parser.declareStringArray(optionalConstructorArg(), new ParseField("failures"));
+ return parser;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeBoolean(success);
+ out.writeOptionalCollection(results, StreamOutput::writeString);
+ // Elasticsearch supports rolling upgrades across 1 major version and within major versions.
+ // For example 7.17 needs to be able to communicate with 8.4 nodes, and 8.1 nodes need to be able to talk with 8.4 nodes.
+ // Serverless removed the notion of transport versions being tied cleanly to ES versions since we release to serverless
+ // every week and have rolling upgrades
+ if (out.getTransportVersion().onOrAfter(PARTIAL_DATA_DEMO)) {
+ out.writeOptionalCollection(failures, StreamOutput::writeString);
+ }
+ }
+
+ SearchResult(StreamInput input) throws IOException {
+ this(
+ input.readBoolean(),
+ input.readOptionalCollectionAsList(StreamInput::readString),
+ input.getTransportVersion().onOrAfter(PARTIAL_DATA_DEMO)
+ ? input.readOptionalCollectionAsList(StreamInput::readString)
+ : List.of()
+ );
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ var xcb = builder.startObject().field("success", success);
+ if (results != null) {
+ xcb = xcb.field("results", results);
+ }
+ if (failures != null) {
+ xcb = xcb.field("failures", failures);
+ }
+ return xcb.endObject();
+ }
+
+ public SearchResult fromXContent(XContentParser parser) {
+ return PARSER.apply(parser, null);
+ }
+
+ }
+
+ public void testRoundTripTransportSerialization() throws IOException {
+ var result = new SearchResult(true, List.of("hit1"), List.of());
+
+ try (var out = new BytesStreamOutput()) {
+ result.writeTo(out);
+ var received = new SearchResult(out.bytes().streamInput());
+
+ System.out.println("Original: " + result);
+ System.out.println("Received: " + received);
+ }
+ }
+
+ public void testToXContent() {
+ var result = new SearchResult(true, List.of("hit1", "hit2"), List.of("failure1"));
+
+ try (var builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
+ result.toXContent(builder, ToXContent.EMPTY_PARAMS);
+ builder.flush();
+ String json = Strings.toString(builder);
+ System.out.println("JSON Output: " + json);
+ // test from XContent
+ try (
+ var parser = XContentType.JSON.xContent()
+ .createParser(
+ NamedXContentRegistry.EMPTY,
+ LoggingDeprecationHandler.INSTANCE,
+ new ByteArrayInputStream(json.getBytes())
+ )
+ ) {
+ var parsedResult = result.fromXContent(parser);
+ System.out.println("Parsed Result: " + parsedResult);
+ }
+ } catch (IOException e) {
+ fail("Failed to convert to XContent: " + e.getMessage());
+ }
+ }
+
+}
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..f3573d8fb6518 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,52 @@ 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) {
+ rewrite(flatIndicesRequest, authorizedIndices);
+ }
+
return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices);
}
+ void rewrite(FlatIndicesRequest request, AuthorizationEngine.AuthorizedIndices authorizedIndices) {
+ var clusters = remoteClusterResolver.clusters();
+ logger.info("Clusters available for remote indices: {}", clusters);
+ // no remotes, nothing to rewrite...
+ if (clusters.isEmpty()) {
+ logger.info("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);
+ }
+ }
+
+ // TODO do not rewrite twice
+ List rewrittenIndices = new ArrayList<>(indices.length);
+ ResolvedIndices resolved = remoteClusterResolver.splitLocalAndRemoteIndexNames(indices);
+ for (var local : resolved.getLocal()) {
+ String rewritten = local;
+ for (var cluster : authorizedClusters) {
+ rewritten += "," + RemoteClusterAware.buildRemoteIndexName(cluster, local);
+ rewrittenIndices.add(rewritten);
+ }
+ logger.info("Rewrote [{}] to [{}]", local, rewritten);
+ }
+ if (resolved.getRemote().isEmpty() == false) {
+ rewrittenIndices.addAll(resolved.getRemote());
+ }
+ request.indices(rewrittenIndices);
+ // skipping mixed expressions, _local expressions and all that jazz
+ }
+
/**
* Attempt to resolve requested indices without expanding any wildcards.
* @return The {@link ResolvedIndices} or null if wildcard expansion must be performed.
@@ -569,5 +616,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..2ef0319e4729a 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,9 @@ 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
+ return Arrays.asList(role.names()).contains("remote_searcher");
});
}
@@ -1125,15 +1128,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 +1155,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);
+ }
}
}
From 4ff5e6eb8cbacedda35dc0a916a604624b823437 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Tue, 24 Jun 2025 22:50:15 +0200
Subject: [PATCH 2/4] poc
---
.../core/security/SerializationDemoTests.java | 136 ------------------
1 file changed, 136 deletions(-)
delete mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java
deleted file mode 100644
index caabd1d1ee0da..0000000000000
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/SerializationDemoTests.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * 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.common.Strings;
-import org.elasticsearch.common.io.stream.BytesStreamOutput;
-import org.elasticsearch.common.io.stream.StreamInput;
-import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.io.stream.Writeable;
-import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
-import org.elasticsearch.core.Nullable;
-import org.elasticsearch.test.ESTestCase;
-import org.elasticsearch.xcontent.ConstructingObjectParser;
-import org.elasticsearch.xcontent.NamedXContentRegistry;
-import org.elasticsearch.xcontent.ParseField;
-import org.elasticsearch.xcontent.ToXContent;
-import org.elasticsearch.xcontent.ToXContentObject;
-import org.elasticsearch.xcontent.XContentBuilder;
-import org.elasticsearch.xcontent.XContentParser;
-import org.elasticsearch.xcontent.XContentType;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.util.List;
-
-import static org.elasticsearch.TransportVersions.PARTIAL_DATA_DEMO;
-import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
-import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
-
-public class SerializationDemoTests extends ESTestCase {
-
- record SearchResult(boolean success, @Nullable List results, @Nullable List failures)
- implements
- Writeable,
- // ToXContentFragment also exists
- ToXContentObject {
-
- private static final ConstructingObjectParser PARSER = buildParser();
-
- @SuppressWarnings("unchecked")
- private static ConstructingObjectParser buildParser() {
- final ConstructingObjectParser parser = new ConstructingObjectParser<>(
- "search_result",
- true,
- a -> new SearchResult((boolean) a[0], (List) a[1], (List) a[2])
- );
- parser.declareBoolean(constructorArg(), new ParseField("success"));
- parser.declareStringArray(optionalConstructorArg(), new ParseField("results"));
- parser.declareStringArray(optionalConstructorArg(), new ParseField("failures"));
- return parser;
- }
-
- @Override
- public void writeTo(StreamOutput out) throws IOException {
- out.writeBoolean(success);
- out.writeOptionalCollection(results, StreamOutput::writeString);
- // Elasticsearch supports rolling upgrades across 1 major version and within major versions.
- // For example 7.17 needs to be able to communicate with 8.4 nodes, and 8.1 nodes need to be able to talk with 8.4 nodes.
- // Serverless removed the notion of transport versions being tied cleanly to ES versions since we release to serverless
- // every week and have rolling upgrades
- if (out.getTransportVersion().onOrAfter(PARTIAL_DATA_DEMO)) {
- out.writeOptionalCollection(failures, StreamOutput::writeString);
- }
- }
-
- SearchResult(StreamInput input) throws IOException {
- this(
- input.readBoolean(),
- input.readOptionalCollectionAsList(StreamInput::readString),
- input.getTransportVersion().onOrAfter(PARTIAL_DATA_DEMO)
- ? input.readOptionalCollectionAsList(StreamInput::readString)
- : List.of()
- );
- }
-
- @Override
- public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
- var xcb = builder.startObject().field("success", success);
- if (results != null) {
- xcb = xcb.field("results", results);
- }
- if (failures != null) {
- xcb = xcb.field("failures", failures);
- }
- return xcb.endObject();
- }
-
- public SearchResult fromXContent(XContentParser parser) {
- return PARSER.apply(parser, null);
- }
-
- }
-
- public void testRoundTripTransportSerialization() throws IOException {
- var result = new SearchResult(true, List.of("hit1"), List.of());
-
- try (var out = new BytesStreamOutput()) {
- result.writeTo(out);
- var received = new SearchResult(out.bytes().streamInput());
-
- System.out.println("Original: " + result);
- System.out.println("Received: " + received);
- }
- }
-
- public void testToXContent() {
- var result = new SearchResult(true, List.of("hit1", "hit2"), List.of("failure1"));
-
- try (var builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
- result.toXContent(builder, ToXContent.EMPTY_PARAMS);
- builder.flush();
- String json = Strings.toString(builder);
- System.out.println("JSON Output: " + json);
- // test from XContent
- try (
- var parser = XContentType.JSON.xContent()
- .createParser(
- NamedXContentRegistry.EMPTY,
- LoggingDeprecationHandler.INSTANCE,
- new ByteArrayInputStream(json.getBytes())
- )
- ) {
- var parsedResult = result.fromXContent(parser);
- System.out.println("Parsed Result: " + parsedResult);
- }
- } catch (IOException e) {
- fail("Failed to convert to XContent: " + e.getMessage());
- }
- }
-
-}
From 3df7a1df89671fa0df007c889e5dfbba9922b547 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 26 Jun 2025 13:45:37 +0200
Subject: [PATCH 3/4] Fix ups
---
.../java/org/elasticsearch/FlatIndicesRequest.java | 6 +++++-
.../elasticsearch/action/search/SearchRequest.java | 14 ++++++++++++--
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java b/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java
index 8e9785d3f82fa..2d4594c05c6f1 100644
--- a/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java
+++ b/server/src/main/java/org/elasticsearch/FlatIndicesRequest.java
@@ -14,5 +14,9 @@
import java.util.List;
public interface FlatIndicesRequest extends IndicesRequest {
- void indices(List indices);
+ 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 60b01bda53b28..cd5c0ad466bc7 100644
--- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java
@@ -75,6 +75,9 @@ public class SearchRequest extends LegacyActionRequest
private String[] indices = Strings.EMPTY_ARRAY;
+ @Nullable
+ private List indexExpressions;
+
@Nullable
private String routing;
@Nullable
@@ -860,7 +863,14 @@ public String toString() {
}
@Override
- public void indices(List indices) {
- indices(indices.toArray(Strings.EMPTY_ARRAY));
+ 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));
}
}
From c21a0a9d89845009cd722ee04e00be3a77956c29 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 26 Jun 2025 13:57:33 +0200
Subject: [PATCH 4/4] The missing commit
---
.../authz/IndicesAndAliasesResolver.java | 34 ++++++++++++-------
.../xpack/security/authz/RBACEngine.java | 5 +--
2 files changed, 25 insertions(+), 14 deletions(-)
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 f3573d8fb6518..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
@@ -129,7 +129,7 @@ ResolvedIndices resolve(
throw new IllegalStateException("Request [" + request + "] is not an Indices request, but should be.");
}
- if (request instanceof FlatIndicesRequest flatIndicesRequest) {
+ if (request instanceof FlatIndicesRequest flatIndicesRequest && flatIndicesRequest.requiresRewrite()) {
rewrite(flatIndicesRequest, authorizedIndices);
}
@@ -137,11 +137,13 @@ ResolvedIndices resolve(
}
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("Skipping...");
+ logger.info("No remote clusters linked, skipping...");
return;
}
@@ -156,22 +158,30 @@ void rewrite(FlatIndicesRequest request, AuthorizationEngine.AuthorizedIndices a
}
}
- // TODO do not rewrite twice
- List rewrittenIndices = new ArrayList<>(indices.length);
+ 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()) {
- String rewritten = local;
+ List rewritten = new ArrayList<>();
+ rewritten.add(local);
for (var cluster : authorizedClusters) {
- rewritten += "," + RemoteClusterAware.buildRemoteIndexName(cluster, local);
- rewrittenIndices.add(rewritten);
+ rewritten.add(RemoteClusterAware.buildRemoteIndexName(cluster, local));
+ indexExpressions.add(new FlatIndicesRequest.IndexExpression(local, rewritten));
}
logger.info("Rewrote [{}] to [{}]", local, rewritten);
}
- if (resolved.getRemote().isEmpty() == false) {
- rewrittenIndices.addAll(resolved.getRemote());
- }
- request.indices(rewrittenIndices);
- // skipping mixed expressions, _local expressions and all that jazz
+
+ request.indexExpressions(indexExpressions);
}
/**
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 2ef0319e4729a..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
@@ -999,8 +999,9 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole(
}
return predicate.test(indexAbstraction, selector);
}, name -> {
- // just some bogus predicate that lets us differentiate between roles
- return Arrays.asList(role.names()).contains("remote_searcher");
+ // 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");
});
}