Skip to content

Commit 4e4a40d

Browse files
committed
feedback
1 parent d4288cc commit 4e4a40d

File tree

9 files changed

+80
-31
lines changed

9 files changed

+80
-31
lines changed

core/src/main/java/org/elasticsearch/cli/Terminal.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.io.InputStreamReader;
2828
import java.io.PrintWriter;
2929
import java.nio.charset.Charset;
30+
import java.util.Locale;
3031

3132
/**
3233
* A Terminal wraps access to reading input and writing output for a cli.
@@ -92,6 +93,26 @@ public final void print(Verbosity verbosity, String msg) {
9293
}
9394
}
9495

96+
/**
97+
* Prompt for a yes or no answer from the user. This method will loop until 'y' or 'n'
98+
* (or the default empty value) is entered.
99+
*/
100+
public final boolean promptYesNo(String prompt, boolean defaultYes) {
101+
String answerPrompt = defaultYes ? " [Y/n]" : " [y/N]";
102+
while (true) {
103+
String answer = readText(prompt + answerPrompt).toLowerCase(Locale.ROOT);
104+
if (answer.isEmpty()) {
105+
return defaultYes;
106+
}
107+
boolean answerYes = answer.equals("y");
108+
if (answerYes == false && answer.equals("n") == false) {
109+
println("Did not understand answer '" + answer + "'");
110+
continue;
111+
}
112+
return answerYes;
113+
}
114+
}
115+
95116
private static class ConsoleTerminal extends Terminal {
96117

97118
private static final Console CONSOLE = System.console();

core/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.io.InputStreamReader;
2525
import java.nio.charset.StandardCharsets;
2626
import java.util.Arrays;
27+
import java.util.Locale;
2728

2829
import joptsimple.OptionSet;
2930
import joptsimple.OptionSpec;
@@ -65,8 +66,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
6566

6667
String setting = arguments.value(options);
6768
if (keystore.getSettings().contains(setting) && options.has(forceOption) == false) {
68-
String answer = terminal.readText("Setting " + setting + " already exists. Overwrite? [y/N]");
69-
if (answer.equals("y") == false) {
69+
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
7070
terminal.println("Exiting without modifying keystore.");
7171
return;
7272
}

core/src/main/java/org/elasticsearch/common/settings/CreateKeyStoreCommand.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ class CreateKeyStoreCommand extends EnvironmentAwareCommand {
4040
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
4141
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
4242
if (Files.exists(keystoreFile)) {
43-
String answer = terminal.readText("An elasticsearch keystore already exists. Overwrite? [y/N] ");
44-
if (answer.equals("y") == false) {
43+
if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
4544
terminal.println("Exiting without creating keystore.");
4645
return;
4746
}

core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import java.security.KeyStoreException;
3838
import java.security.NoSuchAlgorithmException;
3939
import java.util.Arrays;
40-
import java.util.Collections;
4140
import java.util.Enumeration;
4241
import java.util.HashSet;
4342
import java.util.Set;
@@ -59,22 +58,18 @@ public class KeyStoreWrapper implements Closeable {
5958
/** The keystore type for a newly created keystore. */
6059
private static final String NEW_KEYSTORE_TYPE = "PKCS12";
6160

62-
/** A factory necessary for constructing instances of secrets in a {@link KeyStore}. */
63-
private static final SecretKeyFactory passwordFactory;
64-
static {
65-
try {
66-
passwordFactory = SecretKeyFactory.getInstance("PBE");
67-
} catch (NoSuchAlgorithmException e) {
68-
throw new RuntimeException(e);
69-
}
70-
}
61+
/** The algorithm used to store password for a newly created keystore. */
62+
private static final String NEW_KEYSTORE_SECRET_KEY_ALGO = "PBEWithHmacSHA256AndAES_128";
7163

7264
/** True iff the keystore has a password needed to read. */
7365
private final boolean hasPassword;
7466

7567
/** The type of the keystore, as passed to {@link java.security.KeyStore#getInstance(String)} */
7668
private final String type;
7769

70+
/** A factory necessary for constructing instances of secrets in a {@link KeyStore}. */
71+
private final SecretKeyFactory secretFactory;
72+
7873
/** A stream of the actual keystore data. */
7974
private final InputStream input;
8075

@@ -87,9 +82,14 @@ public class KeyStoreWrapper implements Closeable {
8782
/** The setting names contained in the loaded keystore. */
8883
private final Set<String> settingNames = new HashSet<>();
8984

90-
private KeyStoreWrapper(boolean hasPassword, String type, InputStream input) {
85+
private KeyStoreWrapper(boolean hasPassword, String type, String secretKeyAlgo, InputStream input) {
9186
this.hasPassword = hasPassword;
9287
this.type = type;
88+
try {
89+
secretFactory = SecretKeyFactory.getInstance(secretKeyAlgo);
90+
} catch (NoSuchAlgorithmException e) {
91+
throw new RuntimeException(e);
92+
}
9393
this.input = input;
9494
}
9595

@@ -100,7 +100,7 @@ static Path keystorePath(Path configDir) {
100100

101101
/** Constructs a new keystore with the given password. */
102102
static KeyStoreWrapper create(char[] password) throws Exception {
103-
KeyStoreWrapper wrapper = new KeyStoreWrapper(password.length != 0, NEW_KEYSTORE_TYPE, null);
103+
KeyStoreWrapper wrapper = new KeyStoreWrapper(password.length != 0, NEW_KEYSTORE_TYPE, NEW_KEYSTORE_SECRET_KEY_ALGO, null);
104104
KeyStore keyStore = KeyStore.getInstance(NEW_KEYSTORE_TYPE);
105105
keyStore.load(null, null);
106106
wrapper.keystore.set(keyStore);
@@ -126,7 +126,8 @@ public static KeyStoreWrapper loadMetadata(Path configDir) throws IOException {
126126
}
127127
boolean hasPassword = inputStream.readBoolean();
128128
String type = inputStream.readUTF();
129-
return new KeyStoreWrapper(hasPassword, type, inputStream);
129+
String secretKeyAlgo = inputStream.readUTF();
130+
return new KeyStoreWrapper(hasPassword, type, secretKeyAlgo, inputStream);
130131
}
131132

132133
/** Returns true iff {@link #loadKeystore(char[])} has been called. */
@@ -164,6 +165,7 @@ void save(Path configDir) throws Exception {
164165
char[] password = this.keystorePassword.get().getPassword();
165166
outputStream.writeBoolean(password.length != 0);
166167
outputStream.writeUTF(type);
168+
outputStream.writeUTF(secretFactory.getAlgorithm());
167169
keystore.get().store(outputStream, password);
168170
}
169171
PosixFileAttributeView attrs = Files.getFileAttributeView(keystoreFile, PosixFileAttributeView.class);
@@ -186,15 +188,15 @@ SecureString getStringSetting(String setting) throws GeneralSecurityException {
186188
}
187189
// TODO: only allow getting a setting once?
188190
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)entry;
189-
PBEKeySpec keySpec = (PBEKeySpec)passwordFactory.getKeySpec(secretKeyEntry.getSecretKey(), PBEKeySpec.class);
191+
PBEKeySpec keySpec = (PBEKeySpec) secretFactory.getKeySpec(secretKeyEntry.getSecretKey(), PBEKeySpec.class);
190192
SecureString value = new SecureString(keySpec.getPassword());
191193
keySpec.clearPassword();
192194
return value;
193195
}
194196

195197
/** Set a string setting. */
196198
void setStringSetting(String setting, char[] value) throws GeneralSecurityException {
197-
SecretKey secretKey = passwordFactory.generateSecret(new PBEKeySpec(value));
199+
SecretKey secretKey = secretFactory.generateSecret(new PBEKeySpec(value));
198200
keystore.get().setEntry(setting, new KeyStore.SecretKeyEntry(secretKey), keystorePassword.get());
199201
settingNames.add(setting);
200202
}

core/src/main/java/org/elasticsearch/common/settings/ListKeyStoreCommand.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,5 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
5454
for (String entry : sortedEntries) {
5555
terminal.println(entry);
5656
}
57-
58-
// TODO:
59-
// 2. delete command
60-
// 3. tests for delete
61-
// 4. shell script
6257
}
6358
}

core/src/main/java/org/elasticsearch/common/settings/SecureString.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public SecureString subSequence(int start, int end) {
8080
}
8181

8282
/**
83-
* Convert to a {@link String}. This only be used with APIs that do not take {@link CharSequence}.
83+
* Convert to a {@link String}. This should only be used with APIs that do not take {@link CharSequence}.
8484
*/
8585
@Override
8686
public String toString() {

core/src/main/java/org/elasticsearch/common/settings/Settings.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.common.settings;
2121

22+
import org.apache.lucene.util.SetOnce;
2223
import org.elasticsearch.Version;
2324
import org.elasticsearch.common.Booleans;
2425
import org.elasticsearch.common.Strings;
@@ -612,7 +613,7 @@ public static class Builder {
612613
// we use a sorted map for consistent serialization when using getAsMap()
613614
private final Map<String, String> map = new TreeMap<>();
614615

615-
private KeyStoreWrapper keystore;
616+
private SetOnce<KeyStoreWrapper> keystore = new SetOnce<>();
616617

617618
private Builder() {
618619

@@ -638,9 +639,10 @@ public String get(String key) {
638639

639640
/** Sets the secret store for these settings. */
640641
public void setKeyStore(KeyStoreWrapper keystore) {
641-
assert this.keystore == null; // only set once!
642-
assert keystore.isLoaded();
643-
this.keystore = Objects.requireNonNull(keystore);
642+
if (keystore.isLoaded()) {
643+
throw new IllegalStateException("The keystore wrapper must already be loaded");
644+
}
645+
this.keystore.set(Objects.requireNonNull(keystore));
644646
}
645647

646648
/**
@@ -1049,7 +1051,7 @@ public Builder normalizePrefix(String prefix) {
10491051
* set on this builder.
10501052
*/
10511053
public Settings build() {
1052-
return new Settings(map, keystore);
1054+
return new Settings(map, keystore.get());
10531055
}
10541056
}
10551057

core/src/test/java/org/elasticsearch/cli/TerminalTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,33 @@ public void testEscaping() throws Exception {
4646
assertPrinted(terminal, Terminal.Verbosity.NORMAL, "This message contains percent like %20n");
4747
}
4848

49+
public void testPromptYesNoDefault() throws Exception {
50+
MockTerminal terminal = new MockTerminal();
51+
terminal.addTextInput("");
52+
assertTrue(terminal.promptYesNo("Answer?", true));
53+
terminal.addTextInput("");
54+
assertFalse(terminal.promptYesNo("Answer?", false));
55+
}
56+
57+
public void testPromptYesNoReprompt() throws Exception {
58+
MockTerminal terminal = new MockTerminal();
59+
terminal.addTextInput("blah");
60+
terminal.addTextInput("y");
61+
assertTrue(terminal.promptYesNo("Answer? [Y/n]\nDid not understand answer 'blah'\nAnswer? [Y/n]", true));
62+
}
63+
64+
public void testPromptYesNoCase() throws Exception {
65+
MockTerminal terminal = new MockTerminal();
66+
terminal.addTextInput("Y");
67+
assertTrue(terminal.promptYesNo("Answer?", false));
68+
terminal.addTextInput("y");
69+
assertTrue(terminal.promptYesNo("Answer?", false));
70+
terminal.addTextInput("N");
71+
assertFalse(terminal.promptYesNo("Answer?", true));
72+
terminal.addTextInput("n");
73+
assertFalse(terminal.promptYesNo("Answer?", true));
74+
}
75+
4976
private void assertPrinted(MockTerminal logTerminal, Terminal.Verbosity verbosity, String text) throws Exception {
5077
logTerminal.println(verbosity, text);
5178
String output = logTerminal.getOutput();

core/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
package org.elasticsearch.common.settings;
2121

22+
import javax.crypto.SecretKeyFactory;
23+
import java.security.Provider;
24+
import java.security.Security;
2225
import java.util.Map;
2326
import java.util.Set;
2427

@@ -43,7 +46,7 @@ protected Environment createEnv(Terminal terminal, Map<String, String> settings)
4346
}
4447

4548
public void testMissing() throws Exception {
46-
UserException e = expectThrows(UserException.class, this::execute);
49+
UserException e = expectThrows(UserException.class, () -> execute("foo"));
4750
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
4851
assertThat(e.getMessage(), containsString("keystore not found"));
4952
}

0 commit comments

Comments
 (0)