diff --git a/config-cli/pom.xml b/config-cli/pom.xml index e58df25dae..176edec2d4 100644 --- a/config-cli/pom.xml +++ b/config-cli/pom.xml @@ -24,6 +24,11 @@ config + + com.quorum.tessera + jaxrs-client + + javax.validation validation-api @@ -44,11 +49,29 @@ test-util + + javax.ws.rs + javax.ws.rs-api + + com.quorum.tessera key-generation + + com.quorum.tessera + jersey-server + test + + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + 2.27 + test + jar + diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/AdminCliAdapter.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/AdminCliAdapter.java new file mode 100644 index 0000000000..aab51d26cd --- /dev/null +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/AdminCliAdapter.java @@ -0,0 +1,124 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.Config; +import com.quorum.tessera.config.Peer; +import com.quorum.tessera.config.ServerConfig; +import com.quorum.tessera.config.cli.parsers.ConfigurationParser; +import com.quorum.tessera.jaxrs.client.ClientFactory; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +/** + * Cli Adapter to be used for runtime updates + */ +public class AdminCliAdapter implements CliAdapter { + + private final ClientFactory clientFactory; + + public AdminCliAdapter(ClientFactory clientFactory) { + this.clientFactory = Objects.requireNonNull(clientFactory); + } + + /** + * + * @param args + * @return CliResult with config object always null. + * @throws Exception + */ + @Override + public CliResult execute(String... args) throws Exception { + + final Options options = new Options(); + + options.addOption( + Option.builder("configfile") + .desc("Path to node configuration file") + .hasArg(true) + .required() + .optionalArg(false) + .numberOfArgs(1) + .argName("PATH") + .build()); + + options.addOption(Option.builder("addpeer") + .desc("Add peer to running node") + .hasArg(true) + .optionalArg(false) + .numberOfArgs(1) + .argName("URL") + .build()); + + final List argsList = Arrays.asList(args); + + if (argsList.contains("help") || argsList.isEmpty()) { + HelpFormatter formatter = new HelpFormatter(); + formatter.setWidth(200); + formatter.printHelp("tessera admin", options); + return new CliResult(0, true, null); + } + + final CommandLine line = new DefaultParser().parse(options, args); + if(!line.hasOption("addpeer")) { + System.out.println("No peer defined"); + return new CliResult(1, true, null); + } + + Config config = new ConfigurationParser().parse(line); + + Client restClient = clientFactory.buildFrom(config.getServerConfig()); + + String peerUrl = line.getOptionValue("addpeer"); + + final Peer peer = new Peer(peerUrl); + + String scheme = Optional.of(config) + .map(Config::getServerConfig) + .map(ServerConfig::getServerUri) + .map(URI::getScheme) + .orElse("http"); + + Integer port = Optional.of(config) + .map(Config::getServerConfig) + .map(ServerConfig::getPort) + .orElse(80); + + URI uri = UriBuilder.fromPath("/") + .port(port) + .host("localhost") + .scheme(scheme).build(); + + + Response response = restClient.target(uri) + .path("config") + .path("peers") + .request(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .put(Entity.entity(peer, MediaType.APPLICATION_JSON)); + + if(response.getStatus() == Response.Status.CREATED.getStatusCode()) { + + System.out.printf("Peer %s added.",response.getLocation()); + System.out.println(); + + return new CliResult(0, true, null); + } + + System.err.println("Unable to create peer"); + + return new CliResult(1, true, null); + } + +} diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java index 6af3b68d3b..6b2a908c13 100644 --- a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java @@ -4,8 +4,4 @@ public interface CliAdapter { CliResult execute(String... args) throws Exception; - static CliAdapter create() { - return new DefaultCliAdapter(); - } - } diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java index 768a52f72c..07cfd70f77 100644 --- a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java @@ -1,6 +1,9 @@ package com.quorum.tessera.config.cli; import com.quorum.tessera.config.Config; +import com.quorum.tessera.jaxrs.client.ClientFactory; +import java.util.Arrays; +import java.util.List; public enum CliDelegate { @@ -18,7 +21,15 @@ public Config getConfig() { public CliResult execute(String... args) throws Exception { - final CliAdapter cliAdapter = CliAdapter.create(); + final List argsList = Arrays.asList(args); + + final CliAdapter cliAdapter; + + if(argsList.contains("admin")) { + cliAdapter = new AdminCliAdapter(new ClientFactory()); + } else { + cliAdapter = new DefaultCliAdapter(); + } final CliResult result = cliAdapter.execute(args); diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java index 4100d480cb..4bb7563c38 100644 --- a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java @@ -8,14 +8,14 @@ public class CliResult { private final Integer status; - private final boolean isHelpOn; - private final boolean isKeyGenOn; + + private final boolean suppressStartup; + private final Config config; - public CliResult(Integer status, boolean isHelpOn, boolean isKeyGenOn, Config config) { + public CliResult(Integer status, boolean suppressStartup,Config config) { this.status = Objects.requireNonNull(status); - this.isHelpOn = isHelpOn; - this.isKeyGenOn = isKeyGenOn; + this.suppressStartup = suppressStartup; this.config = config; } @@ -23,11 +23,10 @@ public Integer getStatus() { return status; } - public boolean isHelpOn() { - return isHelpOn; + public boolean isSuppressStartup() { + return suppressStartup; } - public boolean isKeyGenOn() {return isKeyGenOn;} public Optional getConfig() { return Optional.ofNullable(config); diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java index 3524669347..3595be46a1 100644 --- a/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java @@ -59,7 +59,7 @@ public CliResult execute(String... args) throws Exception { HelpFormatter formatter = new HelpFormatter(); formatter.setWidth(200); formatter.printHelp("tessera -configfile [-keygen ] [-pidfile ]", options); - return new CliResult(0, true, false, null); + return new CliResult(0, true, null); } try { @@ -87,7 +87,7 @@ public CliResult execute(String... args) throws Exception { new PidFileParser().parse(line); - return new CliResult(0, false, line.hasOption("keygen"), config); + return new CliResult(0, line.hasOption("keygen"), config); } catch (ParseException exp) { throw new CliException(exp.getMessage()); diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.java index 043643308c..f873b35d15 100644 --- a/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.java +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.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 com.quorum.tessera.config.keypairs.ConfigKeyPair; import com.quorum.tessera.config.util.JaxbUtil; import org.apache.commons.cli.CommandLine; @@ -50,6 +51,8 @@ public Config parse(final CommandLine commandLine) throws IOException { //we have generated new keys, so we need to output the new configuration output(commandLine, config); } + + ConfigFileStore.create(path); } diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/AdminCliAdapterTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/AdminCliAdapterTest.java new file mode 100644 index 0000000000..47a8ff5875 --- /dev/null +++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/AdminCliAdapterTest.java @@ -0,0 +1,116 @@ +package com.quorum.tessera.config.cli; + +import com.quorum.tessera.config.Peer; +import com.quorum.tessera.config.ServerConfig; +import com.quorum.tessera.jaxrs.client.ClientFactory; +import com.quorum.tessera.test.util.ElUtil; +import java.net.URI; +import java.nio.file.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Before; +import org.junit.Test; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class AdminCliAdapterTest { + + private AdminCliAdapter adminCliAdapter; + + private ClientFactory clientFactory; + + private Invocation.Builder invocationBuilder; + + @Before + public void onSetUp() { + + invocationBuilder = mock(Invocation.Builder.class); + + clientFactory = mock(ClientFactory.class); + + Client client = mock(Client.class); + WebTarget webTarget = mock(WebTarget.class); + + when(client.target(any(URI.class))).thenReturn(webTarget); + when(webTarget.path(anyString())).thenReturn(webTarget,webTarget); + + when(webTarget.request(MediaType.APPLICATION_JSON)).thenReturn(invocationBuilder); + + when(invocationBuilder.accept(MediaType.APPLICATION_JSON)).thenReturn(invocationBuilder); + + when(clientFactory.buildFrom(any(ServerConfig.class))).thenReturn(client); + + adminCliAdapter = new AdminCliAdapter(clientFactory); + } + + public void onTearDown() { + verifyNoMoreInteractions(invocationBuilder); + } + + + @Test + public void help() throws Exception { + //new CliResult(0, true, false, null); + CliResult result = adminCliAdapter.execute("help"); + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isNotPresent(); + assertThat(result.isSuppressStartup()).isTrue(); + + } + + @Test + public void addPeer() throws Exception { + + Peer peer = new Peer("http://junit.com:8989"); + Entity entity = Entity.entity(peer, MediaType.APPLICATION_JSON); + + URI uri = UriBuilder.fromPath("/result").build(); + when(invocationBuilder.put(entity)).thenReturn(Response.created(uri).build()); + + Path configFile = ElUtil.createAndPopulatePaths(getClass().getResource("/sample-config.json")); + + CliResult result = adminCliAdapter.execute("-addpeer",peer.getUrl(),"-configfile",configFile.toString()); + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isNotPresent(); + assertThat(result.isSuppressStartup()).isTrue(); + + assertThat(result.getStatus()).isEqualTo(0); + + verify(invocationBuilder).put(entity); + + + } + + @Test + public void addPeerSomethingBadWentDown() throws Exception { + + Peer peer = new Peer("http://junit.com:8989"); + Entity entity = Entity.entity(peer, MediaType.APPLICATION_JSON); + + URI uri = UriBuilder.fromPath("/result").build(); + when(invocationBuilder.put(entity)).thenReturn(Response.serverError().build()); + + Path configFile = ElUtil.createAndPopulatePaths(getClass().getResource("/sample-config.json")); + + CliResult result = adminCliAdapter.execute("-addpeer",peer.getUrl(),"-configfile",configFile.toString()); + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isNotPresent(); + assertThat(result.isSuppressStartup()).isTrue(); + + assertThat(result.getStatus()).isEqualTo(1); + + verify(invocationBuilder).put(entity); + + + } +} diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliAdapterTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/CliAdapterTest.java deleted file mode 100644 index bf96ed95c3..0000000000 --- a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliAdapterTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.quorum.tessera.config.cli; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class CliAdapterTest { - - @Test - public void createDefault() { - - CliAdapter result = CliAdapter.create(); - - assertThat(result).isExactlyInstanceOf(DefaultCliAdapter.class); - - } -} diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java index dd248b47be..64711d6f18 100644 --- a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java +++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java @@ -18,6 +18,15 @@ public void createInstance() { } + @Test + public void createAdminInstance() throws Exception { + + Path configFile = ElUtil.createAndPopulatePaths(getClass().getResource("/sample-config.json")); + CliResult result = instance.execute("admin", "-configfile", configFile.toString()); + + assertThat(result).isNotNull(); + } + @Test public void withValidConfig() throws Exception { @@ -31,8 +40,8 @@ public void withValidConfig() throws Exception { assertThat(result.getConfig()).isPresent(); assertThat(result.getConfig().get()).isSameAs(instance.getConfig()); assertThat(result.getStatus()).isEqualTo(0); - assertThat(result.isHelpOn()).isFalse(); - assertThat(result.isKeyGenOn()).isFalse(); + assertThat(result.isSuppressStartup()).isFalse(); + } @Test diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java index 7db7b34a98..c77ead533a 100644 --- a/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java +++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java @@ -36,7 +36,7 @@ public class DefaultCliAdapterTest { @Before public void setUp() { MockKeyGeneratorFactory.reset(); - this.cliDelegate = CliAdapter.create(); + this.cliDelegate = new DefaultCliAdapter(); } @After @@ -53,8 +53,8 @@ public void help() throws Exception { assertThat(result).isNotNull(); assertThat(result.getConfig()).isNotPresent(); assertThat(result.getStatus()).isEqualTo(0); - assertThat(result.isHelpOn()).isTrue(); - assertThat(result.isKeyGenOn()).isFalse(); + assertThat(result.isSuppressStartup()).isTrue(); + } @@ -66,8 +66,8 @@ public void noArgsPrintsHelp() throws Exception { assertThat(result).isNotNull(); assertThat(result.getConfig()).isNotPresent(); assertThat(result.getStatus()).isEqualTo(0); - assertThat(result.isHelpOn()).isTrue(); - assertThat(result.isKeyGenOn()).isFalse(); + assertThat(result.isSuppressStartup()).isTrue(); + } @@ -80,7 +80,7 @@ public void withValidConfig() throws Exception { assertThat(result).isNotNull(); assertThat(result.getConfig()).isPresent(); assertThat(result.getStatus()).isEqualTo(0); - assertThat(result.isHelpOn()).isFalse(); + assertThat(result.isSuppressStartup()).isFalse(); } @Test(expected = FileNotFoundException.class) @@ -133,7 +133,7 @@ public void keygen() throws Exception { assertThat(result).isNotNull(); assertThat(result.getStatus()).isEqualTo(0); assertThat(result.getConfig()).isNotNull(); - assertThat(result.isHelpOn()).isFalse(); + assertThat(result.isSuppressStartup()).isTrue(); verify(keyGenerator).generate(anyString(), eq(null)); verifyNoMoreInteractions(keyGenerator); @@ -146,7 +146,7 @@ public void keygenThenExit() throws Exception { final CliResult result = cliDelegate.execute("-keygen"); assertThat(result).isNotNull(); - assertThat(result.isKeyGenOn()).isTrue(); + assertThat(result.isSuppressStartup()).isTrue(); } diff --git a/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java b/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java index 514b4385a1..e2356de125 100644 --- a/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java +++ b/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java @@ -45,7 +45,7 @@ public CliResult execute(String... args) throws Exception { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("tessera-config-migration", header, options, null); final int exitCode = argsList.isEmpty() ? 1 : 0; - return new CliResult(exitCode, true, false, null); + return new CliResult(exitCode, true, null); } CommandLineParser parser = new DefaultParser(); @@ -86,7 +86,7 @@ static CliResult writeToOutputFile(Config config, Path outputPath) throws IOExce JaxbUtil.marshal(config, outputStream); System.out.printf("Saved config to %s", outputPath); System.out.println(); - return new CliResult(0, false, false, config); + return new CliResult(0, false, config); } catch (ConstraintViolationException validationException) { validationException.getConstraintViolations() .stream() @@ -96,7 +96,7 @@ static CliResult writeToOutputFile(Config config, Path outputPath) throws IOExce Files.write(outputPath, JaxbUtil.marshalToStringNoValidation(config).getBytes()); System.out.printf("Saved config to %s", outputPath); System.out.println(); - return new CliResult(2, false, false, config); + return new CliResult(2, false, config); } } diff --git a/config/src/main/java/com/quorum/tessera/config/Config.java b/config/src/main/java/com/quorum/tessera/config/Config.java index 57df24cce5..8bd68d448b 100644 --- a/config/src/main/java/com/quorum/tessera/config/Config.java +++ b/config/src/main/java/com/quorum/tessera/config/Config.java @@ -12,6 +12,7 @@ import javax.xml.bind.annotation.*; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.nio.file.Path; +import java.util.Collections; import java.util.List; @XmlRootElement @@ -98,7 +99,7 @@ public Path getUnixSocketFile() { } public List getPeers() { - return this.peers; + return Collections.unmodifiableList(peers); } public KeyConfiguration getKeys() { @@ -116,5 +117,10 @@ public boolean isUseWhiteList() { public boolean isDisablePeerDiscovery() { return disablePeerDiscovery; } + + @XmlTransient + public void addPeer(Peer peer) { + this.peers.add(peer); + } } diff --git a/config/src/main/java/com/quorum/tessera/config/util/ConfigFileStore.java b/config/src/main/java/com/quorum/tessera/config/util/ConfigFileStore.java new file mode 100644 index 0000000000..711cb06340 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/util/ConfigFileStore.java @@ -0,0 +1,47 @@ +package com.quorum.tessera.config.util; + +import com.quorum.tessera.config.Config; +import com.quorum.tessera.io.IOCallback; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.UUID; + +public interface ConfigFileStore { + + enum Store implements ConfigFileStore { + + INSTANCE; + + private Path path; + + @Override + public void save(Config config) { + + IOCallback.execute(() -> { + Path temp = Files.createTempFile(UUID.randomUUID().toString(), ".tmp"); + try(OutputStream fout = Files.newOutputStream(temp)){ + JaxbUtil.marshalWithNoValidation(config, fout); + } + Files.copy(temp, path,StandardCopyOption.REPLACE_EXISTING); + return null; + }); + } + } + + static ConfigFileStore create(Path path) { + Store.INSTANCE.path = path; + return Store.INSTANCE; + } + + + static ConfigFileStore get() { + return Store.INSTANCE; + } + + void save(Config config); + + + +} diff --git a/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java b/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java index 77f10cb4ae..f37758abc4 100644 --- a/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java +++ b/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java @@ -10,6 +10,7 @@ public ConsolePasswordReader(final Console console) { this.console = console; } + @Override public String readPasswordFromConsole() { final char[] consolePassword = this.console.readPassword(); return new String(consolePassword); diff --git a/config/src/test/java/com/quorum/tessera/config/ConfigTest.java b/config/src/test/java/com/quorum/tessera/config/ConfigTest.java new file mode 100644 index 0000000000..de3cae6f25 --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/ConfigTest.java @@ -0,0 +1,25 @@ +package com.quorum.tessera.config; + +import java.util.ArrayList; +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; + +public class ConfigTest { + + @Test + public void createWithNullArgs() { + Config config = new Config(null, null, null, null, null, null, false, false); + assertThat(config).isNotNull(); + } + + @Test + public void addPeer() { + Config config = new Config(null, null, new ArrayList<>(), null, null, null, false, false); + assertThat(config.getPeers()).isEmpty(); + Peer peer = new Peer("Junit"); + config.addPeer(peer); + assertThat(config.getPeers()).containsOnly(peer); + + } + +} diff --git a/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java b/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java index 81413c76e4..266bb3b3b9 100644 --- a/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java +++ b/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java @@ -24,6 +24,7 @@ public void executeOpenPojoValidations() { pc -> !pc.getClazz().isAssignableFrom(ObjectFactory.class), pc -> !pc.getClazz().isAssignableFrom(JaxbConfigFactory.class), pc -> !pc.getClazz().isAssignableFrom(ConfigException.class), + pc -> !pc.getClazz().isAssignableFrom(Config.class), pc -> !pc.getClazz().getName().contains(ConfigItem.class.getName()), pc -> !pc.getClazz().getSimpleName().contains("Test") }; diff --git a/config/src/test/java/com/quorum/tessera/config/util/ConfigFileStoreTest.java b/config/src/test/java/com/quorum/tessera/config/util/ConfigFileStoreTest.java new file mode 100644 index 0000000000..0958862455 --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/util/ConfigFileStoreTest.java @@ -0,0 +1,64 @@ +package com.quorum.tessera.config.util; + +import com.quorum.tessera.config.Config; +import com.quorum.tessera.io.FilesDelegate; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.UUID; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Before; +import org.junit.Test; + +public class ConfigFileStoreTest { + + private ConfigFileStore configFileStore; + + private Path path; + + @Before + public void onSetUp() throws Exception { + path = Files.createTempFile(UUID.randomUUID().toString(), ".junit"); + path.toFile().deleteOnExit(); + + final URL sampleConfig = getClass().getResource("/sample.json"); + try (InputStream in = sampleConfig.openStream()) { + Config initialConfig = JaxbUtil.unmarshal(in, Config.class); + JaxbUtil.marshal(initialConfig, Files.newOutputStream(path)); + } + + configFileStore = ConfigFileStore.create(path); + } + + @Test + public void getReturnsSameInstance() { + assertThat(ConfigFileStore.get()).isSameAs(configFileStore); + + } + + @Test + public void save() throws Exception { + + final URL updatedConfig = getClass().getResource("/sample_full.json"); + try (InputStream in = updatedConfig.openStream()) { + Config config = JaxbUtil.unmarshal(in, Config.class); + configFileStore.save(config); + } + + final JsonObject result = Optional.of(path) + .map(FilesDelegate.create()::newInputStream) + .map(Json::createReader) + .map(JsonReader::readObject) + .get(); + + assertThat(result.getJsonObject("server").getString("hostName")) + .isEqualTo("http://localhost"); + + } + +} diff --git a/config/src/test/resources/sample.json b/config/src/test/resources/sample.json index ffb517ae43..731152846a 100644 --- a/config/src/test/resources/sample.json +++ b/config/src/test/resources/sample.json @@ -45,7 +45,7 @@ }, "type": "argon2sbox" }, - "publicKey": "PUBLIC_KEY" + "publicKey": "UFVCTElDX0tFWQ==" } ] }, diff --git a/jaxrs-client/pom.xml b/jaxrs-client/pom.xml new file mode 100644 index 0000000000..2dd9c9a623 --- /dev/null +++ b/jaxrs-client/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + com.quorum.tessera + tessera + 0.7-SNAPSHOT + + jaxrs-client + jar + + + com.quorum.tessera + config + + + com.quorum.tessera + security + + + javax.ws.rs + javax.ws.rs-api + + + + com.quorum.tessera + jersey-server + test + + + \ No newline at end of file diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/client/ClientFactory.java b/jaxrs-client/src/main/java/com/quorum/tessera/jaxrs/client/ClientFactory.java similarity index 96% rename from jaxrs-service/src/main/java/com/quorum/tessera/client/ClientFactory.java rename to jaxrs-client/src/main/java/com/quorum/tessera/jaxrs/client/ClientFactory.java index 3d6f22d45f..0e57238ac5 100644 --- a/jaxrs-service/src/main/java/com/quorum/tessera/client/ClientFactory.java +++ b/jaxrs-client/src/main/java/com/quorum/tessera/jaxrs/client/ClientFactory.java @@ -1,4 +1,4 @@ -package com.quorum.tessera.client; +package com.quorum.tessera.jaxrs.client; import com.quorum.tessera.config.ServerConfig; import com.quorum.tessera.ssl.context.SSLContextFactory; diff --git a/jaxrs-service/src/test/java/com/quorum/tessera/node/ClientFactoryTest.java b/jaxrs-client/src/test/java/com/quorum/tessera/jaxrs/client/ClientFactoryTest.java similarity index 96% rename from jaxrs-service/src/test/java/com/quorum/tessera/node/ClientFactoryTest.java rename to jaxrs-client/src/test/java/com/quorum/tessera/jaxrs/client/ClientFactoryTest.java index 13f09f8a8b..e853834e4a 100644 --- a/jaxrs-service/src/test/java/com/quorum/tessera/node/ClientFactoryTest.java +++ b/jaxrs-client/src/test/java/com/quorum/tessera/jaxrs/client/ClientFactoryTest.java @@ -1,6 +1,6 @@ -package com.quorum.tessera.node; +package com.quorum.tessera.jaxrs.client; + -import com.quorum.tessera.client.ClientFactory; import com.quorum.tessera.config.ServerConfig; import com.quorum.tessera.config.SslConfig; import com.quorum.tessera.ssl.context.SSLContextFactory; diff --git a/jaxrs-service/pom.xml b/jaxrs-service/pom.xml index f8fed45324..ac0125066c 100644 --- a/jaxrs-service/pom.xml +++ b/jaxrs-service/pom.xml @@ -30,6 +30,11 @@ com.quorum.tessera config + + com.quorum.tessera + jaxrs-client + + javax.servlet javax.servlet-api @@ -82,7 +87,7 @@ - + com.github.kongchen swagger-maven-plugin 3.1.7 diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/api/ConfigResource.java b/jaxrs-service/src/main/java/com/quorum/tessera/api/ConfigResource.java new file mode 100644 index 0000000000..86692f3202 --- /dev/null +++ b/jaxrs-service/src/main/java/com/quorum/tessera/api/ConfigResource.java @@ -0,0 +1,59 @@ +package com.quorum.tessera.api; + +import com.quorum.tessera.api.filter.PrivateApi; +import com.quorum.tessera.config.Peer; +import com.quorum.tessera.core.config.ConfigService; +import java.net.URI; +import java.util.List; +import java.util.Objects; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +@PrivateApi +@Path("/config") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class ConfigResource { + + private final ConfigService configService; + + public ConfigResource(ConfigService configService) { + this.configService = Objects.requireNonNull(configService); + } + + @PUT + @Path("/peers") + public Response addPeer(@Valid Peer peer) { + + configService.addPeer(peer.getUrl()); + + int index = configService.getPeers().size() - 1; + + URI uri = UriBuilder.fromPath("config") + .path("peers") + .path(String.valueOf(index)) + .build(); + return Response.created(uri).build(); + } + + @GET + @Path("/peers/{index}") + public Response getPeer(@PathParam("index") Integer index) { + + List peers = configService.getPeers(); + if (peers.size() <= index) { + throw new NotFoundException("No peer found at index "+ index); + } + return Response.ok(peers.get(index)).build(); + } + +} diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java b/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java index 2c7cafb3f0..7728e038ca 100644 --- a/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java +++ b/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java @@ -1,6 +1,7 @@ package com.quorum.tessera.api.exception; import com.quorum.tessera.node.AutoDiscoveryDisabledException; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @@ -12,6 +13,7 @@ public class AutoDiscoveryDisabledExceptionMapper implements ExceptionMapper whitelisted; + private static final Logger LOGGER = LoggerFactory.getLogger(IPWhitelistFilter.class); + + private final ConfigService configService; + //private final Set whitelisted; private boolean disabled; private HttpServletRequest httpServletRequest; - public IPWhitelistFilter(final Config configuration) { - - this.whitelisted = configuration - .getPeers() - .stream() - .map(Peer::getUrl) - .map(s -> { - try { - return new URL(s); - } catch (final MalformedURLException ex) { - throw new RuntimeException(ex); - } - }).map(URL::getHost) - .collect(Collectors.toSet()); - - //add ourself to the whitelist to let the unix socket in - //don't use the advertised address, as we only want to talk to ourselves - this.whitelisted.add("127.0.0.1"); - - this.disabled = !configuration.isUseWhiteList(); + public IPWhitelistFilter(ConfigService configService) { + this.configService = Objects.requireNonNull(configService); + this.disabled = !configService.isUseWhiteList(); } /** - * If the filter is disabled, return immediately - * Otherwise, extract the callers hostname and address, and check it against the whitelist + * If the filter is disabled, return immediately Otherwise, extract the + * callers hostname and address, and check it against the whitelist * * If a problem occurs, then disable the filter * - * If the host is not whitelisted, finish the filter chain here and return an Unauthorized response + * If the host is not whitelisted, finish the filter chain here and return + * an Unauthorized response * * @param requestContext the context of the current request */ @Override public void filter(final ContainerRequestContext requestContext) { - if(disabled) { + if (disabled) { return; } try { + final Set whitelisted = configService.getPeers().stream() + .map(Peer::getUrl) + .map(s -> IOCallback.execute(() -> new URL(s))) + .map(URL::getHost) + .collect(Collectors.toSet()); + + //add ourself to the whitelist to let the unix socket in + //don't use the advertised address, as we only want to talk to ourselves + whitelisted.add("127.0.0.1"); + final String remoteAddress = httpServletRequest.getRemoteAddr(); final String remoteHost = httpServletRequest.getRemoteHost(); @@ -81,6 +81,7 @@ public void filter(final ContainerRequestContext requestContext) { } } catch (final Exception ex) { + LOGGER.error("Unexpected error while processing request.", ex); this.disabled = true; } diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java b/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java index 30971ee99e..38cbe8513f 100644 --- a/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java +++ b/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java @@ -2,6 +2,7 @@ import com.quorum.tessera.config.CommunicationType; import com.quorum.tessera.config.Config; +import com.quorum.tessera.jaxrs.client.ClientFactory; import com.quorum.tessera.ssl.context.ClientSSLContextFactory; import com.quorum.tessera.ssl.context.SSLContextFactory; import javax.ws.rs.client.Client; diff --git a/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml b/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml index 4b7e3759b4..0b9cc3deb2 100644 --- a/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml +++ b/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml @@ -24,6 +24,10 @@ + + + + @@ -37,7 +41,7 @@ - + @@ -46,7 +50,7 @@ - + diff --git a/jaxrs-service/src/test/java/com/quorum/tessera/api/ConfigResourceTest.java b/jaxrs-service/src/test/java/com/quorum/tessera/api/ConfigResourceTest.java new file mode 100644 index 0000000000..9ed0ab7c2e --- /dev/null +++ b/jaxrs-service/src/test/java/com/quorum/tessera/api/ConfigResourceTest.java @@ -0,0 +1,84 @@ +package com.quorum.tessera.api; + +import com.quorum.tessera.config.Peer; +import com.quorum.tessera.core.config.ConfigService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; +import static org.assertj.core.api.Assertions.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.mockito.ArgumentMatchers.anyString; +import org.mockito.Mockito; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class ConfigResourceTest { + + private ConfigResource configResource; + + private ConfigService configService; + + @Before + public void onSetUp() { + configService = mock(ConfigService.class); + configResource = new ConfigResource(configService); + } + + @After + public void onTearDown() { + verifyNoMoreInteractions(configService); + } + + @Test + public void addPeerIsSucessful() { + + List peers = new ArrayList<>(); + Mockito.doAnswer((inv) -> { + peers.add(new Peer(inv.getArgument(0))); + return null; + }).when(configService).addPeer(anyString()); + when(configService.getPeers()).thenReturn(peers); + + Peer peer = new Peer("junit"); + + Response response = configResource.addPeer(peer); + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getLocation().toString()).isEqualTo("config/peers/0"); + assertThat(peers).containsExactly(peer); + verify(configService).addPeer(peer.getUrl()); + verify(configService).getPeers(); + } + + @Test + public void getPeerIsSucessful() { + Peer peer = new Peer("getPeerIsSucessfulUrl"); + when(configService.getPeers()).thenReturn(Arrays.asList(peer)); + + Response response = configResource.getPeer(0); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isEqualTo(peer); + + verify(configService).getPeers(); + + } + + @Test + public void getPeerNotFound() { + Peer peer = new Peer("getPeerNoptFound"); + when(configService.getPeers()).thenReturn(Arrays.asList(peer)); + + try { + configResource.getPeer(2); + failBecauseExceptionWasNotThrown(NotFoundException.class); + } catch (NotFoundException ex) { + verify(configService).getPeers(); + } + } +} diff --git a/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java b/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java index 61c0c8caf0..9a2dcf2070 100644 --- a/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java +++ b/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java @@ -1,8 +1,7 @@ package com.quorum.tessera.api.filter; -import com.quorum.tessera.config.Config; import com.quorum.tessera.config.Peer; -import com.quorum.tessera.config.ServerConfig; +import com.quorum.tessera.core.config.ConfigService; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -10,14 +9,11 @@ import javax.servlet.http.HttpServletRequest; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.core.Response; -import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.*; public class IPWhitelistFilterTest { @@ -26,36 +22,30 @@ public class IPWhitelistFilterTest { private IPWhitelistFilter filter; + @Before public void init() throws URISyntaxException, UnknownHostException { this.ctx = mock(ContainerRequestContext.class); - final Config configuration = mock(Config.class); + final ConfigService configService = mock(ConfigService.class); Peer peer = new Peer("http://whitelistedHost:8080"); - when(configuration.getPeers()).thenReturn(Collections.singletonList(peer)); - when(configuration.isUseWhiteList()).thenReturn(true); + when(configService.getPeers()).thenReturn(Collections.singletonList(peer)); + when(configService.isUseWhiteList()).thenReturn(true); - final ServerConfig serverConfig = mock(ServerConfig.class); - when(serverConfig.getBindingAddress()).thenReturn("http://localhost:8080"); - when(configuration.getServerConfig()).thenReturn(serverConfig); - - this.filter = new IPWhitelistFilter(configuration); + this.filter = new IPWhitelistFilter(configService); } @Test public void disabledFilterAllowsAllRequests() throws URISyntaxException, UnknownHostException { - final Config configuration = mock(Config.class); - when(configuration.getPeers()).thenReturn(Collections.emptyList()); - when(configuration.isUseWhiteList()).thenReturn(false); + final ConfigService configService = mock(ConfigService.class); + when(configService.getPeers()).thenReturn(Collections.emptyList()); + when(configService.isUseWhiteList()).thenReturn(false); - final ServerConfig serverConfig = mock(ServerConfig.class); - when(serverConfig.getBindingAddress()).thenReturn("http://localhost:8080"); - when(configuration.getServerConfig()).thenReturn(serverConfig); - final IPWhitelistFilter filter = new IPWhitelistFilter(configuration); + final IPWhitelistFilter filter = new IPWhitelistFilter(configService); final HttpServletRequest request = mock(HttpServletRequest.class); doReturn("someotherhost").when(request).getRemoteAddr(); @@ -148,25 +138,6 @@ public void selfIsWhitelisted() { verifyZeroInteractions(ctx); } - @Test - public void invalidPeerCantBeWhitelisted() throws URISyntaxException { - final Config configuration = mock(Config.class); - - Peer peer = new Peer("ht:whitelistedHost:8080"); - when(configuration.getPeers()).thenReturn(Collections.singletonList(peer)); - when(configuration.isUseWhiteList()).thenReturn(true); - - final ServerConfig serverConfig = mock(ServerConfig.class); - when(serverConfig.getServerUri()).thenReturn(new URI("http://localhost:8080")); - when(configuration.getServerConfig()).thenReturn(serverConfig); - final Throwable throwable = catchThrowable(() -> new IPWhitelistFilter(configuration)); - - assertThat(throwable) - .isInstanceOf(RuntimeException.class) - .hasCauseExactlyInstanceOf(MalformedURLException.class); - - assertThat(throwable.getCause()).hasMessage("unknown protocol: ht"); - } } diff --git a/pom.xml b/pom.xml index ad8217b5bb..3fe14b39f8 100644 --- a/pom.xml +++ b/pom.xml @@ -220,18 +220,18 @@ - org.ec4j.maven - editorconfig-maven-plugin - 0.0.5 - - - check - verify - - check - - - + org.ec4j.maven + editorconfig-maven-plugin + 0.0.5 + + + check + verify + + check + + + @@ -257,6 +257,7 @@ grpc-service jaxrs-service tessera-core + jaxrs-client key-generation @@ -398,6 +399,12 @@ 0.7-SNAPSHOT + + com.quorum.tessera + jaxrs-client + 0.7-SNAPSHOT + + org.glassfish.jersey jersey-bom @@ -719,7 +726,7 @@ -