Settings: Move keystore creation to plugin installation (#26329)
This commit removes the keystore creation on elasticsearch startup, and instead adds a plugin property which indicates the plugin needs the keystore to exist. It does still make sure the keystore.seed exists on ES startup, but through an "upgrade" method that loading the keystore in Bootstrap calls. closes #26309
This commit is contained in:
parent
7fb716daab
commit
5202e7e93b
|
@ -46,6 +46,10 @@ class PluginPropertiesExtension {
|
||||||
@Input
|
@Input
|
||||||
boolean hasClientJar = false
|
boolean hasClientJar = false
|
||||||
|
|
||||||
|
/** True if the plugin requires the elasticsearch keystore to exist, false otherwise. */
|
||||||
|
@Input
|
||||||
|
boolean requiresKeystore = false
|
||||||
|
|
||||||
/** A license file that should be included in the built plugin zip. */
|
/** A license file that should be included in the built plugin zip. */
|
||||||
@Input
|
@Input
|
||||||
File licenseFile = null
|
File licenseFile = null
|
||||||
|
|
|
@ -80,7 +80,8 @@ class PluginPropertiesTask extends Copy {
|
||||||
'elasticsearchVersion': stringSnap(VersionProperties.elasticsearch),
|
'elasticsearchVersion': stringSnap(VersionProperties.elasticsearch),
|
||||||
'javaVersion': project.targetCompatibility as String,
|
'javaVersion': project.targetCompatibility as String,
|
||||||
'classname': extension.classname,
|
'classname': extension.classname,
|
||||||
'hasNativeController': extension.hasNativeController
|
'hasNativeController': extension.hasNativeController,
|
||||||
|
'requiresKeystore': extension.requiresKeystore
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,3 +42,6 @@ elasticsearch.version=${elasticsearchVersion}
|
||||||
#
|
#
|
||||||
# 'has.native.controller': whether or not the plugin has a native controller
|
# 'has.native.controller': whether or not the plugin has a native controller
|
||||||
has.native.controller=${hasNativeController}
|
has.native.controller=${hasNativeController}
|
||||||
|
#
|
||||||
|
# 'requires.keystore': whether or not the plugin needs the elasticsearch keystore be created
|
||||||
|
requires.keystore=${requiresKeystore}
|
||||||
|
|
|
@ -226,16 +226,13 @@ final class Bootstrap {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new BootstrapException(e);
|
throw new BootstrapException(e);
|
||||||
}
|
}
|
||||||
|
if (keystore == null) {
|
||||||
|
return null; // no keystore
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (keystore == null) {
|
keystore.decrypt(new char[0] /* TODO: read password from stdin */);
|
||||||
// create it, we always want one! we use an empty passphrase, but a user can change this later if they want.
|
KeyStoreWrapper.upgrade(keystore, initialEnv.configFile());
|
||||||
KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create(new char[0]);
|
|
||||||
keyStoreWrapper.save(initialEnv.configFile());
|
|
||||||
return keyStoreWrapper;
|
|
||||||
} else {
|
|
||||||
keystore.decrypt(new char[0] /* TODO: read password from stdin */);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BootstrapException(e);
|
throw new BootstrapException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,7 +154,7 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a path representing the ES keystore in the given config dir. */
|
/** Returns a path representing the ES keystore in the given config dir. */
|
||||||
static Path keystorePath(Path configDir) {
|
public static Path keystorePath(Path configDir) {
|
||||||
return configDir.resolve(KEYSTORE_FILENAME);
|
return configDir.resolve(KEYSTORE_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +171,8 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add the bootstrap seed setting, which may be used as a unique, secure, random value by the node */
|
/** Add the bootstrap seed setting, which may be used as a unique, secure, random value by the node */
|
||||||
private static void addBootstrapSeed(KeyStoreWrapper wrapper) throws GeneralSecurityException {
|
public static void addBootstrapSeed(KeyStoreWrapper wrapper) throws GeneralSecurityException {
|
||||||
|
assert wrapper.getSettingNames().contains(SEED_SETTING.getKey()) == false;
|
||||||
SecureRandom random = Randomness.createSecure();
|
SecureRandom random = Randomness.createSecure();
|
||||||
int passwordLength = 20; // Generate 20 character passwords
|
int passwordLength = 20; // Generate 20 character passwords
|
||||||
char[] characters = new char[passwordLength];
|
char[] characters = new char[passwordLength];
|
||||||
|
@ -227,6 +228,16 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Upgrades the format of the keystore, if necessary. */
|
||||||
|
public static void upgrade(KeyStoreWrapper wrapper, Path configDir) throws Exception {
|
||||||
|
// ensure keystore.seed exists
|
||||||
|
if (wrapper.getSettingNames().contains(SEED_SETTING.getKey())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addBootstrapSeed(wrapper);
|
||||||
|
wrapper.save(configDir);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLoaded() {
|
public boolean isLoaded() {
|
||||||
return keystore.get() != null;
|
return keystore.get() != null;
|
||||||
|
@ -252,11 +263,9 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
} finally {
|
} finally {
|
||||||
Arrays.fill(keystoreBytes, (byte)0);
|
Arrays.fill(keystoreBytes, (byte)0);
|
||||||
}
|
}
|
||||||
|
|
||||||
keystorePassword.set(new KeyStore.PasswordProtection(password));
|
keystorePassword.set(new KeyStore.PasswordProtection(password));
|
||||||
Arrays.fill(password, '\0');
|
Arrays.fill(password, '\0');
|
||||||
|
|
||||||
|
|
||||||
Enumeration<String> aliases = keystore.get().aliases();
|
Enumeration<String> aliases = keystore.get().aliases();
|
||||||
if (formatVersion == 1) {
|
if (formatVersion == 1) {
|
||||||
while (aliases.hasMoreElements()) {
|
while (aliases.hasMoreElements()) {
|
||||||
|
@ -279,6 +288,7 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
|
|
||||||
/** Write the keystore to the given config directory. */
|
/** Write the keystore to the given config directory. */
|
||||||
public void save(Path configDir) throws Exception {
|
public void save(Path configDir) throws Exception {
|
||||||
|
assert isLoaded();
|
||||||
char[] password = this.keystorePassword.get().getPassword();
|
char[] password = this.keystorePassword.get().getPassword();
|
||||||
|
|
||||||
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
|
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
|
||||||
|
@ -332,6 +342,7 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
// TODO: make settings accessible only to code that registered the setting
|
// TODO: make settings accessible only to code that registered the setting
|
||||||
@Override
|
@Override
|
||||||
public SecureString getString(String setting) throws GeneralSecurityException {
|
public SecureString getString(String setting) throws GeneralSecurityException {
|
||||||
|
assert isLoaded();
|
||||||
KeyStore.Entry entry = keystore.get().getEntry(setting, keystorePassword.get());
|
KeyStore.Entry entry = keystore.get().getEntry(setting, keystorePassword.get());
|
||||||
if (settingTypes.get(setting) != KeyType.STRING ||
|
if (settingTypes.get(setting) != KeyType.STRING ||
|
||||||
entry instanceof KeyStore.SecretKeyEntry == false) {
|
entry instanceof KeyStore.SecretKeyEntry == false) {
|
||||||
|
@ -347,6 +358,7 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getFile(String setting) throws GeneralSecurityException {
|
public InputStream getFile(String setting) throws GeneralSecurityException {
|
||||||
|
assert isLoaded();
|
||||||
KeyStore.Entry entry = keystore.get().getEntry(setting, keystorePassword.get());
|
KeyStore.Entry entry = keystore.get().getEntry(setting, keystorePassword.get());
|
||||||
if (settingTypes.get(setting) != KeyType.FILE ||
|
if (settingTypes.get(setting) != KeyType.FILE ||
|
||||||
entry instanceof KeyStore.SecretKeyEntry == false) {
|
entry instanceof KeyStore.SecretKeyEntry == false) {
|
||||||
|
@ -377,6 +389,7 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
* @throws IllegalArgumentException if the value is not ASCII
|
* @throws IllegalArgumentException if the value is not ASCII
|
||||||
*/
|
*/
|
||||||
void setString(String setting, char[] value) throws GeneralSecurityException {
|
void setString(String setting, char[] value) throws GeneralSecurityException {
|
||||||
|
assert isLoaded();
|
||||||
if (ASCII_ENCODER.canEncode(CharBuffer.wrap(value)) == false) {
|
if (ASCII_ENCODER.canEncode(CharBuffer.wrap(value)) == false) {
|
||||||
throw new IllegalArgumentException("Value must be ascii");
|
throw new IllegalArgumentException("Value must be ascii");
|
||||||
}
|
}
|
||||||
|
@ -387,6 +400,7 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
|
|
||||||
/** Set a file setting. */
|
/** Set a file setting. */
|
||||||
void setFile(String setting, byte[] bytes) throws GeneralSecurityException {
|
void setFile(String setting, byte[] bytes) throws GeneralSecurityException {
|
||||||
|
assert isLoaded();
|
||||||
bytes = Base64.getEncoder().encode(bytes);
|
bytes = Base64.getEncoder().encode(bytes);
|
||||||
char[] chars = new char[bytes.length];
|
char[] chars = new char[bytes.length];
|
||||||
for (int i = 0; i < chars.length; ++i) {
|
for (int i = 0; i < chars.length; ++i) {
|
||||||
|
@ -399,6 +413,7 @@ public class KeyStoreWrapper implements SecureSettings {
|
||||||
|
|
||||||
/** Remove the given setting from the keystore. */
|
/** Remove the given setting from the keystore. */
|
||||||
void remove(String setting) throws KeyStoreException {
|
void remove(String setting) throws KeyStoreException {
|
||||||
|
assert isLoaded();
|
||||||
keystore.get().deleteEntry(setting);
|
keystore.get().deleteEntry(setting);
|
||||||
settingTypes.remove(setting);
|
settingTypes.remove(setting);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ package org.elasticsearch.plugins;
|
||||||
public class DummyPluginInfo extends PluginInfo {
|
public class DummyPluginInfo extends PluginInfo {
|
||||||
|
|
||||||
private DummyPluginInfo(String name, String description, String version, String classname) {
|
private DummyPluginInfo(String name, String description, String version, String classname) {
|
||||||
super(name, description, version, classname, false);
|
super(name, description, version, classname, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final DummyPluginInfo INSTANCE =
|
public static final DummyPluginInfo INSTANCE =
|
||||||
|
|
|
@ -21,6 +21,7 @@ package org.elasticsearch.plugins;
|
||||||
|
|
||||||
import org.elasticsearch.Version;
|
import org.elasticsearch.Version;
|
||||||
import org.elasticsearch.bootstrap.JarHell;
|
import org.elasticsearch.bootstrap.JarHell;
|
||||||
|
import org.elasticsearch.common.Booleans;
|
||||||
import org.elasticsearch.common.io.stream.StreamInput;
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
import org.elasticsearch.common.io.stream.Writeable;
|
import org.elasticsearch.common.io.stream.Writeable;
|
||||||
|
@ -48,6 +49,7 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
private final String version;
|
private final String version;
|
||||||
private final String classname;
|
private final String classname;
|
||||||
private final boolean hasNativeController;
|
private final boolean hasNativeController;
|
||||||
|
private final boolean requiresKeystore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct plugin info.
|
* Construct plugin info.
|
||||||
|
@ -57,18 +59,16 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
* @param version the version of Elasticsearch the plugin is built for
|
* @param version the version of Elasticsearch the plugin is built for
|
||||||
* @param classname the entry point to the plugin
|
* @param classname the entry point to the plugin
|
||||||
* @param hasNativeController whether or not the plugin has a native controller
|
* @param hasNativeController whether or not the plugin has a native controller
|
||||||
|
* @param requiresKeystore whether or not the plugin requires the elasticsearch keystore to be created
|
||||||
*/
|
*/
|
||||||
public PluginInfo(
|
public PluginInfo(String name, String description, String version, String classname,
|
||||||
final String name,
|
boolean hasNativeController, boolean requiresKeystore) {
|
||||||
final String description,
|
|
||||||
final String version,
|
|
||||||
final String classname,
|
|
||||||
final boolean hasNativeController) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.classname = classname;
|
this.classname = classname;
|
||||||
this.hasNativeController = hasNativeController;
|
this.hasNativeController = hasNativeController;
|
||||||
|
this.requiresKeystore = requiresKeystore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,6 +87,11 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
} else {
|
} else {
|
||||||
hasNativeController = false;
|
hasNativeController = false;
|
||||||
}
|
}
|
||||||
|
if (in.getVersion().onOrAfter(Version.V_6_0_0_beta2)) {
|
||||||
|
requiresKeystore = in.readBoolean();
|
||||||
|
} else {
|
||||||
|
requiresKeystore = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -98,6 +103,9 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
if (out.getVersion().onOrAfter(Version.V_5_4_0)) {
|
if (out.getVersion().onOrAfter(Version.V_5_4_0)) {
|
||||||
out.writeBoolean(hasNativeController);
|
out.writeBoolean(hasNativeController);
|
||||||
}
|
}
|
||||||
|
if (out.getVersion().onOrAfter(Version.V_6_0_0_beta2)) {
|
||||||
|
out.writeBoolean(requiresKeystore);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** reads (and validates) plugin metadata descriptor file */
|
/** reads (and validates) plugin metadata descriptor file */
|
||||||
|
@ -173,17 +181,26 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
final String message = String.format(
|
final String message = String.format(
|
||||||
Locale.ROOT,
|
Locale.ROOT,
|
||||||
"property [%s] must be [%s], [%s], or unspecified but was [%s]",
|
"property [%s] must be [%s], [%s], or unspecified but was [%s]",
|
||||||
"has_native_controller",
|
"has_native_controller",
|
||||||
"true",
|
"true",
|
||||||
"false",
|
"false",
|
||||||
hasNativeControllerValue);
|
hasNativeControllerValue);
|
||||||
throw new IllegalArgumentException(message);
|
throw new IllegalArgumentException(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PluginInfo(name, description, version, classname, hasNativeController);
|
final String requiresKeystoreValue = props.getProperty("requires.keystore", "false");
|
||||||
|
final boolean requiresKeystore;
|
||||||
|
try {
|
||||||
|
requiresKeystore = Booleans.parseBoolean(requiresKeystoreValue);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IllegalArgumentException("property [requires.keystore] must be [true] or [false]," +
|
||||||
|
" but was [" + requiresKeystoreValue + "]", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PluginInfo(name, description, version, classname, hasNativeController, requiresKeystore);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -231,6 +248,15 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
return hasNativeController;
|
return hasNativeController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the plugin requires the elasticsearch keystore to exist.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the plugin requires a keystore, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean requiresKeystore() {
|
||||||
|
return requiresKeystore;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
builder.startObject();
|
builder.startObject();
|
||||||
|
@ -240,6 +266,7 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
builder.field("description", description);
|
builder.field("description", description);
|
||||||
builder.field("classname", classname);
|
builder.field("classname", classname);
|
||||||
builder.field("has_native_controller", hasNativeController);
|
builder.field("has_native_controller", hasNativeController);
|
||||||
|
builder.field("requires_keystore", requiresKeystore);
|
||||||
}
|
}
|
||||||
builder.endObject();
|
builder.endObject();
|
||||||
|
|
||||||
|
@ -272,6 +299,7 @@ public class PluginInfo implements Writeable, ToXContentObject {
|
||||||
.append("Description: ").append(description).append("\n")
|
.append("Description: ").append(description).append("\n")
|
||||||
.append("Version: ").append(version).append("\n")
|
.append("Version: ").append(version).append("\n")
|
||||||
.append("Native Controller: ").append(hasNativeController).append("\n")
|
.append("Native Controller: ").append(hasNativeController).append("\n")
|
||||||
|
.append("Requires Keystore: ").append(requiresKeystore).append("\n")
|
||||||
.append(" * Classname: ").append(classname);
|
.append(" * Classname: ").append(classname);
|
||||||
return information.toString();
|
return information.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ public class PluginsService extends AbstractComponent {
|
||||||
// first we load plugins that are on the classpath. this is for tests and transport clients
|
// first we load plugins that are on the classpath. this is for tests and transport clients
|
||||||
for (Class<? extends Plugin> pluginClass : classpathPlugins) {
|
for (Class<? extends Plugin> pluginClass : classpathPlugins) {
|
||||||
Plugin plugin = loadPlugin(pluginClass, settings, configPath);
|
Plugin plugin = loadPlugin(pluginClass, settings, configPath);
|
||||||
PluginInfo pluginInfo = new PluginInfo(pluginClass.getName(), "classpath plugin", "NA", pluginClass.getName(), false);
|
PluginInfo pluginInfo = new PluginInfo(pluginClass.getName(), "classpath plugin", "NA", pluginClass.getName(), false, false);
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.trace("plugin loaded from classpath [{}]", pluginInfo);
|
logger.trace("plugin loaded from classpath [{}]", pluginInfo);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,13 +50,6 @@ public class BootstrapTests extends ESTestCase {
|
||||||
env = KeyStoreCommandTestCase.setupEnv(true, fileSystems);
|
env = KeyStoreCommandTestCase.setupEnv(true, fileSystems);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testLoadSecureSettingsCreatesKeystore() throws BootstrapException {
|
|
||||||
final Path configPath = env.configFile();
|
|
||||||
assertFalse(Files.exists(configPath.resolve("elasticsearch.keystore")));
|
|
||||||
Bootstrap.loadSecureSettings(env);
|
|
||||||
assertTrue(Files.exists(configPath.resolve("elasticsearch.keystore")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testLoadSecureSettings() throws Exception {
|
public void testLoadSecureSettings() throws Exception {
|
||||||
final Path configPath = env.configFile();
|
final Path configPath = env.configFile();
|
||||||
final SecureString seed;
|
final SecureString seed;
|
||||||
|
|
|
@ -69,8 +69,32 @@ public class KeyStoreWrapperTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testKeystoreSeed() throws Exception {
|
public void testCreate() throws Exception {
|
||||||
KeyStoreWrapper keystore = KeyStoreWrapper.create(new char[0]);
|
KeyStoreWrapper keystore = KeyStoreWrapper.create(new char[0]);
|
||||||
assertTrue(keystore.getSettingNames().contains(KeyStoreWrapper.SEED_SETTING.getKey()));
|
assertTrue(keystore.getSettingNames().contains(KeyStoreWrapper.SEED_SETTING.getKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testUpgradeNoop() throws Exception {
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.create(new char[0]);
|
||||||
|
SecureString seed = keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey());
|
||||||
|
keystore.save(env.configFile());
|
||||||
|
// upgrade does not overwrite seed
|
||||||
|
KeyStoreWrapper.upgrade(keystore, env.configFile());
|
||||||
|
assertEquals(seed.toString(), keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()).toString());
|
||||||
|
keystore = KeyStoreWrapper.load(env.configFile());
|
||||||
|
keystore.decrypt(new char[0]);
|
||||||
|
assertEquals(seed.toString(), keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testUpgradeAddsSeed() throws Exception {
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.create(new char[0]);
|
||||||
|
keystore.remove(KeyStoreWrapper.SEED_SETTING.getKey());
|
||||||
|
keystore.save(env.configFile());
|
||||||
|
KeyStoreWrapper.upgrade(keystore, env.configFile());
|
||||||
|
SecureString seed = keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey());
|
||||||
|
assertNotNull(seed);
|
||||||
|
keystore = KeyStoreWrapper.load(env.configFile());
|
||||||
|
keystore.decrypt(new char[0]);
|
||||||
|
assertEquals(seed.toString(), keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()).toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,13 +143,13 @@ public class NodeInfoStreamingTests extends ESTestCase {
|
||||||
List<PluginInfo> plugins = new ArrayList<>();
|
List<PluginInfo> plugins = new ArrayList<>();
|
||||||
for (int i = 0; i < numPlugins; i++) {
|
for (int i = 0; i < numPlugins; i++) {
|
||||||
plugins.add(new PluginInfo(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10),
|
plugins.add(new PluginInfo(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10),
|
||||||
randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomBoolean()));
|
randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomBoolean(), randomBoolean()));
|
||||||
}
|
}
|
||||||
int numModules = randomIntBetween(0, 5);
|
int numModules = randomIntBetween(0, 5);
|
||||||
List<PluginInfo> modules = new ArrayList<>();
|
List<PluginInfo> modules = new ArrayList<>();
|
||||||
for (int i = 0; i < numModules; i++) {
|
for (int i = 0; i < numModules; i++) {
|
||||||
modules.add(new PluginInfo(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10),
|
modules.add(new PluginInfo(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10),
|
||||||
randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomBoolean()));
|
randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomBoolean(), randomBoolean()));
|
||||||
}
|
}
|
||||||
pluginsAndModules = new PluginsAndModules(plugins, modules);
|
pluginsAndModules = new PluginsAndModules(plugins, modules);
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,11 +209,11 @@ public class PluginInfoTests extends ESTestCase {
|
||||||
|
|
||||||
public void testPluginListSorted() {
|
public void testPluginListSorted() {
|
||||||
List<PluginInfo> plugins = new ArrayList<>();
|
List<PluginInfo> plugins = new ArrayList<>();
|
||||||
plugins.add(new PluginInfo("c", "foo", "dummy", "dummyclass", randomBoolean()));
|
plugins.add(new PluginInfo("c", "foo", "dummy", "dummyclass", randomBoolean(), randomBoolean()));
|
||||||
plugins.add(new PluginInfo("b", "foo", "dummy", "dummyclass", randomBoolean()));
|
plugins.add(new PluginInfo("b", "foo", "dummy", "dummyclass", randomBoolean(), randomBoolean()));
|
||||||
plugins.add(new PluginInfo("e", "foo", "dummy", "dummyclass", randomBoolean()));
|
plugins.add(new PluginInfo("e", "foo", "dummy", "dummyclass", randomBoolean(), randomBoolean()));
|
||||||
plugins.add(new PluginInfo("a", "foo", "dummy", "dummyclass", randomBoolean()));
|
plugins.add(new PluginInfo("a", "foo", "dummy", "dummyclass", randomBoolean(), randomBoolean()));
|
||||||
plugins.add(new PluginInfo("d", "foo", "dummy", "dummyclass", randomBoolean()));
|
plugins.add(new PluginInfo("d", "foo", "dummy", "dummyclass", randomBoolean(), randomBoolean()));
|
||||||
PluginsAndModules pluginsInfo = new PluginsAndModules(plugins, Collections.emptyList());
|
PluginsAndModules pluginsInfo = new PluginsAndModules(plugins, Collections.emptyList());
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.elasticsearch.common.SuppressForbidden;
|
||||||
import org.elasticsearch.common.collect.Tuple;
|
import org.elasticsearch.common.collect.Tuple;
|
||||||
import org.elasticsearch.common.hash.MessageDigests;
|
import org.elasticsearch.common.hash.MessageDigests;
|
||||||
import org.elasticsearch.common.io.FileSystemUtils;
|
import org.elasticsearch.common.io.FileSystemUtils;
|
||||||
|
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
||||||
import org.elasticsearch.env.Environment;
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
@ -572,6 +573,15 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (info.requiresKeystore()) {
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
|
||||||
|
if (keystore == null) {
|
||||||
|
terminal.println("Elasticsearch keystore is required by plugin [" + info.getName() + "], creating...");
|
||||||
|
keystore = KeyStoreWrapper.create(new char[0]);
|
||||||
|
keystore.save(env.configFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
terminal.println("-> Installed " + info.getName());
|
terminal.println("-> Installed " + info.getName());
|
||||||
|
|
||||||
} catch (Exception installProblem) {
|
} catch (Exception installProblem) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.elasticsearch.common.collect.Tuple;
|
||||||
import org.elasticsearch.common.io.FileSystemUtils;
|
import org.elasticsearch.common.io.FileSystemUtils;
|
||||||
import org.elasticsearch.common.io.PathUtils;
|
import org.elasticsearch.common.io.PathUtils;
|
||||||
import org.elasticsearch.common.io.PathUtilsForTesting;
|
import org.elasticsearch.common.io.PathUtilsForTesting;
|
||||||
|
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.env.Environment;
|
import org.elasticsearch.env.Environment;
|
||||||
import org.elasticsearch.test.ESTestCase;
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
@ -61,13 +62,16 @@ import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
import java.nio.file.attribute.PosixFileAttributes;
|
import java.nio.file.attribute.PosixFileAttributes;
|
||||||
import java.nio.file.attribute.PosixFilePermission;
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.nio.file.attribute.UserPrincipal;
|
import java.nio.file.attribute.UserPrincipal;
|
||||||
|
import java.security.KeyStore;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
@ -201,18 +205,20 @@ public class InstallPluginCommandTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** creates a plugin .zip and returns the url for testing */
|
/** creates a plugin .zip and returns the url for testing */
|
||||||
static String createPluginUrl(String name, Path structure) throws IOException {
|
static String createPluginUrl(String name, Path structure, String... additionalProps) throws IOException {
|
||||||
return createPlugin(name, structure, false).toUri().toURL().toString();
|
return createPlugin(name, structure, false, additionalProps).toUri().toURL().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Path createPlugin(String name, Path structure, boolean createSecurityPolicyFile) throws IOException {
|
static Path createPlugin(String name, Path structure, boolean createSecurityPolicyFile, String... additionalProps) throws IOException {
|
||||||
PluginTestUtil.writeProperties(structure,
|
String[] properties = Stream.concat(Stream.of(
|
||||||
"description", "fake desc",
|
"description", "fake desc",
|
||||||
"name", name,
|
"name", name,
|
||||||
"version", "1.0",
|
"version", "1.0",
|
||||||
"elasticsearch.version", Version.CURRENT.toString(),
|
"elasticsearch.version", Version.CURRENT.toString(),
|
||||||
"java.version", System.getProperty("java.specification.version"),
|
"java.version", System.getProperty("java.specification.version"),
|
||||||
"classname", "FakePlugin");
|
"classname", "FakePlugin"
|
||||||
|
), Arrays.stream(additionalProps)).toArray(String[]::new);
|
||||||
|
PluginTestUtil.writeProperties(structure, properties);
|
||||||
if (createSecurityPolicyFile) {
|
if (createSecurityPolicyFile) {
|
||||||
String securityPolicyContent = "grant {\n permission java.lang.RuntimePermission \"setFactory\";\n};\n";
|
String securityPolicyContent = "grant {\n permission java.lang.RuntimePermission \"setFactory\";\n};\n";
|
||||||
Files.write(structure.resolve("plugin-security.policy"), securityPolicyContent.getBytes(StandardCharsets.UTF_8));
|
Files.write(structure.resolve("plugin-security.policy"), securityPolicyContent.getBytes(StandardCharsets.UTF_8));
|
||||||
|
@ -808,4 +814,32 @@ public class InstallPluginCommandTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test checksum (need maven/official below)
|
// TODO: test checksum (need maven/official below)
|
||||||
|
|
||||||
|
public void testKeystoreNotRequired() throws Exception {
|
||||||
|
Tuple<Path, Environment> env = createEnv(fs, temp);
|
||||||
|
Path pluginDir = createPluginDir(temp);
|
||||||
|
String pluginZip = createPluginUrl("fake", pluginDir, "requires.keystore", "false");
|
||||||
|
installPlugin(pluginZip, env.v1());
|
||||||
|
assertFalse(Files.exists(KeyStoreWrapper.keystorePath(env.v2().configFile())));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testKeystoreRequiredAlreadyExists() throws Exception {
|
||||||
|
Tuple<Path, Environment> env = createEnv(fs, temp);
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.create(new char[0]);
|
||||||
|
keystore.save(env.v2().configFile());
|
||||||
|
byte[] expectedBytes = Files.readAllBytes(KeyStoreWrapper.keystorePath(env.v2().configFile()));
|
||||||
|
Path pluginDir = createPluginDir(temp);
|
||||||
|
String pluginZip = createPluginUrl("fake", pluginDir, "requires.keystore", "true");
|
||||||
|
installPlugin(pluginZip, env.v1());
|
||||||
|
byte[] gotBytes = Files.readAllBytes(KeyStoreWrapper.keystorePath(env.v2().configFile()));
|
||||||
|
assertArrayEquals("Keystore was modified", expectedBytes, gotBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testKeystoreRequiredCreated() throws Exception {
|
||||||
|
Tuple<Path, Environment> env = createEnv(fs, temp);
|
||||||
|
Path pluginDir = createPluginDir(temp);
|
||||||
|
String pluginZip = createPluginUrl("fake", pluginDir, "requires.keystore", "true");
|
||||||
|
MockTerminal terminal = installPlugin(pluginZip, env.v1());
|
||||||
|
assertTrue(Files.exists(KeyStoreWrapper.keystorePath(env.v2().configFile())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ public class ListPluginsCommandTests extends ESTestCase {
|
||||||
final String description,
|
final String description,
|
||||||
final String name,
|
final String name,
|
||||||
final String classname) throws IOException {
|
final String classname) throws IOException {
|
||||||
buildFakePlugin(env, description, name, classname, false);
|
buildFakePlugin(env, description, name, classname, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void buildFakePlugin(
|
private static void buildFakePlugin(
|
||||||
|
@ -99,7 +99,8 @@ public class ListPluginsCommandTests extends ESTestCase {
|
||||||
final String description,
|
final String description,
|
||||||
final String name,
|
final String name,
|
||||||
final String classname,
|
final String classname,
|
||||||
final boolean hasNativeController) throws IOException {
|
final boolean hasNativeController,
|
||||||
|
final boolean requiresKeystore) throws IOException {
|
||||||
PluginTestUtil.writeProperties(
|
PluginTestUtil.writeProperties(
|
||||||
env.pluginsFile().resolve(name),
|
env.pluginsFile().resolve(name),
|
||||||
"description", description,
|
"description", description,
|
||||||
|
@ -108,7 +109,8 @@ public class ListPluginsCommandTests extends ESTestCase {
|
||||||
"elasticsearch.version", Version.CURRENT.toString(),
|
"elasticsearch.version", Version.CURRENT.toString(),
|
||||||
"java.version", System.getProperty("java.specification.version"),
|
"java.version", System.getProperty("java.specification.version"),
|
||||||
"classname", classname,
|
"classname", classname,
|
||||||
"has.native.controller", Boolean.toString(hasNativeController));
|
"has.native.controller", Boolean.toString(hasNativeController),
|
||||||
|
"requires.keystore", Boolean.toString(requiresKeystore));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testPluginsDirMissing() throws Exception {
|
public void testPluginsDirMissing() throws Exception {
|
||||||
|
@ -148,25 +150,45 @@ public class ListPluginsCommandTests extends ESTestCase {
|
||||||
"Description: fake desc",
|
"Description: fake desc",
|
||||||
"Version: 1.0",
|
"Version: 1.0",
|
||||||
"Native Controller: false",
|
"Native Controller: false",
|
||||||
|
"Requires Keystore: false",
|
||||||
" * Classname: org.fake"),
|
" * Classname: org.fake"),
|
||||||
terminal.getOutput());
|
terminal.getOutput());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testPluginWithNativeController() throws Exception {
|
public void testPluginWithNativeController() throws Exception {
|
||||||
buildFakePlugin(env, "fake desc 1", "fake_plugin1", "org.fake", true);
|
buildFakePlugin(env, "fake desc 1", "fake_plugin1", "org.fake", true, false);
|
||||||
String[] params = { "-v" };
|
String[] params = { "-v" };
|
||||||
MockTerminal terminal = listPlugins(home, params);
|
MockTerminal terminal = listPlugins(home, params);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
buildMultiline(
|
buildMultiline(
|
||||||
"Plugins directory: " + env.pluginsFile(),
|
"Plugins directory: " + env.pluginsFile(),
|
||||||
"fake_plugin1",
|
"fake_plugin1",
|
||||||
"- Plugin information:",
|
"- Plugin information:",
|
||||||
"Name: fake_plugin1",
|
"Name: fake_plugin1",
|
||||||
"Description: fake desc 1",
|
"Description: fake desc 1",
|
||||||
"Version: 1.0",
|
"Version: 1.0",
|
||||||
"Native Controller: true",
|
"Native Controller: true",
|
||||||
" * Classname: org.fake"),
|
"Requires Keystore: false",
|
||||||
terminal.getOutput());
|
" * Classname: org.fake"),
|
||||||
|
terminal.getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPluginWithRequiresKeystore() throws Exception {
|
||||||
|
buildFakePlugin(env, "fake desc 1", "fake_plugin1", "org.fake", false, 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: false",
|
||||||
|
"Requires Keystore: true",
|
||||||
|
" * Classname: org.fake"),
|
||||||
|
terminal.getOutput());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testPluginWithVerboseMultiplePlugins() throws Exception {
|
public void testPluginWithVerboseMultiplePlugins() throws Exception {
|
||||||
|
@ -183,6 +205,7 @@ public class ListPluginsCommandTests extends ESTestCase {
|
||||||
"Description: fake desc 1",
|
"Description: fake desc 1",
|
||||||
"Version: 1.0",
|
"Version: 1.0",
|
||||||
"Native Controller: false",
|
"Native Controller: false",
|
||||||
|
"Requires Keystore: false",
|
||||||
" * Classname: org.fake",
|
" * Classname: org.fake",
|
||||||
"fake_plugin2",
|
"fake_plugin2",
|
||||||
"- Plugin information:",
|
"- Plugin information:",
|
||||||
|
@ -190,6 +213,7 @@ public class ListPluginsCommandTests extends ESTestCase {
|
||||||
"Description: fake desc 2",
|
"Description: fake desc 2",
|
||||||
"Version: 1.0",
|
"Version: 1.0",
|
||||||
"Native Controller: false",
|
"Native Controller: false",
|
||||||
|
"Requires Keystore: false",
|
||||||
" * Classname: org.fake2"),
|
" * Classname: org.fake2"),
|
||||||
terminal.getOutput());
|
terminal.getOutput());
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ public class PluginSecurityTests extends ESTestCase {
|
||||||
"test cannot run with security manager enabled",
|
"test cannot run with security manager enabled",
|
||||||
System.getSecurityManager() == null);
|
System.getSecurityManager() == null);
|
||||||
final PluginInfo info =
|
final PluginInfo info =
|
||||||
new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", true);
|
new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", true, false);
|
||||||
final MockTerminal terminal = new MockTerminal();
|
final MockTerminal terminal = new MockTerminal();
|
||||||
terminal.addTextInput("y");
|
terminal.addTextInput("y");
|
||||||
terminal.addTextInput("y");
|
terminal.addTextInput("y");
|
||||||
|
@ -63,7 +63,7 @@ public class PluginSecurityTests extends ESTestCase {
|
||||||
"test cannot run with security manager enabled",
|
"test cannot run with security manager enabled",
|
||||||
System.getSecurityManager() == null);
|
System.getSecurityManager() == null);
|
||||||
final PluginInfo info =
|
final PluginInfo info =
|
||||||
new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", true);
|
new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", true, false);
|
||||||
final MockTerminal terminal = new MockTerminal();
|
final MockTerminal terminal = new MockTerminal();
|
||||||
terminal.addTextInput("y");
|
terminal.addTextInput("y");
|
||||||
terminal.addTextInput("n");
|
terminal.addTextInput("n");
|
||||||
|
@ -79,7 +79,7 @@ public class PluginSecurityTests extends ESTestCase {
|
||||||
"test cannot run with security manager enabled",
|
"test cannot run with security manager enabled",
|
||||||
System.getSecurityManager() == null);
|
System.getSecurityManager() == null);
|
||||||
final PluginInfo info =
|
final PluginInfo info =
|
||||||
new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", false);
|
new PluginInfo("fake", "fake", Version.CURRENT.toString(), "Fake", false, false);
|
||||||
final MockTerminal terminal = new MockTerminal();
|
final MockTerminal terminal = new MockTerminal();
|
||||||
terminal.addTextInput("y");
|
terminal.addTextInput("y");
|
||||||
final Path policyFile = this.getDataPath("security/simple-plugin-security.policy");
|
final Path policyFile = this.getDataPath("security/simple-plugin-security.policy");
|
||||||
|
|
Loading…
Reference in New Issue