[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
This commit is contained in:
Lorenzo Dematté 2025-04-02 12:03:27 +02:00 committed by GitHub
parent 1f0551a995
commit 40dd91b800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 256 additions and 254 deletions

View File

@ -24,7 +24,8 @@ dependencies {
compileOnly project(":libs:cli") compileOnly project(":libs:cli")
implementation project(":libs:plugin-api") implementation project(":libs:plugin-api")
implementation project(":libs:plugin-scanner") 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:9.7.1'
implementation 'org.ow2.asm:asm-tree:9.7.1' implementation 'org.ow2.asm:asm-tree:9.7.1'

View File

@ -24,8 +24,6 @@ import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.elasticsearch.Build; import org.elasticsearch.Build;
import org.elasticsearch.bootstrap.PluginPolicyInfo;
import org.elasticsearch.bootstrap.PolicyUtil;
import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException; import org.elasticsearch.cli.UserException;
@ -36,9 +34,9 @@ import org.elasticsearch.core.IOUtils;
import org.elasticsearch.core.PathUtils; import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.Tuple; import org.elasticsearch.core.Tuple;
import org.elasticsearch.entitlement.runtime.policy.PolicyUtils;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.jdk.JarHell; import org.elasticsearch.jdk.JarHell;
import org.elasticsearch.jdk.RuntimeVersionFeature;
import org.elasticsearch.plugin.scanner.ClassReaders; import org.elasticsearch.plugin.scanner.ClassReaders;
import org.elasticsearch.plugin.scanner.NamedComponentScanner; import org.elasticsearch.plugin.scanner.NamedComponentScanner;
import org.elasticsearch.plugins.Platforms; import org.elasticsearch.plugins.Platforms;
@ -934,13 +932,10 @@ public class InstallPluginAction implements Closeable {
); );
} }
if (RuntimeVersionFeature.isSecurityManagerAvailable()) { var pluginPolicy = PolicyUtils.parsePolicyIfExists(info.getName(), tmpRoot, true);
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpDir());
if (pluginPolicy != null) { Set<String> entitlements = PolicyUtils.getEntitlementsDescriptions(pluginPolicy);
Set<String> permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy, env.tmpDir()); PluginSecurity.confirmPolicyExceptions(terminal, entitlements, batch);
PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch);
}
}
// Validate that the downloaded plugin's ID matches what we expect from the descriptor. The // 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 // exception is if we install a plugin via `InstallPluginCommand` by specifying a URL or

View File

