diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyRpcMethodDecorator.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyRpcMethodDecorator.java new file mode 100644 index 0000000000..fdd2b64331 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyRpcMethodDecorator.java @@ -0,0 +1,57 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcUnauthorizedResponse; + +import java.util.Optional; + +import io.vertx.ext.auth.User; +import org.apache.logging.log4j.Logger; + +public class MultiTenancyRpcMethodDecorator implements JsonRpcMethod { + private static final Logger LOG = getLogger(); + private JsonRpcMethod rpcMethod; + + public MultiTenancyRpcMethodDecorator(final JsonRpcMethod rpcMethod) { + this.rpcMethod = rpcMethod; + } + + @Override + public String getName() { + return rpcMethod.getName(); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + final Optional user = requestContext.getUser(); + final Object id = requestContext.getRequest().getId(); + if (user.isEmpty()) { + LOG.error("Request does not contain an authorization token"); + return new JsonRpcUnauthorizedResponse(id, JsonRpcError.UNAUTHORIZED); + } else if (MultiTenancyUserUtil.enclavePublicKey(user).isEmpty()) { + LOG.error("Request token does not contain an enclave public key"); + return new JsonRpcUnauthorizedResponse(id, JsonRpcError.UNAUTHORIZED); + } else { + return rpcMethod.response(requestContext); + } + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyUserUtil.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyUserUtil.java new file mode 100644 index 0000000000..882f74d9b8 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyUserUtil.java @@ -0,0 +1,27 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods; + +import java.util.Optional; + +import io.vertx.ext.auth.User; + +public class MultiTenancyUserUtil { + private static final String ENCLAVE_PRIVACY_PUBLIC_KEY_CLAIM = "privacyPublicKey"; + + public static Optional enclavePublicKey(final Optional user) { + return user.map(u -> u.principal().getString(ENCLAVE_PRIVACY_PUBLIC_KEY_CLAIM)); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java index 7d1b2572ef..33c43b1753 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethods.java @@ -17,6 +17,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.methods; import org.hyperledger.besu.ethereum.api.jsonrpc.LatestNonceProvider; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.DisabledPrivacyRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.MultiTenancyRpcMethodDecorator; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.Address; import org.hyperledger.besu.ethereum.core.PrivacyParameters; @@ -79,9 +80,7 @@ public abstract class PrivacyApiGroupJsonRpcMethods extends ApiGroupJsonRpcMetho return create(privacyController).entrySet().stream() .collect( Collectors.toMap( - Entry::getKey, - rpcMethod -> - createPrivacyMethod(privacyParameters.isEnabled(), rpcMethod.getValue()))); + Entry::getKey, entry -> createPrivacyMethod(privacyParameters, entry.getValue()))); } protected abstract Map create(final PrivacyController privacyController); @@ -103,7 +102,14 @@ public abstract class PrivacyApiGroupJsonRpcMethods extends ApiGroupJsonRpcMetho return new RandomSigningPrivateMarkerTransactionFactory(privateContractAddress); } - private JsonRpcMethod createPrivacyMethod(final Boolean enabled, final JsonRpcMethod rpcMethod) { - return enabled ? rpcMethod : new DisabledPrivacyRpcMethod(rpcMethod.getName()); + private JsonRpcMethod createPrivacyMethod( + final PrivacyParameters privacyParameters, final JsonRpcMethod rpcMethod) { + if (privacyParameters.isEnabled() && privacyParameters.isMultiTenancyEnabled()) { + return new MultiTenancyRpcMethodDecorator(rpcMethod); + } else if (!privacyParameters.isEnabled()) { + return new DisabledPrivacyRpcMethod(rpcMethod.getName()); + } else { + return rpcMethod; + } } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyRpcMethodDecoratorTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyRpcMethodDecoratorTest.java new file mode 100644 index 0000000000..0120d2b24c --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyRpcMethodDecoratorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponseType; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcUnauthorizedResponse; + +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.jwt.impl.JWTUser; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MultiTenancyRpcMethodDecoratorTest { + + @Mock private JsonRpcMethod jsonRpcMethod; + private final JsonRpcRequest rpcRequest = new JsonRpcRequest("1", "test", new String[] {"a"}); + + @Test + public void delegatesWhenHasValidToken() { + final JsonObject principle = new JsonObject(); + principle.put("privacyPublicKey", "ABC123"); + final JWTUser user = new JWTUser(principle, ""); + final JsonRpcRequestContext rpcRequestContext = new JsonRpcRequestContext(rpcRequest, user); + + when(jsonRpcMethod.response(rpcRequestContext)) + .thenReturn(new JsonRpcSuccessResponse("1", "b")); + when(jsonRpcMethod.getName()).thenReturn("delegate"); + + final MultiTenancyRpcMethodDecorator tokenRpcDecorator = + new MultiTenancyRpcMethodDecorator(jsonRpcMethod); + + assertThat(tokenRpcDecorator.getName()).isEqualTo("delegate"); + + final JsonRpcResponse response = tokenRpcDecorator.response(rpcRequestContext); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo("b"); + } + + @Test + public void failsWhenHasNoToken() { + final JsonRpcRequestContext rpcRequestContext = new JsonRpcRequestContext(rpcRequest); + final MultiTenancyRpcMethodDecorator tokenRpcDecorator = + new MultiTenancyRpcMethodDecorator(jsonRpcMethod); + when(jsonRpcMethod.getName()).thenReturn("delegate"); + + assertThat(tokenRpcDecorator.getName()).isEqualTo("delegate"); + + final JsonRpcResponse response = tokenRpcDecorator.response(rpcRequestContext); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.UNAUTHORIZED); + final JsonRpcUnauthorizedResponse errorResponse = (JsonRpcUnauthorizedResponse) response; + assertThat(errorResponse.getError()).isEqualTo(JsonRpcError.UNAUTHORIZED); + } + + @Test + public void failsWhenTokenDoesNotHavePrivacyPublicKey() { + final JWTUser user = new JWTUser(new JsonObject(), ""); + final JsonRpcRequestContext rpcRequestContext = new JsonRpcRequestContext(rpcRequest, user); + final MultiTenancyRpcMethodDecorator tokenRpcDecorator = + new MultiTenancyRpcMethodDecorator(jsonRpcMethod); + when(jsonRpcMethod.getName()).thenReturn("delegate"); + + assertThat(tokenRpcDecorator.getName()).isEqualTo("delegate"); + + final JsonRpcResponse response = tokenRpcDecorator.response(rpcRequestContext); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.UNAUTHORIZED); + final JsonRpcUnauthorizedResponse errorResponse = (JsonRpcUnauthorizedResponse) response; + assertThat(errorResponse.getError()).isEqualTo(JsonRpcError.UNAUTHORIZED); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyUserUtilTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyUserUtilTest.java new file mode 100644 index 0000000000..83f9ac6245 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/MultiTenancyUserUtilTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods; + +import static java.util.Optional.empty; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.MultiTenancyUserUtil.enclavePublicKey; + +import java.util.Optional; + +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.jwt.impl.JWTUser; +import org.junit.Test; + +public class MultiTenancyUserUtilTest { + + @Test + public void noEnclavePublicKeyWhenNoUserProvided() { + assertThat(enclavePublicKey(empty())).isEmpty(); + } + + @Test + public void noEnclavePublicKeyWhenUserWithoutEnclavePublicKeyClaimProvided() { + final JsonObject token = new JsonObject(); + final Optional user = Optional.of(new JWTUser(token, "")); + + assertThat(enclavePublicKey(user)).isEmpty(); + } + + @Test + public void enclavePublicKeyKeyReturnedForUserWithEnclavePublicKeyClaim() { + final JsonObject principle = new JsonObject(); + principle.put("privacyPublicKey", "ABC123"); + final Optional user = Optional.of(new JWTUser(principle, "")); + + assertThat(enclavePublicKey(user)).contains("ABC123"); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethodsTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethodsTest.java index c3eefaad71..61534db150 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethodsTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/PrivacyApiGroupJsonRpcMethodsTest.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.MultiTenancyRpcMethodDecorator; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponseType; @@ -56,6 +57,24 @@ public class PrivacyApiGroupJsonRpcMethodsTest { privacyApiGroupJsonRpcMethods = createPrivacyApiGroupJsonRpcMethods(); } + @Test + public void rpcMethodsCreatedWhenMultiTenancyIsEnabledHaveMultiTenancyValidator() { + final Map rpcMethods = privacyApiGroupJsonRpcMethods.create(); + final JsonRpcMethod privMethod = rpcMethods.get("priv_method"); + + assertThat(privMethod).isNotSameAs(rpcMethod); + assertThat(privMethod.getClass()).hasSameClassAs(MultiTenancyRpcMethodDecorator.class); + } + + @Test + public void rpcsCreatedWithoutMultiTenancyUseOriginalRpcMethod() { + when(privacyParameters.isEnabled()).thenReturn(true); + final Map rpcMethods = privacyApiGroupJsonRpcMethods.create(); + final JsonRpcMethod privMethod = rpcMethods.get("priv_method"); + + assertThat(privMethod).isSameAs(rpcMethod); + } + @Test public void rpcMethodsCreatedWhenPrivacyIsNotEnabledAreDisabled() { final Map rpcMethods = privacyApiGroupJsonRpcMethods.create();