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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/changelog/126990.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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));
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a significance to the fact that we switched from uppercase to lowercase?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, just a suggestion from Ryan to make it shorter and more readable

Copy link
Contributor

@prdoyle prdoyle Apr 23, 2025

Choose a reason for hiding this comment

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

toUpperCase is equally short and readable, no?

I just think if we use one that's different from the actual filesystem code you referenced, we're asking for unexpected corner cases.

}

@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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

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;
Expand All @@ -33,7 +35,6 @@

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;
Expand Down Expand Up @@ -70,9 +71,9 @@
* 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"}.
* </p>
* With the paths pruned, sorted, and segregated by permission, each binary search has the following properties:
* <ul>
Expand Down Expand Up @@ -118,7 +119,11 @@ public String toString() {
}
}

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<>();
for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) {
for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) {
Expand Down Expand Up @@ -150,15 +155,16 @@ static List<ExclusivePath> buildExclusivePathList(List<ExclusiveFileEntitlement>
}
}
}
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) {
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
);
Expand All @@ -168,9 +174,18 @@ static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
}
}

@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
Expand All @@ -188,19 +203,27 @@ static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
private static String[] buildUpdatedAndSortedExclusivePaths(
String componentName,
String moduleName,
List<ExclusivePath> exclusivePaths
List<ExclusivePath> exclusivePaths,
FileAccessTreeComparison comparison
) {
List<String> updatedExclusivePaths = new ArrayList<>();
for (ExclusivePath exclusivePath : exclusivePaths) {
if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleNames().contains(moduleName) == false) {
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<String> readPaths = new ArrayList<>();
List<String> writePaths = new ArrayList<>();
BiConsumer<Path, Mode> addPath = (path, mode) -> {
Expand Down Expand Up @@ -252,12 +275,12 @@ private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup,
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(
Expand All @@ -270,14 +293,14 @@ private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup,
}

// 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<>();
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;
}
Expand All @@ -298,7 +321,8 @@ static FileAccessTree of(
filesEntitlement,
pathLookup,
componentPath,
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths)
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths, DEFAULT_COMPARISON),
DEFAULT_COMPARISON
);
}

Expand All @@ -310,7 +334,7 @@ public static FileAccessTree withoutExclusivePaths(
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) {
Expand Down Expand Up @@ -346,24 +370,18 @@ private boolean checkPath(String path, String[] paths) {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading