From defd0452e729617db5678fb4351b32fc403ca784 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 27 Mar 2017 15:52:45 -0400 Subject: [PATCH] Modify permissions dialog for plugins This commit modifies the handling of plugins that require special permissions to cover a case that was not previously covered. Relates #23742 --- .../plugin/PluginPropertiesExtension.groovy | 3 + .../gradle/plugin/PluginPropertiesTask.groovy | 3 +- .../resources/checkstyle_suppressions.xml | 8 - .../resources/plugin-descriptor.properties | 8 +- .../org/elasticsearch/bootstrap/Spawner.java | 119 ++++++------ .../plugins/DummyPluginInfo.java | 10 +- .../plugins/InstallPluginCommand.java | 2 +- .../plugins/ListPluginsCommand.java | 2 +- .../org/elasticsearch/plugins/Platforms.java | 83 +++++++++ .../org/elasticsearch/plugins/PluginInfo.java | 170 +++++++++++++----- .../elasticsearch/plugins/PluginSecurity.java | 88 +++++---- .../elasticsearch/plugins/PluginsService.java | 2 +- .../nodesinfo/NodeInfoStreamingTests.java | 4 +- .../plugins/PluginInfoTests.java | 31 ++-- .../PluginsTests.java} | 23 ++- .../plugins/ListPluginsCommandTests.java | 119 +++++++++--- .../plugins/PluginSecurityTests.java | 90 +++++++++- .../bootstrap/SpawnerNoBootstrapTests.java | 103 +++++++++-- 18 files changed, 630 insertions(+), 238 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/plugins/Platforms.java rename core/src/test/java/org/elasticsearch/{bootstrap/SpawnerTests.java => plugins/PluginsTests.java} (60%) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy index 353b8127545bf..1251be265da9a 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy @@ -39,6 +39,9 @@ class PluginPropertiesExtension { @Input String classname + @Input + boolean hasNativeController = false + /** Indicates whether the plugin jar should be made available for the transport client. */ @Input boolean hasClientJar = false diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy index 94bc0ba3e750b..91efe247a016b 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy @@ -79,7 +79,8 @@ class PluginPropertiesTask extends Copy { 'version': stringSnap(extension.version), 'elasticsearchVersion': stringSnap(VersionProperties.elasticsearch), 'javaVersion': project.targetCompatibility as String, - 'classname': extension.classname + 'classname': extension.classname, + 'hasNativeController': extension.hasNativeController ] } } diff --git a/buildSrc/src/main/resources/checkstyle_suppressions.xml b/buildSrc/src/main/resources/checkstyle_suppressions.xml index ab0a75a007aa4..3ba1d004e69e5 100644 --- a/buildSrc/src/main/resources/checkstyle_suppressions.xml +++ b/buildSrc/src/main/resources/checkstyle_suppressions.xml @@ -605,7 +605,6 @@ - @@ -1565,14 +1564,12 @@ - - @@ -2388,7 +2385,6 @@ - @@ -3009,7 +3005,6 @@ - @@ -3949,11 +3944,8 @@ - - - diff --git a/buildSrc/src/main/resources/plugin-descriptor.properties b/buildSrc/src/main/resources/plugin-descriptor.properties index ebde46d326ba9..67c6ee39968cd 100644 --- a/buildSrc/src/main/resources/plugin-descriptor.properties +++ b/buildSrc/src/main/resources/plugin-descriptor.properties @@ -30,11 +30,15 @@ name=${name} # 'classname': the name of the class to load, fully-qualified. classname=${classname} # -# 'java.version' version of java the code is built against +# 'java.version': version of java the code is built against # use the system property java.specification.version # version string must be a sequence of nonnegative decimal integers # separated by "."'s and may have leading zeros java.version=${javaVersion} # -# 'elasticsearch.version' version of elasticsearch compiled against +# 'elasticsearch.version': version of elasticsearch compiled against elasticsearch.version=${elasticsearchVersion} +### optional elements for plugins: +# +# 'has.native.controller': whether or not the plugin has a native controller +has.native.controller=${hasNativeController} diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java b/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java index 44cf2d2b0aa40..53983cb472ef1 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java @@ -19,9 +19,10 @@ package org.elasticsearch.bootstrap; -import org.apache.lucene.util.Constants; import org.apache.lucene.util.IOUtils; import org.elasticsearch.env.Environment; +import org.elasticsearch.plugins.PluginInfo; +import org.elasticsearch.plugins.Platforms; import java.io.Closeable; import java.io.IOException; @@ -32,97 +33,89 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; /** - * Spawns native plugin controller processes if present. Will only work prior to a system call filter being installed. + * Spawns native plugin controller processes if present. Will only work prior to a system call + * filter being installed. */ final class Spawner implements Closeable { - private static final String PROGRAM_NAME = Constants.WINDOWS ? "controller.exe" : "controller"; - private static final String PLATFORM_NAME = makePlatformName(Constants.OS_NAME, Constants.OS_ARCH); - private static final String TMP_ENVVAR = "TMPDIR"; - - /** + /* * References to the processes that have been spawned, so that we can destroy them. */ private final List processes = new ArrayList<>(); + private AtomicBoolean spawned = new AtomicBoolean(); @Override public void close() throws IOException { - try { - IOUtils.close(() -> processes.stream().map(s -> (Closeable)s::destroy).iterator()); - } finally { - processes.clear(); - } + IOUtils.close(() -> processes.stream().map(s -> (Closeable) s::destroy).iterator()); } /** - * For each plugin, attempt to spawn the controller daemon. Silently ignore any plugins - * that don't include a controller for the correct platform. + * Spawns the native controllers for each plugin + * + * @param environment the node environment + * @throws IOException if an I/O error occurs reading the plugins or spawning a native process */ - void spawnNativePluginControllers(Environment environment) throws IOException { - if (Files.exists(environment.pluginsFile())) { - try (DirectoryStream stream = Files.newDirectoryStream(environment.pluginsFile())) { - for (Path plugin : stream) { - Path spawnPath = makeSpawnPath(plugin); - if (Files.isRegularFile(spawnPath)) { - spawnNativePluginController(spawnPath, environment.tmpFile()); - } + void spawnNativePluginControllers(final Environment environment) throws IOException { + if (!spawned.compareAndSet(false, true)) { + throw new IllegalStateException("native controllers already spawned"); + } + final Path pluginsFile = environment.pluginsFile(); + if (!Files.exists(pluginsFile)) { + throw new IllegalStateException("plugins directory [" + pluginsFile + "] not found"); + } + /* + * For each plugin, attempt to spawn the controller daemon. Silently ignore any plugin that + * don't include a controller for the correct platform. + */ + try (DirectoryStream stream = Files.newDirectoryStream(pluginsFile)) { + for (final Path plugin : stream) { + final PluginInfo info = PluginInfo.readFromProperties(plugin); + final Path spawnPath = Platforms.nativeControllerPath(plugin); + if (!Files.isRegularFile(spawnPath)) { + continue; + } + if (!info.hasNativeController()) { + final String message = String.format( + Locale.ROOT, + "plugin [%s] does not have permission to fork native controller", + plugin.getFileName()); + throw new IllegalArgumentException(message); } + final Process process = + spawnNativePluginController(spawnPath, environment.tmpFile()); + processes.add(process); } } } /** - * Attempt to spawn the controller daemon for a given plugin. The spawned process - * will remain connected to this JVM via its stdin, stdout and stderr, but the - * references to these streams are not available to code outside this package. + * Attempt to spawn the controller daemon for a given plugin. The spawned process will remain + * connected to this JVM via its stdin, stdout, and stderr streams, but the references to these + * streams are not available to code outside this package. */ - private void spawnNativePluginController(Path spawnPath, Path tmpPath) throws IOException { - ProcessBuilder pb = new ProcessBuilder(spawnPath.toString()); + private Process spawnNativePluginController( + final Path spawnPath, + final Path tmpPath) throws IOException { + final ProcessBuilder pb = new ProcessBuilder(spawnPath.toString()); - // The only environment variable passes on the path to the temporary directory + // the only environment variable passes on the path to the temporary directory pb.environment().clear(); - pb.environment().put(TMP_ENVVAR, tmpPath.toString()); + pb.environment().put("TMPDIR", tmpPath.toString()); - // The output stream of the Process object corresponds to the daemon's stdin - processes.add(pb.start()); - } - - List getProcesses() { - return Collections.unmodifiableList(processes); + // the output stream of the process object corresponds to the daemon's stdin + return pb.start(); } /** - * Make the full path to the program to be spawned. + * The collection of processes representing spawned native controllers. + * + * @return the processes */ - static Path makeSpawnPath(Path plugin) { - return plugin.resolve("platform").resolve(PLATFORM_NAME).resolve("bin").resolve(PROGRAM_NAME); + List getProcesses() { + return Collections.unmodifiableList(processes); } - /** - * Make the platform name in the format used in Kibana downloads, for example: - * - darwin-x86_64 - * - linux-x86-64 - * - windows-x86_64 - * For *nix platforms this is more-or-less `uname -s`-`uname -m` converted to lower case. - * However, for consistency between different operating systems on the same architecture - * "amd64" is replaced with "x86_64" and "i386" with "x86". - * For Windows it's "windows-" followed by either "x86" or "x86_64". - */ - static String makePlatformName(String osName, String osArch) { - String os = osName.toLowerCase(Locale.ROOT); - if (os.startsWith("windows")) { - os = "windows"; - } else if (os.equals("mac os x")) { - os = "darwin"; - } - String cpu = osArch.toLowerCase(Locale.ROOT); - if (cpu.equals("amd64")) { - cpu = "x86_64"; - } else if (cpu.equals("i386")) { - cpu = "x86"; - } - return os + "-" + cpu; - } } diff --git a/core/src/main/java/org/elasticsearch/plugins/DummyPluginInfo.java b/core/src/main/java/org/elasticsearch/plugins/DummyPluginInfo.java index 90b1d32f4ae4a..73a3811f729f7 100644 --- a/core/src/main/java/org/elasticsearch/plugins/DummyPluginInfo.java +++ b/core/src/main/java/org/elasticsearch/plugins/DummyPluginInfo.java @@ -21,9 +21,13 @@ public class DummyPluginInfo extends PluginInfo { private DummyPluginInfo(String name, String description, String version, String classname) { - super(name, description, version, classname); + super(name, description, version, classname, false); } - public static final DummyPluginInfo INSTANCE = new DummyPluginInfo( - "dummy_plugin_name", "dummy plugin description", "dummy_plugin_version", "DummyPluginName"); + public static final DummyPluginInfo INSTANCE = + new DummyPluginInfo( + "dummy_plugin_name", + "dummy plugin description", + "dummy_plugin_version", + "DummyPluginName"); } diff --git a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java index 7ab9996a7098e..7360eef9238a4 100644 --- a/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java +++ b/core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -458,7 +458,7 @@ private PluginInfo verify(Terminal terminal, Path pluginRoot, boolean isBatch, E // if it exists, confirm or warn the user Path policy = pluginRoot.resolve(PluginInfo.ES_PLUGIN_POLICY); if (Files.exists(policy)) { - PluginSecurity.readPolicy(policy, terminal, env, isBatch); + PluginSecurity.readPolicy(info, policy, terminal, env::tmpFile, isBatch); } return info; diff --git a/core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java b/core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java index 1c6c4f17ff284..c2b5ce34b5469 100644 --- a/core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java +++ b/core/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java @@ -61,7 +61,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th PluginInfo info = PluginInfo.readFromProperties(env.pluginsFile().resolve(plugin.toAbsolutePath())); terminal.println(Terminal.Verbosity.VERBOSE, info.toString()); } catch (IllegalArgumentException e) { - if (e.getMessage().contains("incompatible with Elasticsearch")) { + if (e.getMessage().contains("incompatible with version")) { terminal.println("WARNING: " + e.getMessage()); } else { throw e; diff --git a/core/src/main/java/org/elasticsearch/plugins/Platforms.java b/core/src/main/java/org/elasticsearch/plugins/Platforms.java new file mode 100644 index 0000000000000..62bb32a4e9a1a --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/Platforms.java @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import org.apache.lucene.util.Constants; + +import java.nio.file.Path; +import java.util.Locale; + +/** + * Encapsulates platform-dependent methods for handling native components of plugins. + */ +public class Platforms { + + private static final String PROGRAM_NAME = Constants.WINDOWS ? "controller.exe" : "controller"; + private static final String PLATFORM_NAME = + Platforms.platformName(Constants.OS_NAME, Constants.OS_ARCH); + + private Platforms() {} + + /** + * The path to the native controller for a plugin with native components. + */ + public static Path nativeControllerPath(Path plugin) { + return plugin + .resolve("platform") + .resolve(PLATFORM_NAME) + .resolve("bin") + .resolve(PROGRAM_NAME); + } + + /** + * Return the platform name based on the OS name and + * - darwin-x86_64 + * - linux-x86-64 + * - windows-x86_64 + * For *nix platforms this is more-or-less `uname -s`-`uname -m` converted to lower case. + * However, for consistency between different operating systems on the same architecture + * "amd64" is replaced with "x86_64" and "i386" with "x86". + * For Windows it's "windows-" followed by either "x86" or "x86_64". + */ + public static String platformName(final String osName, final String osArch) { + final String lowerCaseOs = osName.toLowerCase(Locale.ROOT); + final String normalizedOs; + if (lowerCaseOs.startsWith("windows")) { + normalizedOs = "windows"; + } else if (lowerCaseOs.equals("mac os x")) { + normalizedOs = "darwin"; + } else { + normalizedOs = lowerCaseOs; + } + + final String lowerCaseArch = osArch.toLowerCase(Locale.ROOT); + final String normalizedArch; + if (lowerCaseArch.equals("amd64")) { + normalizedArch = "x86_64"; + } else if (lowerCaseArch.equals("i386")) { + normalizedArch = "x86"; + } else { + normalizedArch = lowerCaseArch; + } + + return normalizedOs + "-" + normalizedArch; + } + +} diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java b/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java index 3e241eadd37ba..9c0ae89e95416 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.plugins; import org.elasticsearch.Version; @@ -30,133 +31,215 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Locale; import java.util.Properties; +/** + * An in-memory representation of the plugin descriptor. + */ public class PluginInfo implements Writeable, ToXContent { public static final String ES_PLUGIN_PROPERTIES = "plugin-descriptor.properties"; public static final String ES_PLUGIN_POLICY = "plugin-security.policy"; - static final class Fields { - static final String NAME = "name"; - static final String DESCRIPTION = "description"; - static final String URL = "url"; - static final String VERSION = "version"; - static final String CLASSNAME = "classname"; - } - private final String name; private final String description; private final String version; private final String classname; + private final boolean hasNativeController; /** - * Information about plugins + * Construct plugin info. * - * @param name Its name - * @param description Its description - * @param version Version number + * @param name the name of the plugin + * @param description a description of the plugin + * @param version the version of Elasticsearch the plugin is built for + * @param classname the entry point to the plugin + * @param hasNativeController whether or not the plugin has a native controller */ - public PluginInfo(String name, String description, String version, String classname) { + public PluginInfo( + final String name, + final String description, + final String version, + final String classname, + final boolean hasNativeController) { this.name = name; this.description = description; this.version = version; this.classname = classname; + this.hasNativeController = hasNativeController; } - public PluginInfo(StreamInput in) throws IOException { + /** + * Construct plugin info from a stream. + * + * @param in the stream + * @throws IOException if an I/O exception occurred reading the plugin info from the stream + */ + public PluginInfo(final StreamInput in) throws IOException { this.name = in.readString(); this.description = in.readString(); this.version = in.readString(); this.classname = in.readString(); + if (in.getVersion().after(Version.V_5_4_0_UNRELEASED)) { + hasNativeController = in.readBoolean(); + } else { + hasNativeController = false; + } } @Override - public void writeTo(StreamOutput out) throws IOException { + public void writeTo(final StreamOutput out) throws IOException { out.writeString(name); out.writeString(description); out.writeString(version); out.writeString(classname); + if (out.getVersion().after(Version.V_5_4_0_UNRELEASED)) { + out.writeBoolean(hasNativeController); + } } /** reads (and validates) plugin metadata descriptor file */ - public static PluginInfo readFromProperties(Path dir) throws IOException { - Path descriptor = dir.resolve(ES_PLUGIN_PROPERTIES); - Properties props = new Properties(); + + /** + * Reads and validates the plugin descriptor file. + * + * @param path the path to the root directory for the plugin + * @return the plugin info + * @throws IOException if an I/O exception occurred reading the plugin descriptor + */ + public static PluginInfo readFromProperties(final Path path) throws IOException { + final Path descriptor = path.resolve(ES_PLUGIN_PROPERTIES); + final Properties props = new Properties(); try (InputStream stream = Files.newInputStream(descriptor)) { props.load(stream); } - String name = props.getProperty("name"); + final String name = props.getProperty("name"); if (name == null || name.isEmpty()) { - throw new IllegalArgumentException("Property [name] is missing in [" + descriptor + "]"); + throw new IllegalArgumentException( + "property [name] is missing in [" + descriptor + "]"); } - String description = props.getProperty("description"); + final String description = props.getProperty("description"); if (description == null) { - throw new IllegalArgumentException("Property [description] is missing for plugin [" + name + "]"); + throw new IllegalArgumentException( + "property [description] is missing for plugin [" + name + "]"); } - String version = props.getProperty("version"); + final String version = props.getProperty("version"); if (version == null) { - throw new IllegalArgumentException("Property [version] is missing for plugin [" + name + "]"); + throw new IllegalArgumentException( + "property [version] is missing for plugin [" + name + "]"); } - String esVersionString = props.getProperty("elasticsearch.version"); + final String esVersionString = props.getProperty("elasticsearch.version"); if (esVersionString == null) { - throw new IllegalArgumentException("Property [elasticsearch.version] is missing for plugin [" + name + "]"); + throw new IllegalArgumentException( + "property [elasticsearch.version] is missing for plugin [" + name + "]"); } - Version esVersion = Version.fromString(esVersionString); + final Version esVersion = Version.fromString(esVersionString); if (esVersion.equals(Version.CURRENT) == false) { - throw new IllegalArgumentException("Plugin [" + name + "] is incompatible with Elasticsearch [" + Version.CURRENT.toString() + - "]. Was designed for version [" + esVersionString + "]"); + final String message = String.format( + Locale.ROOT, + "plugin [%s] is incompatible with version [%s]; was designed for version [%s]", + name, + Version.CURRENT.toString(), + esVersionString); + throw new IllegalArgumentException(message); } - String javaVersionString = props.getProperty("java.version"); + final String javaVersionString = props.getProperty("java.version"); if (javaVersionString == null) { - throw new IllegalArgumentException("Property [java.version] is missing for plugin [" + name + "]"); + throw new IllegalArgumentException( + "property [java.version] is missing for plugin [" + name + "]"); } JarHell.checkVersionFormat(javaVersionString); JarHell.checkJavaVersion(name, javaVersionString); - String classname = props.getProperty("classname"); + final String classname = props.getProperty("classname"); if (classname == null) { - throw new IllegalArgumentException("Property [classname] is missing for plugin [" + name + "]"); + throw new IllegalArgumentException( + "property [classname] is missing for plugin [" + name + "]"); } - return new PluginInfo(name, description, version, classname); + final String hasNativeControllerValue = props.getProperty("has.native.controller"); + final boolean hasNativeController; + if (hasNativeControllerValue == null) { + hasNativeController = false; + } else { + switch (hasNativeControllerValue) { + case "true": + hasNativeController = true; + break; + case "false": + hasNativeController = false; + break; + default: + final String message = String.format( + Locale.ROOT, + "property [%s] must be [%s], [%s], or unspecified but was [%s]", + "has_native_controller", + "true", + "false", + hasNativeControllerValue); + throw new IllegalArgumentException(message); + } + } + + return new PluginInfo(name, description, version, classname, hasNativeController); } /** - * @return Plugin's name + * The name of the plugin. + * + * @return the plugin name */ public String getName() { return name; } /** - * @return Plugin's description if any + * The description of the plugin. + * + * @return the plugin description */ public String getDescription() { return description; } /** - * @return plugin's classname + * The entry point to the plugin. + * + * @return the entry point to the plugin */ public String getClassname() { return classname; } /** - * @return Version number for the plugin + * The version of Elasticsearch the plugin was built for. + * + * @return the version */ public String getVersion() { return version; } + /** + * Whether or not the plugin has a native controller. + * + * @return {@code true} if the plugin has a native controller + */ + public boolean hasNativeController() { + return hasNativeController; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(Fields.NAME, name); - builder.field(Fields.VERSION, version); - builder.field(Fields.DESCRIPTION, description); - builder.field(Fields.CLASSNAME, classname); + { + builder.field("name", name); + builder.field("version", version); + builder.field("description", description); + builder.field("classname", classname); + builder.field("has_native_controller", hasNativeController); + } builder.endObject(); return builder; @@ -187,8 +270,9 @@ public String toString() { .append("Name: ").append(name).append("\n") .append("Description: ").append(description).append("\n") .append("Version: ").append(version).append("\n") + .append("Native Controller: ").append(hasNativeController).append("\n") .append(" * Classname: ").append(classname); - return information.toString(); } + } diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java b/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java index f9c3d1826c992..55a3c6069e7ef 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginSecurity.java @@ -37,60 +37,74 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.function.Supplier; class PluginSecurity { /** * Reads plugin policy, prints/confirms exceptions */ - static void readPolicy(Path file, Terminal terminal, Environment environment, boolean batch) throws IOException { - PermissionCollection permissions = parsePermissions(terminal, file, environment.tmpFile()); + static void readPolicy(PluginInfo info, Path file, Terminal terminal, Supplier tmpFile, boolean batch) throws IOException { + PermissionCollection permissions = parsePermissions(terminal, file, tmpFile.get()); List requested = Collections.list(permissions.elements()); if (requested.isEmpty()) { terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions"); - return; - } + } else { - // sort permissions in a reasonable order - Collections.sort(requested, new Comparator() { - @Override - public int compare(Permission o1, Permission o2) { - int cmp = o1.getClass().getName().compareTo(o2.getClass().getName()); - if (cmp == 0) { - String name1 = o1.getName(); - String name2 = o2.getName(); - if (name1 == null) { - name1 = ""; - } - if (name2 == null) { - name2 = ""; - } - cmp = name1.compareTo(name2); + // sort permissions in a reasonable order + Collections.sort(requested, new Comparator() { + @Override + public int compare(Permission o1, Permission o2) { + int cmp = o1.getClass().getName().compareTo(o2.getClass().getName()); if (cmp == 0) { - String actions1 = o1.getActions(); - String actions2 = o2.getActions(); - if (actions1 == null) { - actions1 = ""; + String name1 = o1.getName(); + String name2 = o2.getName(); + if (name1 == null) { + name1 = ""; + } + if (name2 == null) { + name2 = ""; } - if (actions2 == null) { - actions2 = ""; + cmp = name1.compareTo(name2); + if (cmp == 0) { + String actions1 = o1.getActions(); + String actions2 = o2.getActions(); + if (actions1 == null) { + actions1 = ""; + } + if (actions2 == null) { + actions2 = ""; + } + cmp = actions1.compareTo(actions2); } - cmp = actions1.compareTo(actions2); } + return cmp; } - return cmp; + }); + + terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + terminal.println(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @"); + terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + // print all permissions: + for (Permission permission : requested) { + terminal.println(Verbosity.NORMAL, "* " + formatPermission(permission)); } - }); - - terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - terminal.println(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @"); - terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - // print all permissions: - for (Permission permission : requested) { - terminal.println(Verbosity.NORMAL, "* " + formatPermission(permission)); + terminal.println(Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"); + terminal.println(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks."); + prompt(terminal, batch); + } + + if (info.hasNativeController()) { + terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + terminal.println(Verbosity.NORMAL, "@ WARNING: plugin forks a native controller @"); + terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + terminal.println(Verbosity.NORMAL, "This plugin launches a native controller that is not subject to the Java"); + terminal.println(Verbosity.NORMAL, "security manager nor to system call filters."); + prompt(terminal, batch); } - terminal.println(Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"); - terminal.println(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks."); + } + + private static void prompt(final Terminal terminal, final boolean batch) { if (!batch) { terminal.println(Verbosity.NORMAL, ""); String text = terminal.readText("Continue with installation? [y/N]"); diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginsService.java b/core/src/main/java/org/elasticsearch/plugins/PluginsService.java index 9295c6c38d872..fc63678b94f98 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginsService.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginsService.java @@ -101,7 +101,7 @@ public PluginsService(Settings settings, Path modulesDirectory, Path pluginsDire // first we load plugins that are on the classpath. this is for tests and transport clients for (Class pluginClass : classpathPlugins) { Plugin plugin = loadPlugin(pluginClass, settings); - PluginInfo pluginInfo = new PluginInfo(pluginClass.getName(), "classpath plugin", "NA", pluginClass.getName()); + PluginInfo pluginInfo = new PluginInfo(pluginClass.getName(), "classpath plugin", "NA", pluginClass.getName(), false); if (logger.isTraceEnabled()) { logger.trace("plugin loaded from classpath [{}]", pluginInfo); } diff --git a/core/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java b/core/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java index 9c36a7a649ad6..51aad14227883 100644 --- a/core/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java +++ b/core/src/test/java/org/elasticsearch/nodesinfo/NodeInfoStreamingTests.java @@ -143,13 +143,13 @@ private static NodeInfo createNodeInfo() { List plugins = new ArrayList<>(); for (int i = 0; i < numPlugins; i++) { plugins.add(new PluginInfo(randomAsciiOfLengthBetween(3, 10), randomAsciiOfLengthBetween(3, 10), - randomAsciiOfLengthBetween(3, 10), randomAsciiOfLengthBetween(3, 10))); + randomAsciiOfLengthBetween(3, 10), randomAsciiOfLengthBetween(3, 10), randomBoolean())); } int numModules = randomIntBetween(0, 5); List modules = new ArrayList<>(); for (int i = 0; i < numModules; i++) { modules.add(new PluginInfo(randomAsciiOfLengthBetween(3, 10), randomAsciiOfLengthBetween(3, 10), - randomAsciiOfLengthBetween(3, 10), randomAsciiOfLengthBetween(3, 10))); + randomAsciiOfLengthBetween(3, 10), randomAsciiOfLengthBetween(3, 10), randomBoolean())); } pluginsAndModules = new PluginsAndModules(plugins, modules); } diff --git a/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java b/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java index 4ad52be8866a8..04afdd5839181 100644 --- a/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java +++ b/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java @@ -56,7 +56,7 @@ public void testReadFromPropertiesNameMissing() throws Exception { PluginInfo.readFromProperties(pluginDir); fail("expected missing name exception"); } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("Property [name] is missing in")); + assertTrue(e.getMessage().contains("property [name] is missing in")); } PluginTestUtil.writeProperties(pluginDir, "name", ""); @@ -64,7 +64,7 @@ public void testReadFromPropertiesNameMissing() throws Exception { PluginInfo.readFromProperties(pluginDir); fail("expected missing name exception"); } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("Property [name] is missing in")); + assertTrue(e.getMessage().contains("property [name] is missing in")); } } @@ -81,7 +81,8 @@ public void testReadFromPropertiesDescriptionMissing() throws Exception { public void testReadFromPropertiesVersionMissing() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, "description", "fake desc", "name", "fake-plugin"); + PluginTestUtil.writeProperties( + pluginDir, "description", "fake desc", "name", "fake-plugin"); try { PluginInfo.readFromProperties(pluginDir); fail("expected missing version exception"); @@ -151,7 +152,11 @@ public void testReadFromPropertiesBadJavaVersionFormat() throws Exception { PluginInfo.readFromProperties(pluginDir); fail("expected bad java version format exception"); } catch (IllegalStateException e) { - assertTrue(e.getMessage(), e.getMessage().equals("version string must be a sequence of nonnegative decimal integers separated by \".\"'s and may have leading zeros but was 1.7.0_80")); + assertTrue( + e.getMessage(), + e.getMessage().equals("version string must be a sequence of nonnegative " + + "decimal integers separated by \".\"'s and may have leading zeros " + + "but was 1.7.0_80")); } } @@ -166,7 +171,8 @@ public void testReadFromPropertiesBogusElasticsearchVersion() throws Exception { PluginInfo.readFromProperties(pluginDir); fail("expected bogus elasticsearch version exception"); } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("version needs to contain major, minor, and revision")); + assertTrue(e.getMessage().contains( + "version needs to contain major, minor, and revision")); } } @@ -181,7 +187,7 @@ public void testReadFromPropertiesOldElasticsearchVersion() throws Exception { PluginInfo.readFromProperties(pluginDir); fail("expected old elasticsearch version exception"); } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("Was designed for version [2.0.0]")); + assertTrue(e.getMessage().contains("was designed for version [2.0.0]")); } } @@ -197,17 +203,17 @@ public void testReadFromPropertiesJvmMissingClassname() throws Exception { PluginInfo.readFromProperties(pluginDir); fail("expected old elasticsearch version exception"); } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("Property [classname] is missing")); + assertTrue(e.getMessage().contains("property [classname] is missing")); } } public void testPluginListSorted() { List plugins = new ArrayList<>(); - plugins.add(new PluginInfo("c", "foo", "dummy", "dummyclass")); - plugins.add(new PluginInfo("b", "foo", "dummy", "dummyclass")); - plugins.add(new PluginInfo("e", "foo", "dummy", "dummyclass")); - plugins.add(new PluginInfo("a", "foo", "dummy", "dummyclass")); - plugins.add(new PluginInfo("d", "foo", "dummy", "dummyclass")); + plugins.add(new PluginInfo("c", "foo", "dummy", "dummyclass", randomBoolean())); + plugins.add(new PluginInfo("b", "foo", "dummy", "dummyclass", randomBoolean())); + plugins.add(new PluginInfo("e", "foo", "dummy", "dummyclass", randomBoolean())); + plugins.add(new PluginInfo("a", "foo", "dummy", "dummyclass", randomBoolean())); + plugins.add(new PluginInfo("d", "foo", "dummy", "dummyclass", randomBoolean())); PluginsAndModules pluginsInfo = new PluginsAndModules(plugins, Collections.emptyList()); @@ -215,4 +221,5 @@ public void testPluginListSorted() { List names = infos.stream().map(PluginInfo::getName).collect(Collectors.toList()); assertThat(names, contains("a", "b", "c", "d", "e")); } + } diff --git a/core/src/test/java/org/elasticsearch/bootstrap/SpawnerTests.java b/core/src/test/java/org/elasticsearch/plugins/PluginsTests.java similarity index 60% rename from core/src/test/java/org/elasticsearch/bootstrap/SpawnerTests.java rename to core/src/test/java/org/elasticsearch/plugins/PluginsTests.java index 58c112ba96d3a..a26bcb1991e4f 100644 --- a/core/src/test/java/org/elasticsearch/bootstrap/SpawnerTests.java +++ b/core/src/test/java/org/elasticsearch/plugins/PluginsTests.java @@ -17,20 +17,17 @@ * under the License. */ -package org.elasticsearch.bootstrap; +package org.elasticsearch.plugins; import org.apache.lucene.util.Constants; import org.elasticsearch.test.ESTestCase; import java.util.Locale; -/** - * Doesn't actually test spawning a process, as a system call filter is installed before tests run and forbids it. - */ -public class SpawnerTests extends ESTestCase { +public class PluginsTests extends ESTestCase { public void testMakePlatformName() { - String platformName = Spawner.makePlatformName(Constants.OS_NAME, Constants.OS_ARCH); + final String platformName = Platforms.platformName(Constants.OS_NAME, Constants.OS_ARCH); assertFalse(platformName, platformName.isEmpty()); assertTrue(platformName, platformName.equals(platformName.toLowerCase(Locale.ROOT))); @@ -40,13 +37,13 @@ public void testMakePlatformName() { } public void testMakeSpecificPlatformNames() { - assertEquals("darwin-x86_64", Spawner.makePlatformName("Mac OS X", "x86_64")); - assertEquals("linux-x86_64", Spawner.makePlatformName("Linux", "amd64")); - assertEquals("linux-x86", Spawner.makePlatformName("Linux", "i386")); - assertEquals("windows-x86_64", Spawner.makePlatformName("Windows Server 2008 R2", "amd64")); - assertEquals("windows-x86", Spawner.makePlatformName("Windows Server 2008", "x86")); - assertEquals("windows-x86_64", Spawner.makePlatformName("Windows 8.1", "amd64")); - assertEquals("sunos-x86_64", Spawner.makePlatformName("SunOS", "amd64")); + assertEquals("darwin-x86_64", Platforms.platformName("Mac OS X", "x86_64")); + assertEquals("linux-x86_64", Platforms.platformName("Linux", "amd64")); + assertEquals("linux-x86", Platforms.platformName("Linux", "i386")); + assertEquals("windows-x86_64", Platforms.platformName("Windows Server 2008 R2", "amd64")); + assertEquals("windows-x86", Platforms.platformName("Windows Server 2008", "x86")); + assertEquals("windows-x86_64", Platforms.platformName("Windows 8.1", "amd64")); + assertEquals("sunos-x86_64", Platforms.platformName("SunOS", "amd64")); } } diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java index 10918eea189d7..7ebc2f0709bce 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java @@ -25,6 +25,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Arrays; +import java.util.Locale; import java.util.stream.Collectors; import org.apache.lucene.util.LuceneTestCase; @@ -72,25 +73,39 @@ protected boolean addShutdownHook() { return terminal; } - static String buildMultiline(String... args){ - return Arrays.asList(args).stream().collect(Collectors.joining("\n", "", "\n")); + private static String buildMultiline(String... args){ + return Arrays.stream(args).collect(Collectors.joining("\n", "", "\n")); } - static void buildFakePlugin(Environment env, String description, String name, String classname) throws IOException { - PluginTestUtil.writeProperties(env.pluginsFile().resolve(name), + private static void buildFakePlugin( + final Environment env, + final String description, + final String name, + final String classname) throws IOException { + buildFakePlugin(env, description, name, classname, false); + } + + private static void buildFakePlugin( + final Environment env, + final String description, + final String name, + final String classname, + final boolean hasNativeController) throws IOException { + PluginTestUtil.writeProperties( + env.pluginsFile().resolve(name), "description", description, "name", name, "version", "1.0", "elasticsearch.version", Version.CURRENT.toString(), "java.version", System.getProperty("java.specification.version"), - "classname", classname); + "classname", classname, + "has.native.controller", Boolean.toString(hasNativeController)); } - public void testPluginsDirMissing() throws Exception { Files.delete(env.pluginsFile()); IOException e = expectThrows(IOException.class, () -> listPlugins(home)); - assertEquals(e.getMessage(), "Plugins directory missing: " + env.pluginsFile()); + assertEquals("Plugins directory missing: " + env.pluginsFile(), e.getMessage()); } public void testNoPlugins() throws Exception { @@ -101,22 +116,48 @@ public void testNoPlugins() throws Exception { public void testOnePlugin() throws Exception { buildFakePlugin(env, "fake desc", "fake", "org.fake"); MockTerminal terminal = listPlugins(home); - assertEquals(terminal.getOutput(), buildMultiline("fake")); + assertEquals(buildMultiline("fake"), terminal.getOutput()); } public void testTwoPlugins() throws Exception { buildFakePlugin(env, "fake desc", "fake1", "org.fake"); buildFakePlugin(env, "fake desc 2", "fake2", "org.fake"); MockTerminal terminal = listPlugins(home); - assertEquals(terminal.getOutput(), buildMultiline("fake1", "fake2")); + assertEquals(buildMultiline("fake1", "fake2"), terminal.getOutput()); } public void testPluginWithVerbose() throws Exception { buildFakePlugin(env, "fake desc", "fake_plugin", "org.fake"); String[] params = { "-v" }; MockTerminal terminal = listPlugins(home, params); - assertEquals(terminal.getOutput(), buildMultiline("Plugins directory: " + env.pluginsFile(), "fake_plugin", - "- Plugin information:", "Name: fake_plugin", "Description: fake desc", "Version: 1.0", " * Classname: org.fake")); + assertEquals( + buildMultiline( + "Plugins directory: " + env.pluginsFile(), + "fake_plugin", + "- Plugin information:", + "Name: fake_plugin", + "Description: fake desc", + "Version: 1.0", + "Native Controller: false", + " * Classname: org.fake"), + terminal.getOutput()); + } + + public void testPluginWithNativeController() throws Exception { + buildFakePlugin(env, "fake desc 1", "fake_plugin1", "org.fake", true); + String[] params = { "-v" }; + MockTerminal terminal = listPlugins(home, params); + assertEquals( + buildMultiline( + "Plugins directory: " + env.pluginsFile(), + "fake_plugin1", + "- Plugin information:", + "Name: fake_plugin1", + "Description: fake desc 1", + "Version: 1.0", + "Native Controller: true", + " * Classname: org.fake"), + terminal.getOutput()); } public void testPluginWithVerboseMultiplePlugins() throws Exception { @@ -124,10 +165,24 @@ public void testPluginWithVerboseMultiplePlugins() throws Exception { buildFakePlugin(env, "fake desc 2", "fake_plugin2", "org.fake2"); String[] params = { "-v" }; MockTerminal terminal = listPlugins(home, params); - assertEquals(terminal.getOutput(), buildMultiline("Plugins directory: " + env.pluginsFile(), - "fake_plugin1", "- Plugin information:", "Name: fake_plugin1", "Description: fake desc 1", "Version: 1.0", - " * Classname: org.fake", "fake_plugin2", "- Plugin information:", "Name: fake_plugin2", - "Description: fake desc 2", "Version: 1.0", " * Classname: org.fake2")); + assertEquals( + buildMultiline( + "Plugins directory: " + env.pluginsFile(), + "fake_plugin1", + "- Plugin information:", + "Name: fake_plugin1", + "Description: fake desc 1", + "Version: 1.0", + "Native Controller: false", + " * Classname: org.fake", + "fake_plugin2", + "- Plugin information:", + "Name: fake_plugin2", + "Description: fake desc 2", + "Version: 1.0", + "Native Controller: false", + " * Classname: org.fake2"), + terminal.getOutput()); } public void testPluginWithoutVerboseMultiplePlugins() throws Exception { @@ -135,21 +190,26 @@ public void testPluginWithoutVerboseMultiplePlugins() throws Exception { buildFakePlugin(env, "fake desc 2", "fake_plugin2", "org.fake2"); MockTerminal terminal = listPlugins(home, new String[0]); String output = terminal.getOutput(); - assertEquals(output, buildMultiline("fake_plugin1", "fake_plugin2")); + assertEquals(buildMultiline("fake_plugin1", "fake_plugin2"), output); } public void testPluginWithoutDescriptorFile() throws Exception{ - Files.createDirectories(env.pluginsFile().resolve("fake1")); + final Path pluginDir = env.pluginsFile().resolve("fake1"); + Files.createDirectories(pluginDir); NoSuchFileException e = expectThrows(NoSuchFileException.class, () -> listPlugins(home)); - assertEquals(e.getFile(), env.pluginsFile().resolve("fake1").resolve(PluginInfo.ES_PLUGIN_PROPERTIES).toString()); + assertEquals(pluginDir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES).toString(), e.getFile()); } public void testPluginWithWrongDescriptorFile() throws Exception{ - PluginTestUtil.writeProperties(env.pluginsFile().resolve("fake1"), - "description", "fake desc"); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> listPlugins(home)); - assertEquals(e.getMessage(), "Property [name] is missing in [" + - env.pluginsFile().resolve("fake1").resolve(PluginInfo.ES_PLUGIN_PROPERTIES).toString() + "]"); + final Path pluginDir = env.pluginsFile().resolve("fake1"); + PluginTestUtil.writeProperties(pluginDir, "description", "fake desc"); + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> listPlugins(home)); + final Path descriptorPath = pluginDir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES); + assertEquals( + "property [name] is missing in [" + descriptorPath.toString() + "]", + e.getMessage()); } public void testExistingIncompatiblePlugin() throws Exception { @@ -163,11 +223,14 @@ public void testExistingIncompatiblePlugin() throws Exception { buildFakePlugin(env, "fake desc 2", "fake_plugin2", "org.fake2"); MockTerminal terminal = listPlugins(home); - assertEquals("fake_plugin1\n" + - "WARNING: Plugin [fake_plugin1] is incompatible with Elasticsearch [" + - Version.CURRENT.toString() + "]. Was designed for version [1.0.0]\n" + - "fake_plugin2\n", - terminal.getOutput()); + final String message = String.format(Locale.ROOT, + "plugin [%s] is incompatible with version [%s]; was designed for version [%s]", + "fake_plugin1", + Version.CURRENT.toString(), + "1.0.0"); + assertEquals( + "fake_plugin1\n" + "WARNING: " + message + "\n" + "fake_plugin2\n", + terminal.getOutput()); String[] params = {"-s"}; terminal = listPlugins(home, params); diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginSecurityTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginSecurityTests.java index 466f7d05cd1d2..77ecd12f786fb 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginSecurityTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/plugins/PluginSecurityTests.java @@ -19,60 +19,132 @@ package org.elasticsearch.plugins; +import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.Version; +import org.elasticsearch.cli.MockTerminal; import org.elasticsearch.cli.Terminal; import org.elasticsearch.test.ESTestCase; +import java.io.IOException; import java.nio.file.Path; import java.security.Permission; import java.security.PermissionCollection; import java.security.Permissions; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.not; /** Tests plugin manager security check */ public class PluginSecurityTests extends ESTestCase { + private final Supplier tmpFile = LuceneTestCase::createTempDir; + + public void testHasNativeController() throws IOException { + assumeTrue( + "test cannot run with security manager enabled", + System.getSecurityManager() == null); + final PluginInfo info = + new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", true); + final MockTerminal terminal = new MockTerminal(); + terminal.addTextInput("y"); + terminal.addTextInput("y"); + final Path policyFile = this.getDataPath("security/simple-plugin-security.policy"); + PluginSecurity.readPolicy(info, policyFile, terminal, tmpFile, false); + final String output = terminal.getOutput(); + assertThat(output, containsString("plugin forks a native controller")); + } + + public void testDeclineNativeController() throws IOException { + assumeTrue( + "test cannot run with security manager enabled", + System.getSecurityManager() == null); + final PluginInfo info = + new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", true); + final MockTerminal terminal = new MockTerminal(); + terminal.addTextInput("y"); + terminal.addTextInput("n"); + final Path policyFile = this.getDataPath("security/simple-plugin-security.policy"); + RuntimeException e = expectThrows( + RuntimeException.class, + () -> PluginSecurity.readPolicy(info, policyFile, terminal, tmpFile, false)); + assertThat(e, hasToString(containsString("installation aborted by user"))); + } + + public void testDoesNotHaveNativeController() throws IOException { + assumeTrue( + "test cannot run with security manager enabled", + System.getSecurityManager() == null); + final PluginInfo info = + new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", false); + final MockTerminal terminal = new MockTerminal(); + terminal.addTextInput("y"); + final Path policyFile = this.getDataPath("security/simple-plugin-security.policy"); + PluginSecurity.readPolicy(info, policyFile, terminal, tmpFile, false); + final String output = terminal.getOutput(); + assertThat(output, not(containsString("plugin forks a native controller"))); + } + /** 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); + assumeTrue( + "test cannot run with security manager enabled", + System.getSecurityManager() == null); Path scratch = createTempDir(); Path testFile = this.getDataPath("security/simple-plugin-security.policy"); Permissions expected = new Permissions(); expected.add(new RuntimePermission("queuePrintJob")); - PermissionCollection actual = PluginSecurity.parsePermissions(Terminal.DEFAULT, testFile, scratch); + PermissionCollection actual = + PluginSecurity.parsePermissions(Terminal.DEFAULT, testFile, scratch); assertEquals(expected, actual); } /** 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); + assumeTrue( + "test cannot run with security manager enabled", + System.getSecurityManager() == null); Path scratch = createTempDir(); Path testFile = this.getDataPath("security/complex-plugin-security.policy"); Permissions expected = new Permissions(); expected.add(new RuntimePermission("getClassLoader")); expected.add(new RuntimePermission("closeClassLoader")); - PermissionCollection actual = PluginSecurity.parsePermissions(Terminal.DEFAULT, testFile, scratch); + PermissionCollection actual = + PluginSecurity.parsePermissions(Terminal.DEFAULT, testFile, scratch); assertEquals(expected, actual); } /** Test that we can format some simple permissions properly */ public void testFormatSimplePermission() throws Exception { - assertEquals("java.lang.RuntimePermission queuePrintJob", PluginSecurity.formatPermission(new RuntimePermission("queuePrintJob"))); + assertEquals( + "java.lang.RuntimePermission queuePrintJob", + PluginSecurity.formatPermission(new RuntimePermission("queuePrintJob"))); } /** Test that we can format an unresolved permission properly */ public void testFormatUnresolvedPermission() throws Exception { - assumeTrue("test cannot run with security manager enabled", System.getSecurityManager() == null); + assumeTrue( + "test cannot run with security manager enabled", + System.getSecurityManager() == null); Path scratch = createTempDir(); Path testFile = this.getDataPath("security/unresolved-plugin-security.policy"); - PermissionCollection actual = PluginSecurity.parsePermissions(Terminal.DEFAULT, testFile, scratch); + PermissionCollection actual = + PluginSecurity.parsePermissions(Terminal.DEFAULT, testFile, scratch); List permissions = Collections.list(actual.elements()); assertEquals(1, permissions.size()); - assertEquals("org.fake.FakePermission fakeName", PluginSecurity.formatPermission(permissions.get(0))); + assertEquals( + "org.fake.FakePermission fakeName", + PluginSecurity.formatPermission(permissions.get(0))); } /** no guaranteed equals on these classes, we assert they contain the same set */ private void assertEquals(PermissionCollection expected, PermissionCollection actual) { - assertEquals(asSet(Collections.list(expected.elements())), asSet(Collections.list(actual.elements()))); + assertEquals( + asSet(Collections.list(expected.elements())), + asSet(Collections.list(actual.elements()))); } + } diff --git a/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java b/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java index 743d2408b9d49..f81bd3b2d4794 100644 --- a/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java +++ b/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java @@ -21,8 +21,11 @@ import org.apache.lucene.util.Constants; import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.Version; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; +import org.elasticsearch.plugins.PluginTestUtil; +import org.elasticsearch.plugins.Platforms; import java.io.BufferedReader; import java.io.IOException; @@ -36,11 +39,15 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + /** * Create a simple "daemon controller", put it in the right place and check that it runs. * - * Extends LuceneTestCase rather than ESTestCase as ESTestCase installs a system call filter, and that prevents the Spawner class doing its - * job. Also needs to run in a separate JVM to other tests that extend ESTestCase for the same reason. + * Extends LuceneTestCase rather than ESTestCase as ESTestCase installs a system call filter, and + * that prevents the Spawner class from doing its job. Also needs to run in a separate JVM to other + * tests that extend ESTestCase for the same reason. */ public class SpawnerNoBootstrapTests extends LuceneTestCase { @@ -64,10 +71,19 @@ public void testNoControllerSpawn() throws IOException, InterruptedException { // This plugin will NOT have a controller daemon Path plugin = environment.pluginsFile().resolve("a_plugin"); Files.createDirectories(plugin); + PluginTestUtil.writeProperties( + plugin, + "description", "a_plugin", + "version", Version.CURRENT.toString(), + "elasticsearch.version", Version.CURRENT.toString(), + "name", "a_plugin", + "java.version", "1.8", + "classname", "APlugin", + "has.native.controller", "false"); try (Spawner spawner = new Spawner()) { spawner.spawnNativePluginControllers(environment); - assertTrue(spawner.getProcesses().isEmpty()); + assertThat(spawner.getProcesses(), hasSize(0)); } } @@ -75,10 +91,10 @@ public void testNoControllerSpawn() throws IOException, InterruptedException { * Two plugins - one with a controller daemon and one without. */ public void testControllerSpawn() throws IOException, InterruptedException { - // On Windows you cannot directly run a batch file - you have to run cmd.exe with the batch file - // as an argument and that's out of the remit of the controller daemon process spawner. If - // you need to build on Windows, just don't run this test. The process spawner itself will work - // with native processes. + /* + * On Windows you can not directly run a batch file - you have to run cmd.exe with the batch + * file as an argument and that's out of the remit of the controller daemon process spawner. + */ assumeFalse("This test does not work on Windows", Constants.WINDOWS); Path esHome = createTempDir().resolve("esHome"); @@ -88,32 +104,90 @@ public void testControllerSpawn() throws IOException, InterruptedException { Environment environment = new Environment(settings); - // This plugin WILL have a controller daemon + // this plugin will have a controller daemon Path plugin = environment.pluginsFile().resolve("test_plugin"); Files.createDirectories(plugin); - Path controllerProgram = Spawner.makeSpawnPath(plugin); + PluginTestUtil.writeProperties( + plugin, + "description", "test_plugin", + "version", Version.CURRENT.toString(), + "elasticsearch.version", Version.CURRENT.toString(), + "name", "test_plugin", + "java.version", "1.8", + "classname", "TestPlugin", + "has.native.controller", "true"); + Path controllerProgram = Platforms.nativeControllerPath(plugin); createControllerProgram(controllerProgram); - // This plugin will NOT have a controller daemon + // this plugin will not have a controller daemon Path otherPlugin = environment.pluginsFile().resolve("other_plugin"); Files.createDirectories(otherPlugin); + PluginTestUtil.writeProperties( + otherPlugin, + "description", "other_plugin", + "version", Version.CURRENT.toString(), + "elasticsearch.version", Version.CURRENT.toString(), + "name", "other_plugin", + "java.version", "1.8", + "classname", "OtherPlugin", + "has.native.controller", "false"); Spawner spawner = new Spawner(); spawner.spawnNativePluginControllers(environment); List processes = spawner.getProcesses(); - // 1 because there should only be a reference in the list for the plugin that had the controller daemon, not the other plugin - assertEquals(1, processes.size()); + /* + * As there should only be a reference in the list for the plugin that had the controller + * daemon, we expect one here. + */ + assertThat(processes, hasSize(1)); Process process = processes.get(0); - try (BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + final InputStreamReader in = + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8); + try (BufferedReader stdoutReader = new BufferedReader(in)) { String line = stdoutReader.readLine(); assertEquals("I am alive", line); spawner.close(); - // Fail if the process doesn't die within 1 second - usually it will be even quicker but it depends on OS scheduling + /* + * Fail if the process does not die within one second; usually it will be even quicker + * but it depends on OS scheduling. + */ assertTrue(process.waitFor(1, TimeUnit.SECONDS)); } } + public void testControllerSpawnWithIncorrectDescriptor() throws IOException { + // this plugin will have a controller daemon + Path esHome = createTempDir().resolve("esHome"); + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.toString()); + Settings settings = settingsBuilder.build(); + + Environment environment = new Environment(settings); + + Path plugin = environment.pluginsFile().resolve("test_plugin"); + Files.createDirectories(plugin); + PluginTestUtil.writeProperties( + plugin, + "description", "test_plugin", + "version", Version.CURRENT.toString(), + "elasticsearch.version", Version.CURRENT.toString(), + "name", "test_plugin", + "java.version", "1.8", + "classname", "TestPlugin", + "has.native.controller", "false"); + Path controllerProgram = Platforms.nativeControllerPath(plugin); + createControllerProgram(controllerProgram); + + Spawner spawner = new Spawner(); + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> spawner.spawnNativePluginControllers(environment)); + assertThat( + e.getMessage(), + equalTo("plugin [test_plugin] does not have permission to fork native controller")); + } + private void createControllerProgram(Path outputFile) throws IOException { Path outputDir = outputFile.getParent(); Files.createDirectories(outputDir); @@ -128,4 +202,5 @@ private void createControllerProgram(Path outputFile) throws IOException { perms.add(PosixFilePermission.OTHERS_EXECUTE); Files.setPosixFilePermissions(outputFile, perms); } + }