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

[Minor] fix auth file validation errors #886

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
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.cli.custom.RpcAuthFileValidator;
import tech.pegasys.pantheon.config.GenesisConfigFile;
import tech.pegasys.pantheon.consensus.clique.jsonrpc.CliqueRpcApis;
import tech.pegasys.pantheon.consensus.ibft.jsonrpc.IbftRpcApis;
Expand Down Expand Up @@ -65,7 +65,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
Expand Down Expand Up @@ -641,12 +640,11 @@ private JsonRpcConfiguration jsonRpcConfiguration() throws Exception {
"--rpc-http-authentication-enabled",
"--rpc-http-authentication-credentials-file"));

CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-http-authentication-enabled",
!isRpcHttpAuthenticationEnabled,
Collections.singletonList("--rpc-http-authentication-credentials-file"));
if (isRpcHttpAuthenticationEnabled && rpcHttpAuthenticationCredentialsFile() == null) {
throw new ParameterException(
commandLine,
"Unable to authenticate RPC HTTP endpoint without a supplied credentials file");
}

final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault();
jsonRpcConfiguration.setEnabled(isRpcHttpEnabled);
Expand Down Expand Up @@ -676,12 +674,11 @@ private WebSocketConfiguration webSocketConfiguration() {
"--rpc-ws-authentication-enabled",
"--rpc-ws-authentication-credentials-file"));

CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-ws-authentication-enabled",
!isRpcWsAuthenticationEnabled,
Collections.singletonList("--rpc-ws-authentication-credentials-file"));
if (isRpcWsAuthenticationEnabled && rpcWsAuthenticationCredentialsFile() == null) {
throw new ParameterException(
commandLine,
"Unable to authenticate RPC WS endpoint without a supplied credentials file");
}

final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
webSocketConfiguration.setEnabled(isRpcWsEnabled);
Expand Down Expand Up @@ -972,47 +969,37 @@ private File privacyPublicKeyFile() {
}

private String rpcHttpAuthenticationCredentialsFile() throws Exception {
String filename = null;
if (isFullInstantiation()) {
return standaloneCommands.rpcHttpAuthenticationCredentialsFile;
filename = standaloneCommands.rpcHttpAuthenticationCredentialsFile;
} else if (isDocker) {
final File authFile = new File(DOCKER_RPC_HTTP_AUTHENTICATION_CREDENTIALS_FILE_LOCATION);
if (authFile.exists()) {
final String path = authFile.getAbsolutePath();
try {
new RpcAuthConverter().convert(path);
} catch (Exception e) {
throw new ParameterException(
commandLine, "Invalid RPC HTTP authentication credentials file: " + e.getMessage());
}
return path;
} else {
return null;
filename = authFile.getAbsolutePath();
}
} else {
return null;
}

if (filename != null) {
RpcAuthFileValidator.validate(commandLine, filename, "HTTP");
}
return filename;
}

private String rpcWsAuthenticationCredentialsFile() {
String filename = null;
if (isFullInstantiation()) {
return standaloneCommands.rpcWsAuthenticationCredentialsFile;
filename = standaloneCommands.rpcWsAuthenticationCredentialsFile;
} else if (isDocker) {
final File authFile = new File(DOCKER_RPC_WS_AUTHENTICATION_CREDENTIALS_FILE_LOCATION);
if (authFile.exists()) {
final String path = authFile.getAbsolutePath();
try {
new RpcAuthConverter().convert(path);
} catch (Exception e) {
throw new ParameterException(
commandLine, "Invalid RPC WS authentication credentials file: " + e.getMessage());
}
return path;
} else {
return null;
filename = authFile.getAbsolutePath();
}
} else {
return null;
}

if (filename != null) {
RpcAuthFileValidator.validate(commandLine, filename, "WS");
}
return filename;
}

private boolean isFullInstantiation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

import static tech.pegasys.pantheon.cli.DefaultCommandValues.getDefaultPantheonDataPath;

import tech.pegasys.pantheon.cli.custom.RpcAuthConverter;

import java.io.File;
import java.nio.file.Path;

Expand Down Expand Up @@ -59,17 +57,15 @@ class StandaloneCommand implements DefaultCommandValues {
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description =
"Storage file for rpc http authentication credentials (default: ${DEFAULT-VALUE})",
arity = "1",
converter = RpcAuthConverter.class)
arity = "1")
String rpcHttpAuthenticationCredentialsFile = null;

@CommandLine.Option(
names = {"--rpc-ws-authentication-credentials-file"},
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description =
"Storage file for rpc websocket authentication credentials (default: ${DEFAULT-VALUE})",
arity = "1",
converter = RpcAuthConverter.class)
arity = "1")
String rpcWsAuthenticationCredentialsFile = null;

