Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 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

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,42 @@
/*
* 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);
}

/**
* Case-insensitive character comparison. Taken from the JDK {@code WindowsPath#compareTo} implementation.
*/
private static int caseInsensitiveCharacterComparator(char c1, char c2) {
if (c1 == c2) {
return 0;
}
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 == c2) {
return 0;
}
return Character.compare(c1, 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);
}
}
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