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 178c13b0e2728..2a587903c9e9f 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 @@ -122,7 +122,7 @@ public static void bootstrap( suppressFailureLogPackages ); exportInitializationToAgent(); - loadAgent(findAgentJar()); + loadAgent(findAgentJar(), EntitlementInitialization.class.getName()); } private static Path getUserHome() { @@ -134,11 +134,11 @@ private static Path getUserHome() { } @SuppressForbidden(reason = "The VirtualMachine API is the only way to attach a java agent dynamically") - private static void loadAgent(String agentPath) { + static void loadAgent(String agentPath, String entitlementInitializationClassName) { try { VirtualMachine vm = VirtualMachine.attach(Long.toString(ProcessHandle.current().pid())); try { - vm.loadAgent(agentPath, EntitlementInitialization.class.getName()); + vm.loadAgent(agentPath, entitlementInitializationClassName); } finally { vm.detach(); } @@ -154,7 +154,7 @@ private static void exportInitializationToAgent() { EntitlementInitialization.class.getModule().addExports(initPkg, unnamedModule); } - public static String findAgentJar() { + static String findAgentJar() { String propertyName = "es.entitlement.agentJar"; String propertyValue = System.getProperty(propertyName); if (propertyValue != null) { 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 867ee2cdcbc1f..5fcc2b738b624 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 @@ -34,11 +34,11 @@ public class EntitlementInitialization { private static final Module ENTITLEMENTS_MODULE = PolicyManager.class.getModule(); - private static ElasticsearchEntitlementChecker manager; + private static ElasticsearchEntitlementChecker checker; // Note: referenced by bridge reflectively public static EntitlementChecker checker() { - return manager; + return checker; } /** @@ -61,18 +61,7 @@ public static EntitlementChecker checker() { * @param inst the JVM instrumentation class instance */ public static void initialize(Instrumentation inst) throws Exception { - manager = initChecker(); - - var verifyBytecode = Booleans.parseBoolean(System.getProperty("es.entitlements.verify_bytecode", "false")); - if (verifyBytecode) { - ensureClassesSensitiveToVerificationAreInitialized(); - } - - DynamicInstrumentation.initialize( - inst, - EntitlementCheckerUtils.getVersionSpecificCheckerClass(EntitlementChecker.class, Runtime.version().feature()), - verifyBytecode - ); + checker = initChecker(inst, createPolicyManager()); } private static PolicyManager createPolicyManager() { @@ -112,9 +101,7 @@ private static void ensureClassesSensitiveToVerificationAreInitialized() { } } - private static ElasticsearchEntitlementChecker initChecker() { - final PolicyManager policyManager = createPolicyManager(); - + static ElasticsearchEntitlementChecker initChecker(Instrumentation inst, PolicyManager policyManager) throws Exception { final Class clazz = EntitlementCheckerUtils.getVersionSpecificCheckerClass( ElasticsearchEntitlementChecker.class, Runtime.version().feature() @@ -126,10 +113,25 @@ private static ElasticsearchEntitlementChecker initChecker() { } catch (NoSuchMethodException e) { throw new AssertionError("entitlement impl is missing no arg constructor", e); } + + ElasticsearchEntitlementChecker checker; try { - return (ElasticsearchEntitlementChecker) constructor.newInstance(policyManager); + checker = (ElasticsearchEntitlementChecker) constructor.newInstance(policyManager); } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) { throw new AssertionError(e); } + + var verifyBytecode = Booleans.parseBoolean(System.getProperty("es.entitlements.verify_bytecode", "false")); + if (verifyBytecode) { + ensureClassesSensitiveToVerificationAreInitialized(); + } + + DynamicInstrumentation.initialize( + inst, + EntitlementCheckerUtils.getVersionSpecificCheckerClass(EntitlementChecker.class, Runtime.version().feature()), + verifyBytecode + ); + + return checker; } } diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginDescriptor.java b/server/src/main/java/org/elasticsearch/plugins/PluginDescriptor.java index 4e6af08a63ffa..db02bc3abd9fe 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginDescriptor.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginDescriptor.java @@ -250,6 +250,31 @@ public static PluginDescriptor readFromProperties(final Path pluginDir) throws I return descriptor; } + /** + * Reads the internal descriptor for a classic plugin. + * + * @param stream the InputStream from which to read the plugin data + * @return the plugin info + * @throws IOException if an I/O exception occurred reading the plugin descriptor + */ + public static PluginDescriptor readInternalDescriptor(InputStream stream) throws IOException { + final Map propsMap; + { + final Properties props = new Properties(); + props.load(stream); + propsMap = props.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), props::getProperty)); + } + + PluginDescriptor descriptor = readerInternalDescriptor(propsMap, INTERNAL_DESCRIPTOR_FILENAME); + String name = descriptor.getName(); + + if (propsMap.isEmpty() == false) { + throw new IllegalArgumentException("Unknown properties for plugin [" + name + "] in plugin descriptor: " + propsMap.keySet()); + } + + return descriptor; + } + private static PluginDescriptor readerInternalDescriptor(Map propsMap, String filename) { String name = readNonEmptyString(propsMap, filename, "name"); String desc = readString(propsMap, name, "description"); diff --git a/test/framework/build.gradle b/test/framework/build.gradle index 9601ee2c6a648..cd3b2489fdb84 100644 --- a/test/framework/build.gradle +++ b/test/framework/build.gradle @@ -16,6 +16,7 @@ dependencies { api project(':libs:ssl-config') api project(":server") api project(":libs:cli") + api project(":libs:entitlement:bridge") api "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}" api "junit:junit:${versions.junit}" api "org.hamcrest:hamcrest:${versions.hamcrest}" diff --git a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfo.java b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfo.java index 92b642e635b3d..d11373c114c1b 100644 --- a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfo.java +++ b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfo.java @@ -11,4 +11,4 @@ import java.util.List; -record TestBuildInfo(String component, List locations) {} +public record TestBuildInfo(String component, List locations) {} diff --git a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfoParser.java b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfoParser.java index bd4b9182186ec..5d620591250aa 100644 --- a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfoParser.java +++ b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestBuildInfoParser.java @@ -23,7 +23,7 @@ import java.util.ArrayList; import java.util.List; -class TestBuildInfoParser { +public class TestBuildInfoParser { private static final String PLUGIN_TEST_BUILD_INFO_RESOURCES = "META-INF/plugin-test-build-info.json"; private static final String SERVER_TEST_BUILD_INFO_RESOURCE = "META-INF/server-test-build-info.json"; @@ -75,7 +75,7 @@ static TestBuildInfo fromXContent(final XContentParser parser) throws IOExceptio return PARSER.parse(parser, null).build(); } - static List parseAllPluginTestBuildInfo() throws IOException { + public static List parseAllPluginTestBuildInfo() throws IOException { var xContent = XContentFactory.xContent(XContentType.JSON); List pluginsTestBuildInfos = new ArrayList<>(); var resources = TestBuildInfoParser.class.getClassLoader().getResources(PLUGIN_TEST_BUILD_INFO_RESOURCES); @@ -88,7 +88,7 @@ static List parseAllPluginTestBuildInfo() throws IOException { return pluginsTestBuildInfos; } - static TestBuildInfo parseServerTestBuildInfo() throws IOException { + public static TestBuildInfo parseServerTestBuildInfo() throws IOException { var xContent = XContentFactory.xContent(XContentType.JSON); var resource = TestBuildInfoParser.class.getClassLoader().getResource(SERVER_TEST_BUILD_INFO_RESOURCE); // No test-build-info for server: this might be a non-gradle build. Proceed without TestBuildInfo diff --git a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java index 82fa0d1dee58c..c29bd84d1fda9 100644 --- a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java +++ b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java @@ -21,7 +21,7 @@ import java.util.Map; import java.util.function.Function; -record TestScopeResolver(Map scopeMap) { +public record TestScopeResolver(Map scopeMap) { private static final Logger logger = LogManager.getLogger(TestScopeResolver.class); @@ -38,7 +38,7 @@ PolicyManager.PolicyScope getScope(Class callerClass) { return scope; } - static Function, PolicyManager.PolicyScope> createScopeResolver( + public static Function, PolicyManager.PolicyScope> createScopeResolver( TestBuildInfo serverBuildInfo, List pluginsBuildInfo ) { diff --git a/test/framework/src/main/java/org/elasticsearch/entitlement/bootstrap/TestEntitlementBootstrap.java b/test/framework/src/main/java/org/elasticsearch/entitlement/bootstrap/TestEntitlementBootstrap.java new file mode 100644 index 0000000000000..54c5ed001fb5c --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/entitlement/bootstrap/TestEntitlementBootstrap.java @@ -0,0 +1,38 @@ +/* + * 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.bootstrap; + +import org.elasticsearch.entitlement.initialization.TestEntitlementInitialization; +import org.elasticsearch.entitlement.runtime.policy.PathLookup; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; + +public class TestEntitlementBootstrap { + + private static final Logger logger = LogManager.getLogger(TestEntitlementBootstrap.class); + private static BootstrapArgs bootstrapArgs; + + /** + * Activates entitlement checking in tests. + * @param bootstrapArgs arguments used for and passed to entitlement initialization + */ + public static void bootstrap(BootstrapArgs bootstrapArgs) { + assert bootstrapArgs != null; + TestEntitlementBootstrap.bootstrapArgs = bootstrapArgs; + logger.debug("Loading entitlement agent"); + EntitlementBootstrap.loadAgent(EntitlementBootstrap.findAgentJar(), TestEntitlementInitialization.class.getName()); + } + + public static BootstrapArgs bootstrapArgs() { + return bootstrapArgs; + } + + public record BootstrapArgs(PathLookup pathLookup) {} +} diff --git a/test/framework/src/main/java/org/elasticsearch/entitlement/initialization/TestEntitlementInitialization.java b/test/framework/src/main/java/org/elasticsearch/entitlement/initialization/TestEntitlementInitialization.java new file mode 100644 index 0000000000000..42fc978dd7523 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/entitlement/initialization/TestEntitlementInitialization.java @@ -0,0 +1,120 @@ +/* + * 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.bootstrap.TestEntitlementBootstrap; +import org.elasticsearch.entitlement.bridge.EntitlementChecker; +import org.elasticsearch.entitlement.runtime.api.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.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 java.util.Set; + +/** + * Test-only version of {@code EntitlementInitialization} + */ +public class TestEntitlementInitialization { + + private static ElasticsearchEntitlementChecker checker; + + // Note: referenced by bridge reflectively + public static EntitlementChecker checker() { + return checker; + } + + public static void initialize(Instrumentation inst) throws Exception { + TestEntitlementBootstrap.BootstrapArgs bootstrapArgs = TestEntitlementBootstrap.bootstrapArgs(); + checker = EntitlementInitialization.initChecker(inst, createPolicyManager(bootstrapArgs.pathLookup())); + } + + private record TestPluginData(String pluginName, boolean isModular, boolean isExternalPlugin) {} + + private static Map parsePluginsPolicies(List pluginsData) { + Map 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 parsePluginsDescriptors(List pluginNames) { + List 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.readInternalDescriptor(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 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 pluginPolicies = parsePluginsPolicies(pluginsData); + + FilesEntitlementsValidation.validate(pluginPolicies, pathLookup); + + return new PolicyManager( + HardcodedEntitlements.serverPolicy(null, null), + HardcodedEntitlements.agentEntitlements(), + pluginPolicies, + scopeResolver, + Map.of(), + null, // TODO: this will need to change -- encapsulate it when we extract isTriviallyAllowed + pathLookup, + Set.of() + ); + } +}