From 002fef75ff61a44aa3f663a79d67727faacce16a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?=
Date: Wed, 23 Apr 2025 20:23:45 +0200
Subject: [PATCH] [Entitlements] Fix: consider case sensitiveness differences
(#126990)
Our path comparison for file access is string based, due to the fact that we need to support Paths created for different file systems/platforms.
However, Windows files and paths are (sort of) case insensitive.
This PR fixes the problem by abstracting String comparison operations and making them case sensitive or not based on the host OS.
---
docs/changelog/126990.yaml | 7 ++
.../policy/CaseInsensitiveComparison.java | 31 +++++++
.../policy/CaseSensitiveComparison.java | 27 +++++++
.../runtime/policy/FileAccessTree.java | 76 ++++++++++-------
.../policy/FileAccessTreeComparison.java | 81 +++++++++++++++++++
.../entitlement/runtime/policy/FileUtils.java | 40 ---------
.../runtime/policy/PolicyManager.java | 8 +-
.../policy/FileAccessTreeComparisonTests.java | 68 ++++++++++++++++
.../runtime/policy/FileAccessTreeTests.java | 60 +++++++++++---
.../runtime/policy/FileUtilsTests.java | 51 ------------
10 files changed, 317 insertions(+), 132 deletions(-)
create mode 100644 docs/changelog/126990.yaml
create mode 100644 libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseInsensitiveComparison.java
create mode 100644 libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/CaseSensitiveComparison.java
create mode 100644 libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparison.java
create mode 100644 libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeComparisonTests.java
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));
- }
}