Skip to content

Refactor file path resolution for entitlements #127040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 21, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
import com.sun.tools.attach.VirtualMachine;

import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.initialization.EntitlementInitialization;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.PathLookupImpl;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
Expand All @@ -37,35 +40,15 @@ public record BootstrapArgs(
@Nullable Policy serverPolicyPatch,
Map<String, Policy> pluginPolicies,
Function<Class<?>, String> pluginResolver,
Function<String, Stream<String>> settingResolver,
Path[] dataDirs,
Path[] sharedRepoDirs,
Path configDir,
Path libDir,
Path modulesDir,
Path pluginsDir,
PathLookup pathLookup,
Map<String, Path> sourcePaths,
Path logsDir,
Path tempDir,
Path pidFile,
Set<Class<?>> suppressFailureLogClasses
) {
public BootstrapArgs {
requireNonNull(pluginPolicies);
requireNonNull(pluginResolver);
requireNonNull(settingResolver);
requireNonNull(dataDirs);
if (dataDirs.length == 0) {
throw new IllegalArgumentException("must provide at least one data directory");
}
requireNonNull(sharedRepoDirs);
requireNonNull(configDir);
requireNonNull(libDir);
requireNonNull(modulesDir);
requireNonNull(pluginsDir);
requireNonNull(pathLookup);
requireNonNull(sourcePaths);
requireNonNull(logsDir);
requireNonNull(tempDir);
requireNonNull(suppressFailureLogClasses);
}
}
Expand Down Expand Up @@ -121,23 +104,34 @@ public static void bootstrap(
serverPolicyPatch,
pluginPolicies,
pluginResolver,
settingResolver,
dataDirs,
sharedRepoDirs,
configDir,
libDir,
modulesDir,
pluginsDir,
new PathLookupImpl(
getUserHome(),
configDir,
dataDirs,
sharedRepoDirs,
libDir,
modulesDir,
pluginsDir,
logsDir,
tempDir,
pidFile,
settingResolver
),
sourcePaths,
logsDir,
tempDir,
pidFile,
suppressFailureLogClasses
);
exportInitializationToAgent();
loadAgent(findAgentJar());
}

private static Path getUserHome() {
String userHome = System.getProperty("user.home");
if (userHome == null) {
throw new IllegalStateException("user.home system property is required");
}
return PathUtils.get(userHome);
}

@SuppressForbidden(reason = "The VirtualMachine API is the only way to attach a java agent dynamically")
private static void loadAgent(String agentPath) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@
import java.nio.file.attribute.FileAttribute;
import java.nio.file.spi.FileSystemProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -70,10 +70,14 @@
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.DATA;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.LIB;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.LOGS;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.MODULES;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.PLUGINS;
import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.SHARED_REPO;
import static org.elasticsearch.entitlement.runtime.policy.Platform.LINUX;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.BaseDir.CONFIG;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.BaseDir.DATA;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.BaseDir.SHARED_REPO;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ;
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE;

