diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java index b44f04c3a26a4..87521795d8824 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java @@ -60,6 +60,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -77,7 +78,7 @@ public class ScriptScoreBenchmark { private final PluginsService pluginsService = new PluginsService( Settings.EMPTY, null, - PluginsLoader.createPluginsLoader(null, Path.of(System.getProperty("plugins.dir"))) + PluginsLoader.createPluginsLoader(Set.of(), PluginsLoader.loadPluginsBundles(Path.of(System.getProperty("plugins.dir")))) ); private final ScriptModule scriptModule = new ScriptModule(Settings.EMPTY, pluginsService.filterPlugins(ScriptPlugin.class).toList()); 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 257d130302580..496a28a448381 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 @@ -17,32 +17,27 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.entitlement.initialization.EntitlementInitialization; import org.elasticsearch.entitlement.runtime.api.NotEntitledException; +import org.elasticsearch.entitlement.runtime.policy.Policy; import org.elasticsearch.logging.LogManager; 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.function.Function; import static java.util.Objects.requireNonNull; public class EntitlementBootstrap { - public record BootstrapArgs(Collection pluginData, Function, String> pluginResolver) { + public record BootstrapArgs(Map pluginPolicies, Function, String> pluginResolver) { public BootstrapArgs { - requireNonNull(pluginData); + requireNonNull(pluginPolicies); requireNonNull(pluginResolver); } } - public record PluginData(Path pluginPath, boolean isModular, boolean isExternalPlugin) { - public PluginData { - requireNonNull(pluginPath); - } - } - private static BootstrapArgs bootstrapArgs; public static BootstrapArgs bootstrapArgs() { @@ -52,16 +47,16 @@ public static BootstrapArgs bootstrapArgs() { /** * Activates entitlement checking. Once this method returns, calls to methods protected by Entitlements from classes without a valid * policy will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}. - * @param pluginData a collection of (plugin path, boolean, boolean), that holds the paths of all the installed Elasticsearch modules - * and plugins, whether they are Java modular or not, and whether they are Elasticsearch modules or external plugins. + * + * @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). */ - public static void bootstrap(Collection pluginData, Function, String> pluginResolver) { + public static void bootstrap(Map pluginPolicies, Function, String> pluginResolver) { logger.debug("Loading entitlement agent"); if (EntitlementBootstrap.bootstrapArgs != null) { throw new IllegalStateException("plugin data is already set"); } - EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(pluginData, pluginResolver); + EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(pluginPolicies, pluginResolver); exportInitializationToAgent(); loadAgent(findAgentJar()); selfTest(); 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 0ad8fc350026c..d63e30f009c42 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 @@ -9,7 +9,6 @@ package org.elasticsearch.entitlement.initialization; -import org.elasticsearch.core.Strings; import org.elasticsearch.core.internal.provider.ProviderLocator; import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap; import org.elasticsearch.entitlement.bridge.EntitlementChecker; @@ -26,28 +25,17 @@ import org.elasticsearch.entitlement.runtime.policy.OutboundNetworkEntitlement; 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.Scope; -import java.io.IOException; import java.lang.instrument.Instrumentation; -import java.lang.module.ModuleFinder; -import java.lang.module.ModuleReference; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; - /** * Called by the agent during {@code agentmain} to configure the entitlement system, * instantiate and configure an {@link EntitlementChecker}, @@ -57,7 +45,6 @@ */ public class EntitlementInitialization { - private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; private static final Module ENTITLEMENTS_MODULE = PolicyManager.class.getModule(); private static ElasticsearchEntitlementChecker manager; @@ -90,8 +77,8 @@ private static Class[] findClassesToRetransform(Class[] loadedClasses, Set return retransform.toArray(new Class[0]); } - private static PolicyManager createPolicyManager() throws IOException { - Map pluginPolicies = createPluginPolicies(EntitlementBootstrap.bootstrapArgs().pluginData()); + private static PolicyManager createPolicyManager() { + Map pluginPolicies = EntitlementBootstrap.bootstrapArgs().pluginPolicies(); // TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it var serverPolicy = new Policy( @@ -119,62 +106,7 @@ private static PolicyManager createPolicyManager() throws IOException { return new PolicyManager(serverPolicy, agentEntitlements, pluginPolicies, resolver, ENTITLEMENTS_MODULE); } - private static Map createPluginPolicies(Collection pluginData) throws IOException { - Map pluginPolicies = new HashMap<>(pluginData.size()); - for (var entry : pluginData) { - Path pluginRoot = entry.pluginPath(); - String pluginName = pluginRoot.getFileName().toString(); - - final Policy policy = loadPluginPolicy(pluginRoot, entry.isModular(), pluginName, entry.isExternalPlugin()); - - pluginPolicies.put(pluginName, policy); - } - return pluginPolicies; - } - - private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, String pluginName, boolean isExternalPlugin) - throws IOException { - Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); - - final Set moduleNames = getModuleNames(pluginRoot, isModular); - final Policy policy = parsePolicyIfExists(pluginName, policyFile, isExternalPlugin); - - // TODO: should this check actually be part of the parser? - for (Scope scope : policy.scopes()) { - if (moduleNames.contains(scope.moduleName()) == false) { - throw new IllegalStateException( - Strings.format( - "Invalid module name in policy: plugin [%s] does not have module [%s]; available modules [%s]; policy file [%s]", - pluginName, - scope.moduleName(), - String.join(", ", moduleNames), - policyFile - ) - ); - } - } - return policy; - } - - private static Policy parsePolicyIfExists(String pluginName, Path policyFile, boolean isExternalPlugin) throws IOException { - if (Files.exists(policyFile)) { - return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName, isExternalPlugin).parsePolicy(); - } - return new Policy(pluginName, List.of()); - } - - private static Set getModuleNames(Path pluginRoot, boolean isModular) { - if (isModular) { - ModuleFinder moduleFinder = ModuleFinder.of(pluginRoot); - Set moduleReferences = moduleFinder.findAll(); - - return moduleReferences.stream().map(mr -> mr.descriptor().name()).collect(Collectors.toUnmodifiableSet()); - } - // When isModular == false we use the same "ALL-UNNAMED" constant as the JDK to indicate (any) unnamed module for this plugin - return Set.of(ALL_UNNAMED); - } - - private static ElasticsearchEntitlementChecker initChecker() throws IOException { + private static ElasticsearchEntitlementChecker initChecker() { final PolicyManager policyManager = createPolicyManager(); int javaVersion = Runtime.version().feature(); diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtils.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtils.java new file mode 100644 index 0000000000000..6e1ea8551825b --- /dev/null +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.entitlement.runtime.policy; + +import org.elasticsearch.core.Strings; + +import java.io.IOException; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; + +public class PolicyParserUtils { + + public record PluginData(Path pluginPath, boolean isModular, boolean isExternalPlugin) { + public PluginData { + requireNonNull(pluginPath); + } + } + + private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; + + public static Map createPluginPolicies(Collection pluginData) throws IOException { + Map pluginPolicies = new HashMap<>(pluginData.size()); + for (var entry : pluginData) { + Path pluginRoot = entry.pluginPath(); + String pluginName = pluginRoot.getFileName().toString(); + + final Policy policy = loadPluginPolicy(pluginRoot, entry.isModular(), pluginName, entry.isExternalPlugin()); + + pluginPolicies.put(pluginName, policy); + } + return pluginPolicies; + } + + private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, String pluginName, boolean isExternalPlugin) + throws IOException { + Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); + + final Set moduleNames = getModuleNames(pluginRoot, isModular); + final Policy policy = parsePolicyIfExists(pluginName, policyFile, isExternalPlugin); + + // TODO: should this check actually be part of the parser? + for (Scope scope : policy.scopes()) { + if (moduleNames.contains(scope.moduleName()) == false) { + throw new IllegalStateException( + Strings.format( + "Invalid module name in policy: plugin [%s] does not have module [%s]; available modules [%s]; policy file [%s]", + pluginName, + scope.moduleName(), + String.join(", ", moduleNames), + policyFile + ) + ); + } + } + return policy; + } + + private static Policy parsePolicyIfExists(String pluginName, Path policyFile, boolean isExternalPlugin) throws IOException { + if (Files.exists(policyFile)) { + return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName, isExternalPlugin).parsePolicy(); + } + return new Policy(pluginName, List.of()); + } + + private static Set getModuleNames(Path pluginRoot, boolean isModular) { + if (isModular) { + ModuleFinder moduleFinder = ModuleFinder.of(pluginRoot); + Set moduleReferences = moduleFinder.findAll(); + + return moduleReferences.stream().map(mr -> mr.descriptor().name()).collect(Collectors.toUnmodifiableSet()); + } + // When isModular == false we use the same "ALL-UNNAMED" constant as the JDK to indicate (any) unnamed module for this plugin + return Set.of(ALL_UNNAMED); + } + +} diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java index 2c05edd530813..b6b0b4d85722c 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java @@ -32,6 +32,7 @@ import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap; +import org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.jdk.JarHell; @@ -210,24 +211,25 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { ); // load the plugin Java modules and layers now for use in entitlements - var pluginsLoader = PluginsLoader.createPluginsLoader(nodeEnv.modulesFile(), nodeEnv.pluginsFile()); + var modulesBundles = PluginsLoader.loadModulesBundles(nodeEnv.modulesFile()); + var pluginsBundles = PluginsLoader.loadPluginsBundles(nodeEnv.pluginsFile()); + var pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles); bootstrap.setPluginsLoader(pluginsLoader); if (bootstrap.useEntitlements()) { LogManager.getLogger(Elasticsearch.class).info("Bootstrapping Entitlements"); - List pluginData = Stream.concat( + var pluginData = Stream.concat( pluginsLoader.moduleBundles() .stream() - .map(bundle -> new EntitlementBootstrap.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), false)), + .map(bundle -> new PolicyParserUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), false)), pluginsLoader.pluginBundles() .stream() - .map(bundle -> new EntitlementBootstrap.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) + .map(bundle -> new PolicyParserUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) ).toList(); - + var pluginPolicies = PolicyParserUtils.createPluginPolicies(pluginData); var pluginsResolver = PluginsResolver.create(pluginsLoader); - - EntitlementBootstrap.bootstrap(pluginData, pluginsResolver::resolveClassToPluginName); + EntitlementBootstrap.bootstrap(pluginPolicies, pluginsResolver::resolveClassToPluginName); } else if (RuntimeVersionFeature.isSecurityManagerAvailable()) { // install SM after natives, shutdown hooks, etc. LogManager.getLogger(Elasticsearch.class).info("Bootstrapping java SecurityManager"); diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java b/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java index c7dc2c405ffba..5e23603bebe52 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java @@ -126,47 +126,31 @@ public static LayerAndLoader ofUberModuleLoader(UberModuleClassLoader loader) { private final Set pluginBundles; /** - * Constructs a new PluginsLoader + * Loads a set of PluginBundles from the modules directory * * @param modulesDirectory The directory modules exist in, or null if modules should not be loaded from the filesystem - * @param pluginsDirectory The directory plugins exist in, or null if plugins should not be loaded from the filesystem */ - public static PluginsLoader createPluginsLoader(Path modulesDirectory, Path pluginsDirectory) { - return createPluginsLoader(modulesDirectory, pluginsDirectory, true); - } - - /** - * Constructs a new PluginsLoader - * - * @param modulesDirectory The directory modules exist in, or null if modules should not be loaded from the filesystem - * @param pluginsDirectory The directory plugins exist in, or null if plugins should not be loaded from the filesystem - * @param withServerExports {@code true} to add server module exports - */ - public static PluginsLoader createPluginsLoader(Path modulesDirectory, Path pluginsDirectory, boolean withServerExports) { - Map> qualifiedExports; - if (withServerExports) { - qualifiedExports = new HashMap<>(ModuleQualifiedExportsService.getBootServices()); - addServerExportsService(qualifiedExports); - } else { - qualifiedExports = Collections.emptyMap(); - } - - Set seenBundles = new LinkedHashSet<>(); - + public static Set loadModulesBundles(Path modulesDirectory) { // load (elasticsearch) module layers final Set modules; if (modulesDirectory != null) { try { modules = PluginsUtils.getModuleBundles(modulesDirectory); - seenBundles.addAll(modules); } catch (IOException ex) { throw new IllegalStateException("Unable to initialize modules", ex); } } else { modules = Collections.emptySet(); } + return modules; + } - // load plugin layers + /** + * Loads a set of PluginBundles from the plugins directory + * + * @param pluginsDirectory The directory plugins exist in, or null if plugins should not be loaded from the filesystem + */ + public static Set loadPluginsBundles(Path pluginsDirectory) { final Set plugins; if (pluginsDirectory != null) { try { @@ -174,8 +158,6 @@ public static PluginsLoader createPluginsLoader(Path modulesDirectory, Path plug if (isAccessibleDirectory(pluginsDirectory, logger)) { PluginsUtils.checkForFailedPluginRemovals(pluginsDirectory); plugins = PluginsUtils.getPluginBundles(pluginsDirectory); - - seenBundles.addAll(plugins); } else { plugins = Collections.emptySet(); } @@ -185,6 +167,38 @@ public static PluginsLoader createPluginsLoader(Path modulesDirectory, Path plug } else { plugins = Collections.emptySet(); } + return plugins; + } + + /** + * Constructs a new PluginsLoader + * + * @param modules The set of module bundles present on the filesystem + * @param plugins The set of plugin bundles present on the filesystem + */ + public static PluginsLoader createPluginsLoader(Set modules, Set plugins) { + return createPluginsLoader(modules, plugins, true); + } + + /** + * Constructs a new PluginsLoader + * + * @param modules The set of module bundles present on the filesystem + * @param plugins The set of plugin bundles present on the filesystem + * @param withServerExports {@code true} to add server module exports + */ + public static PluginsLoader createPluginsLoader(Set modules, Set plugins, boolean withServerExports) { + Map> qualifiedExports; + if (withServerExports) { + qualifiedExports = new HashMap<>(ModuleQualifiedExportsService.getBootServices()); + addServerExportsService(qualifiedExports); + } else { + qualifiedExports = Collections.emptyMap(); + } + + Set seenBundles = new LinkedHashSet<>(); + seenBundles.addAll(modules); + seenBundles.addAll(plugins); Map loadedPluginLayers = new LinkedHashMap<>(); Map> transitiveUrls = new HashMap<>(); diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java index b7d63b7d612c9..2c3f7626e1013 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java @@ -28,6 +28,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; +import java.util.Set; import static java.util.Map.entry; import static org.elasticsearch.test.LambdaMatchers.transformedMatch; @@ -45,7 +46,11 @@ public class PluginsLoaderTests extends ESTestCase { private static final Logger logger = LogManager.getLogger(PluginsLoaderTests.class); static PluginsLoader newPluginsLoader(Settings settings) { - return PluginsLoader.createPluginsLoader(null, TestEnvironment.newEnvironment(settings).pluginsFile(), false); + return PluginsLoader.createPluginsLoader( + Set.of(), + PluginsLoader.loadPluginsBundles(TestEnvironment.newEnvironment(settings).pluginsFile()), + false + ); } public void testToModuleName() { diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java index 79d8f98c7dca6..74caf2c38e309 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java @@ -68,7 +68,11 @@ static PluginsService newPluginsService(Settings settings) { return new PluginsService( settings, null, - PluginsLoader.createPluginsLoader(null, TestEnvironment.newEnvironment(settings).pluginsFile(), false) + PluginsLoader.createPluginsLoader( + Set.of(), + PluginsLoader.loadPluginsBundles(TestEnvironment.newEnvironment(settings).pluginsFile()), + false + ) ); } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/bench/WatcherScheduleEngineBenchmark.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/bench/WatcherScheduleEngineBenchmark.java index 59dc1db88e991..8b8d8caacdd96 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/bench/WatcherScheduleEngineBenchmark.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/test/bench/WatcherScheduleEngineBenchmark.java @@ -111,7 +111,10 @@ public static void main(String[] args) throws Exception { try ( Node node = new Node( internalNodeEnv, - PluginsLoader.createPluginsLoader(internalNodeEnv.modulesFile(), internalNodeEnv.pluginsFile()) + PluginsLoader.createPluginsLoader( + PluginsLoader.loadModulesBundles(internalNodeEnv.modulesFile()), + PluginsLoader.loadPluginsBundles(internalNodeEnv.pluginsFile()) + ) ).start() ) { final Client client = node.client();