Merge pull request #22335 from rjernst/keystore
Settings: Add infrastructure for elasticsearch keystore
This commit is contained in:
commit
d235e489e6
|
@ -94,6 +94,8 @@ dependencies {
|
||||||
exclude group: 'org.elasticsearch', module: 'elasticsearch'
|
exclude group: 'org.elasticsearch', module: 'elasticsearch'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
testCompile 'com.google.jimfs:jimfs:1.1'
|
||||||
|
testCompile 'com.google.guava:guava:18.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEclipse) {
|
if (isEclipse) {
|
||||||
|
|
|
@ -30,17 +30,16 @@ import org.apache.lucene.util.IOUtils;
|
||||||
import org.apache.lucene.util.StringHelper;
|
import org.apache.lucene.util.StringHelper;
|
||||||
import org.elasticsearch.ElasticsearchException;
|
import org.elasticsearch.ElasticsearchException;
|
||||||
import org.elasticsearch.Version;
|
import org.elasticsearch.Version;
|
||||||
import org.elasticsearch.cli.ExitCodes;
|
|
||||||
import org.elasticsearch.cli.Terminal;
|
import org.elasticsearch.cli.Terminal;
|
||||||
import org.elasticsearch.cli.UserException;
|
import org.elasticsearch.cli.UserException;
|
||||||
import org.elasticsearch.common.PidFile;
|
import org.elasticsearch.common.PidFile;
|
||||||
import org.elasticsearch.common.SuppressForbidden;
|
import org.elasticsearch.common.SuppressForbidden;
|
||||||
import org.elasticsearch.common.inject.CreationException;
|
import org.elasticsearch.common.inject.CreationException;
|
||||||
import org.elasticsearch.common.logging.DeprecationLogger;
|
|
||||||
import org.elasticsearch.common.logging.ESLoggerFactory;
|
import org.elasticsearch.common.logging.ESLoggerFactory;
|
||||||
import org.elasticsearch.common.logging.LogConfigurator;
|
import org.elasticsearch.common.logging.LogConfigurator;
|
||||||
import org.elasticsearch.common.logging.Loggers;
|
import org.elasticsearch.common.logging.Loggers;
|
||||||
import org.elasticsearch.common.network.IfConfig;
|
import org.elasticsearch.common.network.IfConfig;
|
||||||
|
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.transport.BoundTransportAddress;
|
import org.elasticsearch.common.transport.BoundTransportAddress;
|
||||||
import org.elasticsearch.env.Environment;
|
import org.elasticsearch.env.Environment;
|
||||||
|
@ -228,13 +227,36 @@ final class Bootstrap {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Environment initialEnvironment(boolean foreground, Path pidFile, Settings initialSettings) {
|
private static KeyStoreWrapper loadKeyStore(Environment initialEnv) throws BootstrapException {
|
||||||
|
final KeyStoreWrapper keystore;
|
||||||
|
try {
|
||||||
|
keystore = KeyStoreWrapper.load(initialEnv.configFile());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BootstrapException(e);
|
||||||
|
}
|
||||||
|
if (keystore == null) {
|
||||||
|
return null; // no keystore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
keystore.decrypt(new char[0] /* TODO: read password from stdin */);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BootstrapException(e);
|
||||||
|
}
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Environment createEnvironment(boolean foreground, Path pidFile,
|
||||||
|
KeyStoreWrapper keystore, Settings initialSettings) {
|
||||||
Terminal terminal = foreground ? Terminal.DEFAULT : null;
|
Terminal terminal = foreground ? Terminal.DEFAULT : null;
|
||||||
Settings.Builder builder = Settings.builder();
|
Settings.Builder builder = Settings.builder();
|
||||||
if (pidFile != null) {
|
if (pidFile != null) {
|
||||||
builder.put(Environment.PIDFILE_SETTING.getKey(), pidFile);
|
builder.put(Environment.PIDFILE_SETTING.getKey(), pidFile);
|
||||||
}
|
}
|
||||||
builder.put(initialSettings);
|
builder.put(initialSettings);
|
||||||
|
if (keystore != null) {
|
||||||
|
builder.setKeyStore(keystore);
|
||||||
|
}
|
||||||
return InternalSettingsPreparer.prepareEnvironment(builder.build(), terminal, Collections.emptyMap());
|
return InternalSettingsPreparer.prepareEnvironment(builder.build(), terminal, Collections.emptyMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +287,7 @@ final class Bootstrap {
|
||||||
final boolean foreground,
|
final boolean foreground,
|
||||||
final Path pidFile,
|
final Path pidFile,
|
||||||
final boolean quiet,
|
final boolean quiet,
|
||||||
final Settings initialSettings) throws BootstrapException, NodeValidationException, UserException {
|
final Environment initialEnv) throws BootstrapException, NodeValidationException, UserException {
|
||||||
// Set the system property before anything has a chance to trigger its use
|
// Set the system property before anything has a chance to trigger its use
|
||||||
initLoggerPrefix();
|
initLoggerPrefix();
|
||||||
|
|
||||||
|
@ -275,7 +297,8 @@ final class Bootstrap {
|
||||||
|
|
||||||
INSTANCE = new Bootstrap();
|
INSTANCE = new Bootstrap();
|
||||||
|
|
||||||
Environment environment = initialEnvironment(foreground, pidFile, initialSettings);
|
final KeyStoreWrapper keystore = loadKeyStore(initialEnv);
|
||||||
|
Environment environment = createEnvironment(foreground, pidFile, keystore, initialEnv.settings());
|
||||||
try {
|
try {
|
||||||
LogConfigurator.configure(environment);
|
LogConfigurator.configure(environment);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -313,6 +336,13 @@ final class Bootstrap {
|
||||||
|
|
||||||
INSTANCE.setup(true, environment);
|
INSTANCE.setup(true, environment);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// any secure settings must be read during node construction
|
||||||
|
IOUtils.close(keystore);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BootstrapException(e);
|
||||||
|
}
|
||||||
|
|
||||||
INSTANCE.start();
|
INSTANCE.start();
|
||||||
|
|
||||||
if (closeStandardStreams) {
|
if (closeStandardStreams) {
|
||||||
|
|
|
@ -26,7 +26,7 @@ import java.nio.file.Path;
|
||||||
* during bootstrap should explicitly declare the checked exceptions that they can throw, rather
|
* during bootstrap should explicitly declare the checked exceptions that they can throw, rather
|
||||||
* than declaring the top-level checked exception {@link Exception}. This exception exists to wrap
|
* than declaring the top-level checked exception {@link Exception}. This exception exists to wrap
|
||||||
* these checked exceptions so that
|
* these checked exceptions so that
|
||||||
* {@link Bootstrap#init(boolean, Path, boolean, org.elasticsearch.common.settings.Settings)}
|
* {@link Bootstrap#init(boolean, Path, boolean, org.elasticsearch.env.Environment)}
|
||||||
* does not have to declare all of these checked exceptions.
|
* does not have to declare all of these checked exceptions.
|
||||||
*/
|
*/
|
||||||
class BootstrapException extends Exception {
|
class BootstrapException extends Exception {
|
||||||
|
|
|
@ -111,16 +111,16 @@ class Elasticsearch extends EnvironmentAwareCommand {
|
||||||
final boolean quiet = options.has(quietOption);
|
final boolean quiet = options.has(quietOption);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
init(daemonize, pidFile, quiet, env.settings());
|
init(daemonize, pidFile, quiet, env);
|
||||||
} catch (NodeValidationException e) {
|
} catch (NodeValidationException e) {
|
||||||
throw new UserException(ExitCodes.CONFIG, e.getMessage());
|
throw new UserException(ExitCodes.CONFIG, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Settings initialSettings)
|
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Environment initialEnv)
|
||||||
throws NodeValidationException, UserException {
|
throws NodeValidationException, UserException {
|
||||||
try {
|
try {
|
||||||
Bootstrap.init(!daemonize, pidFile, quiet, initialSettings);
|
Bootstrap.init(!daemonize, pidFile, quiet, initialEnv);
|
||||||
} catch (BootstrapException | RuntimeException e) {
|
} catch (BootstrapException | RuntimeException e) {
|
||||||
// format exceptions to the console in a special way
|
// format exceptions to the console in a special way
|
||||||
// to avoid 2MB stacktraces from guice, etc.
|
// to avoid 2MB stacktraces from guice, etc.
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Terminal wraps access to reading input and writing output for a cli.
|
* A Terminal wraps access to reading input and writing output for a cli.
|
||||||
|
@ -92,6 +93,26 @@ public abstract class Terminal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt for a yes or no answer from the user. This method will loop until 'y' or 'n'
|
||||||
|
* (or the default empty value) is entered.
|
||||||
|
*/
|
||||||
|
public final boolean promptYesNo(String prompt, boolean defaultYes) {
|
||||||
|
String answerPrompt = defaultYes ? " [Y/n]" : " [y/N]";
|
||||||
|
while (true) {
|
||||||
|
String answer = readText(prompt + answerPrompt).toLowerCase(Locale.ROOT);
|
||||||
|
if (answer.isEmpty()) {
|
||||||
|
return defaultYes;
|
||||||
|
}
|
||||||
|
boolean answerYes = answer.equals("y");
|
||||||
|
if (answerYes == false && answer.equals("n") == false) {
|
||||||
|
println("Did not understand answer '" + answer + "'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return answerYes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class ConsoleTerminal extends Terminal {
|
private static class ConsoleTerminal extends Terminal {
|
||||||
|
|
||||||
private static final Console CONSOLE = System.console();
|
private static final Console CONSOLE = System.console();
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import joptsimple.OptionSet;
|
||||||
|
import joptsimple.OptionSpec;
|
||||||
|
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subcommand for the keystore cli which adds a string setting.
|
||||||
|
*/
|
||||||
|
class AddStringKeyStoreCommand extends EnvironmentAwareCommand {
|
||||||
|
|
||||||
|
private final OptionSpec<Void> stdinOption;
|
||||||
|
private final OptionSpec<Void> forceOption;
|
||||||
|
private final OptionSpec<String> arguments;
|
||||||
|
|
||||||
|
AddStringKeyStoreCommand() {
|
||||||
|
super("Add a string setting to the keystore");
|
||||||
|
this.stdinOption = parser.acceptsAll(Arrays.asList("x", "stdin"), "Read setting value from stdin");
|
||||||
|
this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"), "Overwrite existing setting without prompting");
|
||||||
|
this.arguments = parser.nonOptions("setting name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkg private so tests can manipulate
|
||||||
|
InputStream getStdin() {
|
||||||
|
return System.in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
|
||||||
|
if (keystore == null) {
|
||||||
|
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
|
||||||
|
}
|
||||||
|
|
||||||
|
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
|
||||||
|
|
||||||
|
String setting = arguments.value(options);
|
||||||
|
if (keystore.getSettings().contains(setting) && options.has(forceOption) == false) {
|
||||||
|
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
|
||||||
|
terminal.println("Exiting without modifying keystore.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final char[] value;
|
||||||
|
if (options.has(stdinOption)) {
|
||||||
|
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
|
||||||
|
value = stdinReader.readLine().toCharArray();
|
||||||
|
} else {
|
||||||
|
value = terminal.readSecret("Enter value for " + setting + ": ");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
keystore.setStringSetting(setting, value);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new UserException(ExitCodes.DATA_ERROR, "String value must contain only ASCII");
|
||||||
|
}
|
||||||
|
keystore.save(env.configFile());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import joptsimple.OptionSet;
|
||||||
|
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subcommand for the keystore cli to create a new keystore.
|
||||||
|
*/
|
||||||
|
class CreateKeyStoreCommand extends EnvironmentAwareCommand {
|
||||||
|
|
||||||
|
CreateKeyStoreCommand() {
|
||||||
|
super("Creates a new elasticsearch keystore");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||||
|
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
|
||||||
|
if (Files.exists(keystoreFile)) {
|
||||||
|
if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
|
||||||
|
terminal.println("Exiting without creating keystore.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
char[] password = new char[0];// terminal.readSecret("Enter passphrase (empty for no passphrase): ");
|
||||||
|
/* TODO: uncomment when entering passwords on startup is supported
|
||||||
|
char[] passwordRepeat = terminal.readSecret("Enter same passphrase again: ");
|
||||||
|
if (Arrays.equals(password, passwordRepeat) == false) {
|
||||||
|
throw new UserException(ExitCodes.DATA_ERROR, "Passphrases are not equal, exiting.");
|
||||||
|
}*/
|
||||||
|
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.create(password);
|
||||||
|
keystore.save(env.configFile());
|
||||||
|
terminal.println("Created elasticsearch keystore in " + env.configFile());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import org.elasticsearch.cli.MultiCommand;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cli tool for managing secrets in the elasticsearch keystore.
|
||||||
|
*/
|
||||||
|
public class KeyStoreCli extends MultiCommand {
|
||||||
|
|
||||||
|
private KeyStoreCli() {
|
||||||
|
super("A tool for managing settings stored in the elasticsearch keystore");
|
||||||
|
subcommands.put("create", new CreateKeyStoreCommand());
|
||||||
|
subcommands.put("list", new ListKeyStoreCommand());
|
||||||
|
subcommands.put("add", new AddStringKeyStoreCommand());
|
||||||
|
subcommands.put("remove", new RemoveSettingKeyStoreCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
exit(new KeyStoreCli().main(args, Terminal.DEFAULT));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,278 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.security.auth.DestroyFailedException;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import java.nio.charset.CharsetEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.lucene.codecs.CodecUtil;
|
||||||
|
import org.apache.lucene.store.BufferedChecksumIndexInput;
|
||||||
|
import org.apache.lucene.store.ChecksumIndexInput;
|
||||||
|
import org.apache.lucene.store.IOContext;
|
||||||
|
import org.apache.lucene.store.IndexInput;
|
||||||
|
import org.apache.lucene.store.IndexOutput;
|
||||||
|
import org.apache.lucene.store.SimpleFSDirectory;
|
||||||
|
import org.apache.lucene.util.SetOnce;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around a Java KeyStore which provides supplements the keystore with extra metadata.
|
||||||
|
*
|
||||||
|
* Loading a keystore has 2 phases. First, call {@link #load(Path)}. Then call
|
||||||
|
* {@link #decrypt(char[])} with the keystore password, or an empty char array if
|
||||||
|
* {@link #hasPassword()} is {@code false}. Loading and decrypting should happen
|
||||||
|
* in a single thread. Once decrypted, keys may be read with the wrapper in
|
||||||
|
* multiple threads.
|
||||||
|
*/
|
||||||
|
public class KeyStoreWrapper implements Closeable {
|
||||||
|
|
||||||
|
/** The name of the keystore file to read and write. */
|
||||||
|
private static final String KEYSTORE_FILENAME = "elasticsearch.keystore";
|
||||||
|
|
||||||
|
/** The version of the metadata written before the keystore data. */
|
||||||
|
private static final int FORMAT_VERSION = 1;
|
||||||
|
|
||||||
|
/** The keystore type for a newly created keystore. */
|
||||||
|
private static final String NEW_KEYSTORE_TYPE = "PKCS12";
|
||||||
|
|
||||||
|
/** The algorithm used to store password for a newly created keystore. */
|
||||||
|
private static final String NEW_KEYSTORE_SECRET_KEY_ALGO = "PBE";//"PBEWithHmacSHA256AndAES_128";
|
||||||
|
|
||||||
|
/** An encoder to check whether string values are ascii. */
|
||||||
|
private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
|
||||||
|
|
||||||
|
/** True iff the keystore has a password needed to read. */
|
||||||
|
private final boolean hasPassword;
|
||||||
|
|
||||||
|
/** The type of the keystore, as passed to {@link java.security.KeyStore#getInstance(String)} */
|
||||||
|
private final String type;
|
||||||
|
|
||||||
|
/** A factory necessary for constructing instances of secrets in a {@link KeyStore}. */
|
||||||
|
private final SecretKeyFactory secretFactory;
|
||||||
|
|
||||||
|
/** The raw bytes of the encrypted keystore. */
|
||||||
|
private final byte[] keystoreBytes;
|
||||||
|
|
||||||
|
/** The loaded keystore. See {@link #decrypt(char[])}. */
|
||||||
|
private final SetOnce<KeyStore> keystore = new SetOnce<>();
|
||||||
|
|
||||||
|
/** The password for the keystore. See {@link #decrypt(char[])}. */
|
||||||
|
private final SetOnce<KeyStore.PasswordProtection> keystorePassword = new SetOnce<>();
|
||||||
|
|
||||||
|
/** The setting names contained in the loaded keystore. */
|
||||||
|
private final Set<String> settingNames = new HashSet<>();
|
||||||
|
|
||||||
|
private KeyStoreWrapper(boolean hasPassword, String type, String secretKeyAlgo, byte[] keystoreBytes) {
|
||||||
|
this.hasPassword = hasPassword;
|
||||||
|
this.type = type;
|
||||||
|
try {
|
||||||
|
secretFactory = SecretKeyFactory.getInstance(secretKeyAlgo);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
this.keystoreBytes = keystoreBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a path representing the ES keystore in the given config dir. */
|
||||||
|
static Path keystorePath(Path configDir) {
|
||||||
|
return configDir.resolve(KEYSTORE_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constructs a new keystore with the given password. */
|
||||||
|
static KeyStoreWrapper create(char[] password) throws Exception {
|
||||||
|
KeyStoreWrapper wrapper = new KeyStoreWrapper(password.length != 0, NEW_KEYSTORE_TYPE, NEW_KEYSTORE_SECRET_KEY_ALGO, null);
|
||||||
|
KeyStore keyStore = KeyStore.getInstance(NEW_KEYSTORE_TYPE);
|
||||||
|
keyStore.load(null, null);
|
||||||
|
wrapper.keystore.set(keyStore);
|
||||||
|
wrapper.keystorePassword.set(new KeyStore.PasswordProtection(password));
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads information about the Elasticsearch keystore from the provided config directory.
|
||||||
|
*
|
||||||
|
* {@link #decrypt(char[])} must be called before reading or writing any entries.
|
||||||
|
* Returns {@code null} if no keystore exists.
|
||||||
|
*/
|
||||||
|
public static KeyStoreWrapper load(Path configDir) throws IOException {
|
||||||
|
Path keystoreFile = keystorePath(configDir);
|
||||||
|
if (Files.exists(keystoreFile) == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
|
||||||
|
try (IndexInput indexInput = directory.openInput(KEYSTORE_FILENAME, IOContext.READONCE)) {
|
||||||
|
ChecksumIndexInput input = new BufferedChecksumIndexInput(indexInput);
|
||||||
|
CodecUtil.checkHeader(input, KEYSTORE_FILENAME, FORMAT_VERSION, FORMAT_VERSION);
|
||||||
|
byte hasPasswordByte = input.readByte();
|
||||||
|
boolean hasPassword = hasPasswordByte == 1;
|
||||||
|
if (hasPassword == false && hasPasswordByte != 0) {
|
||||||
|
throw new IllegalStateException("hasPassword boolean is corrupt: "
|
||||||
|
+ String.format(Locale.ROOT, "%02x", hasPasswordByte));
|
||||||
|
}
|
||||||
|
String type = input.readString();
|
||||||
|
String secretKeyAlgo = input.readString();
|
||||||
|
byte[] keystoreBytes = new byte[input.readInt()];
|
||||||
|
input.readBytes(keystoreBytes, 0, keystoreBytes.length);
|
||||||
|
CodecUtil.checkFooter(input);
|
||||||
|
return new KeyStoreWrapper(hasPassword, type, secretKeyAlgo, keystoreBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true iff {@link #decrypt(char[])} has been called. */
|
||||||
|
public boolean isLoaded() {
|
||||||
|
return keystore.get() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return true iff calling {@link #decrypt(char[])} requires a non-empty password. */
|
||||||
|
public boolean hasPassword() {
|
||||||
|
return hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the underlying java keystore.
|
||||||
|
*
|
||||||
|
* This may only be called once. The provided password will be zeroed out.
|
||||||
|
*/
|
||||||
|
public void decrypt(char[] password) throws GeneralSecurityException, IOException {
|
||||||
|
if (keystore.get() != null) {
|
||||||
|
throw new IllegalStateException("Keystore has already been decrypted");
|
||||||
|
}
|
||||||
|
keystore.set(KeyStore.getInstance(type));
|
||||||
|
try (InputStream in = new ByteArrayInputStream(keystoreBytes)) {
|
||||||
|
keystore.get().load(in, password);
|
||||||
|
} finally {
|
||||||
|
Arrays.fill(keystoreBytes, (byte)0);
|
||||||
|
}
|
||||||
|
|
||||||
|
keystorePassword.set(new KeyStore.PasswordProtection(password));
|
||||||
|
Arrays.fill(password, '\0');
|
||||||
|
|
||||||
|
// convert keystore aliases enum into a set for easy lookup
|
||||||
|
Enumeration<String> aliases = keystore.get().aliases();
|
||||||
|
while (aliases.hasMoreElements()) {
|
||||||
|
settingNames.add(aliases.nextElement());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write the keystore to the given config directory. */
|
||||||
|
void save(Path configDir) throws Exception {
|
||||||
|
char[] password = this.keystorePassword.get().getPassword();
|
||||||
|
|
||||||
|
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
|
||||||
|
// write to tmp file first, then overwrite
|
||||||
|
String tmpFile = KEYSTORE_FILENAME + ".tmp";
|
||||||
|
try (IndexOutput output = directory.createOutput(tmpFile, IOContext.DEFAULT)) {
|
||||||
|
CodecUtil.writeHeader(output, KEYSTORE_FILENAME, FORMAT_VERSION);
|
||||||
|
output.writeByte(password.length == 0 ? (byte)0 : (byte)1);
|
||||||
|
output.writeString(type);
|
||||||
|
output.writeString(secretFactory.getAlgorithm());
|
||||||
|
|
||||||
|
ByteArrayOutputStream keystoreBytesStream = new ByteArrayOutputStream();
|
||||||
|
keystore.get().store(keystoreBytesStream, password);
|
||||||
|
byte[] keystoreBytes = keystoreBytesStream.toByteArray();
|
||||||
|
output.writeInt(keystoreBytes.length);
|
||||||
|
output.writeBytes(keystoreBytes, keystoreBytes.length);
|
||||||
|
CodecUtil.writeFooter(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path keystoreFile = keystorePath(configDir);
|
||||||
|
Files.move(configDir.resolve(tmpFile), keystoreFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
PosixFileAttributeView attrs = Files.getFileAttributeView(keystoreFile, PosixFileAttributeView.class);
|
||||||
|
if (attrs != null) {
|
||||||
|
// don't rely on umask: ensure the keystore has minimal permissions
|
||||||
|
attrs.setPermissions(PosixFilePermissions.fromString("rw-------"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the names of all settings in this keystore. */
|
||||||
|
public Set<String> getSettings() {
|
||||||
|
return settingNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make settings accessible only to code that registered the setting
|
||||||
|
/** Retrieve a string setting. The {@link SecureString} should be closed once it is used. */
|
||||||
|
SecureString getStringSetting(String setting) throws GeneralSecurityException {
|
||||||
|
KeyStore.Entry entry = keystore.get().getEntry(setting, keystorePassword.get());
|
||||||
|
if (entry instanceof KeyStore.SecretKeyEntry == false) {
|
||||||
|
throw new IllegalStateException("Secret setting " + setting + " is not a string");
|
||||||
|
}
|
||||||
|
// TODO: only allow getting a setting once?
|
||||||
|
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)entry;
|
||||||
|
PBEKeySpec keySpec = (PBEKeySpec) secretFactory.getKeySpec(secretKeyEntry.getSecretKey(), PBEKeySpec.class);
|
||||||
|
SecureString value = new SecureString(keySpec.getPassword());
|
||||||
|
keySpec.clearPassword();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a string setting.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if the value is not ASCII
|
||||||
|
*/
|
||||||
|
void setStringSetting(String setting, char[] value) throws GeneralSecurityException {
|
||||||
|
if (ASCII_ENCODER.canEncode(CharBuffer.wrap(value)) == false) {
|
||||||
|
throw new IllegalArgumentException("Value must be ascii");
|
||||||
|
}
|
||||||
|
SecretKey secretKey = secretFactory.generateSecret(new PBEKeySpec(value));
|
||||||
|
keystore.get().setEntry(setting, new KeyStore.SecretKeyEntry(secretKey), keystorePassword.get());
|
||||||
|
settingNames.add(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove the given setting from the keystore. */
|
||||||
|
void remove(String setting) throws KeyStoreException {
|
||||||
|
keystore.get().deleteEntry(setting);
|
||||||
|
settingNames.remove(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
try {
|
||||||
|
if (keystorePassword.get() != null) {
|
||||||
|
keystorePassword.get().destroy();
|
||||||
|
}
|
||||||
|
} catch (DestroyFailedException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import joptsimple.OptionSet;
|
||||||
|
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subcommand for the keystore cli to list all settings in the keystore.
|
||||||
|
*/
|
||||||
|
class ListKeyStoreCommand extends EnvironmentAwareCommand {
|
||||||
|
|
||||||
|
ListKeyStoreCommand() {
|
||||||
|
super("List entries in the keystore");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
|
||||||
|
if (keystore == null) {
|
||||||
|
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
|
||||||
|
}
|
||||||
|
|
||||||
|
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
|
||||||
|
|
||||||
|
List<String> sortedEntries = new ArrayList<>(keystore.getSettings());
|
||||||
|
Collections.sort(sortedEntries);
|
||||||
|
for (String entry : sortedEntries) {
|
||||||
|
terminal.println(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import joptsimple.OptionSet;
|
||||||
|
import joptsimple.OptionSpec;
|
||||||
|
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subcommand for the keystore cli to remove a setting.
|
||||||
|
*/
|
||||||
|
class RemoveSettingKeyStoreCommand extends EnvironmentAwareCommand {
|
||||||
|
|
||||||
|
private final OptionSpec<String> arguments;
|
||||||
|
|
||||||
|
RemoveSettingKeyStoreCommand() {
|
||||||
|
super("Remove a setting from the keystore");
|
||||||
|
arguments = parser.nonOptions("setting names");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||||
|
List<String> settings = arguments.values(options);
|
||||||
|
if (settings.isEmpty()) {
|
||||||
|
throw new UserException(ExitCodes.USAGE, "Must supply at least one setting to remove");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
|
||||||
|
if (keystore == null) {
|
||||||
|
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
|
||||||
|
}
|
||||||
|
|
||||||
|
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
|
||||||
|
|
||||||
|
for (String setting : arguments.values(options)) {
|
||||||
|
if (keystore.getSettings().contains(setting) == false) {
|
||||||
|
throw new UserException(ExitCodes.CONFIG, "Setting [" + setting + "] does not exist in the keystore.");
|
||||||
|
}
|
||||||
|
keystore.remove(setting);
|
||||||
|
}
|
||||||
|
keystore.save(env.configFile());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A secure setting.
|
||||||
|
*
|
||||||
|
* This class allows access to settings from the Elasticsearch keystore.
|
||||||
|
*/
|
||||||
|
public abstract class SecureSetting<T> extends Setting<T> {
|
||||||
|
private static final Set<Property> ALLOWED_PROPERTIES = new HashSet<>(
|
||||||
|
Arrays.asList(Property.Deprecated, Property.Shared)
|
||||||
|
);
|
||||||
|
|
||||||
|
private SecureSetting(String key, Setting.Property... properties) {
|
||||||
|
super(key, (String)null, null, properties);
|
||||||
|
assert assertAllowedProperties(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean assertAllowedProperties(Setting.Property... properties) {
|
||||||
|
for (Setting.Property property : properties) {
|
||||||
|
if (ALLOWED_PROPERTIES.contains(property) == false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDefaultRaw(Settings settings) {
|
||||||
|
throw new UnsupportedOperationException("secure settings are not strings");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T getDefault(Settings settings) {
|
||||||
|
throw new UnsupportedOperationException("secure settings are not strings");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRaw(Settings settings) {
|
||||||
|
throw new UnsupportedOperationException("secure settings are not strings");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T get(Settings settings) {
|
||||||
|
checkDeprecation(settings);
|
||||||
|
final KeyStoreWrapper keystore = Objects.requireNonNull(settings.getKeyStore());
|
||||||
|
if (keystore.getSettings().contains(getKey()) == false) {
|
||||||
|
return getFallback(settings);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return getSecret(keystore);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new RuntimeException("failed to read secure setting " + getKey(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the secret setting from the keyStoreReader store. */
|
||||||
|
abstract T getSecret(KeyStoreWrapper keystore) throws GeneralSecurityException;
|
||||||
|
|
||||||
|
/** Returns the value from a fallback setting. Returns null if no fallback exists. */
|
||||||
|
abstract T getFallback(Settings settings);
|
||||||
|
|
||||||
|
// TODO: override toXContent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setting which contains a sensitive string.
|
||||||
|
*
|
||||||
|
* This may be any sensitive string, e.g. a username, a password, an auth token, etc.
|
||||||
|
*/
|
||||||
|
public static SecureSetting<SecureString> stringSetting(String name, Setting<String> fallback, Property... properties) {
|
||||||
|
return new SecureSetting<SecureString>(name, properties) {
|
||||||
|
@Override
|
||||||
|
protected SecureString getSecret(KeyStoreWrapper keystore) throws GeneralSecurityException {
|
||||||
|
return keystore.getStringSetting(getKey());
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
SecureString getFallback(Settings settings) {
|
||||||
|
if (fallback != null) {
|
||||||
|
return new SecureString(fallback.get(settings).toCharArray());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A String implementations which allows clearing the underlying char array.
|
||||||
|
*/
|
||||||
|
public final class SecureString implements CharSequence, AutoCloseable {
|
||||||
|
|
||||||
|
private char[] chars;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new SecureString which controls the passed in char array.
|
||||||
|
*
|
||||||
|
* Note: When this instance is closed, the array will be zeroed out.
|
||||||
|
*/
|
||||||
|
public SecureString(char[] chars) {
|
||||||
|
this.chars = Objects.requireNonNull(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constant time equality to avoid potential timing attacks. */
|
||||||
|
@Override
|
||||||
|
public synchronized boolean equals(Object o) {
|
||||||
|
ensureNotClosed();
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || o instanceof CharSequence == false) return false;
|
||||||
|
CharSequence that = (CharSequence) o;
|
||||||
|
if (chars.length != that.length()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int equals = 0;
|
||||||
|
for (int i = 0; i < chars.length; i++) {
|
||||||
|
equals |= chars[i] ^ that.charAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return equals == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int hashCode() {
|
||||||
|
return Arrays.hashCode(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int length() {
|
||||||
|
ensureNotClosed();
|
||||||
|
return chars.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized char charAt(int index) {
|
||||||
|
ensureNotClosed();
|
||||||
|
return chars[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SecureString subSequence(int start, int end) {
|
||||||
|
throw new UnsupportedOperationException("Cannot get subsequence of SecureString");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to a {@link String}. This should only be used with APIs that do not take {@link CharSequence}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized String toString() {
|
||||||
|
return new String(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the string by clearing the underlying char array.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
Arrays.fill(chars, '\0');
|
||||||
|
chars = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throw an exception if this string has been closed, indicating something is trying to access the data after being closed. */
|
||||||
|
private void ensureNotClosed() {
|
||||||
|
if (chars == null) {
|
||||||
|
throw new IllegalStateException("SecureString has already been closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -274,7 +274,7 @@ public class Setting<T> extends ToXContentToBytes {
|
||||||
* Returns the default value string representation for this setting.
|
* Returns the default value string representation for this setting.
|
||||||
* @param settings a settings object for settings that has a default value depending on another setting if available
|
* @param settings a settings object for settings that has a default value depending on another setting if available
|
||||||
*/
|
*/
|
||||||
public final String getDefaultRaw(Settings settings) {
|
public String getDefaultRaw(Settings settings) {
|
||||||
return defaultValue.apply(settings);
|
return defaultValue.apply(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ public class Setting<T> extends ToXContentToBytes {
|
||||||
* Returns the default value for this setting.
|
* Returns the default value for this setting.
|
||||||
* @param settings a settings object for settings that has a default value depending on another setting if available
|
* @param settings a settings object for settings that has a default value depending on another setting if available
|
||||||
*/
|
*/
|
||||||
public final T getDefault(Settings settings) {
|
public T getDefault(Settings settings) {
|
||||||
return parser.apply(getDefaultRaw(settings));
|
return parser.apply(getDefaultRaw(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +290,7 @@ public class Setting<T> extends ToXContentToBytes {
|
||||||
* Returns <code>true</code> iff this setting is present in the given settings object. Otherwise <code>false</code>
|
* Returns <code>true</code> iff this setting is present in the given settings object. Otherwise <code>false</code>
|
||||||
*/
|
*/
|
||||||
public boolean exists(Settings settings) {
|
public boolean exists(Settings settings) {
|
||||||
return settings.get(getKey()) != null;
|
return settings.contains(getKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -330,6 +330,12 @@ public class Setting<T> extends ToXContentToBytes {
|
||||||
* instead. This is useful if the value can't be parsed due to an invalid value to access the actual value.
|
* instead. This is useful if the value can't be parsed due to an invalid value to access the actual value.
|
||||||
*/
|
*/
|
||||||
public String getRaw(Settings settings) {
|
public String getRaw(Settings settings) {
|
||||||
|
checkDeprecation(settings);
|
||||||
|
return settings.get(getKey(), defaultValue.apply(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logs a deprecation warning if the setting is deprecated and used. */
|
||||||
|
protected void checkDeprecation(Settings settings) {
|
||||||
// They're using the setting, so we need to tell them to stop
|
// They're using the setting, so we need to tell them to stop
|
||||||
if (this.isDeprecated() && this.exists(settings)) {
|
if (this.isDeprecated() && this.exists(settings)) {
|
||||||
// It would be convenient to show its replacement key, but replacement is often not so simple
|
// It would be convenient to show its replacement key, but replacement is often not so simple
|
||||||
|
@ -337,7 +343,6 @@ public class Setting<T> extends ToXContentToBytes {
|
||||||
deprecationLogger.deprecated("[{}] setting was deprecated in Elasticsearch and it will be removed in a future release! " +
|
deprecationLogger.deprecated("[{}] setting was deprecated in Elasticsearch and it will be removed in a future release! " +
|
||||||
"See the breaking changes lists in the documentation for details", getKey());
|
"See the breaking changes lists in the documentation for details", getKey());
|
||||||
}
|
}
|
||||||
return settings.get(getKey(), defaultValue.apply(settings));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
package org.elasticsearch.common.settings;
|
package org.elasticsearch.common.settings;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.SetOnce;
|
||||||
import org.elasticsearch.Version;
|
import org.elasticsearch.Version;
|
||||||
import org.elasticsearch.common.Booleans;
|
import org.elasticsearch.common.Booleans;
|
||||||
import org.elasticsearch.common.Strings;
|
import org.elasticsearch.common.Strings;
|
||||||
|
@ -76,10 +77,29 @@ public final class Settings implements ToXContent {
|
||||||
public static final Settings EMPTY = new Builder().build();
|
public static final Settings EMPTY = new Builder().build();
|
||||||
private static final Pattern ARRAY_PATTERN = Pattern.compile("(.*)\\.\\d+$");
|
private static final Pattern ARRAY_PATTERN = Pattern.compile("(.*)\\.\\d+$");
|
||||||
|
|
||||||
|
/** The raw settings from the full key to raw string value. */
|
||||||
private Map<String, String> settings;
|
private Map<String, String> settings;
|
||||||
|
|
||||||
Settings(Map<String, String> settings) {
|
/** The keystore storage associated with these settings. */
|
||||||
this.settings = Collections.unmodifiableMap(settings);
|
private KeyStoreWrapper keystore;
|
||||||
|
|
||||||
|
Settings(Map<String, String> settings, KeyStoreWrapper keystore) {
|
||||||
|
// we use a sorted map for consistent serialization when using getAsMap()
|
||||||
|
this.settings = Collections.unmodifiableSortedMap(new TreeMap<>(settings));
|
||||||
|
this.keystore = keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the keystore that contains secure settings.
|
||||||
|
*/
|
||||||
|
KeyStoreWrapper getKeyStore() {
|
||||||
|
// pkg private so it can only be accessed by local subclasses of SecureSetting
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the setting exists, false otherwise. */
|
||||||
|
public boolean contains(String key) {
|
||||||
|
return settings.containsKey(key) || keystore != null && keystore.getSettings().contains(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -185,16 +205,18 @@ public final class Settings implements ToXContent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A settings that are filtered (and key is removed) with the specified prefix.
|
* A settings that are filtered (and key is removed) with the specified prefix.
|
||||||
|
* Secure settings may not be access through the prefixed settings.
|
||||||
*/
|
*/
|
||||||
public Settings getByPrefix(String prefix) {
|
public Settings getByPrefix(String prefix) {
|
||||||
return new Settings(new FilteredMap(this.settings, (k) -> k.startsWith(prefix), prefix));
|
return new Settings(new FilteredMap(this.settings, (k) -> k.startsWith(prefix), prefix), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new settings object that contains all setting of the current one filtered by the given settings key predicate.
|
* Returns a new settings object that contains all setting of the current one filtered by the given settings key predicate.
|
||||||
|
* Secure settings may not be accessed through a filter.
|
||||||
*/
|
*/
|
||||||
public Settings filter(Predicate<String> predicate) {
|
public Settings filter(Predicate<String> predicate) {
|
||||||
return new Settings(new FilteredMap(this.settings, predicate, null));
|
return new Settings(new FilteredMap(this.settings, predicate, null), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -456,7 +478,7 @@ public final class Settings implements ToXContent {
|
||||||
}
|
}
|
||||||
Map<String, Settings> retVal = new LinkedHashMap<>();
|
Map<String, Settings> retVal = new LinkedHashMap<>();
|
||||||
for (Map.Entry<String, Map<String, String>> entry : map.entrySet()) {
|
for (Map.Entry<String, Map<String, String>> entry : map.entrySet()) {
|
||||||
retVal.put(entry.getKey(), new Settings(Collections.unmodifiableMap(entry.getValue())));
|
retVal.put(entry.getKey(), new Settings(Collections.unmodifiableMap(entry.getValue()), keystore));
|
||||||
}
|
}
|
||||||
return Collections.unmodifiableMap(retVal);
|
return Collections.unmodifiableMap(retVal);
|
||||||
}
|
}
|
||||||
|
@ -591,6 +613,8 @@ public final class Settings implements ToXContent {
|
||||||
// we use a sorted map for consistent serialization when using getAsMap()
|
// we use a sorted map for consistent serialization when using getAsMap()
|
||||||
private final Map<String, String> map = new TreeMap<>();
|
private final Map<String, String> map = new TreeMap<>();
|
||||||
|
|
||||||
|
private SetOnce<KeyStoreWrapper> keystore = new SetOnce<>();
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -613,6 +637,14 @@ public final class Settings implements ToXContent {
|
||||||
return map.get(key);
|
return map.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the secret store for these settings. */
|
||||||
|
public void setKeyStore(KeyStoreWrapper keystore) {
|
||||||
|
if (keystore.isLoaded()) {
|
||||||
|
throw new IllegalStateException("The keystore wrapper must already be loaded");
|
||||||
|
}
|
||||||
|
this.keystore.set(keystore);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts tuples of key value pairs of settings. Simplified version instead of repeating calling
|
* Puts tuples of key value pairs of settings. Simplified version instead of repeating calling
|
||||||
* put for each one.
|
* put for each one.
|
||||||
|
@ -1019,7 +1051,7 @@ public final class Settings implements ToXContent {
|
||||||
* set on this builder.
|
* set on this builder.
|
||||||
*/
|
*/
|
||||||
public Settings build() {
|
public Settings build() {
|
||||||
return new Settings(map);
|
return new Settings(map, keystore.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,14 +19,6 @@
|
||||||
|
|
||||||
package org.elasticsearch.node.internal;
|
package org.elasticsearch.node.internal;
|
||||||
|
|
||||||
import org.elasticsearch.cli.Terminal;
|
|
||||||
import org.elasticsearch.cluster.ClusterName;
|
|
||||||
import org.elasticsearch.common.Strings;
|
|
||||||
import org.elasticsearch.common.collect.Tuple;
|
|
||||||
import org.elasticsearch.common.settings.Settings;
|
|
||||||
import org.elasticsearch.common.settings.SettingsException;
|
|
||||||
import org.elasticsearch.env.Environment;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
@ -39,6 +31,14 @@ import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cluster.ClusterName;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.collect.Tuple;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.settings.SettingsException;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
import static org.elasticsearch.common.Strings.cleanPath;
|
import static org.elasticsearch.common.Strings.cleanPath;
|
||||||
|
|
||||||
public class InternalSettingsPreparer {
|
public class InternalSettingsPreparer {
|
||||||
|
|
|
@ -152,8 +152,8 @@ public class ElasticsearchCliTests extends ESElasticsearchCliTestCase {
|
||||||
ExitCodes.OK,
|
ExitCodes.OK,
|
||||||
true,
|
true,
|
||||||
output -> {},
|
output -> {},
|
||||||
(foreground, pidFile, quiet, esSettings) -> {
|
(foreground, pidFile, quiet, env) -> {
|
||||||
Map<String, String> settings = esSettings.getAsMap();
|
Map<String, String> settings = env.settings().getAsMap();
|
||||||
assertThat(settings, hasEntry("foo", "bar"));
|
assertThat(settings, hasEntry("foo", "bar"));
|
||||||
assertThat(settings, hasEntry("baz", "qux"));
|
assertThat(settings, hasEntry("baz", "qux"));
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,6 +46,33 @@ public class TerminalTests extends ESTestCase {
|
||||||
assertPrinted(terminal, Terminal.Verbosity.NORMAL, "This message contains percent like %20n");
|
assertPrinted(terminal, Terminal.Verbosity.NORMAL, "This message contains percent like %20n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testPromptYesNoDefault() throws Exception {
|
||||||
|
MockTerminal terminal = new MockTerminal();
|
||||||
|
terminal.addTextInput("");
|
||||||
|
assertTrue(terminal.promptYesNo("Answer?", true));
|
||||||
|
terminal.addTextInput("");
|
||||||
|
assertFalse(terminal.promptYesNo("Answer?", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPromptYesNoReprompt() throws Exception {
|
||||||
|
MockTerminal terminal = new MockTerminal();
|
||||||
|
terminal.addTextInput("blah");
|
||||||
|
terminal.addTextInput("y");
|
||||||
|
assertTrue(terminal.promptYesNo("Answer? [Y/n]\nDid not understand answer 'blah'\nAnswer? [Y/n]", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPromptYesNoCase() throws Exception {
|
||||||
|
MockTerminal terminal = new MockTerminal();
|
||||||
|
terminal.addTextInput("Y");
|
||||||
|
assertTrue(terminal.promptYesNo("Answer?", false));
|
||||||
|
terminal.addTextInput("y");
|
||||||
|
assertTrue(terminal.promptYesNo("Answer?", false));
|
||||||
|
terminal.addTextInput("N");
|
||||||
|
assertFalse(terminal.promptYesNo("Answer?", true));
|
||||||
|
terminal.addTextInput("n");
|
||||||
|
assertFalse(terminal.promptYesNo("Answer?", true));
|
||||||
|
}
|
||||||
|
|
||||||
private void assertPrinted(MockTerminal logTerminal, Terminal.Verbosity verbosity, String text) throws Exception {
|
private void assertPrinted(MockTerminal logTerminal, Terminal.Verbosity verbosity, String text) throws Exception {
|
||||||
logTerminal.println(verbosity, text);
|
logTerminal.println(verbosity, text);
|
||||||
String output = logTerminal.getOutput();
|
String output = logTerminal.getOutput();
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.elasticsearch.cli.Command;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
|
public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
|
||||||
|
InputStream input;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Command newCommand() {
|
||||||
|
return new AddStringKeyStoreCommand() {
|
||||||
|
@Override
|
||||||
|
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
InputStream getStdin() {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMissing() throws Exception {
|
||||||
|
UserException e = expectThrows(UserException.class, this::execute);
|
||||||
|
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
|
||||||
|
assertThat(e.getMessage(), containsString("keystore not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOverwritePromptDefault() throws Exception {
|
||||||
|
createKeystore("", "foo", "bar");
|
||||||
|
terminal.addTextInput("");
|
||||||
|
execute("foo");
|
||||||
|
assertSecureString("foo", "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOverwritePromptExplicitNo() throws Exception {
|
||||||
|
createKeystore("", "foo", "bar");
|
||||||
|
terminal.addTextInput("n"); // explicit no
|
||||||
|
execute("foo");
|
||||||
|
assertSecureString("foo", "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOverwritePromptExplicitYes() throws Exception {
|
||||||
|
createKeystore("", "foo", "bar");
|
||||||
|
terminal.addTextInput("y");
|
||||||
|
terminal.addSecretInput("newvalue");
|
||||||
|
execute("foo");
|
||||||
|
assertSecureString("foo", "newvalue");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOverwriteForceShort() throws Exception {
|
||||||
|
createKeystore("", "foo", "bar");
|
||||||
|
terminal.addSecretInput("newvalue");
|
||||||
|
execute("-f", "foo"); // force
|
||||||
|
assertSecureString("foo", "newvalue");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOverwriteForceLong() throws Exception {
|
||||||
|
createKeystore("", "foo", "bar");
|
||||||
|
terminal.addSecretInput("and yet another secret value");
|
||||||
|
execute("--force", "foo"); // force
|
||||||
|
assertSecureString("foo", "and yet another secret value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testForceNonExistent() throws Exception {
|
||||||
|
createKeystore("");
|
||||||
|
terminal.addSecretInput("value");
|
||||||
|
execute("--force", "foo"); // force
|
||||||
|
assertSecureString("foo", "value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPromptForValue() throws Exception {
|
||||||
|
KeyStoreWrapper.create(new char[0]).save(env.configFile());
|
||||||
|
terminal.addSecretInput("secret value");
|
||||||
|
execute("foo");
|
||||||
|
assertSecureString("foo", "secret value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testStdinShort() throws Exception {
|
||||||
|
KeyStoreWrapper.create(new char[0]).save(env.configFile());
|
||||||
|
setInput("secret value 1");
|
||||||
|
execute("-x", "foo");
|
||||||
|
assertSecureString("foo", "secret value 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testStdinLong() throws Exception {
|
||||||
|
KeyStoreWrapper.create(new char[0]).save(env.configFile());
|
||||||
|
setInput("secret value 2");
|
||||||
|
execute("--stdin", "foo");
|
||||||
|
assertSecureString("foo", "secret value 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNonAsciiValue() throws Exception {
|
||||||
|
KeyStoreWrapper.create(new char[0]).save(env.configFile());
|
||||||
|
terminal.addSecretInput("non-äsčîï");
|
||||||
|
UserException e = expectThrows(UserException.class, () -> execute("foo"));
|
||||||
|
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
|
||||||
|
assertEquals("String value must contain only ASCII", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInput(String inputStr) {
|
||||||
|
input = new ByteArrayInputStream(inputStr.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.elasticsearch.cli.Command;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
public class CreateKeyStoreCommandTests extends KeyStoreCommandTestCase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Command newCommand() {
|
||||||
|
return new CreateKeyStoreCommand() {
|
||||||
|
@Override
|
||||||
|
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPosix() throws Exception {
|
||||||
|
execute();
|
||||||
|
Path configDir = env.configFile();
|
||||||
|
assertNotNull(KeyStoreWrapper.load(configDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNotPosix() throws Exception {
|
||||||
|
setupEnv(false);
|
||||||
|
execute();
|
||||||
|
Path configDir = env.configFile();
|
||||||
|
assertNotNull(KeyStoreWrapper.load(configDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOverwrite() throws Exception {
|
||||||
|
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
|
||||||
|
byte[] content = "not a keystore".getBytes(StandardCharsets.UTF_8);
|
||||||
|
Files.write(keystoreFile, content);
|
||||||
|
|
||||||
|
terminal.addTextInput(""); // default is no
|
||||||
|
execute();
|
||||||
|
assertArrayEquals(content, Files.readAllBytes(keystoreFile));
|
||||||
|
|
||||||
|
terminal.addTextInput("n"); // explicit no
|
||||||
|
execute();
|
||||||
|
assertArrayEquals(content, Files.readAllBytes(keystoreFile));
|
||||||
|
|
||||||
|
terminal.addTextInput("y");
|
||||||
|
execute();
|
||||||
|
assertNotNull(KeyStoreWrapper.load(env.configFile()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.common.jimfs.Configuration;
|
||||||
|
import com.google.common.jimfs.Jimfs;
|
||||||
|
import org.apache.lucene.util.IOUtils;
|
||||||
|
import org.apache.lucene.util.LuceneTestCase;
|
||||||
|
import org.elasticsearch.cli.CommandTestCase;
|
||||||
|
import org.elasticsearch.common.io.PathUtilsForTesting;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base test case for manipulating the ES keystore.
|
||||||
|
*/
|
||||||
|
@LuceneTestCase.SuppressFileSystems("*") // we do our own mocking
|
||||||
|
public abstract class KeyStoreCommandTestCase extends CommandTestCase {
|
||||||
|
|
||||||
|
Environment env;
|
||||||
|
|
||||||
|
List<FileSystem> fileSystems = new ArrayList<>();
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void closeMockFileSystems() throws IOException {
|
||||||
|
IOUtils.close(fileSystems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setupEnv() throws IOException {
|
||||||
|
setupEnv(true); // default to posix, but tests may call setupEnv(false) to overwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupEnv(boolean posix) throws IOException {
|
||||||
|
final Configuration configuration;
|
||||||
|
if (posix) {
|
||||||
|
configuration = Configuration.unix().toBuilder().setAttributeViews("basic", "owner", "posix", "unix").build();
|
||||||
|
} else {
|
||||||
|
configuration = Configuration.unix();
|
||||||
|
}
|
||||||
|
FileSystem fs = Jimfs.newFileSystem(configuration);
|
||||||
|
fileSystems.add(fs);
|
||||||
|
PathUtilsForTesting.installMock(fs); // restored by restoreFileSystem in ESTestCase
|
||||||
|
Path home = fs.getPath("/", "test-home");
|
||||||
|
Files.createDirectories(home.resolve("config"));
|
||||||
|
env = new Environment(Settings.builder().put("path.home", home).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyStoreWrapper createKeystore(String password, String... settings) throws Exception {
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.create(password.toCharArray());
|
||||||
|
assertEquals(0, settings.length % 2);
|
||||||
|
for (int i = 0; i < settings.length; i += 2) {
|
||||||
|
keystore.setStringSetting(settings[i], settings[i + 1].toCharArray());
|
||||||
|
}
|
||||||
|
keystore.save(env.configFile());
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyStoreWrapper loadKeystore(String password) throws Exception {
|
||||||
|
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
|
||||||
|
keystore.decrypt(password.toCharArray());
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertSecureString(String setting, String value) throws Exception {
|
||||||
|
assertSecureString(loadKeystore(""), setting, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertSecureString(KeyStoreWrapper keystore, String setting, String value) throws Exception {
|
||||||
|
assertEquals(value, keystore.getStringSetting(setting).toString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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.common.settings;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.elasticsearch.cli.Command;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
|
public class ListKeyStoreCommandTests extends KeyStoreCommandTestCase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Command newCommand() {
|
||||||
|
return new ListKeyStoreCommand() {
|
||||||
|
@Override
|
||||||
|
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMissing() throws Exception {
|
||||||
|
UserException e = expectThrows(UserException.class, this::execute);
|
||||||
|
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
|
||||||
|
assertThat(e.getMessage(), containsString("keystore not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testEmpty() throws Exception {
|
||||||
|
createKeystore("");
|
||||||
|
execute();
|
||||||
|
assertTrue(terminal.getOutput(), terminal.getOutput().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOne() throws Exception {
|
||||||
|
createKeystore("", "foo", "bar");
|
||||||
|
execute();
|
||||||
|
assertEquals("foo\n", terminal.getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMultiple() throws Exception {
|
||||||
|
createKeystore("", "foo", "1", "baz", "2", "bar", "3");
|
||||||
|
execute();
|
||||||
|
assertEquals("bar\nbaz\nfoo\n", terminal.getOutput()); // sorted
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.common.settings;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.elasticsearch.cli.Command;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
|
public class RemoveSettingKeyStoreCommandTests extends KeyStoreCommandTestCase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Command newCommand() {
|
||||||
|
return new RemoveSettingKeyStoreCommand() {
|
||||||
|
@Override
|
||||||
|
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMissing() throws Exception {
|
||||||
|
UserException e = expectThrows(UserException.class, () -> execute("foo"));
|
||||||
|
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
|
||||||
|
assertThat(e.getMessage(), containsString("keystore not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNoSettings() throws Exception {
|
||||||
|
createKeystore("");
|
||||||
|
UserException e = expectThrows(UserException.class, this::execute);
|
||||||
|
assertEquals(ExitCodes.USAGE, e.exitCode);
|
||||||
|
assertThat(e.getMessage(), containsString("Must supply at least one setting"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNonExistentSetting() throws Exception {
|
||||||
|
createKeystore("");
|
||||||
|
UserException e = expectThrows(UserException.class, () -> execute("foo"));
|
||||||
|
assertEquals(ExitCodes.CONFIG, e.exitCode);
|
||||||
|
assertThat(e.getMessage(), containsString("[foo] does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOne() throws Exception {
|
||||||
|
createKeystore("", "foo", "bar");
|
||||||
|
execute("foo");
|
||||||
|
assertFalse(loadKeystore("").getSettings().contains("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMany() throws Exception {
|
||||||
|
createKeystore("", "foo", "1", "bar", "2", "baz", "3");
|
||||||
|
execute("foo", "baz");
|
||||||
|
Set<String> settings = loadKeystore("").getSettings();
|
||||||
|
assertFalse(settings.contains("foo"));
|
||||||
|
assertFalse(settings.contains("baz"));
|
||||||
|
assertTrue(settings.contains("bar"));
|
||||||
|
assertEquals(1, settings.size());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
CDPATH=""
|
||||||
|
SCRIPT="$0"
|
||||||
|
|
||||||
|
# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
|
||||||
|
while [ -h "$SCRIPT" ] ; do
|
||||||
|
ls=`ls -ld "$SCRIPT"`
|
||||||
|
# Drop everything prior to ->
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
SCRIPT="$link"
|
||||||
|
else
|
||||||
|
SCRIPT=`dirname "$SCRIPT"`/"$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# determine elasticsearch home
|
||||||
|
ES_HOME=`dirname "$SCRIPT"`/..
|
||||||
|
|
||||||
|
# make ELASTICSEARCH_HOME absolute
|
||||||
|
ES_HOME=`cd "$ES_HOME"; pwd`
|
||||||
|
|
||||||
|
|
||||||
|
# Sets the default values for elasticsearch variables used in this script
|
||||||
|
if [ -z "$CONF_DIR" ]; then
|
||||||
|
CONF_DIR="${path.conf}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The default env file is defined at building/packaging time.
|
||||||
|
# For a ${project.name} package, the value is "${path.env}".
|
||||||
|
ES_ENV_FILE="${path.env}"
|
||||||
|
|
||||||
|
# If an include is specified with the ES_INCLUDE environment variable, use it
|
||||||
|
if [ -n "$ES_INCLUDE" ]; then
|
||||||
|
ES_ENV_FILE="$ES_INCLUDE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source the environment file
|
||||||
|
if [ -n "$ES_ENV_FILE" ]; then
|
||||||
|
|
||||||
|
# If the ES_ENV_FILE is not found, try to resolve the path
|
||||||
|
# against the ES_HOME directory
|
||||||
|
if [ ! -f "$ES_ENV_FILE" ]; then
|
||||||
|
ES_ENV_FILE="$ELASTIC_HOME/$ES_ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
. "$ES_ENV_FILE"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Unable to source environment file: $ES_ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# don't let JAVA_TOOL_OPTIONS slip in (e.g. crazy agents in ubuntu)
|
||||||
|
# works around https://bugs.launchpad.net/ubuntu/+source/jayatana/+bug/1441487
|
||||||
|
if [ "x$JAVA_TOOL_OPTIONS" != "x" ]; then
|
||||||
|
echo "Warning: Ignoring JAVA_TOOL_OPTIONS=$JAVA_TOOL_OPTIONS"
|
||||||
|
unset JAVA_TOOL_OPTIONS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CONF_FILE setting was removed
|
||||||
|
if [ ! -z "$CONF_FILE" ]; then
|
||||||
|
echo "CONF_FILE setting is no longer supported. elasticsearch.yml must be placed in the config directory and cannot be renamed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x "$JAVA_HOME/bin/java" ]; then
|
||||||
|
JAVA=$JAVA_HOME/bin/java
|
||||||
|
else
|
||||||
|
JAVA=`which java`
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$JAVA" ]; then
|
||||||
|
echo "Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# full hostname passed through cut for portability on systems that do not support hostname -s
|
||||||
|
# export on separate line for shells that do not support combining definition and export
|
||||||
|
HOSTNAME=`hostname | cut -d. -f1`
|
||||||
|
export HOSTNAME
|
||||||
|
|
||||||
|
declare -a args=("$@")
|
||||||
|
path_props=(-Des.path.home="$ES_HOME")
|
||||||
|
|
||||||
|
if [ -e "$CONF_DIR" ]; then
|
||||||
|
path_props=("${path_props[@]}" -Des.path.conf="$CONF_DIR")
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVA" $ES_JAVA_OPTS -Delasticsearch "${path_props[@]}" -cp "$ES_HOME/lib/*" org.elasticsearch.common.settings.KeyStoreCli "${args[@]}"
|
|
@ -0,0 +1,30 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
SETLOCAL enabledelayedexpansion
|
||||||
|
|
||||||
|
IF DEFINED JAVA_HOME (
|
||||||
|
set JAVA=%JAVA_HOME%\bin\java.exe
|
||||||
|
) ELSE (
|
||||||
|
FOR %%I IN (java.exe) DO set JAVA=%%~$PATH:I
|
||||||
|
)
|
||||||
|
IF NOT EXIST "%JAVA%" (
|
||||||
|
ECHO Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME 1>&2
|
||||||
|
EXIT /B 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set SCRIPT_DIR=%~dp0
|
||||||
|
for %%I in ("%SCRIPT_DIR%..") do set ES_HOME=%%~dpfI
|
||||||
|
|
||||||
|
TITLE Elasticsearch Plugin Manager ${project.version}
|
||||||
|
|
||||||
|
SET path_props=-Des.path.home="%ES_HOME%"
|
||||||
|
IF DEFINED CONF_DIR (
|
||||||
|
SET path_props=!path_props! -Des.path.conf="%CONF_DIR%"
|
||||||
|
)
|
||||||
|
|
||||||
|
SET args=%*
|
||||||
|
SET HOSTNAME=%COMPUTERNAME%
|
||||||
|
|
||||||
|
"%JAVA%" %ES_JAVA_OPTS% !path_props! -cp "%ES_HOME%/lib/*;" "org.elasticsearch.common.settings.KeyStoreCli" !args!
|
||||||
|
|
||||||
|
ENDLOCAL
|
|
@ -93,9 +93,9 @@ https://hc.apache.org/httpcomponents-asyncclient-dev/httpasyncclient/apidocs/org
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
KeyStore keyStore = KeyStore.getInstance("jks");
|
KeyStore keystore = KeyStore.getInstance("jks");
|
||||||
try (InputStream is = Files.newInputStream(keyStorePath)) {
|
try (InputStream is = Files.newInputStream(keyStorePath)) {
|
||||||
keyStore.load(is, keyStorePass.toCharArray());
|
keystore.load(is, keyStorePass.toCharArray());
|
||||||
}
|
}
|
||||||
RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200))
|
RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200))
|
||||||
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
|
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
|
||||||
|
|
|
@ -41,8 +41,7 @@ public class EvilElasticsearchCliTests extends ESElasticsearchCliTestCase {
|
||||||
true,
|
true,
|
||||||
output -> {},
|
output -> {},
|
||||||
(foreground, pidFile, quiet, esSettings) -> {
|
(foreground, pidFile, quiet, esSettings) -> {
|
||||||
Map<String, String> settings = esSettings.getAsMap();
|
Map<String, String> settings = esSettings.settings().getAsMap();
|
||||||
settings.keySet().forEach(System.out::println);
|
|
||||||
assertThat(settings.size(), equalTo(2));
|
assertThat(settings.size(), equalTo(2));
|
||||||
assertThat(settings, hasEntry("path.home", value));
|
assertThat(settings, hasEntry("path.home", value));
|
||||||
assertThat(settings, hasKey("path.logs")); // added by env initialization
|
assertThat(settings, hasKey("path.logs")); // added by env initialization
|
||||||
|
@ -55,7 +54,7 @@ public class EvilElasticsearchCliTests extends ESElasticsearchCliTestCase {
|
||||||
true,
|
true,
|
||||||
output -> {},
|
output -> {},
|
||||||
(foreground, pidFile, quiet, esSettings) -> {
|
(foreground, pidFile, quiet, esSettings) -> {
|
||||||
Map<String, String> settings = esSettings.getAsMap();
|
Map<String, String> settings = esSettings.settings().getAsMap();
|
||||||
assertThat(settings.size(), equalTo(2));
|
assertThat(settings.size(), equalTo(2));
|
||||||
assertThat(settings, hasEntry("path.home", commandLineValue));
|
assertThat(settings, hasEntry("path.home", commandLineValue));
|
||||||
assertThat(settings, hasKey("path.logs")); // added by env initialization
|
assertThat(settings, hasKey("path.logs")); // added by env initialization
|
||||||
|
|
|
@ -35,7 +35,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
abstract class ESElasticsearchCliTestCase extends ESTestCase {
|
abstract class ESElasticsearchCliTestCase extends ESTestCase {
|
||||||
|
|
||||||
interface InitConsumer {
|
interface InitConsumer {
|
||||||
void accept(final boolean foreground, final Path pidFile, final boolean quiet, final Settings initialSettings);
|
void accept(final boolean foreground, final Path pidFile, final boolean quiet, final Environment initialEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
void runTest(
|
void runTest(
|
||||||
|
@ -57,9 +57,9 @@ abstract class ESElasticsearchCliTestCase extends ESTestCase {
|
||||||
return new Environment(realSettings);
|
return new Environment(realSettings);
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Settings initialSettings) {
|
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Environment initialEnv) {
|
||||||
init.set(true);
|
init.set(true);
|
||||||
initConsumer.accept(!daemonize, pidFile, quiet, initialSettings);
|
initConsumer.accept(!daemonize, pidFile, quiet, initialEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in New Issue