Allow optional API methods with no authentication (#3382)

* some unit tests

Signed-off-by: Frank Li <b439988l@gmail.com>

* CLI parsing tests

Signed-off-by: Frank Li <b439988l@gmail.com>

* regression tests

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix failing at

Signed-off-by: Frank Li <b439988l@gmail.com>

* refactor -> api to api methods

Signed-off-by: Frank Li <b439988l@gmail.com>

* functionality in

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix override test

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix duplicates test

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix duplicates test

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix failing test and remove unnecessary code

Signed-off-by: Frank Li <b439988l@gmail.com>

* add entry to changelog

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix typo and NPE

Signed-off-by: Frank Li <b439988l@gmail.com>

* action on some items

Signed-off-by: Frank Li <b439988l@gmail.com>

* some refactoring + more tests + implement auth skip for web sockets

Signed-off-by: Frank Li <b439988l@gmail.com>

* refactor unused method

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix test failing

Signed-off-by: Frank Li <b439988l@gmail.com>

* fix wrong variable used

Signed-off-by: Frank Li <b439988l@gmail.com>
pull/3391/head
Frank Li 3 years ago committed by GitHub
parent 63c5f4ad7f
commit 0377404fe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 8
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java
  3. 26
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java
  4. 26
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java
  5. 66
      acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/JsonRpcHttpAuthenticationAcceptanceTest.java
  6. 67
      acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/jsonrpc/JsonRpcWebsocketAuthenticationAcceptanceTest.java
  7. 45
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  8. 43
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  9. 4
      besu/src/test/resources/everything_config.toml
  10. 9
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfiguration.java
  11. 54
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java
  12. 11
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
  13. 9
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfiguration.java
  14. 39
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketRequestHandler.java
  15. 12
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketService.java
  16. 15
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcConfigurationTest.java
  17. 90
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
  18. 31
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketConfigurationTest.java
  19. 238
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/WebSocketServiceLoginTest.java

@ -1,4 +1,10 @@
# Changelog
## 22.1.1
### 22.1.1 Breaking Changes
### Additions and Improvements
- Allow optional RPC methods that bypass authentication [#3382](https://github.com/hyperledger/besu/pull/3382)
## 22.1.1

@ -168,6 +168,10 @@ public class ProcessBesuNodeRunner implements BesuNodeRunner {
params.add(node.jsonRpcListenPort().map(Object::toString).get());
params.add("--rpc-http-api");
params.add(apiList(node.jsonRpcConfiguration().getRpcApis()));
if (!node.jsonRpcConfiguration().getNoAuthRpcApis().isEmpty()) {
params.add("--rpc-http-api-methods-no-auth");
params.add(apiList(node.jsonRpcConfiguration().getNoAuthRpcApis()));
}
if (node.jsonRpcConfiguration().isAuthenticationEnabled()) {
params.add("--rpc-http-authentication-enabled");
}
@ -193,6 +197,10 @@ public class ProcessBesuNodeRunner implements BesuNodeRunner {
params.add(node.wsRpcListenPort().map(Object::toString).get());
params.add("--rpc-ws-api");
params.add(apiList(node.webSocketConfiguration().getRpcApis()));
if (!node.webSocketConfiguration().getRpcApisNoAuth().isEmpty()) {
params.add("--rpc-ws-api-methods-no-auth");
params.add(apiList(node.webSocketConfiguration().getRpcApisNoAuth()));
}
if (node.webSocketConfiguration().isAuthenticationEnabled()) {
params.add("--rpc-ws-authentication-enabled");
}

@ -169,6 +169,18 @@ public class BesuNodeConfigurationBuilder {
return this;
}
public BesuNodeConfigurationBuilder jsonRpcAuthenticationConfiguration(
final String authFile, final List<String> noAuthApiMethods) throws URISyntaxException {
final String authTomlPath =
Paths.get(ClassLoader.getSystemResource(authFile).toURI()).toAbsolutePath().toString();
this.jsonRpcConfiguration.setAuthenticationEnabled(true);
this.jsonRpcConfiguration.setAuthenticationCredentialsFile(authTomlPath);
this.jsonRpcConfiguration.setNoAuthRpcApis(noAuthApiMethods);
return this;
}
public BesuNodeConfigurationBuilder jsonRpcAuthenticationUsingRSA() throws URISyntaxException {
final File jwtPublicKey =
Paths.get(ClassLoader.getSystemResource("authentication/jwt_public_key_rsa").toURI())
@ -238,6 +250,20 @@ public class BesuNodeConfigurationBuilder {
return this;
}
public BesuNodeConfigurationBuilder webSocketAuthenticationEnabledWithNoAuthMethods(
final List<String> noAuthApiMethods) throws URISyntaxException {
final String authTomlPath =
Paths.get(ClassLoader.getSystemResource("authentication/auth.toml").toURI())
.toAbsolutePath()
.toString();
this.webSocketConfiguration.setAuthenticationEnabled(true);
this.webSocketConfiguration.setAuthenticationCredentialsFile(authTomlPath);
this.webSocketConfiguration.setRpcApisNoAuth(noAuthApiMethods);
return this;
}
public BesuNodeConfigurationBuilder webSocketAuthenticationUsingRsaPublicKeyEnabled()
throws URISyntaxException {
final File jwtPublicKey =

@ -196,6 +196,32 @@ public class BesuNodeFactory {
.build());
}
public BesuNode createNodeWithAuthFileAndNoAuthApi(
final String name, final String authFile, final List<String> noAuthApiMethods)
throws URISyntaxException, IOException {
return create(
new BesuNodeConfigurationBuilder()
.name(name)
.jsonRpcEnabled()
.jsonRpcAuthenticationConfiguration(authFile, noAuthApiMethods)
.webSocketEnabled()
.webSocketAuthenticationEnabled()
.build());
}
public BesuNode createWsNodeWithAuthFileAndNoAuthApi(
final String name, final String authFile, final List<String> noAuthApiMethods)
throws URISyntaxException, IOException {
return create(
new BesuNodeConfigurationBuilder()
.name(name)
.jsonRpcEnabled()
.jsonRpcAuthenticationConfiguration(authFile)
.webSocketEnabled()
.webSocketAuthenticationEnabledWithNoAuthMethods(noAuthApiMethods)
.build());
}
public BesuNode createNodeWithAuthenticationUsingRsaJwtPublicKey(final String name)
throws IOException, URISyntaxException {
return create(

@ -22,15 +22,18 @@ import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfigurati
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
public class HttpServiceLoginAcceptanceTest extends AcceptanceTestBase {
public class JsonRpcHttpAuthenticationAcceptanceTest extends AcceptanceTestBase {
private Cluster authenticatedCluster;
private BesuNode nodeUsingAuthFile;
private BesuNode nodeUsingRsaJwtPublicKey;
private BesuNode nodeUsingEcdsaJwtPublicKey;
private BesuNode nodeUsingAuthFileWithNoAuthApi;
private static final String AUTH_FILE = "authentication/auth.toml";
// token with payload{"iat": 1516239022,"exp": 4729363200,"permissions": ["net:peerCount"]}
@ -47,6 +50,8 @@ public class HttpServiceLoginAcceptanceTest extends AcceptanceTestBase {
+ "c2lvbnMiOlsibmV0OnBlZXJDb3VudCJdfQ.pWXniN6XQ7G8b1nawy8sviPCMxrfbcI6c7UFzeXm26CMGMUEZxiC"
+ "JjRntB8ueuZcsxnGlEhCHt-KngpFEmx5TA";
private static final List<String> NO_AUTH_API_METHODS = Arrays.asList("net_services");
@Before
public void setUp() throws IOException, URISyntaxException {
@ -57,48 +62,95 @@ public class HttpServiceLoginAcceptanceTest extends AcceptanceTestBase {
nodeUsingAuthFile = besu.createNodeWithAuthentication("node1", AUTH_FILE);
nodeUsingRsaJwtPublicKey = besu.createNodeWithAuthenticationUsingRsaJwtPublicKey("node2");
nodeUsingEcdsaJwtPublicKey = besu.createNodeWithAuthenticationUsingEcdsaJwtPublicKey("node3");
nodeUsingAuthFileWithNoAuthApi =
besu.createNodeWithAuthFileAndNoAuthApi("node4", AUTH_FILE, NO_AUTH_API_METHODS);
authenticatedCluster.start(
nodeUsingAuthFile, nodeUsingRsaJwtPublicKey, nodeUsingEcdsaJwtPublicKey);
nodeUsingAuthFile,
nodeUsingRsaJwtPublicKey,
nodeUsingEcdsaJwtPublicKey,
nodeUsingAuthFileWithNoAuthApi);
nodeUsingAuthFile.verify(login.awaitResponse("user", "badpassword"));
nodeUsingRsaJwtPublicKey.verify(login.awaitResponse("user", "badpassword"));
nodeUsingEcdsaJwtPublicKey.verify(login.awaitResponse("user", "badpassword"));
nodeUsingAuthFileWithNoAuthApi.verify(login.awaitResponse("user", "badpassword"));
}
@Test
public void shouldFailLoginWithWrongCredentials() {
nodeUsingAuthFile.verify(login.failure("user", "badpassword"));
nodeUsingAuthFileWithNoAuthApi.verify(login.failure("user", "badpassword"));
}
@Test
public void shouldSucceedLoginWithCorrectCredentials() {
nodeUsingAuthFile.verify(login.success("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.verify(login.success("user", "pegasys"));
}
@Test
public void jsonRpcMethodShouldSucceedWithAuthenticatedUserAndPermission() {
final String token =
String token =
nodeUsingAuthFile.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFile.verify(net.awaitPeerCount(2));
nodeUsingAuthFile.verify(net.awaitPeerCount(3));
token =
nodeUsingAuthFileWithNoAuthApi.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFileWithNoAuthApi.verify(net.awaitPeerCount(3));
}
@Test
public void jsonRpcMethodShouldFailOnNonPermittedMethod() {
final String token =
String token =
nodeUsingAuthFile.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFile.verify(net.netVersionUnauthorized());
nodeUsingAuthFile.verify(net.netServicesUnauthorized());
token =
nodeUsingAuthFileWithNoAuthApi.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
}
@Test
public void jsonRpcMethodsNotIncludedInNoAuthListShouldFailWithoutToken() {
nodeUsingAuthFile.verify(net.netVersionUnauthorized());
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
}
@Test
public void noAuthJsonRpcMethodShouldSucceedWithoutToken() {
nodeUsingAuthFileWithNoAuthApi.verify(net.netServicesAllActive());
}
@Test
public void noAuthJsonRpcConfiguredNodeShouldWorkAsIntended() {
// No token -> all methods other than specified no auth methods should fail
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
nodeUsingAuthFileWithNoAuthApi.verify(net.netServicesAllActive());
// Should behave the same with valid token
String token =
nodeUsingAuthFileWithNoAuthApi.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
nodeUsingAuthFileWithNoAuthApi.verify(net.netServicesAllActive());
nodeUsingAuthFileWithNoAuthApi.verify(net.awaitPeerCount(3));
}
@Test
public void externalRsaJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() {
nodeUsingRsaJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(
RSA_TOKEN_ALLOWING_NET_PEER_COUNT);
nodeUsingRsaJwtPublicKey.verify(net.awaitPeerCount(2));
nodeUsingRsaJwtPublicKey.verify(net.awaitPeerCount(3));
}
@Test
@ -113,7 +165,7 @@ public class HttpServiceLoginAcceptanceTest extends AcceptanceTestBase {
public void externalEcdsaJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() {
nodeUsingEcdsaJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(
ECDSA_TOKEN_ALLOWING_NET_PEER_COUNT);
nodeUsingEcdsaJwtPublicKey.verify(net.awaitPeerCount(2));
nodeUsingEcdsaJwtPublicKey.verify(net.awaitPeerCount(3));
}
@Test

@ -22,17 +22,22 @@ import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfigurati
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
public class WebsocketServiceLoginAcceptanceTest extends AcceptanceTestBase {
public class JsonRpcWebsocketAuthenticationAcceptanceTest extends AcceptanceTestBase {
private BesuNode nodeUsingAuthFile;
private BesuNode nodeUsingRsaJwtPublicKey;
private BesuNode nodeUsingEcdsaJwtPublicKey;
private BesuNode nodeUsingAuthFileWithNoAuthApi;
private Cluster authenticatedCluster;
private static final String AUTH_FILE = "authentication/auth.toml";
private static final List<String> NO_AUTH_API_METHODS = Arrays.asList("net_services");
// token with payload{"iat": 1516239022,"exp": 4729363200,"permissions": ["net:peerCount"]}
private static final String RSA_TOKEN_ALLOWING_NET_PEER_COUNT =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsImV4cCI6NDcyOTM2MzIwMCwicGVybWl"
@ -56,51 +61,99 @@ public class WebsocketServiceLoginAcceptanceTest extends AcceptanceTestBase {
nodeUsingAuthFile = besu.createNodeWithAuthentication("node1", AUTH_FILE);
nodeUsingRsaJwtPublicKey = besu.createNodeWithAuthenticationUsingRsaJwtPublicKey("node2");
nodeUsingEcdsaJwtPublicKey = besu.createNodeWithAuthenticationUsingEcdsaJwtPublicKey("node3");
nodeUsingAuthFileWithNoAuthApi =
besu.createWsNodeWithAuthFileAndNoAuthApi("node4", AUTH_FILE, NO_AUTH_API_METHODS);
authenticatedCluster.start(
nodeUsingAuthFile, nodeUsingRsaJwtPublicKey, nodeUsingEcdsaJwtPublicKey);
nodeUsingAuthFile,
nodeUsingRsaJwtPublicKey,
nodeUsingEcdsaJwtPublicKey,
nodeUsingAuthFileWithNoAuthApi);
nodeUsingAuthFile.useWebSocketsForJsonRpc();
nodeUsingRsaJwtPublicKey.useWebSocketsForJsonRpc();
nodeUsingEcdsaJwtPublicKey.useWebSocketsForJsonRpc();
nodeUsingAuthFileWithNoAuthApi.useWebSocketsForJsonRpc();
nodeUsingAuthFile.verify(login.awaitResponse("user", "badpassword"));
nodeUsingRsaJwtPublicKey.verify(login.awaitResponse("user", "badpassword"));
nodeUsingEcdsaJwtPublicKey.verify(login.awaitResponse("user", "badpassword"));
nodeUsingAuthFileWithNoAuthApi.verify(login.awaitResponse("user", "badpassword"));
}
@Test
public void shouldFailLoginWithWrongCredentials() {
nodeUsingAuthFile.verify(login.failure("user", "badpassword"));
nodeUsingAuthFileWithNoAuthApi.verify(login.failure("user", "badpassword"));
}
@Test
public void shouldSucceedLoginWithCorrectCredentials() {
nodeUsingAuthFile.verify(login.success("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.verify(login.success("user", "pegasys"));
}
@Test
public void jsonRpcMethodShouldSucceedWithAuthenticatedUserAndPermission() {
final String token =
String token =
nodeUsingAuthFile.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFile.verify(net.awaitPeerCount(2));
nodeUsingAuthFile.verify(net.awaitPeerCount(3));
token =
nodeUsingAuthFileWithNoAuthApi.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFileWithNoAuthApi.verify(net.awaitPeerCount(3));
}
@Test
public void jsonRpcMethodShouldFailOnNonPermittedMethod() {
final String token =
String token =
nodeUsingAuthFile.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFile.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFile.verify(net.netVersionUnauthorized());
nodeUsingAuthFile.verify(net.netServicesUnauthorized());
token =
nodeUsingAuthFileWithNoAuthApi.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
}
@Test
public void jsonRpcMethodsNotIncludedInNoAuthListShouldFailWithoutToken() {
nodeUsingAuthFile.verify(net.netVersionUnauthorized());
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
}
@Test
public void noAuthJsonRpcMethodShouldSucceedWithoutToken() {
nodeUsingAuthFileWithNoAuthApi.verify(net.netServicesAllActive());
}
@Test
public void noAuthJsonRpcConfiguredNodeShouldWorkAsIntended() {
// No token -> all methods other than specified no auth methods should fail
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
nodeUsingAuthFileWithNoAuthApi.verify(net.netServicesAllActive());
// Should behave the same with valid token
String token =
nodeUsingAuthFileWithNoAuthApi.execute(
permissioningTransactions.createSuccessfulLogin("user", "pegasys"));
nodeUsingAuthFileWithNoAuthApi.useAuthenticationTokenInHeaderForJsonRpc(token);
nodeUsingAuthFileWithNoAuthApi.verify(net.netVersionUnauthorized());
nodeUsingAuthFileWithNoAuthApi.verify(net.netServicesAllActive());
nodeUsingAuthFileWithNoAuthApi.verify(net.awaitPeerCount(3));
}
@Test
public void externalRsaJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() {
nodeUsingRsaJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(
RSA_TOKEN_ALLOWING_NET_PEER_COUNT);
nodeUsingRsaJwtPublicKey.verify(net.awaitPeerCount(2));
nodeUsingRsaJwtPublicKey.verify(net.awaitPeerCount(3));
}
@Test
@ -115,7 +168,7 @@ public class WebsocketServiceLoginAcceptanceTest extends AcceptanceTestBase {
public void externalEcdsaJwtPublicKeyUsedOnJsonRpcMethodShouldSucceed() {
nodeUsingEcdsaJwtPublicKey.useAuthenticationTokenInHeaderForJsonRpc(
ECDSA_TOKEN_ALLOWING_NET_PEER_COUNT);
nodeUsingEcdsaJwtPublicKey.verify(net.awaitPeerCount(2));
nodeUsingEcdsaJwtPublicKey.verify(net.awaitPeerCount(3));
}
@Test

@ -105,6 +105,7 @@ import org.hyperledger.besu.ethereum.api.ImmutableApiConfiguration;
import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider;
@ -569,6 +570,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
"Comma separated list of APIs to enable on JSON-RPC HTTP service (default: ${DEFAULT-VALUE})")
private final List<String> rpcHttpApis = DEFAULT_RPC_APIS;
@Option(
names = {"--rpc-http-api-method-no-auth", "--rpc-http-api-methods-no-auth"},
paramLabel = "<api name>",
split = " {0,1}, {0,1}",
arity = "1..*",
description =
"Comma separated list of API methods to exclude from RPC authentication services, RPC HTTP authentication must be enabled")
private final List<String> rpcHttpApiMethodsNoAuth = new ArrayList<String>();
@Option(
names = {"--rpc-http-authentication-enabled"},
description =
@ -702,6 +712,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
"Comma separated list of APIs to enable on JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
private final List<String> rpcWsApis = DEFAULT_RPC_APIS;
@Option(
names = {"--rpc-ws-api-methods-no-auth", "--rpc-ws-api-method-no-auth"},
paramLabel = "<api name>",
split = " {0,1}, {0,1}",
arity = "1..*",
description =
"Comma separated list of RPC methods to exclude from RPC authentication services, RPC WebSocket authentication must be enabled")
private final List<String> rpcWsApiMethodsNoAuth = new ArrayList<String>();
@Option(
names = {"--rpc-ws-authentication-enabled"},
description =
@ -1647,6 +1666,24 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
if (!rpcWsApis.stream().allMatch(configuredApis)) {
throw new ParameterException(this.commandLine, "Invalid value for option '--rpc-ws-apis'");
}
final boolean validHttpApiMethods =
rpcHttpApiMethodsNoAuth.stream().allMatch(RpcMethod::rpcMethodExists);
if (!validHttpApiMethods) {
throw new ParameterException(
this.commandLine,
"Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods");
}
final boolean validWsApiMethods =
rpcWsApiMethodsNoAuth.stream().allMatch(RpcMethod::rpcMethodExists);
if (!validWsApiMethods) {
throw new ParameterException(
this.commandLine,
"Invalid value for option '--rpc-ws-api-methods-no-auth', options must be valid RPC methods");
}
}
private GenesisConfigOptions readGenesisConfigOptions() {
@ -1923,6 +1960,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
jsonRpcConfiguration.setMaxActiveConnections(rpcHttpMaxConnections);
jsonRpcConfiguration.setCorsAllowedDomains(rpcHttpCorsAllowedOrigins);
jsonRpcConfiguration.setRpcApis(rpcHttpApis.stream().distinct().collect(Collectors.toList()));
jsonRpcConfiguration.setNoAuthRpcApis(
rpcHttpApiMethodsNoAuth.stream().distinct().collect(Collectors.toList()));
jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist);
jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled);
jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile());
@ -1942,6 +1981,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
asList(
"--rpc-http-api",
"--rpc-http-apis",
"--rpc-http-api-method-no-auth",
"--rpc-http-api-methods-no-auth",
"--rpc-http-cors-origins",
"--rpc-http-host",
"--rpc-http-port",
@ -2077,6 +2118,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
asList(
"--rpc-ws-api",
"--rpc-ws-apis",
"--rpc-ws-api-method-no-auth",
"--rpc-ws-api-methods-no-auth",
"--rpc-ws-host",
"--rpc-ws-port",
"--rpc-ws-max-frame-size",
@ -2110,6 +2153,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
webSocketConfiguration.setMaxFrameSize(rpcWsMaxFrameSize);
webSocketConfiguration.setMaxActiveConnections(rpcWsMaxConnections);
webSocketConfiguration.setRpcApis(rpcWsApis);
webSocketConfiguration.setRpcApisNoAuth(
rpcWsApiMethodsNoAuth.stream().distinct().collect(Collectors.toList()));
webSocketConfiguration.setAuthenticationEnabled(isRpcWsAuthenticationEnabled);
webSocketConfiguration.setAuthenticationCredentialsFile(rpcWsAuthenticationCredentialsFile());
webSocketConfiguration.setHostsAllowlist(hostsAllowlist);

@ -59,6 +59,7 @@ import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration;
import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration;
@ -1941,6 +1942,48 @@ public class BesuCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcApiNoAuthMethodsIgnoresDuplicatesAndMustBeUsed() {
parseCommand(
"--rpc-http-api-methods-no-auth",
"admin_peers, admin_peers, eth_getWork",
"--rpc-http-enabled");
verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(jsonRpcConfigArgumentCaptor.getValue().getNoAuthRpcApis())
.containsExactlyInAnyOrder(
RpcMethod.ADMIN_PEERS.getMethodName(), RpcMethod.ETH_GET_WORK.getMethodName());
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcHttpNoAuthApiMethodsCannotBeInvalid() {
parseCommand("--rpc-http-enabled", "--rpc-http-api-method-no-auth", "invalid");
Mockito.verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods");
}
@Test
public void rpcWsNoAuthApiMethodsCannotBeInvalid() {
parseCommand("--rpc-ws-enabled", "--rpc-ws-api-methods-no-auth", "invalid");
Mockito.verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"Invalid value for option '--rpc-ws-api-methods-no-auth', options must be valid RPC methods");
}
@Test
public void rpcHttpOptionsRequiresServiceToBeEnabled() {
parseCommand(

@ -60,6 +60,10 @@ rpc-http-port=5678
rpc-http-max-active-connections=100
rpc-http-api=["DEBUG","ETH"]
rpc-http-apis=["DEBUG","ETH"]
rpc-http-api-method-no-auth=["admin_peers"]
rpc-http-api-methods-no-auth=["admin_peers"]
rpc-ws-api-method-no-auth=["admin_peers"]
rpc-ws-api-methods-no-auth=["admin_peers"]
rpc-http-cors-origins=["none"]
rpc-http-authentication-enabled=false
rpc-http-authentication-credentials-file="none"

@ -41,6 +41,7 @@ public class JsonRpcConfiguration {
private String host;
private List<String> corsAllowedDomains = Collections.emptyList();
private List<String> rpcApis;
private List<String> noAuthRpcApis = Collections.emptyList();
private List<String> hostsAllowlist = Arrays.asList("localhost", "127.0.0.1");
private boolean authenticationEnabled = false;
private String authenticationCredentialsFile;
@ -105,6 +106,14 @@ public class JsonRpcConfiguration {
this.rpcApis = rpcApis;
}
public Collection<String> getNoAuthRpcApis() {
return this.noAuthRpcApis;
}
public void setNoAuthRpcApis(final List<String> rpcApis) {
this.noAuthRpcApis = rpcApis;
}
public void addRpcApi(final String rpcApi) {
this.rpcApis = new ArrayList<>(rpcApis);
rpcApis.add(rpcApi);

@ -564,35 +564,36 @@ public class JsonRpcHttpService {
private void handleJsonRPCRequest(final RoutingContext routingContext) {
// first check token if authentication is required
final String token = getAuthToken(routingContext);
if (authenticationService.isPresent() && token == null) {
// we check the no auth api methods actually match what's in the request later on
if (authenticationService.isPresent() && token == null && config.getNoAuthRpcApis().isEmpty()) {
// no auth token when auth required
handleJsonRpcUnauthorizedError(routingContext, null, JsonRpcError.UNAUTHORIZED);
} else {
// Parse json
try {
final String json = routingContext.getBodyAsString().trim();
if (!json.isEmpty() && json.charAt(0) == '{') {
final JsonObject requestBodyJsonObject =
ContextKey.REQUEST_BODY_AS_JSON_OBJECT.extractFrom(
routingContext, () -> new JsonObject(json));
AuthenticationUtils.getUser(
authenticationService,
token,
user -> handleJsonSingleRequest(routingContext, requestBodyJsonObject, user));
} else {
final JsonArray array = new JsonArray(json);
if (array.size() < 1) {
handleJsonRpcError(routingContext, null, INVALID_REQUEST);
return;
}
AuthenticationUtils.getUser(
authenticationService,
token,
user -> handleJsonBatchRequest(routingContext, array, user));
return;
}
// Parse json
try {
final String json = routingContext.getBodyAsString().trim();
if (!json.isEmpty() && json.charAt(0) == '{') {
final JsonObject requestBodyJsonObject =
ContextKey.REQUEST_BODY_AS_JSON_OBJECT.extractFrom(
routingContext, () -> new JsonObject(json));
AuthenticationUtils.getUser(
authenticationService,
token,
user -> handleJsonSingleRequest(routingContext, requestBodyJsonObject, user));
} else {
final JsonArray array = new JsonArray(json);
if (array.size() < 1) {
handleJsonRpcError(routingContext, null, INVALID_REQUEST);
return;
}
} catch (final DecodeException | NullPointerException ex) {
handleJsonRpcError(routingContext, null, JsonRpcError.PARSE_ERROR);
AuthenticationUtils.getUser(
authenticationService,
token,
user -> handleJsonBatchRequest(routingContext, array, user));
}
} catch (final DecodeException | NullPointerException ex) {
handleJsonRpcError(routingContext, null, JsonRpcError.PARSE_ERROR);
}
}
@ -744,7 +745,8 @@ public class JsonRpcHttpService {
final JsonRpcMethod method = rpcMethods.get(requestBody.getMethod());
if (AuthenticationUtils.isPermitted(authenticationService, user, method)) {
if (AuthenticationUtils.isPermitted(
authenticationService, user, method, config.getNoAuthRpcApis())) {
// Generate response
try (final OperationTimer.TimingContext ignored =
requestTimer.labels(requestBody.getMethod()).startTimer()) {

@ -16,10 +16,10 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.authentication;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.annotations.VisibleForTesting;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User;
@ -29,11 +29,11 @@ import org.slf4j.LoggerFactory;
public class AuthenticationUtils {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationUtils.class);
@VisibleForTesting
public static boolean isPermitted(
final Optional<AuthenticationService> authenticationService,
final Optional<User> optionalUser,
final JsonRpcMethod jsonRpcMethod) {
final JsonRpcMethod jsonRpcMethod,
final Collection<String> noAuthMethods) {
AtomicBoolean foundMatchingPermission = new AtomicBoolean();
@ -42,6 +42,11 @@ public class AuthenticationUtils {
return true;
}
// if the method is configured as a no auth method we skip permission check
if (noAuthMethods.stream().anyMatch(m -> m.equals(jsonRpcMethod.getName()))) {
return true;
}
if (optionalUser.isPresent()) {
User user = optionalUser.get();
for (String perm : jsonRpcMethod.getPermissions()) {

@ -38,6 +38,7 @@ public class WebSocketConfiguration {
private int port;
private String host;
private List<String> rpcApis;
private List<String> rpcApisNoAuth = Collections.emptyList();
private boolean authenticationEnabled = false;
private String authenticationCredentialsFile;
private List<String> hostsAllowlist = Arrays.asList("localhost", "127.0.0.1");
@ -93,6 +94,14 @@ public class WebSocketConfiguration {
this.rpcApis = rpcApis;
}
public Collection<String> getRpcApisNoAuth() {
return rpcApisNoAuth;
}
public void setRpcApisNoAuth(final List<String> rpcApis) {
this.rpcApisNoAuth = rpcApis;
}
public boolean isAuthenticationEnabled() {
return authenticationEnabled;
}

@ -33,6 +33,8 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods.WebSocketRpcR
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -82,17 +84,19 @@ public class WebSocketRequestHandler {
this.timeoutSec = timeoutSec;
}
// Only for testing
public void handle(final ServerWebSocket websocket, final String payload) {
handle(Optional.empty(), websocket, payload, Optional.empty());
handle(Optional.empty(), websocket, payload, Optional.empty(), Collections.emptyList());
}
public void handle(
final Optional<AuthenticationService> authenticationService,
final ServerWebSocket websocket,
final String payload,
final Optional<User> user) {
final Optional<User> user,
final Collection<String> noAuthApiMethods) {
vertx.executeBlocking(
executeHandler(authenticationService, websocket, payload, user),
executeHandler(authenticationService, websocket, payload, user, noAuthApiMethods),
false,
resultHandler(websocket));
}
@ -101,12 +105,19 @@ public class WebSocketRequestHandler {
final Optional<AuthenticationService> authenticationService,
final ServerWebSocket websocket,
final String payload,
final Optional<User> user) {
final Optional<User> user,
final Collection<String> noAuthApiMethods) {
return future -> {
final String json = payload.trim();
if (!json.isEmpty() && json.charAt(0) == '{') {
try {
handleSingleRequest(authenticationService, websocket, user, future, getRequest(payload));
handleSingleRequest(
authenticationService,
websocket,
user,
future,
getRequest(payload),
noAuthApiMethods);
} catch (final IllegalArgumentException | DecodeException e) {
LOG.debug("Error mapping json to WebSocketRpcRequest", e);
future.complete(new JsonRpcErrorResponse(null, JsonRpcError.INVALID_REQUEST));
@ -123,7 +134,7 @@ public class WebSocketRequestHandler {
}
// handle batch request
LOG.debug("batch request size {}", jsonArray.size());
handleJsonBatchRequest(authenticationService, websocket, jsonArray, user);
handleJsonBatchRequest(authenticationService, websocket, jsonArray, user, noAuthApiMethods);
}
};
}
@ -132,7 +143,8 @@ public class WebSocketRequestHandler {
final Optional<AuthenticationService> authenticationService,
final ServerWebSocket websocket,
final Optional<User> user,
final WebSocketRpcRequest requestBody) {
final WebSocketRpcRequest requestBody,
final Collection<String> noAuthApiMethods) {
if (!methods.containsKey(requestBody.getMethod())) {
LOG.debug("Can't find method {}", requestBody.getMethod());
@ -142,7 +154,7 @@ public class WebSocketRequestHandler {
try {
LOG.debug("WS-RPC request -> {}", requestBody.getMethod());
requestBody.setConnectionId(websocket.textHandlerID());
if (AuthenticationUtils.isPermitted(authenticationService, user, method)) {
if (AuthenticationUtils.isPermitted(authenticationService, user, method, noAuthApiMethods)) {
final JsonRpcRequestContext requestContext =
new JsonRpcRequestContext(
requestBody, user, new IsAliveHandler(ethScheduler, timeoutSec));
@ -167,8 +179,9 @@ public class WebSocketRequestHandler {
final ServerWebSocket websocket,
final Optional<User> user,
final Promise<Object> future,
final WebSocketRpcRequest requestBody) {
future.complete(process(authenticationService, websocket, user, requestBody));
final WebSocketRpcRequest requestBody,
final Collection<String> noAuthApiMethods) {
future.complete(process(authenticationService, websocket, user, requestBody, noAuthApiMethods));
}
@SuppressWarnings("rawtypes")
@ -176,7 +189,8 @@ public class WebSocketRequestHandler {
final Optional<AuthenticationService> authenticationService,
final ServerWebSocket websocket,
final JsonArray jsonArray,
final Optional<User> user) {
final Optional<User> user,
final Collection<String> noAuthApiMethods) {
// Interpret json as rpc request
final List<Future> responses =
jsonArray.stream()
@ -194,7 +208,8 @@ public class WebSocketRequestHandler {
authenticationService,
websocket,
user,
getRequest(req.toString()))));
getRequest(req.toString()),
noAuthApiMethods)));
})
.collect(toList());

@ -137,7 +137,11 @@ public class WebSocketService {
token,
user ->
websocketRequestHandler.handle(
authenticationService, websocket, buffer.toString(), user));
authenticationService,
websocket,
buffer.toString(),
user,
configuration.getRpcApisNoAuth()));
});
websocket.textMessageHandler(
@ -152,7 +156,11 @@ public class WebSocketService {
token,
user ->
websocketRequestHandler.handle(
authenticationService, websocket, payload, user));
authenticationService,
websocket,
payload,
user,
configuration.getRpcApisNoAuth()));
});
websocket.closeHandler(

@ -35,6 +35,7 @@ public class JsonRpcConfigurationTest {
assertThat(configuration.getPort()).isEqualTo(8545);
assertThat(configuration.getCorsAllowedDomains()).isEmpty();
assertThat(configuration.getRpcApis()).containsExactlyInAnyOrderElementsOf(DEFAULT_RPC_APIS);
assertThat(configuration.getNoAuthRpcApis()).isEmpty();
assertThat(configuration.getMaxActiveConnections())
.isEqualTo(JsonRpcConfiguration.DEFAULT_MAX_ACTIVE_CONNECTIONS);
assertThat(configuration.getAuthenticationAlgorithm()).isEqualTo(JwtAlgorithm.RS256);
@ -75,6 +76,20 @@ public class JsonRpcConfigurationTest {
assertThat(configuration.getRpcApis()).containsExactly(RpcApis.DEBUG.name());
}
@Test
public void settingNoAuthRpcApisShouldOverridePreviousValues() {
final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault();
configuration.setNoAuthRpcApis(
Lists.newArrayList(RpcMethod.ADMIN_ADD_PEER.name(), RpcMethod.ADMIN_PEERS.name()));
assertThat(configuration.getNoAuthRpcApis())
.containsExactly(RpcMethod.ADMIN_ADD_PEER.name(), RpcMethod.ADMIN_PEERS.name());
configuration.setNoAuthRpcApis(Lists.newArrayList(RpcMethod.MINER_SET_COINBASE.name()));
assertThat(configuration.getNoAuthRpcApis())
.containsExactly(RpcMethod.MINER_SET_COINBASE.name());
}
@Test
public void tlsConfigurationDefaultShouldBeEmpty() {
final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault();

@ -104,6 +104,8 @@ public class JsonRpcHttpServiceLoginTest {
protected static final Collection<String> JSON_RPC_APIS =
Arrays.asList(
RpcApis.ETH.name(), RpcApis.NET.name(), RpcApis.WEB3.name(), RpcApis.ADMIN.name());
protected static final List<String> NO_AUTH_METHODS =
Arrays.asList(RpcMethod.NET_SERVICES.getMethodName());
protected static JWTAuth jwtAuth;
protected static String authPermissionsConfigFilePath = "JsonRpcHttpService/auth.toml";
protected final JsonRpcTestHelper testHelper = new JsonRpcTestHelper();
@ -167,6 +169,7 @@ public class JsonRpcHttpServiceLoginTest {
final JsonRpcConfiguration config = createJsonRpcConfig();
config.setAuthenticationEnabled(true);
config.setAuthenticationCredentialsFile(authTomlPath);
config.setNoAuthRpcApis(NO_AUTH_METHODS);
return new JsonRpcHttpService(
vertx,
@ -395,26 +398,41 @@ public class JsonRpcHttpServiceLoginTest {
// single eth/blockNumber method permitted
Assertions.assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), ethBlockNumber))
service.authenticationService,
Optional.of(user),
ethBlockNumber,
Collections.emptyList()))
.isTrue();
// eth/accounts NOT permitted
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), ethAccounts))
service.authenticationService,
Optional.of(user),
ethAccounts,
Collections.emptyList()))
.isFalse();
// allowed by web3/*
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), web3ClientVersion))
service.authenticationService,
Optional.of(user),
web3ClientVersion,
Collections.emptyList()))
.isTrue();
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), web3Sha3))
service.authenticationService,
Optional.of(user),
web3Sha3,
Collections.emptyList()))
.isTrue();
// NO net permissions
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), netVersion))
service.authenticationService,
Optional.of(user),
netVersion,
Collections.emptyList()))
.isFalse();
});
}
@ -455,26 +473,41 @@ public class JsonRpcHttpServiceLoginTest {
// single eth/blockNumber method permitted
Assertions.assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), ethBlockNumber))
service.authenticationService,
Optional.of(user),
ethBlockNumber,
Collections.emptyList()))
.isTrue();
// eth/accounts IS permitted
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), ethAccounts))
service.authenticationService,
Optional.of(user),
ethAccounts,
Collections.emptyList()))
.isTrue();
// allowed by *:*
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), web3ClientVersion))
service.authenticationService,
Optional.of(user),
web3ClientVersion,
Collections.emptyList()))
.isTrue();
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), web3Sha3))
service.authenticationService,
Optional.of(user),
web3Sha3,
Collections.emptyList()))
.isTrue();
// YES net permissions
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.of(user), netVersion))
service.authenticationService,
Optional.of(user),
netVersion,
Collections.emptyList()))
.isTrue();
});
}
@ -486,7 +519,10 @@ public class JsonRpcHttpServiceLoginTest {
assertThat(
AuthenticationUtils.isPermitted(
service.authenticationService, Optional.empty(), ethAccounts))
service.authenticationService,
Optional.empty(),
ethAccounts,
Collections.emptyList()))
.isFalse();
}
@ -546,6 +582,38 @@ public class JsonRpcHttpServiceLoginTest {
}
}
@Test
public void noAuthMethodSuccessfulAfterLogin() throws Exception {
final String token = login("user", "pegasys");
final String id = "123";
final RequestBody requestBody =
RequestBody.create(
JSON,
"{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_services\"}");
try (final Response response = client.newCall(buildPostRequest(requestBody, token)).execute()) {
assertThat(response.code()).isEqualTo(200);
final JsonObject json = new JsonObject(response.body().string());
testHelper.assertValidJsonRpcResult(json, id);
}
}
@Test
public void noAuthMethodSuccessfulWithNoToken() throws Exception {
final String id = "123";
final RequestBody requestBody =
RequestBody.create(
JSON,
"{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_services\"}");
try (final Response response = client.newCall(buildPostRequest(requestBody)).execute()) {
assertThat(response.code()).isEqualTo(200);
final JsonObject json = new JsonObject(response.body().string());
testHelper.assertValidJsonRpcResult(json, id);
}
}
@Test
public void ethSyncingUnauthorisedWithoutPermission() throws Exception {
final String token = login("user", "pegasys");

@ -17,6 +17,10 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.websocket;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.DEFAULT_RPC_APIS;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import com.google.common.collect.Lists;
import org.junit.Test;
public class WebSocketConfigurationTest {
@ -29,7 +33,34 @@ public class WebSocketConfigurationTest {
assertThat(configuration.getHost()).isEqualTo("127.0.0.1");
assertThat(configuration.getPort()).isEqualTo(8546);
assertThat(configuration.getRpcApis()).containsExactlyInAnyOrderElementsOf(DEFAULT_RPC_APIS);
assertThat(configuration.getRpcApisNoAuth()).isEmpty();
assertThat(configuration.getMaxActiveConnections())
.isEqualTo(WebSocketConfiguration.DEFAULT_MAX_ACTIVE_CONNECTIONS);
}
@Test
public void settingRpcApisShouldOverridePreviousValues() {
final WebSocketConfiguration configuration = WebSocketConfiguration.createDefault();
configuration.setRpcApis(Lists.newArrayList(RpcApis.ETH.name(), RpcApis.MINER.name()));
assertThat(configuration.getRpcApis())
.containsExactly(RpcApis.ETH.name(), RpcApis.MINER.name());
configuration.setRpcApis(Lists.newArrayList(RpcApis.DEBUG.name()));
assertThat(configuration.getRpcApis()).containsExactly(RpcApis.DEBUG.name());
}
@Test
public void settingNoAuthRpcApisShouldOverridePreviousValues() {
final WebSocketConfiguration configuration = WebSocketConfiguration.createDefault();
configuration.setRpcApisNoAuth(
Lists.newArrayList(RpcMethod.ADMIN_ADD_PEER.name(), RpcMethod.ADMIN_PEERS.name()));
assertThat(configuration.getRpcApisNoAuth())
.containsExactly(RpcMethod.ADMIN_ADD_PEER.name(), RpcMethod.ADMIN_PEERS.name());
configuration.setRpcApisNoAuth(Lists.newArrayList(RpcMethod.MINER_SET_COINBASE.name()));
assertThat(configuration.getRpcApisNoAuth())
.containsExactly(RpcMethod.MINER_SET_COINBASE.name());
}
}

@ -22,28 +22,59 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import org.hyperledger.besu.config.StubGenesisConfigOptions;
import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcHttpService;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.filter.FilterManager;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethodsFactory;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.methods.WebSocketMethodsFactory;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.SubscriptionManager;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.manager.EthPeers;
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule;
import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import java.math.BigInteger;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.WebSocketConnectOptions;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
@ -52,22 +83,44 @@ import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
@RunWith(VertxUnitRunner.class)
public class WebSocketServiceLoginTest {
private static final int VERTX_AWAIT_TIMEOUT_MILLIS = 10000;
@ClassRule public static final TemporaryFolder folder = new TemporaryFolder();
private Vertx vertx;
protected static Map<String, JsonRpcMethod> rpcMethods;
protected static JsonRpcHttpService service;
protected static OkHttpClient client;
protected static String baseUrl;
protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
protected static final String CLIENT_VERSION = "TestClientVersion/0.1.0";
protected static final BigInteger CHAIN_ID = BigInteger.valueOf(123);
protected static P2PNetwork peerDiscoveryMock;
protected static BlockchainQueries blockchainQueries;
protected static Synchronizer synchronizer;
protected static final Collection<String> JSON_RPC_APIS =
Arrays.asList(
RpcApis.ETH.name(), RpcApis.NET.name(), RpcApis.WEB3.name(), RpcApis.ADMIN.name());
protected static final List<String> NO_AUTH_METHODS =
Arrays.asList(RpcMethod.NET_SERVICES.getMethodName());
protected static JWTAuth jwtAuth;
protected static final NatService natService = new NatService(Optional.empty());
private WebSocketConfiguration websocketConfiguration;
private WebSocketRequestHandler webSocketRequestHandlerSpy;
private WebSocketService websocketService;
private HttpClient httpClient;
protected static JWTAuth jwtAuth;
@Before
public void before() throws URISyntaxException {
@ -83,11 +136,51 @@ public class WebSocketServiceLoginTest {
websocketConfiguration.setAuthenticationEnabled(true);
websocketConfiguration.setAuthenticationCredentialsFile(authTomlPath);
websocketConfiguration.setHostsAllowlist(Collections.singletonList("*"));
websocketConfiguration.setRpcApisNoAuth(new ArrayList<>(NO_AUTH_METHODS));
peerDiscoveryMock = mock(P2PNetwork.class);
blockchainQueries = mock(BlockchainQueries.class);
synchronizer = mock(Synchronizer.class);
final Set<Capability> supportedCapabilities = new HashSet<>();
supportedCapabilities.add(EthProtocol.ETH62);
supportedCapabilities.add(EthProtocol.ETH63);
final StubGenesisConfigOptions genesisConfigOptions =
new StubGenesisConfigOptions().constantinopleBlock(0).chainId(CHAIN_ID);
final Map<String, JsonRpcMethod> websocketMethods =
new WebSocketMethodsFactory(
new SubscriptionManager(new NoOpMetricsSystem()), new HashMap<>())
.methods();
rpcMethods =
spy(
new JsonRpcMethodsFactory()
.methods(
CLIENT_VERSION,
CHAIN_ID,
genesisConfigOptions,
peerDiscoveryMock,
blockchainQueries,
synchronizer,
MainnetProtocolSchedule.fromConfig(genesisConfigOptions),
mock(FilterManager.class),
mock(TransactionPool.class),
mock(PoWMiningCoordinator.class),
new NoOpMetricsSystem(),
supportedCapabilities,
Optional.empty(),
Optional.empty(),
JSON_RPC_APIS,
mock(PrivacyParameters.class),
mock(JsonRpcConfiguration.class),
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
new HashMap<>(),
folder.getRoot().toPath(),
mock(EthPeers.class)));
websocketMethods.putAll(rpcMethods);
webSocketRequestHandlerSpy =
spy(
new WebSocketRequestHandler(
@ -144,67 +237,67 @@ public class WebSocketServiceLoginTest {
@Test
public void loginWithGoodCredentials(final TestContext context) {
final Async async = context.async();
Handler<AsyncResult<HttpClientResponse>> responseHandler =
response -> {
assertThat(response.result().statusCode()).isEqualTo(200);
assertThat(response.result().statusMessage()).isEqualTo("OK");
assertThat(response.result().getHeader("Content-Type")).isNotNull();
assertThat(response.result().getHeader("Content-Type")).isEqualTo("application/json");
response
.result()
.bodyHandler(
buffer -> {
final String body = buffer.toString();
assertThat(body).isNotBlank();
final JsonObject respBody = new JsonObject(body);
final String token = respBody.getString("token");
assertThat(token).isNotNull();
assertThat(token).isNotEmpty();
websocketService
.authenticationService
.get()
.getJwtAuthProvider()
.authenticate(
new JsonObject().put("token", token),
(r) -> {
Assertions.assertThat(r.succeeded()).isTrue();
final User user = r.result();
user.isAuthorized(
"noauths",
(authed) -> {
assertThat(authed.succeeded()).isTrue();
assertThat(authed.result()).isFalse();
});
user.isAuthorized(
"fakePermission",
(authed) -> {
assertThat(authed.succeeded()).isTrue();
assertThat(authed.result()).isTrue();
});
user.isAuthorized(
"eth:subscribe",
(authed) -> {
assertThat(authed.succeeded()).isTrue();
assertThat(authed.result()).isTrue();
async.complete();
});
});
});
};
Handler<AsyncResult<HttpClientRequest>> requestHandler =
request -> {
request.result().putHeader("Content-Type", "application/json; charset=utf-8");
request.result().end("{\"username\":\"user\",\"password\":\"pegasys\"}");
request.result().send(responseHandler);
};
httpClient.request(
HttpMethod.POST,
websocketConfiguration.getPort(),
websocketConfiguration.getHost(),
"/login",
request -> {
request.result().putHeader("Content-Type", "application/json; charset=utf-8");
request.result().end("{\"username\":\"user\",\"password\":\"pegasys\"}");
request
.result()
.send(
response -> {
assertThat(response.result().statusCode()).isEqualTo(200);
assertThat(response.result().statusMessage()).isEqualTo("OK");
assertThat(response.result().getHeader("Content-Type")).isNotNull();
assertThat(response.result().getHeader("Content-Type"))
.isEqualTo("application/json");
response
.result()
.bodyHandler(
buffer -> {
final String body = buffer.toString();
assertThat(body).isNotBlank();
final JsonObject respBody = new JsonObject(body);
final String token = respBody.getString("token");
assertThat(token).isNotNull();
assertThat(token).isNotEmpty();
websocketService
.authenticationService
.get()
.getJwtAuthProvider()
.authenticate(
new JsonObject().put("token", token),
(r) -> {
Assertions.assertThat(r.succeeded()).isTrue();
final User user = r.result();
user.isAuthorized(
"noauths",
(authed) -> {
assertThat(authed.succeeded()).isTrue();
assertThat(authed.result()).isFalse();
});
user.isAuthorized(
"fakePermission",
(authed) -> {
assertThat(authed.succeeded()).isTrue();
assertThat(authed.result()).isTrue();
});
user.isAuthorized(
"eth:subscribe",
(authed) -> {
assertThat(authed.succeeded()).isTrue();
assertThat(authed.result()).isTrue();
async.complete();
});
});
});
});
});
requestHandler);
async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS);
}
@ -240,6 +333,37 @@ public class WebSocketServiceLoginTest {
async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS);
}
@Test
public void netServicesSucceedWithNoAuth(final TestContext context) {
final Async async = context.async();
final String id = "123";
final String request =
"{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_services\"}";
final String expectedResponse = "{\"jsonrpc\":\"2.0\",\"id\":\"123\",\"result\":{}}";
WebSocketConnectOptions options = new WebSocketConnectOptions();
options.setURI("/");
options.setHost(websocketConfiguration.getHost());
options.setPort(websocketConfiguration.getPort());
httpClient.webSocket(
options,
webSocket -> {
webSocket.result().writeTextMessage(request);
webSocket
.result()
.handler(
buffer -> {
context.assertEquals(expectedResponse, buffer.toString());
async.complete();
});
});
async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS);
}
@Test
public void websocketServiceWithGoodHeaderAuthenticationToken(final TestContext context) {
final Async async = context.async();

Loading…
Cancel
Save