diff --git a/docs/changelog/126990.yaml b/docs/changelog/126990.yaml
new file mode 100644
index 000000000000..a8b875cc6a22
--- /dev/null
+++ b/docs/changelog/126990.yaml
@@ -0,0 +1,7 @@
+pr: 126990
+summary: "Fix: consider case sensitiveness differences in Windows/Unix-like filesystems\
+ \ for files entitlements"
+area: Infra/Core
+type: bug
+issues:
+ - 127047
diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseInsensitiveComparison.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseInsensitiveComparison.java
new file mode 100644
index 000000000000..4f122b9c36ed
--- /dev/null
+++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseInsensitiveComparison.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.entitlement.runtime.policy;
+
+class CaseInsensitiveComparison extends FileAccessTreeComparison {
+
+ CaseInsensitiveComparison(char separatorChar) {
+ super(CaseInsensitiveComparison::caseInsensitiveCharacterComparator, separatorChar);
+ }
+
+ private static int caseInsensitiveCharacterComparator(char c1, char c2) {
+ return Character.compare(Character.toLowerCase(c1), Character.toLowerCase(c2));
+ }
+
+ @Override
+ protected boolean pathStartsWith(String pathString, String pathPrefix) {
+ return pathString.regionMatches(true, 0, pathPrefix, 0, pathPrefix.length());
+ }
+
+ @Override
+ protected boolean pathsAreEqual(String path1, String path2) {
+ return path1.equalsIgnoreCase(path2);
+ }
+}
diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseSensitiveComparison.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseSensitiveComparison.java
new file mode 100644
index 000000000000..700dc4b7cc79
--- /dev/null
+++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseSensitiveComparison.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.entitlement.runtime.policy;
+
+class CaseSensitiveComparison extends FileAccessTreeComparison {
+
+ CaseSensitiveComparison(char separatorChar) {
+ super(Character::compare, separatorChar);
+ }
+
+ @Override
+ protected boolean pathStartsWith(String pathString, String pathPrefix) {
+ return pathString.startsWith(pathPrefix);
+ }
+
+ @Override
+ protected boolean pathsAreEqual(String path1, String path2) {
+ return path1.equals(path2);
+ }
+}
diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java
index 4b3b7cca75db..5f9af60c802b 100644
--- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java
+++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java
@@ -11,11 +11,13 @@ package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Strings;
+import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
+import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@@ -33,7 +35,6 @@ import java.util.function.BiConsumer;
import static java.util.Comparator.comparing;
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
-import static org.elasticsearch.entitlement.runtime.policy.FileUtils.PATH_ORDER;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE;
@@ -70,9 +71,9 @@ import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEnt
* Secondly, the path separator (whether slash or backslash) sorts after the dot character. This means, for example, if the array contains
* {@code ["/a", "/a.xml"]} and we look up {@code "/a/b"} using normal string comparison, the binary search would land on {@code "/a.xml"},
* which is neither an exact match nor a containing directory, and so the lookup would incorrectly report no match, even though
- * {@code "/a"} is in the array. To fix this, we define {@link FileUtils#PATH_ORDER} which sorts path separators before any other
- * character. In the example, this would cause {@code "/a/b"} to sort between {@code "/a"} and {@code "/a.xml"} so that it correctly
- * finds {@code "/a"}.
+ * {@code "/a"} is in the array. To fix this, we define {@link FileAccessTreeComparison#pathComparator()} which sorts path separators
+ * before any other character. In the example, this would cause {@code "/a/b"} to sort between {@code "/a"} and {@code "/a.xml"} so that
+ * it correctly finds {@code "/a"}.
*
* With the paths pruned, sorted, and segregated by permission, each binary search has the following properties:
*
@@ -118,7 +119,11 @@ public final class FileAccessTree {
}
}
- static List buildExclusivePathList(List exclusiveFileEntitlements, PathLookup pathLookup) {
+ static List buildExclusivePathList(
+ List exclusiveFileEntitlements,
+ PathLookup pathLookup,
+ FileAccessTreeComparison comparison
+ ) {
Map exclusivePaths = new HashMap<>();
for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) {
for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) {
@@ -150,15 +155,16 @@ public final class FileAccessTree {
}
}
}
- return exclusivePaths.values().stream().sorted(comparing(ExclusivePath::path, PATH_ORDER)).distinct().toList();
+ return exclusivePaths.values().stream().sorted(comparing(ExclusivePath::path, comparison.pathComparator())).distinct().toList();
}
- static void validateExclusivePaths(List exclusivePaths) {
+ static void validateExclusivePaths(List exclusivePaths, FileAccessTreeComparison comparison) {
if (exclusivePaths.isEmpty() == false) {
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 (comparison.samePath(currentExclusivePath.path(), nextPath.path)
+ || comparison.isParent(currentExclusivePath.path(), nextPath.path())) {
throw new IllegalArgumentException(
"duplicate/overlapping exclusive paths found in files entitlements: " + currentExclusivePath + " and " + nextPath
);
@@ -168,9 +174,18 @@ public final class FileAccessTree {
}
}
+ @SuppressForbidden(reason = "we need the separator as a char, not a string")
+ static char separatorChar() {
+ return File.separatorChar;
+ }
+
private static final Logger logger = LogManager.getLogger(FileAccessTree.class);
private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator();
+ static final FileAccessTreeComparison DEFAULT_COMPARISON = Platform.LINUX.isCurrent()
+ ? new CaseSensitiveComparison(separatorChar())
+ : new CaseInsensitiveComparison(separatorChar());
+ private final FileAccessTreeComparison comparison;
/**
* lists paths that are forbidden for this component+module because some other component has granted exclusive access to one of its
* modules
@@ -188,7 +203,8 @@ public final class FileAccessTree {
private static String[] buildUpdatedAndSortedExclusivePaths(
String componentName,
String moduleName,
- List exclusivePaths
+ List exclusivePaths,
+ FileAccessTreeComparison comparison
) {
List updatedExclusivePaths = new ArrayList<>();
for (ExclusivePath exclusivePath : exclusivePaths) {
@@ -196,11 +212,18 @@ public final class FileAccessTree {
updatedExclusivePaths.add(exclusivePath.path());
}
}
- updatedExclusivePaths.sort(PATH_ORDER);
+ updatedExclusivePaths.sort(comparison.pathComparator());
return updatedExclusivePaths.toArray(new String[0]);
}
- private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup, Path componentPath, String[] sortedExclusivePaths) {
+ FileAccessTree(
+ FilesEntitlement filesEntitlement,
+ PathLookup pathLookup,
+ Path componentPath,
+ String[] sortedExclusivePaths,
+ FileAccessTreeComparison comparison
+ ) {
+ this.comparison = comparison;
List readPaths = new ArrayList<>();
List writePaths = new ArrayList<>();
BiConsumer addPath = (path, mode) -> {
@@ -252,12 +275,12 @@ public final class FileAccessTree {
Path jdk = Paths.get(System.getProperty("java.home"));
addPathAndMaybeLink.accept(jdk.resolve("conf"), Mode.READ);
- readPaths.sort(PATH_ORDER);
- writePaths.sort(PATH_ORDER);
+ readPaths.sort(comparison.pathComparator());
+ writePaths.sort(comparison.pathComparator());
this.exclusivePaths = sortedExclusivePaths;
- this.readPaths = pruneSortedPaths(readPaths).toArray(new String[0]);
- this.writePaths = pruneSortedPaths(writePaths).toArray(new String[0]);
+ this.readPaths = pruneSortedPaths(readPaths, comparison).toArray(new String[0]);
+ this.writePaths = pruneSortedPaths(writePaths, comparison).toArray(new String[0]);
logger.debug(
() -> Strings.format(
@@ -270,14 +293,14 @@ public final class FileAccessTree {
}
// package private for testing
- static List pruneSortedPaths(List paths) {
+ static List pruneSortedPaths(List paths, FileAccessTreeComparison comparison) {
List prunedReadPaths = new ArrayList<>();
if (paths.isEmpty() == false) {
String currentPath = paths.get(0);
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 (comparison.samePath(currentPath, nextPath) == false && comparison.isParent(currentPath, nextPath) == false) {
prunedReadPaths.add(nextPath);
currentPath = nextPath;
}
@@ -298,7 +321,8 @@ public final class FileAccessTree {
filesEntitlement,
pathLookup,
componentPath,
- buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths)
+ buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths, DEFAULT_COMPARISON),
+ DEFAULT_COMPARISON
);
}
@@ -310,7 +334,7 @@ public final class FileAccessTree {
PathLookup pathLookup,
@Nullable Path componentPath
) {
- return new FileAccessTree(filesEntitlement, pathLookup, componentPath, new String[0]);
+ return new FileAccessTree(filesEntitlement, pathLookup, componentPath, new String[0], DEFAULT_COMPARISON);
}
public boolean canRead(Path path) {
@@ -346,24 +370,18 @@ public final class FileAccessTree {
return false;
}
- int endx = Arrays.binarySearch(exclusivePaths, path, PATH_ORDER);
- if (endx < -1 && isParent(exclusivePaths[-endx - 2], path) || endx >= 0) {
+ int endx = Arrays.binarySearch(exclusivePaths, path, comparison.pathComparator());
+ if (endx < -1 && comparison.isParent(exclusivePaths[-endx - 2], path) || endx >= 0) {
return false;
}
- int ndx = Arrays.binarySearch(paths, path, PATH_ORDER);
+ int ndx = Arrays.binarySearch(paths, path, comparison.pathComparator());
if (ndx < -1) {
- return isParent(paths[-ndx - 2], path);
+ return comparison.isParent(paths[-ndx - 2], path);
}
return ndx >= 0;
}
- private static boolean isParent(String maybeParent, String path) {
- var isParent = path.startsWith(maybeParent) && path.startsWith(FILE_SEPARATOR, maybeParent.length());
- logger.trace(() -> Strings.format("checking isParent [%s] for [%s]: %b", maybeParent, path, isParent));
- return isParent;
- }
-
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparison.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparison.java
new file mode 100644
index 000000000000..80fced024b0f
--- /dev/null
+++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparison.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.entitlement.runtime.policy;
+
+import org.elasticsearch.core.Strings;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+
+import java.util.Comparator;
+
+abstract class FileAccessTreeComparison {
+
+ private static final Logger logger = LogManager.getLogger(FileAccessTreeComparison.class);
+
+ interface CharComparator {
+ int compare(char c1, char c2);
+ }
+
+ private final Comparator pathComparator;
+
+ private final char separatorChar;
+
+ /**
+ * 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.
+ * For example, we require [/a, /a/b, /a.xml] rather than the natural order [/a, /a.xml, /a/b].
+ */
+ FileAccessTreeComparison(CharComparator charComparator, char separatorChar) {
+ pathComparator = (s1, s2) -> {
+ int len1 = s1.length();
+ int len2 = s2.length();
+ int lim = Math.min(len1, len2);
+ for (int k = 0; k < lim; k++) {
+ char c1 = s1.charAt(k);
+ char c2 = s2.charAt(k);
+ var comp = charComparator.compare(c1, c2);
+ if (comp == 0) {
+ continue;
+ }
+ boolean c1IsSeparator = c1 == separatorChar;
+ boolean c2IsSeparator = c2 == separatorChar;
+ if (c1IsSeparator == false || c2IsSeparator == false) {
+ if (c1IsSeparator) {
+ return -1;
+ }
+ if (c2IsSeparator) {
+ return 1;
+ }
+ return comp;
+ }
+ }
+ return len1 - len2;
+ };
+ this.separatorChar = separatorChar;
+ }
+
+ Comparator pathComparator() {
+ return pathComparator;
+ }
+
+ boolean isParent(String maybeParent, String path) {
+ logger.trace(() -> Strings.format("checking isParent [%s] for [%s]", maybeParent, path));
+ return pathStartsWith(path, maybeParent)
+ && (path.length() > maybeParent.length() && path.charAt(maybeParent.length()) == separatorChar);
+ }
+
+ boolean samePath(String currentPath, String nextPath) {
+ return pathsAreEqual(currentPath, nextPath);
+ }
+
+ protected abstract boolean pathStartsWith(String pathString, String pathPrefix);
+
+ protected abstract boolean pathsAreEqual(String path1, String path2);
+}
diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileUtils.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileUtils.java
index 51caacc9d48b..6b7415591c1c 100644
--- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileUtils.java
+++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileUtils.java
@@ -9,52 +9,12 @@
package org.elasticsearch.entitlement.runtime.policy;
-import org.elasticsearch.core.SuppressForbidden;
-
-import java.io.File;
-import java.util.Comparator;
-
import static java.lang.Character.isLetter;
public class FileUtils {
private FileUtils() {}
- /**
- * 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.
- * For example, we require [/a, /a/b, /a.xml] rather than the natural order [/a, /a.xml, /a/b].
- */
- static final Comparator PATH_ORDER = (s1, s2) -> {
- int len1 = s1.length();
- int len2 = s2.length();
- int lim = Math.min(len1, len2);
- for (int k = 0; k < lim; k++) {
- char c1 = s1.charAt(k);
- char c2 = s2.charAt(k);
- if (c1 == c2) {
- continue;
- }
- boolean c1IsSeparator = isPathSeparator(c1);
- boolean c2IsSeparator = isPathSeparator(c2);
- if (c1IsSeparator == false || c2IsSeparator == false) {
- if (c1IsSeparator) {
- return -1;
- }
- if (c2IsSeparator) {
- return 1;
- }
- return c1 - c2;
- }
- }
- return len1 - len2;
- };
-
- @SuppressForbidden(reason = "we need the separator as a char, not a string")
- private static boolean isPathSeparator(char c) {
- return c == File.separatorChar;
- }
-
/**
* 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
diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
index 929e2f435d7e..aa901e9900d1 100644
--- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
+++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
@@ -334,8 +334,12 @@ public class PolicyManager {
validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue(), exclusiveFileEntitlements);
}
}
- List exclusivePaths = FileAccessTree.buildExclusivePathList(exclusiveFileEntitlements, pathLookup);
- FileAccessTree.validateExclusivePaths(exclusivePaths);
+ List exclusivePaths = FileAccessTree.buildExclusivePathList(
+ exclusiveFileEntitlements,
+ pathLookup,
+ FileAccessTree.DEFAULT_COMPARISON
+ );
+ FileAccessTree.validateExclusivePaths(exclusivePaths, FileAccessTree.DEFAULT_COMPARISON);
this.exclusivePaths = exclusivePaths;
}
diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparisonTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparisonTests.java
new file mode 100644
index 000000000000..06e15725de8c
--- /dev/null
+++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparisonTests.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.entitlement.runtime.policy;
+
+import org.elasticsearch.test.ESTestCase;
+
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThan;
+
+public class FileAccessTreeComparisonTests extends ESTestCase {
+
+ public void testPathOrderPosix() {
+ var pathComparator = new CaseSensitiveComparison('/').pathComparator();
+
+ // Unix-style
+ // Directories come BEFORE files; note that this differs from natural lexicographical order
+ assertThat(pathComparator.compare("/a/b", "/a.xml"), lessThan(0));
+
+ // Natural lexicographical order is respected in all the other cases
+ assertThat(pathComparator.compare("/a/b", "/a/b.txt"), lessThan(0));
+ assertThat(pathComparator.compare("/a/c", "/a/b.txt"), greaterThan(0));
+ assertThat(pathComparator.compare("/a/b", "/a/b/foo.txt"), lessThan(0));
+
+ // Inverted-windows style
+ // Directories come BEFORE files; note that this differs from natural lexicographical order
+ assertThat(pathComparator.compare("C:/a/b", "C:/a.xml"), lessThan(0));
+
+ // Natural lexicographical order is respected in all the other cases
+ assertThat(pathComparator.compare("C:/a/b", "C:/a/b.txt"), lessThan(0));
+ assertThat(pathComparator.compare("C:/a/c", "C:/a/b.txt"), greaterThan(0));
+ assertThat(pathComparator.compare("C:/a/b", "C:/a/b/foo.txt"), lessThan(0));
+
+ // "\" is a valid file name character on Posix, test we treat it like that
+ assertThat(pathComparator.compare("/a\\b", "/a/b.txt"), greaterThan(0));
+ }
+
+ public void testPathOrderWindows() {
+ var pathComparator = new CaseInsensitiveComparison('\\').pathComparator();
+
+ // Directories come BEFORE files; note that this differs from natural lexicographical order
+ assertThat(pathComparator.compare("C:\\a\\b", "C:\\a.xml"), lessThan(0));
+
+ // Natural lexicographical order is respected in all the other cases
+ assertThat(pathComparator.compare("C:\\a\\b", "C:\\a\\b.txt"), lessThan(0));
+ assertThat(pathComparator.compare("C:\\a\\b", "C:\\a\\b\\foo.txt"), lessThan(0));
+ assertThat(pathComparator.compare("C:\\a\\c", "C:\\a\\b.txt"), greaterThan(0));
+ }
+
+ public void testPathOrderingSpecialCharacters() {
+ var s = randomFrom('/', '\\');
+ var pathComparator = (randomBoolean() ? new CaseInsensitiveComparison(s) : new CaseSensitiveComparison(s)).pathComparator();
+
+ assertThat(pathComparator.compare("aa\uD801\uDC28", "aa\uD801\uDC28"), is(0));
+ assertThat(pathComparator.compare("aa\uD801\uDC28", "aa\uD801\uDC28a"), lessThan(0));
+
+ // Similarly to the other tests, we assert that Directories come BEFORE files, even when names are special characters
+ assertThat(pathComparator.compare(s + "\uD801\uDC28" + s + "b", s + "\uD801\uDC28.xml"), lessThan(0));
+ assertThat(pathComparator.compare(s + "\uD801\uDC28" + s + "b", s + "b.xml"), greaterThan(0));
+ }
+}
diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java
index dd5d0d93e079..a9fecd662d3b 100644
--- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java
+++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java
@@ -31,6 +31,7 @@ import java.util.Set;
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.buildExclusivePathList;
import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.normalizePath;
+import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.separatorChar;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP;
import static org.elasticsearch.entitlement.runtime.policy.Platform.WINDOWS;
@@ -362,9 +363,19 @@ public class FileAccessTreeTests extends ESTestCase {
}
public void testDuplicatePrunedPaths() {
+ var comparison = new CaseSensitiveComparison(separatorChar());
List 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 outputPaths = List.of("/a", "/b/c", "b/c/d", "e/f");
- var actual = FileAccessTree.pruneSortedPaths(inputPaths.stream().map(p -> normalizePath(path(p))).toList());
+ var actual = FileAccessTree.pruneSortedPaths(inputPaths.stream().map(p -> normalizePath(path(p))).toList(), comparison);
+ var expected = outputPaths.stream().map(p -> normalizePath(path(p))).toList();
+ assertEquals(expected, actual);
+ }
+
+ public void testDuplicatePrunedPathsWindows() {
+ var comparison = new CaseInsensitiveComparison(separatorChar());
+ List 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 outputPaths = List.of("/a", "/b/c", "b/c/d", "e/f");
+ var actual = FileAccessTree.pruneSortedPaths(inputPaths.stream().map(p -> normalizePath(path(p))).toList(), comparison);
var expected = outputPaths.stream().map(p -> normalizePath(path(p))).toList();
assertEquals(expected, actual);
}
@@ -373,6 +384,7 @@ public class FileAccessTreeTests extends ESTestCase {
// Bunch o' handy definitions
var pathAB = path("/a/b");
var pathCD = path("/c/d");
+ var comparison = randomBoolean() ? new CaseSensitiveComparison('/') : new CaseInsensitiveComparison('/');
var originalFileData = FileData.ofPath(pathAB, READ).withExclusive(true);
var fileDataWithWriteMode = FileData.ofPath(pathAB, READ_WRITE).withExclusive(true);
var original = new ExclusiveFileEntitlement("component1", "module1", new FilesEntitlement(List.of(originalFileData)));
@@ -400,24 +412,21 @@ public class FileAccessTreeTests extends ESTestCase {
assertEquals(
"Single element should trivially work",
List.of(originalExclusivePath),
- buildExclusivePathList(List.of(original), TEST_PATH_LOOKUP)
+ buildExclusivePathList(List.of(original), TEST_PATH_LOOKUP, comparison)
);
assertEquals(
"Two identical elements should be combined",
List.of(originalExclusivePath),
- buildExclusivePathList(List.of(original, original), TEST_PATH_LOOKUP)
+ buildExclusivePathList(List.of(original, original), TEST_PATH_LOOKUP, comparison)
);
// Don't merge things we shouldn't
var distinctEntitlements = List.of(original, differentComponent, differentModule, differentPath);
- var distinctPaths = List.of(
- originalExclusivePath,
- new ExclusivePath("component2", Set.of(original.moduleName()), originalExclusivePath.path()),
- new ExclusivePath(original.componentName(), Set.of("module2"), originalExclusivePath.path()),
- new ExclusivePath(original.componentName(), Set.of(original.moduleName()), normalizePath(pathCD))
+ var iae = expectThrows(
+ IllegalArgumentException.class,
+ () -> buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP, comparison)
);
- var iae = expectThrows(IllegalArgumentException.class, () -> buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP));
var pathABString = pathAB.toAbsolutePath().toString();
assertThat(
iae.getMessage(),
@@ -433,7 +442,7 @@ public class FileAccessTreeTests extends ESTestCase {
assertEquals(
"Exclusive paths should be combined even if the entitlements are different",
equivalentPaths,
- buildExclusivePathList(equivalentEntitlements, TEST_PATH_LOOKUP)
+ buildExclusivePathList(equivalentEntitlements, TEST_PATH_LOOKUP, comparison)
);
}
@@ -463,6 +472,37 @@ public class FileAccessTreeTests extends ESTestCase {
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 exclusivePaths) {
return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, null, exclusivePaths);
}
diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileUtilsTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileUtilsTests.java
index 4c7e5e49d3ac..d6e639027b6d 100644
--- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileUtilsTests.java
+++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileUtilsTests.java
@@ -9,14 +9,10 @@
package org.elasticsearch.entitlement.runtime.policy;
-import org.elasticsearch.core.PathUtils;
import org.elasticsearch.test.ESTestCase;
-import static org.elasticsearch.entitlement.runtime.policy.FileUtils.PATH_ORDER;
import static org.elasticsearch.entitlement.runtime.policy.FileUtils.isAbsolutePath;
-import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.lessThan;
public class FileUtilsTests extends ESTestCase {
@@ -42,51 +38,4 @@ public class FileUtilsTests extends ESTestCase {
assertThat(isAbsolutePath(headingSlashRelativePath), is(false));
assertThat(isAbsolutePath(""), is(false));
}
-
- public void testPathOrderPosix() {
- assumeFalse("path ordering rules specific to non-Windows path styles", Platform.WINDOWS.isCurrent());
-
- // Unix-style
- // Directories come BEFORE files; note that this differs from natural lexicographical order
- assertThat(PATH_ORDER.compare("/a/b", "/a.xml"), lessThan(0));
-
- // Natural lexicographical order is respected in all the other cases
- assertThat(PATH_ORDER.compare("/a/b", "/a/b.txt"), lessThan(0));
- assertThat(PATH_ORDER.compare("/a/c", "/a/b.txt"), greaterThan(0));
- assertThat(PATH_ORDER.compare("/a/b", "/a/b/foo.txt"), lessThan(0));
-
- // Inverted-windows style
- // Directories come BEFORE files; note that this differs from natural lexicographical order
- assertThat(PATH_ORDER.compare("C:/a/b", "C:/a.xml"), lessThan(0));
-
- // Natural lexicographical order is respected in all the other cases
- assertThat(PATH_ORDER.compare("C:/a/b", "C:/a/b.txt"), lessThan(0));
- assertThat(PATH_ORDER.compare("C:/a/c", "C:/a/b.txt"), greaterThan(0));
- assertThat(PATH_ORDER.compare("C:/a/b", "C:/a/b/foo.txt"), lessThan(0));
-
- // "\" is a valid file name character on Posix, test we treat it like that
- assertThat(PATH_ORDER.compare("/a\\b", "/a/b.txt"), greaterThan(0));
- }
-
- public void testPathOrderWindows() {
- assumeTrue("path ordering rules specific to Windows", Platform.WINDOWS.isCurrent());
-
- // Directories come BEFORE files; note that this differs from natural lexicographical order
- assertThat(PATH_ORDER.compare("C:\\a\\b", "C:\\a.xml"), lessThan(0));
-
- // Natural lexicographical order is respected in all the other cases
- assertThat(PATH_ORDER.compare("C:\\a\\b", "C:\\a\\b.txt"), lessThan(0));
- assertThat(PATH_ORDER.compare("C:\\a\\b", "C:\\a\\b\\foo.txt"), lessThan(0));
- assertThat(PATH_ORDER.compare("C:\\a\\c", "C:\\a\\b.txt"), greaterThan(0));
- }
-
- public void testPathOrderingSpecialCharacters() {
- assertThat(PATH_ORDER.compare("aa\uD801\uDC28", "aa\uD801\uDC28"), is(0));
- assertThat(PATH_ORDER.compare("aa\uD801\uDC28", "aa\uD801\uDC28a"), lessThan(0));
-
- var s = PathUtils.getDefaultFileSystem().getSeparator();
- // Similarly to the other tests, we assert that Directories come BEFORE files, even when names are special characters
- assertThat(PATH_ORDER.compare(s + "\uD801\uDC28" + s + "b", s + "\uD801\uDC28.xml"), lessThan(0));
- assertThat(PATH_ORDER.compare(s + "\uD801\uDC28" + s + "b", s + "b.xml"), greaterThan(0));
- }
}