Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fb7b6a7
fix: consider case sensitiveness differences
ldematte Apr 17, 2025
89701ef
Update docs/changelog/126990.yaml
ldematte Apr 17, 2025
4c6cc3f
[CI] Auto commit changes from spotless
Apr 17, 2025
d844993
changelog + spotless
ldematte Apr 17, 2025
761b8a3
Merge branch 'entitlements/fileaccesstree-fix-windows-casing' of http…
ldematte Apr 17, 2025
1b0293c
forbidden
ldematte Apr 17, 2025
c7ab3b9
Merge branch 'main' into entitlements/fileaccesstree-fix-windows-casing
ldematte Apr 17, 2025
e199568
Update docs/changelog/126990.yaml
ldematte Apr 18, 2025
6f2eab2
Merge remote-tracking branch 'upstream/main' into entitlements/fileac…
ldematte Apr 18, 2025
3894d03
consider mac case insensitive too
ldematte Apr 18, 2025
ea10eb5
Merge branch 'main' into entitlements/fileaccesstree-fix-windows-casing
ldematte Apr 18, 2025
bd7881a
Merge remote-tracking branch 'upstream/main' into entitlements/fileac…
ldematte Apr 22, 2025
3862969
Refactoring: move path-as-string comparison functions to Comparison c…
ldematte Apr 22, 2025
b252d3d
Merge branch 'main' into entitlements/fileaccesstree-fix-windows-casing
ldematte Apr 22, 2025
38957a2
extract separator
ldematte Apr 22, 2025
65f8bd8
Merge branch 'entitlements/fileaccesstree-fix-windows-casing' of gith…
ldematte Apr 22, 2025
5d3db2e
[CI] Auto commit changes from spotless
Apr 22, 2025
cfc7299
fix separator for windows tests
ldematte Apr 22, 2025
cfaf7e4
iter
ldematte Apr 22, 2025
2438af9
[CI] Auto commit changes from spotless
Apr 22, 2025
61b1219
Merge remote-tracking branch 'upstream/main' into entitlements/fileac…
ldematte Apr 23, 2025
1d15bfb
iter
ldematte Apr 23, 2025
fe22895
iter
ldematte Apr 23, 2025
0db687c
Merge branch 'main' into entitlements/fileaccesstree-fix-windows-casing
ldematte Apr 23, 2025
a1127fd
Merge branch 'main' into entitlements/fileaccesstree-fix-windows-casing
ldematte Apr 23, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/126990.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 126990
summary: "Fix: consider case sensitiveness differences in Windows/Unix-like filesystems for files entitlements"
area: Infra/Core
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
ExclusivePath currentExclusivePath = exclusivePaths.get(0);
for (int i = 1; i < exclusivePaths.size(); ++i) {
ExclusivePath nextPath = exclusivePaths.get(i);
if (currentExclusivePath.path().equals(nextPath.path) || isParent(currentExclusivePath.path(), nextPath.path())) {
if (FileUtils.samePath(currentExclusivePath.path(), nextPath.path)
|| FileUtils.isParent(currentExclusivePath.path(), nextPath.path())) {
throw new IllegalArgumentException(
"duplicate/overlapping exclusive paths found in files entitlements: " + currentExclusivePath + " and " + nextPath
);
Expand Down Expand Up @@ -194,7 +195,7 @@ static List<String> pruneSortedPaths(List<String> paths) {
prunedReadPaths.add(currentPath);
for (int i = 1; i < paths.size(); ++i) {
String nextPath = paths.get(i);
if (currentPath.equals(nextPath) == false && isParent(currentPath, nextPath) == false) {
if (FileUtils.samePath(currentPath, nextPath) == false && FileUtils.isParent(currentPath, nextPath) == false) {
prunedReadPaths.add(nextPath);
currentPath = nextPath;
}
Expand All @@ -203,7 +204,7 @@ static List<String> pruneSortedPaths(List<String> paths) {
return prunedReadPaths;
}

public static FileAccessTree of(
static FileAccessTree of(
String componentName,
String moduleName,
FilesEntitlement filesEntitlement,
Expand Down Expand Up @@ -243,22 +244,17 @@ private boolean checkPath(String path, String[] paths) {
}

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

int ndx = Arrays.binarySearch(paths, path, PATH_ORDER);
if (ndx < -1) {
return isParent(paths[-ndx - 2], path);
return FileUtils.isParent(paths[-ndx - 2], path);
}
return ndx >= 0;
}

private static boolean isParent(String maybeParent, String path) {
logger.trace(() -> Strings.format("checking isParent [%s] for [%s]", maybeParent, path));
return path.startsWith(maybeParent) && path.startsWith(FILE_SEPARATOR, maybeParent.length());
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,37 @@

package org.elasticsearch.entitlement.runtime.policy;

import org.elasticsearch.core.Strings;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;

import java.io.File;
import java.util.Comparator;
import java.util.function.BiPredicate;

import static java.lang.Character.isLetter;

public class FileUtils {

private static final Logger logger = LogManager.getLogger(FileUtils.class);

private FileUtils() {}

interface CharComparator {
int compare(char c1, char c2);
}

private static final CharComparator CHARACTER_COMPARATOR = Platform.WINDOWS.isCurrent()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using static finals here should make code perform a little bit better as the JIT as a better chance of optimizing this. But the downside is that this is harder to test: you need to run tests on a Windows or Linux machine to test case sensitive/insensitive.
@rjernst what's your take on this?
Would you prefer to have a non-static comparator? We could still instantiate it as final in FileAccessTree, and create the correct final predicates in the ctor. Or have 2 different subclasses, and create the correct instance (CaseInsensitveComparator vs CaseSensitiveComparator)

? FileUtils::caseInsensitiveCharacterComparator
: FileUtils::caseSensitiveCharacterComparator;

private static final BiPredicate<String, String> STARTS_WITH = Platform.WINDOWS.isCurrent()
? FileUtils::caseInsensitiveStartsWith
: String::startsWith;

private static final BiPredicate<String, String> EQUALS = Platform.WINDOWS.isCurrent() ? String::equalsIgnoreCase : String::equals;

/**
* For our lexicographic sort trick to work correctly, we must have path separators sort before
* any other character so that files in a directory appear immediately after that directory.
Expand All @@ -32,7 +52,8 @@ private FileUtils() {}
for (int k = 0; k < lim; k++) {
char c1 = s1.charAt(k);
char c2 = s2.charAt(k);
if (c1 == c2) {
var comp = CHARACTER_COMPARATOR.compare(c1, c2);
if (comp == 0) {
continue;
}
boolean c1IsSeparator = isPathSeparator(c1);
Expand All @@ -44,17 +65,51 @@ private FileUtils() {}
if (c2IsSeparator) {
return 1;
}
return c1 - c2;
return comp;
}
}
return len1 - len2;
};

/**
* Case-insensitive character comparison. Inspired by the {@code WindowsPath#compareTo} implementation.
*/
private static int caseInsensitiveCharacterComparator(char c1, char c2) {
if (c1 == c2) {
return 0;
}
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 == c2) {
return 0;
}
return c1 - c2;
}

private static int caseSensitiveCharacterComparator(char c1, char c2) {
return c1 - c2;
}

private static boolean caseInsensitiveStartsWith(String s, String prefix) {
return s.regionMatches(true, 0, prefix, 0, prefix.length());
}

@SuppressForbidden(reason = "we need the separator as a char, not a string")
private static boolean isPathSeparator(char c) {
return c == File.separatorChar;
}

@SuppressForbidden(reason = "we need the separator as a char, not a string")
static boolean isParent(String maybeParent, String path) {
logger.trace(() -> Strings.format("checking isParent [%s] for [%s]", maybeParent, path));
return STARTS_WITH.test(path, maybeParent)
&& (path.length() > maybeParent.length() && path.charAt(maybeParent.length()) == File.separatorChar);
}

static boolean samePath(String currentPath, String nextPath) {
return EQUALS.test(currentPath, nextPath);
}

/**
* Tests if a path is absolute or relative, taking into consideration both Unix and Windows conventions.
* Note that this leads to a conflict, resolved in favor of Unix rules: `/foo` can be either a Unix absolute path, or a Windows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,16 @@ public void testDuplicatePrunedPaths() {
assertEquals(expected, actual);
}

public void testDuplicatePrunedPathsWindows() {
assumeTrue("Specific to windows for paths with mixed casing", WINDOWS.isCurrent());

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");
List<String> outputPaths = List.of("/a", "/b/c", "b/c/d", "e/f");
var actual = FileAccessTree.pruneSortedPaths(inputPaths.stream().map(p -> normalizePath(path(p))).toList());
var expected = outputPaths.stream().map(p -> normalizePath(path(p))).toList();
assertEquals(expected, actual);
}

public void testDuplicateExclusivePaths() {
// Bunch o' handy definitions
var pathAB = path("/a/b");
Expand Down Expand Up @@ -457,6 +467,37 @@ public void testWindowsAbsolutPathAccess() {
assertThat(fileAccessTree.canWrite(Path.of("D:\\foo")), is(false));
}

public void testWindowsMixedCaseAccess() {
assumeTrue("Specific to windows for paths with mixed casing", WINDOWS.isCurrent());

var fileAccessTree = FileAccessTree.of(
"test",
"test",
new FilesEntitlement(
List.of(
FileData.ofPath(Path.of("\\\\.\\pipe\\"), READ),
FileData.ofPath(Path.of("D:\\.gradle"), READ),
FileData.ofPath(Path.of("D:\\foo"), READ),
FileData.ofPath(Path.of("C:\\foo"), FilesEntitlement.Mode.READ_WRITE)
)
),
TEST_PATH_LOOKUP,
null,
List.of()
);

assertThat(fileAccessTree.canRead(Path.of("\\\\.\\PIPE\\bar")), is(true));
assertThat(fileAccessTree.canRead(Path.of("c:\\foo")), is(true));
assertThat(fileAccessTree.canRead(Path.of("C:\\FOO")), is(true));
assertThat(fileAccessTree.canWrite(Path.of("C:\\foo")), is(true));
assertThat(fileAccessTree.canRead(Path.of("c:\\foo")), is(true));
assertThat(fileAccessTree.canRead(Path.of("C:\\FOO")), is(true));
assertThat(fileAccessTree.canRead(Path.of("d:\\foo")), is(true));
assertThat(fileAccessTree.canRead(Path.of("d:\\FOO")), is(true));
assertThat(fileAccessTree.canWrite(Path.of("D:\\foo")), is(false));
assertThat(fileAccessTree.canWrite(Path.of("d:\\foo")), is(false));
}

FileAccessTree accessTree(FilesEntitlement entitlement, List<ExclusivePath> exclusivePaths) {
return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, null, exclusivePaths);
}
Expand Down
Loading