Skip to content

Commit dbac70e

Browse files
authored
[Entitlements] Fix FileAccessTree paths ordering (#123689)
1 parent 678738a commit dbac70e

File tree

8 files changed

+291
-142
lines changed

8 files changed

+291
-142
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@
6464
import java.util.stream.Stream;
6565
import java.util.stream.StreamSupport;
6666

67+
import static org.elasticsearch.entitlement.runtime.policy.Platform.LINUX;
6768
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.BaseDir.DATA;
6869
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.BaseDir.SHARED_REPO;
6970
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ;
7071
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE;
71-
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Platform.LINUX;
7272

7373
/**
7474
* Called by the agent during {@code agentmain} to configure the entitlement system,

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

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
import java.nio.file.Paths;
2323
import java.util.ArrayList;
2424
import java.util.Arrays;
25-
import java.util.Comparator;
2625
import java.util.List;
2726
import java.util.Objects;
2827
import java.util.function.BiConsumer;
2928

3029
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
30+
import static org.elasticsearch.entitlement.runtime.policy.FileUtils.PATH_ORDER;
3131

3232
public final class FileAccessTree {
3333

@@ -236,30 +236,4 @@ public boolean equals(Object o) {
236236
public int hashCode() {
237237
return Objects.hash(Arrays.hashCode(readPaths), Arrays.hashCode(writePaths));
238238
}
239-
240-
/**
241-
* For our lexicographic sort trick to work correctly, we must have path separators sort before
242-
* any other character so that files in a directory appear immediately after that directory.
243-
* For example, we require [/a, /a/b, /a.xml] rather than the natural order [/a, /a.xml, /a/b].
244-
*/
245-
private static final Comparator<String> PATH_ORDER = (s1, s2) -> {
246-
Path p1 = Path.of(s1);
247-
Path p2 = Path.of(s2);
248-
var i1 = p1.iterator();
249-
var i2 = p2.iterator();
250-
while (i1.hasNext() && i2.hasNext()) {
251-
int cmp = i1.next().compareTo(i2.next());
252-
if (cmp != 0) {
253-
return cmp;
254-
}
255-
}
256-
if (i1.hasNext()) {
257-
return 1;
258-
} else if (i2.hasNext()) {
259-
return -1;
260-
} else {
261-
assert p1.equals(p2);
262-
return 0;
263-
}
264-
};
265239
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.entitlement.runtime.policy;
11+
12+
import org.elasticsearch.core.SuppressForbidden;
13+
14+
import java.io.File;
15+
import java.util.Comparator;
16+
17+
import static java.lang.Character.isLetter;
18+
19+
public class FileUtils {
20+
21+
private FileUtils() {}
22+
23+
/**
24+
* For our lexicographic sort trick to work correctly, we must have path separators sort before
25+
* any other character so that files in a directory appear immediately after that directory.
26+
* For example, we require [/a, /a/b, /a.xml] rather than the natural order [/a, /a.xml, /a/b].
27+
*/
28+
static final Comparator<String> PATH_ORDER = (s1, s2) -> {
29+
int len1 = s1.length();
30+
int len2 = s2.length();
31+
int lim = Math.min(len1, len2);
32+
for (int k = 0; k < lim; k++) {
33+
char c1 = s1.charAt(k);
34+
char c2 = s2.charAt(k);
35+
if (c1 == c2) {
36+
continue;
37+
}
38+
boolean c1IsSeparator = isPathSeparator(c1);
39+
boolean c2IsSeparator = isPathSeparator(c2);
40+
if (c1IsSeparator == false || c2IsSeparator == false) {
41+
if (c1IsSeparator) {
42+
return -1;
43+
}
44+
if (c2IsSeparator) {
45+
return 1;
46+
}
47+
return c1 - c2;
48+
}
49+
}
50+
return len1 - len2;
51+
};
52+
53+
@SuppressForbidden(reason = "we need the separator as a char, not a string")
54+
private static boolean isPathSeparator(char c) {
55+
return c == File.separatorChar;
56+
}
57+
58+
/**
59+
* Tests if a path is absolute or relative, taking into consideration both Unix and Windows conventions.
60+
* Note that this leads to a conflict, resolved in favor of Unix rules: `/foo` can be either a Unix absolute path, or a Windows
61+
* relative path with "wrong" directory separator (using non-canonical / in Windows).
62+
* This method is intended to be used as validation for different file entitlements format: therefore it is preferable to reject a
63+
* relative path that is definitely absolute on Unix, rather than accept it as a possible relative path on Windows (if that is the case,
64+
* the developer can easily fix the path by using the correct platform separators).
65+
*/
66+
public static boolean isAbsolutePath(String path) {
67+
if (path.isEmpty()) {
68+
return false;
69+
}
70+
if (path.charAt(0) == '/') {
71+
// Unix/BSD absolute
72+
return true;
73+
}
74+
75+
return isWindowsAbsolutePath(path);
76+
}
77+
78+
/**
79+
* When testing for path separators in a platform-agnostic way, we may encounter both kinds of slashes, especially when
80+
* processing windows paths. The JDK parses paths the same way under Windows.
81+
*/
82+
static boolean isSlash(char c) {
83+
return (c == '\\') || (c == '/');
84+
}
85+
86+
private static boolean isWindowsAbsolutePath(String input) {
87+
// if a prefix is present, we expected (long) UNC or (long) absolute
88+
if (input.startsWith("\\\\?\\")) {
89+
return true;
90+
}
91+
92+
if (input.length() > 1) {
93+
char c0 = input.charAt(0);
94+
char c1 = input.charAt(1);
95+
if (isSlash(c0) && isSlash(c1)) {
96+
// Two slashes or more: UNC
97+
return true;
98+
}
99+
if (isLetter(c0) && c1 == ':') {
100+
// A drive: absolute
101+
return true;
102+
}
103+
}
104+
// Otherwise relative
105+
return false;
106+
}
107+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.entitlement.runtime.policy;
11+
12+
public enum Platform {
13+
LINUX,
14+
MACOS,
15+
WINDOWS;
16+
17+
private static final Platform current = findCurrent();
18+
19+
private static Platform findCurrent() {
20+
String os = System.getProperty("os.name");
21+
if (os.startsWith("Linux")) {
22+
return LINUX;
23+
} else if (os.startsWith("Mac OS")) {
24+
return MACOS;
25+
} else if (os.startsWith("Windows")) {
26+
return WINDOWS;
27+
} else {
28+
throw new AssertionError("Unsupported platform [" + os + "]");
29+
}
30+
}
31+
32+
public boolean isCurrent() {
33+
return this == current;
34+
}
35+
}

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

Lines changed: 4 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
package org.elasticsearch.entitlement.runtime.policy.entitlements;
1111

1212
import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement;
13+
import org.elasticsearch.entitlement.runtime.policy.FileUtils;
1314
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
15+
import org.elasticsearch.entitlement.runtime.policy.Platform;
1416
import org.elasticsearch.entitlement.runtime.policy.PolicyValidationException;
1517

1618
import java.nio.file.Path;
@@ -23,8 +25,6 @@
2325
import java.util.function.BiFunction;
2426
import java.util.stream.Stream;
2527

26-
import static java.lang.Character.isLetter;
27-
2828
/**
2929
* Describes a file entitlement with a path and mode.
3030
*/
@@ -44,31 +44,6 @@ public enum BaseDir {
4444
HOME
4545
}
4646

47-
public enum Platform {
48-
LINUX,
49-
MACOS,
50-
WINDOWS;
51-
52-
private static final Platform current = findCurrent();
53-
54-
private static Platform findCurrent() {
55-
String os = System.getProperty("os.name");
56-
if (os.startsWith("Linux")) {
57-
return LINUX;
58-
} else if (os.startsWith("Mac OS")) {
59-
return MACOS;
60-
} else if (os.startsWith("Windows")) {
61-
return WINDOWS;
62-
} else {
63-
throw new AssertionError("Unsupported platform [" + os + "]");
64-
}
65-
}
66-
67-
public boolean isCurrent() {
68-
return this == current;
69-
}
70-
}
71-
7247
public sealed interface FileData {
7348

7449
Stream<Path> resolvePaths(PathLookup pathLookup);
@@ -94,50 +69,6 @@ static FileData ofRelativePath(Path relativePath, BaseDir baseDir, Mode mode) {
9469
static FileData ofPathSetting(String setting, BaseDir baseDir, Mode mode) {
9570
return new PathSettingFileData(setting, baseDir, mode, null, false);
9671
}
97-
98-
/**
99-
* Tests if a path is absolute or relative, taking into consideration both Unix and Windows conventions.
100-
* Note that this leads to a conflict, resolved in favor of Unix rules: `/foo` can be either a Unix absolute path, or a Windows
101-
* relative path with "wrong" directory separator (using non-canonical slash in Windows).
102-
*/
103-
static boolean isAbsolutePath(String path) {
104-
if (path.isEmpty()) {
105-
return false;
106-
}
107-
if (path.charAt(0) == '/') {
108-
// Unix/BSD absolute
109-
return true;
110-
}
111-
return isWindowsAbsolutePath(path);
112-
}
113-
114-
private static boolean isSlash(char c) {
115-
return (c == '\\') || (c == '/');
116-
}
117-
118-
private static boolean isWindowsAbsolutePath(String input) {
119-
// if a prefix is present, we expected (long) UNC or (long) absolute
120-
if (input.startsWith("\\\\?\\")) {
121-
return true;
122-
}
123-
124-
if (input.length() > 1) {
125-
char c0 = input.charAt(0);
126-
char c1 = input.charAt(1);
127-
char c = 0;
128-
int next = 2;
129-
if (isSlash(c0) && isSlash(c1)) {
130-
// Two slashes or more: UNC
131-
return true;
132-
}
133-
if (isLetter(c0) && c1 == ':') {
134-
// A drive: absolute
135-
return true;
136-
}
137-
}
138-
// Otherwise relative
139-
return false;
140-
}
14172
}
14273

14374
private sealed interface RelativeFileData extends FileData {
@@ -367,13 +298,13 @@ public static FilesEntitlement build(List<Object> paths) {
367298
}
368299
BaseDir baseDir = parseBaseDir(relativeTo);
369300
Path relativePath = Path.of(relativePathAsString);
370-
if (FileData.isAbsolutePath(relativePathAsString)) {
301+
if (FileUtils.isAbsolutePath(relativePathAsString)) {
371302
throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative");
372303
}
373304
fileData = FileData.ofRelativePath(relativePath, baseDir, mode);
374305
} else if (pathAsString != null) {
375306
Path path = Path.of(pathAsString);
376-
if (FileData.isAbsolutePath(pathAsString) == false) {
307+
if (FileUtils.isAbsolutePath(pathAsString) == false) {
377308
throw new PolicyValidationException("'path' [" + pathAsString + "] must be absolute");
378309
}
379310
fileData = FileData.ofPath(path, mode);

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Map;
2727

2828
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
29+
import static org.hamcrest.Matchers.equalTo;
2930
import static org.hamcrest.Matchers.is;
3031

3132
@ESTestCase.WithoutSecurityManager
@@ -193,6 +194,29 @@ public void testNormalizePath() {
193194
assertThat(tree.canRead(path("")), is(false));
194195
}
195196

197+
public void testNormalizeDirectorySeparatorWindows() {
198+
assumeTrue("normalization of windows paths", Platform.WINDOWS.isCurrent());
199+
200+
assertThat(FileAccessTree.normalizePath(Path.of("C:\\a\\b")), equalTo("C:\\a\\b"));
201+
assertThat(FileAccessTree.normalizePath(Path.of("C:/a.xml")), equalTo("C:\\a.xml"));
202+
assertThat(FileAccessTree.normalizePath(Path.of("C:/a/b.txt")), equalTo("C:\\a\\b.txt"));
203+
assertThat(FileAccessTree.normalizePath(Path.of("C:/a/c\\foo.txt")), equalTo("C:\\a\\c\\foo.txt"));
204+
205+
var tree = accessTree(
206+
entitlement("C:\\a\\b", "read", "C:/a.xml", "read", "C:/a/b.txt", "read", "C:/a/c\\foo.txt", "read"),
207+
List.of()
208+
);
209+
210+
assertThat(tree.canRead(Path.of("C:/a.xml")), is(true));
211+
assertThat(tree.canRead(Path.of("C:\\a.xml")), is(true));
212+
assertThat(tree.canRead(Path.of("C:/a/")), is(false));
213+
assertThat(tree.canRead(Path.of("C:/a/b.txt")), is(true));
214+
assertThat(tree.canRead(Path.of("C:/a/b/c.txt")), is(true));
215+
assertThat(tree.canRead(Path.of("C:\\a\\b\\c.txt")), is(true));
216+
assertThat(tree.canRead(Path.of("C:\\a\\c\\")), is(false));
217+
assertThat(tree.canRead(Path.of("C:\\a\\c\\foo.txt")), is(true));
218+
}
219+
196220
public void testNormalizeTrailingSlashes() {
197221
var tree = accessTree(entitlement("/trailing/slash/", "read", "/no/trailing/slash", "read"), List.of());
198222
assertThat(tree.canRead(path("/trailing/slash")), is(true));
@@ -230,6 +254,8 @@ public void testJdkAccess() {
230254

231255
@SuppressForbidden(reason = "don't care about the directory location in tests")
232256
public void testFollowLinks() throws IOException {
257+
assumeFalse("Windows requires admin right to create symbolic links", Platform.WINDOWS.isCurrent());
258+
233259
Path baseSourceDir = Files.createTempDirectory("fileaccess_source");
234260
Path source1Dir = baseSourceDir.resolve("source1");
235261
Files.createDirectory(source1Dir);
@@ -315,6 +341,31 @@ public void testInvalidExclusiveAccess() {
315341
assertThat(tree.canWrite(path("a")), is(false));
316342
}
317343

344+
public void testWindowsAbsolutPathAccess() {
345+
assumeTrue("Specific to windows for paths with a root (DOS or UNC)", Platform.WINDOWS.isCurrent());
346+
347+
var fileAccessTree = FileAccessTree.of(
348+
"test",
349+
"test",
350+
new FilesEntitlement(
351+
List.of(
352+
FilesEntitlement.FileData.ofPath(Path.of("\\\\.\\pipe\\"), FilesEntitlement.Mode.READ),
353+
FilesEntitlement.FileData.ofPath(Path.of("D:\\.gradle"), FilesEntitlement.Mode.READ),
354+
FilesEntitlement.FileData.ofPath(Path.of("D:\\foo"), FilesEntitlement.Mode.READ),
355+
FilesEntitlement.FileData.ofPath(Path.of("C:\\foo"), FilesEntitlement.Mode.READ_WRITE)
356+
)
357+
),
358+
TEST_PATH_LOOKUP,
359+
List.of()
360+
);
361+
362+
assertThat(fileAccessTree.canRead(Path.of("\\\\.\\pipe\\bar")), is(true));
363+
assertThat(fileAccessTree.canRead(Path.of("C:\\foo")), is(true));
364+
assertThat(fileAccessTree.canWrite(Path.of("C:\\foo")), is(true));
365+
assertThat(fileAccessTree.canRead(Path.of("D:\\foo")), is(true));
366+
assertThat(fileAccessTree.canWrite(Path.of("D:\\foo")), is(false));
367+
}
368+
318369
FileAccessTree accessTree(FilesEntitlement entitlement, List<ExclusivePath> exclusivePaths) {
319370
return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, exclusivePaths);
320371
}

0 commit comments

Comments
 (0)