mirror of https://github.com/hyperledger/besu
[NC-2218] RPC auth validation + tests. (#846)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
68ad11e9fe
commit
3c7ed68be6
@ -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…
Reference in new issue