[Entitlements] Replace Permissions with Entitlements in InstallPluginAction (#125207) (#126115)

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 14:03:25 +02:00 committed by GitHub
parent debcd0f024
commit 9add57bc4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 260 additions and 258 deletions

View File

@ -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'

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.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.IOUtils;
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 @@ public class InstallPluginAction implements Closeable {
);
}
if (RuntimeVersionFeature.isSecurityManagerAvailable()) {
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpDir());
if (pluginPolicy != null) {
Set<String> permissions = PluginSecurity.getPermissionDescriptions(pluginPolicy, env.tmpDir());
PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch);
}
}
var pluginPolicy = PolicyUtils.parsePolicyIfExists(info.getName(), tmpRoot, true);
Set<String> 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

View File

@ -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<String> permissions, boolean batch) throws UserException {
List<String> requested = new ArrayList<>(permissions);
static void confirmPolicyExceptions(Terminal terminal, Set<String> entitlements, boolean batch) throws UserException {
List<String> 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 @@ public class PluginSecurity {
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.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.ESTestCase;
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.stream.Stream;
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<String, Path> 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 @@ public class InstallPluginActionTests extends ESTestCase {
).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<XContentBuilder, IOException> 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 class InstallPluginActionTests extends ESTestCase {
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 class InstallPluginActionTests extends ESTestCase {
}
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 class InstallPluginActionTests extends ESTestCase {
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 @@ public class InstallPluginActionTests extends ESTestCase {
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 @@ public class InstallPluginActionTests extends ESTestCase {
}
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());
}

View File

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

View File

@ -49,7 +49,7 @@ import java.util.stream.Stream;
*/
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,
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<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 String policyName;
private final boolean isExternalPlugin;
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();
if (entitlementClassName.endsWith("Entitlement") == false) {
@ -82,8 +87,12 @@ public class PolicyParser {
.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 {
this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENTS);
this(inputStream, policyName, isExternalPlugin, EXTERNAL_ENTITLEMENT_CLASSES_BY_NAME);
}
// package private for tests

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
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 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(
Collection<PluginData> pluginData,
@ -57,7 +58,6 @@ public class PolicyUtils {
Map<String, Policy> 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<String> moduleNames = getModuleNames(pluginRoot, entry.isModular());
@ -68,8 +68,8 @@ public class PolicyUtils {
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 @@ 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)) {
return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName, isExternalPlugin).parsePolicy();
}
@ -184,21 +185,79 @@ public class PolicyUtils {
return entitlementMap.values().stream().toList();
}
static Entitlement mergeEntitlement(Entitlement entitlement1, Entitlement entitlement2) {
return switch (entitlement1) {
case FilesEntitlement e -> 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<Entitlement> mergeEntitlements(Stream<Entitlement> entitlements) {
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(
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;
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 record FilesEntitlement(List<FileData> filesData) implements Entitlement
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 record FilesEntitlement(List<FileData> filesData) implements Entitlement
}
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 record FilesEntitlement(List<FileData> filesData) implements Entitlement
}
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 record FilesEntitlement(List<FileData> filesData) implements Entitlement
}
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) {

View File

@ -82,10 +82,13 @@ public class PolicyParserTests extends ESTestCase {
}
}
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'")

View File

@ -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.Base64;
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 class PolicyUtilsTests extends ESTestCase {
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 class PolicyUtilsTests extends ESTestCase {
)
);
var merged = mergeEntitlement(e1, e2);
var merged = PolicyUtils.mergeEntitlement(e1, e2);
assertThat(
merged,
transformedMatch(
@ -246,7 +245,7 @@ public class PolicyUtilsTests extends ESTestCase {
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 class PolicyUtilsTests extends ESTestCase {
new WriteSystemPropertiesEntitlement(List.of("a"))
);
var merged = mergeEntitlements(a, b);
var merged = PolicyUtils.mergeEntitlements(a, b);
assertThat(
merged,
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";
};