Skip to content

Commit 980723e

Browse files
committed
poc
1 parent fb30e59 commit 980723e

File tree

6 files changed

+235
-3
lines changed

6 files changed

+235
-3
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch;
11+
12+
import org.elasticsearch.action.IndicesRequest;
13+
14+
import java.util.List;
15+
16+
public interface FlatIndicesRequest extends IndicesRequest {
17+
void indices(List<String> indices);
18+
}

server/src/main/java/org/elasticsearch/action/search/SearchRequest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.action.search;
1111

12+
import org.elasticsearch.FlatIndicesRequest;
1213
import org.elasticsearch.TransportVersions;
1314
import org.elasticsearch.Version;
1415
import org.elasticsearch.action.ActionRequestValidationException;
@@ -53,7 +54,11 @@
5354
* @see Client#search(SearchRequest)
5455
* @see SearchResponse
5556
*/
56-
public class SearchRequest extends LegacyActionRequest implements IndicesRequest.Replaceable, Rewriteable<SearchRequest> {
57+
public class SearchRequest extends LegacyActionRequest
58+
implements
59+
FlatIndicesRequest,
60+
IndicesRequest.Replaceable,
61+
Rewriteable<SearchRequest> {
5762

5863
public static final ToXContent.Params FORMAT_PARAMS = new ToXContent.MapParams(Collections.singletonMap("pretty", "false"));
5964

@@ -853,4 +858,9 @@ public String toString() {
853858
+ source
854859
+ '}';
855860
}
861+
862+
@Override
863+
public void indices(List<String> indices) {
864+
indices(indices.toArray(Strings.EMPTY_ARRAY));
865+
}
856866
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ interface AuthorizedIndices {
299299
* Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist.
300300
*/
301301
boolean check(String name, IndexComponentSelector selector);
302+
303+
// Does not belong here
304+
default boolean checkProject(String projectId) {
305+
return false;
306+
}
302307
}
303308

304309
/**
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security;
9+
10+
import org.elasticsearch.common.Strings;
11+
import org.elasticsearch.common.io.stream.BytesStreamOutput;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.common.io.stream.Writeable;
15+
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
16+
import org.elasticsearch.core.Nullable;
17+
import org.elasticsearch.test.ESTestCase;
18+
import org.elasticsearch.xcontent.ConstructingObjectParser;
19+
import org.elasticsearch.xcontent.NamedXContentRegistry;
20+
import org.elasticsearch.xcontent.ParseField;
21+
import org.elasticsearch.xcontent.ToXContent;
22+
import org.elasticsearch.xcontent.ToXContentObject;
23+
import org.elasticsearch.xcontent.XContentBuilder;
24+
import org.elasticsearch.xcontent.XContentParser;
25+
import org.elasticsearch.xcontent.XContentType;
26+
27+
import java.io.ByteArrayInputStream;
28+
import java.io.IOException;
29+
import java.util.List;
30+
31+
import static org.elasticsearch.TransportVersions.PARTIAL_DATA_DEMO;
32+
import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
33+
import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
34+
35+
public class SerializationDemoTests extends ESTestCase {
36+
37+
record SearchResult(boolean success, @Nullable List<String> results, @Nullable List<String> failures)
38+
implements
39+
Writeable,
40+
// ToXContentFragment also exists
41+
ToXContentObject {
42+
43+
private static final ConstructingObjectParser<SearchResult, Void> PARSER = buildParser();
44+
45+
@SuppressWarnings("unchecked")
46+
private static ConstructingObjectParser<SearchResult, Void> buildParser() {
47+
final ConstructingObjectParser<SearchResult, Void> parser = new ConstructingObjectParser<>(
48+
"search_result",
49+
true,
50+
a -> new SearchResult((boolean) a[0], (List<String>) a[1], (List<String>) a[2])
51+
);
52+
parser.declareBoolean(constructorArg(), new ParseField("success"));
53+
parser.declareStringArray(optionalConstructorArg(), new ParseField("results"));
54+
parser.declareStringArray(optionalConstructorArg(), new ParseField("failures"));
55+
return parser;
56+
}
57+
58+
@Override
59+
public void writeTo(StreamOutput out) throws IOException {
60+
out.writeBoolean(success);
61+
out.writeOptionalCollection(results, StreamOutput::writeString);
62+
// Elasticsearch supports rolling upgrades across 1 major version and within major versions.
63+
// 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.
64+
// Serverless removed the notion of transport versions being tied cleanly to ES versions since we release to serverless
65+
// every week and have rolling upgrades
66+
if (out.getTransportVersion().onOrAfter(PARTIAL_DATA_DEMO)) {
67+
out.writeOptionalCollection(failures, StreamOutput::writeString);
68+
}
69+
}
70+
71+
SearchResult(StreamInput input) throws IOException {
72+
this(
73+
input.readBoolean(),
74+
input.readOptionalCollectionAsList(StreamInput::readString),
75+
input.getTransportVersion().onOrAfter(PARTIAL_DATA_DEMO)
76+
? input.readOptionalCollectionAsList(StreamInput::readString)
77+
: List.of()
78+
);
79+
}
80+
81+
@Override
82+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
83+
var xcb = builder.startObject().field("success", success);
84+
if (results != null) {
85+
xcb = xcb.field("results", results);
86+
}
87+
if (failures != null) {
88+
xcb = xcb.field("failures", failures);
89+
}
90+
return xcb.endObject();
91+
}
92+
93+
public SearchResult fromXContent(XContentParser parser) {
94+
return PARSER.apply(parser, null);
95+
}
96+
97+
}
98+
99+
public void testRoundTripTransportSerialization() throws IOException {
100+
var result = new SearchResult(true, List.of("hit1"), List.of());
101+
102+
try (var out = new BytesStreamOutput()) {
103+
result.writeTo(out);
104+
var received = new SearchResult(out.bytes().streamInput());
105+
106+
System.out.println("Original: " + result);
107+
System.out.println("Received: " + received);
108+
}
109+
}
110+
111+
public void testToXContent() {
112+
var result = new SearchResult(true, List.of("hit1", "hit2"), List.of("failure1"));
113+
114+
try (var builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
115+
result.toXContent(builder, ToXContent.EMPTY_PARAMS);
116+
builder.flush();
117+
String json = Strings.toString(builder);
118+
System.out.println("JSON Output: " + json);
119+
// test from XContent
120+
try (
121+
var parser = XContentType.JSON.xContent()
122+
.createParser(
123+
NamedXContentRegistry.EMPTY,
124+
LoggingDeprecationHandler.INSTANCE,
125+
new ByteArrayInputStream(json.getBytes())
126+
)
127+
) {
128+
var parsedResult = result.fromXContent(parser);
129+
System.out.println("Parsed Result: " + parsedResult);
130+
}
131+
} catch (IOException e) {
132+
fail("Failed to convert to XContent: " + e.getMessage());
133+
}
134+
}
135+
136+
}

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
*/
77
package org.elasticsearch.xpack.security.authz;
88

9+
import org.apache.logging.log4j.LogManager;
10+
import org.apache.logging.log4j.Logger;
11+
import org.elasticsearch.FlatIndicesRequest;
912
import org.elasticsearch.action.AliasesRequest;
1013
import org.elasticsearch.action.IndicesRequest;
1114
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
@@ -55,6 +58,8 @@
5558

5659
class IndicesAndAliasesResolver {
5760

61+
private static final Logger logger = LogManager.getLogger(IndicesAndAliasesResolver.class);
62+
5863
private final IndexNameExpressionResolver nameExpressionResolver;
5964
private final IndexAbstractionResolver indexAbstractionResolver;
6065
private final RemoteClusterResolver remoteClusterResolver;
@@ -103,7 +108,6 @@ class IndicesAndAliasesResolver {
103108
* resolving wildcards.
104109
* </p>
105110
*/
106-
107111
ResolvedIndices resolve(
108112
String action,
109113
TransportRequest request,
@@ -124,9 +128,52 @@ ResolvedIndices resolve(
124128
if (request instanceof IndicesRequest == false) {
125129
throw new IllegalStateException("Request [" + request + "] is not an Indices request, but should be.");
126130
}
131+
132+
if (request instanceof FlatIndicesRequest flatIndicesRequest) {
133+
rewrite(flatIndicesRequest, authorizedIndices);
134+
}
135+
127136
return resolveIndicesAndAliases(action, (IndicesRequest) request, projectMetadata, authorizedIndices);
128137
}
129138

139+
void rewrite(FlatIndicesRequest request, AuthorizationEngine.AuthorizedIndices authorizedIndices) {
140+
var clusters = remoteClusterResolver.clusters();
141+
logger.info("Clusters available for remote indices: {}", clusters);
142+
// no remotes, nothing to rewrite...
143+
if (clusters.isEmpty()) {
144+
logger.info("Skipping...");
145+
return;
146+
}
147+
148+
var indices = request.indices();
149+
// empty indices actually means search everything so would need to also rewrite that
150+
151+
var authorizedClusters = new HashSet<String>();
152+
for (var cluster : clusters) {
153+
if (authorizedIndices.checkProject(cluster)) {
154+
logger.info("Remote cluster [{}] authorized", cluster);
155+
authorizedClusters.add(cluster);
156+
}
157+
}
158+
159+
// TODO do not rewrite twice
160+
List<String> rewrittenIndices = new ArrayList<>(indices.length);
161+
ResolvedIndices resolved = remoteClusterResolver.splitLocalAndRemoteIndexNames(indices);
162+
for (var local : resolved.getLocal()) {
163+
String rewritten = local;
164+
for (var cluster : authorizedClusters) {
165+
rewritten += "," + RemoteClusterAware.buildRemoteIndexName(cluster, local);
166+
rewrittenIndices.add(rewritten);
167+
}
168+
logger.info("Rewrote [{}] to [{}]", local, rewritten);
169+
}
170+
if (resolved.getRemote().isEmpty() == false) {
171+
rewrittenIndices.addAll(resolved.getRemote());
172+
}
173+
request.indices(rewrittenIndices);
174+
// skipping mixed expressions, _local expressions and all that jazz
175+
}
176+
130177
/**
131178
* Attempt to resolve requested indices without expanding any wildcards.
132179
* @return The {@link ResolvedIndices} or null if wildcard expansion must be performed.
@@ -569,5 +616,9 @@ ResolvedIndices splitLocalAndRemoteIndexNames(String... indices) {
569616
.toList();
570617
return new ResolvedIndices(local == null ? List.of() : local, remote);
571618
}
619+
620+
Set<String> clusters() {
621+
return Collections.unmodifiableSet(clusters);
622+
}
572623
}
573624
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,9 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole(
998998
} // we don't support granting access to a backing index with a failure selector via the parent data stream
999999
}
10001000
return predicate.test(indexAbstraction, selector);
1001+
}, name -> {
1002+
// just some bogus predicate that lets us differentiate between roles
1003+
return Arrays.asList(role.names()).contains("remote_searcher");
10011004
});
10021005
}
10031006

@@ -1125,15 +1128,18 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn
11251128
private final CachedSupplier<Set<String>> authorizedAndAvailableDataResources;
11261129
private final CachedSupplier<Set<String>> authorizedAndAvailableFailuresResources;
11271130
private final BiPredicate<String, IndexComponentSelector> isAuthorizedPredicate;
1131+
private final Predicate<String> projectPredicate;
11281132

11291133
AuthorizedIndices(
11301134
Supplier<Set<String>> authorizedAndAvailableDataResources,
11311135
Supplier<Set<String>> authorizedAndAvailableFailuresResources,
1132-
BiPredicate<String, IndexComponentSelector> isAuthorizedPredicate
1136+
BiPredicate<String, IndexComponentSelector> isAuthorizedPredicate,
1137+
Predicate<String> projectPredicate
11331138
) {
11341139
this.authorizedAndAvailableDataResources = CachedSupplier.wrap(authorizedAndAvailableDataResources);
11351140
this.authorizedAndAvailableFailuresResources = CachedSupplier.wrap(authorizedAndAvailableFailuresResources);
11361141
this.isAuthorizedPredicate = Objects.requireNonNull(isAuthorizedPredicate);
1142+
this.projectPredicate = projectPredicate;
11371143
}
11381144

11391145
@Override
@@ -1149,5 +1155,11 @@ public boolean check(String name, IndexComponentSelector selector) {
11491155
Objects.requireNonNull(selector, "must specify a selector for authorization check");
11501156
return isAuthorizedPredicate.test(name, selector);
11511157
}
1158+
1159+
@Override
1160+
public boolean checkProject(String name) {
1161+
Objects.requireNonNull(name, "must specify a project name for authorization check");
1162+
return projectPredicate.test(name);
1163+
}
11521164
}
11531165
}

0 commit comments

Comments
 (0)