mirror of
https://github.com/elastic/logstash.git
synced 2025-04-23 22:27:21 -04:00
This commit fixes how the keystore tool handle the command's options, including validation for unknown options, and adding the --stdin flag to the add command.
(cherry picked from commit 41ec183f09
)
Co-authored-by: Edmo Vamerlatti Costa <11836452+edmocosta@users.noreply.github.com>
This commit is contained in:
parent
e9213e952e
commit
25a381b253
2 changed files with 246 additions and 44 deletions
|
@ -29,7 +29,6 @@ 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;
|
||||
|
||||
/**
|
||||
|
@ -42,18 +41,31 @@ public class SecretStoreCli {
|
|||
private final Terminal terminal;
|
||||
private final SecretStoreFactory secretStoreFactory;
|
||||
|
||||
enum Command {
|
||||
CREATE("create"), LIST("list"), ADD("add"), REMOVE("remove"), HELP("--help");
|
||||
enum CommandOptions {
|
||||
HELP("help"),
|
||||
/**
|
||||
* Common flag used to change the read source to the standard input. It has no effect as
|
||||
* this tool already reads values from the stdin by default, but still necessary (BC) to
|
||||
* not consider this flag as an actual value.
|
||||
*/
|
||||
STDIN("stdin");
|
||||
|
||||
private static final String PREFIX = "--";
|
||||
|
||||
private final String option;
|
||||
|
||||
Command(String option) {
|
||||
this.option = option;
|
||||
CommandOptions(final String option) {
|
||||
this.option = String.format("%s%s", PREFIX, option);
|
||||
}
|
||||
|
||||
static Optional<Command> fromString(final String input) {
|
||||
Optional<Command> command = EnumSet.allOf(Command.class).stream().filter(c -> c.option.equals(input)).findFirst();
|
||||
return command;
|
||||
static Optional<CommandOptions> fromString(final String option) {
|
||||
if (option == null || !option.startsWith(PREFIX)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Arrays.stream(values())
|
||||
.filter(p -> p.option.equals(option))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
String getOption() {
|
||||
|
@ -61,6 +73,111 @@ public class SecretStoreCli {
|
|||
}
|
||||
}
|
||||
|
||||
static class CommandLine {
|
||||
private final Command command;
|
||||
private final List<String> arguments;
|
||||
private final Set<CommandOptions> options;
|
||||
|
||||
CommandLine(final Command command, final String[] arguments) {
|
||||
this.command = command;
|
||||
this.arguments = parseCommandArguments(command, arguments);
|
||||
this.options = parseCommandOptions(command, arguments);
|
||||
}
|
||||
|
||||
private static List<String> parseCommandArguments(final Command command, final String[] arguments) {
|
||||
if (arguments == null || arguments.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final Set<String> commandValidOptions = command.getValidOptions()
|
||||
.stream()
|
||||
.map(CommandOptions::getOption)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// Includes all arguments values until it reaches the end or found a command option/flag
|
||||
String[] filteredArguments = arguments;
|
||||
for (int i = 0; i < arguments.length; i++) {
|
||||
if (commandValidOptions.contains(arguments[i])) {
|
||||
filteredArguments = Arrays.copyOfRange(arguments, 0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Arrays.asList(filteredArguments);
|
||||
}
|
||||
|
||||
private static Set<CommandOptions> parseCommandOptions(final Command command, final String[] arguments) {
|
||||
final Set<CommandOptions> providedOptions = EnumSet.noneOf(CommandOptions.class);
|
||||
if (arguments == null) {
|
||||
return providedOptions;
|
||||
}
|
||||
|
||||
for (final String argument : arguments) {
|
||||
if (argument.startsWith(CommandOptions.PREFIX)) {
|
||||
final Optional<CommandOptions> option = CommandOptions.fromString(argument);
|
||||
if (option.isEmpty() || !command.validOptions.contains(option.get())) {
|
||||
throw new InvalidCommandException(String.format("Unrecognized option '%s' for command '%s'", argument, command.command));
|
||||
}
|
||||
providedOptions.add(option.get());
|
||||
}
|
||||
}
|
||||
|
||||
return providedOptions;
|
||||
}
|
||||
|
||||
boolean hasOption(final CommandOptions option) {
|
||||
return options.contains(option);
|
||||
}
|
||||
|
||||
Command getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
List<String> getArguments() {
|
||||
return Collections.unmodifiableList(arguments);
|
||||
}
|
||||
}
|
||||
|
||||
static class InvalidCommandException extends RuntimeException {
|
||||
private static final long serialVersionUID = 8402825920204022022L;
|
||||
|
||||
public InvalidCommandException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
enum Command {
|
||||
CREATE("create"),
|
||||
LIST("list"),
|
||||
ADD("add", List.of(CommandOptions.STDIN)),
|
||||
REMOVE("remove");
|
||||
|
||||
private final Set<CommandOptions> validOptions = EnumSet.of(CommandOptions.HELP);
|
||||
|
||||
private final String command;
|
||||
|
||||
Command(final String command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
Command(final String command, final Collection<CommandOptions> validOptions) {
|
||||
this.command = command;
|
||||
this.validOptions.addAll(validOptions);
|
||||
}
|
||||
|
||||
static Optional<CommandLine> parse(final String command, final String[] arguments) {
|
||||
final Optional<Command> foundCommand = Arrays.stream(values())
|
||||
.filter(c -> c.command.equals(command))
|
||||
.findFirst();
|
||||
|
||||
return foundCommand.map(value -> new CommandLine(value, arguments));
|
||||
}
|
||||
|
||||
Set<CommandOptions> getValidOptions() {
|
||||
return Collections.unmodifiableSet(validOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public SecretStoreCli(Terminal terminal){
|
||||
this(terminal, SecretStoreFactory.fromEnvironment());
|
||||
}
|
||||
|
@ -74,16 +191,28 @@ 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 arguments This can be either identifiers for a secret, or a sub command like --help. May be null.
|
||||
* @param allArguments 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... arguments) {
|
||||
public void command(String primaryCommand, SecureConfig config, String... allArguments) {
|
||||
terminal.writeLine("");
|
||||
final Command command = Command.fromString(primaryCommand).orElse(Command.HELP);
|
||||
boolean help = nonNull(arguments) && Arrays.asList(arguments).contains(Command.HELP.getOption());
|
||||
|
||||
switch (command) {
|
||||
final Optional<CommandLine> commandParseResult;
|
||||
try {
|
||||
commandParseResult = Command.parse(primaryCommand, allArguments);
|
||||
} catch (InvalidCommandException e) {
|
||||
terminal.writeLine(String.format("ERROR: %s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandParseResult.isEmpty()) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandLine commandLine = commandParseResult.get();
|
||||
switch (commandLine.getCommand()) {
|
||||
case CREATE: {
|
||||
if (help){
|
||||
if (commandLine.hasOption(CommandOptions.HELP)){
|
||||
terminal.writeLine("Creates a new keystore. For example: 'bin/logstash-keystore create'");
|
||||
return;
|
||||
}
|
||||
|
@ -98,7 +227,7 @@ public class SecretStoreCli {
|
|||
break;
|
||||
}
|
||||
case LIST: {
|
||||
if (help){
|
||||
if (commandLine.hasOption(CommandOptions.HELP)){
|
||||
terminal.writeLine("List all secret identifiers from the keystore. For example: " +
|
||||
"`bin/logstash-keystore list`. Note - only the identifiers will be listed, not the secrets.");
|
||||
return;
|
||||
|
@ -110,18 +239,18 @@ public class SecretStoreCli {
|
|||
break;
|
||||
}
|
||||
case ADD: {
|
||||
if (help){
|
||||
if (commandLine.hasOption(CommandOptions.HELP)){
|
||||
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 (arguments == null || arguments.length == 0) {
|
||||
if (commandLine.getArguments().isEmpty()) {
|
||||
terminal.writeLine("ERROR: You must supply an identifier to add. (e.g. bin/logstash-keystore add my-secret)");
|
||||
return;
|
||||
}
|
||||
if (secretStoreFactory.exists(config.clone())) {
|
||||
final SecretStore secretStore = secretStoreFactory.load(config);
|
||||
for (String argument : arguments) {
|
||||
for (String argument : commandLine.getArguments()) {
|
||||
final SecretIdentifier id = new SecretIdentifier(argument);
|
||||
final byte[] existingValue = secretStore.retrieveSecret(id);
|
||||
if (existingValue != null) {
|
||||
|
@ -159,18 +288,18 @@ public class SecretStoreCli {
|
|||
break;
|
||||
}
|
||||
case REMOVE: {
|
||||
if (help){
|
||||
if (commandLine.hasOption(CommandOptions.HELP)){
|
||||
terminal.writeLine("Remove secrets from the keystore. For example: " +
|
||||
"`bin/logstash-keystore remove my-secret`");
|
||||
return;
|
||||
}
|
||||
if (arguments == null || arguments.length == 0) {
|
||||
if (commandLine.getArguments().isEmpty()) {
|
||||
terminal.writeLine("ERROR: You must supply a value to remove. (e.g. bin/logstash-keystore remove my-secret)");
|
||||
return;
|
||||
}
|
||||
|
||||
final SecretStore secretStore = secretStoreFactory.load(config);
|
||||
for (String argument : arguments) {
|
||||
for (String argument : commandLine.getArguments()) {
|
||||
SecretIdentifier id = new SecretIdentifier(argument);
|
||||
if (secretStore.containsSecret(id)) {
|
||||
secretStore.purgeSecret(id);
|
||||
|
@ -182,32 +311,32 @@ public class SecretStoreCli {
|
|||
|
||||
break;
|
||||
}
|
||||
case HELP: {
|
||||
terminal.writeLine("Usage:");
|
||||
terminal.writeLine("--------");
|
||||
terminal.writeLine("bin/logstash-keystore [option] command [argument]");
|
||||
terminal.writeLine("");
|
||||
terminal.writeLine("Commands:");
|
||||
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 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("--------");
|
||||
terminal.writeLine("--help - Display command specific help (e.g. bin/logstash-keystore add --help)");
|
||||
terminal.writeLine("");
|
||||
terminal.writeLine("Options:");
|
||||
terminal.writeLine("--------");
|
||||
terminal.writeLine("--path.settings - Set the directory for the keystore. This is should be the same directory as the logstash.yml settings file. " +
|
||||
"The default is the config directory under Logstash home. (e.g. bin/logstash-keystore --path.settings /tmp/foo create)");
|
||||
terminal.writeLine("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void printHelp(){
|
||||
terminal.writeLine("Usage:");
|
||||
terminal.writeLine("--------");
|
||||
terminal.writeLine("bin/logstash-keystore [option] command [argument]");
|
||||
terminal.writeLine("");
|
||||
terminal.writeLine("Commands:");
|
||||
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 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("--------");
|
||||
terminal.writeLine("--help - Display command specific help (e.g. bin/logstash-keystore add --help)");
|
||||
terminal.writeLine("");
|
||||
terminal.writeLine("Options:");
|
||||
terminal.writeLine("--------");
|
||||
terminal.writeLine("--path.settings - Set the directory for the keystore. This is should be the same directory as the logstash.yml settings file. " +
|
||||
"The default is the config directory under Logstash home. (e.g. bin/logstash-keystore --path.settings /tmp/foo create)");
|
||||
terminal.writeLine("");
|
||||
}
|
||||
|
||||
private void add(SecretStore secretStore, SecretIdentifier id, byte[] secret) {
|
||||
secretStore.persistSecret(id, secret);
|
||||
terminal.writeLine(String.format("Added '%s' to the Logstash keystore.", id.getKey()));
|
||||
|
|
|
@ -33,6 +33,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -214,6 +215,22 @@ public class SecretStoreCliTest {
|
|||
assertThat(terminal.out).containsIgnoringCase("ERROR: Logstash keystore not found. Use 'create' command to create one.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddWithStdinOption() {
|
||||
createKeyStore();
|
||||
|
||||
terminal.in.add(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, SecretStoreCli.CommandOptions.STDIN.getOption());
|
||||
terminal.reset();
|
||||
|
||||
cli.command("list", newStoreConfig);
|
||||
assertListed(id);
|
||||
assertNotListed(SecretStoreCli.CommandOptions.STDIN.getOption());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
createKeyStore();
|
||||
|
@ -294,6 +311,58 @@ public class SecretStoreCliTest {
|
|||
assertThat(terminal.out).containsIgnoringCase(expectedMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandWithUnrecognizedOption() {
|
||||
createKeyStore();
|
||||
|
||||
terminal.in.add("foo");
|
||||
|
||||
final String invalidOption = "--invalid-option";
|
||||
cli.command("add", newStoreConfig.clone(), UUID.randomUUID().toString(), invalidOption);
|
||||
assertThat(terminal.out).contains(String.format("Unrecognized option '%s' for command 'add'", invalidOption));
|
||||
|
||||
terminal.reset();
|
||||
cli.command("list", newStoreConfig);
|
||||
assertNotListed(invalidOption);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandParseWithValidCommand() {
|
||||
final String[] args = new String[]{
|
||||
"FOO",
|
||||
"BAR",
|
||||
"--stdin",
|
||||
"ANYTHING"
|
||||
};
|
||||
|
||||
final Optional<SecretStoreCli.CommandLine> commandLineParseResult = SecretStoreCli.Command
|
||||
.parse("add", args);
|
||||
|
||||
assertThat(commandLineParseResult).isPresent();
|
||||
|
||||
final SecretStoreCli.CommandLine commandLine = commandLineParseResult.get();
|
||||
assertThat(commandLine.getCommand()).isEqualTo(SecretStoreCli.Command.ADD);
|
||||
assertThat(commandLine.getArguments()).containsExactly("FOO", "BAR");
|
||||
assertThat(commandLine.hasOption(SecretStoreCli.CommandOptions.STDIN)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandParseWithInvalidCommand() {
|
||||
final Optional<SecretStoreCli.CommandLine> commandLineParseResult = SecretStoreCli.Command
|
||||
.parse("non-existing-command", new String[0]);
|
||||
|
||||
assertThat(commandLineParseResult).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tesCommandsAllowHelpOption() {
|
||||
for (final SecretStoreCli.Command value : SecretStoreCli.Command.values()) {
|
||||
assertThat(value.getValidOptions())
|
||||
.withFailMessage("Command '%s' must support the '--help' option", value.name())
|
||||
.contains(SecretStoreCli.CommandOptions.HELP);
|
||||
}
|
||||
}
|
||||
|
||||
private void createKeyStore() {
|
||||
terminal.reset();
|
||||
terminal.in.add("y");
|
||||
|
@ -314,6 +383,10 @@ public class SecretStoreCliTest {
|
|||
assertTrue(Arrays.stream(expected).allMatch(terminal.out::contains));
|
||||
}
|
||||
|
||||
private void assertNotListed(String... expected) {
|
||||
assertTrue(Arrays.stream(expected).noneMatch(terminal.out::contains));
|
||||
}
|
||||
|
||||
private void assertPrimaryHelped() {
|
||||
assertThat(terminal.out).
|
||||
containsIgnoringCase("Commands").
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue