mirror of
https://github.com/corda/corda.git
synced 2025-06-15 21:58:17 +00:00
[CORDA-792] Standalone Shell (#2663)
- Existing embedded Shell connects via RPC including checking RPC user credentials (before was a direct use of CordaRPCOps): in dev mode when console terminal is enabled, node created `shell` user. - New Standalone Shell app with the same functionalities as Shell: connects to a node via RPC Client, can use SSL and run SSH server.
This commit is contained in:
@ -0,0 +1,239 @@
|
||||
package net.corda.tools.shell
|
||||
|
||||
import com.google.common.io.Files
|
||||
import com.jcraft.jsch.ChannelExec
|
||||
import com.jcraft.jsch.JSch
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.testing.common.internal.withCertificates
|
||||
import net.corda.testing.common.internal.withKeyStores
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.driver.internal.RandomFree
|
||||
import net.corda.testing.internal.useSslRpcOverrides
|
||||
import net.corda.testing.node.User
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.bouncycastle.util.io.Streams
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class InteractiveShellIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `shell should not log in with invalid credentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = "fake", password = "fake",
|
||||
hostAndPort = node.rpcAddress)
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell should log in with valid crentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
driver {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
InteractiveShell.nodeInfo()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell should log in with ssl`() {
|
||||
val user = User("mark", "dadada", setOf(all()))
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["shell"] = markCertificate
|
||||
|
||||
client.keyStore["shell"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
|
||||
var successful = false
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
|
||||
val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword,
|
||||
clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword)
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
ssl = sslConfiguration)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
InteractiveShell.nodeInfo()
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell shoud not log in without ssl keystore`() {
|
||||
val user = User("mark", "dadada", setOf("ALL"))
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["shell"] = markCertificate
|
||||
|
||||
//client key store doesn't have "mark" certificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
|
||||
val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword,
|
||||
clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword)
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
ssl = sslConfiguration)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
|
||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQNotConnectedException::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ssh runs flows via standalone shell`() {
|
||||
val user = User("u", "p", setOf(Permissions.startFlow<SSHServerTest.FlowICanRun>(),
|
||||
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)))
|
||||
driver {
|
||||
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
|
||||
val node = nodeFuture.getOrThrow()
|
||||
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
sshdPort = 2224)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
InteractiveShell.nodeInfo()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2224)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("p")
|
||||
session.connect()
|
||||
|
||||
assertTrue(session.isConnected)
|
||||
|
||||
val channel = session.openChannel("exec") as ChannelExec
|
||||
channel.setCommand("start FlowICanRun")
|
||||
channel.connect(5000)
|
||||
|
||||
assertTrue(channel.isConnected)
|
||||
|
||||
val response = String(Streams.readAll(channel.inputStream))
|
||||
|
||||
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
|
||||
|
||||
channel.disconnect()
|
||||
session.disconnect()
|
||||
|
||||
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
|
||||
assertThat(linesWithDoneCount).hasSize(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ssh run flows via standalone shell over ssl to node`() {
|
||||
val user = User("mark", "dadada", setOf(Permissions.startFlow<SSHServerTest.FlowICanRun>(),
|
||||
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/))
|
||||
withCertificates { server, client, createSelfSigned, createSignedBy ->
|
||||
val rootCertificate = createSelfSigned(CordaX500Name("SystemUsers/Node", "IT", "R3 London", "London", "London", "GB"))
|
||||
val markCertificate = createSignedBy(CordaX500Name("shell", "IT", "R3 London", "London", "London", "GB"), rootCertificate)
|
||||
|
||||
// truststore needs to contain root CA for how the driver works...
|
||||
server.keyStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["cordaclienttls"] = rootCertificate
|
||||
server.trustStore["shell"] = markCertificate
|
||||
|
||||
client.keyStore["shell"] = markCertificate
|
||||
client.trustStore["cordaclienttls"] = rootCertificate
|
||||
|
||||
withKeyStores(server, client) { nodeSslOptions, clientSslOptions ->
|
||||
var successful = false
|
||||
driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = RandomFree)) {
|
||||
startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
||||
|
||||
val sslConfiguration = ShellSslOptions(clientSslOptions.sslKeystore, clientSslOptions.keyStorePassword,
|
||||
clientSslOptions.trustStoreFile, clientSslOptions.trustStorePassword)
|
||||
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
|
||||
user = user.username, password = user.password,
|
||||
hostAndPort = node.rpcAddress,
|
||||
ssl = sslConfiguration,
|
||||
sshdPort = 2223)
|
||||
|
||||
InteractiveShell.startShell(conf)
|
||||
InteractiveShell.nodeInfo()
|
||||
|
||||
val session = JSch().getSession("mark", "localhost", 2223)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("dadada")
|
||||
session.connect()
|
||||
|
||||
assertTrue(session.isConnected)
|
||||
|
||||
val channel = session.openChannel("exec") as ChannelExec
|
||||
channel.setCommand("start FlowICanRun")
|
||||
channel.connect(5000)
|
||||
|
||||
assertTrue(channel.isConnected)
|
||||
|
||||
val response = String(Streams.readAll(channel.inputStream))
|
||||
|
||||
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
|
||||
|
||||
channel.disconnect()
|
||||
session.disconnect() // TODO Simon make sure to close them
|
||||
|
||||
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
|
||||
assertThat(linesWithDoneCount).hasSize(1)
|
||||
|
||||
successful = true
|
||||
}
|
||||
}
|
||||
assertThat(successful).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
package net.corda.tools.shell
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.jcraft.jsch.ChannelExec
|
||||
import com.jcraft.jsch.JSch
|
||||
import com.jcraft.jsch.JSchException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.driver.driver
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.bouncycastle.util.io.Streams
|
||||
import org.junit.Test
|
||||
import java.net.ConnectException
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
class SSHServerTest {
|
||||
|
||||
@Test()
|
||||
fun `ssh server does not start be default`() {
|
||||
val user = User("u", "p", setOf())
|
||||
// The driver will automatically pick up the annotated flows below
|
||||
driver {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user))
|
||||
node.getOrThrow()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2222)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("p")
|
||||
|
||||
try {
|
||||
session.connect()
|
||||
fail()
|
||||
} catch (e:JSchException) {
|
||||
assertTrue(e.cause is ConnectException)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ssh server starts when configured`() {
|
||||
val user = User("u", "p", setOf())
|
||||
// The driver will automatically pick up the annotated flows below
|
||||
driver {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)))
|
||||
node.getOrThrow()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2222)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("p")
|
||||
|
||||
session.connect()
|
||||
|
||||
assertTrue(session.isConnected)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ssh server verify credentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
// The driver will automatically pick up the annotated flows below
|
||||
driver {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)))
|
||||
node.getOrThrow()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2222)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("p_is_bad_password")
|
||||
|
||||
try {
|
||||
session.connect()
|
||||
fail("Server should reject invalid credentials")
|
||||
} catch (e: JSchException) {
|
||||
//There is no specialized exception for this
|
||||
assertTrue(e.message == "Auth fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ssh respects permissions`() {
|
||||
val user = User("u", "p", setOf(startFlow<FlowICanRun>(),
|
||||
invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name)))
|
||||
// The driver will automatically pick up the annotated flows below
|
||||
driver(DriverParameters(isDebug = true)) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)))
|
||||
node.getOrThrow()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2222)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("p")
|
||||
session.connect()
|
||||
|
||||
assertTrue(session.isConnected)
|
||||
|
||||
val channel = session.openChannel("exec") as ChannelExec
|
||||
channel.setCommand("start FlowICannotRun otherParty: \"$ALICE_NAME\"")
|
||||
channel.connect()
|
||||
val response = String(Streams.readAll(channel.inputStream))
|
||||
|
||||
channel.disconnect()
|
||||
session.disconnect()
|
||||
|
||||
assertThat(response).matches("(?s)User not authorized to perform RPC call .*")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ssh runs flows`() {
|
||||
val user = User("u", "p", setOf(startFlow<FlowICanRun>()))
|
||||
// The driver will automatically pick up the annotated flows below
|
||||
driver(DriverParameters(isDebug = true)) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)))
|
||||
node.getOrThrow()
|
||||
|
||||
val session = JSch().getSession("u", "localhost", 2222)
|
||||
session.setConfig("StrictHostKeyChecking", "no")
|
||||
session.setPassword("p")
|
||||
session.connect()
|
||||
|
||||
assertTrue(session.isConnected)
|
||||
|
||||
val channel = session.openChannel("exec") as ChannelExec
|
||||
channel.setCommand("start FlowICanRun")
|
||||
channel.connect(5000)
|
||||
|
||||
assertTrue(channel.isConnected)
|
||||
|
||||
val response = String(Streams.readAll(channel.inputStream))
|
||||
|
||||
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
|
||||
|
||||
channel.disconnect()
|
||||
session.disconnect()
|
||||
|
||||
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
|
||||
assertThat(linesWithDoneCount).hasSize(1)
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class FlowICanRun : FlowLogic<String>() {
|
||||
|
||||
private val HELLO_STEP = ProgressTracker.Step("Hello")
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
progressTracker?.currentStep = HELLO_STEP
|
||||
return "bambam"
|
||||
}
|
||||
|
||||
override val progressTracker: ProgressTracker? = ProgressTracker(HELLO_STEP)
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class FlowICannotRun(val otherParty: Party) : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String = initiateFlow(otherParty).receive<String>().unwrap { it }
|
||||
|
||||
override val progressTracker: ProgressTracker? = ProgressTracker()
|
||||
}
|
||||
}
|
8
tools/shell/src/integration-test/resources/ssl.conf
Normal file
8
tools/shell/src/integration-test/resources/ssl.conf
Normal file
@ -0,0 +1,8 @@
|
||||
user=demo1
|
||||
baseDirectory="/Users/szymonsztuka/Documents/shell-config"
|
||||
hostAndPort="localhost:10006"
|
||||
sshdPort=2223
|
||||
ssl {
|
||||
keyStorePassword=password
|
||||
trustStorePassword=password
|
||||
}
|
Reference in New Issue
Block a user