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

[NC-2046] websocket method permissions #870

Merged
merged 18 commits into from
Feb 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2018 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.tests.acceptance.dsl.condition.net;

import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition;
import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node;
import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.net.NetVersionTransaction;

public class ExpectNetVersionPermissionJsonRpcUnauthorizedResponse implements Condition {

private final NetVersionTransaction transaction;

public ExpectNetVersionPermissionJsonRpcUnauthorizedResponse(
final NetVersionTransaction transaction) {
this.transaction = transaction;
}

@Override
public void verify(final Node node) {
node.execute(transaction);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import tech.pegasys.pantheon.tests.acceptance.dsl.condition.net.ExpectNetVersionConnectionExceptionWithCause;
import tech.pegasys.pantheon.tests.acceptance.dsl.condition.net.ExpectNetVersionIsNotBlank;
import tech.pegasys.pantheon.tests.acceptance.dsl.condition.net.ExpectNetVersionPermissionException;
import tech.pegasys.pantheon.tests.acceptance.dsl.condition.net.ExpectNetVersionPermissionJsonRpcUnauthorizedResponse;
import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.net.NetTransactions;

import java.math.BigInteger;
Expand Down Expand Up @@ -51,6 +52,10 @@ public Condition netVersionUnauthorizedExceptional(final String expectedMessage)
return new ExpectNetVersionPermissionException(transactions.netVersion(), expectedMessage);
}

public Condition netVersionUnauthorizedResponse() {
return new ExpectNetVersionPermissionJsonRpcUnauthorizedResponse(transactions.netVersion());
}

public Condition awaitPeerCountExceptional() {
return new AwaitNetPeerCountException(transactions.peerCount());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public String execute(final JsonRequestFactories node) {
try {
final NetVersion result = node.net().netVersion().send();
assertThat(result).isNotNull();
assertThat(result.hasError()).isFalse();
return result.getNetVersion();
} catch (final Exception e) {
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ public void shouldFailLoginWithWrongCredentials() {
public void shouldSucceedLoginWithCorrectCredentials() {
node.verify(login.loginSucceeds("user", "pegasys"));
}

@Test
public void jsonRpcMethodShouldSucceedWithAuthenticatedUserAndPermission() {
node.verify(login.loginSucceedsAndSetsAuthenticationToken("user", "pegasys"));
node.verify(net.awaitPeerCount(0));
node.verify(net.netVersionUnauthorizedResponse());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static tech.pegasys.pantheon.util.NetworkUtility.urlForSocketAddress;

import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationService;
import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationUtils;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequestId;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters;
Expand All @@ -43,7 +44,6 @@
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
Expand Down Expand Up @@ -231,68 +231,10 @@ private Handler<RoutingContext> checkWhitelistHostHeader() {
};
}

private boolean requiresAuthentication() {
return authenticationService.isPresent();
}

@VisibleForTesting
public boolean isPermitted(final Optional<User> optionalUser, final JsonRpcMethod jsonRpcMethod) {

AtomicBoolean foundMatchingPermission = new AtomicBoolean();

if (requiresAuthentication()) {
if (optionalUser.isPresent()) {
User user = optionalUser.get();
for (String perm : jsonRpcMethod.getPermissions()) {
user.isAuthorized(
perm,
(authed) -> {
if (authed.result()) {
LOG.trace(
"user {} authorized : {} via permission {}",
user,
jsonRpcMethod.getName(),
perm);
foundMatchingPermission.set(true);
}
});
}
}
} else {
// no auth provider configured thus anything is permitted
foundMatchingPermission.set(true);
}

if (!foundMatchingPermission.get()) {
LOG.trace("user NOT authorized : {}", jsonRpcMethod.getName());
}
return foundMatchingPermission.get();
}

private String getToken(final RoutingContext routingContext) {
private String getAuthToken(final RoutingContext routingContext) {
return routingContext.request().getHeader("Bearer");
}

private void getUser(final String token, final Handler<Optional<User>> handler) {
try {
if (!requiresAuthentication()) {
handler.handle(Optional.empty());
} else {
authenticationService
.get()
.getJwtAuthProvider()
.authenticate(
new JsonObject().put("jwt", token),
(r) -> {
final User user = r.result();
handler.handle(Optional.of(user));
});
}
} catch (Exception e) {
handler.handle(Optional.empty());
}
}

private Optional<String> getAndValidateHostHeader(final RoutingContext event) {
final Iterable<String> splitHostHeader = Splitter.on(':').split(event.request().host());
final long hostPieces = stream(splitHostHeader).count();
Expand Down Expand Up @@ -345,16 +287,17 @@ public String url() {

private void handleJsonRPCRequest(final RoutingContext routingContext) {
// first check token if authentication is required
String token = getToken(routingContext);
if (requiresAuthentication() && token == null) {
String token = getAuthToken(routingContext);
if (authenticationService.isPresent() && token == null) {
// 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) == '{') {
getUser(
AuthenticationUtils.getUser(
authenticationService,
token,
user -> {
handleJsonSingleRequest(routingContext, new JsonObject(json), user);
Expand All @@ -365,7 +308,8 @@ private void handleJsonRPCRequest(final RoutingContext routingContext) {
handleJsonRpcError(routingContext, null, JsonRpcError.INVALID_REQUEST);
return;
}
getUser(
AuthenticationUtils.getUser(
authenticationService,
token,
user -> {
handleJsonBatchRequest(routingContext, array, user);
Expand Down Expand Up @@ -502,7 +446,7 @@ private JsonRpcResponse process(final JsonObject requestJson, final Optional<Use
return errorResponse(id, JsonRpcError.METHOD_NOT_FOUND);
}

if (isPermitted(user, method)) {
if (AuthenticationUtils.isPermitted(authenticationService, user, method)) {
// Generate response
try (final TimingContext ignored = requestTimer.labels(request.getMethod()).startTimer()) {
return method.response(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

/** Provides authentication handlers for use in the http and websocket services */
public class AuthenticationService {

private final JWTAuth jwtAuthProvider;
@VisibleForTesting public final JWTAuthOptions jwtAuthOptions;
private final AuthProvider credentialAuthProvider;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.ethereum.jsonrpc.authentication;

import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;

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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class AuthenticationUtils {
Copy link
Contributor

@lucassaldanha lucassaldanha Feb 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find any unit tests for AuthenticationUtils. This will make it harder to change this class in the future as we won't have anything to verify its current behaviour. Do we have unit tests that will assert the behaviour of this class?

private static final Logger LOG = LogManager.getLogger();

@VisibleForTesting
public static boolean isPermitted(
final Optional<AuthenticationService> authenticationService,
final Optional<User> optionalUser,
final JsonRpcMethod jsonRpcMethod) {

AtomicBoolean foundMatchingPermission = new AtomicBoolean();

if (authenticationService.isPresent()) {
if (optionalUser.isPresent()) {
User user = optionalUser.get();
for (String perm : jsonRpcMethod.getPermissions()) {
user.isAuthorized(
perm,
(authed) -> {
if (authed.result()) {
LOG.trace(
"user {} authorized : {} via permission {}",
user,
jsonRpcMethod.getName(),
perm);
foundMatchingPermission.set(true);
}
});
}
}
} else {
// no auth provider configured thus anything is permitted
foundMatchingPermission.set(true);
}

if (!foundMatchingPermission.get()) {
LOG.trace("user NOT authorized : {}", jsonRpcMethod.getName());
}
return foundMatchingPermission.get();
}

public static void getUser(
final Optional<AuthenticationService> authenticationService,
final String token,
final Handler<Optional<User>> handler) {
try {
if (!authenticationService.isPresent()) {
handler.handle(Optional.empty());
} else {
authenticationService
.get()
.getJwtAuthProvider()
.authenticate(
new JsonObject().put("jwt", token),
(r) -> {
final User user = r.result();
handler.handle(Optional.of(user));
});
}
} catch (Exception e) {
handler.handle(Optional.empty());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@
*/
package tech.pegasys.pantheon.ethereum.jsonrpc.websocket;

import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationService;
import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationUtils;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcUnauthorizedResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketRpcRequest;

import java.util.Map;
import java.util.Optional;

import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.Json;
import io.vertx.ext.auth.User;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand All @@ -39,6 +44,14 @@ public WebSocketRequestHandler(final Vertx vertx, final Map<String, JsonRpcMetho
}

public void handle(final String id, final Buffer buffer) {
handle(Optional.empty(), id, buffer, Optional.empty());
}

public void handle(
final Optional<AuthenticationService> authenticationService,
final String id,
final Buffer buffer,
final Optional<User> user) {
vertx.executeBlocking(
future -> {
final WebSocketRpcRequest request;
Expand All @@ -60,7 +73,12 @@ public void handle(final String id, final Buffer buffer) {
try {
LOG.debug("WS-RPC request -> {}", request.getMethod());
request.setConnectionId(id);
future.complete(method.response(request));
if (AuthenticationUtils.isPermitted(authenticationService, user, method)) {
future.complete(method.response(request));
} else {
future.complete(
new JsonRpcUnauthorizedResponse(request.getId(), JsonRpcError.UNAUTHORIZED));
}
} catch (final Exception e) {
LOG.error(JsonRpcError.INTERNAL_ERROR.getMessage(), e);
future.complete(new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package tech.pegasys.pantheon.ethereum.jsonrpc.websocket;

import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationService;
import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationUtils;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager;

import java.net.InetSocketAddress;
Expand Down Expand Up @@ -94,6 +95,10 @@ private Handler<ServerWebSocket> websocketHandler() {
return websocket -> {
final SocketAddress socketAddress = websocket.remoteAddress();
final String connectionId = websocket.textHandlerID();
final String token = getAuthToken(websocket);
if (token != null) {
LOG.trace("Websocket authentication token {}", token);
}

LOG.debug("Websocket Connected ({})", socketAddressAsString(socketAddress));

Expand All @@ -104,7 +109,12 @@ private Handler<ServerWebSocket> websocketHandler() {
buffer.toString(),
socketAddressAsString(socketAddress));

websocketRequestHandler.handle(connectionId, buffer);
AuthenticationUtils.getUser(
authenticationService,
token,
user ->
websocketRequestHandler.handle(
authenticationService, connectionId, buffer, user));
});

websocket.closeHandler(
Expand Down Expand Up @@ -197,4 +207,8 @@ public InetSocketAddress socketAddress() {
private String socketAddressAsString(final SocketAddress socketAddress) {
return String.format("host=%s, port=%d", socketAddress.host(), socketAddress.port());
}

private String getAuthToken(final ServerWebSocket websocket) {
return websocket.headers().get("Bearer");
}
}
Loading