From 9b3037c2bc6355bb5eef8de28e92c01ea9f2d645 Mon Sep 17 00:00:00 2001 From: Patrick Doyle <810052+prdoyle@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:10:04 -0400 Subject: [PATCH] Refactor: ScopeResolver (#126921) (#127174) * Fix: use getScopeName consistently * Rename PolicyManagerTests method * Refacor: simplify PluginsResolver.create * Change PluginsResolver to ScopeResolver * Move boot layer test to ScopeResolverTests * [CI] Auto commit changes from spotless * Rename PolicyScope * Add ComponentKind enum * Package private componentName field --------- Co-authored-by: elasticsearchmachine --- .../bootstrap/EntitlementBootstrap.java | 11 +- .../EntitlementInitialization.java | 4 +- .../runtime/policy/PolicyManager.java | 143 ++++++++++-------- .../runtime/policy/PolicyManagerTests.java | 93 +++++------- .../bootstrap/Elasticsearch.java | 5 +- .../bootstrap/PluginsResolver.java | 47 ------ .../bootstrap/ScopeResolver.java | 81 ++++++++++ ...lverTests.java => ScopeResolverTests.java} | 109 ++++++------- 8 files changed, 260 insertions(+), 233 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/bootstrap/PluginsResolver.java create mode 100644 server/src/main/java/org/elasticsearch/bootstrap/ScopeResolver.java rename server/src/test/java/org/elasticsearch/bootstrap/{PluginsResolverTests.java => ScopeResolverTests.java} (65%) diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java index 63651eaa86a14..9e3a15efd6d6f 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java @@ -21,6 +21,7 @@ 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; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -39,14 +40,14 @@ public class EntitlementBootstrap { public record BootstrapArgs( @Nullable Policy serverPolicyPatch, Map pluginPolicies, - Function, String> pluginResolver, + Function, PolicyManager.PolicyScope> scopeResolver, PathLookup pathLookup, Map sourcePaths, Set> suppressFailureLogClasses ) { public BootstrapArgs { requireNonNull(pluginPolicies); - requireNonNull(pluginResolver); + requireNonNull(scopeResolver); requireNonNull(pathLookup); requireNonNull(sourcePaths); requireNonNull(suppressFailureLogClasses); @@ -65,7 +66,7 @@ public static BootstrapArgs bootstrapArgs() { * * @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 pluginResolver a functor to map a Java Class to the plugin it belongs to (the plugin 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 @@ -82,7 +83,7 @@ public static BootstrapArgs bootstrapArgs() { public static void bootstrap( Policy serverPolicyPatch, Map pluginPolicies, - Function, String> pluginResolver, + Function, PolicyManager.PolicyScope> scopeResolver, Function> settingResolver, Path[] dataDirs, Path[] sharedRepoDirs, @@ -103,7 +104,7 @@ public static void bootstrap( EntitlementBootstrap.bootstrapArgs = new BootstrapArgs( serverPolicyPatch, pluginPolicies, - pluginResolver, + scopeResolver, new PathLookupImpl( getUserHome(), configDir, diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index dd48ac927feff..5ddc3f75ddfc8 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -90,7 +90,6 @@ */ public class EntitlementInitialization { - private static final String AGENTS_PACKAGE_NAME = "co.elastic.apm.agent"; private static final Module ENTITLEMENTS_MODULE = PolicyManager.class.getModule(); private static ElasticsearchEntitlementChecker manager; @@ -350,9 +349,8 @@ private static PolicyManager createPolicyManager() { serverPolicy, agentEntitlements, pluginPolicies, - EntitlementBootstrap.bootstrapArgs().pluginResolver(), + EntitlementBootstrap.bootstrapArgs().scopeResolver(), EntitlementBootstrap.bootstrapArgs().sourcePaths(), - AGENTS_PACKAGE_NAME, ENTITLEMENTS_MODULE, pathLookup, bootstrapArgs.suppressFailureLogClasses() diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index 67d0d9a5a6b80..929e2f435d7e3 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -59,6 +59,10 @@ import static java.util.zip.ZipFile.OPEN_READ; import static org.elasticsearch.entitlement.bridge.Util.NO_CLASS; import static org.elasticsearch.entitlement.runtime.policy.PathLookup.BaseDir.TEMP; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.APM_AGENT; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.PLUGIN; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.SERVER; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.UNKNOWN; /** * This class is responsible for finding the component (system, server, plugin, agent) for a caller class to check, @@ -128,14 +132,55 @@ public class PolicyManager { */ private static final Logger generalLogger = LogManager.getLogger(PolicyManager.class); - static final String UNKNOWN_COMPONENT_NAME = "(unknown)"; - static final String SERVER_COMPONENT_NAME = "(server)"; - static final String APM_AGENT_COMPONENT_NAME = "(APM agent)"; - static final Class DEFAULT_FILESYSTEM_CLASS = PathUtils.getDefaultFileSystem().getClass(); static final Set MODULES_EXCLUDED_FROM_SYSTEM_MODULES = Set.of("java.desktop"); + /** + * Identifies a particular entitlement {@link Scope} within a {@link Policy}. + */ + public record PolicyScope(ComponentKind kind, String componentName, String moduleName) { + public PolicyScope { + requireNonNull(kind); + requireNonNull(componentName); + requireNonNull(moduleName); + assert kind.componentName == null || kind.componentName.equals(componentName); + } + + public static PolicyScope unknown(String moduleName) { + return new PolicyScope(UNKNOWN, UNKNOWN.componentName, moduleName); + } + + public static PolicyScope server(String moduleName) { + return new PolicyScope(SERVER, SERVER.componentName, moduleName); + } + + public static PolicyScope apmAgent(String moduleName) { + return new PolicyScope(APM_AGENT, APM_AGENT.componentName, moduleName); + } + + public static PolicyScope plugin(String componentName, String moduleName) { + return new PolicyScope(PLUGIN, componentName, moduleName); + } + } + + public enum ComponentKind { + UNKNOWN("(unknown)"), + SERVER("(server)"), + APM_AGENT("(APM agent)"), + PLUGIN(null); + + /** + * If this kind corresponds to a single component, this is that component's name; + * otherwise null. + */ + final String componentName; + + ComponentKind(String componentName) { + this.componentName = componentName; + } + } + /** * This class contains all the entitlements by type, plus the {@link FileAccessTree} for the special case of filesystem entitlements. *

@@ -209,7 +254,7 @@ ModuleEntitlements policyEntitlements(String componentName, Path componentPath, private final Map> serverEntitlements; private final List apmAgentEntitlements; private final Map>> pluginsEntitlements; - private final Function, String> pluginResolver; + private final Function, PolicyScope> scopeResolver; private final PathLookup pathLookup; private final Set> mutedClasses; @@ -245,10 +290,6 @@ private static Set findSystemLayerModules() { .collect(Collectors.toUnmodifiableSet()); private final Map sourcePaths; - /** - * The package name containing classes from the APM agent. - */ - private final String apmAgentPackageName; /** * Frames originating from this module are ignored in the permission logic. @@ -266,9 +307,8 @@ public PolicyManager( Policy serverPolicy, List apmAgentEntitlements, Map pluginPolicies, - Function, String> pluginResolver, + Function, PolicyScope> scopeResolver, Map sourcePaths, - String apmAgentPackageName, Module entitlementsModule, PathLookup pathLookup, Set> suppressFailureLogClasses @@ -278,18 +318,17 @@ public PolicyManager( this.pluginsEntitlements = requireNonNull(pluginPolicies).entrySet() .stream() .collect(toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue()))); - this.pluginResolver = pluginResolver; + this.scopeResolver = scopeResolver; this.sourcePaths = sourcePaths; - this.apmAgentPackageName = apmAgentPackageName; this.entitlementsModule = entitlementsModule; this.pathLookup = requireNonNull(pathLookup); this.mutedClasses = suppressFailureLogClasses; List exclusiveFileEntitlements = new ArrayList<>(); for (var e : serverEntitlements.entrySet()) { - validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue(), exclusiveFileEntitlements); + validateEntitlementsPerModule(SERVER.componentName, e.getKey(), e.getValue(), exclusiveFileEntitlements); } - validateEntitlementsPerModule(APM_AGENT_COMPONENT_NAME, ALL_UNNAMED, apmAgentEntitlements, exclusiveFileEntitlements); + validateEntitlementsPerModule(APM_AGENT.componentName, ALL_UNNAMED, apmAgentEntitlements, exclusiveFileEntitlements); for (var p : pluginsEntitlements.entrySet()) { for (var m : p.getValue().entrySet()) { validateEntitlementsPerModule(p.getKey(), m.getKey(), m.getValue(), exclusiveFileEntitlements); @@ -684,50 +723,40 @@ ModuleEntitlements getEntitlements(Class requestingClass) { } private ModuleEntitlements computeEntitlements(Class requestingClass) { - Module requestingModule = requestingClass.getModule(); - if (isServerModule(requestingModule)) { - return getModuleScopeEntitlements( - serverEntitlements, - requestingModule.getName(), - SERVER_COMPONENT_NAME, - getComponentPathFromClass(requestingClass) - ); - } + var policyScope = scopeResolver.apply(requestingClass); + var componentName = policyScope.componentName(); + var moduleName = policyScope.moduleName(); - // plugins - var pluginName = pluginResolver.apply(requestingClass); - if (pluginName != null) { - var pluginEntitlements = pluginsEntitlements.get(pluginName); - if (pluginEntitlements == null) { - return defaultEntitlements(pluginName, sourcePaths.get(pluginName), requestingModule.getName()); - } else { + switch (policyScope.kind()) { + case SERVER -> { return getModuleScopeEntitlements( - pluginEntitlements, - getScopeName(requestingModule), - pluginName, - sourcePaths.get(pluginName) + serverEntitlements, + moduleName, + SERVER.componentName, + getComponentPathFromClass(requestingClass) ); } - } - - if (requestingModule.isNamed() == false && requestingClass.getPackageName().startsWith(apmAgentPackageName)) { - // The APM agent is the only thing running non-modular in the system classloader - return policyEntitlements( - APM_AGENT_COMPONENT_NAME, - getComponentPathFromClass(requestingClass), - ALL_UNNAMED, - apmAgentEntitlements - ); - } - - return defaultEntitlements(UNKNOWN_COMPONENT_NAME, null, requestingModule.getName()); - } - - private static String getScopeName(Module requestingModule) { - if (requestingModule.isNamed() == false) { - return ALL_UNNAMED; - } else { - return requestingModule.getName(); + case APM_AGENT -> { + // The APM agent is the only thing running non-modular in the system classloader + return policyEntitlements( + APM_AGENT.componentName, + getComponentPathFromClass(requestingClass), + ALL_UNNAMED, + apmAgentEntitlements + ); + } + case UNKNOWN -> { + return defaultEntitlements(UNKNOWN.componentName, null, moduleName); + } + default -> { + assert policyScope.kind() == PLUGIN; + var pluginEntitlements = pluginsEntitlements.get(componentName); + if (pluginEntitlements == null) { + return defaultEntitlements(componentName, sourcePaths.get(componentName), moduleName); + } else { + return getModuleScopeEntitlements(pluginEntitlements, moduleName, componentName, sourcePaths.get(componentName)); + } + } } } @@ -763,10 +792,6 @@ private ModuleEntitlements getModuleScopeEntitlements( return policyEntitlements(componentName, componentPath, scopeName, entitlements); } - private static boolean isServerModule(Module requestingModule) { - return requestingModule.isNamed() && requestingModule.getLayer() == ModuleLayer.boot(); - } - /** * Walks the stack to determine which class should be checked for entitlements. * diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java index 2caa7f1715de7..1d11f30c4ba08 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java @@ -9,9 +9,11 @@ package org.elasticsearch.entitlement.runtime.policy; +import org.elasticsearch.bootstrap.ScopeResolver; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Strings; import org.elasticsearch.entitlement.runtime.policy.PolicyManager.ModuleEntitlements; +import org.elasticsearch.entitlement.runtime.policy.PolicyManager.PolicyScope; import org.elasticsearch.entitlement.runtime.policy.agent.TestAgent; import org.elasticsearch.entitlement.runtime.policy.agent.inner.TestInnerAgent; import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement; @@ -37,7 +39,7 @@ import static java.util.Map.entry; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; -import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.SERVER_COMPONENT_NAME; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.SERVER; import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; @@ -93,9 +95,8 @@ public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() { createEmptyTestServerPolicy(), List.of(), Map.of("plugin1", createPluginPolicy("plugin.module")), - c -> "plugin1", + c -> PolicyScope.plugin("plugin1", moduleName(c)), Map.of("plugin1", plugin1SourcePath), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -123,9 +124,8 @@ public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() { createEmptyTestServerPolicy(), List.of(), Map.of(), - c -> "plugin1", + c -> PolicyScope.plugin("plugin1", moduleName(c)), Map.of("plugin1", plugin1SourcePath), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -153,9 +153,8 @@ public void testGetEntitlementsFailureIsCached() { createEmptyTestServerPolicy(), List.of(), Map.of(), - c -> "plugin1", + c -> PolicyScope.plugin("plugin1", moduleName(c)), Map.of("plugin1", plugin1SourcePath), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -192,9 +191,8 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() { createEmptyTestServerPolicy(), List.of(), Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))), - c -> "plugin2", + c -> PolicyScope.plugin("plugin2", moduleName(c)), Map.of("plugin2", Path.of("modules", "plugin2")), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -207,60 +205,53 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() { assertThat(entitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(true)); } - public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotFoundException { + public void testGetEntitlementsReturnsDefaultOnMissingPolicyForServer() throws ClassNotFoundException { var policyManager = new PolicyManager( createTestServerPolicy("example"), List.of(), Map.of(), - c -> null, + c -> PolicyScope.server(moduleName(c)), Map.of(), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() ); - // Tests do not run modular, so we cannot use a server class. - // But we know that in production code the server module and its classes are in the boot layer. - // So we use a random module in the boot layer, and a random class from that module (not java.base -- it is - // loaded too early) to mimic a class that would be in the server module. - var mockServerClass = ModuleLayer.boot().findLoader("jdk.httpserver").loadClass("com.sun.net.httpserver.HttpServer"); + // Any class will do, since our resolver is hardcoded to use SERVER_COMPONENT_NAME. + // Let's pick one with a known module name. + String httpserverModuleName = "jdk.httpserver"; + var mockServerClass = ModuleLayer.boot().findLoader(httpserverModuleName).loadClass("com.sun.net.httpserver.HttpServer"); var mockServerSourcePath = PolicyManager.getComponentPathFromClass(mockServerClass); var requestingModule = mockServerClass.getModule(); assertEquals( "No policy for this module in server", - policyManager.defaultEntitlements(SERVER_COMPONENT_NAME, mockServerSourcePath, requestingModule.getName()), + policyManager.defaultEntitlements(SERVER.componentName, mockServerSourcePath, httpserverModuleName), policyManager.getEntitlements(mockServerClass) ); assertEquals( - Map.of( - requestingModule, - policyManager.defaultEntitlements(SERVER_COMPONENT_NAME, mockServerSourcePath, requestingModule.getName()) - ), + Map.of(requestingModule, policyManager.defaultEntitlements(SERVER.componentName, mockServerSourcePath, httpserverModuleName)), policyManager.moduleEntitlementsMap ); } public void testGetEntitlementsReturnsEntitlementsForServerModule() throws ClassNotFoundException { + String httpserverModuleName = "jdk.httpserver"; var policyManager = new PolicyManager( - createTestServerPolicy("jdk.httpserver"), + createTestServerPolicy(httpserverModuleName), List.of(), Map.of(), - c -> null, + c -> PolicyScope.server(moduleName(c)), Map.of(), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() ); - // Tests do not run modular, so we cannot use a server class. - // But we know that in production code the server module and its classes are in the boot layer. - // So we use a random module in the boot layer, and a random class from that module (not java.base -- it is - // loaded too early) to mimic a class that would be in the server module. - var mockServerClass = ModuleLayer.boot().findLoader("jdk.httpserver").loadClass("com.sun.net.httpserver.HttpServer"); + // Any class will do, since our resolver is hardcoded to use SERVER_COMPONENT_NAME. + // Let's pick one with a known module name. + var mockServerClass = ModuleLayer.boot().findLoader(httpserverModuleName).loadClass("com.sun.net.httpserver.HttpServer"); var entitlements = policyManager.getEntitlements(mockServerClass); assertThat(entitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(true)); @@ -276,9 +267,8 @@ public void testGetEntitlementsReturnsEntitlementsForPluginModule() throws IOExc createEmptyTestServerPolicy(), List.of(), Map.of("mock-plugin", createPluginPolicy("org.example.plugin")), - c -> "mock-plugin", + c -> PolicyScope.plugin("mock-plugin", moduleName(c)), Map.of("mock-plugin", Path.of("modules", "mock-plugin")), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -297,9 +287,8 @@ public void testGetEntitlementsResultIsCached() { createEmptyTestServerPolicy(), List.of(), Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))), - c -> "plugin2", + c -> PolicyScope.plugin("plugin2", moduleName(c)), Map.of("plugin2", Path.of("modules", "plugin2")), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -321,7 +310,7 @@ public void testGetEntitlementsResultIsCached() { public void testRequestingClassFastPath() throws IOException, ClassNotFoundException { var callerClass = makeClassInItsOwnModule(); - assertEquals(callerClass, policyManager(TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE).requestingClass(callerClass)); + assertEquals(callerClass, policyManager(NO_ENTITLEMENTS_MODULE).requestingClass(callerClass)); } public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoundException { @@ -330,7 +319,7 @@ public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoun var instrumentedClass = makeClassInItsOwnModule(); // The class that called the check method var ignorableClass = makeClassInItsOwnModule(); - var policyManager = policyManager(TEST_AGENTS_PACKAGE_NAME, entitlementsClass.getModule()); + var policyManager = policyManager(entitlementsClass.getModule()); assertEquals( "Skip entitlement library and the instrumented method", @@ -361,9 +350,10 @@ public void testAgentsEntitlements() throws IOException, ClassNotFoundException createEmptyTestServerPolicy(), List.of(new CreateClassLoaderEntitlement()), Map.of(), - c -> c.getPackageName().startsWith(TEST_AGENTS_PACKAGE_NAME) ? null : "test", + c -> c.getPackageName().startsWith(TEST_AGENTS_PACKAGE_NAME) + ? PolicyScope.apmAgent("test.agent.module") + : PolicyScope.plugin("test", "test.plugin.module"), Map.of(), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -391,9 +381,8 @@ public void testDuplicateEntitlements() { ), List.of(), Map.of(), - c -> "test", + c -> PolicyScope.plugin("test", moduleName(c)), Map.of(), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -410,9 +399,8 @@ public void testDuplicateEntitlements() { createEmptyTestServerPolicy(), List.of(new CreateClassLoaderEntitlement(), new CreateClassLoaderEntitlement()), Map.of(), - c -> "test", + c -> PolicyScope.plugin("test", moduleName(c)), Map.of(), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -449,9 +437,8 @@ public void testDuplicateEntitlements() { ) ) ), - c -> "plugin1", + c -> PolicyScope.plugin("plugin1", moduleName(c)), Map.of("plugin1", Path.of("modules", "plugin1")), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -502,9 +489,8 @@ public void testFilesEntitlementsWithExclusive() { ) ) ), - c -> "", + c -> PolicyScope.plugin("", moduleName(c)), Map.of("plugin1", Path.of("modules", "plugin1"), "plugin2", Path.of("modules", "plugin2")), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -556,9 +542,8 @@ public void testFilesEntitlementsWithExclusive() { ) ) ), - c -> "", + c -> PolicyScope.plugin("", moduleName(c)), Map.of(), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -583,9 +568,8 @@ public void testPluginResolverOverridesAgents() { createEmptyTestServerPolicy(), List.of(new CreateClassLoaderEntitlement()), Map.of(), - c -> "test", // Insist that the class is in a plugin + c -> PolicyScope.plugin("test", moduleName(c)), // Insist that the class is in a plugin Map.of(), - TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE, TEST_PATH_LOOKUP, Set.of() @@ -601,14 +585,13 @@ private static Class makeClassInItsOwnModule() throws IOException, ClassNotFo return layer.findLoader("org.example.plugin").loadClass("q.B"); } - private static PolicyManager policyManager(String agentsPackageName, Module entitlementsModule) { + private static PolicyManager policyManager(Module entitlementsModule) { return new PolicyManager( createEmptyTestServerPolicy(), List.of(), Map.of(), - c -> "test", + c -> PolicyScope.plugin("test", moduleName(c)), Map.of(), - agentsPackageName, entitlementsModule, TEST_PATH_LOOKUP, Set.of() @@ -718,4 +701,8 @@ public StackTraceElement toStackTraceElement() { } } + private static String moduleName(Class c) { + return ScopeResolver.getScopeName(c.getModule()); + } + } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java index 6d828301a3420..028dbb20aec76 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java @@ -82,6 +82,7 @@ class Elasticsearch { private static final String POLICY_PATCH_PREFIX = "es.entitlements.policy."; private static final String SERVER_POLICY_PATCH_NAME = POLICY_PATCH_PREFIX + "server"; + private static final String APM_AGENT_PACKAGE_NAME = "co.elastic.apm.agent"; /** * Main entry point for starting elasticsearch. @@ -251,13 +252,13 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles, findPluginsWithNativeAccess(pluginPolicies)); - var pluginsResolver = PluginsResolver.create(pluginsLoader); + var scopeResolver = ScopeResolver.create(pluginsLoader.pluginLayers(), APM_AGENT_PACKAGE_NAME); Map sourcePaths = Stream.concat(modulesBundles.stream(), pluginsBundles.stream()) .collect(Collectors.toUnmodifiableMap(bundle -> bundle.pluginDescriptor().getName(), PluginBundle::getDir)); EntitlementBootstrap.bootstrap( serverPolicyPatch, pluginPolicies, - pluginsResolver::resolveClassToPluginName, + scopeResolver::resolveClassToScope, nodeEnv.settings()::getValues, nodeEnv.dataDirs(), nodeEnv.repoDirs(), diff --git a/server/src/main/java/org/elasticsearch/bootstrap/PluginsResolver.java b/server/src/main/java/org/elasticsearch/bootstrap/PluginsResolver.java deleted file mode 100644 index 256e91cbee16d..0000000000000 --- a/server/src/main/java/org/elasticsearch/bootstrap/PluginsResolver.java +++ /dev/null @@ -1,47 +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.bootstrap; - -import org.elasticsearch.plugins.PluginsLoader; - -import java.util.HashMap; -import java.util.Map; - -class PluginsResolver { - private final Map pluginNameByModule; - - private PluginsResolver(Map pluginNameByModule) { - this.pluginNameByModule = pluginNameByModule; - } - - public static PluginsResolver create(PluginsLoader pluginsLoader) { - Map pluginNameByModule = new HashMap<>(); - - pluginsLoader.pluginLayers().forEach(pluginLayer -> { - var pluginName = pluginLayer.pluginBundle().pluginDescriptor().getName(); - if (pluginLayer.pluginModuleLayer() != null && pluginLayer.pluginModuleLayer() != ModuleLayer.boot()) { - // This plugin is a Java Module - for (var module : pluginLayer.pluginModuleLayer().modules()) { - pluginNameByModule.put(module, pluginName); - } - } else { - // This plugin is not modularized - pluginNameByModule.put(pluginLayer.pluginClassLoader().getUnnamedModule(), pluginName); - } - }); - - return new PluginsResolver(pluginNameByModule); - } - - public String resolveClassToPluginName(Class clazz) { - var module = clazz.getModule(); - return pluginNameByModule.get(module); - } -} diff --git a/server/src/main/java/org/elasticsearch/bootstrap/ScopeResolver.java b/server/src/main/java/org/elasticsearch/bootstrap/ScopeResolver.java new file mode 100644 index 0000000000000..a6b029d759780 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/bootstrap/ScopeResolver.java @@ -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.bootstrap; + +import org.elasticsearch.entitlement.runtime.policy.PolicyManager.PolicyScope; +import org.elasticsearch.plugins.PluginsLoader; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; + +public class ScopeResolver { + private final Map pluginNameByModule; + + /** + * The package name containing classes from the APM agent. + */ + private final String apmAgentPackageName; + + private ScopeResolver(Map pluginNameByModule, String apmAgentPackageName) { + this.pluginNameByModule = pluginNameByModule; + this.apmAgentPackageName = apmAgentPackageName; + } + + public static ScopeResolver create(Stream pluginLayers, String apmAgentPackageName) { + Map pluginNameByModule = new HashMap<>(); + + pluginLayers.forEach(pluginLayer -> { + var pluginName = pluginLayer.pluginBundle().pluginDescriptor().getName(); + if (pluginLayer.pluginModuleLayer() != null && pluginLayer.pluginModuleLayer() != ModuleLayer.boot()) { + // This plugin is a Java Module + for (var module : pluginLayer.pluginModuleLayer().modules()) { + pluginNameByModule.put(module, pluginName); + } + } else { + // This plugin is not modularized + pluginNameByModule.put(pluginLayer.pluginClassLoader().getUnnamedModule(), pluginName); + } + }); + + return new ScopeResolver(pluginNameByModule, apmAgentPackageName); + } + + public PolicyScope resolveClassToScope(Class clazz) { + var module = clazz.getModule(); + var scopeName = getScopeName(module); + if (isServerModule(module)) { + return PolicyScope.server(scopeName); + } + String pluginName = pluginNameByModule.get(module); + if (pluginName != null) { + return PolicyScope.plugin(pluginName, scopeName); + } + if (module.isNamed() == false && clazz.getPackageName().startsWith(apmAgentPackageName)) { + // The APM agent is the only thing running non-modular in the system classloader + return PolicyScope.apmAgent(ALL_UNNAMED); + } + return PolicyScope.unknown(scopeName); + } + + private static boolean isServerModule(Module requestingModule) { + return requestingModule.isNamed() && requestingModule.getLayer() == ModuleLayer.boot(); + } + + public static String getScopeName(Module requestingModule) { + if (requestingModule.isNamed() == false) { + return ALL_UNNAMED; + } else { + return requestingModule.getName(); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/bootstrap/PluginsResolverTests.java b/server/src/test/java/org/elasticsearch/bootstrap/ScopeResolverTests.java similarity index 65% rename from server/src/test/java/org/elasticsearch/bootstrap/PluginsResolverTests.java rename to server/src/test/java/org/elasticsearch/bootstrap/ScopeResolverTests.java index 798b576500d72..88b6edd898446 100644 --- a/server/src/test/java/org/elasticsearch/bootstrap/PluginsResolverTests.java +++ b/server/src/test/java/org/elasticsearch/bootstrap/ScopeResolverTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.bootstrap; +import org.elasticsearch.entitlement.runtime.policy.PolicyManager.PolicyScope; import org.elasticsearch.plugins.PluginBundle; import org.elasticsearch.plugins.PluginDescriptor; import org.elasticsearch.plugins.PluginsLoader; @@ -29,16 +30,33 @@ import java.util.stream.Stream; import static java.util.Map.entry; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ESTestCase.WithoutSecurityManager -public class PluginsResolverTests extends ESTestCase { +public class ScopeResolverTests extends ESTestCase { + /** + * A test agent package name for use in tests. + */ + private static final String TEST_AGENTS_PACKAGE_NAME = "org.elasticsearch.entitlement.runtime.policy.agent"; private record TestPluginLayer(PluginBundle pluginBundle, ClassLoader pluginClassLoader, ModuleLayer pluginModuleLayer) implements PluginsLoader.PluginLayer {} + public void testBootLayer() throws ClassNotFoundException { + ScopeResolver scopeResolver = ScopeResolver.create(Stream.empty(), TEST_AGENTS_PACKAGE_NAME); + + // Tests do not run modular, so we cannot use a server class. + // But we know that in production code the server module and its classes are in the boot layer. + // So we use an arbitrary module in the boot layer, and an arbitrary class from that module (not java.base -- it is + // loaded too early) to mimic a class that would be in the server module. + var mockServerClass = ModuleLayer.boot().findLoader("jdk.httpserver").loadClass("com.sun.net.httpserver.HttpServer"); + + assertEquals(PolicyScope.server("jdk.httpserver"), scopeResolver.resolveClassToScope(mockServerClass)); + } + public void testResolveModularPlugin() throws IOException, ClassNotFoundException { String moduleName = "modular.plugin"; String pluginName = "modular-plugin"; @@ -51,19 +69,12 @@ public void testResolveModularPlugin() throws IOException, ClassNotFoundExceptio var loader = layer.findLoader(moduleName); PluginBundle bundle = createMockBundle(pluginName, moduleName, "p.A"); - PluginsLoader mockPluginsLoader = mock(PluginsLoader.class); + Stream pluginLayers = Stream.of(new TestPluginLayer(bundle, loader, layer)); + ScopeResolver scopeResolver = ScopeResolver.create(pluginLayers, TEST_AGENTS_PACKAGE_NAME); - when(mockPluginsLoader.pluginLayers()).thenReturn(Stream.of(new TestPluginLayer(bundle, loader, layer))); - PluginsResolver pluginsResolver = PluginsResolver.create(mockPluginsLoader); - - var testClass = loader.loadClass("p.A"); - var resolvedPluginName = pluginsResolver.resolveClassToPluginName(testClass); - var unresolvedPluginName1 = pluginsResolver.resolveClassToPluginName(PluginsResolver.class); - var unresolvedPluginName2 = pluginsResolver.resolveClassToPluginName(String.class); - - assertEquals(pluginName, resolvedPluginName); - assertNull(unresolvedPluginName1); - assertNull(unresolvedPluginName2); + assertEquals(PolicyScope.plugin(pluginName, moduleName), scopeResolver.resolveClassToScope(loader.loadClass("p.A"))); + assertEquals(PolicyScope.unknown(ALL_UNNAMED), scopeResolver.resolveClassToScope(ScopeResolver.class)); + assertEquals(PolicyScope.server("java.base"), scopeResolver.resolveClassToScope(String.class)); } public void testResolveMultipleModularPlugins() throws IOException, ClassNotFoundException { @@ -79,20 +90,14 @@ public void testResolveMultipleModularPlugins() throws IOException, ClassNotFoun PluginBundle bundle1 = createMockBundle("plugin1", "module.one", "p.A"); PluginBundle bundle2 = createMockBundle("plugin2", "module.two", "q.B"); - PluginsLoader mockPluginsLoader = mock(PluginsLoader.class); - - when(mockPluginsLoader.pluginLayers()).thenReturn( - Stream.of(new TestPluginLayer(bundle1, loader1, layer1), new TestPluginLayer(bundle2, loader2, layer2)) + Stream pluginLayers = Stream.of( + new TestPluginLayer(bundle1, loader1, layer1), + new TestPluginLayer(bundle2, loader2, layer2) ); - PluginsResolver pluginsResolver = PluginsResolver.create(mockPluginsLoader); - - var testClass1 = loader1.loadClass("p.A"); - var testClass2 = loader2.loadClass("q.B"); - var resolvedPluginName1 = pluginsResolver.resolveClassToPluginName(testClass1); - var resolvedPluginName2 = pluginsResolver.resolveClassToPluginName(testClass2); + ScopeResolver scopeResolver = ScopeResolver.create(pluginLayers, TEST_AGENTS_PACKAGE_NAME); - assertEquals("plugin1", resolvedPluginName1); - assertEquals("plugin2", resolvedPluginName2); + assertEquals(PolicyScope.plugin("plugin1", "module.one"), scopeResolver.resolveClassToScope(loader1.loadClass("p.A"))); + assertEquals(PolicyScope.plugin("plugin2", "module.two"), scopeResolver.resolveClassToScope(loader2.loadClass("q.B"))); } public void testResolveReferencedModulesInModularPlugins() throws IOException, ClassNotFoundException { @@ -116,18 +121,11 @@ public void testResolveReferencedModulesInModularPlugins() throws IOException, C var loader = layer.findLoader("module.two"); PluginBundle bundle = createMockBundle("plugin2", "module.two", "q.B"); - PluginsLoader mockPluginsLoader = mock(PluginsLoader.class); + Stream pluginLayers = Stream.of(new TestPluginLayer(bundle, loader, layer)); + ScopeResolver scopeResolver = ScopeResolver.create(pluginLayers, TEST_AGENTS_PACKAGE_NAME); - when(mockPluginsLoader.pluginLayers()).thenReturn(Stream.of(new TestPluginLayer(bundle, loader, layer))); - PluginsResolver pluginsResolver = PluginsResolver.create(mockPluginsLoader); - - var testClass1 = loader.loadClass("p.A"); - var testClass2 = loader.loadClass("q.B"); - var resolvedPluginName1 = pluginsResolver.resolveClassToPluginName(testClass1); - var resolvedPluginName2 = pluginsResolver.resolveClassToPluginName(testClass2); - - assertEquals("plugin2", resolvedPluginName1); - assertEquals("plugin2", resolvedPluginName2); + assertEquals(PolicyScope.plugin("plugin2", "module.one"), scopeResolver.resolveClassToScope(loader.loadClass("p.A"))); + assertEquals(PolicyScope.plugin("plugin2", "module.two"), scopeResolver.resolveClassToScope(loader.loadClass("q.B"))); } public void testResolveMultipleNonModularPlugins() throws IOException, ClassNotFoundException { @@ -137,26 +135,16 @@ public void testResolveMultipleNonModularPlugins() throws IOException, ClassNotF Path jar2 = createNonModularPluginJar(home, "plugin2", "q", "B"); try (var loader1 = createClassLoader(jar1); var loader2 = createClassLoader(jar2)) { - PluginBundle bundle1 = createMockBundle("plugin1", null, "p.A"); PluginBundle bundle2 = createMockBundle("plugin2", null, "q.B"); - PluginsLoader mockPluginsLoader = mock(PluginsLoader.class); - - when(mockPluginsLoader.pluginLayers()).thenReturn( - Stream.of( - new TestPluginLayer(bundle1, loader1, ModuleLayer.boot()), - new TestPluginLayer(bundle2, loader2, ModuleLayer.boot()) - ) + Stream pluginLayers = Stream.of( + new TestPluginLayer(bundle1, loader1, ModuleLayer.boot()), + new TestPluginLayer(bundle2, loader2, ModuleLayer.boot()) ); - PluginsResolver pluginsResolver = PluginsResolver.create(mockPluginsLoader); + ScopeResolver scopeResolver = ScopeResolver.create(pluginLayers, TEST_AGENTS_PACKAGE_NAME); - var testClass1 = loader1.loadClass("p.A"); - var testClass2 = loader2.loadClass("q.B"); - var resolvedPluginName1 = pluginsResolver.resolveClassToPluginName(testClass1); - var resolvedPluginName2 = pluginsResolver.resolveClassToPluginName(testClass2); - - assertEquals("plugin1", resolvedPluginName1); - assertEquals("plugin2", resolvedPluginName2); + assertEquals(PolicyScope.plugin("plugin1", ALL_UNNAMED), scopeResolver.resolveClassToScope(loader1.loadClass("p.A"))); + assertEquals(PolicyScope.plugin("plugin2", ALL_UNNAMED), scopeResolver.resolveClassToScope(loader2.loadClass("q.B"))); } } @@ -169,19 +157,12 @@ public void testResolveNonModularPlugin() throws IOException, ClassNotFoundExcep try (var loader = createClassLoader(jar)) { PluginBundle bundle = createMockBundle(pluginName, null, "p.A"); - PluginsLoader mockPluginsLoader = mock(PluginsLoader.class); - - when(mockPluginsLoader.pluginLayers()).thenReturn(Stream.of(new TestPluginLayer(bundle, loader, ModuleLayer.boot()))); - PluginsResolver pluginsResolver = PluginsResolver.create(mockPluginsLoader); - - var testClass = loader.loadClass("p.A"); - var resolvedPluginName = pluginsResolver.resolveClassToPluginName(testClass); - var unresolvedPluginName1 = pluginsResolver.resolveClassToPluginName(PluginsResolver.class); - var unresolvedPluginName2 = pluginsResolver.resolveClassToPluginName(String.class); + Stream pluginLayers = Stream.of(new TestPluginLayer(bundle, loader, ModuleLayer.boot())); + ScopeResolver scopeResolver = ScopeResolver.create(pluginLayers, TEST_AGENTS_PACKAGE_NAME); - assertEquals(pluginName, resolvedPluginName); - assertNull(unresolvedPluginName1); - assertNull(unresolvedPluginName2); + assertEquals(PolicyScope.plugin(pluginName, ALL_UNNAMED), scopeResolver.resolveClassToScope(loader.loadClass("p.A"))); + assertEquals(PolicyScope.unknown(ALL_UNNAMED), scopeResolver.resolveClassToScope(ScopeResolver.class)); + assertEquals(PolicyScope.server("java.base"), scopeResolver.resolveClassToScope(String.class)); } }