Skip to content

Commit fb7b6a7

Browse files
committed
fix: consider case sensitiveness differences
1 parent 4248c99 commit fb7b6a7

File tree

3 files changed

+104
-12
lines changed

3 files changed

+104
-12
lines changed

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
9494
ExclusivePath currentExclusivePath = exclusivePaths.get(0);
9595
for (int i = 1; i < exclusivePaths.size(); ++i) {
9696
ExclusivePath nextPath = exclusivePaths.get(i);
97-
if (currentExclusivePath.path().equals(nextPath.path) || isParent(currentExclusivePath.path(), nextPath.path())) {
97+
if (FileUtils.samePath(currentExclusivePath.path(), nextPath.path) || FileUtils.isParent(currentExclusivePath.path(), nextPath.path())) {
9898
throw new IllegalArgumentException(
9999
"duplicate/overlapping exclusive paths found in files entitlements: " + currentExclusivePath + " and " + nextPath
100100
);
@@ -194,7 +194,7 @@ static List<String> pruneSortedPaths(List<String> paths) {
194194
prunedReadPaths.add(currentPath);
195195
for (int i = 1; i < paths.size(); ++i) {
196196
String nextPath = paths.get(i);
197-
if (currentPath.equals(nextPath) == false && isParent(currentPath, nextPath) == false) {
197+
if (FileUtils.samePath(currentPath, nextPath) == false && FileUtils.isParent(currentPath, nextPath) == false) {
198198
prunedReadPaths.add(nextPath);
199199
currentPath = nextPath;
200200
}
@@ -203,7 +203,7 @@ static List<String> pruneSortedPaths(List<String> paths) {
203203
return prunedReadPaths;
204204
}
205205

206-
public static FileAccessTree of(
206+
static FileAccessTree of(
207207
String componentName,
208208
String moduleName,
209209
FilesEntitlement filesEntitlement,
@@ -243,22 +243,17 @@ private boolean checkPath(String path, String[] paths) {
243243
}
244244

245245
int endx = Arrays.binarySearch(exclusivePaths, path, PATH_ORDER);
246-
if (endx < -1 && isParent(exclusivePaths[-endx - 2], path) || endx >= 0) {
246+
if (endx < -1 && FileUtils.isParent(exclusivePaths[-endx - 2], path) || endx >= 0) {
247247
return false;
248248
}
249249

250250
int ndx = Arrays.binarySearch(paths, path, PATH_ORDER);
251251
if (ndx < -1) {
252-
return isParent(paths[-ndx - 2], path);
252+
return FileUtils.isParent(paths[-ndx - 2], path);
253253
}
254254
return ndx >= 0;
255255
}
256256

257-
private static boolean isParent(String maybeParent, String path) {
258-
logger.trace(() -> Strings.format("checking isParent [%s] for [%s]", maybeParent, path));
259-
return path.startsWith(maybeParent) && path.startsWith(FILE_SEPARATOR, maybeParent.length());
260-
}
261-
262257
@Override
263258
public boolean equals(Object o) {
264259
if (o == null || getClass() != o.getClass()) return false;

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,39 @@
99

1010
package org.elasticsearch.entitlement.runtime.policy;
1111

12+
import org.elasticsearch.core.Strings;
1213
import org.elasticsearch.core.SuppressForbidden;
14+
import org.elasticsearch.logging.LogManager;
15+
import org.elasticsearch.logging.Logger;
1316

1417
import java.io.File;
1518
import java.util.Comparator;
19+
import java.util.function.BiPredicate;
1620

1721
import static java.lang.Character.isLetter;
1822

1923
public class FileUtils {
2024

25+
private static final Logger logger = LogManager.getLogger(FileUtils.class);
26+
2127
private FileUtils() {}
2228

29+
interface CharComparator {
30+
int compare(char c1, char c2);
31+
}
32+
33+
private static final CharComparator CHARACTER_COMPARATOR = Platform.WINDOWS.isCurrent()
34+
? FileUtils::caseInsensitiveCharacterComparator
35+
: FileUtils::caseSensitiveCharacterComparator;
36+
37+
private static final BiPredicate<String, String> STARTS_WITH = Platform.WINDOWS.isCurrent()
38+
? FileUtils::caseInsensitiveStartsWith
39+
: String::startsWith;
40+
41+
private static final BiPredicate<String, String> EQUALS = Platform.WINDOWS.isCurrent()
42+
? String::equalsIgnoreCase
43+
: String::equals;
44+
2345
/**
2446
* For our lexicographic sort trick to work correctly, we must have path separators sort before
2547
* any other character so that files in a directory appear immediately after that directory.
@@ -32,7 +54,8 @@ private FileUtils() {}
3254
for (int k = 0; k < lim; k++) {
3355
char c1 = s1.charAt(k);
3456
char c2 = s2.charAt(k);
35-
if (c1 == c2) {
57+
var comp = CHARACTER_COMPARATOR.compare(c1, c2);
58+
if (comp == 0) {
3659
continue;
3760
}
3861
boolean c1IsSeparator = isPathSeparator(c1);
@@ -44,17 +67,50 @@ private FileUtils() {}
4467
if (c2IsSeparator) {
4568
return 1;
4669
}
47-
return c1 - c2;
70+
return comp;
4871
}
4972
}
5073
return len1 - len2;
5174
};
5275

76+
/**
77+
* Case-insensitive character comparison. Inspired by the {@code WindowsPath#compareTo} implementation.
78+
*/
79+
private static int caseInsensitiveCharacterComparator(char c1, char c2) {
80+
if (c1 == c2) {
81+
return 0;
82+
}
83+
c1 = Character.toUpperCase(c1);
84+
c2 = Character.toUpperCase(c2);
85+
if (c1 == c2) {
86+
return 0;
87+
}
88+
return c1 - c2;
89+
}
90+
91+
private static int caseSensitiveCharacterComparator(char c1, char c2) {
92+
return c1 - c2;
93+
}
94+
95+
private static boolean caseInsensitiveStartsWith(String s, String prefix) {
96+
return s.regionMatches(true, 0, prefix, 0, prefix.length());
97+
}
98+
5399
@SuppressForbidden(reason = "we need the separator as a char, not a string")
54100
private static boolean isPathSeparator(char c) {
55101
return c == File.separatorChar;
56102
}
57103

104+
static boolean isParent(String maybeParent, String path) {
105+
logger.trace(() -> Strings.format("checking isParent [%s] for [%s]", maybeParent, path));
106+
return STARTS_WITH.test(path, maybeParent) &&
107+
(path.length() > maybeParent.length() && path.charAt(maybeParent.length()) == File.separatorChar);
108+
}
109+
110+
static boolean samePath(String currentPath, String nextPath) {
111+
return EQUALS.test(currentPath, nextPath);
112+
}
113+
58114
/**
59115
* Tests if a path is absolute or relative, taking into consideration both Unix and Windows conventions.
60116
* Note that this leads to a conflict, resolved in favor of Unix rules: `/foo` can be either a Unix absolute path, or a Windows

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,16 @@ public void testDuplicatePrunedPaths() {
363363
assertEquals(expected, actual);
364364
}
365365

366+
public void testDuplicatePrunedPathsWindows() {
367+
assumeTrue("Specific to windows for paths with mixed casing", WINDOWS.isCurrent());
368+
369+
List<String> inputPaths = List.of("/a", "/A", "/a/b", "/a/B", "/b/c", "b/c/d", "B/c/d", "b/c/D", "e/f", "e/f");
370+
List<String> outputPaths = List.of("/a", "/b/c", "b/c/d", "e/f");
371+
var actual = FileAccessTree.pruneSortedPaths(inputPaths.stream().map(p -> normalizePath(path(p))).toList());
372+
var expected = outputPaths.stream().map(p -> normalizePath(path(p))).toList();
373+
assertEquals(expected, actual);
374+
}
375+
366376
public void testDuplicateExclusivePaths() {
367377
// Bunch o' handy definitions
368378
var pathAB = path("/a/b");
@@ -457,6 +467,37 @@ public void testWindowsAbsolutPathAccess() {
457467
assertThat(fileAccessTree.canWrite(Path.of("D:\\foo")), is(false));
458468
}
459469

470+
public void testWindowsMixedCaseAccess() {
471+
assumeTrue("Specific to windows for paths with mixed casing", WINDOWS.isCurrent());
472+
473+
var fileAccessTree = FileAccessTree.of(
474+
"test",
475+
"test",
476+
new FilesEntitlement(
477+
List.of(
478+
FileData.ofPath(Path.of("\\\\.\\pipe\\"), READ),
479+
FileData.ofPath(Path.of("D:\\.gradle"), READ),
480+
FileData.ofPath(Path.of("D:\\foo"), READ),
481+
FileData.ofPath(Path.of("C:\\foo"), FilesEntitlement.Mode.READ_WRITE)
482+
)
483+
),
484+
TEST_PATH_LOOKUP,
485+
null,
486+
List.of()
487+
);
488+
489+
assertThat(fileAccessTree.canRead(Path.of("\\\\.\\PIPE\\bar")), is(true));
490+
assertThat(fileAccessTree.canRead(Path.of("c:\\foo")), is(true));
491+
assertThat(fileAccessTree.canRead(Path.of("C:\\FOO")), is(true));
492+
assertThat(fileAccessTree.canWrite(Path.of("C:\\foo")), is(true));
493+
assertThat(fileAccessTree.canRead(Path.of("c:\\foo")), is(true));
494+
assertThat(fileAccessTree.canRead(Path.of("C:\\FOO")), is(true));
495+
assertThat(fileAccessTree.canRead(Path.of("d:\\foo")), is(true));
496+
assertThat(fileAccessTree.canRead(Path.of("d:\\FOO")), is(true));
497+
assertThat(fileAccessTree.canWrite(Path.of("D:\\foo")), is(false));
498+
assertThat(fileAccessTree.canWrite(Path.of("d:\\foo")), is(false));
499+
}
500+
460501
FileAccessTree accessTree(FilesEntitlement entitlement, List<ExclusivePath> exclusivePaths) {
461502
return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, null, exclusivePaths);
462503
}

0 commit comments

Comments
 (0)