Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Commit

Permalink
Nc 2107 permissioning config toml file (#643)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
macfarla authored Jan 29, 2019
1 parent d88e210 commit 7660e99
Show file tree
Hide file tree
Showing 20 changed files with 525 additions and 196 deletions.
Original file line number Diff line number Diff line change
@@ -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<String> accountsWhitelistToml =
accountWhitelistTomlArray
.toList()
.stream()
.map(Object::toString)
.collect(Collectors.toList());
permissioningConfiguration.setAccountWhitelist(accountsWhitelistToml);
} else {
permissioningConfiguration.setAccountWhitelist(new ArrayList<>());
}
}
if (permissionedNodeEnabled) {
if (nodeWhitelistTomlArray != null) {
List<URI> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ConfigOptionSearchAndRunHandler extends AbstractParseResultHandler<List<Ob
this.configFileOptionName = configFileOptionName;
this.isDocker = isDocker;
// use the same output as the regular options handler to ensure that outputs are all going
// the in the same place. No need to do this for the exception handler as we reuse it directly.
// in the same place. No need to do this for the exception handler as we reuse it directly.
this.useOut(resultHandler.out());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface DefaultCommandValues {
long DEFAULT_MAX_REFRESH_DELAY = 3600000;
long DEFAULT_MIN_REFRESH_DELAY = 1;
String DOCKER_GENESIS_LOCATION = "/etc/pantheon/genesis.json";
String PERMISSIONING_CONFIG_LOCATION = "/etc/pantheon/permissioned_config.toml";
String DOCKER_DATADIR_LOCATION = "/var/lib/pantheon";
String MANDATORY_HOST_FORMAT_HELP = "<HOST>";
String MANDATORY_PORT_FORMAT_HELP = "<PORT>";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration.DEFAULT_METRICS_PORT;
import static tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration.createDefault;

import tech.pegasys.pantheon.PermissioningConfigurationBuilder;
import tech.pegasys.pantheon.Runner;
import tech.pegasys.pantheon.RunnerBuilder;
import tech.pegasys.pantheon.cli.custom.CorsAllowedOriginsProperty;
Expand All @@ -44,15 +45,13 @@
import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApi;
import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.ethereum.p2p.config.DiscoveryConfiguration;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem;
import tech.pegasys.pantheon.util.BlockImporter;
import tech.pegasys.pantheon.util.InvalidConfigurationException;
import tech.pegasys.pantheon.util.PermissioningConfigurationValidator;
import tech.pegasys.pantheon.util.bytes.BytesValue;

import java.io.File;
Expand All @@ -66,7 +65,6 @@
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.io.Resources;
Expand Down Expand Up @@ -457,6 +455,18 @@ private Long configureRefreshDelay(final Long refreshDelay) {
)
private final BytesValue extraData = DEFAULT_EXTRA_DATA;

@Option(
names = {"--permissions-nodes-enabled"},
description = "Set if node level permissions should be enabled (default: ${DEFAULT-VALUE})"
)
private final Boolean permissionsNodesEnabled = false;

@Option(
names = {"--permissions-accounts-enabled"},
description = "Set if account level permissions should be enabled (default: ${DEFAULT-VALUE})"
)
private final Boolean permissionsAccountsEnabled = false;

// Permissioning: A list of whitelist nodes can be passed.
@Option(
names = {"--nodes-whitelist"},
Expand Down Expand Up @@ -600,23 +610,11 @@ private NetworkName getNetwork() {
private void ensureAllBootnodesAreInWhitelist(
final EthNetworkConfig ethNetworkConfig,
final PermissioningConfiguration permissioningConfiguration) {
final List<Peer> bootnodes =
DiscoveryConfiguration.getBootstrapPeersFromGenericCollection(
ethNetworkConfig.getBootNodes());
if (permissioningConfiguration.isNodeWhitelistSet() && bootnodes != null) {
final List<Peer> 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());
}
}

Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public class EnodeToURIPropertyConverter implements ITypeConverter<URI> {

@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.");

Expand Down Expand Up @@ -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");
Expand All @@ -96,15 +100,15 @@ 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),
"Invalid " + portName + " port range. Port should be between 0 - 65535");
return port;
}

private boolean containsDiscoveryPort(final String value) {
private static boolean containsDiscoveryPort(final String value) {
return value.contains("discport");
}
}
Original file line number Diff line number Diff line change
@@ -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<Peer> bootnodesNotInWhitelist = new ArrayList<>();
final List<Peer> 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);
}
}
}
Loading

0 comments on commit 7660e99

Please sign in to comment.