From ea263b3e54b695f88f84e151299bebf26cb23226 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 8 Mar 2019 16:39:22 +0000 Subject: [PATCH] CORDA-2569: Add "flow kill " command to Corda's shell. (#4861) * CORDA-2569: Add "flow kill " command to Corda's shell. * Add testing and documentation for RPC killFlow operation. --- .../client/jackson/internal/CordaModule.kt | 28 +++++++++++- .../client/jackson/StateMachineRunIdTest.kt | 29 ++++++++++++ .../rpc/StandaloneCordaRPCJavaClientTest.java | 16 +++---- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 44 ++++++++++++++----- .../net/corda/core/NodeVersioningTest.kt | 8 ++-- .../corda/core/cordapp/CordappSmokeTest.kt | 8 ++-- docs/source/shell.rst | 7 ++- .../rpc/proxies/AuthenticatedRpcOpsProxy.kt | 4 +- .../security/RPCSecurityManagerImpl.kt | 33 +++++++------- .../node/services/RPCSecurityManagerTest.kt | 29 +++++++----- .../net/corda/testing/node/MockServices.kt | 2 - .../testing/node/internal/DriverDSLImpl.kt | 3 +- .../net/corda/smoketesting/NodeProcess.kt | 4 +- tools/shell/build.gradle | 5 --- .../corda/tools/shell/FlowShellCommand.java | 21 ++++++--- .../corda/tools/shell/RunShellCommand.java | 6 +-- .../net/corda/tools/shell/InteractiveShell.kt | 26 +++++++++++ .../corda/tools/shell/InteractiveShellTest.kt | 33 +++++++++++++- 18 files changed, 226 insertions(+), 80 deletions(-) create mode 100644 client/jackson/src/test/kotlin/net/corda/client/jackson/StateMachineRunIdTest.kt diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index 094294fd60..14d82e2ab4 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -15,16 +15,20 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier import com.fasterxml.jackson.databind.deser.ContextualDeserializer import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.IntNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.ser.BeanPropertyWriter import com.fasterxml.jackson.databind.ser.BeanSerializerModifier +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer +import com.fasterxml.jackson.databind.ser.std.UUIDSerializer import com.google.common.primitives.Booleans import net.corda.client.jackson.JacksonSupport import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.crypto.PartialMerkleTree.PartialTree +import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.* import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.createComponentGroups @@ -40,7 +44,6 @@ import net.corda.core.utilities.parseAsHex import net.corda.core.utilities.toHexString import net.corda.serialization.internal.AllWhitelist import net.corda.serialization.internal.amqp.* -import net.corda.serialization.internal.model.LocalTypeInformation import java.math.BigDecimal import java.security.PublicKey import java.security.cert.CertPath @@ -79,6 +82,7 @@ class CordaModule : SimpleModule("corda-core") { context.setMixInAnnotations(SignatureMetadata::class.java, SignatureMetadataMixin::class.java) context.setMixInAnnotations(PartialTree::class.java, PartialTreeMixin::class.java) context.setMixInAnnotations(NodeInfo::class.java, NodeInfoMixin::class.java) + context.setMixInAnnotations(StateMachineRunId::class.java, StateMachineRunIdMixin::class.java) } } @@ -418,6 +422,28 @@ private interface SecureHashSHA256Mixin @JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class) private interface PublicKeyMixin +@JsonSerialize(using = StateMachineRunIdSerializer::class) +@JsonDeserialize(using = StateMachineRunIdDeserializer::class) +private interface StateMachineRunIdMixin + +private class StateMachineRunIdSerializer : StdScalarSerializer(StateMachineRunId::class.java) { + private val uuidSerializer = UUIDSerializer() + + override fun isEmpty(provider: SerializerProvider?, value: StateMachineRunId): Boolean { + return uuidSerializer.isEmpty(provider, value.uuid) + } + + override fun serialize(value: StateMachineRunId, gen: JsonGenerator?, provider: SerializerProvider?) { + uuidSerializer.serialize(value.uuid, gen, provider) + } +} + +private class StateMachineRunIdDeserializer : FromStringDeserializer(StateMachineRunId::class.java) { + override fun _deserialize(value: String, ctxt: DeserializationContext?): StateMachineRunId { + return StateMachineRunId(UUID.fromString(value)) + } +} + @Suppress("unused_parameter") @ToStringSerialize private abstract class AmountMixin @JsonCreator(mode = DISABLED) constructor( diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/StateMachineRunIdTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/StateMachineRunIdTest.kt new file mode 100644 index 0000000000..e91f8fd870 --- /dev/null +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/StateMachineRunIdTest.kt @@ -0,0 +1,29 @@ +package net.corda.client.jackson + +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.client.jackson.internal.CordaModule +import net.corda.core.flows.StateMachineRunId +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.* + +class StateMachineRunIdTest { + private companion object { + private const val ID = "a9da3d32-a08d-4add-a633-66bc6bf6183d" + private val jsonMapper: ObjectMapper = ObjectMapper().registerModule(CordaModule()) + } + + @Test + fun `state machine run ID deserialise`() { + val str = """"$ID"""" + val runID = jsonMapper.readValue(str, StateMachineRunId::class.java) + assertEquals(StateMachineRunId(UUID.fromString(ID)), runID) + } + + @Test + fun `state machine run ID serialise`() { + val runId = StateMachineRunId(UUID.fromString(ID)) + val str = jsonMapper.writeValueAsString(runId) + assertEquals(""""$ID"""", str) + } +} \ No newline at end of file diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 77fee26039..2c17ac72e3 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -25,6 +25,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; +import static java.util.Collections.singletonList; import static kotlin.test.AssertionsKt.assertEquals; import static kotlin.test.AssertionsKt.fail; import static net.corda.finance.workflows.GetBalances.getCashBalance; @@ -51,13 +52,12 @@ public class StandaloneCordaRPCJavaClientTest { } } - private List perms = Collections.singletonList("ALL"); + private List perms = singletonList("ALL"); private Set permSet = new HashSet<>(perms); - private User rpcUser = new User("user1", "test", permSet); + private User superUser = new User("superUser", "test", permSet); private AtomicInteger port = new AtomicInteger(15000); - private NodeProcess.Factory factory; private NodeProcess notary; private CordaRPCOps rpcProxy; private CordaRPCConnection connection; @@ -69,16 +69,16 @@ public class StandaloneCordaRPCJavaClientTest { port.getAndIncrement(), port.getAndIncrement(), true, - Collections.singletonList(rpcUser), + singletonList(superUser), true ); @Before public void setUp() { - factory = new NodeProcess.Factory(); + NodeProcess.Factory factory = new NodeProcess.Factory(); copyCordapps(factory, notaryConfig); notary = factory.create(notaryConfig); - connection = notary.connect(); + connection = notary.connect(superUser); rpcProxy = connection.getProxy(); notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0); } @@ -95,7 +95,7 @@ public class StandaloneCordaRPCJavaClientTest { } @Test - public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException { + public void testCashBalances() throws ExecutionException, InterruptedException { Amount dollars123 = new Amount<>(123, Currency.getInstance("USD")); FlowHandle flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, @@ -105,7 +105,7 @@ public class StandaloneCordaRPCJavaClientTest { flowHandle.getReturnValue().get(); Amount balance = getCashBalance(rpcProxy, Currency.getInstance("USD")); - System.out.print("Balance: " + balance + "\n"); + System.out.println("Balance: " + balance); assertEquals(dollars123, balance, "matching"); } diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 5731368884..7cd3d6ac46 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -3,6 +3,7 @@ package net.corda.kotlin.rpc import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import net.corda.client.rpc.CordaRPCConnection +import net.corda.client.rpc.PermissionException import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -29,10 +30,8 @@ import net.corda.nodeapi.internal.config.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess import org.apache.commons.io.output.NullOutputStream -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test +import org.junit.* +import org.junit.rules.ExpectedException import java.io.FilterInputStream import java.io.InputStream import java.util.* @@ -46,7 +45,10 @@ import kotlin.test.assertTrue class StandaloneCordaRPClientTest { private companion object { private val log = contextLogger() - val user = User("user1", "test", permissions = setOf("ALL")) + val superUser = User("superUser", "test", permissions = setOf("ALL")) + val nonUser = User("nonUser", "test", permissions = emptySet()) + val rpcUser = User("rpcUser", "test", permissions = setOf("InvokeRpc.startFlow", "InvokeRpc.killFlow")) + val flowUser = User("flowUser", "test", permissions = setOf("StartFlow.net.corda.finance.flows.CashIssueFlow")) val port = AtomicInteger(15200) const val attachmentSize = 2116 val timeout = 60.seconds @@ -65,15 +67,18 @@ class StandaloneCordaRPClientTest { rpcPort = port.andIncrement, rpcAdminPort = port.andIncrement, isNotary = true, - users = listOf(user) + users = listOf(superUser, nonUser, rpcUser, flowUser) ) + @get:Rule + val exception: ExpectedException = ExpectedException.none() + @Before fun setUp() { factory = NodeProcess.Factory() StandaloneCordaRPCJavaClientTest.copyCordapps(factory, notaryConfig) notary = factory.create(notaryConfig) - connection = notary.connect() + connection = notary.connect(superUser) rpcProxy = connection.proxy notaryNode = fetchNotaryIdentity() notaryNodeIdentity = rpcProxy.nodeInfo().legalIdentitiesAndCerts.first().party @@ -81,9 +86,7 @@ class StandaloneCordaRPClientTest { @After fun done() { - try { - connection.close() - } finally { + connection.use { notary.close() } } @@ -232,6 +235,27 @@ class StandaloneCordaRPClientTest { assertEquals(629.DOLLARS, balance) } + @Test + fun `test kill flow without killFlow permission`() { + exception.expect(PermissionException::class.java) + exception.expectMessage("User not authorized to perform RPC call killFlow") + + val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity) + notary.connect(nonUser).use { connection -> + val rpcProxy = connection.proxy + rpcProxy.killFlow(flowHandle.id) + } + } + + @Test + fun `test kill flow with killFlow permission`() { + val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 83.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity) + notary.connect(rpcUser).use { connection -> + val rpcProxy = connection.proxy + assertTrue(rpcProxy.killFlow(flowHandle.id)) + } + } + private fun fetchNotaryIdentity(): NodeInfo { val nodeInfo = rpcProxy.networkMapSnapshot() assertEquals(1, nodeInfo.size) diff --git a/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt b/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt index 33216034da..9cca6d959a 100644 --- a/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt +++ b/core/src/smoke-test/kotlin/net/corda/core/NodeVersioningTest.kt @@ -21,7 +21,7 @@ import kotlin.streams.toList class NodeVersioningTest { private companion object { - val user = User("user1", "test", permissions = setOf("ALL")) + val superUser = User("superUser", "test", permissions = setOf("ALL")) val port = AtomicInteger(15100) } @@ -33,7 +33,7 @@ class NodeVersioningTest { rpcPort = port.andIncrement, rpcAdminPort = port.andIncrement, isNotary = true, - users = listOf(user) + users = listOf(superUser) ) private val aliceConfig = NodeConfig( @@ -42,7 +42,7 @@ class NodeVersioningTest { rpcPort = port.andIncrement, rpcAdminPort = port.andIncrement, isNotary = false, - users = listOf(user) + users = listOf(superUser) ) private lateinit var notary: NodeProcess @@ -73,7 +73,7 @@ class NodeVersioningTest { selfCordapp.copyToDirectory(cordappsDir) factory.create(aliceConfig).use { alice -> - alice.connect().use { + alice.connect(superUser).use { val rpc = it.proxy assertThat(rpc.protocolVersion).isEqualTo(PLATFORM_VERSION) assertThat(rpc.nodeInfo().platformVersion).isEqualTo(PLATFORM_VERSION) diff --git a/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt b/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt index 1d51f74194..6e9cf72bff 100644 --- a/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt +++ b/core/src/smoke-test/kotlin/net/corda/core/cordapp/CordappSmokeTest.kt @@ -38,7 +38,7 @@ import kotlin.streams.toList class CordappSmokeTest { private companion object { - val user = User("user1", "test", permissions = setOf("ALL")) + val superUser = User("superUser", "test", permissions = setOf("ALL")) val port = AtomicInteger(15100) } @@ -50,7 +50,7 @@ class CordappSmokeTest { rpcPort = port.andIncrement, rpcAdminPort = port.andIncrement, isNotary = true, - users = listOf(user) + users = listOf(superUser) ) private val aliceConfig = NodeConfig( @@ -59,7 +59,7 @@ class CordappSmokeTest { rpcPort = port.andIncrement, rpcAdminPort = port.andIncrement, isNotary = false, - users = listOf(user) + users = listOf(superUser) ) private lateinit var notary: NodeProcess @@ -92,7 +92,7 @@ class CordappSmokeTest { createDummyNodeInfo(additionalNodeInfoDir) factory.create(aliceConfig).use { alice -> - alice.connect().use { connectionToAlice -> + alice.connect(superUser).use { connectionToAlice -> val aliceIdentity = connectionToAlice.proxy.nodeInfo().legalIdentitiesAndCerts.first().party val future = connectionToAlice.proxy.startFlow(::GatherContextsFlow, aliceIdentity).returnValue val (sessionInitContext, sessionConfirmContext) = future.getOrThrow() diff --git a/docs/source/shell.rst b/docs/source/shell.rst index c6326b2c9f..c0cd525c37 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -26,9 +26,11 @@ Permissions When accessing the shell (embedded, standalone, via SSH) RPC permissions are required. This is because the shell actually communicates with the node using RPC calls. -* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed`` +* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed``. * Starting flows requires ``InvokeRpc.startTrackedFlowDynamic``, ``InvokeRpc.registeredFlows`` and ``InvokeRpc.wellKnownPartyFromX500Name``, as well as a - permission for the flow being started + permission for the flow being started. +* Killing flows (``flow kill``) requires ``InvokeRpc.killFlow``. This currently + allows the user to kill *any* flow, so please be careful when granting it! The shell via the local terminal -------------------------------- @@ -231,6 +233,7 @@ The shell also has special commands for working with flows: names and types of the arguments will be used to try and automatically determine which one to use. If the match against available constructors is unclear, the reasons each available constructor failed to match will be printed out. In the case of an ambiguous match, the first applicable constructor will be used +* ``flow kill`` kills a single flow, as identified by its UUID. Parameter syntax **************** diff --git a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt index 2c1e7af222..6b908086f8 100644 --- a/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt +++ b/node/src/main/kotlin/net/corda/node/internal/rpc/proxies/AuthenticatedRpcOpsProxy.kt @@ -39,14 +39,14 @@ internal class AuthenticatedRpcOpsProxy(private val delegate: CordaRPCOps) : Cor } private class PermissionsEnforcingInvocationHandler(override val delegate: CordaRPCOps, private val context: () -> RpcAuthContext) : InvocationHandlerTemplate { - override fun invoke(proxy: Any, method: Method, arguments: Array?) = guard(method.name, context, { super.invoke(proxy, method, arguments) }) + override fun invoke(proxy: Any, method: Method, arguments: Array?) = guard(method.name, context) { super.invoke(proxy, method, arguments) } } } private fun guard(methodName: String, context: () -> RpcAuthContext, action: () -> RESULT) = guard(methodName, emptyList(), context, action) private fun guard(methodName: String, args: List>, context: () -> RpcAuthContext, action: () -> RESULT): RESULT { - if (!context().isPermitted(methodName, *(args.map { it.name }.toTypedArray()))) { + if (!context().isPermitted(methodName, *(args.map(Class<*>::getName).toTypedArray()))) { throw PermissionException("User not authorized to perform RPC call $methodName with target $args") } else { return action() diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt index 5186e234b7..8088c8b4ec 100644 --- a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt @@ -26,7 +26,6 @@ import org.apache.shiro.realm.AuthorizingRealm import org.apache.shiro.realm.jdbc.JdbcRealm import org.apache.shiro.subject.PrincipalCollection import org.apache.shiro.subject.SimplePrincipalCollection -import java.io.Closeable import java.util.concurrent.ConcurrentHashMap import javax.security.auth.login.FailedLoginException @@ -66,7 +65,7 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig, cacheFactory: NamedCache manager = manager) override fun close() { - manager.realms?.filterIsInstance()?.forEach { it.close() } + manager.realms?.filterIsInstance()?.forEach(AutoCloseable::close) manager.destroy() } @@ -197,31 +196,31 @@ private fun buildCredentialMatcher(type: PasswordEncryption) = when (type) { } class InMemoryRealm(users: List, - realmId: String, - passwordEncryption: PasswordEncryption = PasswordEncryption.NONE) : AuthorizingRealm() { + realmId: String, + passwordEncryption: PasswordEncryption = PasswordEncryption.NONE) : AuthorizingRealm() { private val authorizationInfoByUser: Map private val authenticationInfoByUser: Map init { permissionResolver = RPCPermissionResolver - users.forEach { - require(it.username.matches("\\w+".toRegex())) { - "Username ${it.username} contains invalid characters" + users.forEach { user -> + require(user.username.matches("\\w+".toRegex())) { + "Username ${user.username} contains invalid characters" } } - val resolvePermission = { s: String -> permissionResolver.resolvePermission(s) } - authorizationInfoByUser = users.associate { - it.username to SimpleAuthorizationInfo().apply { - objectPermissions = it.permissions.map { resolvePermission(it) }.toSet() + val resolvePermission = permissionResolver::resolvePermission + authorizationInfoByUser = users.associate { user -> + user.username to SimpleAuthorizationInfo().apply { + objectPermissions = user.permissions.map(resolvePermission).toSet() roles = emptySet() stringPermissions = emptySet() } } - authenticationInfoByUser = users.associate { - it.username to SimpleAuthenticationInfo().apply { - credentials = it.password - principals = SimplePrincipalCollection(it.username, realmId) + authenticationInfoByUser = users.associate { user -> + user.username to SimpleAuthenticationInfo().apply { + credentials = user.password + principals = SimplePrincipalCollection(user.username, realmId) } } credentialsMatcher = buildCredentialMatcher(passwordEncryption) @@ -236,7 +235,7 @@ class InMemoryRealm(users: List, authorizationInfoByUser[principals.primaryPrincipal as String] } -private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource) : JdbcRealm(), Closeable { +private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource) : JdbcRealm(), AutoCloseable { init { credentialsMatcher = buildCredentialMatcher(config.passwordEncryption) @@ -246,7 +245,7 @@ private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource } override fun close() { - (dataSource as? Closeable)?.close() + (dataSource as? AutoCloseable)?.close() } } diff --git a/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt index 64ed341993..972272370a 100644 --- a/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt @@ -7,6 +7,7 @@ import net.corda.node.internal.security.Password import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.tryAuthenticate import net.corda.node.services.Permissions.Companion.invokeRpc +import net.corda.node.services.Permissions.Companion.startFlow import net.corda.node.services.config.SecurityConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.internal.TestingNamedCacheFactory @@ -31,7 +32,7 @@ class RPCSecurityManagerTest { @Test fun `Generic RPC call authorization`() { checkUserActions( - permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")), + permitted = setOf(listOf("nodeInfo"), listOf("notaryIdentities")), permissions = setOf( invokeRpc(CordaRPCOps::nodeInfo), invokeRpc(CordaRPCOps::notaryIdentities))) @@ -40,41 +41,49 @@ class RPCSecurityManagerTest { @Test fun `Flow invocation authorization`() { checkUserActions( - permissions = setOf(Permissions.startFlow()), + permissions = setOf(startFlow()), permitted = setOf( - arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"), - arrayListOf("startFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"))) + listOf("startTrackedFlowDynamic", DummyFlow::class.java.name), + listOf("startFlowDynamic", DummyFlow::class.java.name))) } @Test fun `Check startFlow RPC permission implies startFlowDynamic`() { checkUserActions( permissions = setOf(invokeRpc("startFlow")), - permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic"))) + permitted = setOf(listOf("startFlow"), listOf("startFlowDynamic"))) } @Test fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() { checkUserActions( - permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")), + permitted = setOf(listOf("startTrackedFlow"), listOf("startTrackedFlowDynamic")), permissions = setOf(invokeRpc("startTrackedFlow"))) } + @Test + fun `check killFlow RPC permission accepted`() { + checkUserActions( + permitted = setOf(listOf("killFlow")), + permissions = setOf(invokeRpc(CordaRPCOps::killFlow)) + ) + } + @Test fun `Admin authorization`() { checkUserActions( permissions = setOf("all"), - permitted = allActions.map { arrayListOf(it) }.toSet()) + permitted = allActions.map { listOf(it) }.toSet()) } @Test fun `flows draining mode permissions`() { checkUserActions( - permitted = setOf(arrayListOf("setFlowsDrainingModeEnabled")), + permitted = setOf(listOf("setFlowsDrainingModeEnabled")), permissions = setOf(invokeRpc(CordaRPCOps::setFlowsDrainingModeEnabled)) ) checkUserActions( - permitted = setOf(arrayListOf("isFlowsDrainingModeEnabled")), + permitted = setOf(listOf("isFlowsDrainingModeEnabled")), permissions = setOf(invokeRpc(CordaRPCOps::isFlowsDrainingModeEnabled)) ) } @@ -134,7 +143,7 @@ class RPCSecurityManagerTest { users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST")) } - private fun checkUserActions(permissions: Set, permitted: Set>) { + private fun checkUserActions(permissions: Set, permitted: Set>) { val user = User(username = "user", password = "password", permissions = permissions) val userRealms = RPCSecurityManagerImpl(SecurityConfiguration.AuthService.fromUsers(listOf(user)), TestingNamedCacheFactory()) val disabled = allActions.filter { !permitted.contains(listOf(it)) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 1a0a0a9dff..bda6cbe7c5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -314,12 +314,10 @@ open class MockServices private constructor( constructor(initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService()) : this(listOf(getCallerPackage(MockServices::class)!!), TestIdentity(initialIdentityName), identityService) - @JvmOverloads constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService, networkParameters: NetworkParameters) : this(cordappPackages, TestIdentity(initialIdentityName), identityService, networkParameters) - @JvmOverloads constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService, networkParameters: NetworkParameters, key: KeyPair) : this(cordappPackages, TestIdentity(initialIdentityName, key), identityService, networkParameters) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index b080857285..14bbe399f8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -695,7 +695,8 @@ class DriverDSLImpl( Permissions.invokeRpc(CordaRPCOps::internalFindVerifiedTransaction), Permissions.invokeRpc("vaultQueryBy"), Permissions.invokeRpc("vaultTrackBy"), - Permissions.invokeRpc(CordaRPCOps::registeredFlows) + Permissions.invokeRpc(CordaRPCOps::registeredFlows), + Permissions.invokeRpc(CordaRPCOps::killFlow) ) private fun oneOf(array: Array) = array[Random().nextInt(array.size)] diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 7f4034ce0f..79603dbefa 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -9,6 +9,7 @@ import net.corda.core.node.NotaryInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.asContextEnv import net.corda.testing.common.internal.checkNotOnClasspath @@ -33,8 +34,7 @@ class NodeProcess( private val log = contextLogger() } - fun connect(): CordaRPCConnection { - val user = config.users[0] + fun connect(user: User): CordaRPCConnection { return client.start(user.username, user.password) } diff --git a/tools/shell/build.gradle b/tools/shell/build.gradle index 36ee942432..8fef472421 100644 --- a/tools/shell/build.gradle +++ b/tools/shell/build.gradle @@ -22,11 +22,6 @@ sourceSets { srcDir file('src/integration-test/resources') } } - test { - resources { - srcDir file('src/test/resources') - } - } } dependencies { diff --git a/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java index 53bb921d3d..c5bbffe9e4 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/FlowShellCommand.java @@ -15,12 +15,12 @@ import org.slf4j.LoggerFactory; import java.util.*; -import static java.util.stream.Collectors.joining; +import static net.corda.tools.shell.InteractiveShell.killFlowById; import static net.corda.tools.shell.InteractiveShell.runFlowByNameFragment; import static net.corda.tools.shell.InteractiveShell.runStateMachinesView; @Man( - "Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" + + "Allows you to start and kill flows, list the ones available and to watch flows currently running on the node.\n\n" + "Starting flow is the primary way in which you command the node to change the ledger.\n\n" + "This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + "command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" + @@ -28,7 +28,7 @@ import static net.corda.tools.shell.InteractiveShell.runStateMachinesView; ) public class FlowShellCommand extends InteractiveShellCommand { - private static Logger logger = LoggerFactory.getLogger(FlowShellCommand.class); + private static final Logger logger = LoggerFactory.getLogger(FlowShellCommand.class); @Command @Usage("Start a (work)flow on the node. This is how you can change the ledger.") @@ -36,13 +36,13 @@ public class FlowShellCommand extends InteractiveShellCommand { @Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name, @Usage("The data to pass as input") @Argument(unquote = false) List input ) { - logger.info("Executing command \"flow start {} {}\",", name, (input != null) ? input.stream().collect(joining(" ")) : ""); + logger.info("Executing command \"flow start {} {}\",", name, (input != null) ? String.join(" ", input) : ""); startFlow(name, input, out, ops(), ansiProgressRenderer(), objectMapper(null)); } // TODO Limit number of flows shown option? @Command - @Usage("watch information about state machines running on the node with result information") + @Usage("Watch information about state machines running on the node with result information.") public void watch(InvocationContext context) throws Exception { logger.info("Executing command \"flow watch\"."); runStateMachinesView(out, ops()); @@ -63,11 +63,20 @@ public class FlowShellCommand extends InteractiveShellCommand { } @Command - @Usage("list flows that user can start") + @Usage("List flows that user can start.") public void list(InvocationContext context) throws Exception { logger.info("Executing command \"flow list\"."); for (String name : ops().registeredFlows()) { context.provide(name + System.lineSeparator()); } } + + @Command + @Usage("Kill a flow that is running on this node.") + public void kill( + @Usage("The UUID for the flow that we wish to kill") @Argument String id + ) { + logger.info("Executing command \"flow kill {}\".", id); + killFlowById(id, out, ops(), objectMapper(null)); + } } \ No newline at end of file diff --git a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java index 87f471092e..c5ac48399b 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/RunShellCommand.java @@ -1,6 +1,5 @@ package net.corda.tools.shell; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import net.corda.client.jackson.StringToMethodCallParser; @@ -20,14 +19,13 @@ import java.util.Map; import java.util.Set; import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.joining; // Note that this class cannot be converted to Kotlin because CRaSH does not understand InvocationContext> which // is the closest you can get in Kotlin to raw types. public class RunShellCommand extends InteractiveShellCommand { - private static Logger logger = LoggerFactory.getLogger(RunShellCommand.class); + private static final Logger logger = LoggerFactory.getLogger(RunShellCommand.class); @Command @Man( @@ -39,7 +37,7 @@ public class RunShellCommand extends InteractiveShellCommand { @Usage("runs a method from the CordaRPCOps interface on the node.") public Object main(InvocationContext context, @Usage("The command to run") @Argument(unquote = false) List command) { - logger.info("Executing command \"run {}\",", (command != null) ? command.stream().collect(joining(" ")) : ""); + logger.info("Executing command \"run {}\",", (command != null) ? String.join(" ", command) : ""); StringToMethodCallParser parser = new StringToMethodCallParser<>(CordaRPCOps.class, objectMapper(InteractiveShell.getCordappsClassloader())); if (command == null) { diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index bff6a27ece..86444f179f 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -1,6 +1,7 @@ package net.corda.tools.shell import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.module.SimpleModule @@ -16,6 +17,7 @@ import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.UniqueIdentifier import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.* import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.openFuture @@ -348,6 +350,30 @@ object InteractiveShell { return innerLoop(type) } + @JvmStatic + fun killFlowById(id: String, + output: RenderPrintWriter, + rpcOps: CordaRPCOps, + inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) { + try { + val runId = try { + inputObjectMapper.readValue(id, StateMachineRunId::class.java) + } catch (e: JsonMappingException) { + output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Color.red) + log.error("Failed to parse flow ID", e) + return + } + + if (rpcOps.killFlow(runId)) { + output.println("Killed flow $runId", Color.yellow) + } else { + output.println("Failed to kill flow $runId", Color.red) + } + } finally { + output.flush() + } + } + // TODO: This utility is generally useful and might be better moved to the node class, or an RPC, if we can commit to making it stable API. /** * Given a [FlowLogic] class and a string in one-line Yaml form, finds an applicable constructor and starts diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt index 045e3add54..df52553a74 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.type.TypeFactory import com.fasterxml.jackson.dataformat.yaml.YAMLFactory -import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever @@ -30,6 +30,7 @@ import net.corda.testing.core.TestIdentity import net.corda.testing.core.getTestPartyAndCertificate import net.corda.testing.internal.DEV_ROOT_CA import org.crsh.command.InvocationContext +import org.crsh.text.Color import org.crsh.text.RenderPrintWriter import org.junit.Before import org.junit.Test @@ -128,7 +129,7 @@ class InteractiveShellTest { } private fun objectMapperWithClassLoader(classLoader: ClassLoader?): ObjectMapper { - val objectMapper = ObjectMapper() + val objectMapper = JacksonSupport.createNonRpcMapper() val tf = TypeFactory.defaultInstance().withClassLoader(classLoader) objectMapper.typeFactory = tf @@ -231,6 +232,34 @@ class InteractiveShellTest { verify(printWriter).println(NETWORK_MAP_JSON_PAYLOAD.replace("\n", System.lineSeparator())) } + @Test + fun killFlowWithNonsenseID() { + InteractiveShell.killFlowById("nonsense", printWriter, cordaRpcOps, om) + verify(printWriter).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Color.red) + verify(printWriter).flush() + } + + @Test + fun killFlowFailure() { + val runId = StateMachineRunId.createRandom() + whenever(cordaRpcOps.killFlow(any())).thenReturn(false) + + InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om) + verify(cordaRpcOps).killFlow(runId) + verify(printWriter).println("Failed to kill flow $runId", Color.red) + verify(printWriter).flush() + } + + @Test + fun killFlowSuccess() { + val runId = StateMachineRunId.createRandom() + whenever(cordaRpcOps.killFlow(any())).thenReturn(true) + + InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om) + verify(cordaRpcOps).killFlow(runId) + verify(printWriter).println("Killed flow $runId", Color.yellow) + verify(printWriter).flush() + } } @ToStringSerialize