Add support to add and remove multiple keystore keys in a single operation (#15612)

This commit added support to add and remove multiple keystore keys in a single operation. It also fixed the empty value validation for editing existing key values and added ASCII validation for values.
This commit is contained in:
Edmo Vamerlatti Costa 2023-11-30 10:21:51 +01:00 committed by GitHub
parent 8b9acf12fa
commit 5543e3c3b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 204 additions and 87 deletions

View file

@ -90,7 +90,7 @@ bin/logstash-keystore create
This setup requires the user running Logstash to have the environment variable
`LOGSTASH_KEYSTORE_PASS=mypassword` defined. If the environment variable is not defined,
Logstash cannot access the the keystore.
Logstash cannot access the keystore.
When you run Logstash from an RPM or DEB package installation, the environment
variables are sourced from `/etc/sysconfig/logstash`.
@ -154,10 +154,12 @@ use the `add` command:
["source","sh",subs="attributes"]
----------------------------------------------------------------
bin/logstash-keystore add ES_PWD
bin/logstash-keystore add ES_USER ES_PWD
----------------------------------------------------------------
When prompted, enter a value for the key.
When prompted, enter a value for each key.
NOTE: Key values are limited to ASCII characters. It includes digits, letters, and a few special symbols.
[discrete]
[[list-settings]]
@ -174,9 +176,9 @@ bin/logstash-keystore list
[[remove-settings]]
=== Remove keys
To remove a key from the keystore, use:
To remove keys from the keystore, use:
["source","sh",subs="attributes"]
----------------------------------------------------------------
bin/logstash-keystore remove ES_PWD
bin/logstash-keystore remove ES_USER ES_PWD
----------------------------------------------------------------

View file

@ -49,7 +49,7 @@ class LogStash::SecretStoreCli
LogStash::Util::SettingsHelper.post_process
secure_config = SecretStoreExt.getConfig(LogStash::SETTINGS.get_setting("keystore.file").value, LogStash::SETTINGS.get_setting("keystore.classname").value)
cli = SecretStoreCli.new(Terminal.new)
cli.command(ARGV[0], secure_config, ARGV[1])
cli.command(ARGV[0], secure_config, *ARGV[1, ARGV.length])
exit 0
rescue => e
logger.error(e.message, :cause => e.cause, :backtrace => e.backtrace)

View file

@ -23,9 +23,13 @@ package org.logstash.secret.cli;
import org.logstash.secret.SecretIdentifier;
import org.logstash.secret.store.*;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.Objects.nonNull;
import static org.logstash.secret.store.SecretStoreFactory.LOGSTASH_MARKER;
/**
@ -34,6 +38,7 @@ import static org.logstash.secret.store.SecretStoreFactory.LOGSTASH_MARKER;
*/
public class SecretStoreCli {
private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
private final Terminal terminal;
private final SecretStoreFactory secretStoreFactory;
@ -50,6 +55,10 @@ public class SecretStoreCli {
Optional<Command> command = EnumSet.allOf(Command.class).stream().filter(c -> c.option.equals(input)).findFirst();
return command;
}
String getOption() {
return option;
}
}
public SecretStoreCli(Terminal terminal){
@ -65,13 +74,13 @@ public class SecretStoreCli {
* Entry point to issue a command line command.
* @param primaryCommand The string representation of a {@link SecretStoreCli.Command}, if the String does not map to a {@link SecretStoreCli.Command}, then it will show the help menu.
* @param config The configuration needed to work a secret store. May be null for help.
* @param argument This can be either the identifier for a secret, or a sub command like --help. May be null.
* @param arguments This can be either identifiers for a secret, or a sub command like --help. May be null.
*/
public void command(String primaryCommand, SecureConfig config, String argument) {
public void command(String primaryCommand, SecureConfig config, String... arguments) {
terminal.writeLine("");
final Command command = Command.fromString(primaryCommand).orElse(Command.HELP);
final Optional<Command> sub = Command.fromString(argument);
boolean help = Command.HELP.equals(sub.orElse(null));
boolean help = nonNull(arguments) && Arrays.asList(arguments).contains(Command.HELP.getOption());
switch (command) {
case CREATE: {
if (help){
@ -102,59 +111,75 @@ public class SecretStoreCli {
}
case ADD: {
if (help){
terminal.writeLine("Adds a new secret to the keystore. For example: " +
terminal.writeLine("Add secrets to the keystore. For example: " +
"`bin/logstash-keystore add my-secret`, at the prompt enter your secret. You will use the identifier ${my-secret} in your Logstash configuration.");
return;
}
if (argument == null || argument.isEmpty()) {
terminal.writeLine("ERROR: You must supply a identifier to add. (e.g. bin/logstash-keystore add my-secret)");
if (arguments == null || arguments.length == 0) {
terminal.writeLine("ERROR: You must supply an identifier to add. (e.g. bin/logstash-keystore add my-secret)");
return;
}
if (secretStoreFactory.exists(config.clone())) {
SecretIdentifier id = new SecretIdentifier(argument);
SecretStore secretStore = secretStoreFactory.load(config);
byte[] s = secretStore.retrieveSecret(id);
if (s == null) {
terminal.write(String.format("Enter value for %s: ", argument));
char[] secret = terminal.readSecret();
if(secret == null || secret.length == 0){
terminal.writeLine("ERROR: You must supply a identifier to add. (e.g. bin/logstash-keystore add my-secret)");
return;
final SecretStore secretStore = secretStoreFactory.load(config);
for (String argument : arguments) {
final SecretIdentifier id = new SecretIdentifier(argument);
final byte[] existingValue = secretStore.retrieveSecret(id);
if (existingValue != null) {
SecretStoreUtil.clearBytes(existingValue);
terminal.write(String.format("%s already exists. Overwrite ? [y/N] ", argument));
if (!isYes(terminal.readLine())) {
continue;
}
}
final String enterValueMessage = String.format("Enter value for %s: ", argument);
char[] secret = null;
while(secret == null) {
terminal.write(enterValueMessage);
final char[] readSecret = terminal.readSecret();
if (readSecret == null || readSecret.length == 0) {
terminal.writeLine("ERROR: Value cannot be empty");
continue;
}
if (!ASCII_ENCODER.canEncode(CharBuffer.wrap(readSecret))) {
terminal.writeLine("ERROR: Value must contain only ASCII characters");
continue;
}
secret = readSecret;
}
add(secretStore, id, SecretStoreUtil.asciiCharToBytes(secret));
} else {
SecretStoreUtil.clearBytes(s);
terminal.write(String.format("%s already exists. Overwrite ? [y/N] ", argument));
if (isYes(terminal.readLine())) {
terminal.write(String.format("Enter value for %s: ", argument));
char[] secret = terminal.readSecret();
add(secretStore, id, SecretStoreUtil.asciiCharToBytes(secret));
}
}
} else {
terminal.writeLine(String.format("ERROR: Logstash keystore not found. Use 'create' command to create one."));
terminal.writeLine("ERROR: Logstash keystore not found. Use 'create' command to create one.");
}
break;
}
case REMOVE: {
if (help){
terminal.writeLine("Removes a secret from the keystore. For example: " +
terminal.writeLine("Remove secrets from the keystore. For example: " +
"`bin/logstash-keystore remove my-secret`");
return;
}
if (argument == null || argument.isEmpty()) {
if (arguments == null || arguments.length == 0) {
terminal.writeLine("ERROR: You must supply a value to remove. (e.g. bin/logstash-keystore remove my-secret)");
return;
}
SecretIdentifier id = new SecretIdentifier(argument);
SecretStore secretStore = secretStoreFactory.load(config);
if (secretStore.containsSecret(id)) {
secretStore.purgeSecret(id);
terminal.writeLine(String.format("Removed '%s' from the Logstash keystore.", id.getKey()));
} else {
terminal.writeLine(String.format("ERROR: '%s' does not exist in the Logstash keystore.", argument));
final SecretStore secretStore = secretStoreFactory.load(config);
for (String argument : arguments) {
SecretIdentifier id = new SecretIdentifier(argument);
if (secretStore.containsSecret(id)) {
secretStore.purgeSecret(id);
terminal.writeLine(String.format("Removed '%s' from the Logstash keystore.", id.getKey()));
} else {
terminal.writeLine(String.format("ERROR: '%s' does not exist in the Logstash keystore.", argument));
}
}
break;
}
case HELP: {
@ -166,8 +191,8 @@ public class SecretStoreCli {
terminal.writeLine("--------");
terminal.writeLine("create - Creates a new Logstash keystore (e.g. bin/logstash-keystore create)");
terminal.writeLine("list - List entries in the keystore (e.g. bin/logstash-keystore list)");
terminal.writeLine("add - Add a value to the keystore (e.g. bin/logstash-keystore add my-secret)");
terminal.writeLine("remove - Remove a value from the keystore (e.g. bin/logstash-keystore remove my-secret)");
terminal.writeLine("add - Add values to the keystore (e.g. bin/logstash-keystore add secret-one secret-two)");
terminal.writeLine("remove - Remove values from the keystore (e.g. bin/logstash-keystore remove secret-one secret-two)");
terminal.writeLine("");
terminal.writeLine("Argument:");
terminal.writeLine("--------");

View file

@ -28,11 +28,15 @@ import org.logstash.secret.store.SecretStoreFactory;
import org.logstash.secret.store.SecureConfig;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.logstash.secret.store.SecretStoreFactory.ENVIRONMENT_PASS_KEY;
@ -54,6 +58,7 @@ public class SecretStoreCliTest {
final SecretStoreFactory secretStoreFactory = SecretStoreFactory.withEnvironment(environment);
cli = new SecretStoreCli(terminal, secretStoreFactory);
existingStoreConfig = new SecureConfig();
existingStoreConfig.add("keystore.file",
Paths.get(this.getClass().getClassLoader().getResource("logstash.keystore.with.default.pass").toURI()).toString().toCharArray());
@ -64,14 +69,14 @@ public class SecretStoreCliTest {
@Test
public void testBadCommand() {
cli.command("nonsense", null, null);
cli.command("nonsense", null);
assertPrimaryHelped();
}
@Test
public void testHelpAdd() {
cli.command("add", null, "--help");
assertThat(terminal.out).containsIgnoringCase("Adds a new secret to the keystore");
assertThat(terminal.out).containsIgnoringCase("Add secrets to the keystore");
}
@Test
@ -89,12 +94,12 @@ public class SecretStoreCliTest {
@Test
public void testHelpRemove() {
cli.command("remove", null, "--help");
assertThat(terminal.out).containsIgnoringCase("Removes a secret from the keystore");
assertThat(terminal.out).containsIgnoringCase("Remove secrets from the keystore");
}
@Test
public void testList() {
cli.command("list", existingStoreConfig, null);
cli.command("list", existingStoreConfig);
// contents of the existing store is a-z for both the key and value
for (int i = 65; i <= 90; i++) {
@ -106,104 +111,166 @@ public class SecretStoreCliTest {
@Test
public void testCreateNewAllYes() {
terminal.in = "y";
cli.command("create", newStoreConfig, null);
terminal.in.add("y");
cli.command("create", newStoreConfig);
assertCreated();
}
@Test
public void testCreateNewAllNo() {
terminal.in = "n";
cli.command("create", newStoreConfig, null);
terminal.in.add("n");
cli.command("create", newStoreConfig);
assertNotCreated();
}
@Test
public void testCreateNoEnvironmentWarning() {
cli.command("create", newStoreConfig, null);
cli.command("create", newStoreConfig);
assertThat(terminal.out).contains("Please set the environment variable `LOGSTASH_KEYSTORE_PASS`. Failure to do so will result in reduced security.");
}
@Test
public void testDoubleCreateWarning() {
terminal.in = "y";
cli.command("create", newStoreConfig, null);
assertCreated();
terminal.reset();
createKeyStore();
cli.command("create", newStoreConfig, null);
cli.command("create", newStoreConfig);
assertThat(terminal.out).contains("Overwrite");
assertNotCreated();
}
@Test
public void testAddEmptyValue() {
terminal.in = "y";
cli.command("create", newStoreConfig, null);
assertCreated();
terminal.reset();
createKeyStore();
terminal.in.add(""); // sets the empty value
terminal.in.add("value");
terminal.in = ""; // sets the value
String id = UUID.randomUUID().toString();
cli.command("add", newStoreConfig.clone(), id);
assertThat(terminal.out).containsIgnoringCase("ERROR");
assertThat(terminal.out).containsIgnoringCase("ERROR: Value cannot be empty");
}
@Test
public void testAddNonAsciiValue() {
createKeyStore();
terminal.in.add("€€€€€"); // sets non-ascii value value
terminal.in.add("value");
String id = UUID.randomUUID().toString();
cli.command("add", newStoreConfig.clone(), id);
assertThat(terminal.out).containsIgnoringCase("ERROR: Value must contain only ASCII characters");
}
@Test
public void testAdd() {
terminal.in = "y";
cli.command("create", newStoreConfig, null);
assertCreated();
terminal.reset();
createKeyStore();
terminal.in = UUID.randomUUID().toString(); // sets the value
terminal.in.add(UUID.randomUUID().toString()); // sets the value
String id = UUID.randomUUID().toString();
cli.command("add", newStoreConfig.clone(), id);
terminal.reset();
cli.command("list", newStoreConfig, null);
cli.command("list", newStoreConfig);
assertListed(id);
}
@Test
public void testRemove() {
terminal.in = "y";
cli.command("create", newStoreConfig, null);
assertCreated();
public void testAddWithNoIdentifiers() {
final String expectedMessage = "ERROR: You must supply an identifier to add";
createKeyStore();
String[] nullArguments = null;
cli.command("add", newStoreConfig.clone(), nullArguments);
assertThat(terminal.out).containsIgnoringCase(expectedMessage);
terminal.reset();
terminal.in = UUID.randomUUID().toString(); // sets the value
cli.command("add", newStoreConfig.clone());
assertThat(terminal.out).containsIgnoringCase(expectedMessage);
}
@Test
public void testAddMultipleKeys() {
createKeyStore();
terminal.in.add(UUID.randomUUID().toString());
terminal.in.add(UUID.randomUUID().toString());
final String keyOne = UUID.randomUUID().toString();
final String keyTwo = UUID.randomUUID().toString();
cli.command("add", newStoreConfig.clone(), keyOne, keyTwo);
terminal.reset();
cli.command("list", newStoreConfig);
assertListed(keyOne, keyTwo);
}
@Test
public void testAddWithoutCreatedKeystore() {
cli.command("add", newStoreConfig.clone(), UUID.randomUUID().toString());
assertThat(terminal.out).containsIgnoringCase("ERROR: Logstash keystore not found. Use 'create' command to create one.");
}
@Test
public void testRemove() {
createKeyStore();
terminal.in.add(UUID.randomUUID().toString()); // sets the value
String id = UUID.randomUUID().toString();
cli.command("add", newStoreConfig.clone(), id);
System.out.println(terminal.out);
terminal.reset();
cli.command("list", newStoreConfig.clone(), null);
cli.command("list", newStoreConfig.clone());
assertListed(id);
terminal.reset();
cli.command("remove", newStoreConfig.clone(), id);
terminal.reset();
cli.command("list", newStoreConfig, null);
cli.command("list", newStoreConfig);
assertThat(terminal.out).doesNotContain(id);
}
@Test
public void testRemoveMissing() {
terminal.in = "y";
cli.command("create", newStoreConfig, null);
assertCreated();
public void testRemoveMultipleKeys() {
createKeyStore();
terminal.in.add(UUID.randomUUID().toString());
terminal.in.add(UUID.randomUUID().toString());
final String keyOne = UUID.randomUUID().toString();
final String keyTwo = UUID.randomUUID().toString();
cli.command("add", newStoreConfig.clone(), keyOne, keyTwo);
terminal.reset();
terminal.in = UUID.randomUUID().toString(); // sets the value
cli.command("list", newStoreConfig.clone());
assertListed(keyOne, keyTwo);
terminal.reset();
cli.command("remove", newStoreConfig.clone(), keyOne, keyTwo);
terminal.reset();
cli.command("list", newStoreConfig);
assertThat(terminal.out).doesNotContain(keyOne);
assertThat(terminal.out).doesNotContain(keyTwo);
}
@Test
public void testRemoveMissing() {
createKeyStore();
terminal.in.add(UUID.randomUUID().toString()); // sets the value
String id = UUID.randomUUID().toString();
cli.command("add", newStoreConfig.clone(), id);
System.out.println(terminal.out);
terminal.reset();
cli.command("list", newStoreConfig.clone(), null);
cli.command("list", newStoreConfig.clone());
assertListed(id);
terminal.reset();
@ -211,6 +278,29 @@ public class SecretStoreCliTest {
assertThat(terminal.out).containsIgnoringCase("error");
}
@Test
public void testRemoveWithNoIdentifiers() {
final String expectedMessage = "ERROR: You must supply a value to remove.";
createKeyStore();
String[] nullArguments = null;
cli.command("remove", newStoreConfig.clone(), nullArguments);
assertThat(terminal.out).containsIgnoringCase(expectedMessage);
terminal.reset();
cli.command("remove", newStoreConfig.clone());
assertThat(terminal.out).containsIgnoringCase(expectedMessage);
}
private void createKeyStore() {
terminal.reset();
terminal.in.add("y");
cli.command("create", newStoreConfig);
assertCreated();
terminal.reset();
}
private void assertNotCreated() {
assertThat(terminal.out).doesNotContain("Created Logstash keystore");
@ -220,8 +310,8 @@ public class SecretStoreCliTest {
assertThat(terminal.out).contains("Created Logstash keystore");
}
private void assertListed(String expected) {
assertThat(terminal.out).contains(expected);
private void assertListed(String... expected) {
assertTrue(Arrays.stream(expected).allMatch(terminal.out::contains));
}
private void assertPrimaryHelped() {
@ -243,7 +333,7 @@ public class SecretStoreCliTest {
class TestTerminal extends Terminal {
public String out = "";
public String in = "";
public final Queue<String> in = new LinkedList<>();
@Override
public void writeLine(String text) {
@ -257,16 +347,16 @@ public class SecretStoreCliTest {
@Override
public String readLine() {
return in;
return in.poll();
}
@Override
public char[] readSecret() {
return in.toCharArray();
return in.poll().toCharArray();
}
public void reset() {
in = "";
in.clear();
out = "";
}
}