Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion distribution/tools/plugin-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

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 @@ -934,13 +932,10 @@ private PluginDescriptor installPlugin(InstallablePlugin descriptor, Path tmpRoo
);
}

if (RuntimeVersionFeature.isSecurityManagerAvailable()) {
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpDir());
if (pluginPolicy != null) {
Set<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,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 {
Expand All @@ -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);
Expand All @@ -86,53 +77,4 @@ private static void prompt(final Terminal terminal) throws UserException {
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
}
}

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

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

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

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

/**
* Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users.
*/
public static Set<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 @@ -787,10 +794,10 @@ public void testConfig() throws Exception {
public void testExistingConfig() throws Exception {
Path envConfigDir = env.v2().configDir().resolve("fake");
Files.createDirectories(envConfigDir);
Files.write(envConfigDir.resolve("custom.yml"), "existing config".getBytes(StandardCharsets.UTF_8));
Files.writeString(envConfigDir.resolve("custom.yml"), "existing config");
Path configDir = pluginDir.resolve("config");
Files.createDirectory(configDir);
Files.write(configDir.resolve("custom.yml"), "new config".getBytes(StandardCharsets.UTF_8));
Files.writeString(configDir.resolve("custom.yml"), "new config");
Files.createFile(configDir.resolve("other.yml"));
InstallablePlugin pluginZip = createPluginZip("fake", pluginDir);
installPlugin(pluginZip);
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 @@ -1033,13 +1039,13 @@ URL openUrl(String urlString) throws IOException {
Path shaFile = temp.apply("shas").resolve("downloaded.zip" + shaExtension);
byte[] zipbytes = Files.readAllBytes(pluginZipPath);
String checksum = shaCalculator.apply(zipbytes);
Files.write(shaFile, checksum.getBytes(StandardCharsets.UTF_8));
Files.writeString(shaFile, checksum);
return shaFile.toUri().toURL();
} else if ((url + ".asc").equals(urlString)) {
final Path ascFile = temp.apply("asc").resolve("downloaded.zip" + ".asc");
final byte[] zipBytes = Files.readAllBytes(pluginZipPath);
final String asc = signature.apply(zipBytes, secretKey);
Files.write(ascFile, asc.getBytes(StandardCharsets.UTF_8));
Files.writeString(ascFile, asc);
return ascFile.toUri().toURL();
}
return null;
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 @@ -517,7 +517,7 @@ private void checkFlagEntitlement(
classEntitlements.componentName(),
getModuleName(requestingClass),
requestingClass,
PolicyParser.getEntitlementTypeName(entitlementClass)
PolicyParser.buildEntitlementNameFromClass(entitlementClass)
),
callerClass,
classEntitlements
Expand All @@ -530,7 +530,7 @@ private void checkFlagEntitlement(
classEntitlements.componentName(),
getModuleName(requestingClass),
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