diff --git a/cli/admin-cli/build.gradle b/cli/admin-cli/build.gradle deleted file mode 100644 index 1d1c2a5ac5..0000000000 --- a/cli/admin-cli/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ - -dependencies { - - compile 'info.picocli:picocli:4.0.4' - compile project(':config') - compile project(':shared') - compile project(':cli:cli-api') - compile project(':tessera-jaxrs:jaxrs-client') - compile 'javax.validation:validation-api' - compile 'javax.ws.rs:javax.ws.rs-api' - testImplementation project(':tests:test-util') -} diff --git a/cli/admin-cli/pom.xml b/cli/admin-cli/pom.xml deleted file mode 100644 index 9a7b496344..0000000000 --- a/cli/admin-cli/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - cli - com.jpmorgan.quorum - 0.11-SNAPSHOT - - 4.0.0 - - admin-cli - - - - com.jpmorgan.quorum - cli-api - - - - com.jpmorgan.quorum - jaxrs-client - - - - javax.validation - validation-api - - - - javax.ws.rs - javax.ws.rs-api - - - - diff --git a/cli/admin-cli/src/main/resources/META-INF/services/com.quorum.tessera.cli.CliAdapter b/cli/admin-cli/src/main/resources/META-INF/services/com.quorum.tessera.cli.CliAdapter deleted file mode 100644 index d9e8355f71..0000000000 --- a/cli/admin-cli/src/main/resources/META-INF/services/com.quorum.tessera.cli.CliAdapter +++ /dev/null @@ -1 +0,0 @@ -com.quorum.tessera.admin.cli.AdminCliAdapter diff --git a/cli/admin-cli/src/test/resources/META-INF/services/com.quorum.tessera.io.SystemAdapter b/cli/admin-cli/src/test/resources/META-INF/services/com.quorum.tessera.io.SystemAdapter deleted file mode 100644 index 29b992460a..0000000000 --- a/cli/admin-cli/src/test/resources/META-INF/services/com.quorum.tessera.io.SystemAdapter +++ /dev/null @@ -1 +0,0 @@ -com.quorum.tessera.io.NoopSystemAdapter \ No newline at end of file diff --git a/cli/admin-cli/src/test/resources/sample-config.json b/cli/admin-cli/src/test/resources/sample-config.json deleted file mode 100644 index 4407f1b349..0000000000 --- a/cli/admin-cli/src/test/resources/sample-config.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "useWhiteList": false, - "jdbc": { - "username": "scott", - "password": "tiger", - "url": "foo:bar" - }, - "serverConfigs": [ - { - "app": "ThirdParty", - "enabled": true, - "serverAddress": "http://localhost:8090", - - "communicationType": "REST" - }, - { - "app": "ADMIN", - "enabled": true, - "serverAddress": "http://localhost:18090", - "communicationType": "REST" - }, - { - "app": "Q2T", - "enabled": true, - "serverAddress": "unix:/tmp/test.ipc", - "communicationType": "REST" - }, - { - "app": "P2P", - "enabled": true, - "serverAddress": "http://localhost:8091", - "sslConfig": { - "tls": "OFF", - "generateKeyStoreIfNotExisted": "false", - "serverKeyStore": "./ssl/server1-keystore", - "serverKeyStorePassword": "quorum", - "serverTrustStore": "./ssl/server-truststore", - "serverTrustStorePassword": "quorum", - "serverTrustMode": "CA", - "clientKeyStore": "./ssl/client1-keystore", - "clientKeyStorePassword": "quorum", - "clientTrustStore": "./ssl/client-truststore", - "clientTrustStorePassword": "quorum", - "clientTrustMode": "CA", - "knownClientsFile": "./ssl/knownClients1", - "knownServersFile": "./ssl/knownServers1" - }, - "communicationType": "REST" - } - ], - "peer": [ - { - "url": "http://bogus1.com" - }, - { - "url": "http://bogus2.com" - } - ], - "keys": { - "passwords": [], - "keyData": [ - { - "config": { - "data": { - "bytes": "Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA=" - }, - "type": "unlocked" - }, - "publicKey": "/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc=" - } - ] - }, - "alwaysSendTo": [ - "/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc=" - ], - "unixSocketFile": "${unixSocketPath}" -} diff --git a/cli/cli-api/build.gradle b/cli/cli-api/build.gradle index 26195d6fc1..1ee25bded0 100644 --- a/cli/cli-api/build.gradle +++ b/cli/cli-api/build.gradle @@ -1,8 +1,7 @@ dependencies { - compile 'info.picocli:picocli:4.0.4' + compile 'info.picocli:picocli' compile 'org.apache.commons:commons-lang3:3.7' - compile 'commons-cli:commons-cli:1.4' compile project(':config') compile project(':shared') compile project(':encryption:encryption-api') diff --git a/cli/cli-api/pom.xml b/cli/cli-api/pom.xml index 8bef00af4a..c8bc3b6ed8 100644 --- a/cli/cli-api/pom.xml +++ b/cli/cli-api/pom.xml @@ -20,7 +20,6 @@ info.picocli picocli - 4.0.4 diff --git a/cli/cli-api/src/main/java/com/quorum/tessera/cli/CliDelegate.java b/cli/cli-api/src/main/java/com/quorum/tessera/cli/CliDelegate.java index c9bd7704de..0575f4097d 100644 --- a/cli/cli-api/src/main/java/com/quorum/tessera/cli/CliDelegate.java +++ b/cli/cli-api/src/main/java/com/quorum/tessera/cli/CliDelegate.java @@ -1,29 +1,13 @@ package com.quorum.tessera.cli; -import com.quorum.tessera.ServiceLoaderUtil; -import com.quorum.tessera.cli.parsers.ConfigConverter; import com.quorum.tessera.config.Config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine; -import java.util.List; -import java.util.Objects; import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import static picocli.CommandLine.Model.CommandSpec.DEFAULT_COMMAND_NAME; +// TODO(cjh) still using CliDelegate as a config store so that config can be injected by spring public enum CliDelegate { INSTANCE; - private static final Logger LOGGER = LoggerFactory.getLogger(CliDelegate.class); - - private static final CliResult HELP_RESULT = new CliResult(0, true, null); - - private static final CliResult DEFAULT_RESULT = new CliResult(1, true, null); - private Config config; public static CliDelegate instance() { @@ -36,70 +20,7 @@ public Config getConfig() { () -> new IllegalStateException("Execute must be invoked before attempting to fetch config")); } - public CliResult execute(String... args) throws Exception { - - final List adapters = ServiceLoaderUtil.loadAll(CliAdapter.class).collect(Collectors.toList()); - - LOGGER.debug("Loaded adapters {}", adapters); - - CliType cliType = CliType.valueOf(System.getProperty(CliType.CLI_TYPE_KEY, CliType.CONFIG.name())); - - LOGGER.debug("cliType {}", cliType); - - Predicate isTopLevel = - a -> a.getClass().getAnnotation(CommandLine.Command.class).name().equals(DEFAULT_COMMAND_NAME); - - // Finds the top level adapter that we want to start with. Exactly one is expected to be on the classpath. - final CliAdapter adapter = - adapters.stream() - .filter(a -> a.getClass().isAnnotationPresent(CommandLine.Command.class)) - .filter(isTopLevel) - .filter(a -> a.getType() == cliType) - .findFirst() - .get(); - - LOGGER.debug("Loaded adapter {}", adapter); - - // Then we find all the others and attach them as sub-commands. It is expected that they have defined their - // own hierarchy and command names. - final List subcommands = - adapters.stream() - // .filter(isTopLevel) - .filter(a -> a != adapter) - .filter(a -> a.getType() != CliType.ENCLAVE) - .collect(Collectors.toList()); - - // the mapper will give us access to the exception from the outside, if one occurred. - // mostly since we have an existing system, and this is a workaround - final CLIExceptionCapturer mapper = new CLIExceptionCapturer(); - final CommandLine commandLine = new CommandLine(adapter); - - subcommands.stream().peek(sc -> LOGGER.debug("Adding subcommand {}", sc)).forEach(commandLine::addSubcommand); - - commandLine - .registerConverter(Config.class, new ConfigConverter()) - .setSeparator(" ") - .setCaseInsensitiveEnumValuesAllowed(true) - .setUnmatchedArgumentsAllowed(true) - .setExecutionExceptionHandler(mapper) - .setParameterExceptionHandler(mapper); - - commandLine.execute(args); - - // if an exception occurred, throw it to to the upper levels where it gets handled - if (mapper.getThrown() != null) { - throw mapper.getThrown(); - } - - // otherwise, set the config object (if there is one) and return - final CliResult result = - commandLine.getParseResult().asCommandLineList().stream() - .map(cl -> cl.isUsageHelpRequested() ? HELP_RESULT : cl.getExecutionResult()) - .filter(Objects::nonNull) - .findFirst() - .orElse(DEFAULT_RESULT); - - this.config = result.getConfig().orElse(null); - return result; + public void setConfig(Config config) { + this.config = config; } } diff --git a/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/ConfigConverter.java b/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/ConfigConverter.java index 67d817f397..d32ddd4a32 100644 --- a/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/ConfigConverter.java +++ b/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/ConfigConverter.java @@ -2,6 +2,7 @@ import com.quorum.tessera.config.Config; import com.quorum.tessera.config.ConfigFactory; +import com.quorum.tessera.config.util.ConfigFileStore; import picocli.CommandLine; import java.io.FileNotFoundException; @@ -22,6 +23,8 @@ public Config convert(final String value) throws Exception { throw new FileNotFoundException(String.format("%s not found.", path)); } + ConfigFileStore.create(path); + try (InputStream in = Files.newInputStream(path)) { return configFactory.create(in); } diff --git a/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/ConfigurationParser.java b/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/ConfigurationParser.java deleted file mode 100644 index b2f10cab79..0000000000 --- a/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/ConfigurationParser.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.quorum.tessera.cli.parsers; - -import com.quorum.tessera.config.Config; -import com.quorum.tessera.config.ConfigException; -import com.quorum.tessera.config.KeyConfiguration; -import com.quorum.tessera.config.keypairs.ConfigKeyPair; -import com.quorum.tessera.config.util.ConfigFileStore; -import com.quorum.tessera.config.util.JaxbUtil; -import com.quorum.tessera.io.FilesDelegate; -import com.quorum.tessera.io.SystemAdapter; -import org.apache.commons.cli.CommandLine; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.PosixFilePermission; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.nio.file.StandardOpenOption.APPEND; -import static java.nio.file.StandardOpenOption.CREATE_NEW; - -public class ConfigurationParser implements Parser { - - protected static final Set NEW_PASSWORD_FILE_PERMS = - Stream.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE).collect(Collectors.toSet()); - - protected static final String passwordsMessage = "Configfile must contain \"passwordFile\" field. The \"passwords\" field is no longer supported."; - - private final List newlyGeneratedKeys; - - private final FilesDelegate filesDelegate; - - public ConfigurationParser(List newlyGeneratedKeys) { - this(newlyGeneratedKeys, FilesDelegate.create()); - } - - protected ConfigurationParser(List newlyGeneratedKeys, FilesDelegate filesDelegate) { - this.newlyGeneratedKeys = Objects.requireNonNull(newlyGeneratedKeys); - this.filesDelegate = Objects.requireNonNull(filesDelegate); - } - - @Override - public Config parse(final CommandLine commandLine) throws IOException { - - Config config = null; - - final boolean isGeneratingWithKeyVault = - commandLine.hasOption("keygen") && commandLine.hasOption("keygenvaulturl"); - - if (commandLine.hasOption("configfile") && !isGeneratingWithKeyVault) { - final Path path = Paths.get(commandLine.getOptionValue("configfile")); - - if (!filesDelegate.exists(path)) { - throw new FileNotFoundException(String.format("%s not found.", path)); - } - - try (InputStream in = filesDelegate.newInputStream(path)) { - config = JaxbUtil.unmarshal(in, Config.class); - - if (!newlyGeneratedKeys.isEmpty()) { - if (config.getKeys() == null) { - config.setKeys(new KeyConfiguration()); - config.getKeys().setKeyData(new ArrayList<>()); - } - if (config.getKeys().getKeyData() == null) { - config.getKeys().setKeyData(new ArrayList<>()); - } - doPasswordStuff(config); - config.getKeys().getKeyData().addAll(newlyGeneratedKeys); - } - } - - if (!newlyGeneratedKeys.isEmpty()) { - // we have generated new keys, so we need to output the new configuration - output(commandLine, config); - } - - ConfigFileStore.create(path); - } - - return config; - } - - private void output(CommandLine commandLine, Config config) throws IOException { - - if (commandLine.hasOption("output")) { - final Path outputConfigFile = Paths.get(commandLine.getOptionValue("output")); - - try (OutputStream out = filesDelegate.newOutputStream(outputConfigFile, CREATE_NEW)) { - JaxbUtil.marshal(config, out); - } - } else { - JaxbUtil.marshal(config, SystemAdapter.INSTANCE.out()); - } - } - - // create a file if it doesn't exist and set the permissions to be only - // read/write for the creator - private void createFile(Path fileToMake) { - boolean notExists = filesDelegate.notExists(fileToMake); - if (notExists) { - filesDelegate.createFile(fileToMake); - filesDelegate.setPosixFilePermissions(fileToMake, NEW_PASSWORD_FILE_PERMS); - } - } - - public Config doPasswordStuff(Config config) throws ConfigException { - final List newPasswords = - newlyGeneratedKeys.stream().map(ConfigKeyPair::getPassword).collect(Collectors.toList()); - - boolean hasNewPasswords = newPasswords.stream().anyMatch(p -> Objects.nonNull(p) && !p.isEmpty()); - boolean isUsingPasswordFile = Objects.nonNull(config.getKeys().getPasswordFile()); - - if (hasNewPasswords) { - if (!isUsingPasswordFile) { - throw new ConfigException(new RuntimeException(passwordsMessage)); - } - - Path passwordFile = config.getKeys().getPasswordFile(); - createFile(passwordFile); - filesDelegate.write(passwordFile, newPasswords, APPEND); - } - - return config; - } -} diff --git a/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/Parser.java b/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/Parser.java deleted file mode 100644 index e24f4ade24..0000000000 --- a/cli/cli-api/src/main/java/com/quorum/tessera/cli/parsers/Parser.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.quorum.tessera.cli.parsers; - -import org.apache.commons.cli.CommandLine; - -/** - * A parser that checks for CLI options and takes actions based upon them - *

- * The actions may have side-effects, and may choose to return a value - * - * @param The return type from parsing the CLI options - */ -public interface Parser { - - /** - * Parses the CLI arguments and performs actions based upon whether the - * arguments relevant to it are present or not - * - * @param commandLine the command line object that has parsed the configuration - * @return the output of the parser, if any - * @throws Exception if there is a problem with the supplied configuration, - * any exception could be thrown - */ - T parse(CommandLine commandLine) throws Exception; - -} diff --git a/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliDelegateTest.java b/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliDelegateTest.java index 315e003aa8..70f2bad34e 100644 --- a/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliDelegateTest.java +++ b/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliDelegateTest.java @@ -1,151 +1,30 @@ package com.quorum.tessera.cli; import com.quorum.tessera.config.Config; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import java.util.NoSuchElementException; - import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; public class CliDelegateTest { private final CliDelegate instance = CliDelegate.INSTANCE; - @Before - public void setUp() { - MockCliAdapter.reset(); - MockSubcommandCliAdapter.reset(); - MockAdminSubcommandCliAdapter.reset(); - } - - @After - public void onTearDown() { - MockCliAdapter.reset(); - MockSubcommandCliAdapter.reset(); - MockAdminSubcommandCliAdapter.reset(); - } - @Test public void createInstance() { assertThat(CliDelegate.instance()).isSameAs(instance); } - @Test - public void adminCliOptionCreatesAdminInstance() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - - int status = 111; - MockAdminSubcommandCliAdapter.setResult(new CliResult(status, true, null)); - - CliResult result = instance.execute("admin", "-configfile", "/path/to/file"); - - assertThat(result).isNotNull(); - assertThat(result.getStatus()).isEqualTo(status); - } - - @Test - public void standardCliOptionsCreatesConfigInstance() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - int status = 111; - Config config = new Config(); - MockCliAdapter.setResult(new CliResult(status, true, config)); - - CliResult result = instance.execute("-configfile", "path/to/file"); - - assertThat(result).isNotNull(); - assertThat(result.getStatus()).isEqualTo(status); - assertThat(result.getConfig().get()).isEqualTo(config); - } - - @Test - public void configFieldUpdatedAfterExecution() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - int status = 111; - Config config = new Config(); - MockCliAdapter.setResult(new CliResult(status, false, config)); - - instance.execute("-configfile", "/path/to/file"); - - assertThat(instance.getConfig()).isEqualTo(new Config()); - } - @Test(expected = IllegalStateException.class) - public void fetchConfigWithoutExecution() { + public void fetchConfigBeforeSet() { instance.getConfig(); } - // PicoCLI tests @Test - public void nonNullResultIsReturned() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - MockSubcommandCliAdapter.setType(CliType.ADMIN); - - final CliResult result = new CliResult(0, true, null); - MockSubcommandCliAdapter.setResult(result); - - assertThat(instance.execute("some-subcommand")).isSameAs(result); - } - - @Test - public void helpOptionGivenReturnsSuccessCliResult() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - - final CliResult expected = new CliResult(0, true, null); - assertThat(instance.execute("help")).isEqualToComparingFieldByField(expected); - } - - @Test - public void helpOptionGivenOnSubcommandReturnsSuccessCliResult() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - MockSubcommandCliAdapter.setType(CliType.ADMIN); - - final CliResult expected = new CliResult(0, true, null); - assertThat(instance.execute("some-subcommand", "help")).isEqualToComparingFieldByField(expected); - } - - @Test - public void exceptionFromCommandBubblesUp() { - System.setProperty("tessera.cli.type", CliType.ADMIN.name()); - MockCliAdapter.setType(CliType.ADMIN); - MockSubcommandCliAdapter.setType(CliType.ADMIN); - final Exception exception = new Exception(); - MockSubcommandCliAdapter.setExceptionToBeThrown(exception); - - final Throwable throwable = catchThrowable(() -> instance.execute("some-subcommand")); - - assertThat(throwable).isSameAs(exception); - - MockSubcommandCliAdapter.setExceptionToBeThrown(null); - MockSubcommandCliAdapter.setType(null); - System.clearProperty("tessera.cli.type"); - } - - @Test - public void noArgsDefaultsTonConfigCli() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - CliResult result = instance.execute(); - - assertThat(result).isNotNull(); - } - - @Test(expected = NoSuchElementException.class) - public void unknownType() throws Exception { - MockCliAdapter.setType(CliType.ENCLAVE); - MockSubcommandCliAdapter.setType(CliType.ENCLAVE); - CliResult result = instance.execute(); - } - - @Test - public void filterEnclaveFromSubcommand() throws Exception { - MockCliAdapter.setType(CliType.CONFIG); - MockSubcommandCliAdapter.setType(CliType.ENCLAVE); + public void fetchConfigAfterSet() { + Config config = new Config(); - // some-subcommand is not recognised as an option because the subcommand has been filtered due to its ENCLAVE type. Therefore, the standard help cli result should be expected - final CliResult expected = new CliResult(0, true, null); + instance.setConfig(config); - assertThat(instance.execute("some-subcommand", "help")).isEqualToComparingFieldByField(expected); + assertThat(instance.getConfig()).isEqualTo(config); } } diff --git a/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliResultTest.java b/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliResultTest.java index 4a41a58519..207f3f7726 100644 --- a/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliResultTest.java +++ b/cli/cli-api/src/test/java/com/quorum/tessera/cli/CliResultTest.java @@ -1,5 +1,6 @@ package com.quorum.tessera.cli; +import com.quorum.tessera.config.Config; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -13,4 +14,21 @@ public void isSuppressStartup() { assertThat(result.isSuppressStartup()).isEqualTo(expected); } + + @Test + public void getStatus() { + int expected = 1; + CliResult result = new CliResult(1, false, null); + + assertThat(result.getStatus()).isEqualTo(expected); + } + + @Test + public void getConfig() { + Config config = new Config(); + CliResult result = new CliResult(1, false, config); + + assertThat(result.getConfig()).isNotEmpty(); + assertThat(result.getConfig().get()).isEqualTo(config); + } } diff --git a/cli/cli-api/src/test/java/com/quorum/tessera/cli/parsers/ConfigurationParserTest.java b/cli/cli-api/src/test/java/com/quorum/tessera/cli/parsers/ConfigurationParserTest.java deleted file mode 100644 index fb8d4a16e8..0000000000 --- a/cli/cli-api/src/test/java/com/quorum/tessera/cli/parsers/ConfigurationParserTest.java +++ /dev/null @@ -1,602 +0,0 @@ -package com.quorum.tessera.cli.parsers; - -import com.quorum.tessera.config.Config; -import com.quorum.tessera.config.ConfigException; -import com.quorum.tessera.config.KeyConfiguration; -import com.quorum.tessera.config.keypairs.ConfigKeyPair; -import com.quorum.tessera.config.keypairs.DirectKeyPair; -import com.quorum.tessera.io.FilesDelegate; -import org.apache.commons.cli.CommandLine; -import org.junit.Before; -import org.junit.Test; - -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; - -import static com.quorum.tessera.cli.parsers.ConfigurationParser.NEW_PASSWORD_FILE_PERMS; -import static com.quorum.tessera.cli.parsers.ConfigurationParser.passwordsMessage; -import static com.quorum.tessera.test.util.ElUtil.createAndPopulatePaths; -import static com.quorum.tessera.test.util.ElUtil.createTempFileFromTemplate; -import static java.nio.file.StandardOpenOption.APPEND; -import static java.nio.file.StandardOpenOption.CREATE_NEW; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Mockito.*; - -public class ConfigurationParserTest { - - private CommandLine commandLine; - private FilesDelegate filesDelegate; - - @Before - public void setUp() { - commandLine = mock(CommandLine.class); - filesDelegate = mock(FilesDelegate.class); - } - - @Test - public void noConfigfileOptionThenDoNothing() throws Exception { - when(commandLine.hasOption("configfile")).thenReturn(false); - ConfigurationParser configParser = new ConfigurationParser(Collections.EMPTY_LIST, filesDelegate); - Config result = configParser.parse(commandLine); - - assertThat(result).isNull(); - - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void configReadFromFile() throws Exception { - Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - ConfigurationParser configParser = new ConfigurationParser(Collections.EMPTY_LIST); - Config result = configParser.parse(commandLine); - - assertThat(result).isNotNull(); - } - - @Test - public void configfileDoesNotExistThrowsException() { - String path = "does/not/exist.config"; - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(path); - ConfigurationParser configParser = new ConfigurationParser(Collections.EMPTY_LIST); - Throwable throwable = catchThrowable(() -> configParser.parse(commandLine)); - - assertThat(throwable).isInstanceOf(FileNotFoundException.class); - assertThat(throwable).hasMessage(path + " not found."); - } - - @Test - public void providingKeygenAndVaultOptionsThenConfigfileNotParsed() throws Exception { - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.hasOption("keygen")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - ConfigurationParser configParser = new ConfigurationParser(Collections.EMPTY_LIST); - Config result = configParser.parse(commandLine); - - assertThat(result).isNull(); - } - - @Test - public void providingKeygenOptionThenConfigfileIsParsed() { - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.hasOption("keygen")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(false); - - when(commandLine.getOptionValue("configfile")).thenReturn("some/path"); - - ConfigurationParser configParser = new ConfigurationParser(Collections.EMPTY_LIST); - Throwable throwable = catchThrowable(() -> configParser.parse(commandLine)); - - // FileNotFoundException thrown as "some/path" does not exist - assertThat(throwable).isInstanceOf(FileNotFoundException.class); - } - - @Test - public void providingVaultOptionThenConfigfileIsParsed() { - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.hasOption("keygen")).thenReturn(false); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - - when(commandLine.getOptionValue("configfile")).thenReturn("some/path"); - ConfigurationParser configParser = new ConfigurationParser(Collections.EMPTY_LIST); - Throwable throwable = catchThrowable(() -> configParser.parse(commandLine)); - - // FileNotFoundException thrown as "some/path" does not exist - assertThat(throwable).isInstanceOf(FileNotFoundException.class); - } - - @Test - public void providingNeitherKeygenOptionsThenConfigfileIsParsed() { - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.hasOption("keygen")).thenReturn(false); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(false); - - when(commandLine.getOptionValue("configfile")).thenReturn("some/path"); - ConfigurationParser configParser = new ConfigurationParser(Collections.EMPTY_LIST); - Throwable throwable = catchThrowable(() -> configParser.parse(commandLine)); - - // FileNotFoundException thrown as "some/path" does not exist - assertThat(throwable).isInstanceOf(FileNotFoundException.class); - } - - @Test - public void withNewKeysOutputsNewConfigToSystemAdapter() throws Exception { - Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - ConfigKeyPair newKey = new DirectKeyPair("pub", "priv"); - - FilesDelegate filesDelegate = mock(FilesDelegate.class); - FilesDelegate fd = new FilesDelegate() {}; - InputStream in = fd.newInputStream(configFile); - - when(filesDelegate.exists(configFile)).thenReturn(true); - when(filesDelegate.newInputStream(configFile)).thenReturn(in); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey), filesDelegate); - Config result = configParser.parse(commandLine); - - in.close(); - assertThat(result).isNotNull(); - assertThat(result.getKeys().getKeyData()).contains(newKey); - - verify(filesDelegate).exists(configFile); - verify(filesDelegate).newInputStream(configFile); - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void withNewKeysAndOutputOptionWritesNewConfigToFile() throws Exception { - Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - when(commandLine.hasOption("output")).thenReturn(true); - - Path output = Paths.get(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString() + ".conf"); - - when(commandLine.getOptionValue("output")).thenReturn(output.toString()); - - ConfigKeyPair newKey = new DirectKeyPair("pub", "priv"); - - FilesDelegate filesDelegate = mock(FilesDelegate.class); - FilesDelegate fd = new FilesDelegate() {}; - InputStream in = fd.newInputStream(configFile); - - when(filesDelegate.exists(configFile)).thenReturn(true); - when(filesDelegate.newInputStream(configFile)).thenReturn(in); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - when(filesDelegate.newOutputStream(output, CREATE_NEW)).thenReturn(os); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey), filesDelegate); - - Config result = configParser.parse(commandLine); - - in.close(); - - assertThat(result).isNotNull(); - assertThat(result.getKeys().getKeyData()).contains(newKey); - - verify(filesDelegate).exists(configFile); - verify(filesDelegate).newInputStream(configFile); - verify(filesDelegate).newOutputStream(output, CREATE_NEW); - verifyNoMoreInteractions(filesDelegate); - - byte[] bytesOut = os.toByteArray(); - assertThat(bytesOut).isNotEmpty(); - } - - @Test - public void withNewKeysAndNullKeyConfig() throws Exception { - - Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config-no-keyconfig.json")); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - when(commandLine.hasOption("output")).thenReturn(true); - - String tempDir = System.getProperty("java.io.tmpdir"); - - Path output = Paths.get(tempDir, UUID.randomUUID().toString() + ".conf"); - - when(commandLine.getOptionValue("output")).thenReturn(output.toString()); - - ConfigKeyPair newKey = new DirectKeyPair("pub", "priv"); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey)); - - Config result = configParser.parse(commandLine); - - assertThat(result).isNotNull(); - assertThat(result.getKeys().getKeyData()).contains(newKey); - - assertThat(output).exists(); - output.toFile().deleteOnExit(); - } - - @Test - public void withNewPasswordProtectedKeysAndPasswordsListInConfigThrowsException() throws Exception { - Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - when(commandLine.hasOption("output")).thenReturn(true); - - String tempDir = System.getProperty("java.io.tmpdir"); - - Path output = Paths.get(tempDir, UUID.randomUUID().toString() + ".conf"); - - when(commandLine.getOptionValue("output")).thenReturn(output.toString()); - - FilesDelegate fd = new FilesDelegate() {}; - InputStream in = fd.newInputStream(configFile); - - when(filesDelegate.exists(configFile)).thenReturn(true); - when(filesDelegate.newInputStream(configFile)).thenReturn(in); - - ConfigKeyPair newKey = mock(ConfigKeyPair.class); - when(newKey.getPassword()).thenReturn("A TEST PASSWORD"); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey), filesDelegate); - - Throwable ex = catchThrowable(() -> configParser.parse(commandLine)); - - in.close(); - - assertThat(ex).isExactlyInstanceOf(ConfigException.class); - assertThat(ex.getMessage()).contains(passwordsMessage); - - verify(filesDelegate).exists(configFile); - verify(filesDelegate).newInputStream(configFile); - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void withNewPasswordProtectedKeysAndNullKeyConfigThrowsException() throws Exception { - Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config-no-keyconfig.json")); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - when(commandLine.hasOption("output")).thenReturn(true); - - String tempDir = System.getProperty("java.io.tmpdir"); - - Path output = Paths.get(tempDir, UUID.randomUUID().toString() + ".conf"); - - when(commandLine.getOptionValue("output")).thenReturn(output.toString()); - - FilesDelegate fd = new FilesDelegate() {}; - InputStream in = fd.newInputStream(configFile); - - when(filesDelegate.exists(configFile)).thenReturn(true); - when(filesDelegate.newInputStream(configFile)).thenReturn(in); - - ConfigKeyPair newKey = mock(ConfigKeyPair.class); - when(newKey.getPassword()).thenReturn("A TEST PASSWORD"); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey), filesDelegate); - - Throwable ex = catchThrowable(() -> configParser.parse(commandLine)); - - in.close(); - - assertThat(ex).isExactlyInstanceOf(ConfigException.class); - assertThat(ex.getMessage()).contains(passwordsMessage); - - verify(filesDelegate).exists(configFile); - verify(filesDelegate).newInputStream(configFile); - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void withNewPasswordProtectedKeysAndConfigExistingPasswordFileInConfigUpdatesConfigfileAndPasswordFile() - throws Exception { - - Path unixSocketPath = Files.createTempFile(UUID.randomUUID().toString(), ".ipc"); - Path passwordFilePath = Files.createTempFile(UUID.randomUUID().toString(), ".pwds"); - - Map params = new HashMap<>(); - params.put("unixSocketPath", unixSocketPath.toString()); - params.put("passwordFilePath", passwordFilePath.toString()); - - Path configFile = - createTempFileFromTemplate(getClass().getResource("/sample-config-password-file.json"), params); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - when(commandLine.hasOption("output")).thenReturn(true); - - String tempDir = System.getProperty("java.io.tmpdir"); - - Path output = Paths.get(tempDir, UUID.randomUUID().toString() + ".conf"); - - when(commandLine.getOptionValue("output")).thenReturn(output.toString()); - - FilesDelegate fd = new FilesDelegate() {}; - InputStream in = fd.newInputStream(configFile); - - when(filesDelegate.exists(configFile)).thenReturn(true); - when(filesDelegate.notExists(passwordFilePath)).thenReturn(false); - when(filesDelegate.newInputStream(configFile)).thenReturn(in); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - when(filesDelegate.newOutputStream(output, CREATE_NEW)).thenReturn(os); - - ConfigKeyPair newKey = mock(ConfigKeyPair.class); - final String testPassword = "A TEST PASSWORD"; - when(newKey.getPassword()).thenReturn(testPassword); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey), filesDelegate); - - Config result = configParser.parse(commandLine); - - in.close(); - - assertThat(result).isNotNull(); - - verify(filesDelegate).exists(configFile); - verify(filesDelegate).notExists(passwordFilePath); - verify(filesDelegate).newInputStream(configFile); - verify(filesDelegate).newOutputStream(output, CREATE_NEW); - verify(filesDelegate).write(passwordFilePath, Arrays.asList(testPassword), APPEND); - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void withNewPasswordProtectedKeysPasswordFileOnlyInKeyConfigUpdatesConfigfileAndPasswordFile() - throws Exception { - - Path unixSocketPath = Files.createTempFile(UUID.randomUUID().toString(), ".ipc"); - Path passwordFilePath = Files.createTempFile(UUID.randomUUID().toString(), ".pwds"); - - Map params = new HashMap<>(); - params.put("unixSocketPath", unixSocketPath.toString()); - params.put("passwordFilePath", passwordFilePath.toString()); - - Path configFile = - createTempFileFromTemplate(getClass().getResource("/sample-config-password-file-only.json"), params); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - when(commandLine.hasOption("output")).thenReturn(true); - - String tempDir = System.getProperty("java.io.tmpdir"); - - Path output = Paths.get(tempDir, UUID.randomUUID().toString() + ".conf"); - - when(commandLine.getOptionValue("output")).thenReturn(output.toString()); - - FilesDelegate fd = new FilesDelegate() {}; - InputStream in = fd.newInputStream(configFile); - - when(filesDelegate.exists(configFile)).thenReturn(true); - when(filesDelegate.notExists(passwordFilePath)).thenReturn(false); - when(filesDelegate.newInputStream(configFile)).thenReturn(in); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - when(filesDelegate.newOutputStream(output, CREATE_NEW)).thenReturn(os); - - ConfigKeyPair newKey = mock(ConfigKeyPair.class); - final String testPassword = "A TEST PASSWORD"; - when(newKey.getPassword()).thenReturn(testPassword); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey), filesDelegate); - - Config result = configParser.parse(commandLine); - - in.close(); - - assertThat(result).isNotNull(); - - verify(filesDelegate).exists(configFile); - verify(filesDelegate).notExists(passwordFilePath); - verify(filesDelegate).newInputStream(configFile); - verify(filesDelegate).newOutputStream(output, CREATE_NEW); - verify(filesDelegate).write(passwordFilePath, Arrays.asList(testPassword), APPEND); - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void withNewPasswordProtectedKeysAndNonExistingPasswordFileInConfigUpdatesConfigfileAndCreatesPasswordFile() - throws Exception { - - Path unixSocketPath = Files.createTempFile(UUID.randomUUID().toString(), ".ipc"); - Path passwordFilePath = Files.createTempFile(UUID.randomUUID().toString(), ".pwds"); - - Map params = new HashMap<>(); - params.put("unixSocketPath", unixSocketPath.toString()); - params.put("passwordFilePath", passwordFilePath.toString()); - - Path configFile = - createTempFileFromTemplate(getClass().getResource("/sample-config-password-file.json"), params); - - configFile.toFile().deleteOnExit(); - - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn(configFile.toString()); - - when(commandLine.hasOption("output")).thenReturn(true); - - String tempDir = System.getProperty("java.io.tmpdir"); - - Path output = Paths.get(tempDir, UUID.randomUUID().toString() + ".conf"); - - when(commandLine.getOptionValue("output")).thenReturn(output.toString()); - - FilesDelegate fd = new FilesDelegate() {}; - InputStream in = fd.newInputStream(configFile); - - when(filesDelegate.exists(configFile)).thenReturn(true); - when(filesDelegate.notExists(passwordFilePath)).thenReturn(true); - when(filesDelegate.newInputStream(configFile)).thenReturn(in); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - when(filesDelegate.newOutputStream(output, CREATE_NEW)).thenReturn(os); - - ConfigKeyPair newKey = mock(ConfigKeyPair.class); - final String testPassword = "A TEST PASSWORD"; - when(newKey.getPassword()).thenReturn(testPassword); - - ConfigurationParser configParser = new ConfigurationParser(Arrays.asList(newKey), filesDelegate); - - Config result = configParser.parse(commandLine); - - in.close(); - - assertThat(result).isNotNull(); - - verify(filesDelegate).exists(configFile); - verify(filesDelegate).notExists(passwordFilePath); - verify(filesDelegate).createFile(passwordFilePath); - verify(filesDelegate).setPosixFilePermissions(passwordFilePath, NEW_PASSWORD_FILE_PERMS); - verify(filesDelegate).newInputStream(configFile); - verify(filesDelegate).newOutputStream(output, CREATE_NEW); - verify(filesDelegate).write(passwordFilePath, Arrays.asList(testPassword), APPEND); - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void doPasswordStuffWithEmptyPasswordsElement() throws Exception { - - final String password = "I LOVE SPARROWS!"; - final ConfigKeyPair newKey = mock(ConfigKeyPair.class); - when(newKey.getPassword()).thenReturn(password); - - final List newKeys = Arrays.asList(newKey); - - FilesDelegate filesDelegate = mock(FilesDelegate.class); - - final ConfigurationParser configParser = new ConfigurationParser(newKeys, filesDelegate); - - Config config = mock(Config.class); - KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); - when(keyConfiguration.getPasswords()).thenReturn(new ArrayList<>()); - when(config.getKeys()).thenReturn(keyConfiguration); - - Throwable ex = catchThrowable(() -> configParser.doPasswordStuff(config)); - - assertThat(ex).isInstanceOf(ConfigException.class); - verifyZeroInteractions(filesDelegate); - } - - @Test - public void doPasswordStuffWithPasswordFileDefined() throws Exception { - - final String password = "I LOVE SPARROWS!"; - - final ConfigKeyPair newKey = mock(ConfigKeyPair.class); - when(newKey.getPassword()).thenReturn(password); - - FilesDelegate filesDelegate = mock(FilesDelegate.class); - - final List newKeys = Arrays.asList(newKey); - - final ConfigurationParser configParser = new ConfigurationParser(newKeys, filesDelegate); - - Config config = mock(Config.class); - KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); - when(keyConfiguration.getPasswords()).thenReturn(null); - - when(config.getKeys()).thenReturn(keyConfiguration); - - final Path passwordFile = mock(Path.class); - when(keyConfiguration.getPasswordFile()).thenReturn(passwordFile); - - when(filesDelegate.notExists(passwordFile)).thenReturn(true); - - when(filesDelegate.setPosixFilePermissions(passwordFile, NEW_PASSWORD_FILE_PERMS)).thenReturn(passwordFile); - - Config result = configParser.doPasswordStuff(config); - - assertThat(result).isSameAs(config); - - verify(filesDelegate).notExists(passwordFile); - verify(filesDelegate).setPosixFilePermissions(passwordFile, NEW_PASSWORD_FILE_PERMS); - verify(filesDelegate).createFile(passwordFile); - - verify(filesDelegate).write(passwordFile, Arrays.asList(password), APPEND); - - verifyNoMoreInteractions(filesDelegate); - } - - @Test - public void doPasswordStuffNewPasswordsOnly() { - final String password = "I LOVE SPARROWS!"; - - final ConfigKeyPair newKey = mock(ConfigKeyPair.class); - when(newKey.getPassword()).thenReturn(password); - - FilesDelegate filesDelegate = mock(FilesDelegate.class); - - final List newKeys = Arrays.asList(newKey); - - final ConfigurationParser configParser = new ConfigurationParser(newKeys, filesDelegate); - - Config config = mock(Config.class); - - KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); - when(keyConfiguration.getKeyData()).thenReturn(Collections.emptyList()); - - when(keyConfiguration.getPasswords()).thenReturn(null); - when(keyConfiguration.getPasswordFile()).thenReturn(null); - when(config.getKeys()).thenReturn(keyConfiguration); - - Throwable ex = catchThrowable(() -> configParser.doPasswordStuff(config)); - - assertThat(ex).isInstanceOf(ConfigException.class); - verifyZeroInteractions(filesDelegate); - } - - @Test - public void doPasswordStuffNoNewPasswords() { - - FilesDelegate filesDelegate = mock(FilesDelegate.class); - - final ConfigurationParser configParser = new ConfigurationParser(Collections.emptyList(), filesDelegate); - - Config config = mock(Config.class); - KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); - when(keyConfiguration.getPasswordFile()).thenReturn(null); - when(keyConfiguration.getPasswords()).thenReturn(null); - - when(config.getKeys()).thenReturn(keyConfiguration); - - Config result = configParser.doPasswordStuff(config); - assertThat(result).isSameAs(config); - } -} diff --git a/cli/config-cli/build.gradle b/cli/config-cli/build.gradle index 7c8743cb68..a2d728af80 100644 --- a/cli/config-cli/build.gradle +++ b/cli/config-cli/build.gradle @@ -1,12 +1,13 @@ dependencies { - compile 'info.picocli:picocli' - compile 'commons-cli:commons-cli:1.4' + compile 'info.picocli:picocli:4.0' compile project(':encryption:encryption-api') compile project(':config') compile project(':shared') compile project(':cli:cli-api') compile project(':key-generation') + compile project(':tessera-jaxrs:jaxrs-client') + compile 'javax.ws.rs:javax.ws.rs-api' runtimeOnly project(':encryption:encryption-jnacl') diff --git a/cli/config-cli/pom.xml b/cli/config-cli/pom.xml index a3a1a0892a..35c9e257d4 100644 --- a/cli/config-cli/pom.xml +++ b/cli/config-cli/pom.xml @@ -14,10 +14,16 @@ cli-api + + com.jpmorgan.quorum + jaxrs-client + + com.jpmorgan.quorum key-generation + diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/ArgonOptionsConverter.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/ArgonOptionsConverter.java new file mode 100644 index 0000000000..875d4cbb27 --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/ArgonOptionsConverter.java @@ -0,0 +1,27 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.ArgonOptions; +import com.quorum.tessera.config.util.JaxbUtil; +import picocli.CommandLine; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ArgonOptionsConverter implements CommandLine.ITypeConverter { + + @Override + public ArgonOptions convert(String value) throws Exception { + final Path path = Paths.get(value); + + if (!Files.exists(path)) { + throw new FileNotFoundException(String.format("%s not found.", path)); + } + + try (InputStream in = Files.newInputStream(path)) { + return JaxbUtil.unmarshal(in, ArgonOptions.class); + } + } +} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java deleted file mode 100644 index 8ed7a941f1..0000000000 --- a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java +++ /dev/null @@ -1,300 +0,0 @@ -package com.quorum.tessera.config.cli; - -import com.quorum.tessera.ServiceLoaderUtil; -import com.quorum.tessera.cli.CliAdapter; -import com.quorum.tessera.cli.CliException; -import com.quorum.tessera.cli.CliResult; -import com.quorum.tessera.cli.CliType; -import com.quorum.tessera.cli.keypassresolver.CliKeyPasswordResolver; -import com.quorum.tessera.cli.keypassresolver.KeyPasswordResolver; -import com.quorum.tessera.cli.parsers.ConfigurationParser; -import com.quorum.tessera.cli.parsers.PidFileMixin; -import com.quorum.tessera.config.Config; -import com.quorum.tessera.config.EncryptorConfig; -import com.quorum.tessera.config.cli.parsers.EncryptorConfigParser; -import com.quorum.tessera.config.cli.parsers.KeyGenerationParser; -import com.quorum.tessera.config.cli.parsers.KeyUpdateParser; -import com.quorum.tessera.config.keypairs.ConfigKeyPair; -import com.quorum.tessera.config.keys.KeyEncryptor; -import com.quorum.tessera.config.keys.KeyEncryptorFactory; -import com.quorum.tessera.passwords.PasswordReaderFactory; -import org.apache.commons.cli.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Validation; -import javax.validation.Validator; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.*; -import java.util.concurrent.Callable; - -@picocli.CommandLine.Command -public class DefaultCliAdapter implements CliAdapter, Callable { - - private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCliAdapter.class); - - private final KeyPasswordResolver keyPasswordResolver; - - private final Validator validator = - Validation.byDefaultProvider().configure().ignoreXmlConfiguration().buildValidatorFactory().getValidator(); - - @picocli.CommandLine.Mixin private PidFileMixin pidFileMixin = new PidFileMixin(); - - @picocli.CommandLine.Unmatched private String[] allParameters = new String[0]; - - public DefaultCliAdapter() { - this(ServiceLoaderUtil.load(KeyPasswordResolver.class).orElse(new CliKeyPasswordResolver())); - } - - public DefaultCliAdapter(final KeyPasswordResolver keyPasswordResolver) { - this.keyPasswordResolver = Objects.requireNonNull(keyPasswordResolver); - } - - @Override - public CliResult call() throws Exception { - return this.execute(allParameters); - } - - @Override - public CliType getType() { - return CliType.CONFIG; - } - - @Override - public CliResult execute(String... args) throws Exception { - - Options options = this.buildBaseOptions(); - - Map overrideOptions = OverrideUtil.buildConfigOptions(); - - overrideOptions.forEach( - (optionName, optionType) -> { - final boolean isCollection = optionType.isArray(); - - Option.Builder optionBuilder = - Option.builder() - .longOpt(optionName) - .desc( - String.format( - "Override option for %s , type: %s", - optionName, optionType.getSimpleName())); - - if (isCollection) { - optionBuilder.hasArgs().argName(optionType.getSimpleName().toUpperCase() + "..."); - } else { - optionBuilder.hasArg().argName(optionType.getSimpleName().toUpperCase()); - } - - options.addOption(optionBuilder.build()); - }); - - final List argsList = Arrays.asList(args); - if (argsList.contains("help") || argsList.isEmpty()) { - HelpFormatter formatter = new HelpFormatter(); - PrintWriter pw = new PrintWriter(sys().out()); - formatter.printHelp( - pw, - 200, - "tessera -configfile [-keygen ] [-pidfile ]", - null, - options, - formatter.getLeftPadding(), - formatter.getDescPadding(), - null, - false); - pw.flush(); - return new CliResult(0, true, null); - } - - try { - - final CommandLine line = new DefaultParser().parse(options, args); - - final Config config = parseConfig(line); - - if (Objects.nonNull(config)) { - - overrideOptions.forEach( - (optionName, value) -> { - if (line.hasOption(optionName)) { - String[] values = line.getOptionValues(optionName); - LOGGER.debug("Setting : {} with value(s) {}", optionName, values); - OverrideUtil.setValue(config, optionName, values); - LOGGER.debug("Set : {} with value(s) {}", optionName, values); - } - }); - - final Set> violations = validator.validate(config); - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); - } - - keyPasswordResolver.resolveKeyPasswords(config); - } - - pidFileMixin.call(); - - boolean suppressStartup = line.hasOption("keygen") && Objects.isNull(config); - - return new CliResult(0, suppressStartup, config); - - } catch (ParseException exp) { - throw new CliException(exp.getMessage()); - } - } - - private Config parseConfig(CommandLine commandLine) throws IOException { - - if (!commandLine.hasOption("configfile") - && !commandLine.hasOption("keygen") - && !commandLine.hasOption("updatepassword")) { - throw new CliException("One or more: -configfile or -keygen or -updatepassword options are required."); - } - - EncryptorConfig encryptorConfig = new EncryptorConfigParser().parse(commandLine); - KeyEncryptorFactory keyEncryptorFactory = KeyEncryptorFactory.newFactory(); - KeyEncryptor keyEncryptor = keyEncryptorFactory.create(encryptorConfig); - // Handle update password stuff - if (commandLine.hasOption("updatepassword")) { - - if (!commandLine.hasOption("encryptor.type")) { - System.out.println("No encryptor type defined NACL will be used as default"); - } - - new KeyUpdateParser(keyEncryptor, PasswordReaderFactory.create()).parse(commandLine); - - // return early so other options don't get processed - return null; - } // end update password stuff - - final List newKeys = new KeyGenerationParser(encryptorConfig).parse(commandLine); - - final Config config = new ConfigurationParser(newKeys).parse(commandLine); - Optional.ofNullable(config).ifPresent(c -> c.setEncryptor(encryptorConfig)); - return config; - } - - private Options buildBaseOptions() { - - final Options options = new Options(); - - options.addOption( - Option.builder("configfile") - .desc("Path to node configuration file") - .hasArg(true) - .optionalArg(false) - .numberOfArgs(1) - .argName("PATH") - .build()); - - options.addOption( - Option.builder("keygen") - .desc("Use this option to generate public/private keypair") - .hasArg(false) - .build()); - - options.addOption( - Option.builder("filename") - .desc( - "Comma-separated list of paths to save generated key files. Can also be used with keyvault. Number of args equals number of key-pairs generated.") - .hasArgs() - .optionalArg(false) - .argName("PATH") - .build()); - - options.addOption( - Option.builder("keygenconfig") - .desc("Path to private key config for generation of missing key files") - .hasArg(true) - .optionalArg(false) - .argName("PATH") - .build()); - - options.addOption( - Option.builder("output") - .desc("Generate updated config file with generated keys") - .hasArg(true) - .numberOfArgs(1) - .build()); - - options.addOption( - Option.builder("keygenvaulttype") - .desc("Type of key vault the generated key is to be saved in") - .hasArg() - .optionalArg(false) - .argName("KEYVAULTTYPE") - .build()); - - options.addOption( - Option.builder("keygenvaulturl") - .desc("Base url for key vault") - .hasArg() - .optionalArg(false) - .argName("STRING") - .build()); - - options.addOption( - Option.builder("keygenvaultapprole") - .desc("AppRole path for Hashicorp Vault authentication (defaults to 'approle')") - .hasArg() - .optionalArg(false) - .argName("STRING") - .build()); - - options.addOption( - Option.builder("keygenvaultkeystore") - .desc("Path to JKS keystore for TLS Hashicorp Vault communication") - .hasArg() - .optionalArg(false) - .argName("PATH") - .build()); - - options.addOption( - Option.builder("keygenvaulttruststore") - .desc("Path to JKS truststore for TLS Hashicorp Vault communication") - .hasArg() - .optionalArg(false) - .argName("PATH") - .build()); - - options.addOption( - Option.builder("keygenvaultsecretengine") - .desc("Name of already enabled Hashicorp v2 kv secret engine") - .hasArg() - .optionalArg(false) - .argName("STRING") - .build()); - - // Moved already to PicoCLI, but kept here for the help option - options.addOption( - Option.builder("pidfile") - .desc("Path to pid file") - .hasArg(true) - .optionalArg(false) - .numberOfArgs(1) - .argName("PATH") - .build()); - - options.addOption( - Option.builder("updatepassword").desc("Update the password for a locked key").hasArg(false).build()); - - options.addOption( - Option.builder() - .longOpt("encryptor.type") - .desc("Encryptor type NACL or EC") - .hasArg(true) - .optionalArg(false) - .numberOfArgs(1) - .build()); - - return options; - } - - // TODO: for testing, remove if possible - public void setAllParameters(final String[] allParameters) { - this.allParameters = allParameters; - } -} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/EncryptorOptions.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/EncryptorOptions.java new file mode 100644 index 0000000000..a1af182c1d --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/EncryptorOptions.java @@ -0,0 +1,53 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.EncryptorConfig; +import com.quorum.tessera.config.EncryptorType; +import picocli.CommandLine; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +class EncryptorOptions { + + @CommandLine.Option( + names = {"--encryptor.type"}, + description = "Valid values: ${COMPLETION-CANDIDATES}") + EncryptorType type; + + @CommandLine.Option(names = {"--encryptor.symmetricCipher"}) + String symmetricCipher; + + @CommandLine.Option(names = {"--encryptor.ellipticCurve"}) + String ellipticCurve; + + @CommandLine.Option(names = {"--encryptor.nonceLength"}) + String nonceLength; + + @CommandLine.Option(names = {"--encryptor.sharedKeyLength"}) + String sharedKeyLength; + + EncryptorConfig parseEncryptorConfig() { + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + + // we set the default here instead of in the option annotation as enum values cannot be used in annotations + if (Objects.isNull(type)) { + type = EncryptorType.NACL; + } + + Map properties = new HashMap<>(); + if (type == EncryptorType.EC) { + + Optional.ofNullable(symmetricCipher).ifPresent(v -> properties.put("symmetricCipher", v)); + Optional.ofNullable(ellipticCurve).ifPresent(v -> properties.put("ellipticCurve", v)); + Optional.ofNullable(nonceLength).ifPresent(v -> properties.put("nonceLength", v)); + Optional.ofNullable(sharedKeyLength).ifPresent(v -> properties.put("sharedKeyLength", v)); + } + + encryptorConfig.setType(type); + encryptorConfig.setProperties(properties); + + return encryptorConfig; + } +} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommand.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommand.java new file mode 100644 index 0000000000..d74062c888 --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommand.java @@ -0,0 +1,181 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.cli.CliException; +import com.quorum.tessera.cli.CliResult; +import com.quorum.tessera.config.*; +import com.quorum.tessera.key.generation.KeyGenerator; +import com.quorum.tessera.key.generation.KeyGeneratorFactory; +import com.quorum.tessera.key.generation.KeyVaultOptions; +import picocli.CommandLine; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; + +@CommandLine.Command( + name = "keygen", + aliases = {"-keygen"}, + headerHeading = "Usage:%n%n", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + parameterListHeading = "%nParameters:%n", + optionListHeading = "%nOptions:%n", + header = "Generate Tessera encryption keys", + abbreviateSynopsis = true, + subcommands = {CommandLine.HelpCommand.class}) +public class KeyGenCommand implements Callable { + private KeyGeneratorFactory factory; + + private final Validator validator = + Validation.byDefaultProvider().configure().ignoreXmlConfiguration().buildValidatorFactory().getValidator(); + + // TODO(cjh) changes have been made to CLI option descriptions. should these be raised as a separate change ? + + @CommandLine.Option( + names = {"--keyout", "-filename"}, + split = ",", + description = + "Comma-separated list of paths to save generated key files. Can also be used with keyvault. Number of args determines number of key-pairs generated (default = ${DEFAULT-VALUE})") + public List keyOut; + + @CommandLine.Option( + names = {"--argonconfig", "-keygenconfig"}, + description = + "File containing Argon2 encryption config used to secure the new private key when storing to the filesystem") + public ArgonOptions argonOptions; + + @CommandLine.Option( + names = {"--vault.type", "-keygenvaulttype"}, + description = + "Specify the key vault provider the generated key is to be saved in. If not set, the key will be encrypted and stored on the local filesystem. Valid values: ${COMPLETION-CANDIDATES})") + public KeyVaultType vaultType; + + @CommandLine.Option( + names = {"--vault.url", "-keygenvaulturl"}, + description = "Base url for key vault") + public String vaultUrl; + + @CommandLine.Option( + names = {"--vault.hashicorp.approlepath", "-keygenvaultapprole"}, + description = "AppRole path for Hashicorp Vault authentication (defaults to 'approle')") + public String hashicorpApprolePath; + + @CommandLine.Option( + names = {"--vault.hashicorp.secretenginepath", "-keygenvaultsecretengine"}, + description = "Name of already enabled Hashicorp v2 kv secret engine") + public String hashicorpSecretEnginePath; + + @CommandLine.Option( + names = {"--vault.hashicorp.tlskeystore", "-keygenvaultkeystore"}, + description = "Path to JKS keystore for TLS Hashicorp Vault communication") + public Path hashicorpTlsKeystore; + + @CommandLine.Option( + names = {"--vault.hashicorp.tlstruststore", "-keygenvaulttruststore"}, + description = "Path to JKS truststore for TLS Hashicorp Vault communication") + public Path hashicorpTlsTruststore; + + @CommandLine.Option( + names = {"--configfile", "-configfile"}, + description = "Path to node configuration file") + public Config config; + + // TODO(cjh) implement config output and password file update ? + // we've removed the ability to start the node straight away after generating keys. Not sure if updating + // configfile + // and password file is something we want to still support or put onus on users to go and update as required + @CommandLine.Option( + names = {"--configout", "-output"}, + description = + "Path to save updated configfile to. Updated config will be printed to terminal if not provided. Only valid if --configfile option also provided.") + public List configOut; + + @CommandLine.Mixin public EncryptorOptions encryptorOptions; + + KeyGenCommand(KeyGeneratorFactory keyGeneratorFactory) { + this.factory = keyGeneratorFactory; + } + + // TODO(cjh) 'tessera keygen' with no args prints help. this is consistent with the other commands' behaviour, but + // previously this would generate keys at the default location '.'. do we want to reintroduce this or keep + // consistency with the other commands? + @Override + public CliResult call() { + final EncryptorConfig encryptorConfig; + + if (Optional.ofNullable(config).map(Config::getEncryptor).isPresent()) { + encryptorConfig = config.getEncryptor(); + } else { + encryptorConfig = encryptorOptions.parseEncryptorConfig(); + } + + final KeyVaultOptions keyVaultOptions = this.keyVaultOptions().orElse(null); + final KeyVaultConfig keyVaultConfig = this.keyVaultConfig().orElse(null); + + final KeyGenerator generator = factory.create(keyVaultConfig, encryptorConfig); + + if (Objects.isNull(keyOut) || keyOut.isEmpty()) { + generator.generate("", argonOptions, keyVaultOptions); + } else { + keyOut.forEach(name -> generator.generate(name, argonOptions, keyVaultOptions)); + } + + return new CliResult(0, true, null); + } + + private Optional keyVaultOptions() { + if (Objects.isNull(hashicorpSecretEnginePath)) { + return Optional.empty(); + } + + return Optional.of(new KeyVaultOptions(hashicorpSecretEnginePath)); + } + + private Optional keyVaultConfig() { + if (Objects.isNull(vaultType) && Objects.isNull(vaultUrl)) { + return Optional.empty(); + } + + if (Objects.isNull(vaultType)) { + throw new CliException("Key vault type either not provided or not recognised"); + } + + final KeyVaultConfig keyVaultConfig; + + if (vaultType.equals(KeyVaultType.AZURE)) { + keyVaultConfig = new AzureKeyVaultConfig(vaultUrl); + + Set> violations = + validator.validate((AzureKeyVaultConfig) keyVaultConfig); + + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } else { + if (Objects.isNull(keyOut) || keyOut.isEmpty()) { + throw new CliException( + "At least one -filename must be provided when saving generated keys in a Hashicorp Vault"); + } + + keyVaultConfig = + new HashicorpKeyVaultConfig( + vaultUrl, hashicorpApprolePath, hashicorpTlsKeystore, hashicorpTlsTruststore); + + Set> violations = + validator.validate((HashicorpKeyVaultConfig) keyVaultConfig); + + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } + + return Optional.of(keyVaultConfig); + } +} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommandFactory.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommandFactory.java new file mode 100644 index 0000000000..b7761717f2 --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommandFactory.java @@ -0,0 +1,23 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.key.generation.KeyGeneratorFactory; +import picocli.CommandLine; + +public class KeyGenCommandFactory implements CommandLine.IFactory { + + @Override + public K create(Class cls) throws Exception { + try { + if (cls != KeyGenCommand.class) { + throw new RuntimeException( + this.getClass().getSimpleName() + " cannot create instance of type " + cls.getSimpleName()); + } + + KeyGeneratorFactory keyGeneratorFactory = KeyGeneratorFactory.newFactory(); + + return (K) new KeyGenCommand(keyGeneratorFactory); + } catch (Exception e) { + return CommandLine.defaultFactory().create(cls); // fallback if missing + } + } +} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyUpdateCommand.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyUpdateCommand.java new file mode 100644 index 0000000000..2a76b718c3 --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyUpdateCommand.java @@ -0,0 +1,178 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.cli.CliException; +import com.quorum.tessera.cli.CliResult; +import com.quorum.tessera.config.*; +import com.quorum.tessera.config.keys.KeyEncryptor; +import com.quorum.tessera.config.keys.KeyEncryptorFactory; +import com.quorum.tessera.config.util.JaxbUtil; +import com.quorum.tessera.encryption.PrivateKey; +import com.quorum.tessera.io.SystemAdapter; +import com.quorum.tessera.passwords.PasswordReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +@CommandLine.Command( + name = "keyupdate", + aliases = {"-updatepassword"}, + headerHeading = "Usage:%n%n", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + parameterListHeading = "%nParameters:%n", + optionListHeading = "%nOptions:%n", + header = "Update the password for a key", + description = + "Change the password or update the encryption options for an already locked key, or apply a new password to an unlocked key", + subcommands = {CommandLine.HelpCommand.class}, + abbreviateSynopsis = true) +public class KeyUpdateCommand implements Callable { + + private static final Logger LOGGER = LoggerFactory.getLogger(KeyUpdateCommand.class); + + @CommandLine.Option(names = "--keys.keyData.privateKeyPath", required = true) + public Path privateKeyPath; + + static String invalidArgonAlgorithmMsg = + "Allowed values for --keys.keyData.config.data.aopts.algorithm are 'i', 'd' or 'id'"; + + @CommandLine.Option(names = "--keys.keyData.config.data.aopts.algorithm", defaultValue = "i") + public String algorithm; + + @CommandLine.Option(names = "--keys.keyData.config.data.aopts.iterations", defaultValue = "10") + public Integer iterations; + + @CommandLine.Option(names = "--keys.keyData.config.data.aopts.memory", defaultValue = "1048576") + public Integer memory; + + @CommandLine.Option(names = "--keys.keyData.config.data.aopts.parallelism", defaultValue = "4") + public Integer parallelism; + + // TODO(cjh) remove plaintext passwords being provided on CLI, replace with prompt and password file - can be done + // as + // a separate change + @CommandLine.Option(names = {"--keys.passwords"}) + public String password; + + @CommandLine.Option(names = {"--keys.passwordFile"}) + public Path passwordFile; + + @CommandLine.Option( + names = {"--configfile", "-configfile"}, + description = "Path to node configuration file") + public Config config; + + @CommandLine.Mixin public EncryptorOptions encryptorOptions; + + private KeyEncryptorFactory keyEncryptorFactory; + + // TODO(cjh) is package-private for migrated apache commons CLI tests + KeyEncryptor keyEncryptor; + + private PasswordReader passwordReader; + + KeyUpdateCommand(KeyEncryptorFactory keyEncryptorFactory, PasswordReader passwordReader) { + this.keyEncryptorFactory = keyEncryptorFactory; + this.passwordReader = passwordReader; + } + + @Override + public CliResult call() throws Exception { + final EncryptorConfig encryptorConfig; + + if (Optional.ofNullable(config).map(Config::getEncryptor).isPresent()) { + encryptorConfig = config.getEncryptor(); + } else { + encryptorConfig = encryptorOptions.parseEncryptorConfig(); + } + + this.keyEncryptor = keyEncryptorFactory.create(encryptorConfig); + + return execute(); + } + + public CliResult execute() throws IOException { + final ArgonOptions argonOptions = argonOptions(); + final List passwords = passwords(); + final Path keypath = privateKeyPath(); + + final KeyDataConfig keyDataConfig = JaxbUtil.unmarshal(Files.newInputStream(keypath), KeyDataConfig.class); + final PrivateKey privateKey = this.getExistingKey(keyDataConfig, passwords); + + final String newPassword = passwordReader.requestUserPassword(); + + final KeyDataConfig updatedKey; + if (newPassword.isEmpty()) { + final PrivateKeyData privateKeyData = + new PrivateKeyData(privateKey.encodeToBase64(), null, null, null, null); + updatedKey = new KeyDataConfig(privateKeyData, PrivateKeyType.UNLOCKED); + } else { + final PrivateKeyData privateKeyData = keyEncryptor.encryptPrivateKey(privateKey, newPassword, argonOptions); + updatedKey = new KeyDataConfig(privateKeyData, PrivateKeyType.LOCKED); + } + + // write the key to file + Files.write(keypath, JaxbUtil.marshalToString(updatedKey).getBytes(UTF_8)); + SystemAdapter.INSTANCE.out().println("Private key at " + keypath.toString() + " updated."); + + return new CliResult(0, true, null); + } + + PrivateKey getExistingKey(final KeyDataConfig kdc, final List passwords) { + + if (kdc.getType() == PrivateKeyType.UNLOCKED) { + byte[] privateKeyData = Base64.getDecoder().decode(kdc.getValue().getBytes(UTF_8)); + return PrivateKey.from(privateKeyData); + } else { + + for (final String pass : passwords) { + try { + return PrivateKey.from(keyEncryptor.decryptPrivateKey(kdc.getPrivateKeyData(), pass).getKeyBytes()); + } catch (final Exception e) { + LOGGER.debug("Password failed to decrypt. Trying next if available."); + } + } + + throw new IllegalArgumentException("Locked key but no valid password given"); + } + } + + Path privateKeyPath() { + if (Files.notExists(privateKeyPath)) { + throw new IllegalArgumentException("Private key path must exist when updating key password"); + } + + return privateKeyPath; + } + + List passwords() throws IOException { + if (password != null) { + return singletonList(password); + } else if (passwordFile != null) { + return Files.readAllLines(passwordFile); + } else { + return emptyList(); + } + } + + ArgonOptions argonOptions() { + if ("i".equals(algorithm) || "d".equals(algorithm) || "id".equals(algorithm)) { + return new ArgonOptions( + algorithm, Integer.valueOf(iterations), Integer.valueOf(memory), Integer.valueOf(parallelism)); + } + + throw new CliException(invalidArgonAlgorithmMsg); + } +} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyUpdateCommandFactory.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyUpdateCommandFactory.java new file mode 100644 index 0000000000..205d2101ed --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyUpdateCommandFactory.java @@ -0,0 +1,26 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.keys.KeyEncryptorFactory; +import com.quorum.tessera.passwords.PasswordReader; +import com.quorum.tessera.passwords.PasswordReaderFactory; +import picocli.CommandLine; + +public class KeyUpdateCommandFactory implements CommandLine.IFactory { + + @Override + public K create(Class cls) throws Exception { + try { + if (cls != KeyUpdateCommand.class) { + throw new RuntimeException( + this.getClass().getSimpleName() + " cannot create instance of type " + cls.getSimpleName()); + } + + KeyEncryptorFactory keyEncryptorFactory = KeyEncryptorFactory.newFactory(); + PasswordReader passwordReader = PasswordReaderFactory.create(); + + return (K) new KeyUpdateCommand(keyEncryptorFactory, passwordReader); + } catch (Exception e) { + return CommandLine.defaultFactory().create(cls); // fallback if missing + } + } +} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/NoTesseraCmdArgsException.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/NoTesseraCmdArgsException.java new file mode 100644 index 0000000000..9a6667a908 --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/NoTesseraCmdArgsException.java @@ -0,0 +1,3 @@ +package com.quorum.tessera.config.cli; + +public class NoTesseraCmdArgsException extends RuntimeException {} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/NoTesseraConfigfileOptionException.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/NoTesseraConfigfileOptionException.java new file mode 100644 index 0000000000..5fd156824c --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/NoTesseraConfigfileOptionException.java @@ -0,0 +1,3 @@ +package com.quorum.tessera.config.cli; + +public class NoTesseraConfigfileOptionException extends RuntimeException {} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/OverrideUtil.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/OverrideUtil.java index b33832e7bb..c7e9687a13 100644 --- a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/OverrideUtil.java +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/OverrideUtil.java @@ -1,9 +1,11 @@ package com.quorum.tessera.config.cli; +import com.quorum.tessera.cli.CliException; import com.quorum.tessera.config.Config; import com.quorum.tessera.reflect.ReflectCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import java.lang.reflect.Array; @@ -15,6 +17,8 @@ import java.nio.file.Paths; import java.util.*; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -28,7 +32,7 @@ public interface OverrideUtil { Collections.unmodifiableList( Arrays.asList(String.class, Path.class, Integer.class, Boolean.class, Long.class)); - Map, Class> PRIMATIVE_LOOKUP = + Map, Class> PRIMITIVE_LOOKUP = Collections.unmodifiableMap( new HashMap, Class>() { { @@ -142,7 +146,7 @@ static Class toArrayType(Class t) { * @param path * @param value */ - static void setValue(Object root, String path, String... value) { + static void setValue(Object root, String path, String value) { if (root == null) { return; @@ -158,25 +162,55 @@ static void setValue(Object root, String path, String... value) { while (pathTokens.hasNext()) { final String token = pathTokens.next(); - final Field field = resolveField(rootType, token); + + final String target; + final String position; + + final String collectionPattern = "^(.*)\\[([0-9].*)\\]$"; + final Pattern r = Pattern.compile(collectionPattern); + final Matcher m = r.matcher(token); + + if (m.matches()) { + target = m.group(1); + position = m.group(2); + LOGGER.debug("Setting {} at position {}", target, position); + } else { + target = token; + position = null; + } + + final Field field = resolveField(rootType, target); field.setAccessible(true); final Class fieldType = field.getType(); if (Collection.class.isAssignableFrom(fieldType)) { + if (Objects.isNull(position)) { + throw new CliException(path + ": position not provided for Collection parameter override " + token); + } + + final int i = Integer.parseInt(position); final Class genericType = resolveCollectionParameter(field.getGenericType()); List list = (List) Optional.ofNullable(getValue(root, field)).orElse(new ArrayList<>()); + if (isSimple(genericType)) { - List convertedValues = - (List) Stream.of(value).map(v -> convertTo(genericType, v)).collect(Collectors.toList()); + Object convertedValue = convertTo(genericType, value); + + List updated = new ArrayList(list); + + while (updated.size() <= i) { + Class convertedType = PRIMITIVE_LOOKUP.getOrDefault(fieldType, fieldType); + Object emptyValue = convertTo(convertedType, null); + + updated.add(emptyValue); + } - List merged = new ArrayList(list); - merged.addAll(convertedValues); + updated.set(i, convertedValue); - setValue(root, field, merged); + setValue(root, field, updated); } else { @@ -184,34 +218,24 @@ static void setValue(Object root, String path, String... value) { pathTokens.forEachRemaining(builder::add); String nestedPath = builder.stream().collect(Collectors.joining(".")); - final Object[] newList; - if (ADDITIVE_COLLECTION_FIELDS.contains(field.getName())) { - newList = new Object[value.length]; - } else { - newList = Arrays.copyOf(list.toArray(), value.length); + while (list.size() <= i) { + final Object newObject = createInstance(genericType); + initialiseNestedObjects(newObject); + list.add(newObject); } - for (int i = 0; i < value.length; i++) { - final String v = value[i]; + final Object nestedObject = list.get(i); - final Object nestedObject = Optional.ofNullable(newList[i]).orElse(createInstance(genericType)); + // update the collection's complex object + setValue(nestedObject, nestedPath, value); - initialiseNestedObjects(nestedObject); - - setValue(nestedObject, nestedPath, v); - newList[i] = nestedObject; - } - List merged = new ArrayList(); - if (ADDITIVE_COLLECTION_FIELDS.contains(field.getName())) { - merged.addAll(list); - } - merged.addAll(Arrays.asList(newList)); - setValue(root, field, merged); + // update the root object with the updated collection + setValue(root, field, list); } } else if (isSimple(fieldType)) { - Class convertedType = PRIMATIVE_LOOKUP.getOrDefault(fieldType, fieldType); - Object convertedValue = convertTo(convertedType, value[0]); + Class convertedType = PRIMITIVE_LOOKUP.getOrDefault(fieldType, fieldType); + Object convertedValue = convertTo(convertedType, value); setValue(root, field, convertedValue); } else { diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/PicoCliDelegate.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/PicoCliDelegate.java new file mode 100644 index 0000000000..6db0fc180e --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/PicoCliDelegate.java @@ -0,0 +1,215 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.ServiceLoaderUtil; +import com.quorum.tessera.cli.CLIExceptionCapturer; +import com.quorum.tessera.cli.CliException; +import com.quorum.tessera.cli.CliResult; +import com.quorum.tessera.config.cli.admin.AdminCliAdapter; +import com.quorum.tessera.cli.keypassresolver.CliKeyPasswordResolver; +import com.quorum.tessera.cli.keypassresolver.KeyPasswordResolver; +import com.quorum.tessera.cli.parsers.ConfigConverter; +import com.quorum.tessera.config.ArgonOptions; +import com.quorum.tessera.config.Config; +import com.quorum.tessera.reflect.ReflectException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +// TODO(cjh) clean up cli-api and config-cli modules. the parser and adapter behaviour should be encapsulated in these +// commands so are no longer needed + +public class PicoCliDelegate { + private static final Logger LOGGER = LoggerFactory.getLogger(PicoCliDelegate.class); + + private final Validator validator = + Validation.byDefaultProvider().configure().ignoreXmlConfiguration().buildValidatorFactory().getValidator(); + + private final KeyPasswordResolver keyPasswordResolver; + + public PicoCliDelegate() { + this(ServiceLoaderUtil.load(KeyPasswordResolver.class).orElse(new CliKeyPasswordResolver())); + } + + private PicoCliDelegate(final KeyPasswordResolver keyPasswordResolver) { + this.keyPasswordResolver = Objects.requireNonNull(keyPasswordResolver); + } + + public CliResult execute(String... args) throws Exception { + final CommandSpec command = CommandSpec.forAnnotatedObject(TesseraCommand.class); + + final CLIExceptionCapturer mapper = new CLIExceptionCapturer(); + + final CommandLine.IFactory keyGenCommandFactory = new KeyGenCommandFactory(); + CommandLine keyGenCommandLine = new CommandLine(KeyGenCommand.class, keyGenCommandFactory); + + final CommandLine.IFactory keyUpdateCommandFactory = new KeyUpdateCommandFactory(); + CommandLine keyUpdateCommandLine = new CommandLine(KeyUpdateCommand.class, keyUpdateCommandFactory); + + command.addSubcommand(null, new CommandLine(CommandLine.HelpCommand.class)); + command.addSubcommand(null, new CommandLine(AdminCliAdapter.class)); + command.addSubcommand(null, keyGenCommandLine); + command.addSubcommand(null, keyUpdateCommandLine); + + final CommandLine commandLine = new CommandLine(command); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .registerConverter(ArgonOptions.class, new ArgonOptionsConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true) + .setExecutionExceptionHandler(mapper) + .setParameterExceptionHandler(mapper) + .setStopAtUnmatched(false); + + final CommandLine.ParseResult parseResult; + try { + parseResult = commandLine.parseArgs(args); + } catch (CommandLine.ParameterException ex) { + try { + commandLine.getParameterExceptionHandler().handleParseException(ex, args); + throw new CliException(ex.getMessage()); + } catch (Exception e) { + throw new CliException(ex.getMessage()); + } + } + + if (CommandLine.printHelpIfRequested(parseResult)) { + return new CliResult(0, true, null); + } + + if (!parseResult.hasSubcommand()) { + // the node is being started + final Config config; + try { + config = getConfigFromCLI(parseResult); + } catch (NoTesseraCmdArgsException e) { + commandLine.execute("help"); + return new CliResult(0, true, null); + } catch (NoTesseraConfigfileOptionException e) { + throw new CliException("Missing required option '--configfile '"); + } + + return new CliResult(0, false, config); + + } else { + // there is a subcommand + CommandLine.ParseResult subParseResult = parseResult.subcommand(); + + String[] subCmdAndArgs = subParseResult.originalArgs().toArray(new String[0]); + + // print help as no args provided + if (subCmdAndArgs.length == 1) { + subParseResult.asCommandLineList().get(0).execute("help"); + return new CliResult(0, true, null); + } + + String[] subArgs = new String[subCmdAndArgs.length - 1]; + System.arraycopy(subCmdAndArgs, 1, subArgs, 0, subArgs.length); + + // TODO(cjh) document the change of behaviour meaning node cannot start after keygen + subParseResult.asCommandLineList().get(0).execute(subArgs); + + // if an exception occurred, throw it to to the upper levels where it gets handled + if (mapper.getThrown() != null) { + throw mapper.getThrown(); + } + + return new CliResult(0, true, null); + } + } + + private Config getConfigFromCLI(CommandLine.ParseResult parseResult) throws Exception { + List parsedArgs = parseResult.matchedArgs(); + + if (parsedArgs.size() == 0) { + throw new NoTesseraCmdArgsException(); + } + + final Config config; + + // start with any config read from the file + if (parseResult.hasMatchedOption("configfile")) { + config = parseResult.matchedOption("configfile").getValue(); + } else { + throw new NoTesseraConfigfileOptionException(); + } + + if (parseResult.hasMatchedOption("override")) { + Map overrides = parseResult.matchedOption("override").getValue(); + + for (String target : overrides.keySet()) { + String value = overrides.get(target); + + // apply CLI overrides + LOGGER.debug("Setting : {} with value(s) {}", target, value); + OverrideUtil.setValue(config, target, value); + LOGGER.debug("Set : {} with value(s) {}", target, value); + } + } + + if (Objects.nonNull(parseResult.unmatched())) { + List unmatched = new ArrayList<>(parseResult.unmatched()); + + for (int i = 0; i < unmatched.size(); i++) { + String line = unmatched.get(i); + if (line.startsWith("-")) { + final String name = line.replaceFirst("-{1,2}", ""); + final int nextIndex = i + 1; + if(nextIndex > (unmatched.size() -1)) { + break; + } + i = nextIndex; + final String value = unmatched.get(nextIndex); + try { + OverrideUtil.setValue(config, name, value); + } catch(ReflectException ex) { + //Ignore error + LOGGER.debug("",ex); + continue; + } + } + } + } + + keyPasswordResolver.resolveKeyPasswords(config); + + final Set> violations = validator.validate(config); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + + if (parseResult.hasMatchedOption("pidfile")) { + createPidFile(parseResult.matchedOption("pidfile").getValue()); + } + + return config; + } + + private void createPidFile(Path pidFilePath) throws Exception { + if (Files.exists(pidFilePath)) { + LOGGER.info("File already exists {}", pidFilePath); + } else { + LOGGER.info("Created pid file {}", pidFilePath); + } + + final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + + try (OutputStream stream = Files.newOutputStream(pidFilePath, CREATE, TRUNCATE_EXISTING)) { + stream.write(pid.getBytes(UTF_8)); + } + } +} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/TesseraCommand.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/TesseraCommand.java new file mode 100644 index 0000000000..a65d5a878a --- /dev/null +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/TesseraCommand.java @@ -0,0 +1,41 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.Config; +import picocli.CommandLine; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@CommandLine.Command( + name = "tessera", + headerHeading = "Usage:%n%n", + header = "Tessera private transaction manager for Quorum", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + description = "Start a Tessera node. Other commands exist to manage Tessera encryption keys", + parameterListHeading = "%nParameters:%n", + optionListHeading = "%nOptions:%n", + abbreviateSynopsis = true) +public class TesseraCommand { + + @CommandLine.Option( + names = {"--configfile", "-configfile"}, + description = "Path to node configuration file") + public Config config; + + @CommandLine.Option( + names = {"--pidfile", "-pidfile"}, + description = "the path to write the PID to") + public Path pidFilePath; + + @CommandLine.Option( + names = {"-o", "--override"}, + paramLabel = "KEY=VALUE") + private Map overrides = new LinkedHashMap<>(); + + @CommandLine.Unmatched public List unmatchedEntries; + + // TODO(cjh) dry run option to print effective config to terminal to allow review of CLI overrides +} diff --git a/cli/admin-cli/src/main/java/com/quorum/tessera/admin/cli/AdminCliAdapter.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/admin/AdminCliAdapter.java similarity index 92% rename from cli/admin-cli/src/main/java/com/quorum/tessera/admin/cli/AdminCliAdapter.java rename to cli/config-cli/src/main/java/com/quorum/tessera/config/cli/admin/AdminCliAdapter.java index 1aaf015aaa..70962a26ae 100644 --- a/cli/admin-cli/src/main/java/com/quorum/tessera/admin/cli/AdminCliAdapter.java +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/admin/AdminCliAdapter.java @@ -1,9 +1,9 @@ -package com.quorum.tessera.admin.cli; +package com.quorum.tessera.config.cli.admin; -import com.quorum.tessera.admin.cli.subcommands.AddPeerCommand; import com.quorum.tessera.cli.CliAdapter; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.config.cli.admin.subcommands.AddPeerCommand; import picocli.CommandLine; import java.util.concurrent.Callable; diff --git a/cli/admin-cli/src/main/java/com/quorum/tessera/admin/cli/subcommands/AddPeerCommand.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/admin/subcommands/AddPeerCommand.java similarity index 98% rename from cli/admin-cli/src/main/java/com/quorum/tessera/admin/cli/subcommands/AddPeerCommand.java rename to cli/config-cli/src/main/java/com/quorum/tessera/config/cli/admin/subcommands/AddPeerCommand.java index 571c456400..252bd40731 100644 --- a/cli/admin-cli/src/main/java/com/quorum/tessera/admin/cli/subcommands/AddPeerCommand.java +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/admin/subcommands/AddPeerCommand.java @@ -1,4 +1,4 @@ -package com.quorum.tessera.admin.cli.subcommands; +package com.quorum.tessera.config.cli.admin.subcommands; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.parsers.ConfigurationMixin; diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/EncryptorConfigParser.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/EncryptorConfigParser.java deleted file mode 100644 index 2ee948bb98..0000000000 --- a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/EncryptorConfigParser.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.quorum.tessera.config.cli.parsers; - -import com.quorum.tessera.cli.parsers.Parser; -import com.quorum.tessera.config.Config; -import com.quorum.tessera.config.EncryptorConfig; -import com.quorum.tessera.config.EncryptorType; -import com.quorum.tessera.config.util.JaxbUtil; -import com.quorum.tessera.io.FilesDelegate; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import org.apache.commons.cli.CommandLine; - -public class EncryptorConfigParser implements Parser { - - protected static final String NO_ENCRYPTOR_DEFINED_ERROR_MESSAGE = - "Encryptor type hasn't been defined in the config file or as a cli arg"; - - private final FilesDelegate filesDelegate; - - public EncryptorConfigParser() { - this(FilesDelegate.create()); - } - - protected EncryptorConfigParser(FilesDelegate filesDelegate) { - this.filesDelegate = Objects.requireNonNull(filesDelegate); - } - - @Override - public EncryptorConfig parse(CommandLine commandLine) throws IOException { - - final String encryptorTypeValue = commandLine.getOptionValue("encryptor.type", EncryptorType.NACL.name()); - - if (commandLine.hasOption("configfile")) { - final String path = commandLine.getOptionValue("configfile"); - final Config config = JaxbUtil.unmarshal(filesDelegate.newInputStream(Paths.get(path)), Config.class); - - if (Objects.nonNull(config.getEncryptor())) { - return config.getEncryptor(); - } - } - - final EncryptorConfig encryptorConfig = new EncryptorConfig(); - - final EncryptorType encryptorType = EncryptorType.valueOf(encryptorTypeValue.toUpperCase()); - encryptorConfig.setType(encryptorType); - - Map properties = new HashMap<>(); - if (encryptorType == EncryptorType.EC) { - - Optional.ofNullable(commandLine.getOptionValue("encryptor.symmetricCipher")) - .ifPresent(v -> properties.put("symmetricCipher", v)); - - Optional.ofNullable(commandLine.getOptionValue("encryptor.ellipticCurve")) - .ifPresent(v -> properties.put("ellipticCurve", v)); - - Optional.ofNullable(commandLine.getOptionValue("encryptor.nonceLength")) - .ifPresent(v -> properties.put("nonceLength", v)); - - Optional.ofNullable(commandLine.getOptionValue("encryptor.sharedKeyLength")) - .ifPresent(v -> properties.put("sharedKeyLength", v)); - } - - encryptorConfig.setProperties(properties); - - return encryptorConfig; - } -} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParser.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParser.java deleted file mode 100644 index 103fdb6478..0000000000 --- a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParser.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.quorum.tessera.config.cli.parsers; - -import com.quorum.tessera.cli.CliException; -import com.quorum.tessera.cli.parsers.Parser; -import com.quorum.tessera.config.*; -import com.quorum.tessera.config.keypairs.ConfigKeyPair; -import com.quorum.tessera.config.util.JaxbUtil; -import com.quorum.tessera.key.generation.KeyGenerator; -import com.quorum.tessera.key.generation.KeyGeneratorFactory; -import com.quorum.tessera.key.generation.KeyVaultOptions; -import org.apache.commons.cli.CommandLine; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Validation; -import javax.validation.Validator; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.Collections.singletonList; -import java.util.Objects; - -public class KeyGenerationParser implements Parser> { - - private final KeyGeneratorFactory factory = KeyGeneratorFactory.newFactory(); - - private final Validator validator = - Validation.byDefaultProvider().configure().ignoreXmlConfiguration().buildValidatorFactory().getValidator(); - - private final EncryptorConfig encryptorConfig; - - public KeyGenerationParser(EncryptorConfig encryptorConfig) { - this.encryptorConfig = Objects.requireNonNull(encryptorConfig); - } - - @Override - public List parse(final CommandLine commandLine) throws IOException { - - final ArgonOptions argonOptions = this.argonOptions(commandLine).orElse(null); - final KeyVaultOptions keyVaultOptions = this.keyVaultOptions(commandLine).orElse(null); - final KeyVaultConfig keyVaultConfig = this.keyVaultConfig(commandLine).orElse(null); - - final KeyGenerator generator = factory.create(keyVaultConfig, encryptorConfig); - - if (commandLine.hasOption("keygen")) { - return this.filenames(commandLine).stream() - .map(name -> generator.generate(name, argonOptions, keyVaultOptions)) - .collect(Collectors.toList()); - } - - return new ArrayList<>(); - } - - private Optional argonOptions(final CommandLine commandLine) throws IOException { - - if (commandLine.hasOption("keygenconfig")) { - final String pathName = commandLine.getOptionValue("keygenconfig"); - final InputStream configStream = Files.newInputStream(Paths.get(pathName)); - - final ArgonOptions argonOptions = JaxbUtil.unmarshal(configStream, ArgonOptions.class); - return Optional.of(argonOptions); - } - - return Optional.empty(); - } - - private Optional keyVaultOptions(final CommandLine commandLine) { - Optional secretEngineName = Optional.ofNullable(commandLine.getOptionValue("keygenvaultsecretengine")); - - return secretEngineName.map(KeyVaultOptions::new); - } - - private List filenames(final CommandLine commandLine) { - - if (commandLine.hasOption("filename")) { - - final String keyNames = commandLine.getOptionValue("filename"); - if (keyNames != null) { - return Stream.of(keyNames.split(",")).collect(Collectors.toList()); - } - } - - return singletonList(""); - } - - private Optional keyVaultConfig(CommandLine commandLine) { - if (!commandLine.hasOption("keygenvaulttype") && !commandLine.hasOption("keygenvaulturl")) { - return Optional.empty(); - } - - final String t = commandLine.getOptionValue("keygenvaulttype"); - - KeyVaultType keyVaultType; - try { - keyVaultType = KeyVaultType.valueOf(t.trim().toUpperCase()); - } catch (IllegalArgumentException | NullPointerException e) { - throw new CliException("Key vault type either not provided or not recognised"); - } - - String keyVaultUrl = commandLine.getOptionValue("keygenvaulturl"); - - KeyVaultConfig keyVaultConfig; - - if (keyVaultType.equals(KeyVaultType.AZURE)) { - keyVaultConfig = new AzureKeyVaultConfig(keyVaultUrl); - - Set> violations = - validator.validate((AzureKeyVaultConfig) keyVaultConfig); - - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); - } - } else { - if (!commandLine.hasOption("filename")) { - throw new CliException( - "At least one -filename must be provided when saving generated keys in a Hashicorp Vault"); - } - - String approlePath = commandLine.getOptionValue("keygenvaultapprole"); - - Optional tlsKeyStorePath = - Optional.ofNullable(commandLine.getOptionValue("keygenvaultkeystore")).map(Paths::get); - - Optional tlsTrustStorePath = - Optional.ofNullable(commandLine.getOptionValue("keygenvaulttruststore")).map(Paths::get); - - keyVaultConfig = - new HashicorpKeyVaultConfig( - keyVaultUrl, approlePath, tlsKeyStorePath.orElse(null), tlsTrustStorePath.orElse(null)); - - Set> violations = - validator.validate((HashicorpKeyVaultConfig) keyVaultConfig); - - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); - } - } - - return Optional.of(keyVaultConfig); - } -} diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyUpdateParser.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyUpdateParser.java deleted file mode 100644 index 856ebc6bb4..0000000000 --- a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyUpdateParser.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.quorum.tessera.config.cli.parsers; - -import com.quorum.tessera.cli.parsers.Parser; -import com.quorum.tessera.config.ArgonOptions; -import com.quorum.tessera.config.KeyDataConfig; -import com.quorum.tessera.config.PrivateKeyData; -import com.quorum.tessera.config.PrivateKeyType; -import com.quorum.tessera.config.keys.KeyEncryptor; -import com.quorum.tessera.config.util.JaxbUtil; -import com.quorum.tessera.encryption.PrivateKey; -import com.quorum.tessera.io.SystemAdapter; -import com.quorum.tessera.passwords.PasswordReader; -import org.apache.commons.cli.CommandLine; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Base64; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; - -public class KeyUpdateParser implements Parser { - - private static final Logger LOGGER = LoggerFactory.getLogger(KeyUpdateParser.class); - - private final KeyEncryptor keyEncryptor; - - private final PasswordReader passwordReader; - - public KeyUpdateParser(final KeyEncryptor keyEncryptor, final PasswordReader passwordReader) { - this.keyEncryptor = Objects.requireNonNull(keyEncryptor); - this.passwordReader = Objects.requireNonNull(passwordReader); - } - - @Override - public Optional parse(final CommandLine commandLine) throws IOException { - final ArgonOptions argonOptions = argonOptions(commandLine); - final List passwords = passwords(commandLine); - final Path keypath = privateKeyPath(commandLine); - - final KeyDataConfig keyDataConfig = JaxbUtil.unmarshal(Files.newInputStream(keypath), KeyDataConfig.class); - final PrivateKey privateKey = this.getExistingKey(keyDataConfig, passwords); - - final String newPassword = passwordReader.requestUserPassword(); - - final KeyDataConfig updatedKey; - if(newPassword.isEmpty()) { - final PrivateKeyData privateKeyData = new PrivateKeyData(privateKey.encodeToBase64(), null, null, null, null); - updatedKey = new KeyDataConfig(privateKeyData, PrivateKeyType.UNLOCKED); - } else { - final PrivateKeyData privateKeyData = keyEncryptor.encryptPrivateKey(privateKey, newPassword, argonOptions); - updatedKey = new KeyDataConfig(privateKeyData, PrivateKeyType.LOCKED); - } - - //write the key to file - Files.write(keypath, JaxbUtil.marshalToString(updatedKey).getBytes(UTF_8)); - SystemAdapter.INSTANCE.out().println("Private key at " + keypath.toString() + " updated."); - - return Optional.empty(); - } - - PrivateKey getExistingKey(final KeyDataConfig kdc, final List passwords) { - - if (kdc.getType() == PrivateKeyType.UNLOCKED) { - byte[] privateKeyData = Base64.getDecoder().decode(kdc.getValue().getBytes(UTF_8)); - return PrivateKey.from(privateKeyData); - } else { - - for (final String pass : passwords) { - try { - return PrivateKey.from(keyEncryptor.decryptPrivateKey(kdc.getPrivateKeyData(), pass).getKeyBytes()); - } catch (final Exception e) { - LOGGER.debug("Password failed to decrypt. Trying next if available."); - } - } - - throw new IllegalArgumentException("Locked key but no valid password given"); - } - } - - static Path privateKeyPath(final CommandLine commandLine) { - final String privateKeyPath = commandLine.getOptionValue("keys.keyData.privateKeyPath"); - - if (privateKeyPath == null) { - throw new IllegalArgumentException("Private key path cannot be null when updating key password"); - } - - final Path keypath = Paths.get(privateKeyPath); - if (Files.notExists(keypath)) { - throw new IllegalArgumentException("Private key path must exist when updating key password"); - } - - return keypath; - } - - static List passwords(final CommandLine commandLine) throws IOException { - final String password = commandLine.getOptionValue("keys.passwords"); - final String passwordFile = commandLine.getOptionValue("keys.passwordFile"); - - if (password != null) { - return singletonList(password); - } else if (passwordFile != null) { - return Files.readAllLines(Paths.get(passwordFile)); - } else { - return emptyList(); - } - - } - - static ArgonOptions argonOptions(final CommandLine commandLine) { - final String algorithm = commandLine.getOptionValue("keys.keyData.config.data.aopts.algorithm", "i"); - final String iterations = commandLine.getOptionValue("keys.keyData.config.data.aopts.iterations", "10"); - final String memory = commandLine.getOptionValue("keys.keyData.config.data.aopts.memory", "1048576"); - final String parallelism = commandLine.getOptionValue("keys.keyData.config.data.aopts.parallelism", "4"); - - return new ArgonOptions( - algorithm, Integer.valueOf(iterations), Integer.valueOf(memory), Integer.valueOf(parallelism) - ); - } - -} diff --git a/cli/config-cli/src/main/resources/META-INF/services/com.quorum.tessera.cli.CliAdapter b/cli/config-cli/src/main/resources/META-INF/services/com.quorum.tessera.cli.CliAdapter deleted file mode 100644 index 023e8dc70e..0000000000 --- a/cli/config-cli/src/main/resources/META-INF/services/com.quorum.tessera.cli.CliAdapter +++ /dev/null @@ -1 +0,0 @@ -com.quorum.tessera.config.cli.DefaultCliAdapter diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/ArgonOptionsConverterTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/ArgonOptionsConverterTest.java new file mode 100644 index 0000000000..d5c1ca584a --- /dev/null +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/ArgonOptionsConverterTest.java @@ -0,0 +1,55 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.ArgonOptions; +import org.junit.Before; +import org.junit.Test; + +import java.io.FileNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArgonOptionsConverterTest { + + private ArgonOptionsConverter argonOptionsConverter; + + @Before + public void onSetUp() { + argonOptionsConverter = new ArgonOptionsConverter(); + } + + @Test(expected = FileNotFoundException.class) + public void fileNotFound() throws Exception { + argonOptionsConverter.convert("path/to/nothing"); + } + + @Test + public void fileContainsValidArgonJsonConfig() throws Exception { + final String algorithm = "id"; + final Integer iterations = 10; + final Integer memory = 10; + final Integer parallelism = 10; + + final String config = + String.format( + "{\"variant\": \"%s\", \"iterations\":%s, \"memory\":%s, \"parallelism\":%s}", + algorithm, iterations, memory, parallelism); + + final Path argonPath = Files.createTempFile(UUID.randomUUID().toString(), ""); + argonPath.toFile().deleteOnExit(); + + Files.write(argonPath, config.getBytes()); + + final ArgonOptions result = argonOptionsConverter.convert(argonPath.toString()); + + final ArgonOptions expected = new ArgonOptions(); + expected.setAlgorithm(algorithm); + expected.setIterations(iterations); + expected.setMemory(memory); + expected.setParallelism(parallelism); + + assertThat(result).isEqualToComparingFieldByField(expected); + } +} diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/EncryptorOptionsTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/EncryptorOptionsTest.java new file mode 100644 index 0000000000..212e0af040 --- /dev/null +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/EncryptorOptionsTest.java @@ -0,0 +1,53 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.EncryptorConfig; +import com.quorum.tessera.config.EncryptorType; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EncryptorOptionsTest { + + @Test + public void ellipticalCurveNoPropertiesDefined() { + EncryptorOptions encryptorOptions = new EncryptorOptions(); + encryptorOptions.type = EncryptorType.EC; + + EncryptorConfig result = encryptorOptions.parseEncryptorConfig(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(EncryptorType.EC); + assertThat(result.getProperties()).isEmpty(); + } + + @Test + public void ellipticalCurveWithDefinedProperties() { + EncryptorOptions encryptorOptions = new EncryptorOptions(); + encryptorOptions.type = EncryptorType.EC; + encryptorOptions.symmetricCipher = "somecipher"; + encryptorOptions.ellipticCurve = "somecurve"; + encryptorOptions.nonceLength = "3"; + encryptorOptions.sharedKeyLength = "2"; + + EncryptorConfig result = encryptorOptions.parseEncryptorConfig(); + + assertThat(result.getType()).isEqualTo(EncryptorType.EC); + assertThat(result.getProperties()) + .containsOnlyKeys("symmetricCipher", "ellipticCurve", "nonceLength", "sharedKeyLength"); + + assertThat(result.getProperties().get("symmetricCipher")).isEqualTo("somecipher"); + assertThat(result.getProperties().get("ellipticCurve")).isEqualTo("somecurve"); + assertThat(result.getProperties().get("nonceLength")).isEqualTo("3"); + assertThat(result.getProperties().get("sharedKeyLength")).isEqualTo("2"); + } + + @Test + public void encryptorTypeDefaultsToNACL() { + EncryptorOptions encryptorOptions = new EncryptorOptions(); + + EncryptorConfig result = encryptorOptions.parseEncryptorConfig(); + + assertThat(result.getType()).isEqualTo(EncryptorType.NACL); + assertThat(result.getProperties()).isEmpty(); + } +} diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyGenCommandTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyGenCommandTest.java new file mode 100644 index 0000000000..e3d6c9e858 --- /dev/null +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyGenCommandTest.java @@ -0,0 +1,501 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.cli.CliException; +import com.quorum.tessera.cli.CliResult; +import com.quorum.tessera.config.*; +import com.quorum.tessera.key.generation.KeyGenerator; +import com.quorum.tessera.key.generation.KeyGeneratorFactory; +import com.quorum.tessera.key.generation.KeyVaultOptions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class KeyGenCommandTest { + + private KeyGenCommand command; + + private KeyGeneratorFactory keyGeneratorFactory; + + private final CliResult wantResult = new CliResult(0, true, null); + + @Before + public void onSetup() { + keyGeneratorFactory = mock(KeyGeneratorFactory.class); + command = new KeyGenCommand(keyGeneratorFactory); + } + + @After + public void onTearDown() { + verifyNoMoreInteractions(keyGeneratorFactory); + } + + @Test + public void usesDefaultEncryptorConfigIfNoneInConfig() { + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + + verify(encryptorOptions).parseEncryptorConfig(); + verify(keyGenerator).generate(anyString(), any(), any()); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void doNotUseEncryptorOptionsIfConfigHasEncryptorConfig() { + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + final Config config = new Config(); + config.setEncryptor(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + command.config = config; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + + verify(keyGenerator).generate(anyString(), any(), any()); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void noKeyEncryptionConfigUsesDefault() { + final ArgonOptions defaultArgonOptions = null; + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + verify(keyGenerator).generate(anyString(), eq(defaultArgonOptions), any()); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void providedKeyEncryptionConfigIsUsed() { + final ArgonOptions argonOptions = new ArgonOptions(); + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + command.argonOptions = argonOptions; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + verify(keyGenerator).generate(anyString(), eq(argonOptions), any()); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void noKeyOutputPathUsesDefault() { + final String defaultOutputPath = ""; + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + verify(keyGenerator).generate(eq(defaultOutputPath), any(), any()); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void providedKeyOutputPathIsUsed() { + final String outputPath = "mynewkey"; + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + command.keyOut = Arrays.asList(outputPath); + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + verify(keyGenerator).generate(eq(outputPath), any(), any()); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void multipleKeyOutputPathsGeneratesMultipleKeys() { + final String outputPath = "mynewkey"; + final String otherOutputPath = "myothernewkey"; + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + command.keyOut = Arrays.asList(outputPath, otherOutputPath); + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + verify(keyGenerator).generate(eq(outputPath), any(), any()); + verify(keyGenerator).generate(eq(otherOutputPath), any(), any()); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void noKeyVaultOptionsUsesDefault() { + final KeyVaultOptions defaultKeyVaultOptions = null; + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + verify(keyGenerator).generate(anyString(), any(), eq(defaultKeyVaultOptions)); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void providedKeyVaultOptionsAreUsed() { + final String keyVaultOptionValue = "somevalue"; + final KeyVaultOptions keyVaultOptions = new KeyVaultOptions(keyVaultOptionValue); + + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + final Map properties = new HashMap<>(); + encryptorConfig.setType(EncryptorType.NACL); + encryptorConfig.setProperties(properties); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(encryptorConfig); + + command.encryptorOptions = encryptorOptions; + command.hashicorpSecretEnginePath = keyVaultOptionValue; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(null, encryptorConfig); + assertThat(result).isEqualToComparingFieldByField(wantResult); + verify(keyGenerator).generate(anyString(), any(), refEq(keyVaultOptions)); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void validAzureKeyVaultConfig() { + final KeyVaultConfig keyVaultConfig = new AzureKeyVaultConfig("someurl"); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.AZURE; + command.vaultUrl = "someurl"; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(refEq(keyVaultConfig), isNull()); + assertThat(result).isEqualToComparingFieldByField(wantResult); + + verify(keyGenerator).generate(anyString(), any(), any()); + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void invalidAzureKeyVaultConfigThrowsException() { + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.AZURE; + + Throwable ex = catchThrowable(() -> command.call()); + + assertThat(ex).isInstanceOf(ConstraintViolationException.class); + + Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); + + assertThat(violations.size()).isEqualTo(1); + + ConstraintViolation violation = violations.iterator().next(); + + assertThat(violation.getPropertyPath().toString()).isEqualTo("url"); + assertThat(violation.getMessage()).isEqualTo("may not be null"); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions); + } + + @Test + public void validHashicorpKeyVaultConfig() throws Exception { + final String vaultUrl = "someurl"; + final String approlePath = "someapprole"; + Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); + tempPath.toFile().deleteOnExit(); + + final KeyVaultConfig keyVaultConfig = new HashicorpKeyVaultConfig(vaultUrl, approlePath, tempPath, tempPath); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.HASHICORP; + command.vaultUrl = vaultUrl; + command.hashicorpApprolePath = approlePath; + command.hashicorpTlsKeystore = tempPath; + command.hashicorpTlsTruststore = tempPath; + command.keyOut = Collections.singletonList("out"); + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(refEq(keyVaultConfig), isNull()); + assertThat(result).isEqualToComparingFieldByField(wantResult); + + verify(keyGenerator).generate(anyString(), any(), any()); + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void hashicorpKeyVaultConfigNoOutputPathsThrowsException() throws Exception { + final String vaultUrl = "someurl"; + final String approlePath = "someapprole"; + Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); + tempPath.toFile().deleteOnExit(); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.HASHICORP; + command.vaultUrl = vaultUrl; + command.hashicorpApprolePath = approlePath; + command.hashicorpTlsKeystore = tempPath; + command.hashicorpTlsTruststore = tempPath; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + Throwable ex = catchThrowable(() -> command.call()); + + assertThat(ex).isInstanceOf(CliException.class); + assertThat(ex) + .hasMessage("At least one -filename must be provided when saving generated keys in a Hashicorp Vault"); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions); + } + + @Test + public void invalidHashicorpKeyVaultConfigThrowsException() { + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + + command.vaultType = KeyVaultType.HASHICORP; + command.keyOut = Collections.singletonList("out"); + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + Throwable ex = catchThrowable(() -> command.call()); + + assertThat(ex).isInstanceOf(ConstraintViolationException.class); + + Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); + + assertThat(violations.size()).isEqualTo(1); + + ConstraintViolation violation = violations.iterator().next(); + + assertThat(violation.getPropertyPath().toString()).isEqualTo("url"); + assertThat(violation.getMessage()).isEqualTo("may not be null"); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions); + } + + @Test + public void hashicorpTlsPathsDontExistThrowsException() throws Exception { + final String vaultUrl = "someurl"; + final String approlePath = "someapprole"; + final Path nonExistentPath = Paths.get(UUID.randomUUID().toString()); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.HASHICORP; + command.vaultUrl = vaultUrl; + command.hashicorpApprolePath = approlePath; + command.hashicorpTlsKeystore = nonExistentPath; + command.hashicorpTlsTruststore = nonExistentPath; + command.keyOut = Collections.singletonList("out"); + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + Throwable ex = catchThrowable(() -> command.call()); + + assertThat(ex).isInstanceOf(ConstraintViolationException.class); + + Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); + + assertThat(violations.size()).isEqualTo(2); + + Iterator> iterator = violations.iterator(); + + assertThat(iterator.next().getMessage()).isEqualTo("File does not exist"); + assertThat(iterator.next().getMessage()).isEqualTo("File does not exist"); + + // verify the correct config is used + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void vaultUrlButNoVaultTypeThrowsException() { + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultUrl = "someurl"; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + Throwable ex = catchThrowable(() -> command.call()); + + assertThat(ex).isInstanceOf(CliException.class); + assertThat(ex.getMessage()).isEqualTo("Key vault type either not provided or not recognised"); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions); + } +} diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyUpdateCommandTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyUpdateCommandTest.java new file mode 100644 index 0000000000..3741ee9c9b --- /dev/null +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyUpdateCommandTest.java @@ -0,0 +1,392 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.cli.CliException; +import com.quorum.tessera.config.*; +import com.quorum.tessera.config.keys.KeyEncryptor; +import com.quorum.tessera.config.keys.KeyEncryptorFactory; +import com.quorum.tessera.config.util.JaxbUtil; +import com.quorum.tessera.encryption.PrivateKey; +import com.quorum.tessera.passwords.PasswordReader; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import javax.xml.bind.UnmarshalException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class KeyUpdateCommandTest { + + private KeyUpdateCommand command; + + private KeyEncryptorFactory keyEncryptorFactory; + + private KeyEncryptor keyEncryptor; + + private PasswordReader passwordReader; + + @Before + public void onSetup() { + keyEncryptorFactory = mock(KeyEncryptorFactory.class); + keyEncryptor = mock(KeyEncryptor.class); + passwordReader = mock(PasswordReader.class); + + when(keyEncryptorFactory.create(any())).thenReturn(keyEncryptor); + when(passwordReader.requestUserPassword()).thenReturn("newPassword"); + + command = new KeyUpdateCommand(keyEncryptorFactory, passwordReader); + command.keyEncryptor = keyEncryptor; + } + + @After + public void onTeardown() { + verifyNoMoreInteractions(keyEncryptorFactory, keyEncryptor, passwordReader); + } + + // Argon Option tests + // TODO(cjh) re-enable this once the tests have become more integration-based (i.e. I think defaults will only be + // set when creating a command line object and calling parseArgs or execute + @Ignore + @Test + public void noArgonOptionsGivenHasDefaults() throws Exception { + // final CommandLine commandLine = new DefaultParser().parse(options, new String[] {}); + // + // final ArgonOptions argonOptions = KeyUpdateParser.argonOptions(commandLine); + // + // assertThat(argonOptions.getAlgorithm()).isEqualTo("i"); + // assertThat(argonOptions.getParallelism()).isEqualTo(4); + // assertThat(argonOptions.getMemory()).isEqualTo(1048576); + // assertThat(argonOptions.getIterations()).isEqualTo(10); + } + + @Test + public void argonOptionsGivenHasOverrides() { + command.algorithm = "d"; + command.memory = 100; + command.iterations = 100; + command.parallelism = 100; + + final ArgonOptions argonOptions = command.argonOptions(); + + assertThat(argonOptions.getAlgorithm()).isEqualTo("d"); + assertThat(argonOptions.getParallelism()).isEqualTo(100); + assertThat(argonOptions.getMemory()).isEqualTo(100); + assertThat(argonOptions.getIterations()).isEqualTo(100); + } + + @Test + public void argonOptionsInvalidTypeThrowsException() { + command.memory = 100; + command.iterations = 100; + command.parallelism = 100; + + command.algorithm = "i"; + command.argonOptions(); + + command.algorithm = "d"; + command.argonOptions(); + + command.algorithm = "id"; + command.argonOptions(); + + command.algorithm = "invalid"; + Throwable ex = catchThrowable(() -> command.argonOptions()); + + assertThat(ex).isInstanceOf(CliException.class); + assertThat(ex).hasMessage(KeyUpdateCommand.invalidArgonAlgorithmMsg); + } + + // Password reading tests + @Test + public void inlinePasswordParsed() throws IOException { + command.password = "pass"; + + final List passwords = command.passwords(); + + assertThat(passwords).isNotNull().hasSize(1).containsExactly("pass"); + } + + @Test + public void passwordFileParsedAndRead() throws IOException { + final Path passwordFile = Files.createTempFile("passwords", ".txt"); + Files.write(passwordFile, "passwordInsideFile\nsecondPassword".getBytes()); + + command.passwordFile = passwordFile; + + final List passwords = command.passwords(); + + assertThat(passwords).isNotNull().hasSize(2).containsExactly("passwordInsideFile", "secondPassword"); + } + + @Test + public void passwordFileThrowsErrorIfCantBeRead() { + command.passwordFile = Paths.get("/tmp/passwords.txt"); + + final Throwable throwable = catchThrowable(() -> command.passwords()); + + assertThat(throwable).isNotNull().isInstanceOf(IOException.class); + } + + @Test + public void emptyListGivenForNoPasswords() throws IOException { + final List passwords = command.passwords(); + + assertThat(passwords).isNotNull().isEmpty(); + } + + // key file tests + // TODO(cjh) re-enable this once the tests have become more integration-based (i.e. required fields can be tested + // when creating a command line object and calling parseArgs or execute + @Ignore + @Test + public void noPrivateKeyGivenThrowsError() { + // final Throwable throwable = catchThrowable(() -> KeyUpdateParser.privateKeyPath(commandLine)); + // + // assertThat(throwable) + // .isInstanceOf(IllegalArgumentException.class) + // .hasMessage("Private key path cannot be null when updating key password"); + } + + @Test + public void cantReadPrivateKeyThrowsError() { + command.privateKeyPath = Paths.get("/tmp/nonexisting.txt"); + + final Throwable throwable = catchThrowable(() -> command.privateKeyPath()); + + assertThat(throwable).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void privateKeyExistsReturnsPath() throws IOException { + final Path key = Files.createTempFile("key", ".key"); + + command.privateKeyPath = key; + + final Path path = command.privateKeyPath(); + + assertThat(path).isEqualTo(key); + } + + // key fetching tests + @Test + public void unlockedKeyReturnedProperly() { + final KeyDataConfig kdc = + new KeyDataConfig( + new PrivateKeyData("/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc=", null, null, null, null), + PrivateKeyType.UNLOCKED); + + final PrivateKey key = command.getExistingKey(kdc, emptyList()); + + String encodedKeyValue = Base64.getEncoder().encodeToString(key.getKeyBytes()); + + assertThat(encodedKeyValue).isEqualTo("/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc="); + } + + @Test + public void lockedKeyFailsWithNoPasswordsMatching() { + + final KeyDataConfig kdc = + new KeyDataConfig( + new PrivateKeyData( + null, + "dwixVoY+pOI2FMuu4k0jLqN/naQiTzWe", + "JoPVq9G6NdOb+Ugv+HnUeA==", + "6Jd/MXn29fk6jcrFYGPb75l7sDJae06I3Y1Op+bZSZqlYXsMpa/8lLE29H0sX3yw", + new ArgonOptions("id", 1, 1024, 1)), + PrivateKeyType.LOCKED); + + final Throwable throwable = catchThrowable(() -> command.getExistingKey(kdc, singletonList("wrong"))); + + assertThat(throwable) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Locked key but no valid password given"); + + verify(keyEncryptor).decryptPrivateKey(kdc.getPrivateKeyData(), "wrong"); + } + + @Test + public void lockedKeySucceedsWithPasswordsMatching() { + PrivateKeyData privateKeyData = + new PrivateKeyData( + null, + "dwixVoY+pOI2FMuu4k0jLqN/naQiTzWe", + "JoPVq9G6NdOb+Ugv+HnUeA==", + "6Jd/MXn29fk6jcrFYGPb75l7sDJae06I3Y1Op+bZSZqlYXsMpa/8lLE29H0sX3yw", + new ArgonOptions("id", 1, 1024, 1)); + + final KeyDataConfig kdc = + new KeyDataConfig( + new PrivateKeyData( + null, + "dwixVoY+pOI2FMuu4k0jLqN/naQiTzWe", + "JoPVq9G6NdOb+Ugv+HnUeA==", + "6Jd/MXn29fk6jcrFYGPb75l7sDJae06I3Y1Op+bZSZqlYXsMpa/8lLE29H0sX3yw", + new ArgonOptions("id", 1, 1024, 1)), + PrivateKeyType.LOCKED); + + PrivateKey privateKey = mock(PrivateKey.class); + when(privateKey.getKeyBytes()).thenReturn("SUCCESS".getBytes()); + when(keyEncryptor.decryptPrivateKey(privateKeyData, "testpassword")).thenReturn(privateKey); + + final PrivateKey result = command.getExistingKey(kdc, singletonList("testpassword")); + + assertThat(result.getKeyBytes()).isEqualTo("SUCCESS".getBytes()); + + verify(keyEncryptor).decryptPrivateKey(privateKeyData, "testpassword"); + } + + @Test + public void loadingMalformedKeyfileThrowsError() throws Exception { + final Path key = Files.createTempFile("key", ".key"); + Files.write(key, "BAD JSON DATA".getBytes()); + + command.privateKeyPath = key; + + addEmptyEncryptorConfigToCommand(); + addDefaultArgonConfigToCommand(); + + final Throwable throwable = catchThrowable(() -> command.call()); + + assertThat(throwable).isInstanceOf(ConfigException.class).hasCauseExactlyInstanceOf(UnmarshalException.class); + + verify(keyEncryptorFactory).create(any()); + } + + @Test + public void keyGetsUpdated() throws Exception { + final KeyDataConfig startingKey = + JaxbUtil.unmarshal(getClass().getResourceAsStream("/lockedprivatekey.json"), KeyDataConfig.class); + + final Path key = Files.createTempFile("key", ".key"); + Files.write(key, JaxbUtil.marshalToString(startingKey).getBytes()); + + command.privateKeyPath = key; + command.password = "testpassword"; + + addDefaultArgonConfigToCommand(); + addEmptyEncryptorConfigToCommand(); + + PrivateKey privatekey = mock(PrivateKey.class); + when(keyEncryptor.decryptPrivateKey(any(PrivateKeyData.class), anyString())).thenReturn(privatekey); + + PrivateKeyData privateKeyData = mock(PrivateKeyData.class); + + when(keyEncryptor.encryptPrivateKey(any(PrivateKey.class), anyString(), any(ArgonOptions.class))) + .thenReturn(privateKeyData); + + command.call(); + + final KeyDataConfig endingKey = JaxbUtil.unmarshal(Files.newInputStream(key), KeyDataConfig.class); + + assertThat(endingKey.getSbox()).isNotEqualTo(startingKey.getSbox()); + assertThat(endingKey.getSnonce()).isNotEqualTo(startingKey.getSnonce()); + assertThat(endingKey.getAsalt()).isNotEqualTo(startingKey.getAsalt()); + + verify(keyEncryptorFactory).create(any()); + verify(keyEncryptor).decryptPrivateKey(any(PrivateKeyData.class), anyString()); + verify(keyEncryptor).encryptPrivateKey(any(PrivateKey.class), anyString(), any(ArgonOptions.class)); + verify(passwordReader).requestUserPassword(); + } + + @Test + public void keyGetsUpdatedUsingEncryptorOptions() throws Exception { + final KeyDataConfig startingKey = + JaxbUtil.unmarshal(getClass().getResourceAsStream("/lockedprivatekey.json"), KeyDataConfig.class); + + final Path key = Files.createTempFile("key", ".key"); + Files.write(key, JaxbUtil.marshalToString(startingKey).getBytes()); + + command.privateKeyPath = key; + command.password = "testpassword"; + + addDefaultArgonConfigToCommand(); + addEncryptorOptionsToCommand(); + + PrivateKey privatekey = mock(PrivateKey.class); + when(keyEncryptor.decryptPrivateKey(any(PrivateKeyData.class), anyString())).thenReturn(privatekey); + + PrivateKeyData privateKeyData = mock(PrivateKeyData.class); + + when(keyEncryptor.encryptPrivateKey(any(PrivateKey.class), anyString(), any(ArgonOptions.class))) + .thenReturn(privateKeyData); + + command.call(); + + final KeyDataConfig endingKey = JaxbUtil.unmarshal(Files.newInputStream(key), KeyDataConfig.class); + + assertThat(endingKey.getSbox()).isNotEqualTo(startingKey.getSbox()); + assertThat(endingKey.getSnonce()).isNotEqualTo(startingKey.getSnonce()); + assertThat(endingKey.getAsalt()).isNotEqualTo(startingKey.getAsalt()); + + verify(keyEncryptorFactory).create(any()); + verify(keyEncryptor).decryptPrivateKey(any(PrivateKeyData.class), anyString()); + verify(keyEncryptor).encryptPrivateKey(any(PrivateKey.class), anyString(), any(ArgonOptions.class)); + verify(passwordReader).requestUserPassword(); + } + + @Test + public void keyGetsUpdatedToNoPassword() throws Exception { + final KeyDataConfig startingKey = + JaxbUtil.unmarshal(getClass().getResourceAsStream("/lockedprivatekey.json"), KeyDataConfig.class); + + when(passwordReader.requestUserPassword()).thenReturn(""); + + final Path key = Files.createTempFile("key", ".key"); + Files.write(key, JaxbUtil.marshalToString(startingKey).getBytes()); + + command.privateKeyPath = key; + command.password = "testpassword"; + + addDefaultArgonConfigToCommand(); + addEmptyEncryptorConfigToCommand(); + + byte[] privateKeyData = "SOME PRIVATE DATA".getBytes(); + PrivateKey privateKey = PrivateKey.from(privateKeyData); + when(keyEncryptor.decryptPrivateKey(any(PrivateKeyData.class), anyString())).thenReturn(privateKey); + + command.call(); + + final KeyDataConfig endingKey = JaxbUtil.unmarshal(Files.newInputStream(key), KeyDataConfig.class); + + assertThat(endingKey.getSbox()).isNotEqualTo(startingKey.getSbox()); + assertThat(endingKey.getSnonce()).isNotEqualTo(startingKey.getSnonce()); + assertThat(endingKey.getAsalt()).isNotEqualTo(startingKey.getAsalt()); + assertThat(endingKey.getPrivateKeyData().getValue()) + .isEqualTo(Base64.getEncoder().encodeToString(privateKeyData)); + + verify(keyEncryptorFactory).create(any()); + verify(keyEncryptor).decryptPrivateKey(any(PrivateKeyData.class), anyString()); + verify(keyEncryptor, never()).encryptPrivateKey(any(PrivateKey.class), anyString(), any(ArgonOptions.class)); + verify(passwordReader).requestUserPassword(); + } + + private void addEmptyEncryptorConfigToCommand() { + final Config config = new Config(); + final EncryptorConfig encryptorConfig = new EncryptorConfig(); + config.setEncryptor(encryptorConfig); + command.config = config; + } + + private void addEncryptorOptionsToCommand() { + command.encryptorOptions = new EncryptorOptions(); + } + + private void addDefaultArgonConfigToCommand() { + command.algorithm = "d"; + command.memory = 100; + command.iterations = 100; + command.parallelism = 100; + } +} diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java index a44319b864..dadafcff9e 100644 --- a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java @@ -1,5 +1,6 @@ package com.quorum.tessera.config.cli; +import com.quorum.tessera.cli.CliException; import com.quorum.tessera.config.Config; import com.quorum.tessera.config.KeyConfiguration; import com.quorum.tessera.config.Peer; @@ -17,6 +18,7 @@ import java.lang.reflect.Field; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -330,12 +332,19 @@ public void createConfigInstanceWithInterfaceReturnsNull() { assertThat(interfaceObject).isNull(); } + @Test + public void convertToByteArray() { + final byte[] result = OverrideUtil.convertTo(byte[].class, "HELLOW"); + assertThat(result).isEqualTo("HELLOW".getBytes()); + } + @Test public void setValue() { Config config = OverrideUtil.createInstance(Config.class); OverrideUtil.setValue(config, "jdbc.username", "someuser"); - OverrideUtil.setValue(config, "peers.url", "snonce1", "snonce2"); + OverrideUtil.setValue(config, "peers[0].url", "snonce1"); + OverrideUtil.setValue(config, "peers[1].url", "snonce2"); assertThat(config.getJdbcConfig().getUsername()).isEqualTo("someuser"); @@ -346,7 +355,8 @@ public void setValue() { @Test public void setValueWithoutAdditions() { final OtherClass someList = new OtherClass(); - OverrideUtil.setValue(someList, "someList.someValue", "password1", "password2"); + OverrideUtil.setValue(someList, "someList[0].someValue", "password1"); + OverrideUtil.setValue(someList, "someList[1].someValue", "password2"); assertThat(someList.someList.get(0).someValue).isEqualTo("password1"); assertThat(someList.someList.get(1).someValue).isEqualTo("password2"); } @@ -379,8 +389,8 @@ public void definePrivateAndPublicKeyWithOverridesOnly() throws Exception { Config config = OverrideUtil.createInstance(Config.class); - OverrideUtil.setValue(config, "keys.keyData.publicKey", "PUBLICKEY"); - OverrideUtil.setValue(config, "keys.keyData.privateKey", "PRIVATEKEY"); + OverrideUtil.setValue(config, "keys[0].keyData.publicKey", "PUBLICKEY"); + OverrideUtil.setValue(config, "keys[0].keyData.privateKey", "PRIVATEKEY"); // UNmarshlling to COnfig to try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) { JaxbUtil.marshalWithNoValidation(config, bout); @@ -402,7 +412,8 @@ public void defineAlwaysSendToWithOverridesOnly() throws Exception { Config config = OverrideUtil.createInstance(Config.class); - OverrideUtil.setValue(config, "alwaysSendTo", "ONE", "TWO"); + OverrideUtil.setValue(config, "alwaysSendTo[0]", "ONE"); + OverrideUtil.setValue(config, "alwaysSendTo[1]", "TWO"); try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) { JaxbUtil.marshalWithNoValidation(config, bout); @@ -416,15 +427,9 @@ public void defineAlwaysSendToWithOverridesOnly() throws Exception { } @Test - public void convertToByteArray() { - final byte[] result = OverrideUtil.convertTo(byte[].class, "HELLOW"); - assertThat(result).isEqualTo("HELLOW".getBytes()); - } - - @Test - public void setValueWithAnnoClass() throws Exception { + public void setValueWithAnonClassDoesNothing() { - SomeIFace annon = + SomeIFace anon = new SomeIFace() { private String value = "HEllow"; @@ -434,11 +439,291 @@ public String getValue() { } }; - OverrideUtil.setValue(annon, "value", "SOMETHING", "SOMETHINGELSE"); + OverrideUtil.setValue(anon, "value", "SOMETHING"); } interface SomeIFace { String getValue(); } + + @Test + public void setValueCollectionButNoPositionProvided() { + final String initialValue = "initial test value"; + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + + final List simpleList = Arrays.asList("element 1", initialValue, "element 3"); + toOverride.setSimpleList(simpleList); + + Throwable ex = catchThrowable(() -> OverrideUtil.setValue(toOverride, "simpleList", overriddenValue)); + + assertThat(ex).isNotNull(); + assertThat(ex).isExactlyInstanceOf(CliException.class); + assertThat(ex).hasMessage("simpleList: position not provided for Collection parameter override simpleList"); + } + + @Test + public void setValueElementOfSimpleCollectionReplaced() { + final String initialValue = "initial test value"; + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + + final List simpleList = Arrays.asList("element 1", initialValue, "element 3"); + toOverride.setSimpleList(simpleList); + + OverrideUtil.setValue(toOverride, "simpleList[1]", overriddenValue); + + assertThat(toOverride.getSimpleList()).hasSize(3); + assertThat(toOverride.getSimpleList().get(0)).isEqualTo("element 1"); + assertThat(toOverride.getSimpleList().get(1)).isEqualTo(overriddenValue); + assertThat(toOverride.getSimpleList().get(2)).isEqualTo("element 3"); + } + + @Test + public void setValuePropertyOfElementInComplexCollectionReplaced() { + final int initialValue = 11; + final int overriddenValue = 20; + + final ToOverride toOverride = new ToOverride(); + + final ToOverride.OtherTestClass otherClass = new ToOverride.OtherTestClass(); + otherClass.setCount(initialValue); + + final List someList = new ArrayList<>(); + someList.add(otherClass); + + toOverride.setSomeList(someList); + + OverrideUtil.setValue(toOverride, "someList[0].count", Integer.toString(overriddenValue)); + + assertThat(toOverride.getSomeList()).hasSize(1); + assertThat(toOverride.getSomeList().get(0).getCount()).isEqualTo(overriddenValue); + } + + @Test + public void setValueElementOfSimpleCollectionInComplexCollectionReplaced() { + final String initialValue = "initial test value"; + final String overriddenValue = "updated test value"; + + final ToOverride toOverride = new ToOverride(); + + final List otherList = Arrays.asList("some value", initialValue); + final ToOverride.OtherTestClass otherClass = new ToOverride.OtherTestClass(); + otherClass.setOtherList(otherList); + + final List someList = new ArrayList<>(); + someList.add(otherClass); + + toOverride.setSomeList(someList); + + OverrideUtil.setValue(toOverride, "someList[0].otherList[1]", overriddenValue); + + assertThat(toOverride.getSomeList()).hasSize(1); + assertThat(toOverride.getSomeList().get(0).getOtherList()).hasSize(2); + assertThat(toOverride.getSomeList().get(0).getOtherList().get(1)).isEqualTo(overriddenValue); + } + + @Test + public void setValueSimplePropertyReplaced() { + final String initialValue = "the initial value"; + final String overriddenValue = "the overridden value"; + + final ToOverride toOverride = new ToOverride(); + toOverride.setOtherValue(initialValue); + + OverrideUtil.setValue(toOverride, "otherValue", overriddenValue); + + assertThat(toOverride.getOtherValue()).isEqualTo(overriddenValue); + } + + @Test + public void setValuePropertyOfComplexPropertyReplaced() { + final int initialValue = 11; + final int overriddenValue = 20; + + final ToOverride.OtherTestClass complexProperty = new ToOverride.OtherTestClass(); + complexProperty.setCount(initialValue); + + final ToOverride toOverride = new ToOverride(); + toOverride.setComplexProperty(complexProperty); + + OverrideUtil.setValue(toOverride, "complexProperty.count", Integer.toString(overriddenValue)); + + assertThat(toOverride.getComplexProperty()).isNotNull(); + assertThat(toOverride.getComplexProperty().getCount()).isEqualTo(overriddenValue); + } + + @Test + public void setValueSimpleCollectionCreated() { + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + + OverrideUtil.setValue(toOverride, "simpleList[2]", overriddenValue); + + assertThat(toOverride.getSimpleList()).isNotNull(); + assertThat(toOverride.getSimpleList()).hasSize(3); + assertThat(toOverride.getSimpleList().get(0)).isNull(); + assertThat(toOverride.getSimpleList().get(1)).isNull(); + assertThat(toOverride.getSimpleList().get(2)).isEqualTo(overriddenValue); + } + + @Test + public void setValueComplexCollectionCreated() { + final int overriddenCount = 11; + final int otherOverriddenCount = 22; + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + + OverrideUtil.setValue(toOverride, "someList[1].count", Integer.toString(overriddenCount)); + OverrideUtil.setValue(toOverride, "someList[2].count", Integer.toString(otherOverriddenCount)); + OverrideUtil.setValue(toOverride, "someList[2].strVal", overriddenValue); + + assertThat(toOverride.getSomeList()).isNotNull(); + assertThat(toOverride.getSomeList()).hasSize(3); + + assertThat(toOverride.getSomeList().get(0)).isNotNull(); + assertThat(toOverride.getSomeList().get(1)).isNotNull(); + assertThat(toOverride.getSomeList().get(2)).isNotNull(); + + assertThat(toOverride.getSomeList().get(0).getCount()).isZero(); + assertThat(toOverride.getSomeList().get(0).getStrVal()).isNull(); + assertThat(toOverride.getSomeList().get(1).getCount()).isEqualTo(overriddenCount); + assertThat(toOverride.getSomeList().get(1).getStrVal()).isNull(); + assertThat(toOverride.getSomeList().get(2).getCount()).isEqualTo(otherOverriddenCount); + assertThat(toOverride.getSomeList().get(2).getStrVal()).isEqualTo(overriddenValue); + } + + @Test + public void setValueSimpleCollectionInComplexCollectionCreated() { + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + + OverrideUtil.setValue(toOverride, "someList[0].otherList[1]", overriddenValue); + + assertThat(toOverride.getSomeList()).isNotNull(); + assertThat(toOverride.getSomeList()).hasSize(1); + assertThat(toOverride.getSomeList().get(0)).isNotNull(); + + assertThat(toOverride.getSomeList().get(0).getOtherList()).isNotNull(); + assertThat(toOverride.getSomeList().get(0).getOtherList()).hasSize(2); + + assertThat(toOverride.getSomeList().get(0).getOtherList().get(0)).isNull(); + assertThat(toOverride.getSomeList().get(0).getOtherList().get(1)).isEqualTo(overriddenValue); + } + + @Test + public void setValueNullSimplePropertySet() { + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + + OverrideUtil.setValue(toOverride, "otherValue", overriddenValue); + + assertThat(toOverride.getOtherValue()).isNotNull(); + assertThat(toOverride.getOtherValue()).isEqualTo(overriddenValue); + } + + @Test + public void setValueNullPropertyOfComplexPropertySet() { + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + final ToOverride.OtherTestClass complexProperty = new ToOverride.OtherTestClass(); + toOverride.setComplexProperty(complexProperty); + + OverrideUtil.setValue(toOverride, "complexProperty.strVal", overriddenValue); + + assertThat(toOverride.getComplexProperty()).isNotNull(); + assertThat(toOverride.getComplexProperty().getStrVal()).isNotNull(); + assertThat(toOverride.getComplexProperty().getStrVal()).isEqualTo(overriddenValue); + } + + @Test + public void setValueSimpleCollectionExtended() { + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + final List simpleList = new ArrayList<>(); + simpleList.add("element1"); + toOverride.setSimpleList(simpleList); + + assertThat(toOverride.getSimpleList()).hasSize(1); + + OverrideUtil.setValue(toOverride, "simpleList[1]", overriddenValue); + + assertThat(toOverride.getSimpleList()).isNotNull(); + assertThat(toOverride.getSimpleList()).hasSize(2); + assertThat(toOverride.getSimpleList().get(0)).isEqualTo("element1"); + assertThat(toOverride.getSimpleList().get(1)).isEqualTo(overriddenValue); + } + + @Test + public void setValueComplexCollectionExtended() { + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + final ToOverride.OtherTestClass otherTestClass = new ToOverride.OtherTestClass(); + otherTestClass.setStrVal("element1"); + + final List someList = new ArrayList<>(); + someList.add(otherTestClass); + + toOverride.setSomeList(someList); + + assertThat(toOverride.getSomeList()).hasSize(1); + + OverrideUtil.setValue(toOverride, "someList[1].strVal", overriddenValue); + + assertThat(toOverride.getSomeList()).isNotNull(); + assertThat(toOverride.getSomeList()).hasSize(2); + assertThat(toOverride.getSomeList().get(0).getStrVal()).isEqualTo("element1"); + assertThat(toOverride.getSomeList().get(1).getStrVal()).isEqualTo(overriddenValue); + } + + @Test + public void setValueSimpleCollectionInComplexCollectionExtended() { + final String overriddenValue = "overridden test value"; + + final ToOverride toOverride = new ToOverride(); + final List otherList = new ArrayList<>(); + otherList.add("otherElement1"); + + final ToOverride.OtherTestClass otherTestClass = new ToOverride.OtherTestClass(); + otherTestClass.setOtherList(otherList); + + final List someList = new ArrayList<>(); + someList.add(otherTestClass); + + toOverride.setSomeList(someList); + + assertThat(toOverride.getSomeList()).hasSize(1); + assertThat(toOverride.getSomeList().get(0).getOtherList()).hasSize(1); + + OverrideUtil.setValue(toOverride, "someList[0].otherList[1]", overriddenValue); + + assertThat(toOverride.getSomeList()).isNotNull(); + assertThat(toOverride.getSomeList()).hasSize(1); + assertThat(toOverride.getSomeList().get(0)).isNotNull(); + assertThat(toOverride.getSomeList().get(0).getOtherList()).hasSize(2); + + assertThat(toOverride.getSomeList().get(0).getOtherList().get(0)).isEqualTo("otherElement1"); + assertThat(toOverride.getSomeList().get(0).getOtherList().get(1)).isEqualTo(overriddenValue); + } + + @Ignore + @Test + // TODO (cjh) Previously, peer overrides would be appended to any existing peers list. This has now been disabled + // so that behaviour is consistent across all options. It is now possible to overwrite existing peers or append the + // existing list depending on the position provided when calling the CLI, i.e. --peers[i]. It might be worth + // introducing an additional mode to always append so that the position doesn't have to be provided in these simpler + // situations? + public void setValuePeersAppended() { + assertThat(true).isFalse(); + } } diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/PicoCliDelegateTest.java similarity index 61% rename from cli/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java rename to cli/config-cli/src/test/java/com/quorum/tessera/config/cli/PicoCliDelegateTest.java index ca6ca4eafa..1c5689510a 100644 --- a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/PicoCliDelegateTest.java @@ -2,7 +2,7 @@ import com.quorum.tessera.cli.CliException; import com.quorum.tessera.cli.CliResult; -import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.config.Config; import com.quorum.tessera.config.KeyDataConfig; import com.quorum.tessera.config.Peer; import com.quorum.tessera.config.PrivateKeyType; @@ -13,6 +13,7 @@ import com.quorum.tessera.config.util.JaxbUtil; import com.quorum.tessera.key.generation.KeyGenerator; import com.quorum.tessera.test.util.ElUtil; +import org.assertj.core.util.Strings; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -36,29 +37,24 @@ import static com.quorum.tessera.test.util.ElUtil.createAndPopulatePaths; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -public class DefaultCliAdapterTest { +public class PicoCliDelegateTest { - private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCliAdapterTest.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PicoCliDelegateTest.class); - private DefaultCliAdapter cliAdapter; + private PicoCliDelegate cliDelegate; @Before public void setUp() { - MockKeyGeneratorFactory.reset(); - this.cliAdapter = new DefaultCliAdapter(); - } - - @Test - public void getType() { - assertThat(cliAdapter.getType()).isEqualTo(CliType.CONFIG); + cliDelegate = new PicoCliDelegate(); } @Test public void help() throws Exception { - final CliResult result = cliAdapter.execute("help"); + final CliResult result = cliDelegate.execute("help"); assertThat(result).isNotNull(); assertThat(result.getConfig()).isNotPresent(); @@ -67,9 +63,9 @@ public void help() throws Exception { } @Test - public void helpViaCall() throws Exception { - cliAdapter.setAllParameters(new String[] {"help"}); - final CliResult result = cliAdapter.call(); + public void noArgsPrintsHelp() throws Exception { + + final CliResult result = cliDelegate.execute(); assertThat(result).isNotNull(); assertThat(result.getConfig()).isNotPresent(); @@ -78,9 +74,9 @@ public void helpViaCall() throws Exception { } @Test - public void noArgsPrintsHelp() throws Exception { + public void subcommandWithNoArgsPrintsHelp() throws Exception { - final CliResult result = cliAdapter.execute(); + final CliResult result = cliDelegate.execute("keygen"); assertThat(result).isNotNull(); assertThat(result.getConfig()).isNotPresent(); @@ -92,7 +88,49 @@ public void noArgsPrintsHelp() throws Exception { public void withValidConfig() throws Exception { Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - CliResult result = cliAdapter.execute("-configfile", configFile.toString()); + CliResult result = cliDelegate.execute("-configfile", configFile.toString()); + + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getStatus()).isEqualTo(0); + assertThat(result.isSuppressStartup()).isFalse(); + } + + @Test + public void withValidConfigAndPidfile() throws Exception { + + Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); + + String tempDir = System.getProperty("java.io.tmpdir"); + Path pidFilePath = Paths.get(tempDir, UUID.randomUUID().toString()); + + assertThat(pidFilePath).doesNotExist(); + + CliResult result = + cliDelegate.execute("-configfile", configFile.toString(), "-pidfile", pidFilePath.toString()); + + assertThat(pidFilePath).exists(); + pidFilePath.toFile().deleteOnExit(); + + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getStatus()).isEqualTo(0); + assertThat(result.isSuppressStartup()).isFalse(); + } + + @Test + public void withValidConfigAndPidfileAlreadyExists() throws Exception { + + Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); + Path pidFilePath = Files.createTempFile(UUID.randomUUID().toString(), ""); + pidFilePath.toFile().deleteOnExit(); + + assertThat(pidFilePath).exists(); + + CliResult result = + cliDelegate.execute("-configfile", configFile.toString(), "-pidfile", pidFilePath.toString()); + + assertThat(pidFilePath).exists(); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -102,16 +140,15 @@ public void withValidConfig() throws Exception { @Test(expected = CliException.class) public void processArgsMissing() throws Exception { - cliAdapter.execute("-configfile"); + cliDelegate.execute("-configfile"); } @Test public void withConstraintViolations() throws Exception { - Path configFile = createAndPopulatePaths(getClass().getResource("/missing-config.json")); try { - cliAdapter.execute("-configfile", configFile.toString()); + cliDelegate.execute("-configfile", configFile.toString()); failBecauseExceptionWasNotThrown(ConstraintViolationException.class); } catch (ConstraintViolationException ex) { assertThat(ex.getConstraintViolations()).isNotEmpty(); @@ -141,16 +178,16 @@ public void keygenWithConfig() throws Exception { Map params = new HashMap<>(); params.put("unixSocketPath", unixSocketPath.toString()); - Path configFilePath = ElUtil.createTempFileFromTemplate(getClass().getResource("/keygen-sample.json"), params); + Path configFilePath = ElUtil.createTempFileFromTemplate(getClass().getResource("/sample-config.json"), params); CliResult result = - cliAdapter.execute( + cliDelegate.execute( "-keygen", "-filename", UUID.randomUUID().toString(), "-configfile", configFilePath.toString()); assertThat(result).isNotNull(); assertThat(result.getStatus()).isEqualTo(0); assertThat(result.getConfig()).isNotNull(); - assertThat(result.isSuppressStartup()).isFalse(); + assertThat(result.isSuppressStartup()).isTrue(); verify(keyGenerator).generate(anyString(), eq(null), eq(null)); verifyNoMoreInteractions(keyGenerator); @@ -159,32 +196,23 @@ public void keygenWithConfig() throws Exception { @Test public void keygenThenExit() throws Exception { - final CliResult result = cliAdapter.execute("-keygen", "--encryptor.type", "NACL"); + final CliResult result = cliDelegate.execute("-keygen", "--encryptor.type", "NACL"); assertThat(result).isNotNull(); assertThat(result.isSuppressStartup()).isTrue(); } @Test - public void fileNameWithoutKeygenArgThenExit() throws Exception { - - try { - cliAdapter.execute("-filename"); - failBecauseExceptionWasNotThrown(CliException.class); - } catch (CliException ex) { - assertThat(ex).hasMessage("Missing argument for option: filename"); - } - } - - @Test - public void outputWithoutKeygenOrConfig() { + public void noConfigfileOption() { - final Throwable throwable = catchThrowable(() -> cliAdapter.execute("-output", "bogus")); + final Throwable throwable = catchThrowable(() -> cliDelegate.execute("--pidfile", "bogus")); assertThat(throwable) .isInstanceOf(CliException.class) - .hasMessage("One or more: -configfile or -keygen or -updatepassword options are required."); + .hasMessage("Missing required option '--configfile '"); } + // TODO (cjh) remove ignore once implemented + @Ignore @Test public void output() throws Exception { @@ -212,7 +240,6 @@ public void output() throws Exception { Files.deleteIfExists(generatedKey); assertThat(Files.exists(generatedKey)).isFalse(); - Path keyConfigPath = Paths.get(getClass().getResource("/lockedprivatekey.json").toURI()); Path tempKeyFile = Files.createTempFile(UUID.randomUUID().toString(), ""); Path unixSocketPath = Files.createTempFile(UUID.randomUUID().toString(), ".ipc"); @@ -222,21 +249,27 @@ public void output() throws Exception { Path configFile = createAndPopulatePaths(getClass().getResource("/keygen-sample.json")); CliResult result = - cliAdapter.execute( - "-keygen", keyConfigPath.toString(), - "-filename", tempKeyFile.toAbsolutePath().toString(), - "-output", generatedKey.toFile().getPath(), - "-configfile", configFile.toString()); + cliDelegate.execute( + "-keygen", + "-filename", + tempKeyFile.toAbsolutePath().toString(), + "-output", + generatedKey.toFile().getPath(), + "-configfile", + configFile.toString()); assertThat(result).isNotNull(); assertThat(Files.exists(generatedKey)).isTrue(); try { - cliAdapter.execute( - "-keygen", keyConfigPath.toString(), - "-filename", UUID.randomUUID().toString(), - "-output", generatedKey.toFile().getPath(), - "-configfile", configFile.toString()); + cliDelegate.execute( + "-keygen", + "-filename", + UUID.randomUUID().toString(), + "-output", + generatedKey.toFile().getPath(), + "-configfile", + configFile.toString()); failBecauseExceptionWasNotThrown(Exception.class); } catch (Exception ex) { assertThat(ex).isInstanceOf(UncheckedIOException.class); @@ -249,7 +282,7 @@ public void dynOption() throws Exception { Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - CliResult result = cliAdapter.execute("-configfile", configFile.toString(), "-jdbc.username", "somename"); + CliResult result = cliDelegate.execute("-configfile", configFile.toString(), "-o", "jdbc.username=somename"); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -257,7 +290,7 @@ public void dynOption() throws Exception { assertThat(result.getConfig().get().getJdbcConfig().getPassword()).isEqualTo("tiger"); } - @Ignore + @Test public void withInvalidPath() throws Exception { // unixSocketPath Map params = new HashMap<>(); @@ -268,13 +301,13 @@ public void withInvalidPath() throws Exception { ElUtil.createTempFileFromTemplate(getClass().getResource("/sample-config-invalidpath.json"), params); try { - cliAdapter.execute("-configfile", configFile.toString()); + cliDelegate.execute("-configfile", configFile.toString()); failBecauseExceptionWasNotThrown(ConstraintViolationException.class); } catch (ConstraintViolationException ex) { assertThat(ex.getConstraintViolations()) - .hasSize(1) + .hasSize(2) .extracting("messageTemplate") - .containsExactly("{UnsupportedKeyPair.message}"); + .containsExactly("File does not exist", "File does not exist"); } } @@ -289,13 +322,13 @@ public void withEmptyConfigOverrideAll() throws Exception { Files.write(configFile, "{}".getBytes()); try { CliResult result = - cliAdapter.execute( + cliDelegate.execute( "-configfile", configFile.toString(), - "--unixSocketFile", - unixSocketFile.toString(), - "--encryptor.type", - "NACL"); + "-o", + Strings.join("unixSocketFile=", unixSocketFile.toString()).with(""), + "-o", + "encryptor.type=NACL"); assertThat(result).isNotNull(); failBecauseExceptionWasNotThrown(ConstraintViolationException.class); @@ -312,7 +345,12 @@ public void overrideAlwaysSendTo() throws Exception { Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); CliResult result = null; try { - result = cliAdapter.execute("-configfile", configFile.toString(), "-alwaysSendTo", alwaysSendToKey); + result = + cliDelegate.execute( + "-configfile", + configFile.toString(), + "-o", + Strings.join("alwaysSendTo[1]=", alwaysSendToKey).with("")); } catch (Exception ex) { fail(ex.getMessage()); } @@ -329,13 +367,10 @@ public void overridePeers() throws Exception { Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); CliResult result = - cliAdapter.execute( - "-configfile", - configFile.toString(), - "-peer.url", - "anotherpeer", - "-peer.url", - "yetanotherpeer"); + cliDelegate.execute( + "-configfile", configFile.toString(), + "-o", "peer[2].url=anotherpeer", + "--override", "peer[3].url=yetanotherpeer"); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -360,7 +395,7 @@ public void updatingPasswordsDoesntProcessOtherOptions() throws Exception { Files.write(key, JaxbUtil.marshalToString(startingKey).getBytes()); final CliResult result = - cliAdapter.execute( + cliDelegate.execute( "-updatepassword", "--keys.keyData.privateKeyPath", key.toString(), @@ -375,13 +410,13 @@ public void updatingPasswordsDoesntProcessOtherOptions() throws Exception { @Test public void suppressStartupForKeygenOption() throws Exception { - final CliResult cliResult = cliAdapter.execute("-keygen", "--encryptor.type", "NACL"); + final CliResult cliResult = cliDelegate.execute("-keygen", "--encryptor.type", "NACL"); assertThat(cliResult.isSuppressStartup()).isTrue(); } @Test - public void allowStartupForKeygenAndConfigfileOptions() throws Exception { + public void suppressStartupForKeygenOptionWithConfigfile() throws Exception { final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); Path publicKeyPath = Files.createTempFile(UUID.randomUUID().toString(), ""); Path privateKeyPath = Files.createTempFile(UUID.randomUUID().toString(), ""); @@ -394,31 +429,83 @@ public void allowStartupForKeygenAndConfigfileOptions() throws Exception { final Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - final CliResult cliResult = cliAdapter.execute("-keygen", "-configfile", configFile.toString()); + final CliResult cliResult = cliDelegate.execute("-keygen", "-configfile", configFile.toString()); - assertThat(cliResult.isSuppressStartup()).isFalse(); + assertThat(cliResult.isSuppressStartup()).isTrue(); } @Test - public void suppressStartupForKeygenAndVaultUrlAndConfigfileOptions() throws Exception { - final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); + public void subcommandExceptionIsThrown() { + Throwable ex = catchThrowable(() -> cliDelegate.execute("-keygen", "-keygenvaulturl", "urlButNoVaultType")); - final FilesystemKeyPair keypair = new FilesystemKeyPair(Paths.get(""), Paths.get(""), null); - when(keyGenerator.generate(anyString(), eq(null), eq(null))).thenReturn(keypair); + assertThat(ex).isNotNull(); + assertThat(ex).isInstanceOf(CliException.class); + } - final Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); - final String vaultUrl = "https://test.vault.azure.net"; + @Test + public void withValidConfigAndJdbcOveride() throws Exception { - final CliResult cliResult = - cliAdapter.execute( - "-keygen", - "-keygenvaulttype", - "AZURE", - "-keygenvaulturl", - vaultUrl, - "-configfile", - configFile.toString()); + Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); + CliResult result = cliDelegate.execute("-configfile", configFile.toString(), "-jdbc.autoCreateTables", "true"); - assertThat(cliResult.isSuppressStartup()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getStatus()).isEqualTo(0); + + assertThat(result.isSuppressStartup()).isFalse(); + + Config config = result.getConfig().get(); + assertThat(config.getJdbcConfig()).isNotNull(); + assertThat(config.getJdbcConfig().isAutoCreateTables()).isTrue(); + } + + @Test + public void withValidConfigAndUnmatchableDynamicOption() throws Exception { + + Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); + CliResult result = cliDelegate.execute("-configfile", configFile.toString(), "-bogus"); + + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getStatus()).isEqualTo(0); + + assertThat(result.isSuppressStartup()).isFalse(); + + } + + @Test + public void withValidConfigAndUnmatchableDynamicOptionWithValue() throws Exception { + + Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); + CliResult result = cliDelegate.execute("-configfile", configFile.toString(), "-bogus","bogus value"); + + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getStatus()).isEqualTo(0); + + assertThat(result.isSuppressStartup()).isFalse(); + + } + + @Test + public void withValidConfigAndJdbcOverides() throws Exception { + + Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); + CliResult result = cliDelegate.execute("-configfile", configFile.toString(), "-jdbc.autoCreateTables", "true", "-jdbc.url", "someurl"); + + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getConfig()).isPresent(); + assertThat(result.getStatus()).isEqualTo(0); + + assertThat(result.isSuppressStartup()).isFalse(); + + Config config = result.getConfig().get(); + assertThat(config.getJdbcConfig()).isNotNull(); + assertThat(config.getJdbcConfig().isAutoCreateTables()).isTrue(); + assertThat(config.getJdbcConfig().getUrl()).isEqualTo("someurl"); } } diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/ToOverride.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/ToOverride.java new file mode 100644 index 0000000000..93705e3a61 --- /dev/null +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/ToOverride.java @@ -0,0 +1,96 @@ +package com.quorum.tessera.config.cli; + +import javax.xml.bind.annotation.XmlElement; +import java.util.List; + +class ToOverride { + @XmlElement(name = "some_value") + private String someValue; + + @XmlElement + private String otherValue; + + @XmlElement + private OtherTestClass complexProperty; + + @XmlElement + private List someList; + + @XmlElement + private List simpleList; + + String getSomeValue() { + return someValue; + } + + String getOtherValue() { + return otherValue; + } + + List getSimpleList() { + return simpleList; + } + + OtherTestClass getComplexProperty() { + return complexProperty; + } + + List getSomeList() { + return someList; + } + + void setSomeValue(String someValue) { + this.someValue = someValue; + } + + void setOtherValue(String otherValue) { + this.otherValue = otherValue; + } + + void setSomeList(List someList) { + this.someList = someList; + } + + void setSimpleList(List simpleList) { + this.simpleList = simpleList; + } + + void setComplexProperty(OtherTestClass otherTestClass) { + complexProperty = otherTestClass; + } + + static class OtherTestClass { + @XmlElement + private int count; + + @XmlElement + private String strVal; + + @XmlElement + private List otherList; + + int getCount() { + return count; + } + + void setCount(int count) { + this.count = count; + } + + String getStrVal() { + return strVal; + } + + void setStrVal(String strVal) { + this.strVal = strVal; + } + + List getOtherList() { + return otherList; + } + + void setOtherList(List otherList) { + this.otherList = otherList; + } + } +} diff --git a/cli/admin-cli/src/test/java/com/quorum/tessera/admin/cli/AdminCliAdapterTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/admin/AdminCliAdapterTest.java similarity index 95% rename from cli/admin-cli/src/test/java/com/quorum/tessera/admin/cli/AdminCliAdapterTest.java rename to cli/config-cli/src/test/java/com/quorum/tessera/config/cli/admin/AdminCliAdapterTest.java index 6c378fc2cb..43c8f2a9a3 100644 --- a/cli/admin-cli/src/test/java/com/quorum/tessera/admin/cli/AdminCliAdapterTest.java +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/admin/AdminCliAdapterTest.java @@ -1,4 +1,4 @@ -package com.quorum.tessera.admin.cli; +package com.quorum.tessera.config.cli.admin; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; diff --git a/cli/admin-cli/src/test/java/com/quorum/tessera/admin/cli/subcommands/AddPeerCommandTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/admin/subcommands/AddPeerCommandTest.java similarity index 98% rename from cli/admin-cli/src/test/java/com/quorum/tessera/admin/cli/subcommands/AddPeerCommandTest.java rename to cli/config-cli/src/test/java/com/quorum/tessera/config/cli/admin/subcommands/AddPeerCommandTest.java index 5869dd7238..f012fe87e1 100644 --- a/cli/admin-cli/src/test/java/com/quorum/tessera/admin/cli/subcommands/AddPeerCommandTest.java +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/admin/subcommands/AddPeerCommandTest.java @@ -1,4 +1,4 @@ -package com.quorum.tessera.admin.cli.subcommands; +package com.quorum.tessera.config.cli.admin.subcommands; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.parsers.ConfigurationMixin; diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/EncryptorConfigParserTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/EncryptorConfigParserTest.java deleted file mode 100644 index b439f1cb27..0000000000 --- a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/EncryptorConfigParserTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.quorum.tessera.config.cli.parsers; - -import com.quorum.tessera.config.Config; -import com.quorum.tessera.config.EncryptorConfig; -import com.quorum.tessera.config.EncryptorType; -import com.quorum.tessera.io.FilesDelegate; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Path; -import org.apache.commons.cli.CommandLine; -import static org.assertj.core.api.Assertions.*; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import static org.mockito.Mockito.*; - -public class EncryptorConfigParserTest { - - private EncryptorConfigParser parser; - - private CommandLine commandLine; - - private FilesDelegate filesDelegate; - - @Before - public void onSetup() { - commandLine = mock(CommandLine.class); - filesDelegate = mock(FilesDelegate.class); - this.parser = new EncryptorConfigParser(filesDelegate); - } - - @After - public void onTearDown() { - verifyNoMoreInteractions(commandLine); - } - - @Test - public void elipticalCurveNoPropertiesDefined() throws IOException { - when(commandLine.hasOption("configfile")).thenReturn(false); - - when(commandLine.getOptionValue("encryptor.type", EncryptorType.NACL.name())) - .thenReturn(EncryptorType.EC.name()); - - EncryptorConfig result = parser.parse(commandLine); - - assertThat(result.getType()).isEqualTo(EncryptorType.EC); - assertThat(result.getProperties()).isEmpty(); - - verify(commandLine).getOptionValue("encryptor.type", EncryptorType.NACL.name()); - verify(commandLine).hasOption("configfile"); - - verify(commandLine).getOptionValue("encryptor.symmetricCipher"); - verify(commandLine).getOptionValue("encryptor.ellipticCurve"); - verify(commandLine).getOptionValue("encryptor.nonceLength"); - verify(commandLine).getOptionValue("encryptor.sharedKeyLength"); - } - - @Test - public void elipticalCurveWithDefinedProperties() throws IOException { - - when(commandLine.getOptionValue("encryptor.type", EncryptorType.NACL.name())) - .thenReturn(EncryptorType.EC.name()); - - Config config = new Config(); - config.setEncryptor(new EncryptorConfig()); - config.getEncryptor().setType(EncryptorType.EC); - - when(commandLine.hasOption("configfile")).thenReturn(false); - - when(commandLine.getOptionValue("encryptor.type")).thenReturn(EncryptorType.EC.name()); - - when(commandLine.getOptionValue("encryptor.symmetricCipher")).thenReturn("somecipher"); - when(commandLine.getOptionValue("encryptor.ellipticCurve")).thenReturn("somecurve"); - when(commandLine.getOptionValue("encryptor.nonceLength")).thenReturn("3"); - when(commandLine.getOptionValue("encryptor.sharedKeyLength")).thenReturn("2"); - - EncryptorConfig result = parser.parse(commandLine); - - assertThat(result.getType()).isEqualTo(EncryptorType.EC); - assertThat(result.getProperties()) - .containsOnlyKeys("symmetricCipher", "ellipticCurve", "nonceLength", "sharedKeyLength"); - - assertThat(result.getProperties().get("symmetricCipher")).isEqualTo("somecipher"); - assertThat(result.getProperties().get("ellipticCurve")).isEqualTo("somecurve"); - assertThat(result.getProperties().get("nonceLength")).isEqualTo("3"); - assertThat(result.getProperties().get("sharedKeyLength")).isEqualTo("2"); - - verify(commandLine).getOptionValue("encryptor.symmetricCipher"); - verify(commandLine).getOptionValue("encryptor.ellipticCurve"); - verify(commandLine).getOptionValue("encryptor.nonceLength"); - verify(commandLine).getOptionValue("encryptor.sharedKeyLength"); - - verify(commandLine).getOptionValue("encryptor.type", EncryptorType.NACL.name()); - verify(commandLine).hasOption("configfile"); - } - - @Test - public void noEncryptorTypeDefinedAndNoConfigFile() throws IOException { - - when(commandLine.getOptionValue("encryptor.type", EncryptorType.NACL.name())) - .thenReturn(EncryptorType.NACL.name()); - when(commandLine.hasOption("configfile")).thenReturn(false); - EncryptorConfig result = parser.parse(commandLine); - assertThat(result.getType()).isEqualTo(EncryptorType.NACL); - assertThat(result.getProperties()).isEmpty(); - - verify(commandLine).getOptionValue("encryptor.type", EncryptorType.NACL.name()); - verify(commandLine).hasOption("configfile"); - } - - @Test - public void keyGenUsedDefaulIfNoTypeDefined() throws Exception { - when(commandLine.getOptionValue("encryptor.type", EncryptorType.NACL.name())) - .thenReturn(EncryptorType.NACL.name()); - when(commandLine.hasOption("configfile")).thenReturn(true); - when(commandLine.getOptionValue("configfile")).thenReturn("somepath"); - - InputStream inputStream = new ByteArrayInputStream("{}".getBytes()); - when(filesDelegate.newInputStream(any(Path.class))).thenReturn(inputStream); - - EncryptorConfig result = parser.parse(commandLine); - - assertThat(result.getType()).isEqualTo(EncryptorType.NACL); - assertThat(result.getProperties()).isEmpty(); - - verify(commandLine).getOptionValue("encryptor.type", EncryptorType.NACL.name()); - verify(commandLine).getOptionValue("configfile"); - verify(commandLine).hasOption("configfile"); - - verify(filesDelegate).newInputStream(any(Path.class)); - } -} diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParserTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParserTest.java deleted file mode 100644 index 00e1c283f5..0000000000 --- a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParserTest.java +++ /dev/null @@ -1,378 +0,0 @@ -package com.quorum.tessera.config.cli.parsers; - -import com.quorum.tessera.cli.CliException; -import com.quorum.tessera.config.ArgonOptions; -import com.quorum.tessera.config.EncryptorConfig; -import com.quorum.tessera.config.EncryptorType; -import com.quorum.tessera.config.cli.keys.MockKeyGeneratorFactory; -import com.quorum.tessera.config.keypairs.ConfigKeyPair; -import com.quorum.tessera.key.generation.KeyGenerator; -import org.apache.commons.cli.CommandLine; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -public class KeyGenerationParserTest { - - private KeyGenerationParser parser = - new KeyGenerationParser( - new EncryptorConfig() { - { - setType(EncryptorType.NACL); - setProperties(Collections.emptyMap()); - } - }); - - private CommandLine commandLine = mock(CommandLine.class); - - @Test - public void notProvidingArgonOptionsGivesNull() throws Exception { - final Path keyLocation = Files.createTempFile(UUID.randomUUID().toString(), ""); - - when(commandLine.hasOption("keygen")).thenReturn(true); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("filename")).thenReturn(keyLocation.toString()); - when(commandLine.hasOption("keygenconfig")).thenReturn(false); - - final List result = parser.parse(commandLine); - - assertThat(result).isNotNull().hasSize(1); - - final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(ArgonOptions.class); - verify(keyGenerator).generate(eq(keyLocation.toString()), captor.capture(), eq(null)); - - assertThat(captor.getAllValues()).hasSize(1); - assertThat(captor.getValue()).isNull(); - } - - @Test - public void providingArgonOptionsGetSentCorrectly() throws Exception { - final String options = "{\"variant\": \"id\",\"memory\": 100,\"iterations\": 7,\"parallelism\": 22}"; - final Path argonOptions = Files.createTempFile(UUID.randomUUID().toString(), ""); - Files.write(argonOptions, options.getBytes()); - - final Path keyLocation = Files.createTempFile(UUID.randomUUID().toString(), ""); - - when(commandLine.hasOption("keygen")).thenReturn(true); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("filename")).thenReturn(keyLocation.toString()); - when(commandLine.hasOption("keygenconfig")).thenReturn(true); - when(commandLine.getOptionValue("keygenconfig")).thenReturn(argonOptions.toString()); - - final List result = parser.parse(commandLine); - - assertThat(result).isNotNull().hasSize(1); - - final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(ArgonOptions.class); - verify(keyGenerator).generate(eq(keyLocation.toString()), captor.capture(), eq(null)); - - assertThat(captor.getAllValues()).hasSize(1); - assertThat(captor.getValue().getAlgorithm()).isEqualTo("id"); - assertThat(captor.getValue().getIterations()).isEqualTo(7); - assertThat(captor.getValue().getMemory()).isEqualTo(100); - assertThat(captor.getValue().getParallelism()).isEqualTo(22); - } - - @Test - public void keygenWithNoName() throws Exception { - - when(commandLine.hasOption("keygen")).thenReturn(true); - when(commandLine.hasOption("filename")).thenReturn(false); - when(commandLine.hasOption("keygenconfig")).thenReturn(false); - - final List result = this.parser.parse(commandLine); - - assertThat(result).isNotNull().hasSize(1); - - final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); - verify(keyGenerator).generate("", null, null); - } - - @Test - public void keygenNotGivenReturnsEmptyList() throws Exception { - - when(commandLine.hasOption("keygen")).thenReturn(false); - when(commandLine.hasOption("filename")).thenReturn(false); - when(commandLine.hasOption("keygenconfig")).thenReturn(false); - - final List result = this.parser.parse(commandLine); - - assertThat(result).isNotNull().hasSize(0); - - final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); - verifyZeroInteractions(keyGenerator); - } - - @Test - public void vaultOptionsNotUsedIfNoneProvided() throws Exception { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(false); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(false); - - this.parser.parse(commandLine); - - verify(commandLine, times(0)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(0)).getOptionValue("keygenvaulturl"); - } - - @Test - public void ifAllVaultOptionsProvidedAndValidThenOkay() throws Exception { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("AZURE"); - - this.parser.parse(commandLine); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - } - - @Test - public void ifAzureVaultTypeOptionProvidedButNoVaultUrlThenValidationException() { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(false); - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("AZURE"); - - Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - - assertThat(ex).isInstanceOf(ConstraintViolationException.class); - - Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); - - assertThat(violations.size()).isEqualTo(1); - - ConstraintViolation violation = violations.iterator().next(); - - assertThat(violation.getPropertyPath().toString()).isEqualTo("url"); - assertThat(violation.getMessage()).isEqualTo("may not be null"); - } - - @Test - public void ifHashicorpVaultTypeOptionAndFilenameProvidedButNoVaultUrlThenValidationException() { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(false); - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); - - Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - - assertThat(ex).isInstanceOf(ConstraintViolationException.class); - - Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); - - assertThat(violations.size()).isEqualTo(1); - - ConstraintViolation violation = violations.iterator().next(); - - assertThat(violation.getPropertyPath().toString()).isEqualTo("url"); - assertThat(violation.getMessage()).isEqualTo("may not be null"); - } - - @Test - public void ifOnlyVaultUrlOptionProvidedThenException() { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(false); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); - - Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); - - assertThat(ex).isInstanceOf(CliException.class); - assertThat(ex.getMessage()).isEqualTo("Key vault type either not provided or not recognised"); - } - - @Test - public void ifVaultOptionsProvidedButTypeUnknownThenException() { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("unknown"); - - Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); - - assertThat(ex).isInstanceOf(CliException.class); - assertThat(ex.getMessage()).isEqualTo("Key vault type either not provided or not recognised"); - } - - @Test - public void noFilenameProvidedWhenUsingHashicorpVaultThrowsException() { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); - when(commandLine.hasOption("filename")).thenReturn(false); - - Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); - - assertThat(ex).isInstanceOf(CliException.class); - assertThat(ex.getMessage()) - .isEqualTo("At least one -filename must be provided when saving generated keys in a Hashicorp Vault"); - } - - @Test - public void ifAllVaultOptionsAndFilenameProvidedForHashicorpThenOkay() throws Exception { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); - when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); - when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); - when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); - - Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); - tempPath.toFile().deleteOnExit(); - - when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); - when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); - - this.parser.parse(commandLine); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); - verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); - verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); - verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); - } - - @Test - public void ifHashicorpTlsOptionsProvidedButPathsDontExistThenValidationException() { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); - when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); - when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); - when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); - when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn("non/existent/path"); - when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn("non/existent/path"); - - Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); - verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); - verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); - verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); - - assertThat(ex).isInstanceOf(ConstraintViolationException.class); - - Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); - - assertThat(violations.size()).isEqualTo(2); - - Iterator> iterator = violations.iterator(); - - assertThat(iterator.next().getMessage()).isEqualTo("File does not exist"); - assertThat(iterator.next().getMessage()).isEqualTo("File does not exist"); - } - - @Test - public void lowercaseVaultTypeIsOkay() throws Exception { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); - when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); - when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); - when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); - - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("hashicorp"); - - Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); - tempPath.toFile().deleteOnExit(); - - when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); - when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); - - this.parser.parse(commandLine); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); - verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); - verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); - verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); - } - - @Test - public void leadingWhitespaceVaultTypeIsOkay() throws Exception { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); - when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); - when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); - when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); - - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn(" HASHICORP"); - - Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); - tempPath.toFile().deleteOnExit(); - - when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); - when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); - - this.parser.parse(commandLine); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); - verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); - verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); - verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); - } - - @Test - public void trailingWhitespaceVaultTypeIsOkay() throws Exception { - when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); - when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); - when(commandLine.hasOption("filename")).thenReturn(true); - when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); - when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); - when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); - when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); - - when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP "); - - Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); - tempPath.toFile().deleteOnExit(); - - when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); - when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); - - this.parser.parse(commandLine); - - verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); - verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); - verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); - verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); - verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); - verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); - } -} diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyUpdateParserTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyUpdateParserTest.java deleted file mode 100644 index 69b5b95fba..0000000000 --- a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyUpdateParserTest.java +++ /dev/null @@ -1,336 +0,0 @@ -package com.quorum.tessera.config.cli.parsers; - -import com.quorum.tessera.config.*; -import com.quorum.tessera.config.keys.KeyEncryptor; -import com.quorum.tessera.config.util.JaxbUtil; -import com.quorum.tessera.encryption.PrivateKey; -import com.quorum.tessera.passwords.PasswordReader; -import org.apache.commons.cli.*; -import org.junit.Before; -import org.junit.Test; - -import javax.xml.bind.UnmarshalException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Mockito.*; - -public class KeyUpdateParserTest { - - private PasswordReader passwordReader; - - private KeyUpdateParser parser; - - private Options options; - - private KeyEncryptor keyEncryptor; - - @Before - public void init() { - this.passwordReader = mock(PasswordReader.class); - when(passwordReader.requestUserPassword()).thenReturn("newPassword"); - - this.keyEncryptor = mock(KeyEncryptor.class); - this.parser = new KeyUpdateParser(keyEncryptor, passwordReader); - - this.options = new Options(); - - this.options.addOption(Option.builder().longOpt("keys.keyData.config.data.aopts.algorithm").hasArg().build()); - this.options.addOption(Option.builder().longOpt("keys.keyData.config.data.aopts.iterations").hasArg().build()); - this.options.addOption(Option.builder().longOpt("keys.keyData.config.data.aopts.memory").hasArg().build()); - this.options.addOption(Option.builder().longOpt("keys.keyData.config.data.aopts.parallelism").hasArg().build()); - - this.options.addOption(Option.builder().longOpt("keys.passwords").hasArg().build()); - this.options.addOption(Option.builder().longOpt("keys.passwordFile").hasArg().build()); - - this.options.addOption(Option.builder().longOpt("keys.keyData.privateKeyPath").hasArg().build()); - } - - // Argon Option tests - @Test - public void noArgonOptionsGivenHasDefaults() throws ParseException { - final CommandLine commandLine = new DefaultParser().parse(options, new String[] {}); - - final ArgonOptions argonOptions = KeyUpdateParser.argonOptions(commandLine); - - assertThat(argonOptions.getAlgorithm()).isEqualTo("i"); - assertThat(argonOptions.getParallelism()).isEqualTo(4); - assertThat(argonOptions.getMemory()).isEqualTo(1048576); - assertThat(argonOptions.getIterations()).isEqualTo(10); - } - - @Test - public void argonOptionsGivenHasOverrides() throws ParseException { - final String[] args = - new String[] { - "--keys.keyData.config.data.aopts.algorithm", "d", - "--keys.keyData.config.data.aopts.memory", "100", - "--keys.keyData.config.data.aopts.iterations", "100", - "--keys.keyData.config.data.aopts.parallelism", "100" - }; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final ArgonOptions argonOptions = KeyUpdateParser.argonOptions(commandLine); - - assertThat(argonOptions.getAlgorithm()).isEqualTo("d"); - assertThat(argonOptions.getParallelism()).isEqualTo(100); - assertThat(argonOptions.getMemory()).isEqualTo(100); - assertThat(argonOptions.getIterations()).isEqualTo(100); - } - - // Password reading tests - @Test - public void inlinePasswordParsed() throws ParseException, IOException { - final String[] args = new String[] {"--keys.passwords", "pass"}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final List passwords = KeyUpdateParser.passwords(commandLine); - - assertThat(passwords).isNotNull().hasSize(1).containsExactly("pass"); - } - - @Test - public void passwordFileParsedAndRead() throws ParseException, IOException { - final Path passwordFile = Files.createTempFile("passwords", ".txt"); - Files.write(passwordFile, "passwordInsideFile\nsecondPassword".getBytes()); - - final String[] args = new String[] {"--keys.passwordFile", passwordFile.toAbsolutePath().toString()}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final List passwords = KeyUpdateParser.passwords(commandLine); - - assertThat(passwords).isNotNull().hasSize(2).containsExactly("passwordInsideFile", "secondPassword"); - } - - @Test - public void passwordFileThrowsErrorIfCantBeRead() throws ParseException { - final String[] args = new String[] {"--keys.passwordFile", "/tmp/passwords.txt"}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final Throwable throwable = catchThrowable(() -> KeyUpdateParser.passwords(commandLine)); - - assertThat(throwable).isNotNull().isInstanceOf(IOException.class); - } - - @Test - public void emptyListGivenForNoPasswords() throws ParseException, IOException { - final String[] args = new String[] {}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final List passwords = KeyUpdateParser.passwords(commandLine); - - assertThat(passwords).isNotNull().isEmpty(); - } - - // key file tests - @Test - public void noPrivateKeyGivenThrowsError() throws ParseException { - final String[] args = new String[] {}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final Throwable throwable = catchThrowable(() -> KeyUpdateParser.privateKeyPath(commandLine)); - - assertThat(throwable) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Private key path cannot be null when updating key password"); - } - - @Test - public void cantReadPrivateKeyThrowsError() throws ParseException { - final String[] args = new String[] {"--keys.keyData.privateKeyPath", "/tmp/nonexisting.txt"}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final Throwable throwable = catchThrowable(() -> KeyUpdateParser.privateKeyPath(commandLine)); - - assertThat(throwable).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void privateKeyExistsReturnsPath() throws ParseException, IOException { - final Path key = Files.createTempFile("key", ".key"); - - final String[] args = new String[] {"--keys.keyData.privateKeyPath", key.toString()}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final Path path = KeyUpdateParser.privateKeyPath(commandLine); - - assertThat(path).isEqualTo(key); - } - - // key fetching tests - @Test - public void unlockedKeyReturnedProperly() { - final KeyDataConfig kdc = - new KeyDataConfig( - new PrivateKeyData("/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc=", null, null, null, null), - PrivateKeyType.UNLOCKED); - - final PrivateKey key = this.parser.getExistingKey(kdc, emptyList()); - - String encodedKeyValue = Base64.getEncoder().encodeToString(key.getKeyBytes()); - - assertThat(encodedKeyValue).isEqualTo("/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc="); - } - - @Test - public void lockedKeyFailsWithNoPasswordsMatching() { - - final KeyDataConfig kdc = - new KeyDataConfig( - new PrivateKeyData( - null, - "dwixVoY+pOI2FMuu4k0jLqN/naQiTzWe", - "JoPVq9G6NdOb+Ugv+HnUeA==", - "6Jd/MXn29fk6jcrFYGPb75l7sDJae06I3Y1Op+bZSZqlYXsMpa/8lLE29H0sX3yw", - new ArgonOptions("id", 1, 1024, 1)), - PrivateKeyType.LOCKED); - - final Throwable throwable = catchThrowable(() -> this.parser.getExistingKey(kdc, singletonList("wrong"))); - - assertThat(throwable) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Locked key but no valid password given"); - } - - @Test - public void lockedKeySucceedsWithPasswordsMatching() { - PrivateKeyData privateKeyData = - new PrivateKeyData( - null, - "dwixVoY+pOI2FMuu4k0jLqN/naQiTzWe", - "JoPVq9G6NdOb+Ugv+HnUeA==", - "6Jd/MXn29fk6jcrFYGPb75l7sDJae06I3Y1Op+bZSZqlYXsMpa/8lLE29H0sX3yw", - new ArgonOptions("id", 1, 1024, 1)); - - final KeyDataConfig kdc = - new KeyDataConfig( - new PrivateKeyData( - null, - "dwixVoY+pOI2FMuu4k0jLqN/naQiTzWe", - "JoPVq9G6NdOb+Ugv+HnUeA==", - "6Jd/MXn29fk6jcrFYGPb75l7sDJae06I3Y1Op+bZSZqlYXsMpa/8lLE29H0sX3yw", - new ArgonOptions("id", 1, 1024, 1)), - PrivateKeyType.LOCKED); - - PrivateKey privateKey = mock(PrivateKey.class); - when(privateKey.getKeyBytes()).thenReturn("SUCCESS".getBytes()); - when(keyEncryptor.decryptPrivateKey(privateKeyData, "testpassword")).thenReturn(privateKey); - - final PrivateKey result = this.parser.getExistingKey(kdc, singletonList("testpassword")); - - assertThat(result.getKeyBytes()).isEqualTo("SUCCESS".getBytes()); - } - - @Test - public void lockedKeySucceedsWithAtleastOnePasswordsMatching() { - - PrivateKeyData privateKeyData = mock(PrivateKeyData.class); - - final KeyDataConfig kdc = new KeyDataConfig(privateKeyData, PrivateKeyType.LOCKED); - - PrivateKey privateKey = mock(PrivateKey.class); - when(privateKey.getKeyBytes()).thenReturn("SUCCESS".getBytes()); - - when(keyEncryptor.decryptPrivateKey(privateKeyData, "wrong")).thenReturn(null); - when(keyEncryptor.decryptPrivateKey(privateKeyData, "testpassword")).thenReturn(privateKey); - - final PrivateKey key = this.parser.getExistingKey(kdc, Arrays.asList("wrong", "testpassword")); - - assertThat(key).isNotNull(); - - assertThat(key.getKeyBytes()).isEqualTo("SUCCESS".getBytes()); - } - - @Test - public void loadingMalformedKeyfileThrowsError() throws IOException, ParseException { - final Path key = Files.createTempFile("key", ".key"); - Files.write(key, "BAD JSON DATA".getBytes()); - - final String[] args = new String[] {"--keys.keyData.privateKeyPath", key.toString()}; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - final Throwable throwable = catchThrowable(() -> this.parser.parse(commandLine)); - - assertThat(throwable).isInstanceOf(ConfigException.class).hasCauseExactlyInstanceOf(UnmarshalException.class); - } - - @Test - public void keyGetsUpdated() throws IOException, ParseException { - final KeyDataConfig startingKey = - JaxbUtil.unmarshal(getClass().getResourceAsStream("/lockedprivatekey.json"), KeyDataConfig.class); - - final Path key = Files.createTempFile("key", ".key"); - Files.write(key, JaxbUtil.marshalToString(startingKey).getBytes()); - - final String[] args = - new String[] { - "--keys.keyData.privateKeyPath", key.toString(), - "--keys.passwords", "testpassword", - "--keys.keyData.config.data.aopts.algorithm", "id", - "--keys.keyData.config.data.aopts.memory", "2048", - "--keys.keyData.config.data.aopts.iterations", "1", - "--keys.keyData.config.data.aopts.parallelism", "1" - }; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - PrivateKey privatekey = mock(PrivateKey.class); - when(keyEncryptor.decryptPrivateKey(any(PrivateKeyData.class), anyString())).thenReturn(privatekey); - - PrivateKeyData privateKeyData = mock(PrivateKeyData.class); - - when(keyEncryptor.encryptPrivateKey(any(PrivateKey.class), anyString(), any(ArgonOptions.class))) - .thenReturn(privateKeyData); - - this.parser.parse(commandLine); - - final KeyDataConfig endingKey = JaxbUtil.unmarshal(Files.newInputStream(key), KeyDataConfig.class); - - assertThat(endingKey.getSbox()).isNotEqualTo(startingKey.getSbox()); - assertThat(endingKey.getSnonce()).isNotEqualTo(startingKey.getSnonce()); - assertThat(endingKey.getAsalt()).isNotEqualTo(startingKey.getAsalt()); - } - - @Test - public void keyGetsUpdatedToNoPassword() throws IOException, ParseException { - final KeyDataConfig startingKey = - JaxbUtil.unmarshal(getClass().getResourceAsStream("/lockedprivatekey.json"), KeyDataConfig.class); - - when(passwordReader.requestUserPassword()).thenReturn(""); - - final Path key = Files.createTempFile("key", ".key"); - Files.write(key, JaxbUtil.marshalToString(startingKey).getBytes()); - - final String[] args = - new String[] { - "--keys.keyData.privateKeyPath", key.toString(), - "--keys.passwords", "testpassword", - "--keys.keyData.config.data.aopts.algorithm", "id", - "--keys.keyData.config.data.aopts.memory", "2048", - "--keys.keyData.config.data.aopts.iterations", "1", - "--keys.keyData.config.data.aopts.parallelism", "1" - }; - final CommandLine commandLine = new DefaultParser().parse(options, args); - - byte[] privateKeyData = "SOME PRIVATE DATA".getBytes(); - PrivateKey privateKey = PrivateKey.from(privateKeyData); - when(keyEncryptor.decryptPrivateKey(any(PrivateKeyData.class), anyString())).thenReturn(privateKey); - - this.parser.parse(commandLine); - - final KeyDataConfig endingKey = JaxbUtil.unmarshal(Files.newInputStream(key), KeyDataConfig.class); - - assertThat(endingKey.getSbox()).isNotEqualTo(startingKey.getSbox()); - assertThat(endingKey.getSnonce()).isNotEqualTo(startingKey.getSnonce()); - assertThat(endingKey.getAsalt()).isNotEqualTo(startingKey.getAsalt()); - assertThat(endingKey.getPrivateKeyData().getValue()) - .isEqualTo(Base64.getEncoder().encodeToString(privateKeyData)); - } -} diff --git a/cli/pom.xml b/cli/pom.xml index 02010d98fc..520fdb2eea 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,6 @@ cli-api config-cli - admin-cli @@ -19,11 +18,6 @@ - - commons-cli - commons-cli - - com.jpmorgan.quorum config diff --git a/config-migration/src/main/java/com/quorum/tessera/config/migration/Main.java b/config-migration/src/main/java/com/quorum/tessera/config/migration/Main.java index b559796c51..d6dd8363f6 100644 --- a/config-migration/src/main/java/com/quorum/tessera/config/migration/Main.java +++ b/config-migration/src/main/java/com/quorum/tessera/config/migration/Main.java @@ -1,8 +1,10 @@ package com.quorum.tessera.config.migration; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.cli.parsers.ConfigConverter; +import com.quorum.tessera.config.Config; +import picocli.CommandLine; public class Main { @@ -11,8 +13,16 @@ public static void main(String... args) { System.setProperty("javax.xml.bind.context.factory", "org.eclipse.persistence.jaxb.JAXBContextFactory"); System.setProperty(CliType.CLI_TYPE_KEY, CliType.CONFIG_MIGRATION.name()); try { - final CliResult result = CliDelegate.instance().execute(args); - System.exit(result.getStatus()); + final CommandLine commandLine = new CommandLine(new LegacyCliAdapter()); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true); + + commandLine.execute(args); + final CliResult cliResult = commandLine.getExecutionResult(); + + System.exit(cliResult.getStatus()); } catch (final Exception ex) { System.err.println(ex.toString()); System.exit(1); diff --git a/config-migration/src/test/java/com/quorum/tessera/config/migration/LegacyCliAdapterTest.java b/config-migration/src/test/java/com/quorum/tessera/config/migration/LegacyCliAdapterTest.java index 72f0ad4cc3..a49d969f0e 100644 --- a/config-migration/src/test/java/com/quorum/tessera/config/migration/LegacyCliAdapterTest.java +++ b/config-migration/src/test/java/com/quorum/tessera/config/migration/LegacyCliAdapterTest.java @@ -1,8 +1,8 @@ package com.quorum.tessera.config.migration; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.cli.parsers.ConfigConverter; import com.quorum.tessera.config.*; import com.quorum.tessera.config.builder.ConfigBuilder; import com.quorum.tessera.config.builder.KeyDataBuilder; @@ -15,6 +15,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.SystemErrRule; +import picocli.CommandLine; import java.io.ByteArrayOutputStream; import java.io.File; @@ -30,7 +31,6 @@ import static com.quorum.tessera.config.AppType.Q2T; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.mock; public class LegacyCliAdapterTest { @@ -39,14 +39,25 @@ public class LegacyCliAdapterTest { private final ConfigBuilder builderWithValidValues = FixtureUtil.builderWithValidValues(); + // TODO(cjh) remove this and do all testing through the CommandLine instance private final LegacyCliAdapter instance = new LegacyCliAdapter(); + private CommandLine commandLine; + private Path dataDirectory; @Before public void onSetUp() throws IOException { System.setProperty(CliType.CLI_TYPE_KEY, CliType.CONFIG_MIGRATION.name()); + commandLine = new CommandLine(new LegacyCliAdapter()); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true); + + systemErrRule.clearLog(); + dataDirectory = Files.createTempDirectory("data"); Files.createFile(dataDirectory.resolve("foo.pub")); @@ -91,7 +102,8 @@ public void withoutCliArgsAllConfigIsSetFromTomlFile() throws Exception { Path configFile = Files.createTempFile("noOptions", ".txt"); Files.write(configFile, data.getBytes()); - CliResult result = CliDelegate.instance().execute("--tomlfile", configFile.toString()); + commandLine.execute("--tomlfile", configFile.toString()); + final CliResult result = commandLine.getExecutionResult(); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -211,7 +223,8 @@ public void providingCliArgsOverridesTomlFileConfig() throws Exception { "over-known-servers" }; - CliResult result = CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -270,7 +283,8 @@ public void ifConfigParameterIsNotSetInTomlOrCliThenDefaultIsUsed() throws Excep "--privatekeys", keysFile.toString() }; - CliResult result = CliDelegate.instance().execute(requiredParams); + commandLine.execute(requiredParams); + final CliResult result = commandLine.getExecutionResult(); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -334,7 +348,8 @@ public void ifWorkDirCliOverrideIsProvidedThenItIsAppliedToBothTomlAndCliSetPara "--privatekeys", "new.key" }; - CliResult result = CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -405,7 +420,8 @@ public void urlWithPortSet() throws Exception { "--tomlfile", configFile.toString(), "--url", "http://127.0.0.1:9001", "--port", "9001" }; - final CliResult result = CliDelegate.instance().execute(requiredParams); + commandLine.execute(requiredParams); + final CliResult result = commandLine.getExecutionResult(); assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); @@ -419,11 +435,11 @@ public void invalidUrlProvided() throws Exception { String[] requiredParams = {"--tomlfile", configFile.toString(), "--url", "htt://invalidHost", "--port", "9001"}; - final Throwable throwable = catchThrowable(() -> CliDelegate.instance().execute(requiredParams)); + commandLine.execute(requiredParams); + + final String output = systemErrRule.getLog(); - assertThat(throwable) - .isInstanceOf(RuntimeException.class) - .hasMessage("Bad server url given: unknown protocol: htt"); + assertThat(output).contains("Bad server url given: unknown protocol: htt"); } @Test diff --git a/data-migration/src/main/java/com/quorum/tessera/data/migration/Main.java b/data-migration/src/main/java/com/quorum/tessera/data/migration/Main.java index ac99fa29fc..08030b93c7 100644 --- a/data-migration/src/main/java/com/quorum/tessera/data/migration/Main.java +++ b/data-migration/src/main/java/com/quorum/tessera/data/migration/Main.java @@ -1,8 +1,10 @@ package com.quorum.tessera.data.migration; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.cli.parsers.ConfigConverter; +import com.quorum.tessera.config.Config; +import picocli.CommandLine; import java.util.Arrays; @@ -14,10 +16,18 @@ private Main() { public static void main(final String... args) { - System.setProperty(CliType.CLI_TYPE_KEY,CliType.DATA_MIGRATION.name()); + System.setProperty(CliType.CLI_TYPE_KEY, CliType.DATA_MIGRATION.name()); try { - final CliResult cliResult = CliDelegate.instance().execute(args); + final CommandLine commandLine = new CommandLine(new CmdLineExecutor()); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true); + + commandLine.execute(args); + final CliResult cliResult = commandLine.getExecutionResult(); + System.exit(cliResult.getStatus()); } catch (final Exception ex) { System.err.println("An error has occurred: " + ex.getMessage()); diff --git a/data-migration/src/test/java/com/quorum/tessera/data/migration/CmdLineExecutorTest.java b/data-migration/src/test/java/com/quorum/tessera/data/migration/CmdLineExecutorTest.java index 19c22f8e0e..44825a5915 100644 --- a/data-migration/src/test/java/com/quorum/tessera/data/migration/CmdLineExecutorTest.java +++ b/data-migration/src/test/java/com/quorum/tessera/data/migration/CmdLineExecutorTest.java @@ -1,9 +1,10 @@ package com.quorum.tessera.data.migration; import com.mockrunner.mock.jdbc.JDBCMockObjectFactory; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.cli.parsers.ConfigConverter; +import com.quorum.tessera.config.Config; import com.sun.management.UnixOperatingSystemMXBean; import org.apache.commons.codec.binary.Base32; import org.apache.commons.io.IOUtils; @@ -13,6 +14,7 @@ import org.junit.contrib.java.lang.system.SystemErrRule; import org.junit.contrib.java.lang.system.SystemOutRule; import org.junit.rules.TestName; +import picocli.CommandLine; import java.io.InputStream; import java.lang.management.ManagementFactory; @@ -32,12 +34,20 @@ public class CmdLineExecutorTest { private Path outputPath; + private CommandLine commandLine; + @Before public void onSetup() throws Exception { - System.setProperty(CliType.CLI_TYPE_KEY,CliType.DATA_MIGRATION.name()); + System.setProperty(CliType.CLI_TYPE_KEY, CliType.DATA_MIGRATION.name()); systemErrRule.clearLog(); systemOutRule.clearLog(); this.outputPath = Files.createTempFile(testName.getMethodName(), ".db"); + + commandLine = new CommandLine(new CmdLineExecutor()); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true); } @Test @@ -46,12 +56,14 @@ public void correctCliType() { } @Test - public void help() throws Exception { + public void help() { final String[] args = new String[] {"help"}; - final CliResult result = CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); - assertThat(result).isEqualToComparingFieldByField(new CliResult(0, true, null)); + assertThat(result).isNull(); + // assertThat(result).isEqualToComparingFieldByField(new CliResult(0, true, null)); assertThat(systemOutRule.getLog()) .contains( "Usage:", @@ -62,18 +74,20 @@ public void help() throws Exception { } @Test - public void noOptions() throws Exception { - final CliResult result = CliDelegate.instance().execute(); + public void noOptions() { + commandLine.execute(); + final CliResult result = commandLine.getExecutionResult(); final String expectedLog = "Missing required options [-storetype , -inputpath , -exporttype , -outputfile ]"; - assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); + // assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); + assertThat(result).isNull(); assertThat(systemErrRule.getLog()).contains(expectedLog); } @Test - public void missingStoreTypeOption() throws Exception { + public void missingStoreTypeOption() { final String[] args = new String[] { "-inputpath", "somefile.txt", @@ -82,9 +96,11 @@ public void missingStoreTypeOption() throws Exception { "-dbpass", "-dbuser" }; - final CliResult result = CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); - assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); + // assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); + assertThat(result).isNull(); assertThat(systemErrRule.getLog()).contains("Missing required option '-storetype '"); } @@ -98,9 +114,11 @@ public void missingInputFileOption() throws Exception { "-dbpass", "-dbuser" }; - final CliResult result = CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); - assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); + // assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); + assertThat(result).isNull(); assertThat(systemErrRule.getLog()).contains("Missing required option '-inputpath '"); } @@ -117,7 +135,8 @@ public void bdbStoreType() throws Exception { "-dbpass", "-dbuser" }; - CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); } @Test @@ -133,10 +152,11 @@ public void dirStoreType() throws Exception { "-dbpass", "-dbuser" }; - CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); } - @Test(expected = IllegalArgumentException.class) + @Test() public void exportTypeJdbcNoDbConfigProvided() throws Exception { final Path inputFile = Paths.get(getClass().getResource("/dir/").toURI()); @@ -149,7 +169,13 @@ public void exportTypeJdbcNoDbConfigProvided() throws Exception { "-dbpass", "-dbuser" }; - CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); + + String output = systemErrRule.getLog(); + assertThat(output) + .contains( + "java.lang.IllegalArgumentException: dbconfig file path is required when no export type is defined."); } @Test @@ -180,7 +206,8 @@ public void exportTypeJdbc() throws Exception { "-dbuser" }; - CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); } finally { mockObjectFactory.restoreDrivers(); } @@ -233,6 +260,7 @@ public void directoryStoreAndSqliteWithLotsOfFilesWorks() throws Exception { "-dbpass", "-dbuser" }; - CliDelegate.instance().execute(args); + commandLine.execute(args); + final CliResult result = commandLine.getExecutionResult(); } } diff --git a/enclave/enclave-jaxrs/src/main/java/com/quorum/tessera/enclave/rest/Main.java b/enclave/enclave-jaxrs/src/main/java/com/quorum/tessera/enclave/rest/Main.java index ea6360fb6d..06c001341a 100644 --- a/enclave/enclave-jaxrs/src/main/java/com/quorum/tessera/enclave/rest/Main.java +++ b/enclave/enclave-jaxrs/src/main/java/com/quorum/tessera/enclave/rest/Main.java @@ -1,17 +1,18 @@ package com.quorum.tessera.enclave.rest; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.cli.CliResult; -import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.cli.parsers.ConfigConverter; import com.quorum.tessera.config.CommunicationType; import com.quorum.tessera.config.Config; import com.quorum.tessera.config.ServerConfig; import com.quorum.tessera.enclave.Enclave; import com.quorum.tessera.enclave.EnclaveFactory; +import com.quorum.tessera.enclave.server.EnclaveCliAdapter; import com.quorum.tessera.server.TesseraServer; import com.quorum.tessera.server.TesseraServerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import picocli.CommandLine; import java.util.Collections; import java.util.concurrent.CountDownLatch; @@ -21,11 +22,18 @@ public class Main { private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); public static void main(String... args) throws Exception { - - System.setProperty(CliType.CLI_TYPE_KEY, CliType.ENCLAVE.name()); System.setProperty("javax.xml.bind.JAXBContextFactory", "org.eclipse.persistence.jaxb.JAXBContextFactory"); System.setProperty("javax.xml.bind.context.factory", "org.eclipse.persistence.jaxb.JAXBContextFactory"); - CliResult cliResult = CliDelegate.INSTANCE.execute(args); + + final CommandLine commandLine = new CommandLine(new EnclaveCliAdapter()); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true); + + commandLine.execute(args); + final CliResult cliResult = commandLine.getExecutionResult(); + if (!cliResult.getConfig().isPresent()) { System.exit(cliResult.getStatus()); } diff --git a/enclave/enclave-jaxrs/src/test/java/com/quorum/tessera/enclave/rest/EnclaveRestIT.java b/enclave/enclave-jaxrs/src/test/java/com/quorum/tessera/enclave/rest/EnclaveRestIT.java index 1c04c15bc1..78d74d1dbd 100644 --- a/enclave/enclave-jaxrs/src/test/java/com/quorum/tessera/enclave/rest/EnclaveRestIT.java +++ b/enclave/enclave-jaxrs/src/test/java/com/quorum/tessera/enclave/rest/EnclaveRestIT.java @@ -1,17 +1,20 @@ package com.quorum.tessera.enclave.rest; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.cli.parsers.ConfigConverter; import com.quorum.tessera.config.Config; import com.quorum.tessera.enclave.Enclave; import com.quorum.tessera.enclave.EnclaveFactory; +import com.quorum.tessera.enclave.server.EnclaveCliAdapter; import com.quorum.tessera.encryption.PublicKey; import com.quorum.tessera.service.Service; import org.glassfish.jersey.test.JerseyTest; import org.junit.After; import org.junit.Before; import org.junit.Test; +import picocli.CommandLine; + import java.net.URL; import java.util.Set; @@ -35,7 +38,14 @@ public void setUp() throws Exception { System.setProperty(CliType.CLI_TYPE_KEY, CliType.ENCLAVE.name()); URL url = EnclaveRestIT.class.getResource("/sample-config.json"); - CliResult cliResult = CliDelegate.INSTANCE.execute("-configfile", url.getFile()); + final CommandLine commandLine = new CommandLine(new EnclaveCliAdapter()); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true); + + commandLine.execute("-configfile", url.getFile()); + CliResult cliResult = commandLine.getExecutionResult(); EnclaveFactory enclaveFactory = EnclaveFactory.create(); diff --git a/enclave/enclave-server/src/test/java/com/quorum/tessera/enclave/server/EnclaveCliAdapterTest.java b/enclave/enclave-server/src/test/java/com/quorum/tessera/enclave/server/EnclaveCliAdapterTest.java index 0fdcd77bee..37c210fff3 100644 --- a/enclave/enclave-server/src/test/java/com/quorum/tessera/enclave/server/EnclaveCliAdapterTest.java +++ b/enclave/enclave-server/src/test/java/com/quorum/tessera/enclave/server/EnclaveCliAdapterTest.java @@ -1,19 +1,21 @@ package com.quorum.tessera.enclave.server; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.cli.CliResult; import com.quorum.tessera.cli.CliType; +import com.quorum.tessera.cli.parsers.ConfigConverter; +import com.quorum.tessera.config.Config; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.SystemErrRule; import org.junit.contrib.java.lang.system.SystemOutRule; +import picocli.CommandLine; import java.nio.file.Path; import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; -import org.junit.After; public class EnclaveCliAdapterTest { @@ -21,10 +23,18 @@ public class EnclaveCliAdapterTest { @Rule public SystemOutRule systemOutOutput = new SystemOutRule().enableLog(); + private CommandLine commandLine; + @Before public void onSetUp() { System.setProperty(CliType.CLI_TYPE_KEY, CliType.ENCLAVE.name()); this.systemErrOutput.clearLog(); + + commandLine = new CommandLine(new EnclaveCliAdapter()); + commandLine + .registerConverter(Config.class, new ConfigConverter()) + .setSeparator(" ") + .setCaseInsensitiveEnumValuesAllowed(true); } @After @@ -38,22 +48,26 @@ public void getType() { } @Test - public void missingConfigurationOutputsErrorMessage() throws Exception { - final CliResult result = CliDelegate.instance().execute(); + public void missingConfigurationOutputsErrorMessage() { + commandLine.execute(); + final CliResult result = commandLine.getExecutionResult(); final String output = systemErrOutput.getLog(); - assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); + assertThat(result).isNull(); +// assertThat(result).isEqualToComparingFieldByField(new CliResult(1, true, null)); assertThat(output).contains("Missing required option '-configfile '"); } @Test - public void helpOptionOutputsUsageMessage() throws Exception { - final CliResult result = CliDelegate.instance().execute("help"); + public void helpOptionOutputsUsageMessage() { + commandLine.execute("help"); + final CliResult result = commandLine.getExecutionResult(); final String output = systemOutOutput.getLog(); - assertThat(result).isEqualToComparingFieldByField(new CliResult(0, true, null)); +// assertThat(result).isEqualToComparingFieldByField(new CliResult(0, true, null)); + assertThat(result).isNull(); assertThat(output) .contains( "Usage:", @@ -79,7 +93,8 @@ public void helpOptionOutputsUsageMessage() throws Exception { public void configPassedToResolver() throws Exception { final Path inputFile = Paths.get(getClass().getResource("/sample-config.json").toURI()); - final CliResult result = CliDelegate.instance().execute("-configfile", inputFile.toString()); + commandLine.execute("-configfile", inputFile.toString()); + final CliResult result = commandLine.getExecutionResult(); assertThat(result).isNotNull(); assertThat(result.getStatus()).isEqualTo(0); diff --git a/key-generation/build.gradle b/key-generation/build.gradle index fbc2bdb917..6d126e84d8 100644 --- a/key-generation/build.gradle +++ b/key-generation/build.gradle @@ -1,7 +1,7 @@ dependencies { - implementation project(':encryption:encryption-api') - implementation project(':config') - implementation project(':shared') - implementation project(':key-vault:key-vault-api') + compile project(':encryption:encryption-api') + compile project(':config') + compile project(':shared') + compile project(':key-vault:key-vault-api') } diff --git a/key-generation/pom.xml b/key-generation/pom.xml index e65ab10414..af01ff16d7 100644 --- a/key-generation/pom.xml +++ b/key-generation/pom.xml @@ -24,6 +24,14 @@ com.jpmorgan.quorum key-vault-api + + info.picocli + picocli + + + com.jpmorgan.quorum + cli-api + diff --git a/pom.xml b/pom.xml index d985592aa8..aa240f8dd5 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ github 7.0 1.9.3 + 4.0.4 @@ -515,11 +516,6 @@ 0.11-SNAPSHOT - - com.jpmorgan.quorum - admin-cli - 0.11-SNAPSHOT - com.jpmorgan.quorum @@ -694,6 +690,24 @@ 0.11-SNAPSHOT + + com.jpmorgan.quorum + grpc-server + 0.11-SNAPSHOT + + + + com.jpmorgan.quorum + grpc + 0.11-SNAPSHOT + + + + com.jpmorgan.quorum + grpc-api + 0.11-SNAPSHOT + + com.jpmorgan.quorum @@ -968,12 +982,6 @@ 1.5.4 - - commons-cli - commons-cli - 1.4 - - org.apache.commons commons-lang3 @@ -986,14 +994,12 @@ 1.2.2 - com.github.stefanbirkner system-rules 1.18.0 - javax.xml.bind jaxb-api @@ -1049,18 +1055,21 @@ ${jetty.version} jar + org.eclipse.jetty jetty-client ${jetty.version} jar + org.eclipse.jetty jetty-servlet ${jetty.version} jar + org.eclipse.jetty jetty-unixsocket @@ -1093,7 +1102,6 @@ ${jetty.version} - com.github.jnr jnr-constants @@ -1131,7 +1139,6 @@ native - org.glassfish.grizzly grizzly-http-server @@ -1139,7 +1146,6 @@ test - @@ -1172,7 +1178,6 @@ 2.7 - commons-codec commons-codec @@ -1257,13 +1262,17 @@ ${asm.version} - org.jasypt jasypt ${jasypt.version} + + info.picocli + picocli + ${picocli.version} + @@ -1301,7 +1310,7 @@ system-rules test - + com.google.code.gson gson @@ -1339,8 +1348,6 @@ test - - diff --git a/settings.gradle b/settings.gradle index d221a6997d..0c00259abc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,6 @@ include(':argon2') include(':config') include(':cli:cli-api') include(':cli:config-cli') -include(':cli:admin-cli') include(':cli') include(':tests:acceptance-test') include(':tests:test-util') @@ -58,7 +57,6 @@ include(':tessera-jaxrs') include(':tessera-data') project(':cli:cli-api').projectDir = file('cli/cli-api') project(':cli:config-cli').projectDir = file('cli/config-cli') -project(':cli:admin-cli').projectDir = file('cli/admin-cli') project(':tests:acceptance-test').projectDir = file('tests/acceptance-test') project(':tests:test-util').projectDir = file('tests/test-util') project(':tests:jmeter-test').projectDir = file('tests/jmeter-test') diff --git a/tessera-core/pom.xml b/tessera-core/pom.xml index 5bb9f8a20f..4df29ab5cc 100644 --- a/tessera-core/pom.xml +++ b/tessera-core/pom.xml @@ -12,8 +12,8 @@ com.jpmorgan.quorum tessera-data - - + + io.swagger swagger-annotations @@ -67,7 +67,7 @@ spring-orm runtime - + org.springframework spring-test @@ -86,14 +86,14 @@ test jar - + com.jpmorgan.quorum mock-service-locator test jar - + tessera-core diff --git a/tessera-core/src/test/java/com/quorum/tessera/core/CoreIT.java b/tessera-core/src/test/java/com/quorum/tessera/core/CoreIT.java index d48416820b..ab9d23c33a 100644 --- a/tessera-core/src/test/java/com/quorum/tessera/core/CoreIT.java +++ b/tessera-core/src/test/java/com/quorum/tessera/core/CoreIT.java @@ -1,7 +1,5 @@ - package com.quorum.tessera.core; -import com.quorum.tessera.cli.CliDelegate; import com.quorum.tessera.transaction.TransactionManager; import org.junit.BeforeClass; import org.junit.Test; @@ -19,9 +17,7 @@ @ContextConfiguration(locations = "classpath:tessera-core-spring.xml") public class CoreIT { - - @Inject - private TransactionManager transactionManager; + @Inject private TransactionManager transactionManager; @PersistenceContext(unitName = "tessera") private EntityManager entityManager; @@ -29,13 +25,14 @@ public class CoreIT { @BeforeClass public static void onSetup() throws Exception { String configPath = CoreIT.class.getResource("/config1.json").getPath(); - CliDelegate.INSTANCE.execute("-configfile",configPath); -} + // TODO(cjh) introduces a circular dependency between jaxrs-client module and picocli module + // PicoCliDelegate picoCliDelegate = new PicoCliDelegate(); + // picoCliDelegate.execute("-configfile", configPath); + } @Test public void doStuff() throws Exception { assertThat(transactionManager).isNotNull(); assertThat(entityManager).isNotNull(); } - } diff --git a/tessera-dist/tessera-app/build.gradle b/tessera-dist/tessera-app/build.gradle index 011060ee4a..3572c44cb8 100644 --- a/tessera-dist/tessera-app/build.gradle +++ b/tessera-dist/tessera-app/build.gradle @@ -23,7 +23,6 @@ dependencies { compile project(':tessera-core') compile project(':cli:cli-api') compile project(':cli:config-cli') - compile project(':cli:admin-cli') compile project(':tessera-jaxrs:admin-jaxrs') compile project(':tessera-jaxrs:sync-jaxrs') compile project(':tessera-jaxrs:transaction-jaxrs') diff --git a/tessera-dist/tessera-app/pom.xml b/tessera-dist/tessera-app/pom.xml index e3e868586b..0a6eae6a23 100644 --- a/tessera-dist/tessera-app/pom.xml +++ b/tessera-dist/tessera-app/pom.xml @@ -30,12 +30,6 @@ runtime - - com.jpmorgan.quorum - admin-cli - runtime - - com.jpmorgan.quorum encryption-api diff --git a/tessera-dist/tessera-launcher/build.gradle b/tessera-dist/tessera-launcher/build.gradle index 5695fa2884..bf776c1faf 100644 --- a/tessera-dist/tessera-launcher/build.gradle +++ b/tessera-dist/tessera-launcher/build.gradle @@ -2,6 +2,7 @@ dependencies { compile project(':config') compile project(':cli:cli-api') + compile project(':cli:config-cli') compile project(':server:server-api') compile project(':tessera-jaxrs:common-jaxrs') compile 'org.apache.commons:commons-lang3' diff --git a/tessera-dist/tessera-launcher/pom.xml b/tessera-dist/tessera-launcher/pom.xml index f464b8cda8..cf1f5f5e76 100644 --- a/tessera-dist/tessera-launcher/pom.xml +++ b/tessera-dist/tessera-launcher/pom.xml @@ -19,11 +19,17 @@ server-api jar - + com.jpmorgan.quorum common-jaxrs 0.11-SNAPSHOT + + + com.jpmorgan.quorum + config-cli + + diff --git a/tessera-dist/tessera-launcher/src/main/java/com/quorum/tessera/launcher/Main.java b/tessera-dist/tessera-launcher/src/main/java/com/quorum/tessera/launcher/Main.java index b3d60e81fe..d137f0e60e 100644 --- a/tessera-dist/tessera-launcher/src/main/java/com/quorum/tessera/launcher/Main.java +++ b/tessera-dist/tessera-launcher/src/main/java/com/quorum/tessera/launcher/Main.java @@ -8,6 +8,7 @@ import com.quorum.tessera.config.Config; import com.quorum.tessera.config.ConfigException; import com.quorum.tessera.config.apps.TesseraAppFactory; +import com.quorum.tessera.config.cli.PicoCliDelegate; import com.quorum.tessera.server.TesseraServer; import com.quorum.tessera.server.TesseraServerFactory; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -26,12 +27,14 @@ public class Main { private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); public static void main(final String... args) throws Exception { - System.setProperty(CliType.CLI_TYPE_KEY,CliType.CONFIG.name()); + System.setProperty(CliType.CLI_TYPE_KEY, CliType.CONFIG.name()); System.setProperty("javax.xml.bind.JAXBContextFactory", "org.eclipse.persistence.jaxb.JAXBContextFactory"); System.setProperty("javax.xml.bind.context.factory", "org.eclipse.persistence.jaxb.JAXBContextFactory"); try { - final CliResult cliResult = CliDelegate.instance().execute(args); + PicoCliDelegate picoCliDelegate = new PicoCliDelegate(); + final CliResult cliResult = picoCliDelegate.execute(args); + CliDelegate.instance().setConfig(cliResult.getConfig().orElse(null)); if (cliResult.isSuppressStartup()) { System.exit(0); diff --git a/tessera-dist/tessera-simple/pom.xml b/tessera-dist/tessera-simple/pom.xml index 9a538ce65b..d65ea86d6e 100644 --- a/tessera-dist/tessera-simple/pom.xml +++ b/tessera-dist/tessera-simple/pom.xml @@ -34,12 +34,6 @@ runtime - - com.jpmorgan.quorum - admin-cli - runtime - - com.jpmorgan.quorum encryption-api diff --git a/tests/acceptance-test/src/test/java/admin/cmd/Utils.java b/tests/acceptance-test/src/test/java/admin/cmd/Utils.java index 4160958198..9773370d1b 100644 --- a/tests/acceptance-test/src/test/java/admin/cmd/Utils.java +++ b/tests/acceptance-test/src/test/java/admin/cmd/Utils.java @@ -22,8 +22,6 @@ public class Utils { private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); - - public static ExecutionResult start(Party party) throws IOException, InterruptedException { List args = @@ -91,16 +89,14 @@ public static ExecutionResult start(Party party) throws IOException, Interrupted public static int addPeer(Party party, String url) throws IOException, InterruptedException { - List args = new ExecArgsBuilder() - .withJvmArg(String.format("-Dnode.number=%S", party.getAlias())) - .withStartScriptOrExecutableJarFile(Paths.get(jarPath)) - .withConfigFile(party.getConfigFilePath()) - .withArg("admin") - .withArg("-addpeer",url) - .withArg("-configfile",party.getConfigFilePath().toAbsolutePath().toString()) - .build(); - - + List args = + new ExecArgsBuilder() + .withJvmArg(String.format("-Dnode.number=%S", party.getAlias())) + .withStartScriptOrExecutableJarFile(Paths.get(jarPath)) + .withConfigFile(party.getConfigFilePath()) + .withSubcommands("admin", "addpeer") + .withArg(url) + .build(); LOGGER.info("exec : {}", String.join(" ", args)); ProcessBuilder processBuilder = new ProcessBuilder(args); diff --git a/tests/acceptance-test/src/test/java/com/quorum/tessera/test/cli/keygen/FileKeygenSteps.java b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/cli/keygen/FileKeygenSteps.java index 154474f223..e7cfe7c11a 100644 --- a/tests/acceptance-test/src/test/java/com/quorum/tessera/test/cli/keygen/FileKeygenSteps.java +++ b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/cli/keygen/FileKeygenSteps.java @@ -80,9 +80,7 @@ public FileKeygenSteps() { // here to explicitly state we are doing nothing Given("no file path is provided", () -> {}); - Given( - "a file path of {string}", - (String path) -> this.args.addAll(Arrays.asList("-filename", path, "--encryptor.type", "NACL"))); + Given("a file path of {string}", (String path) -> this.args.addAll(Arrays.asList("-filename", path))); When( "new keys are generated", diff --git a/tests/acceptance-test/src/test/java/exec/ExecArgsBuilder.java b/tests/acceptance-test/src/test/java/exec/ExecArgsBuilder.java index 727e9370b5..df36b4e7b8 100644 --- a/tests/acceptance-test/src/test/java/exec/ExecArgsBuilder.java +++ b/tests/acceptance-test/src/test/java/exec/ExecArgsBuilder.java @@ -1,6 +1,7 @@ package exec; import com.quorum.tessera.config.Config; + import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; @@ -13,6 +14,8 @@ public class ExecArgsBuilder { private Path configFile; + private List subcommands; + private Path pidFile; private Class mainClass; @@ -78,6 +81,15 @@ private ExecArgsBuilder withStartScript(Path startScript) { return this; } + public ExecArgsBuilder withSubcommands(String subcommand, String... s) { + List subcommands = new ArrayList<>(); + subcommands.add(subcommand); + subcommands.addAll(Arrays.asList(s)); + + this.subcommands = subcommands; + return this; + } + public ExecArgsBuilder withArg(String name) { argList.put(name, null); return this; @@ -122,6 +134,10 @@ public List build() { tokens.add(startScript.toAbsolutePath().toString()); } + if (Objects.nonNull(subcommands)) { + tokens.addAll(subcommands); + } + tokens.add("-configfile"); tokens.add(configFile.toAbsolutePath().toString());