From a4b604518cd645eb137d33a839a96eb48c53c79c Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Tue, 29 Jan 2019 21:56:59 +1000 Subject: [PATCH] Nc 2107 permissioning config toml file (#643) * create permissioning config from toml file * added CLI options for separately enabling node and account permissioning * moved check for bootnodes on whitelist out of CLI --- .../PermissioningConfigurationBuilder.java | 108 +++++++++++ .../pantheon/TomlConfigFileParser.java | 47 +++++ .../cli/ConfigOptionSearchAndRunHandler.java | 2 +- .../pantheon/cli/DefaultCommandValues.java | 1 + .../pegasys/pantheon/cli/PantheonCommand.java | 52 +++--- .../custom/EnodeToURIPropertyConverter.java | 10 +- .../PermissioningConfigurationValidator.java | 69 ++++++++ ...PermissioningConfigurationBuilderTest.java | 154 ++++++++++++++++ .../pantheon/cli/PantheonCommandTest.java | 167 +----------------- ...rmissioningConfigurationValidatorTest.java | 77 ++++++++ .../src/test/resources/complete_config.toml | 2 +- .../src/test/resources/everything_config.toml | 4 +- .../src/test/resources/partial_config.toml | 2 +- .../test/resources/permissioning_config.toml | 4 + ...ermissioning_config_absent_whitelists.toml | 3 + ...sioning_config_account_whitelist_only.toml | 3 + ...permissioning_config_empty_whitelists.toml | 4 + .../permissioning_config_invalid_enode.toml | 4 + ...missioning_config_node_whitelist_only.toml | 3 + ...ermissioning_config_ropsten_bootnodes.toml | 5 + 20 files changed, 525 insertions(+), 196 deletions(-) create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/PermissioningConfigurationBuilder.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/TomlConfigFileParser.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidator.java create mode 100644 pantheon/src/test/java/tech/pegasys/pantheon/PermissioningConfigurationBuilderTest.java create mode 100644 pantheon/src/test/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidatorTest.java create mode 100644 pantheon/src/test/resources/permissioning_config.toml create mode 100644 pantheon/src/test/resources/permissioning_config_absent_whitelists.toml create mode 100644 pantheon/src/test/resources/permissioning_config_account_whitelist_only.toml create mode 100644 pantheon/src/test/resources/permissioning_config_empty_whitelists.toml create mode 100644 pantheon/src/test/resources/permissioning_config_invalid_enode.toml create mode 100644 pantheon/src/test/resources/permissioning_config_node_whitelist_only.toml create mode 100644 pantheon/src/test/resources/permissioning_config_ropsten_bootnodes.toml diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/PermissioningConfigurationBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/PermissioningConfigurationBuilder.java new file mode 100644 index 0000000000..9d555edd65 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/PermissioningConfigurationBuilder.java @@ -0,0 +1,108 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import tech.pegasys.pantheon.cli.custom.EnodeToURIPropertyConverter; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; + +import java.io.File; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.io.Resources; +import net.consensys.cava.toml.TomlArray; +import net.consensys.cava.toml.TomlParseResult; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class PermissioningConfigurationBuilder { + + private static final Logger LOG = LogManager.getLogger(); + + // will be used to reload the config from a file while node is running + public static PermissioningConfiguration permissioningConfigurationFromToml( + final String configFilePath, + final boolean permissionedNodeEnabled, + final boolean permissionedAccountEnabled) { + + String permToml = permissioningConfigTomlAsString(permissioningConfigFile(configFilePath)); + return permissioningConfiguration( + TomlConfigFileParser.loadConfiguration(permToml), + permissionedNodeEnabled, + permissionedAccountEnabled); + } + + public static PermissioningConfiguration permissioningConfiguration( + final TomlParseResult permToml, + final boolean permissionedNodeEnabled, + final boolean permissionedAccountEnabled) { + + TomlArray accountWhitelistTomlArray = permToml.getArray("accounts-whitelist"); + TomlArray nodeWhitelistTomlArray = permToml.getArray("nodes-whitelist"); + + final PermissioningConfiguration permissioningConfiguration = + PermissioningConfiguration.createDefault(); + if (permissionedAccountEnabled) { + if (accountWhitelistTomlArray != null) { + List accountsWhitelistToml = + accountWhitelistTomlArray + .toList() + .stream() + .map(Object::toString) + .collect(Collectors.toList()); + permissioningConfiguration.setAccountWhitelist(accountsWhitelistToml); + } else { + permissioningConfiguration.setAccountWhitelist(new ArrayList<>()); + } + } + if (permissionedNodeEnabled) { + if (nodeWhitelistTomlArray != null) { + List nodesWhitelistToml = + nodeWhitelistTomlArray + .toList() + .stream() + .map(Object::toString) + .map(EnodeToURIPropertyConverter::convertToURI) + .collect(Collectors.toList()); + permissioningConfiguration.setNodeWhitelist(nodesWhitelistToml); + } else { + permissioningConfiguration.setNodeWhitelist(new ArrayList<>()); + } + } + return permissioningConfiguration; + } + + private static String permissioningConfigTomlAsString(final File file) { + try { + return Resources.toString(file.toURI().toURL(), UTF_8); + } catch (final Exception e) { + LOG.error("Unable to load permissioning config {}.", file, e); + return null; + } + } + + private static File permissioningConfigFile(final String filename) { + + final File permissioningConfigFile = new File(filename); + if (permissioningConfigFile.exists()) { + return permissioningConfigFile; + } else { + LOG.error("File does not exist: permissioning config path: {}.", filename); + return null; + } + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/TomlConfigFileParser.java b/pantheon/src/main/java/tech/pegasys/pantheon/TomlConfigFileParser.java new file mode 100644 index 0000000000..65e3dd8f83 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/TomlConfigFileParser.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon; + +import java.util.stream.Collectors; + +import net.consensys.cava.toml.Toml; +import net.consensys.cava.toml.TomlParseError; +import net.consensys.cava.toml.TomlParseResult; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TomlConfigFileParser { + + private static final Logger LOG = LogManager.getLogger(); + + private static TomlParseResult checkConfigurationValidity( + final TomlParseResult result, final String toml) { + if (result == null || result.isEmpty()) { + LOG.error("Unable to read TOML configuration file %s", toml); + } + return result; + } + + public static TomlParseResult loadConfiguration(final String toml) { + TomlParseResult result = Toml.parse(toml); + + if (result.hasErrors()) { + final String errors = + result.errors().stream().map(TomlParseError::toString).collect(Collectors.joining("%n")); + ; + LOG.error("Invalid TOML configuration : %s", errors); + } + + return checkConfigurationValidity(result, toml); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java index e805dd6368..784a7fdb71 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java @@ -41,7 +41,7 @@ class ConfigOptionSearchAndRunHandler extends AbstractParseResultHandler bootnodes = - DiscoveryConfiguration.getBootstrapPeersFromGenericCollection( - ethNetworkConfig.getBootNodes()); - if (permissioningConfiguration.isNodeWhitelistSet() && bootnodes != null) { - final List whitelist = - permissioningConfiguration - .getNodeWhitelist() - .stream() - .map(DefaultPeer::fromURI) - .collect(Collectors.toList()); - for (final Peer bootnode : bootnodes) { - if (!whitelist.contains(bootnode)) { - throw new ParameterException( - new CommandLine(this), - "Cannot start node with bootnode(s) that are not in nodes-whitelist " + bootnode); - } - } + try { + PermissioningConfigurationValidator.areAllBootnodesAreInWhitelist( + ethNetworkConfig, permissioningConfiguration); + } catch (Exception e) { + throw new ParameterException(new CommandLine(this), e.getMessage()); } } @@ -681,10 +679,16 @@ MetricsConfiguration metricsConfiguration() { } private PermissioningConfiguration permissioningConfiguration() { + + if (!permissionsAccountsEnabled && !permissionsNodesEnabled) { + return PermissioningConfiguration.createDefault(); + } + final PermissioningConfiguration permissioningConfiguration = - PermissioningConfiguration.createDefault(); - permissioningConfiguration.setNodeWhitelist(nodesWhitelist); - permissioningConfiguration.setAccountWhitelist(accountsWhitelist); + PermissioningConfigurationBuilder.permissioningConfigurationFromToml( + DefaultCommandValues.PERMISSIONING_CONFIG_LOCATION, + permissionsNodesEnabled, + permissionsAccountsEnabled); return permissioningConfiguration; } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/EnodeToURIPropertyConverter.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/EnodeToURIPropertyConverter.java index 83e0f09f4f..44e647e521 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/EnodeToURIPropertyConverter.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/EnodeToURIPropertyConverter.java @@ -38,6 +38,10 @@ public class EnodeToURIPropertyConverter implements ITypeConverter { @Override public URI convert(final String value) throws IllegalArgumentException { + return convertToURI(value); + } + + public static URI convertToURI(final String value) throws IllegalArgumentException { checkArgument( value != null && !value.isEmpty(), "Can't convert null/empty string to EnodeURLProperty."); @@ -85,7 +89,7 @@ public URI convert(final String value) throws IllegalArgumentException { } } - private String getAndValidateNodeId(final Matcher matcher) { + private static String getAndValidateNodeId(final Matcher matcher) { final String invalidNodeIdErrorMsg = "Enode URL contains an invalid node ID. Node ID must have 128 characters and shouldn't include the '0x' hex prefix."; final String nodeId = matcher.group("nodeId"); @@ -96,7 +100,7 @@ private String getAndValidateNodeId(final Matcher matcher) { return nodeId; } - private Integer getAndValidatePort(final Matcher matcher, final String portName) { + private static Integer getAndValidatePort(final Matcher matcher, final String portName) { int port = Integer.valueOf(matcher.group(portName)); checkArgument( NetworkUtility.isValidPort(port), @@ -104,7 +108,7 @@ private Integer getAndValidatePort(final Matcher matcher, final String portName) return port; } - private boolean containsDiscoveryPort(final String value) { + private static boolean containsDiscoveryPort(final String value) { return value.contains("discport"); } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidator.java b/pantheon/src/main/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidator.java new file mode 100644 index 0000000000..fcd3d1bad2 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidator.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.util; + +import tech.pegasys.pantheon.cli.EthNetworkConfig; +import tech.pegasys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import tech.pegasys.pantheon.ethereum.p2p.peers.Endpoint; +import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.stream.Collectors; + +import org.bouncycastle.util.encoders.Hex; + +public class PermissioningConfigurationValidator { + + public static void areAllBootnodesAreInWhitelist( + final EthNetworkConfig ethNetworkConfig, + final PermissioningConfiguration permissioningConfiguration) + throws Exception { + List bootnodesNotInWhitelist = new ArrayList<>(); + final List bootnodes = + DiscoveryConfiguration.getBootstrapPeersFromGenericCollection( + ethNetworkConfig.getBootNodes()); + if (permissioningConfiguration.isNodeWhitelistSet() && bootnodes != null) { + bootnodesNotInWhitelist = + bootnodes + .stream() + .filter( + node -> + !permissioningConfiguration + .getNodeWhitelist() + .contains(URI.create(buildEnodeURI(node)))) + .collect(Collectors.toList()); + } + if (!bootnodesNotInWhitelist.isEmpty()) { + throw new Exception("Bootnode(s) not in nodes-whitelist " + bootnodesNotInWhitelist); + } + } + + private static String buildEnodeURI(final Peer s) { + String url = Hex.toHexString(s.getId().extractArray()); + Endpoint endpoint = s.getEndpoint(); + String nodeIp = endpoint.getHost(); + OptionalInt tcpPort = endpoint.getTcpPort(); + int udpPort = endpoint.getUdpPort(); + + if (tcpPort.isPresent() && (tcpPort.getAsInt() != udpPort)) { + return String.format( + "enode://%s@%s:%d?discport=%d", url, nodeIp, tcpPort.getAsInt(), udpPort); + } else { + return String.format("enode://%s@%s:%d", url, nodeIp, udpPort); + } + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/PermissioningConfigurationBuilderTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/PermissioningConfigurationBuilderTest.java new file mode 100644 index 0000000000..22e8b7d6bc --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/PermissioningConfigurationBuilderTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.fail; + +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.google.common.io.Resources; +import net.consensys.cava.toml.Toml; +import net.consensys.cava.toml.TomlParseResult; +import org.junit.Test; + +public class PermissioningConfigurationBuilderTest { + + static final String PERMISSIONING_CONFIG_TOML = "permissioning_config.toml"; + static final String PERMISSIONING_CONFIG_ACCOUNT_WHITELIST_ONLY = + "permissioning_config_account_whitelist_only.toml"; + static final String PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY = + "permissioning_config_node_whitelist_only.toml"; + static final String PERMISSIONING_CONFIG_INVALID_ENODE = + "permissioning_config_invalid_enode.toml"; + static final String PERMISSIONING_CONFIG_EMPTY_WHITELISTS = + "permissioning_config_empty_whitelists.toml"; + static final String PERMISSIONING_CONFIG_ABSENT_WHITELISTS = + "permissioning_config_absent_whitelists.toml"; + + private final String VALID_NODE_ID = + "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"; + + @Test + public void permissioningConfig() throws IOException { + + final String uri = "enode://" + VALID_NODE_ID + "@192.168.0.9:4567"; + final String uri2 = "enode://" + VALID_NODE_ID + "@192.169.0.9:4568"; + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_TOML); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + PermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, true, true); + + assertThat(permissioningConfiguration.isAccountWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getAccountWhitelist()) + .containsExactly("0x0000000000000000000000000000000000000009"); + assertThat(permissioningConfiguration.isNodeWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getNodeWhitelist()) + .containsExactly(URI.create(uri), URI.create(uri2)); + } + + @Test + public void permissioningConfigWithOnlyNodeWhitelistSet() throws IOException { + + final String uri = "enode://" + VALID_NODE_ID + "@192.168.0.9:4567"; + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + PermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, true, false); + + assertThat(permissioningConfiguration.isAccountWhitelistSet()).isFalse(); + assertThat(permissioningConfiguration.isNodeWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getNodeWhitelist()).containsExactly(URI.create(uri)); + } + + @Test + public void permissioningConfigWithOnlyAccountWhitelistSet() throws IOException { + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_ACCOUNT_WHITELIST_ONLY); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + PermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, false, true); + + assertThat(permissioningConfiguration.isNodeWhitelistSet()).isFalse(); + assertThat(permissioningConfiguration.isAccountWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getAccountWhitelist()) + .containsExactly("0x0000000000000000000000000000000000000009"); + } + + @Test + public void permissioningConfigWithInvalidEnode() throws IOException { + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_INVALID_ENODE); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + try { + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, true, true); + fail("Expecting IllegalArgumentException: Enode URL contains an invalid node ID"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).startsWith("Enode URL contains an invalid node ID"); + } + } + + @Test + public void permissioningConfigWithEmptyWhitelistMustNotError() throws IOException { + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_EMPTY_WHITELISTS); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + PermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, true, true); + + assertThat(permissioningConfiguration.isNodeWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getNodeWhitelist()).isEmpty(); + assertThat(permissioningConfiguration.isAccountWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getAccountWhitelist()).isEmpty(); + } + + @Test + public void permissioningConfigWithAbsentWhitelistMustSetValues() throws IOException { + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_ABSENT_WHITELISTS); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + PermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, true, true); + + assertThat(permissioningConfiguration.isNodeWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getNodeWhitelist()).isEmpty(); + assertThat(permissioningConfiguration.isAccountWhitelistSet()).isTrue(); + assertThat(permissioningConfiguration.getAccountWhitelist()).isEmpty(); + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index edd4791611..857d7c35ff 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -52,7 +52,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -312,10 +311,10 @@ public void overrideDefaultValuesIfKeyIsPresentInConfigFile() throws IOException } @Test - public void tomlThatConfiguresEverything() throws IOException { + public void tomlThatConfiguresEverythingExceptPermissioning() throws IOException { assumeTrue(isFullInstantiation()); - // Load a TOML that configures literally everything + // Load a TOML that configures literally everything (except permissioning) final URL configFile = Resources.getResource("everything_config.toml"); final Path toml = Files.createTempFile("toml", ""); Files.write(toml, Resources.toByteArray(configFile)); @@ -708,168 +707,6 @@ public void callingWithRefreshDelayWithNegativeValueMustError() { assertThat(commandErrorOutput.toString()).startsWith(expectedErrorMsg); } - @Test - public void callingWithInvalidNodesWhitelistMustDisplayErrorAndUsage() { - parseCommand("--nodes-whitelist", "invalid_enode_url"); - assertThat(commandOutput.toString()).isEmpty(); - /* - Because of the way Picocli handles errors parsing errors for lists with arity 0..*, we don't - get the nice error msg with that was wrong. It only shows to the user the values that weren't - parsed correctly. - */ - final String expectedErrorOutputStart = "Unmatched argument: invalid_enode_url"; - assertThat(commandErrorOutput.toString()).startsWith(expectedErrorOutputStart); - } - - @Test - public void nodesWhitelistOptionMustBeUsed() { - parseCommand( - "--nodes-whitelist", - String.join(",", validENodeStrings), - "--bootnodes", - validENodeStrings[0]); - - verify(mockRunnerBuilder) - .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - final List enodeURLAsStringList = - permissioningConfigurationArgumentCaptor - .getValue() - .getNodeWhitelist() - .stream() - .map(URI::toString) - .collect(Collectors.toList()); - - assertThat(enodeURLAsStringList).containsExactlyInAnyOrder(validENodeStrings); - assertThat(permissioningConfigurationArgumentCaptor.getValue().isNodeWhitelistSet()).isTrue(); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()).isEmpty(); - } - - @Test - public void callingWithAccountsWhitelistOptionButNoValueMustNotError() { - parseCommand("--accounts-whitelist"); - - verify(mockRunnerBuilder) - .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(permissioningConfigurationArgumentCaptor.getValue().getAccountWhitelist()).isEmpty(); - assertThat(permissioningConfigurationArgumentCaptor.getValue().isAccountWhitelistSet()) - .isTrue(); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()).isEmpty(); - } - - @Test - public void accountsWhitelistOptionMustBeUsed() { - final String[] accounts = {"1111111111111111", "2222222222222222", "ffffffffffffffff"}; - parseCommand("--accounts-whitelist", String.join(",", accounts)); - - verify(mockRunnerBuilder) - .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(permissioningConfigurationArgumentCaptor.getValue().getAccountWhitelist()) - .containsExactlyInAnyOrder(accounts); - assertThat(permissioningConfigurationArgumentCaptor.getValue().isAccountWhitelistSet()) - .isTrue(); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()).isEmpty(); - } - - @Test - public void nodesWhitelistOptionMustIncludeBootnodes() { - parseCommand( - "--bootnodes", - String.join(",", validENodeStrings[0], validENodeStrings[1]), - "--nodes-whitelist", - String.join(",", validENodeStrings)); - - verify(mockRunnerBuilder) - .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - final List enodeURLAsStringList = - permissioningConfigurationArgumentCaptor - .getValue() - .getNodeWhitelist() - .stream() - .map(URI::toString) - .collect(Collectors.toList()); - - assertThat(enodeURLAsStringList).containsExactlyInAnyOrder(validENodeStrings); - assertThat(permissioningConfigurationArgumentCaptor.getValue().isNodeWhitelistSet()).isTrue(); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()).isEmpty(); - } - - @Test - public void nodesWhitelistOptionWhichDoesNotIncludeBootnodesMustDisplayError() { - final String bootNodeNotWhitelisted = "enode://" + VALID_NODE_ID + "@192.168.0.9:4567"; - parseCommand( - "--bootnodes", - String.join(",", bootNodeNotWhitelisted, validENodeStrings[2]), - "--nodes-whitelist", - String.join(",", validENodeStrings)); - - verifyZeroInteractions(mockRunnerBuilder); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()) - .contains("Cannot start node with bootnode(s) that are not in nodes-whitelist"); - } - - @Test - public void ropstenWithNodesWhitelistOptionWhichDoesIncludeRopstenBootnodesMustNotDisplayError() { - parseCommand("--network", "ropsten", "--nodes-whitelist", String.join(",", ropstenBootnodes)); - - verify(mockRunnerBuilder) - .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - final List enodeURLAsStringList = - permissioningConfigurationArgumentCaptor - .getValue() - .getNodeWhitelist() - .stream() - .map(URI::toString) - .collect(Collectors.toList()); - - assertThat(enodeURLAsStringList).containsExactlyInAnyOrder(ropstenBootnodes); - assertThat(permissioningConfigurationArgumentCaptor.getValue().isNodeWhitelistSet()).isTrue(); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()).isEmpty(); - } - - @Test - public void ropstenWithNodesWhitelistOptionWhichDoesNotIncludeRopstenBootnodesMustDisplayError() { - parseCommand("--network", "ropsten", "--nodes-whitelist", String.join(",", validENodeStrings)); - - verifyZeroInteractions(mockRunnerBuilder); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()) - .contains("Cannot start node with bootnode(s) that are not in nodes-whitelist"); - } - - @Test - public void nodesWhitelistWithEmptyListAndNonEmptyBootnodesMustDisplayError() { - parseCommand("--bootnodes", String.join(",", validENodeStrings[0]), "--nodes-whitelist"); - - verifyZeroInteractions(mockRunnerBuilder); - - assertThat(commandOutput.toString()).isEmpty(); - assertThat(commandErrorOutput.toString()) - .contains("Cannot start node with bootnode(s) that are not in nodes-whitelist"); - } - @Test public void bannedNodeIdsOptionMustBeUsed() { final String[] nodes = {"0001", "0002", "0003"}; diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidatorTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidatorTest.java new file mode 100644 index 0000000000..a84a3ddc07 --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidatorTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import tech.pegasys.pantheon.PermissioningConfigurationBuilder; +import tech.pegasys.pantheon.cli.EthNetworkConfig; +import tech.pegasys.pantheon.cli.NetworkName; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.google.common.io.Resources; +import net.consensys.cava.toml.Toml; +import net.consensys.cava.toml.TomlParseResult; +import org.junit.Test; + +public class PermissioningConfigurationValidatorTest { + + static final String PERMISSIONING_CONFIG_ROPSTEN_BOOTNODES = + "permissioning_config_ropsten_bootnodes.toml"; + static final String PERMISSIONING_CONFIG = "permissioning_config.toml"; + + @Test + public void ropstenWithNodesWhitelistOptionWhichDoesIncludeRopstenBootnodesMustNotError() + throws Exception { + + EthNetworkConfig ethNetworkConfig = EthNetworkConfig.getNetworkConfig(NetworkName.ROPSTEN); + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_ROPSTEN_BOOTNODES); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + PermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, true, true); + + PermissioningConfigurationValidator.areAllBootnodesAreInWhitelist( + ethNetworkConfig, permissioningConfiguration); + } + + @Test + public void nodesWhitelistOptionWhichDoesNotIncludeBootnodesMustError() throws Exception { + + EthNetworkConfig ethNetworkConfig = EthNetworkConfig.getNetworkConfig(NetworkName.ROPSTEN); + + final URL configFile = Resources.getResource(PERMISSIONING_CONFIG); + final Path toml = Files.createTempFile("toml", ""); + Files.write(toml, Resources.toByteArray(configFile)); + final TomlParseResult tomlResult = Toml.parse(toml); + + PermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration(tomlResult, true, true); + + try { + PermissioningConfigurationValidator.areAllBootnodesAreInWhitelist( + ethNetworkConfig, permissioningConfiguration); + fail("expected exception because ropsten bootnodes are not in node-whitelist"); + } catch (Exception e) { + assertThat(e.getMessage().startsWith("Bootnode")).isTrue(); + } + } +} diff --git a/pantheon/src/test/resources/complete_config.toml b/pantheon/src/test/resources/complete_config.toml index c270fb5e39..fa0d6329e1 100644 --- a/pantheon/src/test/resources/complete_config.toml +++ b/pantheon/src/test/resources/complete_config.toml @@ -25,7 +25,7 @@ metrics-port=309 genesis-file="~/genesis.json" # Path network-id=42 sync-mode="fast"# should be FAST or FULL (or fast or full) -ottoman=false # true means using ottoman testnet if genesys file uses iBFT +ottoman=false # true means using ottoman testnet if genesis file uses iBFT #mining miner-coinbase="0x0000000000000000000000000000000000000002" \ No newline at end of file diff --git a/pantheon/src/test/resources/everything_config.toml b/pantheon/src/test/resources/everything_config.toml index 262b07ae97..3d68fc1b69 100644 --- a/pantheon/src/test/resources/everything_config.toml +++ b/pantheon/src/test/resources/everything_config.toml @@ -2,7 +2,7 @@ # The odds are you are reading this because you added a CLI and didn't add it # here and a test broke. To fix the test add your CLI to this file. # -# Please use a plausable value, Pantheon has to at least be able to parse it. +# Please use a plausible value, Pantheon has to at least be able to parse it. # If it is a multi-valued CLI make it a TOML array. # If it is a number or boolean make it a number or boolean # All other config options are strings, and must be quoted. @@ -67,6 +67,8 @@ min-gas-price="1" # Permissioning accounts-whitelist=["0x0000000000000000000000000000000000000009"] nodes-whitelist=["all"] +permissions-nodes-enabled=false +permissions-accounts-enabled=false # Privacy privacy-url="http://127.0.0.1:8888" diff --git a/pantheon/src/test/resources/partial_config.toml b/pantheon/src/test/resources/partial_config.toml index 3de87f7931..f2d8eaa061 100644 --- a/pantheon/src/test/resources/partial_config.toml +++ b/pantheon/src/test/resources/partial_config.toml @@ -1,4 +1,4 @@ -# this is a valid prtial TOML config file +# this is a valid partial TOML config file #mining miner-coinbase="0x0000000000000000000000000000000000000002" \ No newline at end of file diff --git a/pantheon/src/test/resources/permissioning_config.toml b/pantheon/src/test/resources/permissioning_config.toml new file mode 100644 index 0000000000..0457556cfd --- /dev/null +++ b/pantheon/src/test/resources/permissioning_config.toml @@ -0,0 +1,4 @@ +# Permissioning TOML file + +accounts-whitelist=["0x0000000000000000000000000000000000000009"] +nodes-whitelist=["enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.9:4567","enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.169.0.9:4568"] diff --git a/pantheon/src/test/resources/permissioning_config_absent_whitelists.toml b/pantheon/src/test/resources/permissioning_config_absent_whitelists.toml new file mode 100644 index 0000000000..566e717f0b --- /dev/null +++ b/pantheon/src/test/resources/permissioning_config_absent_whitelists.toml @@ -0,0 +1,3 @@ +# Permissioning TOML file with absent lists + +accounts-whitelist= diff --git a/pantheon/src/test/resources/permissioning_config_account_whitelist_only.toml b/pantheon/src/test/resources/permissioning_config_account_whitelist_only.toml new file mode 100644 index 0000000000..c7333f2d77 --- /dev/null +++ b/pantheon/src/test/resources/permissioning_config_account_whitelist_only.toml @@ -0,0 +1,3 @@ +# Permissioning TOML file (account whitelist only) + +accounts-whitelist=["0x0000000000000000000000000000000000000009"] diff --git a/pantheon/src/test/resources/permissioning_config_empty_whitelists.toml b/pantheon/src/test/resources/permissioning_config_empty_whitelists.toml new file mode 100644 index 0000000000..1e7eae0cf5 --- /dev/null +++ b/pantheon/src/test/resources/permissioning_config_empty_whitelists.toml @@ -0,0 +1,4 @@ +# Permissioning TOML file with empty lists + +accounts-whitelist=[] +nodes-whitelist=[] diff --git a/pantheon/src/test/resources/permissioning_config_invalid_enode.toml b/pantheon/src/test/resources/permissioning_config_invalid_enode.toml new file mode 100644 index 0000000000..89ad7ada23 --- /dev/null +++ b/pantheon/src/test/resources/permissioning_config_invalid_enode.toml @@ -0,0 +1,4 @@ +# Permissioning TOML file + +accounts-whitelist=["0x0000000000000000000000000000000000000009"] +nodes-whitelist=["enode://bob@192.168.0.9:4567"] diff --git a/pantheon/src/test/resources/permissioning_config_node_whitelist_only.toml b/pantheon/src/test/resources/permissioning_config_node_whitelist_only.toml new file mode 100644 index 0000000000..ffd69e6437 --- /dev/null +++ b/pantheon/src/test/resources/permissioning_config_node_whitelist_only.toml @@ -0,0 +1,3 @@ +# Permissioning TOML file (node whitelist only) + +nodes-whitelist=["enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.9:4567"] diff --git a/pantheon/src/test/resources/permissioning_config_ropsten_bootnodes.toml b/pantheon/src/test/resources/permissioning_config_ropsten_bootnodes.toml new file mode 100644 index 0000000000..aac5b1a75d --- /dev/null +++ b/pantheon/src/test/resources/permissioning_config_ropsten_bootnodes.toml @@ -0,0 +1,5 @@ +# Permissioning TOML file + +accounts-whitelist=["0x0000000000000000000000000000000000000009"] +nodes-whitelist=["enode://6332792c4a00e3e4ee0926ed89e0d27ef985424d97b6a45bf0f23e51f0dcb5e66b875777506458aea7af6f9e4ffb69f43f3778ee73c81ed9d34c51c4b16b0b0f@52.232.243.152:30303", + "enode://94c15d1b9e2fe7ce56e458b9a3b672ef11894ddedd0c6f247e0f1d3487f52b66208fb4aeb8179fce6e3a749ea93ed147c37976d67af557508d199d9594c35f09@192.81.208.223:30303"]