From b5d32a2c93f974abdf7becd7d964b4ee04e2402c Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Wed, 2 Apr 2025 13:18:37 +0200 Subject: [PATCH] [Entitlements] Replace Permissions with Entitlements in InstallPluginAction (#125207) This PR replaces the parsing and formatting of SecurityManager policies with the parsing and formatting of Entitlements policy during plugin installation. Relates to ES-10923 --- distribution/tools/plugin-cli/build.gradle | 3 +- .../plugins/cli/InstallPluginAction.java | 15 +-- .../plugins/cli/PluginSecurity.java | 90 +++-------------- .../plugins/cli/InstallPluginActionTests.java | 56 ++++++----- .../runtime/policy/PolicyManager.java | 4 +- .../runtime/policy/PolicyParser.java | 17 +++- .../runtime/policy/PolicyUtils.java | 89 ++++++++++++++--- .../policy/entitlements/FilesEntitlement.java | 18 ++++ .../runtime/policy/PolicyParserTests.java | 9 +- .../runtime/policy/PolicyUtilsTests.java | 99 +++++++++++++++++-- .../plugin-metadata/entitlement-policy.yaml | 5 - .../plugins/cli/PluginSecurityTests.java | 76 -------------- .../cli/complex-plugin-security.policy | 14 --- .../plugins/cli/simple-plugin-security.policy | 12 --- .../cli/unresolved-plugin-security.policy | 13 --- 15 files changed, 261 insertions(+), 259 deletions(-) delete mode 100644 plugins/analysis-icu/src/main/plugin-metadata/entitlement-policy.yaml 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/build.gradle b/distribution/tools/plugin-cli/build.gradle index dc2bcd96b8d9f..becdfbdb4d5e5 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -24,7 +24,8 @@ dependencies { compileOnly project(":libs:cli") implementation project(":libs:plugin-api") implementation project(":libs:plugin-scanner") - // TODO: asm is picked up from the plugin scanner, we should consolidate so it is not defined twice + implementation project(":libs:entitlement") + // 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/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 13c9ac0aa2a16..0733fce0f5c77 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.PolicyUtils; 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; @@ -934,13 +932,10 @@ private PluginDescriptor installPlugin(InstallablePlugin descriptor, Path tmpRoo ); } - 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 = PolicyUtils.parsePolicyIfExists(info.getName(), tmpRoot, true); + + 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 6f671ccbf161b..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 @@ -9,27 +9,19 @@ 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 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.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 { @@ -40,37 +32,36 @@ public class PluginSecurity { /** * 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.NORMAL, + "WARNING: plugin has a policy file with no additional entitlements. Double check this is intentional." + ); } 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); @@ -86,53 +77,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(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); - } - - return allPermissions.stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet()); - } } 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..a4e503a894ab4 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) @@ -787,10 +794,10 @@ public void testConfig() throws Exception { public void testExistingConfig() throws Exception { Path envConfigDir = env.v2().configDir().resolve("fake"); Files.createDirectories(envConfigDir); - Files.write(envConfigDir.resolve("custom.yml"), "existing config".getBytes(StandardCharsets.UTF_8)); + Files.writeString(envConfigDir.resolve("custom.yml"), "existing config"); Path configDir = pluginDir.resolve("config"); Files.createDirectory(configDir); - Files.write(configDir.resolve("custom.yml"), "new config".getBytes(StandardCharsets.UTF_8)); + Files.writeString(configDir.resolve("custom.yml"), "new config"); Files.createFile(configDir.resolve("other.yml")); InstallablePlugin pluginZip = createPluginZip("fake", pluginDir); installPlugin(pluginZip); @@ -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)); @@ -1033,13 +1039,13 @@ URL openUrl(String urlString) throws IOException { Path shaFile = temp.apply("shas").resolve("downloaded.zip" + shaExtension); byte[] zipbytes = Files.readAllBytes(pluginZipPath); String checksum = shaCalculator.apply(zipbytes); - Files.write(shaFile, checksum.getBytes(StandardCharsets.UTF_8)); + Files.writeString(shaFile, checksum); return shaFile.toUri().toURL(); } else if ((url + ".asc").equals(urlString)) { final Path ascFile = temp.apply("asc").resolve("downloaded.zip" + ".asc"); final byte[] zipBytes = Files.readAllBytes(pluginZipPath); final String asc = signature.apply(zipBytes, secretKey); - Files.write(ascFile, asc.getBytes(StandardCharsets.UTF_8)); + Files.writeString(ascFile, asc); return ascFile.toUri().toURL(); } return null; @@ -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()); } 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 cecba80f846b9..8271ade08b3b6 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 @@ -517,7 +517,7 @@ private void checkFlagEntitlement( classEntitlements.componentName(), getModuleName(requestingClass), requestingClass, - PolicyParser.getEntitlementTypeName(entitlementClass) + PolicyParser.buildEntitlementNameFromClass(entitlementClass) ), callerClass, classEntitlements @@ -530,7 +530,7 @@ private void checkFlagEntitlement( classEntitlements.componentName(), getModuleName(requestingClass), 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..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 @@ -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,19 @@ 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 +87,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/PolicyUtils.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyUtils.java index 404a7a62aea12..9485086e50455 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 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.get(0)); + } 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 8a82d85d80eb9..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 @@ -82,10 +82,13 @@ public NonStaticMethodEntitlement create() { } } - public void testGetEntitlementTypeName() { - assertEquals("create_class_loader", PolicyParser.getEntitlementTypeName(CreateClassLoaderEntitlement.class)); + public void testBuildEntitlementNameFromClass() { + assertEquals("create_class_loader", PolicyParser.buildEntitlementNameFromClass(CreateClassLoaderEntitlement.class)); - var ex = expectThrows(IllegalArgumentException.class, () -> 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/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/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 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 52a712518357c..0000000000000 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/cli/PluginSecurityTests.java +++ /dev/null @@ -1,76 +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.jdk.RuntimeVersionFeature; -import org.elasticsearch.plugins.PluginDescriptor; -import org.elasticsearch.test.ESTestCase; -import org.junit.Before; - -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 { - - @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()); - } - - /** 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); - 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); - 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"; -};