Expand Down Expand Up @@ -165,27 +169,20 @@ private static Class<?>[] findClassesToRetransform(Class<?>[] loadedClasses, Set
private static PolicyManager createPolicyManager() {
EntitlementBootstrap.BootstrapArgs bootstrapArgs = EntitlementBootstrap.bootstrapArgs();
Map<String, Policy> pluginPolicies = bootstrapArgs.pluginPolicies();
var pathLookup = new PathLookup(
getUserHome(),
bootstrapArgs.configDir(),
bootstrapArgs.dataDirs(),
bootstrapArgs.sharedRepoDirs(),
bootstrapArgs.tempDir(),
bootstrapArgs.settingResolver()
);
PathLookup pathLookup = bootstrapArgs.pathLookup();

List<Scope> serverScopes = new ArrayList<>();
List<FileData> serverModuleFileDatas = new ArrayList<>();
Collections.addAll(
serverModuleFileDatas,
// Base ES directories
FileData.ofPath(bootstrapArgs.pluginsDir(), READ),
FileData.ofPath(bootstrapArgs.modulesDir(), READ),
FileData.ofPath(bootstrapArgs.configDir(), READ),
FileData.ofPath(bootstrapArgs.logsDir(), READ_WRITE),
FileData.ofPath(bootstrapArgs.libDir(), READ),
FileData.ofRelativePath(Path.of(""), DATA, READ_WRITE),
FileData.ofRelativePath(Path.of(""), SHARED_REPO, READ_WRITE),
FileData.ofBaseDirPath(PLUGINS, READ),
FileData.ofBaseDirPath(MODULES, READ),
FileData.ofBaseDirPath(CONFIG, READ),
FileData.ofBaseDirPath(LOGS, READ_WRITE),
FileData.ofBaseDirPath(LIB, READ),
FileData.ofBaseDirPath(DATA, READ_WRITE),
FileData.ofBaseDirPath(SHARED_REPO, READ_WRITE),
// exclusive settings file
FileData.ofRelativePath(Path.of("operator/settings.json"), CONFIG, READ_WRITE).withExclusive(true),
// OS release on Linux
Expand All @@ -206,8 +203,8 @@ private static PolicyManager createPolicyManager() {
FileData.ofPath(Path.of("/proc/self/mountinfo"), READ).withPlatform(LINUX),
FileData.ofPath(Path.of("/proc/diskstats"), READ).withPlatform(LINUX)
);
if (bootstrapArgs.pidFile() != null) {
serverModuleFileDatas.add(FileData.ofPath(bootstrapArgs.pidFile(), READ_WRITE));
if (pathLookup.pidFile() != null) {
serverModuleFileDatas.add(FileData.ofPath(pathLookup.pidFile(), READ_WRITE));
}

Collections.addAll(
Expand All @@ -219,8 +216,8 @@ private static PolicyManager createPolicyManager() {
new FilesEntitlement(
List.of(
// TODO: what in es.base is accessing shared repo?
FileData.ofRelativePath(Path.of(""), SHARED_REPO, READ_WRITE),
FileData.ofRelativePath(Path.of(""), DATA, READ_WRITE)
FileData.ofBaseDirPath(SHARED_REPO, READ_WRITE),
FileData.ofBaseDirPath(DATA, READ_WRITE)
)
)
)
Expand All @@ -245,25 +242,17 @@ private static PolicyManager createPolicyManager() {
List.of(
new LoadNativeLibrariesEntitlement(),
new ManageThreadsEntitlement(),
new FilesEntitlement(
List.of(FileData.ofPath(bootstrapArgs.configDir(), READ), FileData.ofRelativePath(Path.of(""), DATA, READ_WRITE))
)
new FilesEntitlement(List.of(FileData.ofBaseDirPath(CONFIG, READ), FileData.ofBaseDirPath(DATA, READ_WRITE)))
)
),
new Scope(
"org.apache.lucene.misc",
List.of(new FilesEntitlement(List.of(FileData.ofRelativePath(Path.of(""), DATA, READ_WRITE))))
),
new Scope("org.apache.lucene.misc", List.of(new FilesEntitlement(List.of(FileData.ofBaseDirPath(DATA, READ_WRITE))))),
new Scope(
"org.apache.logging.log4j.core",
List.of(new ManageThreadsEntitlement(), new FilesEntitlement(List.of(FileData.ofPath(bootstrapArgs.logsDir(), READ_WRITE))))
List.of(new ManageThreadsEntitlement(), new FilesEntitlement(List.of(FileData.ofBaseDirPath(LOGS, READ_WRITE))))
),
new Scope(
"org.elasticsearch.nativeaccess",
List.of(
new LoadNativeLibrariesEntitlement(),
new FilesEntitlement(List.of(FileData.ofRelativePath(Path.of(""), DATA, READ_WRITE)))
)
List.of(new LoadNativeLibrariesEntitlement(), new FilesEntitlement(List.of(FileData.ofBaseDirPath(DATA, READ_WRITE))))
)
);

Expand All @@ -288,7 +277,7 @@ private static PolicyManager createPolicyManager() {
new Scope(
"org.bouncycastle.fips.core",
// read to lib dir is required for checksum validation
List.of(new FilesEntitlement(List.of(FileData.ofPath(bootstrapArgs.libDir(), READ))), new ManageThreadsEntitlement())
List.of(new FilesEntitlement(List.of(FileData.ofBaseDirPath(LIB, READ))), new ManageThreadsEntitlement())
)
);
}
Expand All @@ -312,21 +301,14 @@ private static PolicyManager createPolicyManager() {
new LoadNativeLibrariesEntitlement(),
new FilesEntitlement(
List.of(
FileData.ofPath(bootstrapArgs.logsDir(), READ_WRITE),
FileData.ofBaseDirPath(LOGS, READ_WRITE),
FileData.ofPath(Path.of("/proc/meminfo"), READ),
FileData.ofPath(Path.of("/sys/fs/cgroup/"), READ)
)
)
);

validateFilesEntitlements(
pluginPolicies,
pathLookup,
bootstrapArgs.configDir(),
bootstrapArgs.pluginsDir(),
bootstrapArgs.modulesDir(),
bootstrapArgs.libDir()
);
validateFilesEntitlements(pluginPolicies, pathLookup);

return new PolicyManager(
serverPolicy,
Expand All @@ -341,21 +323,14 @@ private static PolicyManager createPolicyManager() {
);
}

private static Set<Path> pathSet(Path... paths) {
return Arrays.stream(paths).map(x -> x.toAbsolutePath().normalize()).collect(Collectors.toUnmodifiableSet());
}

// package visible for tests
static void validateFilesEntitlements(
Map<String, Policy> pluginPolicies,
PathLookup pathLookup,
Path configDir,
Path pluginsDir,
Path modulesDir,
Path libDir
) {
var readAccessForbidden = pathSet(pluginsDir, modulesDir, libDir);
var writeAccessForbidden = pathSet(configDir);
static void validateFilesEntitlements(Map<String, Policy> pluginPolicies, PathLookup pathLookup) {
Set<Path> readAccessForbidden = new HashSet<>();
pathLookup.getBaseDirPaths(PLUGINS).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
pathLookup.getBaseDirPaths(MODULES).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
pathLookup.getBaseDirPaths(LIB).forEach(p -> readAccessForbidden.add(p.toAbsolutePath().normalize()));
Set<Path> writeAccessForbidden = new HashSet<>();
pathLookup.getBaseDirPaths(CONFIG).forEach(p -> writeAccessForbidden.add(p.toAbsolutePath().normalize()));
for (var pluginPolicy : pluginPolicies.entrySet()) {
for (var scope : pluginPolicy.getValue().scopes()) {
var filesEntitlement = scope.entitlements()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
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;

public final class FileAccessTree {
Expand Down Expand Up @@ -167,9 +169,9 @@ private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup,
}

// everything has access to the temp dir, config dir, to their own dir (their own jar files) and the jdk
addPathAndMaybeLink.accept(pathLookup.tempDir(), READ_WRITE);
pathLookup.getBaseDirPaths(TEMP).forEach(tempPath -> addPathAndMaybeLink.accept(tempPath, READ_WRITE));
// TODO: this grants read access to the config dir for all modules until explicit read entitlements can be added
addPathAndMaybeLink.accept(pathLookup.configDir(), Mode.READ);
pathLookup.getBaseDirPaths(CONFIG).forEach(configPath -> addPathAndMaybeLink.accept(configPath, Mode.READ));
if (componentPath != null) {
addPathAndMaybeLink.accept(componentPath, Mode.READ);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,29 @@
package org.elasticsearch.entitlement.runtime.policy;

import java.nio.file.Path;
import java.util.function.Function;
import java.util.stream.Stream;

public record PathLookup(
Path homeDir,
Path configDir,
Path[] dataDirs,
Path[] sharedRepoDirs,
Path tempDir,
Function<String, Stream<String>> settingResolver
) {}
/**
* Resolves paths for known directories checked by entitlements.
*/
public interface PathLookup {
enum BaseDir {
USER_HOME,
CONFIG,
DATA,
SHARED_REPO,
LIB,
MODULES,
PLUGINS,
LOGS,
TEMP
}

Path pidFile();

Stream<Path> getBaseDirPaths(BaseDir baseDir);

Stream<Path> resolveRelativePaths(BaseDir baseDir, Path relativePath);

Stream<Path> resolveSettingPaths(BaseDir baseDir, String settingName);
}
Loading