[NC-2218] RPC auth validation + tests. (#846)

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
mark-terry 6 years ago committed by GitHub
parent 68ad11e9fe
commit 3c7ed68be6
  1. 6
      pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java
  2. 82
      pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/RpcAuthConverter.java
  3. 73
      pantheon/src/test/java/tech/pegasys/pantheon/cli/custom/RpcAuthConverterTest.java
  4. 20
      pantheon/src/test/resources/auth_correct.toml
  5. 21
      pantheon/src/test/resources/auth_duplicate_user.toml
  6. 21
      pantheon/src/test/resources/auth_invalid.toml
  7. 21
      pantheon/src/test/resources/auth_invalid_value.toml
  8. 20
      pantheon/src/test/resources/auth_no_password.toml

@ -30,6 +30,7 @@ import tech.pegasys.pantheon.RunnerBuilder;
import tech.pegasys.pantheon.cli.custom.CorsAllowedOriginsProperty;
import tech.pegasys.pantheon.cli.custom.EnodeToURIPropertyConverter;
import tech.pegasys.pantheon.cli.custom.JsonRPCWhitelistHostsProperty;
import tech.pegasys.pantheon.cli.custom.RpcAuthConverter;
import tech.pegasys.pantheon.config.GenesisConfigFile;
import tech.pegasys.pantheon.consensus.clique.jsonrpc.CliqueRpcApis;
import tech.pegasys.pantheon.consensus.ibft.jsonrpc.IbftRpcApis;
@ -272,8 +273,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
@Option(
names = {"--rpc-http-authentication-enabled"},
description =
"Set if the JSON-RPC service should require authentication (default: ${DEFAULT-VALUE})",
hidden = true)
"Set if the JSON-RPC service should require authentication (default: ${DEFAULT-VALUE})")
private final Boolean isRpcHttpAuthenticationEnabled = false;
@Option(
@ -282,7 +282,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
description =
"Storage file for rpc http authentication credentials (default: ${DEFAULT-VALUE})",
arity = "1",
hidden = true)
converter = RpcAuthConverter.class)
private String rpcHttpAuthenticationCredentialsFile = null;
@Option(

@ -0,0 +1,82 @@
/*
* 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.cli.custom;
import tech.pegasys.pantheon.ethereum.permissioning.TomlConfigFileParser;
import java.io.IOException;
import java.util.stream.Collectors;
import com.google.common.base.Strings;
import net.consensys.cava.toml.TomlParseResult;
import picocli.CommandLine;
public class RpcAuthConverter implements CommandLine.ITypeConverter<String> {
@Override
public String convert(final String value) throws Exception {
TomlParseResult tomlParseResult;
try {
tomlParseResult = TomlConfigFileParser.loadConfigurationFromFile(value);
} catch (IOException e) {
throw new IllegalArgumentException(
"An error occurred while opening the specified RPC authentication configuration file.");
}
if (tomlParseResult.hasErrors()) {
throw new IllegalArgumentException(
"An error occurred while parsing the specified RPC authentication configuration file.");
}
if (!verifyAllUsersHavePassword(tomlParseResult)) {
throw new IllegalArgumentException("RPC user specified without password.");
}
if (!verifyAllEntriesHaveValues(tomlParseResult)) {
throw new IllegalArgumentException(
"RPC authentication configuration file contains invalid values.");
}
return value;
}
private boolean verifyAllUsersHavePassword(final TomlParseResult tomlParseResult) {
int configuredUsers = tomlParseResult.getTable("Users").keySet().size();
int usersWithPasswords =
tomlParseResult
.keyPathSet()
.parallelStream()
.filter(
keySet ->
keySet.contains("Users")
&& keySet.contains("password")
&& !Strings.isNullOrEmpty(tomlParseResult.getString(keySet)))
.collect(Collectors.toList())
.size();
return configuredUsers == usersWithPasswords;
}
private boolean verifyAllEntriesHaveValues(final TomlParseResult tomlParseResult) {
return tomlParseResult
.dottedKeySet()
.parallelStream()
.filter(keySet -> !keySet.contains("password"))
.allMatch(dottedKey -> verifyArray(dottedKey, tomlParseResult));
}
private boolean verifyArray(final String key, final TomlParseResult tomlParseResult) {
return tomlParseResult.isArray(key) && !tomlParseResult.getArrayOrEmpty(key).isEmpty();
}
}

@ -0,0 +1,73 @@
/*
* 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.cli.custom;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.google.common.io.Resources;
import org.junit.Before;
import org.junit.Test;
public class RpcAuthConverterTest {
private RpcAuthConverter rpcAuthConverter;
private static final String CORRECT_TOML = "auth_correct.toml";
private static final String DUPLICATE_USER_TOML = "auth_duplicate_user.toml";
private static final String INVALID_TOML = "auth_invalid.toml";
private static final String INVALID_VALUE_TOML = "auth_invalid_value.toml";
private static final String NO_PASSWORD_TOML = "auth_no_password.toml";
@Before
public void setUp() {
rpcAuthConverter = new RpcAuthConverter();
}
@Test
public void shouldPassWhenCorrectTOML() {
assertThatCode(() -> rpcAuthConverter.convert(getFilePath(CORRECT_TOML)))
.doesNotThrowAnyException();
}
@Test
public void shouldFailWhenInvalidTOML() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(INVALID_TOML)))
.isInstanceOf(Exception.class)
.hasMessageContaining("Invalid TOML configuration");
}
@Test
public void shouldFailWhenMissingPassword() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(NO_PASSWORD_TOML)))
.isInstanceOf(Exception.class)
.hasMessage("RPC user specified without password.");
}
@Test
public void shouldFailWhenInvalidKeyValue() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(INVALID_VALUE_TOML)))
.isInstanceOf(Exception.class)
.hasMessage("RPC authentication configuration file contains invalid values.");
}
@Test
public void shouldFailWhenDuplicateUser() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(DUPLICATE_USER_TOML)))
.isInstanceOf(Exception.class)
.hasMessageContaining("Invalid TOML configuration");
}
private String getFilePath(final String resourceName) {
return Resources.getResource(resourceName).getPath();
}
}

@ -0,0 +1,20 @@
[Users.userA]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = ["admin"]
permissions = ["eth:*", "perm:*"]
roles = ["net"]
[Users.userB]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = ["admin"]
permissions = ["eth:*", "perm:*"]
roles = ["net"]
[Groups.admins]
roles = ["admin"]
[Roles.admin]
permissions = ["admin:*"]
[Roles.net]
permissions = ["net:*"]

@ -0,0 +1,21 @@
[Users.userA]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = ["admin"]
permissions = ["eth:*", "perm:*"]
roles = ["net"]
# This user is a duplicate and invalid
[Users.userA]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGD"
groups = ["admin"]
permissions = ["eth:*", "perm:*"]
roles = ["net"]
[Groups.admins]
roles = ["admin"]
[Roles.admin]
permissions = ["admin:*"]
[Roles.net]
permissions = ["net:*"]

@ -0,0 +1,21 @@
# Replaced square brackets with braces
{Users.userA}
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = {"admin"}
permissions = {"eth:*", "perm:*"}
roles = {"net"}
{Users.userB}
groups = {"admin"}
permissions = {"eth:*", "perm:*"}
roles = {"net"}
{Groups.admins}
roles = {"admin"}
{Roles.admin}
permissions = {"admin:*"}
{Roles.net}
permissions = {"net:*"}

@ -0,0 +1,21 @@
[Users.userA]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
# This line is invalid - should be an array
groups = "admin"
permissions = ["eth:*", "perm:*"]
roles = ["net"]
[Users.userB]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = ["admin"]
permissions = ["eth:*", "perm:*"]
roles = ["net"]
[Groups.admins]
roles = ["admin"]
[Roles.admin]
permissions = ["admin:*"]
[Roles.net]
permissions = ["net:*"]

@ -0,0 +1,20 @@
[Users.userA]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
groups = ["admin"]
permissions = ["eth:*", "perm:*"]
roles = ["net"]
# This user has no password
[Users.userB]
groups = ["admin"]
permissions = ["eth:*", "perm:*"]
roles = ["net"]
[Groups.admins]
roles = ["admin"]
[Roles.admin]
permissions = ["admin:*"]
[Roles.net]
permissions = ["net:*"]
Loading…
Cancel
Save