@ -9,27 +9,19 @@
package org.elasticsearch.plugins.cli; package org.elasticsearch.plugins.cli;
import org.elasticsearch.bootstrap.PluginPolicyInfo;
import org.elasticsearch.bootstrap.PolicyUtil;
import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.Terminal.Verbosity; import org.elasticsearch.cli.Terminal.Verbosity;
import org.elasticsearch.cli.UserException; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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. * plugin installation can proceed.
*/ */
public class PluginSecurity { public class PluginSecurity {
@ -40,37 +32,36 @@ public class PluginSecurity {
/** /**
* prints/confirms policy exceptions with the user * prints/confirms policy exceptions with the user
*/ */
static void confirmPolicyExceptions(Terminal terminal, Set<String> permissions, boolean batch) throws UserException { static void confirmPolicyExceptions(Terminal terminal, Set<String> entitlements, boolean batch) throws UserException {
List<String> requested = new ArrayList<>(permissions); List<String> requested = new ArrayList<>(entitlements);
if (requested.isEmpty()) { 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 { } else {
// sort permissions in a reasonable order // sort entitlements in a reasonable order
Collections.sort(requested); Collections.sort(requested);
if (terminal.isHeadless()) { if (terminal.isHeadless()) {
terminal.errorPrintln( terminal.errorPrintln(
"WARNING: plugin requires additional permissions: [" "WARNING: plugin requires additional entitlements: ["
+ requested.stream().map(each -> '\'' + each + '\'').collect(Collectors.joining(", ")) + requested.stream().map(each -> '\'' + each + '\'').collect(Collectors.joining(", "))
+ "]" + "]"
); );
terminal.errorPrintln( terminal.errorPrintln(
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html" "See " + ENTITLEMENTS_DESCRIPTION_URL + " for descriptions of what these entitlements allow and the associated risks."
+ " for descriptions of what these permissions allow and the associated risks."
); );
} else { } else {
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); 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, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
// print all permissions: // print all entitlements:
for (String permission : requested) { for (String entitlement : requested) {
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission); terminal.errorPrintln(Verbosity.NORMAL, "* " + entitlement);
} }
terminal.errorPrintln( terminal.errorPrintln(Verbosity.NORMAL, "See " + ENTITLEMENTS_DESCRIPTION_URL);
Verbosity.NORMAL, terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these entitlements allow and the associated risks.");
"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.");
if (batch == false) { if (batch == false) {
prompt(terminal); prompt(terminal);
@ -86,53 +77,4 @@ public class PluginSecurity {
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user"); 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<String> getPermissionDescriptions(PluginPolicyInfo pluginPolicyInfo, Path tmpDir) throws IOException {
Set<Permission> allPermissions = new HashSet<>(PolicyUtil.getPolicyPermissions(null, pluginPolicyInfo.policy(), tmpDir));
for (URL jar : pluginPolicyInfo.jars()) {
Set<Permission> jarPermissions = PolicyUtil.getPolicyPermissions(jar, pluginPolicyInfo.policy(), tmpDir);
allPermissions.addAll(jarPermissions);
}
return allPermissions.stream().map(PluginSecurity::formatPermission).collect(Collectors.toSet());
}
} }

View File

@ -41,14 +41,15 @@ import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.core.PathUtils; import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.PathUtilsForTesting; import org.elasticsearch.core.PathUtilsForTesting;
import org.elasticsearch.core.Strings; import org.elasticsearch.core.Strings;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.Tuple; import org.elasticsearch.core.Tuple;
import org.elasticsearch.entitlement.runtime.policy.PolicyUtils;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.jdk.RuntimeVersionFeature;
import org.elasticsearch.plugin.scanner.NamedComponentScanner; import org.elasticsearch.plugin.scanner.NamedComponentScanner;
import org.elasticsearch.plugins.Platforms; import org.elasticsearch.plugins.Platforms;
import org.elasticsearch.plugins.PluginDescriptor; import org.elasticsearch.plugins.PluginDescriptor;
@ -57,6 +58,8 @@ import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.PosixPermissionsResetter; import org.elasticsearch.test.PosixPermissionsResetter;
import org.elasticsearch.test.compiler.InMemoryJavaCompiler; import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
import org.elasticsearch.test.jar.JarUtils; import org.elasticsearch.test.jar.JarUtils;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.yaml.YamlXContent;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -102,6 +105,7 @@ import java.util.stream.Stream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; 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.elasticsearch.snapshots.AbstractSnapshotIntegTestCase.forEachFileRecursively;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
@ -137,8 +141,6 @@ public class InstallPluginActionTests extends ESTestCase {
@SuppressForbidden(reason = "sets java.io.tmpdir") @SuppressForbidden(reason = "sets java.io.tmpdir")
public InstallPluginActionTests(FileSystem fs, Function<String, Path> temp) { public InstallPluginActionTests(FileSystem fs, Function<String, Path> temp) {
assert "false".equals(System.getProperty("tests.security.manager")) : "-Dtests.security.manager=false has to be set";
this.temp = temp; this.temp = temp;
this.isPosix = fs.supportedFileAttributeViews().contains("posix"); this.isPosix = fs.supportedFileAttributeViews().contains("posix");
this.isReal = fs == PathUtils.getDefaultFileSystem(); this.isReal = fs == PathUtils.getDefaultFileSystem();
@ -309,15 +311,20 @@ public class InstallPluginActionTests extends ESTestCase {
).flatMap(Function.identity()).toArray(String[]::new); ).flatMap(Function.identity()).toArray(String[]::new);
} }
static void writePluginSecurityPolicy(Path pluginDir, String... permissions) throws IOException { static void writePluginEntitlementPolicy(Path pluginDir, String moduleName, CheckedConsumer<XContentBuilder, IOException> policyBuilder)
StringBuilder securityPolicyContent = new StringBuilder("grant {\n "); throws IOException {
for (String permission : permissions) { try (var builder = YamlXContent.contentBuilder()) {
securityPolicyContent.append("permission java.lang.RuntimePermission \""); builder.startObject();
securityPolicyContent.append(permission); builder.field(moduleName);
securityPolicyContent.append("\";"); 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.writeString(pluginDir.resolve("plugin-security.policy"), securityPolicyContent.toString());
} }
static InstallablePlugin createStablePlugin(String name, Path structure, boolean hasNamedComponentFile, String... additionalProps) static InstallablePlugin createStablePlugin(String name, Path structure, boolean hasNamedComponentFile, String... additionalProps)
@ -892,9 +899,8 @@ public class InstallPluginActionTests extends ESTestCase {
} }
public void testBatchFlag() throws Exception { public void testBatchFlag() throws Exception {
assumeTrue("security policy validation only available with SecurityManager", RuntimeVersionFeature.isSecurityManagerAvailable());
installPlugin(true); 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")); assertThat(terminal.getOutput(), containsString("-> Downloading"));
// No progress bar in batch mode // No progress bar in batch mode
assertThat(terminal.getOutput(), not(containsString("100%"))); assertThat(terminal.getOutput(), not(containsString("100%")));
@ -942,12 +948,12 @@ public class InstallPluginActionTests extends ESTestCase {
assertThat(e.getMessage(), equalTo("Expected downloaded plugin to have ID [other-fake] but found [fake]")); assertThat(e.getMessage(), equalTo("Expected downloaded plugin to have ID [other-fake] but found [fake]"));
} }
private void installPlugin(boolean isBatch, String... additionalProperties) throws Exception { private void installPlugin(boolean isBatch) throws Exception {
// if batch is enabled, we also want to add a security policy // if batch is enabled, we also want to add an entitlement policy
if (isBatch) { 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.setEnvironment(env.v2());
skipJarHellAction.setBatch(isBatch); skipJarHellAction.setBatch(isBatch);
skipJarHellAction.execute(List.of(pluginZip)); skipJarHellAction.execute(List.of(pluginZip));
@ -1531,11 +1537,13 @@ public class InstallPluginActionTests extends ESTestCase {
} }
public void testPolicyConfirmation() throws Exception { public void testPolicyConfirmation() throws Exception {
assumeTrue("security policy parsing only available with SecurityManager", RuntimeVersionFeature.isSecurityManagerAvailable()); writePluginEntitlementPolicy(pluginDir, "test.plugin.module", builder -> {
writePluginSecurityPolicy(pluginDir, "getClassLoader", "setFactory"); builder.value("manage_threads");
builder.value("outbound_network");
});
InstallablePlugin pluginZip = createPluginZip("fake", pluginDir); InstallablePlugin pluginZip = createPluginZip("fake", pluginDir);
assertPolicyConfirmation(env, pluginZip, "plugin requires additional permissions"); assertPolicyConfirmation(env, pluginZip, "plugin requires additional entitlements");
assertPlugin("fake", pluginDir, env.v2()); assertPlugin("fake", pluginDir, env.v2());
} }

View File

@ -517,7 +517,7 @@ public class PolicyManager {
classEntitlements.componentName(), classEntitlements.componentName(),
getModuleName(requestingClass), getModuleName(requestingClass),
requestingClass, requestingClass,
PolicyParser.getEntitlementTypeName(entitlementClass) PolicyParser.buildEntitlementNameFromClass(entitlementClass)
), ),
callerClass, callerClass,
classEntitlements classEntitlements
@ -530,7 +530,7 @@ public class PolicyManager {
classEntitlements.componentName(), classEntitlements.componentName(),
getModuleName(requestingClass), getModuleName(requestingClass),
requestingClass, requestingClass,
PolicyParser.getEntitlementTypeName(entitlementClass) PolicyParser.buildEntitlementNameFromClass(entitlementClass)
) )
); );
} }

View File

@ -49,7 +49,7 @@ import java.util.stream.Stream;
*/ */
public class PolicyParser { public class PolicyParser {
private static final Map<String, Class<? extends Entitlement>> EXTERNAL_ENTITLEMENTS = Stream.of( private static final Map<String, Class<? extends Entitlement>> EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME = Stream.of(
CreateClassLoaderEntitlement.class, CreateClassLoaderEntitlement.class,
FilesEntitlement.class, FilesEntitlement.class,
InboundNetworkEntitlement.class, InboundNetworkEntitlement.class,
@ -59,14 +59,19 @@ public class PolicyParser {
SetHttpsConnectionPropertiesEntitlement.class, SetHttpsConnectionPropertiesEntitlement.class,
WriteAllSystemPropertiesEntitlement.class, WriteAllSystemPropertiesEntitlement.class,
WriteSystemPropertiesEntitlement.class WriteSystemPropertiesEntitlement.class
).collect(Collectors.toUnmodifiableMap(PolicyParser::getEntitlementTypeName, Function.identity())); ).collect(Collectors.toUnmodifiableMap(PolicyParser::buildEntitlementNameFromClass, Function.identity()));
private static final Map<Class<? extends Entitlement>, 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 XContentParser policyParser;
protected final String policyName; protected final String policyName;
private final boolean isExternalPlugin; private final boolean isExternalPlugin;
private final Map<String, Class<? extends Entitlement>> externalEntitlements; private final Map<String, Class<? extends Entitlement>> externalEntitlements;
static String getEntitlementTypeName(Class<? extends Entitlement> entitlementClass) { static String buildEntitlementNameFromClass(Class<? extends Entitlement> entitlementClass) {
var entitlementClassName = entitlementClass.getSimpleName(); var entitlementClassName = entitlementClass.getSimpleName();
if (entitlementClassName.endsWith("Entitlement") == false) { if (entitlementClassName.endsWith("Entitlement") == false) {
@ -82,8 +87,12 @@ public class PolicyParser {
.collect(Collectors.joining("_")); .collect(Collectors.joining("_"));
} }
public static String getEntitlementName(Class<? extends Entitlement> entitlementClass) {
return EXTERNAL_ENTITLEMENT_NAMES_BY_CLASS.get(entitlementClass);
}
public PolicyParser(InputStream inputStream, String policyName, boolean isExternalPlugin) throws IOException { 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 // package private for tests

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -47,7 +48,7 @@ public class PolicyUtils {
} }
} }
private static final String POLICY_FILE_NAME = "entitlement-policy.yaml"; public static final String POLICY_FILE_NAME = "entitlement-policy.yaml";
public static Map<String, Policy> createPluginPolicies( public static Map<String, Policy> createPluginPolicies(
Collection<PluginData> pluginData, Collection<PluginData> pluginData,
@ -57,7 +58,6 @@ public class PolicyUtils {
Map<String, Policy> pluginPolicies = new HashMap<>(pluginData.size()); Map<String, Policy> pluginPolicies = new HashMap<>(pluginData.size());
for (var entry : pluginData) { for (var entry : pluginData) {
Path pluginRoot = entry.pluginPath(); Path pluginRoot = entry.pluginPath();
Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME);
String pluginName = pluginRoot.getFileName().toString(); String pluginName = pluginRoot.getFileName().toString();
final Set<String> moduleNames = getModuleNames(pluginRoot, entry.isModular()); final Set<String> moduleNames = getModuleNames(pluginRoot, entry.isModular());
@ -68,8 +68,8 @@ public class PolicyUtils {
pluginName, pluginName,
moduleNames moduleNames
); );
var pluginPolicy = parsePolicyIfExists(pluginName, policyFile, entry.isExternalPlugin()); var pluginPolicy = parsePolicyIfExists(pluginName, pluginRoot, entry.isExternalPlugin());
validatePolicyScopes(pluginName, pluginPolicy, moduleNames, policyFile.toString()); validatePolicyScopes(pluginName, pluginPolicy, moduleNames, pluginRoot.resolve(POLICY_FILE_NAME).toString());
pluginPolicies.put( pluginPolicies.put(
pluginName, pluginName,
@ -138,7 +138,8 @@ public class PolicyUtils {
} }
} }
private static Policy parsePolicyIfExists(String pluginName, Path policyFile, boolean isExternalPlugin) throws IOException { public static Policy parsePolicyIfExists(String pluginName, Path pluginRoot, boolean isExternalPlugin) throws IOException {
Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME);
if (Files.exists(policyFile)) { if (Files.exists(policyFile)) {
return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName, isExternalPlugin).parsePolicy(); return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName, isExternalPlugin).parsePolicy();
} }
@ -184,21 +185,79 @@ public class PolicyUtils {
return entitlementMap.values().stream().toList(); return entitlementMap.values().stream().toList();
} }
static Entitlement mergeEntitlement(Entitlement entitlement1, Entitlement entitlement2) { static Entitlement mergeEntitlement(Entitlement entitlement, Entitlement other) {
return switch (entitlement1) { return switch (entitlement) {
case FilesEntitlement e -> merge(e, (FilesEntitlement) entitlement2); case FilesEntitlement e -> mergeFiles(Stream.of(e, (FilesEntitlement) other));
case WriteSystemPropertiesEntitlement e -> merge(e, (WriteSystemPropertiesEntitlement) entitlement2); case WriteSystemPropertiesEntitlement e -> mergeWriteSystemProperties(Stream.of(e, (WriteSystemPropertiesEntitlement) other));
default -> entitlement1; default -> entitlement;
}; };
} }
private static FilesEntitlement merge(FilesEntitlement a, FilesEntitlement b) { public static List<Entitlement> mergeEntitlements(Stream<Entitlement> entitlements) {
return new FilesEntitlement(Stream.concat(a.filesData().stream(), b.filesData().stream()).distinct().toList()); Map<Class<? extends Entitlement>, List<Entitlement>> entitlementMap = entitlements.collect(
Collectors.groupingBy(Entitlement::getClass)
);
List<Entitlement> 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;
} }
private static WriteSystemPropertiesEntitlement merge(WriteSystemPropertiesEntitlement a, WriteSystemPropertiesEntitlement b) { static Entitlement mergeEntitlement(Class<? extends Entitlement> entitlementClass, Stream<Entitlement> 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 FilesEntitlement mergeFiles(Stream<FilesEntitlement> entitlements) {
return new FilesEntitlement(entitlements.flatMap(x -> x.filesData().stream()).distinct().toList());
}
private static WriteSystemPropertiesEntitlement mergeWriteSystemProperties(Stream<WriteSystemPropertiesEntitlement> entitlements) {
return new WriteSystemPropertiesEntitlement( 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<String> describeEntitlement(Entitlement entitlement) {
Set<String> 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<String> getEntitlementsDescriptions(Policy pluginPolicy) {
var allEntitlements = PolicyUtils.mergeEntitlements(pluginPolicy.scopes().stream().flatMap(scope -> scope.entitlements().stream()));
Set<String> descriptions = new HashSet<>();
for (var entitlement : allEntitlements) {
descriptions.addAll(PolicyUtils.describeEntitlement(entitlement));
}
return descriptions;
}
} }

View File

@ -9,6 +9,7 @@
package org.elasticsearch.entitlement.runtime.policy.entitlements; 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.ExternalEntitlement;
import org.elasticsearch.entitlement.runtime.policy.FileUtils; import org.elasticsearch.entitlement.runtime.policy.FileUtils;
import org.elasticsearch.entitlement.runtime.policy.PathLookup; import org.elasticsearch.entitlement.runtime.policy.PathLookup;
@ -58,6 +59,8 @@ public record FilesEntitlement(List<FileData> filesData) implements Entitlement
FileData withPlatform(Platform platform); FileData withPlatform(Platform platform);
String description();
static FileData ofPath(Path path, Mode mode) { static FileData ofPath(Path path, Mode mode) {
return new AbsolutePathFileData(path, mode, null, false); return new AbsolutePathFileData(path, mode, null, false);
} }
@ -125,6 +128,11 @@ public record FilesEntitlement(List<FileData> filesData) implements Entitlement
} }
return new AbsolutePathFileData(path, mode, platform, exclusive); 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) private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode, Platform platform, boolean exclusive)
@ -149,6 +157,11 @@ public record FilesEntitlement(List<FileData> filesData) implements Entitlement
} }
return new RelativePathFileData(relativePath, baseDir, mode, platform, exclusive); 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) private record PathSettingFileData(String setting, BaseDir baseDir, Mode mode, Platform platform, boolean exclusive)
@ -176,6 +189,11 @@ public record FilesEntitlement(List<FileData> filesData) implements Entitlement
} }
return new PathSettingFileData(setting, baseDir, mode, platform, exclusive); 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) { private static Mode parseMode(String mode) {

View File

@ -82,10 +82,13 @@ public class PolicyParserTests extends ESTestCase {
} }
} }
public void testGetEntitlementTypeName() { public void testBuildEntitlementNameFromClass() {
assertEquals("create_class_loader", PolicyParser.getEntitlementTypeName(CreateClassLoaderEntitlement.class)); 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( assertThat(
ex.getMessage(), ex.getMessage(),
equalTo("TestWrongEntitlementName is not a valid Entitlement class name. A valid class name must end with 'Entitlement'") equalTo("TestWrongEntitlementName is not a valid Entitlement class name. A valid class name must end with 'Entitlement'")

View File

@ -9,6 +9,7 @@
package org.elasticsearch.entitlement.runtime.policy; 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.Entitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement; import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
@ -26,8 +27,6 @@ import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Set; 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.elasticsearch.test.LambdaMatchers.transformedMatch;
import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
@ -207,7 +206,7 @@ public class PolicyUtilsTests extends ESTestCase {
var e1 = new InboundNetworkEntitlement(); var e1 = new InboundNetworkEntitlement();
var e2 = new InboundNetworkEntitlement(); var e2 = new InboundNetworkEntitlement();
assertThat(mergeEntitlement(e1, e2), equalTo(new InboundNetworkEntitlement())); assertThat(PolicyUtils.mergeEntitlement(e1, e2), equalTo(new InboundNetworkEntitlement()));
} }
public void testMergeFilesEntitlement() { public void testMergeFilesEntitlement() {
@ -226,7 +225,7 @@ public class PolicyUtilsTests extends ESTestCase {
) )
); );
var merged = mergeEntitlement(e1, e2); var merged = PolicyUtils.mergeEntitlement(e1, e2);
assertThat( assertThat(
merged, merged,
transformedMatch( transformedMatch(
@ -246,7 +245,7 @@ public class PolicyUtilsTests extends ESTestCase {
var e1 = new WriteSystemPropertiesEntitlement(List.of("a", "b", "c")); var e1 = new WriteSystemPropertiesEntitlement(List.of("a", "b", "c"));
var e2 = new WriteSystemPropertiesEntitlement(List.of("b", "c", "d")); var e2 = new WriteSystemPropertiesEntitlement(List.of("b", "c", "d"));
var merged = mergeEntitlement(e1, e2); var merged = PolicyUtils.mergeEntitlement(e1, e2);
assertThat( assertThat(
merged, merged,
transformedMatch(x -> ((WriteSystemPropertiesEntitlement) x).properties(), containsInAnyOrder("a", "b", "c", "d")) transformedMatch(x -> ((WriteSystemPropertiesEntitlement) x).properties(), containsInAnyOrder("a", "b", "c", "d"))
@ -271,7 +270,7 @@ public class PolicyUtilsTests extends ESTestCase {
new WriteSystemPropertiesEntitlement(List.of("a")) new WriteSystemPropertiesEntitlement(List.of("a"))
); );
var merged = mergeEntitlements(a, b); var merged = PolicyUtils.mergeEntitlements(a, b);
assertThat( assertThat(
merged, merged,
containsInAnyOrder( containsInAnyOrder(
@ -288,4 +287,92 @@ public class PolicyUtilsTests extends ESTestCase {
) )
); );
} }
/** 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<String> 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<String> 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<String> actual = PolicyUtils.getEntitlementsDescriptions(policy);
assertThat(actual, containsInAnyOrder("files [READ_WRITE] " + pathAB, "files [READ] <DATA>/c/d", "files [READ] <DATA>/<setting>"));
}
/** 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<String> actual = PolicyUtils.getEntitlementsDescriptions(policy);
assertThat(
actual,
containsInAnyOrder(
"write_system_properties [property1]",
"write_system_properties [property2]",
"write_system_properties [property3]"
)
);
}
} }

View File

@ -1,5 +0,0 @@
org.elasticsearch.analysis.icu:
- files:
- relative_path: ""
relative_to: config
mode: read

View File

@ -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<String> 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<String> 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"))
);
}
}

View File

@ -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";
};

View File

@ -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";
};

View File

@ -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";
};