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
This commit is contained in:
Jason Tedor 2017-03-27 15:52:45 -04:00 committed by GitHub
parent fc8cb417e7
commit defd0452e7
18 changed files with 630 additions and 238 deletions

View File

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

View File

@ -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
]
}
}

View File

@ -605,7 +605,6 @@
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]JavaVersion.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]Natives.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]Security.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]Spawner.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]StartupException.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]SystemCallFilter.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]cli[/\\]Command.java" checks="LineLength" />
@ -1565,14 +1564,12 @@
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]AnalysisPlugin.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]ClusterPlugin.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]DiscoveryPlugin.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]DummyPluginInfo.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]IngestPlugin.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]InstallPluginCommand.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]ListPluginsCommand.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]MetaDataUpgrader.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]NetworkPlugin.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]Plugin.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginInfo.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginSecurity.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginsService.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]ProgressInputStream.java" checks="LineLength" />
@ -2388,7 +2385,6 @@
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]JNANativesTests.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]JarHellTests.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]MaxMapCountCheckTests.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]SpawnerTests.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]broadcast[/\\]BroadcastActionsIT.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bwcompat[/\\]OldIndexBackwardsCompatibilityIT.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bwcompat[/\\]RecoveryWithUnsupportedIndicesIT.java" checks="LineLength" />
@ -3009,7 +3005,6 @@
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]nodesinfo[/\\]NodeInfoStreamingTests.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]nodesinfo[/\\]SimpleNodesInfoIT.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]operateAllIndices[/\\]DestructiveOperationsIT.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginInfoTests.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginsServiceTests.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]recovery[/\\]FullRollingRestartIT.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]recovery[/\\]RecoveriesCollectionTests.java" checks="LineLength" />
@ -3949,11 +3944,8 @@
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]logging[/\\]EvilLoggerTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]env[/\\]NodeEnvironmentEvilTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]InstallPluginCommandTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]ListPluginsCommandTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginSecurityTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]RemovePluginCommandTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]tribe[/\\]TribeUnitTests.java" checks="LineLength" />
<suppress files="qa[/\\]no-bootstrap-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]SpawnerNoBootstrapTests.java" checks="LineLength" />
<suppress files="qa[/\\]smoke-test-client[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]smoketest[/\\]ESSmokeClientTestCase.java" checks="LineLength" />
<suppress files="qa[/\\]smoke-test-client[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]smoketest[/\\]SmokeTestClientIT.java" checks="LineLength" />
<suppress files="qa[/\\]smoke-test-http[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]http[/\\]ContextAndHeaderTransportIT.java" checks="LineLength" />

View File

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

View File

@ -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.ArrayList;
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<Process> 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<Path> 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<Path> 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());
// the output stream of the process object corresponds to the daemon's stdin
return pb.start();
}
/**
* The collection of processes representing spawned native controllers.
*
* @return the processes
*/
List<Process> getProcesses() {
return Collections.unmodifiableList(processes);
}
/**
* Make the full path to the program to be spawned.
*/
static Path makeSpawnPath(Path plugin) {
return plugin.resolve("platform").resolve(PLATFORM_NAME).resolve("bin").resolve(PROGRAM_NAME);
}
/**
* 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;
}
}

View File

@ -21,9 +21,13 @@ package org.elasticsearch.plugins;
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");
}

View File

@ -458,7 +458,7 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
// 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;

View File

@ -61,7 +61,7 @@ class ListPluginsCommand extends EnvironmentAwareCommand {
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;

View File

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

View File

@ -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.IOException;
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 class PluginInfo implements Writeable, ToXContent {
.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();
}
}

View File

@ -37,60 +37,74 @@ import java.security.UnresolvedPermission;
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<Path> tmpFile, boolean batch) throws IOException {
PermissionCollection permissions = parsePermissions(terminal, file, tmpFile.get());
List<Permission> 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<Permission>() {
@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<Permission>() {
@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 (actions2 == null) {
actions2 = "";
if (name2 == null) {
name2 = "";
}
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);
}
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.");
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);
}
}
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]");

View File

@ -101,7 +101,7 @@ public class PluginsService extends AbstractComponent {
// first we load plugins that are on the classpath. this is for tests and transport clients
for (Class<? extends Plugin> 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);
}

View File

@ -143,13 +143,13 @@ public class NodeInfoStreamingTests extends ESTestCase {
List<PluginInfo> 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<PluginInfo> 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);
}

View File

@ -56,7 +56,7 @@ public class PluginInfoTests extends ESTestCase {
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 class PluginInfoTests extends ESTestCase {
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 class PluginInfoTests extends ESTestCase {
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 class PluginInfoTests extends ESTestCase {
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 class PluginInfoTests extends ESTestCase {
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 class PluginInfoTests extends ESTestCase {
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 class PluginInfoTests extends ESTestCase {
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<PluginInfo> 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 class PluginInfoTests extends ESTestCase {
List<String> names = infos.stream().map(PluginInfo::getName).collect(Collectors.toList());
assertThat(names, contains("a", "b", "c", "d", "e"));
}
}

View File

@ -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 class SpawnerTests extends ESTestCase {
}
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"));
}
}

View File

@ -25,6 +25,7 @@ import java.nio.file.Files;
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 @@ public class ListPluginsCommandTests extends ESTestCase {
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 class ListPluginsCommandTests extends ESTestCase {
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 class ListPluginsCommandTests extends ESTestCase {
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 class ListPluginsCommandTests extends ESTestCase {
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 class ListPluginsCommandTests extends ESTestCase {
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);

View File

@ -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<Path> 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<Permission> 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())));
}
}

View File

@ -21,8 +21,11 @@ package org.elasticsearch.bootstrap;
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.List;
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 class SpawnerNoBootstrapTests extends LuceneTestCase {
// 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 class SpawnerNoBootstrapTests extends LuceneTestCase {
* 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 class SpawnerNoBootstrapTests extends LuceneTestCase {
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<Process> 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 @@ public class SpawnerNoBootstrapTests extends LuceneTestCase {
perms.add(PosixFilePermission.OTHERS_EXECUTE);
Files.setPosixFilePermissions(outputFile, perms);
}
}