[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.
This commit is contained in:
Lorenzo Dematté 2025-04-23 20:23:45 +02:00 committed by GitHub
parent 1819a14dfa
commit 002fef75ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 317 additions and 132 deletions

View file

@ -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

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -11,11 +11,13 @@ package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Strings; 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;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode; import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode;
import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger; import org.elasticsearch.logging.Logger;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -33,7 +35,6 @@ import java.util.function.BiConsumer;
import static java.util.Comparator.comparing; import static java.util.Comparator.comparing;
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem; 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.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP; import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE; 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 * 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"}, * {@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 * 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 * {@code "/a"} is in the array. To fix this, we define {@link FileAccessTreeComparison#pathComparator()} which sorts path separators
* character. In the example, this would cause {@code "/a/b"} to sort between {@code "/a"} and {@code "/a.xml"} so that it correctly * before any other character. In the example, this would cause {@code "/a/b"} to sort between {@code "/a"} and {@code "/a.xml"} so that
* finds {@code "/a"}. * it correctly finds {@code "/a"}.
* </p> * </p>
* With the paths pruned, sorted, and segregated by permission, each binary search has the following properties: * With the paths pruned, sorted, and segregated by permission, each binary search has the following properties:
* <ul> * <ul>
@ -118,7 +119,11 @@ public final class FileAccessTree {
} }
} }
static List<ExclusivePath> buildExclusivePathList(List<ExclusiveFileEntitlement> exclusiveFileEntitlements, PathLookup pathLookup) { static List<ExclusivePath> buildExclusivePathList(
List<ExclusiveFileEntitlement> exclusiveFileEntitlements,
PathLookup pathLookup,
FileAccessTreeComparison comparison
) {
Map<String, ExclusivePath> exclusivePaths = new HashMap<>(); Map<String, ExclusivePath> exclusivePaths = new HashMap<>();
for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) { for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) {
for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) { 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<ExclusivePath> exclusivePaths) { static void validateExclusivePaths(List<ExclusivePath> exclusivePaths, FileAccessTreeComparison comparison) {
if (exclusivePaths.isEmpty() == false) { if (exclusivePaths.isEmpty() == false) {
ExclusivePath currentExclusivePath = exclusivePaths.get(0); ExclusivePath currentExclusivePath = exclusivePaths.get(0);
for (int i = 1; i < exclusivePaths.size(); ++i) { for (int i = 1; i < exclusivePaths.size(); ++i) {
ExclusivePath nextPath = exclusivePaths.get(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( throw new IllegalArgumentException(
"duplicate/overlapping exclusive paths found in files entitlements: " + currentExclusivePath + " and " + nextPath "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 Logger logger = LogManager.getLogger(FileAccessTree.class);
private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator(); 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 * lists paths that are forbidden for this component+module because some other component has granted exclusive access to one of its
* modules * modules
@ -188,7 +203,8 @@ public final class FileAccessTree {
private static String[] buildUpdatedAndSortedExclusivePaths( private static String[] buildUpdatedAndSortedExclusivePaths(
String componentName, String componentName,
String moduleName, String moduleName,
List<ExclusivePath> exclusivePaths List<ExclusivePath> exclusivePaths,
FileAccessTreeComparison comparison
) { ) {
List<String> updatedExclusivePaths = new ArrayList<>(); List<String> updatedExclusivePaths = new ArrayList<>();
for (ExclusivePath exclusivePath : exclusivePaths) { for (ExclusivePath exclusivePath : exclusivePaths) {
@ -196,11 +212,18 @@ public final class FileAccessTree {
updatedExclusivePaths.add(exclusivePath.path()); updatedExclusivePaths.add(exclusivePath.path());
} }
} }
updatedExclusivePaths.sort(PATH_ORDER); updatedExclusivePaths.sort(comparison.pathComparator());
return updatedExclusivePaths.toArray(new String[0]); 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<String> readPaths = new ArrayList<>(); List<String> readPaths = new ArrayList<>();
List<String> writePaths = new ArrayList<>(); List<String> writePaths = new ArrayList<>();
BiConsumer<Path, Mode> addPath = (path, mode) -> { BiConsumer<Path, Mode> addPath = (path, mode) -> {
@ -252,12 +275,12 @@ public final class FileAccessTree {
Path jdk = Paths.get(System.getProperty("java.home")); Path jdk = Paths.get(System.getProperty("java.home"));
addPathAndMaybeLink.accept(jdk.resolve("conf"), Mode.READ); addPathAndMaybeLink.accept(jdk.resolve("conf"), Mode.READ);
readPaths.sort(PATH_ORDER); readPaths.sort(comparison.pathComparator());
writePaths.sort(PATH_ORDER); writePaths.sort(comparison.pathComparator());
this.exclusivePaths = sortedExclusivePaths; this.exclusivePaths = sortedExclusivePaths;
this.readPaths = pruneSortedPaths(readPaths).toArray(new String[0]); this.readPaths = pruneSortedPaths(readPaths, comparison).toArray(new String[0]);
this.writePaths = pruneSortedPaths(writePaths).toArray(new String[0]); this.writePaths = pruneSortedPaths(writePaths, comparison).toArray(new String[0]);
logger.debug( logger.debug(
() -> Strings.format( () -> Strings.format(
@ -270,14 +293,14 @@ public final class FileAccessTree {
} }
// package private for testing // package private for testing
static List<String> pruneSortedPaths(List<String> paths) { static List<String> pruneSortedPaths(List<String> paths, FileAccessTreeComparison comparison) {
List<String> prunedReadPaths = new ArrayList<>(); List<String> prunedReadPaths = new ArrayList<>();
if (paths.isEmpty() == false) { if (paths.isEmpty() == false) {
String currentPath = paths.get(0); String currentPath = paths.get(0);
prunedReadPaths.add(currentPath); prunedReadPaths.add(currentPath);
for (int i = 1; i < paths.size(); ++i) { for (int i = 1; i < paths.size(); ++i) {
String nextPath = paths.get(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); prunedReadPaths.add(nextPath);
currentPath = nextPath; currentPath = nextPath;
} }
@ -298,7 +321,8 @@ public final class FileAccessTree {
filesEntitlement, filesEntitlement,
pathLookup, pathLookup,
componentPath, componentPath,
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths) buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths, DEFAULT_COMPARISON),
DEFAULT_COMPARISON
); );
} }
@ -310,7 +334,7 @@ public final class FileAccessTree {
PathLookup pathLookup, PathLookup pathLookup,
@Nullable Path componentPath @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) { public boolean canRead(Path path) {
@ -346,24 +370,18 @@ public final class FileAccessTree {
return false; return false;
} }
int endx = Arrays.binarySearch(exclusivePaths, path, PATH_ORDER); int endx = Arrays.binarySearch(exclusivePaths, path, comparison.pathComparator());
if (endx < -1 && isParent(exclusivePaths[-endx - 2], path) || endx >= 0) { if (endx < -1 && comparison.isParent(exclusivePaths[-endx - 2], path) || endx >= 0) {
return false; return false;
} }
int ndx = Arrays.binarySearch(paths, path, PATH_ORDER); int ndx = Arrays.binarySearch(paths, path, comparison.pathComparator());
if (ndx < -1) { if (ndx < -1) {
return isParent(paths[-ndx - 2], path); return comparison.isParent(paths[-ndx - 2], path);
} }
return ndx >= 0; 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;

View file

@ -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<String> 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<String> 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);
}

View file

@ -9,52 +9,12 @@
package org.elasticsearch.entitlement.runtime.policy; 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; import static java.lang.Character.isLetter;
public class FileUtils { public class FileUtils {
private 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<String> 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. * 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 * Note that this leads to a conflict, resolved in favor of Unix rules: `/foo` can be either a Unix absolute path, or a Windows

View file

@ -334,8 +334,12 @@ public class PolicyManager {
validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue(), exclusiveFileEntitlements); validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue(), exclusiveFileEntitlements);
} }
} }
List<ExclusivePath> exclusivePaths = FileAccessTree.buildExclusivePathList(exclusiveFileEntitlements, pathLookup); List<ExclusivePath> exclusivePaths = FileAccessTree.buildExclusivePathList(
FileAccessTree.validateExclusivePaths(exclusivePaths); exclusiveFileEntitlements,
pathLookup,
FileAccessTree.DEFAULT_COMPARISON
);
FileAccessTree.validateExclusivePaths(exclusivePaths, FileAccessTree.DEFAULT_COMPARISON);
this.exclusivePaths = exclusivePaths; this.exclusivePaths = exclusivePaths;
} }

View file

@ -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));
}
}

View file

@ -31,6 +31,7 @@ import java.util.Set;
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem; import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.buildExclusivePathList; 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.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.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP; import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP;
import static org.elasticsearch.entitlement.runtime.policy.Platform.WINDOWS; import static org.elasticsearch.entitlement.runtime.policy.Platform.WINDOWS;
@ -362,9 +363,19 @@ public class FileAccessTreeTests extends ESTestCase {
} }
public void testDuplicatePrunedPaths() { public void testDuplicatePrunedPaths() {
var comparison = new CaseSensitiveComparison(separatorChar());
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> 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"); 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 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<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(), comparison);
var expected = outputPaths.stream().map(p -> normalizePath(path(p))).toList(); var expected = outputPaths.stream().map(p -> normalizePath(path(p))).toList();
assertEquals(expected, actual); assertEquals(expected, actual);
} }
@ -373,6 +384,7 @@ public class FileAccessTreeTests extends ESTestCase {
// Bunch o' handy definitions // Bunch o' handy definitions
var pathAB = path("/a/b"); var pathAB = path("/a/b");
var pathCD = path("/c/d"); var pathCD = path("/c/d");
var comparison = randomBoolean() ? new CaseSensitiveComparison('/') : new CaseInsensitiveComparison('/');
var originalFileData = FileData.ofPath(pathAB, READ).withExclusive(true); var originalFileData = FileData.ofPath(pathAB, READ).withExclusive(true);
var fileDataWithWriteMode = FileData.ofPath(pathAB, READ_WRITE).withExclusive(true); var fileDataWithWriteMode = FileData.ofPath(pathAB, READ_WRITE).withExclusive(true);
var original = new ExclusiveFileEntitlement("component1", "module1", new FilesEntitlement(List.of(originalFileData))); var original = new ExclusiveFileEntitlement("component1", "module1", new FilesEntitlement(List.of(originalFileData)));
@ -400,24 +412,21 @@ public class FileAccessTreeTests extends ESTestCase {
assertEquals( assertEquals(
"Single element should trivially work", "Single element should trivially work",
List.of(originalExclusivePath), List.of(originalExclusivePath),
buildExclusivePathList(List.of(original), TEST_PATH_LOOKUP) buildExclusivePathList(List.of(original), TEST_PATH_LOOKUP, comparison)
); );
assertEquals( assertEquals(
"Two identical elements should be combined", "Two identical elements should be combined",
List.of(originalExclusivePath), 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 // Don't merge things we shouldn't
var distinctEntitlements = List.of(original, differentComponent, differentModule, differentPath); var distinctEntitlements = List.of(original, differentComponent, differentModule, differentPath);
var distinctPaths = List.of( var iae = expectThrows(
originalExclusivePath, IllegalArgumentException.class,
new ExclusivePath("component2", Set.of(original.moduleName()), originalExclusivePath.path()), () -> buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP, comparison)
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));
var pathABString = pathAB.toAbsolutePath().toString(); var pathABString = pathAB.toAbsolutePath().toString();
assertThat( assertThat(
iae.getMessage(), iae.getMessage(),
@ -433,7 +442,7 @@ public class FileAccessTreeTests extends ESTestCase {
assertEquals( assertEquals(
"Exclusive paths should be combined even if the entitlements are different", "Exclusive paths should be combined even if the entitlements are different",
equivalentPaths, 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)); 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) { FileAccessTree accessTree(FilesEntitlement entitlement, List<ExclusivePath> exclusivePaths) {
return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, null, exclusivePaths); return FileAccessTree.of("test-component", "test-module", entitlement, TEST_PATH_LOOKUP, null, exclusivePaths);
} }

View file

@ -9,14 +9,10 @@
package org.elasticsearch.entitlement.runtime.policy; package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.test.ESTestCase; 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.elasticsearch.entitlement.runtime.policy.FileUtils.isAbsolutePath;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
public class FileUtilsTests extends ESTestCase { public class FileUtilsTests extends ESTestCase {
@ -42,51 +38,4 @@ public class FileUtilsTests extends ESTestCase {
assertThat(isAbsolutePath(headingSlashRelativePath), is(false)); assertThat(isAbsolutePath(headingSlashRelativePath), is(false));
assertThat(isAbsolutePath(""), 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));
}
} }