Refactor before entitlements for testing (#129099)

* Support multiple plugin source paths

* Refactor: remove unncessary PathLookup method.

It's only called in one place, and there's no need to override it for testing.
Removing it just makes things simpler.

* Refactor: local var for pathLookup

* Fix bugs in test build info parsing

* Fix representative_class in test

* Move BridgeUtilTests.

Tests in org.elasticsearch.entitlement.bridge are going to be uniquely hard to
test once we patch the bridge into java.base, due to Java's prohibition on
split packages.

Let's just move this guy to another package.

* Upcast (?!) Java23EntitlementChecker to EntitlementChecker

* Empty TestPathLookup

* Create PolicyManager during bootstrap, allowing us to share initialization

* Use empty component path list instead of null

* Downcast to the class of the check method.

In our unit test, we have a mock checker that doesn't extend
EntitlementChecker, so downcasting to that would require us to needlessly
rework the unit test.

* Fix javadoc typos
This commit is contained in:
Patrick Doyle 2025-06-09 12:56:07 -04:00 committed by GitHub
parent b214fbfcdc
commit 7ec8fccf94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 293 additions and 323 deletions

View file

@ -37,10 +37,11 @@ import java.util.stream.Stream;
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
public class InstrumenterImpl implements Instrumenter {
public final class InstrumenterImpl implements Instrumenter {
private static final Logger logger = LogManager.getLogger(InstrumenterImpl.class);
private final String getCheckerClassMethodDescriptor;
@ -271,7 +272,8 @@ public class InstrumenterImpl implements Instrumenter {
}
private void pushEntitlementChecker() {
InstrumenterImpl.this.pushEntitlementChecker(mv);
mv.visitMethodInsn(INVOKESTATIC, handleClass, "instance", getCheckerClassMethodDescriptor, false);
mv.visitTypeInsn(CHECKCAST, checkMethod.className());
}
private void pushCallerClass() {
@ -319,10 +321,7 @@ public class InstrumenterImpl implements Instrumenter {
true
);
}
}
protected void pushEntitlementChecker(MethodVisitor mv) {
mv.visitMethodInsn(INVOKESTATIC, handleClass, "instance", getCheckerClassMethodDescriptor, false);
}
record ClassFileInfo(String fileName, byte[] bytecodes) {}

View file

@ -14,9 +14,11 @@ import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
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.entitlement.runtime.policy.PolicyManager;
@ -26,6 +28,7 @@ import org.elasticsearch.logging.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@ -38,20 +41,20 @@ public class EntitlementBootstrap {
* calls to methods protected by entitlements from classes without a valid
* policy will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}.
*
* @param serverPolicyPatch a policy with additional entitlements to patch the embedded server layer policy
* @param pluginPolicies a map holding policies for plugins (and modules), by plugin (or module) name.
* @param scopeResolver a functor to map a Java Class to the component and module it belongs to.
* @param settingResolver a functor to resolve a setting name pattern for one or more Elasticsearch settings.
* @param dataDirs data directories for Elasticsearch
* @param sharedRepoDirs shared repository directories for Elasticsearch
* @param configDir the config directory for Elasticsearch
* @param libDir the lib directory for Elasticsearch
* @param modulesDir the directory where Elasticsearch modules are
* @param pluginsDir the directory where plugins are installed for Elasticsearch
* @param sourcePaths a map holding the path to each plugin or module jars, by plugin (or module) name.
* @param tempDir the temp directory for Elasticsearch
* @param logsDir the log directory for Elasticsearch
* @param pidFile path to a pid file for Elasticsearch, or {@code null} if one was not specified
* @param serverPolicyPatch additional entitlements to patch the embedded server layer policy
* @param pluginPolicies maps each plugin name to the corresponding {@link Policy}
* @param scopeResolver a functor to map a Java Class to the component and module it belongs to.
* @param settingResolver a functor to resolve a setting name pattern for one or more Elasticsearch settings.
* @param dataDirs data directories for Elasticsearch
* @param sharedRepoDirs shared repository directories for Elasticsearch
* @param configDir the config directory for Elasticsearch
* @param libDir the lib directory for Elasticsearch
* @param modulesDir the directory where Elasticsearch modules are
* @param pluginsDir the directory where plugins are installed for Elasticsearch
* @param pluginSourcePaths maps each plugin name to the location of that plugin's code
* @param tempDir the temp directory for Elasticsearch
* @param logsDir the log directory for Elasticsearch
* @param pidFile path to a pid file for Elasticsearch, or {@code null} if one was not specified
* @param suppressFailureLogPackages packages for which we do not need or want to log Entitlements failures
*/
public static void bootstrap(
@ -65,35 +68,33 @@ public class EntitlementBootstrap {
Path libDir,
Path modulesDir,
Path pluginsDir,
Map<String, Path> sourcePaths,
Map<String, Collection<Path>> pluginSourcePaths,
Path logsDir,
Path tempDir,
Path pidFile,
@Nullable Path pidFile,
Set<Package> suppressFailureLogPackages
) {
logger.debug("Loading entitlement agent");
if (EntitlementInitialization.initializeArgs != null) {
throw new IllegalStateException("initialization data is already set");
}
PathLookupImpl pathLookup = new PathLookupImpl(
getUserHome(),
configDir,
dataDirs,
sharedRepoDirs,
libDir,
modulesDir,
pluginsDir,
logsDir,
tempDir,
pidFile,
settingResolver
);
EntitlementInitialization.initializeArgs = new EntitlementInitialization.InitializeArgs(
serverPolicyPatch,
pluginPolicies,
scopeResolver,
new PathLookupImpl(
getUserHome(),
configDir,
dataDirs,
sharedRepoDirs,
libDir,
modulesDir,
pluginsDir,
logsDir,
tempDir,
pidFile,
settingResolver
),
sourcePaths,
suppressFailureLogPackages
pathLookup,
suppressFailureLogPackages,
createPolicyManager(pluginPolicies, pathLookup, serverPolicyPatch, scopeResolver, pluginSourcePaths)
);
exportInitializationToAgent();
loadAgent(findAgentJar(), EntitlementInitialization.class.getName());
@ -151,5 +152,24 @@ public class EntitlementBootstrap {
}
}
private static PolicyManager createPolicyManager(
Map<String, Policy> pluginPolicies,
PathLookup pathLookup,
Policy serverPolicyPatch,
Function<Class<?>, PolicyManager.PolicyScope> scopeResolver,
Map<String, Collection<Path>> pluginSourcePaths
) {
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
return new PolicyManager(
HardcodedEntitlements.serverPolicy(pathLookup.pidFile(), serverPolicyPatch),
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
scopeResolver,
pluginSourcePaths,
pathLookup
);
}
private static final Logger logger = LogManager.getLogger(EntitlementBootstrap.class);
}

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.initialization;
package org.elasticsearch.entitlement.bootstrap;
import org.elasticsearch.core.Strings;
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree;
@ -17,6 +17,7 @@ import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlemen
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -44,7 +45,7 @@ class FilesEntitlementsValidation {
.map(x -> ((FilesEntitlement) x))
.findFirst();
if (filesEntitlement.isPresent()) {
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, null);
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, List.of());
validateReadFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, readAccessForbidden);
validateWriteFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, writeAccessForbidden);
}

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.initialization;
package org.elasticsearch.entitlement.bootstrap;
import org.elasticsearch.core.Booleans;
import org.elasticsearch.entitlement.runtime.policy.Policy;

