Skip to content

Commit 0a40c32

Browse files
Merge branch 'main' into feature/hostname-doc-values-sparse-index-setting
2 parents 0f760e3 + c1deef4 commit 0a40c32

File tree

7 files changed

+196
-38
lines changed

7 files changed

+196
-38
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
import java.util.List;
1818
import java.util.Objects;
1919

20+
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
21+
2022
public final class FileAccessTree {
2123
public static final FileAccessTree EMPTY = new FileAccessTree(List.of());
24+
private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator();
2225

2326
private final String[] readPaths;
2427
private final String[] writePaths;
@@ -27,11 +30,11 @@ private FileAccessTree(List<FileEntitlement> fileEntitlements) {
2730
List<String> readPaths = new ArrayList<>();
2831
List<String> writePaths = new ArrayList<>();
2932
for (FileEntitlement fileEntitlement : fileEntitlements) {
30-
var mode = fileEntitlement.mode();
31-
if (mode == FileEntitlement.Mode.READ_WRITE) {
32-
writePaths.add(fileEntitlement.path());
33+
String path = normalizePath(Path.of(fileEntitlement.path()));
34+
if (fileEntitlement.mode() == FileEntitlement.Mode.READ_WRITE) {
35+
writePaths.add(path);
3336
}
34-
readPaths.add(fileEntitlement.path());
37+
readPaths.add(path);
3538
}
3639

3740
readPaths.sort(String::compareTo);
@@ -46,14 +49,20 @@ public static FileAccessTree of(List<FileEntitlement> fileEntitlements) {
4649
}
4750

4851
boolean canRead(Path path) {
49-
return checkPath(normalize(path), readPaths);
52+
return checkPath(normalizePath(path), readPaths);
5053
}
5154

5255
boolean canWrite(Path path) {
53-
return checkPath(normalize(path), writePaths);
56+
return checkPath(normalizePath(path), writePaths);
5457
}
5558

56-
private static String normalize(Path path) {
59+
/**
60+
* @return the "canonical" form of the given {@code path}, to be used for entitlement checks.
61+
*/
62+
static String normalizePath(Path path) {
63+
// Note that toAbsolutePath produces paths separated by the default file separator,
64+
// so on Windows, if the given path uses forward slashes, this consistently
65+
// converts it to backslashes.
5766
return path.toAbsolutePath().normalize().toString();
5867
}
5968

@@ -64,7 +73,7 @@ private static boolean checkPath(String path, String[] paths) {
6473
int ndx = Arrays.binarySearch(paths, path);
6574
if (ndx < -1) {
6675
String maybeParent = paths[-ndx - 2];
67-
return path.startsWith(maybeParent);
76+
return path.startsWith(maybeParent) && path.startsWith(FILE_SEPARATOR, maybeParent.length());
6877
}
6978
return ndx >= 0;
7079
}

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FileEntitlement.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement;
1313
import org.elasticsearch.entitlement.runtime.policy.PolicyValidationException;
1414

15-
import java.nio.file.Paths;
16-
1715
/**
18-
* Describes a file entitlement with a path and mode.
16+
* Describes entitlement to access files at a particular location.
17+
*
18+
* @param path the location of the files. For directories, implicitly includes access to
19+
* all contained files and (recursively) subdirectories.
20+
* @param mode the type of operation
1921
*/
2022
public record FileEntitlement(String path, Mode mode) implements Entitlement {
2123

@@ -24,14 +26,6 @@ public enum Mode {
2426
READ_WRITE
2527
}
2628

27-
public FileEntitlement {
28-
path = normalizePath(path);
29-
}
30-
31-
private static String normalizePath(String path) {
32-
return Paths.get(path).toAbsolutePath().normalize().toString();
33-
}
34-
3529
private static Mode parseMode(String mode) {
3630
if (mode.equals("read")) {
3731
return Mode.READ;

libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.nio.file.Path;
1717
import java.util.List;
1818

19+
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
1920
import static org.hamcrest.Matchers.is;
2021

2122
public class FileAccessTreeTests extends ESTestCase {
@@ -41,7 +42,9 @@ public void testRead() {
4142
var tree = FileAccessTree.of(List.of(entitlement("foo", "read")));
4243
assertThat(tree.canRead(path("foo")), is(true));
4344
assertThat(tree.canRead(path("foo/subdir")), is(true));
45+
assertThat(tree.canRead(path("food")), is(false));
4446
assertThat(tree.canWrite(path("foo")), is(false));
47+
assertThat(tree.canWrite(path("food")), is(false));
4548

4649
assertThat(tree.canRead(path("before")), is(false));
4750
assertThat(tree.canRead(path("later")), is(false));
@@ -51,7 +54,9 @@ public void testWrite() {
5154
var tree = FileAccessTree.of(List.of(entitlement("foo", "read_write")));
5255
assertThat(tree.canWrite(path("foo")), is(true));
5356
assertThat(tree.canWrite(path("foo/subdir")), is(true));
57+
assertThat(tree.canWrite(path("food")), is(false));
5458
assertThat(tree.canRead(path("foo")), is(true));
59+
assertThat(tree.canRead(path("food")), is(false));
5560

5661
assertThat(tree.canWrite(path("before")), is(false));
5762
assertThat(tree.canWrite(path("later")), is(false));
@@ -83,6 +88,22 @@ public void testNormalizePath() {
8388
assertThat(tree.canRead(path("")), is(false));
8489
}
8590

91+
public void testForwardSlashes() {
92+
String sep = getDefaultFileSystem().getSeparator();
93+
var tree = FileAccessTree.of(List.of(entitlement("a/b", "read"), entitlement("m" + sep + "n", "read")));
94+
95+
// Native separators work
96+
assertThat(tree.canRead(path("a" + sep + "b")), is(true));
97+
assertThat(tree.canRead(path("m" + sep + "n")), is(true));
98+
99+
// Forward slashes also work
100+
assertThat(tree.canRead(path("a/b")), is(true));
101+
assertThat(tree.canRead(path("m/n")), is(true));
102+
103+
// In case the native separator is a backslash, don't treat that as an escape
104+
assertThat(tree.canRead(path("m\n")), is(false));
105+
}
106+
86107
FileEntitlement entitlement(String path, String mode) {
87108
Path p = path(path);
88109
return FileEntitlement.create(p.toString(), mode);

server/src/main/java/org/elasticsearch/TransportVersion.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ public static List<TransportVersion> getAllVersions() {
118118
return VersionsHolder.ALL_VERSIONS;
119119
}
120120

121+
/**
122+
* @return whether this is a known {@link TransportVersion}, i.e. one declared in {@link TransportVersions}. Other versions may exist
123+
* in the wild (they're sent over the wire by numeric ID) but we don't know how to communicate using such versions.
124+
*/
125+
public boolean isKnown() {
126+
return VersionsHolder.ALL_VERSIONS_MAP.containsKey(id);
127+
}
128+
121129
public static TransportVersion fromString(String str) {
122130
return TransportVersion.fromId(Integer.parseInt(str));
123131
}

server/src/main/java/org/elasticsearch/transport/TransportHandshaker.java

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@
1111

1212
import org.elasticsearch.Build;
1313
import org.elasticsearch.TransportVersion;
14-
import org.elasticsearch.TransportVersions;
1514
import org.elasticsearch.action.ActionListener;
1615
import org.elasticsearch.cluster.node.DiscoveryNode;
1716
import org.elasticsearch.common.bytes.BytesReference;
1817
import org.elasticsearch.common.io.stream.BytesStreamOutput;
1918
import org.elasticsearch.common.io.stream.StreamInput;
2019
import org.elasticsearch.common.io.stream.StreamOutput;
2120
import org.elasticsearch.common.metrics.CounterMetric;
21+
import org.elasticsearch.core.Strings;
2222
import org.elasticsearch.core.TimeValue;
2323
import org.elasticsearch.core.UpdateForV9;
24+
import org.elasticsearch.logging.LogManager;
25+
import org.elasticsearch.logging.Logger;
2426
import org.elasticsearch.threadpool.ThreadPool;
2527

2628
import java.io.EOFException;
@@ -126,6 +128,8 @@ final class TransportHandshaker {
126128
* [3] Parent task ID should be empty; see org.elasticsearch.tasks.TaskId.writeTo for its structure.
127129
*/
128130

131+
private static final Logger logger = LogManager.getLogger(TransportHandshaker.class);
132+
129133
static final TransportVersion V8_HANDSHAKE_VERSION = TransportVersion.fromId(7_17_00_99);
130134
static final TransportVersion V9_HANDSHAKE_VERSION = TransportVersion.fromId(8_800_00_0);
131135
static final Set<TransportVersion> ALLOWED_HANDSHAKE_VERSIONS = Set.of(V8_HANDSHAKE_VERSION, V9_HANDSHAKE_VERSION);
@@ -159,7 +163,7 @@ void sendHandshake(
159163
ActionListener<TransportVersion> listener
160164
) {
161165
numHandshakes.inc();
162-
final HandshakeResponseHandler handler = new HandshakeResponseHandler(requestId, listener);
166+
final HandshakeResponseHandler handler = new HandshakeResponseHandler(requestId, channel, listener);
163167
pendingHandshakes.put(requestId, handler);
164168
channel.addCloseListener(
165169
ActionListener.running(() -> handler.handleLocalException(new TransportException("handshake failed because connection reset")))
@@ -185,9 +189,9 @@ void sendHandshake(
185189
}
186190

187191
void handleHandshake(TransportChannel channel, long requestId, StreamInput stream) throws IOException {
192+
final HandshakeRequest handshakeRequest;
188193
try {
189-
// Must read the handshake request to exhaust the stream
190-
new HandshakeRequest(stream);
194+
handshakeRequest = new HandshakeRequest(stream);
191195
} catch (Exception e) {
192196
assert ignoreDeserializationErrors : e;
193197
throw e;
@@ -206,9 +210,44 @@ void handleHandshake(TransportChannel channel, long requestId, StreamInput strea
206210
assert ignoreDeserializationErrors : exception;
207211
throw exception;
208212
}
213+
ensureCompatibleVersion(version, handshakeRequest.transportVersion, handshakeRequest.releaseVersion, channel);
209214
channel.sendResponse(new HandshakeResponse(this.version, Build.current().version()));
210215
}
211216

217+
static void ensureCompatibleVersion(
218+
TransportVersion localTransportVersion,
219+
TransportVersion remoteTransportVersion,
220+
String releaseVersion,
221+
Object channel
222+
) {
223+
if (TransportVersion.isCompatible(remoteTransportVersion)) {
224+
if (remoteTransportVersion.onOrAfter(localTransportVersion)) {
225+
// Remote is newer than us, so we will be using our transport protocol and it's up to the other end to decide whether it
226+
// knows how to do that.
227+
return;
228+
}
229+
if (remoteTransportVersion.isKnown()) {
230+
// Remote is older than us, so we will be using its transport protocol, which we can only do if and only if its protocol
231+
// version is known to us.
232+
return;
233+
}
234+
}
235+
236+
final var message = Strings.format(
237+
"""
238+
Rejecting unreadable transport handshake from remote node with version [%s/%s] received on [%s] since this node has \
239+
version [%s/%s] which has an incompatible wire format.""",
240+
releaseVersion,
241+
remoteTransportVersion,
242+
channel,
243+
Build.current().version(),
244+
localTransportVersion
245+
);
246+
logger.warn(message);
247+
throw new IllegalStateException(message);
248+
249+
}
250+
212251
TransportResponseHandler<HandshakeResponse> removeHandlerForHandshake(long requestId) {
213252
return pendingHandshakes.remove(requestId);
214253
}
@@ -224,11 +263,13 @@ long getNumHandshakes() {
224263
private class HandshakeResponseHandler implements TransportResponseHandler<HandshakeResponse> {
225264

226265
private final long requestId;
266+
private final TcpChannel channel;
227267
private final ActionListener<TransportVersion> listener;
228268
private final AtomicBoolean isDone = new AtomicBoolean(false);
229269

230-
private HandshakeResponseHandler(long requestId, ActionListener<TransportVersion> listener) {
270+
private HandshakeResponseHandler(long requestId, TcpChannel channel, ActionListener<TransportVersion> listener) {
231271
this.requestId = requestId;
272+
this.channel = channel;
232273
this.listener = listener;
233274
}
234275

@@ -245,20 +286,13 @@ public Executor executor() {
245286
@Override
246287
public void handleResponse(HandshakeResponse response) {
247288
if (isDone.compareAndSet(false, true)) {
248-
TransportVersion responseVersion = response.transportVersion;
249-
if (TransportVersion.isCompatible(responseVersion) == false) {
250-
listener.onFailure(
251-
new IllegalStateException(
252-
"Received message from unsupported version: ["
253-
+ responseVersion
254-
+ "] minimal compatible version is: ["
255-
+ TransportVersions.MINIMUM_COMPATIBLE
256-
+ "]"
257-
)
258-
);
259-
} else {
260-
listener.onResponse(TransportVersion.min(TransportHandshaker.this.version, response.getTransportVersion()));
261-
}
289+
ActionListener.completeWith(listener, () -> {
290+
ensureCompatibleVersion(version, response.getTransportVersion(), response.getReleaseVersion(), channel);
291+
final var resultVersion = TransportVersion.min(TransportHandshaker.this.version, response.getTransportVersion());
292+
assert TransportVersion.current().before(version) // simulating a newer-version transport service for test purposes
293+
|| resultVersion.isKnown() : "negotiated unknown version " + resultVersion;
294+
return resultVersion;
295+
});
262296
}
263297
}
264298

server/src/test/java/org/elasticsearch/transport/InboundHandlerTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,9 @@ public void testLogsSlowInboundProcessing() throws Exception {
290290
);
291291
BytesStreamOutput byteData = new BytesStreamOutput();
292292
TaskId.EMPTY_TASK_ID.writeTo(byteData);
293+
// simulate bytes of a transport handshake: vInt transport version then release version string
293294
TransportVersion.writeVersion(remoteVersion, byteData);
295+
byteData.writeString(randomIdentifier());
294296
final InboundMessage requestMessage = new InboundMessage(
295297
requestHeader,
296298
ReleasableBytesReference.wrap(byteData.bytes()),

0 commit comments

Comments
 (0)