From df452f64adf1d0d7723f04ac85885a49b3ab0a41 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Fri, 14 Mar 2025 17:09:45 +0100 Subject: [PATCH 1/5] WIP --- distribution/tools/plugin-cli/build.gradle | 1 + .../plugins/cli/InstallPluginAction.java | 16 ++++----- .../plugins/cli/PluginSecurity.java | 35 +++++++++++++++---- .../runtime/policy/PolicyManager.java | 4 +-- .../runtime/policy/PolicyParser.java | 16 ++++++--- .../runtime/policy/PolicyParserUtils.java | 10 +++--- .../runtime/policy/PolicyParserTests.java | 6 ++-- .../plugins/cli/PluginSecurityTests.java | 30 ++++++---------- 8 files changed, 68 insertions(+), 50 deletions(-) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index dc2bcd96b8d9f..9019412cc5ecc 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -24,6 +24,7 @@ dependencies { compileOnly project(":libs:cli") implementation project(":libs:plugin-api") implementation project(":libs:plugin-scanner") + implementation project(":libs:entitlement") // TODO: asm is picked up from the plugin scanner, we should consolidate so it is not defined twice implementation 'org.ow2.asm:asm:9.7.1' implementation 'org.ow2.asm:asm-tree:9.7.1' diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 0803d24c3914f..5c25a0276681c 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -24,8 +24,6 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.elasticsearch.Build; -import org.elasticsearch.bootstrap.PluginPolicyInfo; -import org.elasticsearch.bootstrap.PolicyUtil; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; @@ -36,9 +34,9 @@ import org.elasticsearch.core.PathUtils; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; +import org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.jdk.JarHell; -import org.elasticsearch.jdk.RuntimeVersionFeature; import org.elasticsearch.plugin.scanner.ClassReaders; import org.elasticsearch.plugin.scanner.NamedComponentScanner; import org.elasticsearch.plugins.Platforms; @@ -923,13 +921,11 @@ void jarHellCheck(PluginDescriptor candidateInfo, Path candidateDir, Path plugin */ private PluginDescriptor installPlugin(InstallablePlugin descriptor, Path tmpRoot, List deleteOnFailure) throws Exception { final PluginDescriptor info = loadPluginInfo(tmpRoot); - if (RuntimeVersionFeature.isSecurityManagerAvailable()) { - PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpDir()); - if (pluginPolicy != null) { - Set permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy, env.tmpDir()); - PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch); - } - } + + var pluginPolicy = PolicyParserUtils.parsePolicyIfExists(info.getName(), tmpRoot, true); + + Set permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy); + PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch); // Validate that the downloaded plugin's ID matches what we expect from the descriptor. The // exception is if we install a plugin via `InstallPluginCommand` by specifying a URL or diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java index 376c797d68899..5b179066f6593 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java @@ -9,12 +9,16 @@ package org.elasticsearch.plugins.cli; -import org.elasticsearch.bootstrap.PluginPolicyInfo; import org.elasticsearch.bootstrap.PolicyUtil; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.Terminal.Verbosity; import org.elasticsearch.cli.UserException; +import org.elasticsearch.entitlement.runtime.policy.FileAccessTree; +import org.elasticsearch.entitlement.runtime.policy.Policy; +import org.elasticsearch.entitlement.runtime.policy.PolicyParser; +import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; +import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; import java.io.IOException; import java.net.URL; @@ -25,6 +29,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -123,13 +128,29 @@ static String formatPermission(Permission permission) { /** * Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users. */ - public static Set getPermissionDescriptions(PluginPolicyInfo pluginPolicyInfo, Path tmpDir) throws IOException { - Set allPermissions = new HashSet<>(PolicyUtil.getPolicyPermissions(null, pluginPolicyInfo.policy(), tmpDir)); - for (URL jar : pluginPolicyInfo.jars()) { - Set jarPermissions = PolicyUtil.getPolicyPermissions(jar, pluginPolicyInfo.policy(), tmpDir); - allPermissions.addAll(jarPermissions); + public static Set getPermissionDescriptions(Policy pluginPolicy) { + + Map, List> allEntitlements = pluginPolicy.scopes().stream() + .flatMap(scope -> scope.entitlements().stream()) + .collect(Collectors.groupingBy(Entitlement::getClass)); + + Set descriptions = new HashSet<>(); + for (var entitlements: allEntitlements.entrySet()) { + var entitlementClass = entitlements.getKey(); + if (entitlementClass.equals(FilesEntitlement.class)) { + var filesData = entitlements.getValue().stream() + .flatMap(entitlement -> { + FilesEntitlement filesEntitlement = (FilesEntitlement)entitlement; + return filesEntitlement.filesData().stream(); + }) + .filter(x -> x.platform().isCurrent()) + .distinct(); + + filesData.map(Object::toString).forEach(descriptions::add); + } + descriptions.add(PolicyParser.getEntitlementName(entitlementClass)); } - return allPermissions.stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet()); + return descriptions; } } 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 bed5d094b058e..b2c421f2d69a0 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 @@ -499,7 +499,7 @@ private void checkFlagEntitlement( classEntitlements.componentName(), requestingClass.getModule().getName(), requestingClass, - PolicyParser.getEntitlementTypeName(entitlementClass) + PolicyParser.buildEntitlementNameFromClass(entitlementClass) ), callerClass, classEntitlements.componentName() @@ -511,7 +511,7 @@ private void checkFlagEntitlement( classEntitlements.componentName(), requestingClass.getModule().getName(), requestingClass, - PolicyParser.getEntitlementTypeName(entitlementClass) + PolicyParser.buildEntitlementNameFromClass(entitlementClass) ) ); } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java index ce6ce5f17ce01..bdd302032fbe2 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java @@ -49,7 +49,7 @@ */ public class PolicyParser { - private static final Map> EXTERNAL_ENTITLEMENTS = Stream.of( + private static final Map> EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME = Stream.of( CreateClassLoaderEntitlement.class, FilesEntitlement.class, InboundNetworkEntitlement.class, @@ -59,14 +59,18 @@ public class PolicyParser { SetHttpsConnectionPropertiesEntitlement.class, WriteAllSystemPropertiesEntitlement.class, WriteSystemPropertiesEntitlement.class - ).collect(Collectors.toUnmodifiableMap(PolicyParser::getEntitlementTypeName, Function.identity())); + ).collect(Collectors.toUnmodifiableMap(PolicyParser::buildEntitlementNameFromClass, Function.identity())); + + private static final Map, String> EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS = + EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Map.Entry::getValue, Map.Entry::getKey)); protected final XContentParser policyParser; protected final String policyName; private final boolean isExternalPlugin; private final Map> externalEntitlements; - static String getEntitlementTypeName(Class entitlementClass) { + static String buildEntitlementNameFromClass(Class entitlementClass) { var entitlementClassName = entitlementClass.getSimpleName(); if (entitlementClassName.endsWith("Entitlement") == false) { @@ -82,8 +86,12 @@ static String getEntitlementTypeName(Class entitlementCla .collect(Collectors.joining("_")); } + public static String getEntitlementName(Class entitlementClass) { + return EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS.get(entitlementClass); + } + public PolicyParser(InputStream inputStream, String policyName, boolean isExternalPlugin) throws IOException { - this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENTS); + this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME); } // package private for tests 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 index b33a00ce7fc5c..3b36fb0598d0d 100644 --- 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 @@ -42,7 +42,7 @@ public record PluginData(Path pluginPath, boolean isModular, boolean isExternalP } } - private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; + public static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; public static final String POLICY_OVERRIDE_PREFIX = "es.entitlements.policy."; @@ -58,9 +58,8 @@ public static Map createPluginPolicies(Collection pl if (overriddenPolicy.isPresent()) { pluginPolicies.put(pluginName, overriddenPolicy.get()); } else { - Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); - var policy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin()); - validatePolicyScopes(pluginName, policy, moduleNames, policyFile.toString()); + var policy = parsePolicyIfExists(pluginName, pluginRoot, entry.isExternalPlugin()); + validatePolicyScopes(pluginName, policy, moduleNames, pluginRoot.resolve(POLICY_FILE_NAME).toString()); pluginPolicies.put(pluginName, policy); } } @@ -129,7 +128,8 @@ private static void validatePolicyScopes(String pluginName, Policy policy, Set PolicyParser.getEntitlementTypeName(TestWrongEntitlementName.class)); + var ex = expectThrows(IllegalArgumentException.class, () -> PolicyParser.buildEntitlementNameFromClass(TestWrongEntitlementName.class)); assertThat( ex.getMessage(), equalTo("TestWrongEntitlementName is not a valid Entitlement class name. A valid class name must end with 'Entitlement'") diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java index 52a712518357c..95935990c4e2a 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java @@ -11,10 +11,8 @@ import org.elasticsearch.bootstrap.PluginPolicyInfo; import org.elasticsearch.bootstrap.PolicyUtil; -import org.elasticsearch.jdk.RuntimeVersionFeature; -import org.elasticsearch.plugins.PluginDescriptor; +import org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils; import org.elasticsearch.test.ESTestCase; -import org.junit.Before; import java.io.IOException; import java.nio.file.Files; @@ -28,32 +26,26 @@ /** Tests plugin manager security check */ public class PluginSecurityTests extends ESTestCase { - @Before - public void assumeSecurityManagerSupported() { - assumeTrue("test requires security manager to be supported", RuntimeVersionFeature.isSecurityManagerAvailable()); - } - PluginPolicyInfo makeDummyPlugin(String policy, String... files) throws IOException { - Path plugin = createTempDir(); - Files.copy(this.getDataPath(policy), plugin.resolve(PluginDescriptor.ES_PLUGIN_POLICY)); - for (String file : files) { - Files.createFile(plugin.resolve(file)); - } - return PolicyUtil.getPluginPolicyInfo(plugin, createTempDir()); + Path makeDummyPlugin(String policy) throws IOException { + Path pluginPath = createTempDir(); + Files.copy(this.getDataPath(policy), pluginPath.resolve(PolicyParserUtils.POLICY_FILE_NAME)); + return pluginPath; } /** Test that we can parse the set of permissions correctly for a simple policy */ public void testParsePermissions() throws Exception { - assumeTrue("test cannot run with security manager enabled", System.getSecurityManager() == null); - Path scratch = createTempDir(); - PluginPolicyInfo info = makeDummyPlugin("simple-plugin-security.policy"); - Set actual = PluginSecurity.getPermissionDescriptions(info, scratch); + // TODO: rename these files, make them entitlement policies + var pluginPath = makeDummyPlugin("simple-plugin-security.policy"); + var pluginPolicy = PolicyParserUtils.parsePolicyIfExists("test-plugin", pluginPath, true); + + Set actual = PluginSecurity.getPermissionDescriptions(pluginPolicy); assertThat(actual, contains(PluginSecurity.formatPermission(new PropertyPermission("someProperty", "read")))); } /** Test that we can parse the set of permissions correctly for a complex policy */ public void testParseTwoPermissions() throws Exception { - assumeTrue("test cannot run with security manager enabled", System.getSecurityManager() == null); + // TODO: adjust other tests too Path scratch = createTempDir(); PluginPolicyInfo info = makeDummyPlugin("complex-plugin-security.policy"); Set actual = PluginSecurity.getPermissionDescriptions(info, scratch); From 12698afdbc4e826926a32c387bc01f2f8cd32b08 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Wed, 19 Mar 2025 12:41:47 +0100 Subject: [PATCH 2/5] Replace SM Permissions with Entitlements in InstallPluginAction --- .../plugins/cli/InstallPluginAction.java | 8 +- .../plugins/cli/PluginSecurity.java | 111 +++--------------- .../runtime/policy/PolicyParser.java | 3 +- .../runtime/policy/PolicyUtils.java | 87 +++++++++++--- .../policy/entitlements/FilesEntitlement.java | 18 +++ .../runtime/policy/PolicyParserTests.java | 5 +- .../runtime/policy/PolicyUtilsTests.java | 99 +++++++++++++++- .../plugins/cli/PluginSecurityTests.java | 68 ----------- .../cli/complex-plugin-security.policy | 14 --- .../plugins/cli/simple-plugin-security.policy | 12 -- .../cli/unresolved-plugin-security.policy | 13 -- 11 files changed, 210 insertions(+), 228 deletions(-) delete mode 100644 qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java delete mode 100644 qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/complex-plugin-security.policy delete mode 100644 qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/simple-plugin-security.policy delete mode 100644 qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/unresolved-plugin-security.policy diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 5c25a0276681c..38fe40414197f 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -34,7 +34,7 @@ import org.elasticsearch.core.PathUtils; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; -import org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils; +import org.elasticsearch.entitlement.runtime.policy.PolicyUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.jdk.JarHell; import org.elasticsearch.plugin.scanner.ClassReaders; @@ -922,10 +922,10 @@ void jarHellCheck(PluginDescriptor candidateInfo, Path candidateDir, Path plugin private PluginDescriptor installPlugin(InstallablePlugin descriptor, Path tmpRoot, List deleteOnFailure) throws Exception { final PluginDescriptor info = loadPluginInfo(tmpRoot); - var pluginPolicy = PolicyParserUtils.parsePolicyIfExists(info.getName(), tmpRoot, true); + var pluginPolicy = PolicyUtils.parsePolicyIfExists(info.getName(), tmpRoot, true); - Set permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy); - PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch); + Set entitlements = PolicyUtils.getEntitlementsDescriptions(pluginPolicy); + PluginSecurity.confirmPolicyExceptions(terminal, entitlements, batch); // Validate that the downloaded plugin's ID matches what we expect from the descriptor. The // exception is if we install a plugin via `InstallPluginCommand` by specifying a URL or diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java index 5b179066f6593..4ab2359728926 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java @@ -9,70 +9,56 @@ package org.elasticsearch.plugins.cli; -import org.elasticsearch.bootstrap.PolicyUtil; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.Terminal.Verbosity; import org.elasticsearch.cli.UserException; -import org.elasticsearch.entitlement.runtime.policy.FileAccessTree; -import org.elasticsearch.entitlement.runtime.policy.Policy; -import org.elasticsearch.entitlement.runtime.policy.PolicyParser; -import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; -import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Path; -import java.security.Permission; -import java.security.UnresolvedPermission; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** - * Contains methods for displaying extended plugin permissions to the user, and confirming that + * Contains methods for displaying extended plugin entitlements to the user, and confirming that * plugin installation can proceed. */ public class PluginSecurity { + public static final String ENTITLEMENTS_DESCRIPTION_URL = + "https://www.elastic.co/guide/en/elasticsearch/plugins/current/creating-classic-plugins.html"; + /** * prints/confirms policy exceptions with the user */ - static void confirmPolicyExceptions(Terminal terminal, Set permissions, boolean batch) throws UserException { - List requested = new ArrayList<>(permissions); + static void confirmPolicyExceptions(Terminal terminal, Set entitlements, boolean batch) throws UserException { + List requested = new ArrayList<>(entitlements); if (requested.isEmpty()) { - terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions"); + terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional entitlements"); } else { - // sort permissions in a reasonable order + // sort entitlements in a reasonable order Collections.sort(requested); if (terminal.isHeadless()) { terminal.errorPrintln( - "WARNING: plugin requires additional permissions: [" + "WARNING: plugin requires additional entitlements: [" + requested.stream().map(each -> '\'' + each + '\'').collect(Collectors.joining(", ")) + "]" ); terminal.errorPrintln( - "See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html" - + " for descriptions of what these permissions allow and the associated risks." + "See " + ENTITLEMENTS_DESCRIPTION_URL + " for descriptions of what these entitlements allow and the associated risks." ); } else { terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @"); + terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional entitlements @"); terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - // print all permissions: - for (String permission : requested) { - terminal.errorPrintln(Verbosity.NORMAL, "* " + permission); + // print all entitlements: + for (String entitlement : requested) { + terminal.errorPrintln(Verbosity.NORMAL, "* " + entitlement); } - terminal.errorPrintln( - Verbosity.NORMAL, - "See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html" - ); - terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks."); + terminal.errorPrintln(Verbosity.NORMAL, "See " + ENTITLEMENTS_DESCRIPTION_URL); + terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these entitlements allow and the associated risks."); if (batch == false) { prompt(terminal); @@ -88,69 +74,4 @@ private static void prompt(final Terminal terminal) throws UserException { throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user"); } } - - /** Format permission type, name, and actions into a string */ - static String formatPermission(Permission permission) { - StringBuilder sb = new StringBuilder(); - - String clazz = null; - if (permission instanceof UnresolvedPermission) { - clazz = ((UnresolvedPermission) permission).getUnresolvedType(); - } else { - clazz = permission.getClass().getName(); - } - sb.append(clazz); - - String name = null; - if (permission instanceof UnresolvedPermission) { - name = ((UnresolvedPermission) permission).getUnresolvedName(); - } else { - name = permission.getName(); - } - if (name != null && name.length() > 0) { - sb.append(' '); - sb.append(name); - } - - String actions = null; - if (permission instanceof UnresolvedPermission) { - actions = ((UnresolvedPermission) permission).getUnresolvedActions(); - } else { - actions = permission.getActions(); - } - if (actions != null && actions.length() > 0) { - sb.append(' '); - sb.append(actions); - } - return sb.toString(); - } - - /** - * Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users. - */ - public static Set getPermissionDescriptions(Policy pluginPolicy) { - - Map, List> allEntitlements = pluginPolicy.scopes().stream() - .flatMap(scope -> scope.entitlements().stream()) - .collect(Collectors.groupingBy(Entitlement::getClass)); - - Set descriptions = new HashSet<>(); - for (var entitlements: allEntitlements.entrySet()) { - var entitlementClass = entitlements.getKey(); - if (entitlementClass.equals(FilesEntitlement.class)) { - var filesData = entitlements.getValue().stream() - .flatMap(entitlement -> { - FilesEntitlement filesEntitlement = (FilesEntitlement)entitlement; - return filesEntitlement.filesData().stream(); - }) - .filter(x -> x.platform().isCurrent()) - .distinct(); - - filesData.map(Object::toString).forEach(descriptions::add); - } - descriptions.add(PolicyParser.getEntitlementName(entitlementClass)); - } - - return descriptions; - } } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java index bdd302032fbe2..6ff86f3f30dbf 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java @@ -62,7 +62,8 @@ public class PolicyParser { ).collect(Collectors.toUnmodifiableMap(PolicyParser::buildEntitlementNameFromClass, Function.identity())); private static final Map, String> EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS = - EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME.entrySet().stream() + EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME.entrySet() + .stream() .collect(Collectors.toUnmodifiableMap(Map.Entry::getValue, Map.Entry::getKey)); protected final XContentParser policyParser; diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java index 7e8232a9a11a3..3355b01cb4d4b 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java @@ -27,6 +27,7 @@ import java.util.Base64; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,7 +48,7 @@ public record PluginData(Path pluginPath, boolean isModular, boolean isExternalP } } - private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; + public static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; public static Map createPluginPolicies( Collection pluginData, @@ -57,7 +58,6 @@ public static Map createPluginPolicies( Map pluginPolicies = new HashMap<>(pluginData.size()); for (var entry : pluginData) { Path pluginRoot = entry.pluginPath(); - Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); String pluginName = pluginRoot.getFileName().toString(); final Set moduleNames = getModuleNames(pluginRoot, entry.isModular()); @@ -68,8 +68,8 @@ public static Map createPluginPolicies( pluginName, moduleNames ); - var pluginPolicy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin()); - validatePolicyScopes(pluginName, pluginPolicy, moduleNames, policyFile.toString()); + var pluginPolicy = parsePolicyIfExists(pluginName, pluginRoot, entry.isExternalPlugin()); + validatePolicyScopes(pluginName, pluginPolicy, moduleNames, pluginRoot.resolve(POLICY_FILE_NAME).toString()); pluginPolicies.put( pluginName, @@ -138,7 +138,8 @@ private static void validatePolicyScopes(String layerName, Policy policy, Set mergeEntitlements(List a, List merge(e, (FilesEntitlement) entitlement2); - case WriteSystemPropertiesEntitlement e -> merge(e, (WriteSystemPropertiesEntitlement) entitlement2); - default -> entitlement1; + static Entitlement mergeEntitlement(Entitlement entitlement, Entitlement other) { + return switch (entitlement) { + case FilesEntitlement e -> mergeFiles(Stream.of(e, (FilesEntitlement) other)); + case WriteSystemPropertiesEntitlement e -> mergeWriteSystemProperties(Stream.of(e, (WriteSystemPropertiesEntitlement) other)); + default -> entitlement; }; } - private static FilesEntitlement merge(FilesEntitlement a, FilesEntitlement b) { - return new FilesEntitlement(Stream.concat(a.filesData().stream(), b.filesData().stream()).distinct().toList()); + public static List mergeEntitlements(Stream entitlements) { + Map, List> entitlementMap = entitlements.collect( + Collectors.groupingBy(Entitlement::getClass) + ); + + List result = new ArrayList<>(); + for (var kv : entitlementMap.entrySet()) { + var entitlementClass = kv.getKey(); + var classEntitlements = kv.getValue(); + if (classEntitlements.size() == 1) { + result.add(classEntitlements.getFirst()); + } else { + result.add(PolicyUtils.mergeEntitlement(entitlementClass, classEntitlements.stream())); + } + } + return result; + } + + static Entitlement mergeEntitlement(Class entitlementClass, Stream entitlements) { + if (entitlementClass.equals(FilesEntitlement.class)) { + return mergeFiles(entitlements.map(FilesEntitlement.class::cast)); + } else if (entitlementClass.equals(WriteSystemPropertiesEntitlement.class)) { + return mergeWriteSystemProperties(entitlements.map(WriteSystemPropertiesEntitlement.class::cast)); + } + return entitlements.findFirst().orElseThrow(); } - private static WriteSystemPropertiesEntitlement merge(WriteSystemPropertiesEntitlement a, WriteSystemPropertiesEntitlement b) { + private static FilesEntitlement mergeFiles(Stream entitlements) { + return new FilesEntitlement(entitlements.flatMap(x -> x.filesData().stream()).distinct().toList()); + } + + private static WriteSystemPropertiesEntitlement mergeWriteSystemProperties(Stream entitlements) { return new WriteSystemPropertiesEntitlement( - Stream.concat(a.properties().stream(), b.properties().stream()).collect(Collectors.toUnmodifiableSet()) + entitlements.flatMap(x -> x.properties().stream()).collect(Collectors.toUnmodifiableSet()) ); } + + static Set describeEntitlement(Entitlement entitlement) { + Set descriptions = new HashSet<>(); + if (entitlement instanceof FilesEntitlement f) { + f.filesData() + .stream() + .filter(x -> x.platform() == null || x.platform().isCurrent()) + .map(x -> Strings.format("%s %s", PolicyParser.getEntitlementName(FilesEntitlement.class), x.description())) + .forEach(descriptions::add); + } else if (entitlement instanceof WriteSystemPropertiesEntitlement w) { + w.properties() + .stream() + .map(p -> Strings.format("%s [%s]", PolicyParser.getEntitlementName(WriteSystemPropertiesEntitlement.class), p)) + .forEach(descriptions::add); + } else { + descriptions.add(PolicyParser.getEntitlementName(entitlement.getClass())); + } + return descriptions; + } + + /** + * Extract a unique set of entitlements descriptions from the plugin's policy file. Each entitlement is formatted for output to users. + */ + public static Set getEntitlementsDescriptions(Policy pluginPolicy) { + var allEntitlements = PolicyUtils.mergeEntitlements(pluginPolicy.scopes().stream().flatMap(scope -> scope.entitlements().stream())); + Set descriptions = new HashSet<>(); + for (var entitlement : allEntitlements) { + descriptions.addAll(PolicyUtils.describeEntitlement(entitlement)); + } + return descriptions; + } } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java index c771da019d2b6..f4ded0a2dfc93 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java @@ -9,6 +9,7 @@ package org.elasticsearch.entitlement.runtime.policy.entitlements; +import org.elasticsearch.core.Strings; import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement; import org.elasticsearch.entitlement.runtime.policy.FileUtils; import org.elasticsearch.entitlement.runtime.policy.PathLookup; @@ -58,6 +59,8 @@ public sealed interface FileData { FileData withPlatform(Platform platform); + String description(); + static FileData ofPath(Path path, Mode mode) { return new AbsolutePathFileData(path, mode, null, false); } @@ -125,6 +128,11 @@ public FileData withPlatform(Platform platform) { } return new AbsolutePathFileData(path, mode, platform, exclusive); } + + @Override + public String description() { + return Strings.format("[%s] %s%s", mode, path.toAbsolutePath().normalize(), exclusive ? " (exclusive)" : ""); + } } private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode, Platform platform, boolean exclusive) @@ -149,6 +157,11 @@ public FileData withPlatform(Platform platform) { } return new RelativePathFileData(relativePath, baseDir, mode, platform, exclusive); } + + @Override + public String description() { + return Strings.format("[%s] <%s>/%s%s", mode, baseDir, relativePath, exclusive ? " (exclusive)" : ""); + } } private record PathSettingFileData(String setting, BaseDir baseDir, Mode mode, Platform platform, boolean exclusive) @@ -176,6 +189,11 @@ public FileData withPlatform(Platform platform) { } return new PathSettingFileData(setting, baseDir, mode, platform, exclusive); } + + @Override + public String description() { + return Strings.format("[%s] <%s>/<%s>%s", mode, baseDir, setting, exclusive ? " (exclusive)" : ""); + } } private static Mode parseMode(String mode) { diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java index 5908ca37b10f7..8518b60f0ed01 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java @@ -85,7 +85,10 @@ public NonStaticMethodEntitlement create() { public void testBuildEntitlementNameFromClass() { assertEquals("create_class_loader", PolicyParser.buildEntitlementNameFromClass(CreateClassLoaderEntitlement.class)); - var ex = expectThrows(IllegalArgumentException.class, () -> PolicyParser.buildEntitlementNameFromClass(TestWrongEntitlementName.class)); + var ex = expectThrows( + IllegalArgumentException.class, + () -> PolicyParser.buildEntitlementNameFromClass(TestWrongEntitlementName.class) + ); assertThat( ex.getMessage(), equalTo("TestWrongEntitlementName is not a valid Entitlement class name. A valid class name must end with 'Entitlement'") diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtilsTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtilsTests.java index 5742d45f83aef..fb52d58c1c33c 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtilsTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtilsTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.entitlement.runtime.policy; +import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement; @@ -26,8 +27,6 @@ import java.util.List; import java.util.Set; -import static org.elasticsearch.entitlement.runtime.policy.PolicyUtils.mergeEntitlement; -import static org.elasticsearch.entitlement.runtime.policy.PolicyUtils.mergeEntitlements; import static org.elasticsearch.test.LambdaMatchers.transformedMatch; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -207,7 +206,7 @@ public void testMergeSameFlagEntitlement() { var e1 = new InboundNetworkEntitlement(); var e2 = new InboundNetworkEntitlement(); - assertThat(mergeEntitlement(e1, e2), equalTo(new InboundNetworkEntitlement())); + assertThat(PolicyUtils.mergeEntitlement(e1, e2), equalTo(new InboundNetworkEntitlement())); } public void testMergeFilesEntitlement() { @@ -226,7 +225,7 @@ public void testMergeFilesEntitlement() { ) ); - var merged = mergeEntitlement(e1, e2); + var merged = PolicyUtils.mergeEntitlement(e1, e2); assertThat( merged, transformedMatch( @@ -246,7 +245,7 @@ public void testMergeWritePropertyEntitlement() { var e1 = new WriteSystemPropertiesEntitlement(List.of("a", "b", "c")); var e2 = new WriteSystemPropertiesEntitlement(List.of("b", "c", "d")); - var merged = mergeEntitlement(e1, e2); + var merged = PolicyUtils.mergeEntitlement(e1, e2); assertThat( merged, transformedMatch(x -> ((WriteSystemPropertiesEntitlement) x).properties(), containsInAnyOrder("a", "b", "c", "d")) @@ -271,7 +270,7 @@ public void testMergeEntitlements() { new WriteSystemPropertiesEntitlement(List.of("a")) ); - var merged = mergeEntitlements(a, b); + var merged = PolicyUtils.mergeEntitlements(a, b); assertThat( merged, containsInAnyOrder( @@ -288,4 +287,92 @@ public void testMergeEntitlements() { ) ); } + + /** Test that we can parse the set of entitlements correctly for a simple policy */ + public void testFormatSimplePolicy() { + var pluginPolicy = new Policy( + "test-plugin", + List.of(new Scope("module1", List.of(new WriteSystemPropertiesEntitlement(List.of("property1", "property2"))))) + ); + + Set actual = PolicyUtils.getEntitlementsDescriptions(pluginPolicy); + assertThat(actual, containsInAnyOrder("write_system_properties [property1]", "write_system_properties [property2]")); + } + + /** Test that we can format the set of entitlements correctly for a complex policy */ + public void testFormatPolicyWithMultipleScopes() { + var pluginPolicy = new Policy( + "test-plugin", + List.of( + new Scope("module1", List.of(new CreateClassLoaderEntitlement())), + new Scope("module2", List.of(new CreateClassLoaderEntitlement(), new OutboundNetworkEntitlement())), + new Scope("module3", List.of(new InboundNetworkEntitlement(), new OutboundNetworkEntitlement())) + ) + ); + + Set actual = PolicyUtils.getEntitlementsDescriptions(pluginPolicy); + assertThat(actual, containsInAnyOrder("create_class_loader", "outbound_network", "inbound_network")); + } + + /** Test that we can format some simple files entitlement properly */ + public void testFormatFilesEntitlement() { + var pathAB = Path.of("/a/b"); + var policy = new Policy( + "test-plugin", + List.of( + new Scope( + "module1", + List.of( + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(pathAB, FilesEntitlement.Mode.READ_WRITE), + FilesEntitlement.FileData.ofRelativePath( + Path.of("c/d"), + FilesEntitlement.BaseDir.DATA, + FilesEntitlement.Mode.READ + ) + ) + ) + ) + ), + new Scope( + "module2", + List.of( + new FilesEntitlement( + List.of( + FilesEntitlement.FileData.ofPath(pathAB, FilesEntitlement.Mode.READ_WRITE), + FilesEntitlement.FileData.ofPathSetting( + "setting", + FilesEntitlement.BaseDir.DATA, + FilesEntitlement.Mode.READ + ) + ) + ) + ) + ) + ) + ); + Set actual = PolicyUtils.getEntitlementsDescriptions(policy); + assertThat(actual, containsInAnyOrder("files [READ_WRITE] " + pathAB, "files [READ] /c/d", "files [READ] /")); + } + + /** Test that we can format some simple files entitlement properly */ + public void testFormatWriteSystemPropertiesEntitlement() { + var policy = new Policy( + "test-plugin", + List.of( + new Scope("module1", List.of(new WriteSystemPropertiesEntitlement(List.of("property1", "property2")))), + new Scope("module2", List.of(new WriteSystemPropertiesEntitlement(List.of("property2", "property3")))) + ) + ); + Set actual = PolicyUtils.getEntitlementsDescriptions(policy); + assertThat( + actual, + containsInAnyOrder( + "write_system_properties [property1]", + "write_system_properties [property2]", + "write_system_properties [property3]" + ) + ); + } } diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java deleted file mode 100644 index 95935990c4e2a..0000000000000 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java +++ /dev/null @@ -1,68 +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.plugins.cli; - -import org.elasticsearch.bootstrap.PluginPolicyInfo; -import org.elasticsearch.bootstrap.PolicyUtil; -import org.elasticsearch.entitlement.runtime.policy.PolicyParserUtils; -import org.elasticsearch.test.ESTestCase; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.PropertyPermission; -import java.util.Set; - -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; - -/** Tests plugin manager security check */ -public class PluginSecurityTests extends ESTestCase { - - - Path makeDummyPlugin(String policy) throws IOException { - Path pluginPath = createTempDir(); - Files.copy(this.getDataPath(policy), pluginPath.resolve(PolicyParserUtils.POLICY_FILE_NAME)); - return pluginPath; - } - - /** Test that we can parse the set of permissions correctly for a simple policy */ - public void testParsePermissions() throws Exception { - // TODO: rename these files, make them entitlement policies - var pluginPath = makeDummyPlugin("simple-plugin-security.policy"); - var pluginPolicy = PolicyParserUtils.parsePolicyIfExists("test-plugin", pluginPath, true); - - Set actual = PluginSecurity.getPermissionDescriptions(pluginPolicy); - assertThat(actual, contains(PluginSecurity.formatPermission(new PropertyPermission("someProperty", "read")))); - } - - /** Test that we can parse the set of permissions correctly for a complex policy */ - public void testParseTwoPermissions() throws Exception { - // TODO: adjust other tests too - Path scratch = createTempDir(); - PluginPolicyInfo info = makeDummyPlugin("complex-plugin-security.policy"); - Set actual = PluginSecurity.getPermissionDescriptions(info, scratch); - assertThat( - actual, - containsInAnyOrder( - PluginSecurity.formatPermission(new RuntimePermission("getClassLoader")), - PluginSecurity.formatPermission(new RuntimePermission("setFactory")) - ) - ); - } - - /** Test that we can format some simple permissions properly */ - public void testFormatSimplePermission() throws Exception { - assertEquals( - "java.lang.RuntimePermission accessDeclaredMembers", - PluginSecurity.formatPermission(new RuntimePermission("accessDeclaredMembers")) - ); - } -} diff --git a/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/complex-plugin-security.policy b/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/complex-plugin-security.policy deleted file mode 100644 index da4792e587d05..0000000000000 --- a/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/complex-plugin-security.policy +++ /dev/null @@ -1,14 +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". - */ - -grant { - // needed to cause problems - permission java.lang.RuntimePermission "getClassLoader"; - permission java.lang.RuntimePermission "setFactory"; -}; diff --git a/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/simple-plugin-security.policy b/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/simple-plugin-security.policy deleted file mode 100644 index f554bc62d7311..0000000000000 --- a/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/simple-plugin-security.policy +++ /dev/null @@ -1,12 +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". - */ - -grant { - permission java.util.PropertyPermission "someProperty", "read"; -}; diff --git a/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/unresolved-plugin-security.policy b/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/unresolved-plugin-security.policy deleted file mode 100644 index fe1c607f2f196..0000000000000 --- a/qa/evil-tests/src/test/resources/org/elasticsearch/plugins/cli/unresolved-plugin-security.policy +++ /dev/null @@ -1,13 +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". - */ - -grant { - // an unresolved permission - permission org.fake.FakePermission "fakeName"; -}; From c340fe46bca592ceede2a26bd172cfe15d58380c Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Wed, 19 Mar 2025 13:27:13 +0100 Subject: [PATCH 3/5] Remove unneeded policy --- .../src/main/plugin-metadata/entitlement-policy.yaml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 plugins/analysis-icu/src/main/plugin-metadata/entitlement-policy.yaml diff --git a/plugins/analysis-icu/src/main/plugin-metadata/entitlement-policy.yaml b/plugins/analysis-icu/src/main/plugin-metadata/entitlement-policy.yaml deleted file mode 100644 index 7a261a774e4aa..0000000000000 --- a/plugins/analysis-icu/src/main/plugin-metadata/entitlement-policy.yaml +++ /dev/null @@ -1,5 +0,0 @@ -org.elasticsearch.analysis.icu: - - files: - - relative_path: "" - relative_to: config - mode: read From e4698483139f72ce3c0b0ff52691c3399aeed1d7 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Wed, 19 Mar 2025 15:20:33 +0100 Subject: [PATCH 4/5] Fix InstallPluginActionTests --- .../plugins/cli/InstallPluginActionTests.java | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index 67c053e5caec3..97f75cd7a9263 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -41,14 +41,15 @@ import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.PathUtils; import org.elasticsearch.core.PathUtilsForTesting; import org.elasticsearch.core.Strings; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; +import org.elasticsearch.entitlement.runtime.policy.PolicyUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.jdk.RuntimeVersionFeature; import org.elasticsearch.plugin.scanner.NamedComponentScanner; import org.elasticsearch.plugins.Platforms; import org.elasticsearch.plugins.PluginDescriptor; @@ -57,6 +58,8 @@ import org.elasticsearch.test.PosixPermissionsResetter; import org.elasticsearch.test.compiler.InMemoryJavaCompiler; import org.elasticsearch.test.jar.JarUtils; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.yaml.YamlXContent; import org.junit.After; import org.junit.Before; @@ -102,6 +105,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; import static org.elasticsearch.snapshots.AbstractSnapshotIntegTestCase.forEachFileRecursively; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -137,8 +141,6 @@ public class InstallPluginActionTests extends ESTestCase { @SuppressForbidden(reason = "sets java.io.tmpdir") public InstallPluginActionTests(FileSystem fs, Function temp) { - assert "false".equals(System.getProperty("tests.security.manager")) : "-Dtests.security.manager=false has to be set"; - this.temp = temp; this.isPosix = fs.supportedFileAttributeViews().contains("posix"); this.isReal = fs == PathUtils.getDefaultFileSystem(); @@ -309,15 +311,20 @@ private static String[] pluginProperties(String name, String[] additionalProps, ).flatMap(Function.identity()).toArray(String[]::new); } - static void writePluginSecurityPolicy(Path pluginDir, String... permissions) throws IOException { - StringBuilder securityPolicyContent = new StringBuilder("grant {\n "); - for (String permission : permissions) { - securityPolicyContent.append("permission java.lang.RuntimePermission \""); - securityPolicyContent.append(permission); - securityPolicyContent.append("\";"); + static void writePluginEntitlementPolicy(Path pluginDir, String moduleName, CheckedConsumer policyBuilder) + throws IOException { + try (var builder = YamlXContent.contentBuilder()) { + builder.startObject(); + builder.field(moduleName); + builder.startArray(); + + policyBuilder.accept(builder); + builder.endArray(); + builder.endObject(); + + String policy = org.elasticsearch.common.Strings.toString(builder); + Files.writeString(pluginDir.resolve(PolicyUtils.POLICY_FILE_NAME), policy); } - securityPolicyContent.append("\n};\n"); - Files.write(pluginDir.resolve("plugin-security.policy"), securityPolicyContent.toString().getBytes(StandardCharsets.UTF_8)); } static InstallablePlugin createStablePlugin(String name, Path structure, boolean hasNamedComponentFile, String... additionalProps) @@ -892,9 +899,8 @@ public void testInstallMisspelledOfficialPlugins() { } public void testBatchFlag() throws Exception { - assumeTrue("security policy validation only available with SecurityManager", RuntimeVersionFeature.isSecurityManagerAvailable()); installPlugin(true); - assertThat(terminal.getErrorOutput(), containsString("WARNING: plugin requires additional permissions")); + assertThat(terminal.getErrorOutput(), containsString("WARNING: plugin requires additional entitlements")); assertThat(terminal.getOutput(), containsString("-> Downloading")); // No progress bar in batch mode assertThat(terminal.getOutput(), not(containsString("100%"))); @@ -942,12 +948,12 @@ public void testPluginHasDifferentNameThatDescriptor() throws Exception { assertThat(e.getMessage(), equalTo("Expected downloaded plugin to have ID [other-fake] but found [fake]")); } - private void installPlugin(boolean isBatch, String... additionalProperties) throws Exception { - // if batch is enabled, we also want to add a security policy + private void installPlugin(boolean isBatch) throws Exception { + // if batch is enabled, we also want to add an entitlement policy if (isBatch) { - writePluginSecurityPolicy(pluginDir, "setFactory"); + writePluginEntitlementPolicy(pluginDir, ALL_UNNAMED, builder -> builder.value("manage_threads")); } - InstallablePlugin pluginZip = createPlugin("fake", pluginDir, additionalProperties); + InstallablePlugin pluginZip = createPlugin("fake", pluginDir); skipJarHellAction.setEnvironment(env.v2()); skipJarHellAction.setBatch(isBatch); skipJarHellAction.execute(List.of(pluginZip)); @@ -1531,11 +1537,13 @@ private void assertPolicyConfirmation(Tuple pathEnvironmentTu } public void testPolicyConfirmation() throws Exception { - assumeTrue("security policy parsing only available with SecurityManager", RuntimeVersionFeature.isSecurityManagerAvailable()); - writePluginSecurityPolicy(pluginDir, "getClassLoader", "setFactory"); + writePluginEntitlementPolicy(pluginDir, "test.plugin.module", builder -> { + builder.value("manage_threads"); + builder.value("outbound_network"); + }); InstallablePlugin pluginZip = createPluginZip("fake", pluginDir); - assertPolicyConfirmation(env, pluginZip, "plugin requires additional permissions"); + assertPolicyConfirmation(env, pluginZip, "plugin requires additional entitlements"); assertPlugin("fake", pluginDir, env.v2()); } From 986138bfdacf3dfd262dc0c5c5180959e6cdada4 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Wed, 2 Apr 2025 11:01:24 +0200 Subject: [PATCH 5/5] PR comments --- distribution/tools/plugin-cli/build.gradle | 2 +- .../java/org/elasticsearch/plugins/cli/PluginSecurity.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 9019412cc5ecc..becdfbdb4d5e5 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -25,7 +25,7 @@ dependencies { implementation project(":libs:plugin-api") implementation project(":libs:plugin-scanner") implementation project(":libs:entitlement") - // TODO: asm is picked up from the plugin scanner, we should consolidate so it is not defined twice + // TODO: asm is picked up from the plugin scanner and entitlements, we should consolidate so it is not defined twice implementation 'org.ow2.asm:asm:9.7.1' implementation 'org.ow2.asm:asm-tree:9.7.1' diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java index 4ab2359728926..47bc6145c61bf 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PluginSecurity.java @@ -35,7 +35,10 @@ public class PluginSecurity { static void confirmPolicyExceptions(Terminal terminal, Set entitlements, boolean batch) throws UserException { List requested = new ArrayList<>(entitlements); if (requested.isEmpty()) { - terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional entitlements"); + terminal.println( + Verbosity.NORMAL, + "WARNING: plugin has a policy file with no additional entitlements. Double check this is intentional." + ); } else { // sort entitlements in a reasonable order Collections.sort(requested);