View file

@ -10,11 +10,9 @@
package org.elasticsearch.entitlement.initialization;
import org.elasticsearch.core.Booleans;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.ElasticsearchEntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.PolicyChecker;
import org.elasticsearch.entitlement.runtime.policy.PolicyCheckerImpl;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
@ -22,10 +20,7 @@ import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
@ -69,7 +64,7 @@ public class EntitlementInitialization {
*/
public static void initialize(Instrumentation inst) throws Exception {
// the checker _MUST_ be set before _any_ instrumentation is done
checker = initChecker(createPolicyManager());
checker = initChecker(initializeArgs.policyManager());
initInstrumentation(inst);
}
@ -77,27 +72,15 @@ public class EntitlementInitialization {
* Arguments to {@link #initialize}. Since that's called in a static context from the agent,
* we have no way to pass arguments directly, so we stuff them in here.
*
* @param serverPolicyPatch
* @param pluginPolicies
* @param scopeResolver
* @param pathLookup
* @param sourcePaths
* @param suppressFailureLogPackages
* @param policyManager
*/
public record InitializeArgs(
@Nullable Policy serverPolicyPatch,
Map<String, Policy> pluginPolicies,
Function<Class<?>, PolicyManager.PolicyScope> scopeResolver,
PathLookup pathLookup,
Map<String, Path> sourcePaths,
Set<Package> suppressFailureLogPackages
) {
public record InitializeArgs(PathLookup pathLookup, Set<Package> suppressFailureLogPackages, PolicyManager policyManager) {
public InitializeArgs {
requireNonNull(pluginPolicies);
requireNonNull(scopeResolver);
requireNonNull(pathLookup);
requireNonNull(sourcePaths);
requireNonNull(suppressFailureLogPackages);
requireNonNull(policyManager);
}
}
@ -110,22 +93,6 @@ public class EntitlementInitialization {
);
}
private static PolicyManager createPolicyManager() {
Map<String, Policy> pluginPolicies = initializeArgs.pluginPolicies();
PathLookup pathLookup = initializeArgs.pathLookup();
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
return new PolicyManager(
HardcodedEntitlements.serverPolicy(pathLookup.pidFile(), initializeArgs.serverPolicyPatch()),
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
initializeArgs.scopeResolver(),
initializeArgs.sourcePaths(),
pathLookup
);
}
/**
* If bytecode verification is enabled, ensure these classes get loaded before transforming/retransforming them.
* For these classes, the order in which we transform and verify them matters. Verification during class transformation is at least an

View file

@ -9,7 +9,6 @@
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;
@ -25,6 +24,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -219,7 +219,7 @@ public final class FileAccessTree {
FileAccessTree(
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
Path componentPath,
Collection<Path> componentPaths,
String[] sortedExclusivePaths,
FileAccessTreeComparison comparison
) {
@ -267,9 +267,7 @@ public final class FileAccessTree {
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
pathLookup.getBaseDirPaths(CONFIG).forEach(configPath -> addPathAndMaybeLink.accept(configPath, Mode.READ));
if (componentPath != null) {
addPathAndMaybeLink.accept(componentPath, Mode.READ);
}
componentPaths.forEach(p -> addPathAndMaybeLink.accept(p, Mode.READ));
// TODO: watcher uses javax.activation which looks for known mime types configuration, should this be global or explicit in watcher?
Path jdk = Paths.get(System.getProperty("java.home"));
@ -314,13 +312,13 @@ public final class FileAccessTree {
String moduleName,
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
@Nullable Path componentPath,
Collection<Path> componentPaths,
List<ExclusivePath> exclusivePaths
) {
return new FileAccessTree(
filesEntitlement,
pathLookup,
componentPath,
componentPaths,
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths, DEFAULT_COMPARISON),
DEFAULT_COMPARISON
);
@ -332,9 +330,9 @@ public final class FileAccessTree {
public static FileAccessTree withoutExclusivePaths(
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
@Nullable Path componentPath
Collection<Path> componentPaths
) {
return new FileAccessTree(filesEntitlement, pathLookup, componentPath, new String[0], DEFAULT_COMPARISON);
return new FileAccessTree(filesEntitlement, pathLookup, componentPaths, new String[0], DEFAULT_COMPARISON);
}
public boolean canRead(Path path) {

View file

@ -32,7 +32,9 @@ public interface PathLookup {
Stream<Path> getBaseDirPaths(BaseDir baseDir);
Stream<Path> resolveRelativePaths(BaseDir baseDir, Path relativePath);
/**
* @return all paths obtained by resolving all values of the given setting under all
* paths of the given {@code baseDir}.
*/
Stream<Path> resolveSettingPaths(BaseDir baseDir, String settingName);
}

View file

@ -66,11 +66,6 @@ public record PathLookupImpl(
};
}
@Override
public Stream<Path> resolveRelativePaths(BaseDir baseDir, Path relativePath) {
return getBaseDirPaths(baseDir).map(path -> path.resolve(relativePath));
}
@Override
public Stream<Path> resolveSettingPaths(BaseDir baseDir, String settingName) {
List<Path> relativePaths = settingResolver.apply(settingName)

View file

@ -21,6 +21,7 @@ import java.lang.module.ModuleReference;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -141,17 +142,22 @@ public class PolicyManager {
}
}
private FileAccessTree getDefaultFileAccess(Path componentPath) {
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPath);
private FileAccessTree getDefaultFileAccess(Collection<Path> componentPaths) {
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPaths);
}
// pkg private for testing
ModuleEntitlements defaultEntitlements(String componentName, Path componentPath, String moduleName) {
return new ModuleEntitlements(componentName, Map.of(), getDefaultFileAccess(componentPath), getLogger(componentName, moduleName));
ModuleEntitlements defaultEntitlements(String componentName, Collection<Path> componentPaths, String moduleName) {
return new ModuleEntitlements(componentName, Map.of(), getDefaultFileAccess(componentPaths), getLogger(componentName, moduleName));
}
// pkg private for testing
ModuleEntitlements policyEntitlements(String componentName, Path componentPath, String moduleName, List<Entitlement> entitlements) {
ModuleEntitlements policyEntitlements(
String componentName,
Collection<Path> componentPaths,
String moduleName,
List<Entitlement> entitlements
) {
FilesEntitlement filesEntitlement = FilesEntitlement.EMPTY;
for (Entitlement entitlement : entitlements) {
if (entitlement instanceof FilesEntitlement) {
@ -161,7 +167,7 @@ public class PolicyManager {
return new ModuleEntitlements(
componentName,
entitlements.stream().collect(groupingBy(Entitlement::getClass)),
FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPath, exclusivePaths),
FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPaths, exclusivePaths),
getLogger(componentName, moduleName)
);
}
@ -203,7 +209,7 @@ public class PolicyManager {
.filter(m -> SYSTEM_LAYER_MODULES.contains(m) == false)
.collect(Collectors.toUnmodifiableSet());
private final Map<String, Path> sourcePaths;
private final Map<String, Collection<Path>> pluginSourcePaths;
/**
* Paths that are only allowed for a single module. Used to generate
@ -217,7 +223,7 @@ public class PolicyManager {
List<Entitlement> apmAgentEntitlements,
Map<String, Policy> pluginPolicies,
Function<Class<?>, PolicyScope> scopeResolver,
Map<String, Path> sourcePaths,
Map<String, Collection<Path>> pluginSourcePaths,
PathLookup pathLookup
) {
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy));
@ -226,7 +232,7 @@ public class PolicyManager {
.stream()
.collect(toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue())));
this.scopeResolver = scopeResolver;
this.sourcePaths = sourcePaths;
this.pluginSourcePaths = pluginSourcePaths;
this.pathLookup = requireNonNull(pathLookup);
List<ExclusiveFileEntitlement> exclusiveFileEntitlements = new ArrayList<>();
@ -302,41 +308,42 @@ public class PolicyManager {
serverEntitlements,
moduleName,
SERVER.componentName,
getComponentPathFromClass(requestingClass)
getComponentPathsFromClass(requestingClass)
);
}
case APM_AGENT -> {
// The APM agent is the only thing running non-modular in the system classloader
return policyEntitlements(
APM_AGENT.componentName,
getComponentPathFromClass(requestingClass),
getComponentPathsFromClass(requestingClass),
ALL_UNNAMED,
apmAgentEntitlements
);
}
case UNKNOWN -> {
return defaultEntitlements(UNKNOWN.componentName, null, moduleName);
return defaultEntitlements(UNKNOWN.componentName, List.of(), moduleName);
}
default -> {
assert policyScope.kind() == PLUGIN;
var pluginEntitlements = pluginsEntitlements.get(componentName);
Collection<Path> componentPaths = pluginSourcePaths.getOrDefault(componentName, List.of());
if (pluginEntitlements == null) {
return defaultEntitlements(componentName, sourcePaths.get(componentName), moduleName);
return defaultEntitlements(componentName, componentPaths, moduleName);
} else {
return getModuleScopeEntitlements(pluginEntitlements, moduleName, componentName, sourcePaths.get(componentName));
return getModuleScopeEntitlements(pluginEntitlements, moduleName, componentName, componentPaths);
}
}
}
}
// pkg private for testing
static Path getComponentPathFromClass(Class<?> requestingClass) {
static Collection<Path> getComponentPathsFromClass(Class<?> requestingClass) {
var codeSource = requestingClass.getProtectionDomain().getCodeSource();
if (codeSource == null) {
return null;
return List.of();
}
try {
return Paths.get(codeSource.getLocation().toURI());
return List.of(Paths.get(codeSource.getLocation().toURI()));
} catch (Exception e) {
// If we get a URISyntaxException, or any other Exception due to an invalid URI, we return null to safely skip this location
generalLogger.info(
@ -344,7 +351,7 @@ public class PolicyManager {
requestingClass.getName(),
codeSource.getLocation().toString()
);
return null;
return List.of();
}
}
@ -352,13 +359,13 @@ public class PolicyManager {
Map<String, List<Entitlement>> scopeEntitlements,
String scopeName,
String componentName,
Path componentPath
Collection<Path> componentPaths
) {
var entitlements = scopeEntitlements.get(scopeName);
if (entitlements == null) {
return defaultEntitlements(componentName, componentPath, scopeName);
return defaultEntitlements(componentName, componentPaths, scopeName);
}
return policyEntitlements(componentName, componentPath, scopeName, entitlements);
return policyEntitlements(componentName, componentPaths, scopeName, entitlements);
}
/**

View file

@ -118,7 +118,7 @@ public record FilesEntitlement(List<FileData> filesData) implements Entitlement
@Override
public Stream<Path> resolvePaths(PathLookup pathLookup) {
return pathLookup.resolveRelativePaths(baseDir, relativePath);
return pathLookup.getBaseDirPaths(baseDir).map(path -> path.resolve(relativePath));
}
@Override

View file

@ -0,0 +1,55 @@
/*
* 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;
import org.elasticsearch.entitlement.bridge.Util;
import org.elasticsearch.test.ESTestCase;
import static org.elasticsearch.entitlement.BridgeUtilTests.MockSensitiveClass.mockSensitiveMethod;
/**
* Note: this is not in the bridge package because that is a uniquely bad one to use for tests.
* Since:
* <ol>
* <li>
* we must patch the bridge module into {@code java.base} for it to be reachable
* from JDK methods,
* </li>
* <li>
* the bridge module exports {@code org.elasticsearch.entitlement.bridge}, and
* </li>
* <li>
* Java forbids split packages
* </li>
* </ol>
*
* ...therefore, we'll be unable to load any tests in the {@code org.elasticsearch.entitlement.bridge}
* package from the classpath.
* <p>
* Hence, we put this test in another package. It's still accessible during testing, though,
* because we export the bridge to `ALL-UNNAMED` anyway.
*/
public class BridgeUtilTests extends ESTestCase {
public void testCallerClass() {
assertEquals(BridgeUtilTests.class, mockSensitiveMethod());
}
/**
* A separate class so the stack walk can discern the sensitive method's own class
* from that of its caller.
*/
static class MockSensitiveClass {
public static Class<?> mockSensitiveMethod() {
return Util.getCallerClass();
}
}
}

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.initialization;
package org.elasticsearch.entitlement.bootstrap;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;

View file

@ -1,32 +0,0 @@
/*
* 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.bridge;
import org.elasticsearch.test.ESTestCase;
import static org.elasticsearch.entitlement.bridge.UtilTests.MockSensitiveClass.mockSensitiveMethod;
public class UtilTests extends ESTestCase {
public void testCallerClass() {
assertEquals(UtilTests.class, mockSensitiveMethod());
}
/**
* A separate class so the stack walk can discern the sensitive method's own class
* from that of its caller.
*/
static class MockSensitiveClass {
public static Class<?> mockSensitiveMethod() {
return Util.getCallerClass();
}
}
}

View file

@ -299,15 +299,15 @@ public class FileAccessTreeTests extends ESTestCase {
}
public void testTempDirAccess() {
var tree = FileAccessTree.of("test-component", "test-module", FilesEntitlement.EMPTY, TEST_PATH_LOOKUP, null, List.of());
assertThat(tree.canRead(TEST_PATH_LOOKUP.resolveRelativePaths(TEMP, Path.of("")).findFirst().get()), is(true));
assertThat(tree.canWrite(TEST_PATH_LOOKUP.resolveRelativePaths(TEMP, Path.of("")).findFirst().get()), is(true));
var tree = FileAccessTree.of("test-component", "test-module", FilesEntitlement.EMPTY, TEST_PATH_LOOKUP, List.of(), List.of());
assertThat(tree.canRead(TEST_PATH_LOOKUP.getBaseDirPaths(TEMP).findFirst().get()), is(true));
assertThat(tree.canWrite(TEST_PATH_LOOKUP.getBaseDirPaths(TEMP).findFirst().get()), is(true));
}
public void testConfigDirAccess() {
var tree = FileAccessTree.of("test-component", "test-module", FilesEntitlement.EMPTY, TEST_PATH_LOOKUP, null, List.of());
assertThat(tree.canRead(TEST_PATH_LOOKUP.resolveRelativePaths(CONFIG, Path.of("")).findFirst().get()), is(true));
assertThat(tree.canWrite(TEST_PATH_LOOKUP.resolveRelativePaths(CONFIG, Path.of("")).findFirst().get()), is(false));
var tree = FileAccessTree.of("test-component", "test-module", FilesEntitlement.EMPTY, TEST_PATH_LOOKUP, List.of(), List.of());
assertThat(tree.canRead(TEST_PATH_LOOKUP.getBaseDirPaths(CONFIG).findFirst().get()), is(true));
assertThat(tree.canWrite(TEST_PATH_LOOKUP.getBaseDirPaths(CONFIG).findFirst().get()), is(false));
}
public void testBasicExclusiveAccess() {
@ -504,7 +504,7 @@ public class FileAccessTreeTests extends ESTestCase {
}
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, List.of(), exclusivePaths);
}
static FilesEntitlement entitlement(String... values) {

View file

@ -32,6 +32,7 @@ import java.lang.module.ModuleFinder;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -88,14 +89,14 @@ public class PolicyManagerTests extends ESTestCase {
AtomicReference<PolicyScope> policyScope = new AtomicReference<>();
// A common policy with a variety of entitlements to test
Path thisSourcePath = PolicyManager.getComponentPathFromClass(getClass());
var plugin1SourcePath = Path.of("modules", "plugin1");
Collection<Path> thisSourcePaths = PolicyManager.getComponentPathsFromClass(getClass());
var plugin1SourcePaths = List.of(Path.of("modules", "plugin1"));
var policyManager = new PolicyManager(
new Policy("server", List.of(new Scope("org.example.httpclient", List.of(new OutboundNetworkEntitlement())))),
List.of(),
Map.of("plugin1", new Policy("plugin1", List.of(new Scope("plugin.module1", List.of(new ExitVMEntitlement()))))),
c -> policyScope.get(),
Map.of("plugin1", plugin1SourcePath),
Map.of("plugin1", plugin1SourcePaths),
TEST_PATH_LOOKUP
);
@ -107,7 +108,7 @@ public class PolicyManagerTests extends ESTestCase {
getClass(),
policyManager.policyEntitlements(
SERVER.componentName,
thisSourcePath,
thisSourcePaths,
"org.example.httpclient",
List.of(new OutboundNetworkEntitlement())
),
@ -118,7 +119,7 @@ public class PolicyManagerTests extends ESTestCase {
resetAndCheckEntitlements(
"Default entitlements for unspecified module",
getClass(),
policyManager.defaultEntitlements(SERVER.componentName, thisSourcePath, "plugin.unspecifiedModule"),
policyManager.defaultEntitlements(SERVER.componentName, thisSourcePaths, "plugin.unspecifiedModule"),
policyManager
);
@ -126,7 +127,7 @@ public class PolicyManagerTests extends ESTestCase {
resetAndCheckEntitlements(
"Specified entitlements for plugin",
getClass(),
policyManager.policyEntitlements("plugin1", plugin1SourcePath, "plugin.module1", List.of(new ExitVMEntitlement())),
policyManager.policyEntitlements("plugin1", plugin1SourcePaths, "plugin.module1", List.of(new ExitVMEntitlement())),
policyManager
);
@ -134,7 +135,7 @@ public class PolicyManagerTests extends ESTestCase {
resetAndCheckEntitlements(
"Default entitlements for plugin",
getClass(),
policyManager.defaultEntitlements("plugin1", plugin1SourcePath, "plugin.unspecifiedModule"),
policyManager.defaultEntitlements("plugin1", plugin1SourcePaths, "plugin.unspecifiedModule"),
policyManager
);
}
@ -248,7 +249,7 @@ public class PolicyManagerTests extends ESTestCase {
)
),
c -> PolicyScope.plugin("plugin1", moduleName(c)),
Map.of("plugin1", Path.of("modules", "plugin1")),
Map.of("plugin1", List.of(Path.of("modules", "plugin1"))),
TEST_PATH_LOOKUP
)
);
@ -298,7 +299,7 @@ public class PolicyManagerTests extends ESTestCase {
)
),
c -> PolicyScope.plugin("", moduleName(c)),
Map.of("plugin1", Path.of("modules", "plugin1"), "plugin2", Path.of("modules", "plugin2")),
Map.of("plugin1", List.of(Path.of("modules", "plugin1")), "plugin2", List.of(Path.of("modules", "plugin2"))),
TEST_PATH_LOOKUP
)
);

View file

@ -63,6 +63,7 @@ import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Security;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -247,8 +248,8 @@ class Elasticsearch {
pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles, findPluginsWithNativeAccess(pluginPolicies));
var scopeResolver = ScopeResolver.create(pluginsLoader.pluginLayers(), APM_AGENT_PACKAGE_NAME);
Map<String, Path> sourcePaths = Stream.concat(modulesBundles.stream(), pluginsBundles.stream())
.collect(Collectors.toUnmodifiableMap(bundle -> bundle.pluginDescriptor().getName(), PluginBundle::getDir));
Map<String, Collection<Path>> pluginSourcePaths = Stream.concat(modulesBundles.stream(), pluginsBundles.stream())
.collect(Collectors.toUnmodifiableMap(bundle -> bundle.pluginDescriptor().getName(), bundle -> List.of(bundle.getDir())));
EntitlementBootstrap.bootstrap(
serverPolicyPatch,
pluginPolicies,
@ -260,7 +261,7 @@ class Elasticsearch {
nodeEnv.libDir(),
nodeEnv.modulesDir(),
nodeEnv.pluginsDir(),
sourcePaths,
pluginSourcePaths,
nodeEnv.logsDir(),
nodeEnv.tmpDir(),
args.pidFile(),

View file

@ -31,7 +31,7 @@ public class TestBuildInfoParser {
private static final ObjectParser<Builder, Void> PARSER = new ObjectParser<>("test_build_info", Builder::new);
private static final ObjectParser<Location, Void> LOCATION_PARSER = new ObjectParser<>("location", Location::new);
static {
LOCATION_PARSER.declareString(Location::representativeClass, new ParseField("representativeClass"));
LOCATION_PARSER.declareString(Location::representativeClass, new ParseField("representative_class"));
LOCATION_PARSER.declareString(Location::module, new ParseField("module"));
PARSER.declareString(Builder::component, new ParseField("component"));
@ -79,9 +79,11 @@ public class TestBuildInfoParser {
var xContent = XContentFactory.xContent(XContentType.JSON);
List<TestBuildInfo> pluginsTestBuildInfos = new ArrayList<>();
var resources = TestBuildInfoParser.class.getClassLoader().getResources(PLUGIN_TEST_BUILD_INFO_RESOURCES);
URL resource;
while ((resource = resources.nextElement()) != null) {
try (var stream = getStream(resource); var parser = xContent.createParser(XContentParserConfiguration.EMPTY, stream)) {
while (resources.hasMoreElements()) {
try (
var stream = getStream(resources.nextElement());
var parser = xContent.createParser(XContentParserConfiguration.EMPTY, stream)
) {
pluginsTestBuildInfos.add(fromXContent(parser));
}
}

View file

@ -9,12 +9,30 @@
package org.elasticsearch.entitlement.bootstrap;
import org.elasticsearch.entitlement.initialization.TestEntitlementInitialization;
import org.elasticsearch.bootstrap.TestBuildInfo;
import org.elasticsearch.bootstrap.TestBuildInfoParser;
import org.elasticsearch.bootstrap.TestScopeResolver;
import org.elasticsearch.core.Strings;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.initialization.EntitlementInitialization;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
import org.elasticsearch.entitlement.runtime.policy.PolicyParser;
import org.elasticsearch.entitlement.runtime.policy.TestPolicyManager;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.plugins.PluginDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
public class TestEntitlementBootstrap {
@ -24,36 +42,99 @@ public class TestEntitlementBootstrap {
/**
* Activates entitlement checking in tests.
*/
public static void bootstrap() {
TestEntitlementInitialization.initializeArgs = new TestEntitlementInitialization.InitializeArgs(new TestPathLookup());
public static void bootstrap() throws IOException {
TestPathLookup pathLookup = new TestPathLookup();
EntitlementInitialization.initializeArgs = new EntitlementInitialization.InitializeArgs(
pathLookup,
Set.of(),
createPolicyManager(pathLookup)
);
logger.debug("Loading entitlement agent");
EntitlementBootstrap.loadAgent(EntitlementBootstrap.findAgentJar(), TestEntitlementInitialization.class.getName());
EntitlementBootstrap.loadAgent(EntitlementBootstrap.findAgentJar(), EntitlementInitialization.class.getName());
}
private record TestPathLookup() implements PathLookup {
@Override
public Path pidFile() {
throw notYetImplemented();
return null;
}
@Override
public Stream<Path> getBaseDirPaths(BaseDir baseDir) {
throw notYetImplemented();
}
@Override
public Stream<Path> resolveRelativePaths(BaseDir baseDir, Path relativePath) {
throw notYetImplemented();
return Stream.empty();
}
@Override
public Stream<Path> resolveSettingPaths(BaseDir baseDir, String settingName) {
throw notYetImplemented();
}
private static IllegalStateException notYetImplemented() {
return new IllegalStateException("not yet implemented");
return Stream.empty();
}
}
private static PolicyManager createPolicyManager(PathLookup pathLookup) throws IOException {
var pluginsTestBuildInfo = TestBuildInfoParser.parseAllPluginTestBuildInfo();
var serverTestBuildInfo = TestBuildInfoParser.parseServerTestBuildInfo();
var scopeResolver = TestScopeResolver.createScopeResolver(serverTestBuildInfo, pluginsTestBuildInfo);
List<String> pluginNames = pluginsTestBuildInfo.stream().map(TestBuildInfo::component).toList();
var pluginDescriptors = parsePluginsDescriptors(pluginNames);
var pluginsData = pluginDescriptors.stream()
.map(descriptor -> new TestPluginData(descriptor.getName(), descriptor.isModular(), false))
.toList();
Map<String, Policy> pluginPolicies = parsePluginsPolicies(pluginsData);
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
return new TestPolicyManager(
HardcodedEntitlements.serverPolicy(null, null),
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
scopeResolver,
Map.of(),
pathLookup
);
}
private record TestPluginData(String pluginName, boolean isModular, boolean isExternalPlugin) {}
private static Map<String, Policy> parsePluginsPolicies(List<TestPluginData> pluginsData) {
Map<String, Policy> policies = new HashMap<>();
for (var pluginData : pluginsData) {
String pluginName = pluginData.pluginName();
var resourceName = Strings.format("META-INF/es-plugins/%s/entitlement-policy.yaml", pluginName);
var resource = EntitlementInitialization.class.getClassLoader().getResource(resourceName);
if (resource != null) {
try (var inputStream = getStream(resource)) {
policies.put(pluginName, new PolicyParser(inputStream, pluginName, pluginData.isExternalPlugin()).parsePolicy());
} catch (IOException e) {
throw new IllegalArgumentException(Strings.format("Cannot read policy for plugin [%s]", pluginName), e);
}
}
}
return policies;
}
private static List<PluginDescriptor> parsePluginsDescriptors(List<String> pluginNames) {
List<PluginDescriptor> descriptors = new ArrayList<>();
for (var pluginName : pluginNames) {
var resourceName = Strings.format("META-INF/es-plugins/%s/plugin-descriptor.properties", pluginName);
var resource = EntitlementInitialization.class.getClassLoader().getResource(resourceName);
if (resource != null) {
try (var inputStream = getStream(resource)) {
descriptors.add(PluginDescriptor.readInternalDescriptorFromStream(inputStream));
} catch (IOException e) {
throw new IllegalArgumentException(Strings.format("Cannot read descriptor for plugin [%s]", pluginName), e);
}
}
}
return descriptors;
}
@SuppressForbidden(reason = "URLs from class loader")
private static InputStream getStream(URL resource) throws IOException {
return resource.openStream();
}
}

View file

@ -1,123 +0,0 @@
/*
* 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.initialization;
import org.elasticsearch.bootstrap.TestBuildInfo;
import org.elasticsearch.bootstrap.TestBuildInfoParser;
import org.elasticsearch.bootstrap.TestScopeResolver;
import org.elasticsearch.core.Strings;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.ElasticsearchEntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
import org.elasticsearch.entitlement.runtime.policy.PolicyParser;
import org.elasticsearch.entitlement.runtime.policy.TestPolicyManager;
import org.elasticsearch.plugins.PluginDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.elasticsearch.entitlement.initialization.EntitlementInitialization.initInstrumentation;
/**
* Test-specific version of {@code EntitlementInitialization}
*/
public class TestEntitlementInitialization {
private static ElasticsearchEntitlementChecker checker;
public static InitializeArgs initializeArgs;
// Note: referenced by bridge reflectively
public static EntitlementChecker checker() {
return checker;
}
public static void initialize(Instrumentation inst) throws Exception {
checker = EntitlementInitialization.initChecker(createPolicyManager(initializeArgs.pathLookup()));
initInstrumentation(inst);
}
public record InitializeArgs(PathLookup pathLookup) {}
private record TestPluginData(String pluginName, boolean isModular, boolean isExternalPlugin) {}
private static Map<String, Policy> parsePluginsPolicies(List<TestPluginData> pluginsData) {
Map<String, Policy> policies = new HashMap<>();
for (var pluginData : pluginsData) {
String pluginName = pluginData.pluginName();
var resourceName = Strings.format("META-INF/es-plugins/%s/entitlement-policy.yaml", pluginName);
var resource = TestEntitlementInitialization.class.getClassLoader().getResource(resourceName);
if (resource != null) {
try (var inputStream = getStream(resource)) {
policies.put(pluginName, new PolicyParser(inputStream, pluginName, pluginData.isExternalPlugin()).parsePolicy());
} catch (IOException e) {
throw new IllegalArgumentException(Strings.format("Cannot read policy for plugin [%s]", pluginName), e);
}
}
}
return policies;
}
private static List<PluginDescriptor> parsePluginsDescriptors(List<String> pluginNames) {
List<PluginDescriptor> descriptors = new ArrayList<>();
for (var pluginName : pluginNames) {
var resourceName = Strings.format("META-INF/es-plugins/%s/plugin-descriptor.properties", pluginName);
var resource = TestEntitlementInitialization.class.getClassLoader().getResource(resourceName);
if (resource != null) {
try (var inputStream = getStream(resource)) {
descriptors.add(PluginDescriptor.readInternalDescriptorFromStream(inputStream));
} catch (IOException e) {
throw new IllegalArgumentException(Strings.format("Cannot read descriptor for plugin [%s]", pluginName), e);
}
}
}
return descriptors;
}
@SuppressForbidden(reason = "URLs from class loader")
private static InputStream getStream(URL resource) throws IOException {
return resource.openStream();
}
private static PolicyManager createPolicyManager(PathLookup pathLookup) throws IOException {
var pluginsTestBuildInfo = TestBuildInfoParser.parseAllPluginTestBuildInfo();
var serverTestBuildInfo = TestBuildInfoParser.parseServerTestBuildInfo();
var scopeResolver = TestScopeResolver.createScopeResolver(serverTestBuildInfo, pluginsTestBuildInfo);
List<String> pluginNames = pluginsTestBuildInfo.stream().map(TestBuildInfo::component).toList();
var pluginDescriptors = parsePluginsDescriptors(pluginNames);
var pluginsData = pluginDescriptors.stream()
.map(descriptor -> new TestPluginData(descriptor.getName(), descriptor.isModular(), false))
.toList();
Map<String, Policy> pluginPolicies = parsePluginsPolicies(pluginsData);
FilesEntitlementsValidation.validate(pluginPolicies, pathLookup);
return new TestPolicyManager(
HardcodedEntitlements.serverPolicy(null, null),
HardcodedEntitlements.agentEntitlements(),
pluginPolicies,
scopeResolver,
Map.of(),
pathLookup
);
}
}

View file

@ -23,11 +23,6 @@ public class TestPathLookup implements PathLookup {
return Stream.empty();
}
@Override
public Stream<Path> resolveRelativePaths(BaseDir baseDir, Path relativePath) {
return Stream.empty();
}
@Override
public Stream<Path> resolveSettingPaths(BaseDir baseDir, String settingName) {
return Stream.empty();

View file

@ -12,6 +12,7 @@ package org.elasticsearch.entitlement.runtime.policy;
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@ -22,10 +23,10 @@ public class TestPolicyManager extends PolicyManager {
List<Entitlement> apmAgentEntitlements,
Map<String, Policy> pluginPolicies,
Function<Class<?>, PolicyScope> scopeResolver,
Map<String, Path> sourcePaths,
Map<String, Collection<Path>> pluginSourcePaths,
PathLookup pathLookup
) {
super(serverPolicy, apmAgentEntitlements, pluginPolicies, scopeResolver, sourcePaths, pathLookup);
super(serverPolicy, apmAgentEntitlements, pluginPolicies, scopeResolver, pluginSourcePaths, pathLookup);
}
/**

View file

@ -28,19 +28,19 @@ public class TestBuildInfoParserTests extends ESTestCase {
"component": "lang-painless",
"locations": [
{
"representativeClass": "Location.class",
"representative_class": "Location.class",
"module": "org.elasticsearch.painless"
},
{
"representativeClass": "org/objectweb/asm/AnnotationVisitor.class",
"representative_class": "org/objectweb/asm/AnnotationVisitor.class",
"module": "org.objectweb.asm"
},
{
"representativeClass": "org/antlr/v4/runtime/ANTLRErrorListener.class",
"representative_class": "org/antlr/v4/runtime/ANTLRErrorListener.class",
"module": "org.antlr.antlr4.runtime"
},
{
"representativeClass": "org/objectweb/asm/commons/AdviceAdapter.class",
"representative_class": "org/objectweb/asm/commons/AdviceAdapter.class",
"module": "org.objectweb.asm.commons"
}
]