@CommandLine.Option(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,65 @@

import tech.pegasys.pantheon.ethereum.permissioning.TomlConfigFileParser;

import java.io.File;
import java.io.IOException;
import java.util.stream.Collectors;

import com.google.common.base.Strings;
import net.consensys.cava.toml.TomlParseResult;
import picocli.CommandLine;
import picocli.CommandLine.ParameterException;

public class RpcAuthConverter implements CommandLine.ITypeConverter<String> {
public class RpcAuthFileValidator {

@Override
public String convert(final String value) throws Exception {
TomlParseResult tomlParseResult;
public static String validate(
final CommandLine commandLine, final String filename, final String type) {

final File authfile = new File(filename);
if (!authfile.exists()) {
throw new ParameterException(
commandLine,
"The specified RPC "
+ type
+ " authentication credential file '"
+ filename
+ "' does not exist");
}

final TomlParseResult tomlParseResult;
try {
tomlParseResult = TomlConfigFileParser.loadConfigurationFromFile(value);
tomlParseResult = TomlConfigFileParser.loadConfigurationFromFile(filename);
} catch (IOException e) {
throw new IllegalArgumentException(
"An error occurred while opening the specified RPC authentication configuration file.");
throw new ParameterException(
commandLine,
"An error occurred while opening the specified RPC "
+ type
+ " authentication configuration file.");
} catch (Exception e) {
throw new ParameterException(
commandLine,
"Invalid RPC " + type + " authentication credentials file: " + e.getMessage());
}

if (tomlParseResult.hasErrors()) {
throw new IllegalArgumentException(
throw new ParameterException(
commandLine,
"An error occurred while parsing the specified RPC authentication configuration file.");
}

if (!verifyAllUsersHavePassword(tomlParseResult)) {
throw new IllegalArgumentException("RPC user specified without password.");
throw new ParameterException(commandLine, "RPC user specified without password.");
}

if (!verifyAllEntriesHaveValues(tomlParseResult)) {
throw new IllegalArgumentException(
"RPC authentication configuration file contains invalid values.");
throw new ParameterException(
commandLine, "RPC authentication configuration file contains invalid values.");
}

return value;
return filename;
}

private boolean verifyAllUsersHavePassword(final TomlParseResult tomlParseResult) {
private static boolean verifyAllUsersHavePassword(final TomlParseResult tomlParseResult) {
int configuredUsers = tomlParseResult.getTable("Users").keySet().size();

int usersWithPasswords =
Expand All @@ -68,15 +90,15 @@ private boolean verifyAllUsersHavePassword(final TomlParseResult tomlParseResult
return configuredUsers == usersWithPasswords;
}

private boolean verifyAllEntriesHaveValues(final TomlParseResult tomlParseResult) {
private static 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) {
private static boolean verifyArray(final String key, final TomlParseResult tomlParseResult) {
return tomlParseResult.isArray(key) && !tomlParseResult.getArrayOrEmpty(key).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,76 @@
import com.google.common.io.Resources;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import picocli.CommandLine;
import picocli.CommandLine.ParameterException;

public class RpcAuthConverterTest {
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RpcAuthFileValidatorTest {

private RpcAuthConverter rpcAuthConverter;
private RpcAuthFileValidator rpcAuthFileValidator;
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";
@Mock CommandLine commandLine;

@Before
public void setUp() {
rpcAuthConverter = new RpcAuthConverter();
rpcAuthFileValidator = new RpcAuthFileValidator();
}

@Test
public void shouldPassWhenCorrectTOML() {
assertThatCode(() -> rpcAuthConverter.convert(getFilePath(CORRECT_TOML)))
assertThatCode(
() -> RpcAuthFileValidator.validate(commandLine, getFilePath(CORRECT_TOML), "HTTP"))
.doesNotThrowAnyException();
}

@Test
public void shouldFailWhenInvalidTOML() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(INVALID_TOML)))
.isInstanceOf(Exception.class)
assertThatThrownBy(
() -> RpcAuthFileValidator.validate(commandLine, getFilePath(INVALID_TOML), "HTTP"))
.isInstanceOf(ParameterException.class)
.hasMessageContaining("Invalid TOML configuration");
}

@Test
public void shouldFailWhenMissingTOML() {
assertThatThrownBy(
() -> RpcAuthFileValidator.validate(commandLine, "thisshouldntexist", "HTTP"))
.isInstanceOf(ParameterException.class)
.hasMessage(
"The specified RPC HTTP authentication credential file 'thisshouldntexist' does not exist");
}

@Test
public void shouldFailWhenMissingPassword() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(NO_PASSWORD_TOML)))
.isInstanceOf(Exception.class)
assertThatThrownBy(
() -> RpcAuthFileValidator.validate(commandLine, getFilePath(NO_PASSWORD_TOML), "HTTP"))
.isInstanceOf(ParameterException.class)
.hasMessage("RPC user specified without password.");
}

@Test
public void shouldFailWhenInvalidKeyValue() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(INVALID_VALUE_TOML)))
.isInstanceOf(Exception.class)
assertThatThrownBy(
() ->
RpcAuthFileValidator.validate(commandLine, getFilePath(INVALID_VALUE_TOML), "HTTP"))
.isInstanceOf(ParameterException.class)
.hasMessage("RPC authentication configuration file contains invalid values.");
}

@Test
public void shouldFailWhenDuplicateUser() {
assertThatThrownBy(() -> rpcAuthConverter.convert(getFilePath(DUPLICATE_USER_TOML)))
.isInstanceOf(Exception.class)
assertThatThrownBy(
() ->
RpcAuthFileValidator.validate(
commandLine, getFilePath(DUPLICATE_USER_TOML), "HTTP"))
.isInstanceOf(ParameterException.class)
.hasMessageContaining("Invalid TOML configuration");
}

Expand Down