Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions distribution/tools/plugin-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
compileOnly project(":libs:cli")
implementation project(":libs:plugin-api")
implementation project(":libs:plugin-scanner")
implementation project(":libs:entitlement")
// TODO: asm is picked up from the plugin scanner, we should consolidate so it is not defined twice
implementation 'org.ow2.asm:asm:9.7.1'
implementation 'org.ow2.asm:asm-tree:9.7.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -923,13 +921,11 @@ void jarHellCheck(PluginDescriptor candidateInfo, Path candidateDir, Path plugin
*/
private PluginDescriptor installPlugin(InstallablePlugin descriptor, Path tmpRoot, List<Path> deleteOnFailure) throws Exception {
final PluginDescriptor info = loadPluginInfo(tmpRoot);
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,65 +9,56 @@

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 {

public static final String ENTITLEMENTS_DESCRIPTION_URL =
"https://www.elastic.co/guide/en/elasticsearch/plugins/current/creating-classic-plugins.html";

/**
* prints/confirms policy exceptions with the user
*/
static void confirmPolicyExceptions(Terminal terminal, Set<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.VERBOSE, "plugin has a policy file with no additional entitlements");
} else {
// sort permissions in a reasonable order
// sort entitlements in a reasonable order
Collections.sort(requested);

if (terminal.isHeadless()) {
terminal.errorPrintln(
"WARNING: plugin requires additional permissions: ["
"WARNING: plugin requires additional entitlements: ["
+ requested.stream().map(each -> '\'' + each + '\'').collect(Collectors.joining(", "))
+ "]"
);
terminal.errorPrintln(
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
+ " for descriptions of what these permissions allow and the associated risks."
"See " + ENTITLEMENTS_DESCRIPTION_URL + " for descriptions of what these entitlements allow and the associated risks."
);
} else {
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional entitlements @");
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
// print all permissions:
for (String permission : requested) {
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission);
// print all entitlements:
for (String entitlement : requested) {
terminal.errorPrintln(Verbosity.NORMAL, "* " + entitlement);
}
terminal.errorPrintln(
Verbosity.NORMAL,
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
);
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
terminal.errorPrintln(Verbosity.NORMAL, "See " + ENTITLEMENTS_DESCRIPTION_URL);
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these entitlements allow and the associated risks.");

if (batch == false) {
prompt(terminal);
Expand All @@ -83,53 +74,4 @@ private static void prompt(final Terminal terminal) throws UserException {
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
}
}

/** Format permission type, name, and actions into a string */
static String formatPermission(Permission permission) {
StringBuilder sb = new StringBuilder();

String clazz = null;
if (permission instanceof UnresolvedPermission) {
clazz = ((UnresolvedPermission) permission).getUnresolvedType();
} else {
clazz = permission.getClass().getName();
}
sb.append(clazz);

String name = null;
if (permission instanceof UnresolvedPermission) {
name = ((UnresolvedPermission) permission).getUnresolvedName();
} else {
name = permission.getName();
}
if (name != null && name.length() > 0) {
sb.append(' ');
sb.append(name);
}

String actions = null;
if (permission instanceof UnresolvedPermission) {
actions = ((UnresolvedPermission) permission).getUnresolvedActions();
} else {
actions = permission.getActions();
}
if (actions != null && actions.length() > 0) {
sb.append(' ');
sb.append(actions);
}
return sb.toString();
}

/**
* Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users.
*/
public static Set<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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<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)
Expand Down Expand Up @@ -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%")));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -1531,11 +1537,13 @@ private void assertPolicyConfirmation(Tuple<Path, Environment> 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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ private void checkFlagEntitlement(
classEntitlements.componentName(),
requestingClass.getModule().getName(),
requestingClass,
PolicyParser.getEntitlementTypeName(entitlementClass)
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
),
callerClass,
classEntitlements
Expand All @@ -529,7 +529,7 @@ private void checkFlagEntitlement(
classEntitlements.componentName(),
requestingClass.getModule().getName(),
requestingClass,
PolicyParser.getEntitlementTypeName(entitlementClass)
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
*/
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,
Expand All @@ -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) {
Expand All @@ -82,8 +87,12 @@ static String getEntitlementTypeName(Class<? extends Entitlement> entitlementCla
.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
Expand Down
Loading
Loading