Open Source
This commit is contained in:
Nick Dunstone 2019-12-03 10:28:00 +00:00 committed by Rick Parker
parent 96e6313fbd
commit e6f9b46584
18 changed files with 156 additions and 44 deletions

View File

@ -102,7 +102,7 @@ buildscript {
ext.dependency_checker_version = '5.2.0'
ext.commons_collections_version = '4.3'
ext.beanutils_version = '1.9.3'
ext.crash_version = '810d2b774b85d4938be01b9b65e29e4fddbc450b'
ext.crash_version = '1.7.1'
ext.jsr305_version = constants.getProperty("jsr305Version")
ext.shiro_version = '1.4.1'
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')

View File

@ -6,6 +6,7 @@ import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.client.rpc.CordaRPCConnection
import net.corda.client.rpc.GracefulReconnect
import net.corda.client.rpc.MaxRpcRetryException
import net.corda.client.rpc.PermissionException
import net.corda.client.rpc.RPCConnection
import net.corda.client.rpc.RPCException
import net.corda.client.rpc.internal.ReconnectingCordaRPCOps.ReconnectingRPCConnection.CurrentState.CLOSED
@ -230,8 +231,12 @@ class ReconnectingCordaRPCOps private constructor(
// Deliberately not logging full stack trace as it will be full of internal stacktraces.
log.debug { "Exception upon establishing connection: ${ex.message}" }
}
is PermissionException -> {
// Deliberately not logging full stack trace as it will be full of internal stacktraces.
log.debug { "Permission Exception establishing connection: ${ex.message}" }
}
else -> {
log.warn("Unknown exception upon establishing connection.", ex)
log.warn("Unknown exception [${ex.javaClass.name}] upon establishing connection.", ex)
}
}

View File

@ -185,6 +185,18 @@ To run SSH server use ``--sshd-port`` option when starting standalone shell or `
For connection to SSH refer to `Connecting to the shell`_.
Certain operations (like starting Flows) will require Shell's ``--cordpass-directory`` to be configured correctly (see `Starting the standalone shell`_).
Shell Safe Mode
---------------
This is a new mode added in the Enterprise 4.3 release to prevent the Crash shell embedded commands (e.g. 'java', 'system') from being
executed by a user with insufficient privilege. This is part of a general security tightening initiative.
When a shell is running in unsafe mode, the shell behaviour will be the same as before and will include Crash built in commands. By default
the internal shell will run in safe mode but will still be have the ability to execute RPC client calls as before based on
existing RPC permissions. No Corda functionality is affected by this change; only the ability to access to the Crash shell embedded commands.
When running an SSH shell, it will run in safe mode for any user that does not explicitly have permission 'ALL' as one the items
in their RPC permission list, see :doc:`tutorial-clientrpc-api` for more information about the RPC Client API. These shell changes are
also applied to the Stand Alone shell which will now run in safe mode (Enterprise 4.3 onwards). It may be possible that, in the future,
the Crash shell embedded commands may become deprecated. Where possible, please do not write any new code that depends on them as they
are technically not part of Corda functionality.
Interacting with the node via the shell
---------------------------------------
@ -209,7 +221,7 @@ You can shut the node down via shell:
* ``run shutdown`` will shut the node down immediately
Output Formats
**********************
**************
You can choose the format in which the output of the commands will be shown.
@ -377,6 +389,13 @@ list the supported subcommands.
Extending the shell
-------------------
THIS FUNCTIONALITY IS NOW ONLY AVAILABLE WHEN RUNNING THE SHELL IN UNSAFE MODE (see Safe Shell section above). This is because of possible
security vulnerabilities caused by the Crash shell embedded commands. When shell commands are executed via SSH, a remote user has the ability
to effect the node internal state (including running scripts, gc and even shutting down the node). Prior to Enterprise 4.3 this was possible
regardless of the user's permissions. A user must now have 'ALL' as one of their RPC Permissions to be able to use the embedded commands via
the unsafe shell. You are advised, where possible to design out dependencies on the Crash shell embedded commands (non-Corda commands)
and, as a minimum, not to introduce any new dependencies on the unsafe shell environment.
The shell can be extended using commands written in either Java or `Groovy`_ (a Java-compatible scripting language).
These commands have full access to the node's internal APIs and thus can be used to achieve almost anything.

View File

@ -25,11 +25,6 @@ class ArtemisMessagingComponent {
const val NODE_P2P_USER = "SystemUsers/Node"
const val NODE_RPC_USER = "SystemUsers/NodeRPC"
const val PEER_USER = "SystemUsers/Peer"
// User used only in devMode when nodes have a shell attached by default.
const val INTERNAL_SHELL_USER = "internalShell"
val internalShellPassword: String by lazy { SecureHash.randomSHA256().toString() }
const val INTERNAL_PREFIX = "internal."
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue
const val P2P_PREFIX = "p2p.inbound."

View File

@ -95,6 +95,7 @@ import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.config.rpc.NodeRpcOptions
import net.corda.node.services.config.shell.determineUnsafeUsers
import net.corda.node.services.config.shell.toShellConfig
import net.corda.node.services.config.shouldInitCrashShell
import net.corda.node.services.events.NodeSchedulerService
@ -126,9 +127,7 @@ import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import net.corda.node.services.statemachine.FlowMonitor
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.SingleThreadedStateMachineManager
import net.corda.node.services.statemachine.StaffedFlowHospital
import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.services.statemachine.StateMachineManagerInternal
import net.corda.node.services.transactions.BasicVerifierFactoryService
import net.corda.node.services.transactions.DeterministicVerifierFactoryService
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
@ -534,6 +533,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
shellConfiguration.sshdPort?.let {
log.info("Binding Shell SSHD server on port $it.")
}
val unsafeUsers = determineUnsafeUsers(configuration)
org.crsh.ssh.term.CRaSHCommand.setUserInfo(unsafeUsers, true, false)
log.info("Setting unsafe users as: ${unsafeUsers}")
InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader)
}
}

View File

@ -55,6 +55,8 @@ import net.corda.node.services.config.JmxReporterType
import net.corda.node.services.config.MB
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.SecurityConfiguration
import net.corda.node.services.config.shell.INTERNAL_SHELL_USER
import net.corda.node.services.config.shell.internalShellPassword
import net.corda.node.services.config.shouldInitCrashShell
import net.corda.node.services.config.shouldStartLocalShell
import net.corda.node.services.messaging.ArtemisMessagingServer
@ -72,7 +74,6 @@ import net.corda.node.utilities.DemoClock
import net.corda.node.utilities.errorAndTerminate
import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER
import net.corda.nodeapi.internal.ShutdownHook
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.bridging.BridgeControlListener
@ -353,7 +354,7 @@ open class Node(configuration: NodeConfiguration,
val securityManager = with(RPCSecurityManagerImpl(securityManagerConfig, cacheFactory)) {
if (configuration.shouldStartLocalShell()) RPCSecurityManagerWithAdditionalUser(this,
User(INTERNAL_SHELL_USER, ArtemisMessagingComponent.internalShellPassword, setOf(Permissions.all()))) else this
User(INTERNAL_SHELL_USER, internalShellPassword, setOf(Permissions.all()))) else this
}
val messageBroker = if (!configuration.messagingServerExternal) {

View File

@ -53,6 +53,8 @@ interface NodeConfiguration {
val lazyBridgeStart: Boolean
val detectPublicIp: Boolean get() = false
val sshd: SSHDConfiguration?
val localShellAllowExitInSafeMode: Boolean
val localShellUnsafe: Boolean
val database: DatabaseConfig
val noLocalShell: Boolean get() = false
val transactionCacheSizeBytes: Long get() = defaultTransactionCacheSize

View File

@ -59,6 +59,8 @@ data class NodeConfigurationImpl(
// TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration
override val additionalNodeInfoPollingFrequencyMsec: Long = Defaults.additionalNodeInfoPollingFrequencyMsec,
override val sshd: SSHDConfiguration? = Defaults.sshd,
override val localShellAllowExitInSafeMode: Boolean = Defaults.localShellAllowExitInSafeMode,
override val localShellUnsafe: Boolean = Defaults.localShellUnsafe,
override val database: DatabaseConfig = Defaults.database(devMode),
private val transactionCacheSizeMegaBytes: Int? = Defaults.transactionCacheSizeMegaBytes,
private val attachmentContentCacheSizeMegaBytes: Int? = Defaults.attachmentContentCacheSizeMegaBytes,
@ -98,6 +100,8 @@ data class NodeConfigurationImpl(
const val detectPublicIp: Boolean = false
val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis()
val sshd: SSHDConfiguration? = null
const val localShellAllowExitInSafeMode: Boolean = true
const val localShellUnsafe: Boolean = false
val transactionCacheSizeMegaBytes: Int? = null
val attachmentContentCacheSizeMegaBytes: Int? = null
const val attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound

View File

@ -35,6 +35,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
private val lazyBridgeStart by boolean().optional().withDefaultValue(Defaults.lazyBridgeStart)
private val detectPublicIp by boolean().optional().withDefaultValue(Defaults.detectPublicIp)
private val sshd by nested(SSHDConfigurationSpec).optional()
private val localShellAllowExitInSafeMode by boolean().optional().withDefaultValue(Defaults.localShellAllowExitInSafeMode)
private val localShellUnsafe by boolean().optional().withDefaultValue(Defaults.localShellUnsafe)
private val database by nested(DatabaseConfigSpec).optional()
private val noLocalShell by boolean().optional().withDefaultValue(Defaults.noLocalShell)
private val attachmentCacheBound by long().optional().withDefaultValue(Defaults.attachmentCacheBound)
@ -103,6 +105,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
lazyBridgeStart = configuration[lazyBridgeStart],
detectPublicIp = configuration[detectPublicIp],
sshd = configuration[sshd],
localShellAllowExitInSafeMode = configuration[localShellAllowExitInSafeMode],
localShellUnsafe = configuration[localShellUnsafe],
database = database,
noLocalShell = configuration[noLocalShell],
attachmentCacheBound = configuration[attachmentCacheBound],

View File

@ -3,8 +3,6 @@ package net.corda.node.services.config.shell
import net.corda.core.internal.div
import net.corda.node.internal.clientSslOptionsCompatibleWith
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER
import net.corda.tools.shell.ShellConfiguration
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR
@ -15,7 +13,10 @@ fun NodeConfiguration.toShellConfig() = ShellConfiguration(
commandsDirectory = this.baseDirectory / COMMANDS_DIR,
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
user = INTERNAL_SHELL_USER,
password = ArtemisMessagingComponent.internalShellPassword,
password = internalShellPassword,
permissions = internalShellPermissions(!this.localShellUnsafe),
localShellAllowExitInSafeMode = this.localShellAllowExitInSafeMode,
localShellUnsafe = this.localShellUnsafe,
hostAndPort = this.rpcOptions.address,
ssl = clientSslOptionsCompatibleWith(this.rpcOptions),
sshdPort = this.sshd?.port,

View File

@ -0,0 +1,53 @@
package net.corda.node.services.config.shell
import net.corda.core.crypto.SecureHash
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.config.User
const val SAFE_INTERNAL_SHELL_PERMISSION = ""
const val UNSAFE_INTERNAL_SHELL_PERMISSION = "ALL"
const val INTERNAL_SHELL_USER = "internalShell"
val internalShellPassword: String by lazy { SecureHash.randomSHA256().toString() }
fun internalShellPermissions(safe: Boolean): Set<String> {
return setOf(if (safe) { SAFE_INTERNAL_SHELL_PERMISSION } else { UNSAFE_INTERNAL_SHELL_PERMISSION })
}
fun determineUnsafeUsers(config: NodeConfiguration): Set<String> {
var unsafeUsers = HashSet<String>()
for (user in config.rpcUsers) {
for (perm in user.permissions) {
if (perm == UNSAFE_INTERNAL_SHELL_PERMISSION) {
unsafeUsers.add(user.username)
break
}
}
}
checkSecurityUsers(config, unsafeUsers)
return unsafeUsers
}
private fun checkSecurityPermListForUser(user: User, unsafeUsers: MutableSet<String>) {
for (perm in user.permissions) {
if (perm == UNSAFE_INTERNAL_SHELL_PERMISSION) {
unsafeUsers.add(user.username)
return
}
}
}
private fun checkSecurityForUserList(users: List<User>, unsafeUsers: MutableSet<String>) {
for (user in users) {
checkSecurityPermListForUser(user, unsafeUsers)
}
}
private fun checkSecurityUsers(config: NodeConfiguration, unsafeUsers: MutableSet<String>) {
val users = config.security?.authService?.dataSource?.users
if (users != null) {
checkSecurityForUserList(users, unsafeUsers)
}
}

View File

@ -4,6 +4,7 @@ import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.internal.artemis.BrokerJaasLoginModule
import net.corda.node.internal.artemis.SecureArtemisConfiguration
import net.corda.node.services.config.shell.INTERNAL_SHELL_USER
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent
@ -49,7 +50,7 @@ internal class RpcBrokerConfiguration(baseDirectory: Path, maxMessageSize: Int,
val nodeInternalRole = Role(BrokerJaasLoginModule.NODE_RPC_ROLE, true, true, true, true, true, true, true, true, true, true)
val addRPCRoleToUsers = if (shouldStartLocalShell) listOf(ArtemisMessagingComponent.INTERNAL_SHELL_USER) else emptyList()
val addRPCRoleToUsers = if (shouldStartLocalShell) listOf(INTERNAL_SHELL_USER) else emptyList()
val rolesAdderOnLogin = RolesAdderOnLogin(addRPCRoleToUsers) { username ->
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#" to setOf(nodeInternalRole, restrictedRole(
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username",

View File

@ -35,12 +35,12 @@ dependencies {
compile project(':client:jackson')
// CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy.
compile("com.github.corda.crash:crash.shell:$crash_version") {
compile("org.crashub:crash.shell:$crash_version") {
exclude group: "org.slf4j", module: "slf4j-jdk14"
exclude group: "org.bouncycastle"
}
compile("com.github.corda.crash:crash.connectors.ssh:$crash_version") {
compile("org.crashub:crash.connectors.ssh:$crash_version") {
exclude group: "org.slf4j", module: "slf4j-jdk14"
exclude group: "org.bouncycastle"
}

View File

@ -55,7 +55,7 @@ public class FlowShellCommand extends InteractiveShellCommand {
ANSIProgressRenderer ansiProgressRenderer,
ObjectMapper om) {
if (name == null) {
out.println("You must pass a name for the flow, see 'man flow'", Color.red);
out.println("You must pass a name for the flow, see 'man flow'", Decoration.bold, Color.red);
return;
}
String inp = input == null ? "" : String.join(" ", input).trim();

View File

@ -8,6 +8,7 @@ import org.crsh.cli.Command;
import org.crsh.cli.Man;
import org.crsh.cli.Usage;
import org.crsh.text.Color;
import org.crsh.text.Decoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -27,7 +28,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
logger.info("Executing command \"hash-lookup\".");
if (txIdHash == null) {
out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Color.red);
out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Decoration.bold, Color.red);
return;
}
@ -38,7 +39,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
try {
txIdHashParsed = SecureHash.parse(txIdHash);
} catch (IllegalArgumentException e) {
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Color.red);
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Decoration.bold, Color.red);
return;
}
@ -53,7 +54,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
SecureHash found = match.get();
out.println("Found a matching transaction with Id: " + found.toString());
} else {
out.println("No matching transaction found", Color.red);
out.println("No matching transaction found", Decoration.bold, Color.red);
}
}
}

View File

@ -38,6 +38,7 @@ import net.corda.core.messaging.pendingFlowsCount
import net.corda.tools.shell.utlities.ANSIProgressRenderer
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
import org.crsh.command.InvocationContext
import org.crsh.command.ShellSafety
import org.crsh.console.jline.JLineProcessor
import org.crsh.console.jline.TerminalFactory
import org.crsh.console.jline.console.ConsoleReader
@ -50,6 +51,7 @@ import org.crsh.shell.Shell
import org.crsh.shell.ShellFactory
import org.crsh.shell.impl.command.ExternalResolver
import org.crsh.text.Color
import org.crsh.text.Decoration
import org.crsh.text.RenderPrintWriter
import org.crsh.util.InterruptHandler
import org.crsh.util.Utils
@ -87,6 +89,8 @@ import kotlin.concurrent.thread
// TODO: Resurrect or reimplement the mail plugin.
// TODO: Make it notice new shell commands added after the node started.
const val STANDALONE_SHELL_PERMISSION = "ALL"
@Suppress("MaxLineLength")
object InteractiveShell {
private val log = LoggerFactory.getLogger(javaClass)
@ -128,14 +132,21 @@ object InteractiveShell {
rpcConn = connection
connection.proxy as InternalCordaRPCOps
}
_startShell(configuration, classLoader)
launchShell(configuration, standalone, classLoader)
}
private fun _startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
private fun launchShell(configuration: ShellConfiguration, standalone: Boolean, classLoader: ClassLoader? = null) {
shellConfiguration = configuration
InteractiveShell.classLoader = classLoader
val runSshDaemon = configuration.sshdPort != null
var runShellInSafeMode = true
if (!standalone) {
log.info("launchShell: User=${configuration.user} perm=${configuration.permissions}")
log.info("Shell: PermitExit= ${configuration.localShellAllowExitInSafeMode}, UNSAFELOCAL=${configuration.localShellUnsafe}")
runShellInSafeMode = configuration.permissions?.filter { it.contains(STANDALONE_SHELL_PERMISSION); }?.isEmpty() != false
}
val config = Properties()
if (runSshDaemon) {
// Enable SSH access. Note: these have to be strings, even though raw object assignments also work.
@ -183,7 +194,14 @@ object InteractiveShell {
"Commands to extract information about checkpoints stored within the node",
CheckpointShellCommand::class.java
)
shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password)
val shellSafety = ShellSafety().apply {
setSafeShell(runShellInSafeMode)
setInternal(!standalone)
setStandAlone(standalone)
setAllowExitInSafeMode(configuration.localShellAllowExitInSafeMode || standalone)
}
shell = ShellLifecycle(configuration.commandsDirectory, shellSafety).start(config, configuration.user, configuration.password)
}
fun runLocalShell(onExit: () -> Unit = {}) {
@ -210,7 +228,7 @@ object InteractiveShell {
}
}
class ShellLifecycle(private val shellCommands: Path) : PluginLifeCycle() {
class ShellLifecycle(private val shellCommands: Path, private val shellSafety: ShellSafety) : PluginLifeCycle() {
fun start(config: Properties, localUserName: String = "", localUserPassword: String = ""): Shell {
val classLoader = this.javaClass.classLoader
val classpathDriver = ClassPathMountFactory(classLoader)
@ -243,7 +261,8 @@ object InteractiveShell {
this.config = config
start(context)
ops = makeRPCOps(rpcOps, localUserName, localUserPassword)
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops, StdoutANSIProgressRenderer))
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops,
StdoutANSIProgressRenderer), shellSafety)
}
}
@ -317,15 +336,15 @@ object InteractiveShell {
val matches = try {
rpcOps.registeredFlows().filter { nameFragment in it }
} catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red)
output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
return
}
if (matches.isEmpty()) {
output.println("No matching flow found, run 'flow list' to see your options.", Color.red)
output.println("No matching flow found, run 'flow list' to see your options.", Decoration.bold, Color.red)
return
} else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) {
output.println("Ambiguous name provided, please be more specific. Your options are:")
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) }
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Decoration.bold, Color.yellow) }
return
}
@ -364,10 +383,10 @@ object InteractiveShell {
}
output.println("Flow completed with result: ${stateObservable.returnValue.get()}")
} catch (e: NoApplicableConstructor) {
output.println("No matching constructor found:", Color.red)
e.errors.forEach { output.println("- $it", Color.red) }
output.println("No matching constructor found:", Decoration.bold, Color.red)
e.errors.forEach { output.println("- $it", Decoration.bold, Color.red) }
} catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red)
output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
} catch (e: ExecutionException) {
// ignoring it as already logged by the progress handler subscriber
} finally {
@ -421,7 +440,7 @@ object InteractiveShell {
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)
output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Decoration.bold, Color.red)
log.error("Failed to parse flow ID", e)
return
}
@ -429,14 +448,14 @@ object InteractiveShell {
if (id.length < uuidStringSize) {
val msg = "Can not kill the flow. Flow ID of '$id' seems to be malformed - a UUID should have $uuidStringSize characters. " +
"Expand the terminal window to see the full UUID value."
output.println(msg, Color.red)
output.println(msg, Decoration.bold, Color.red)
log.warn(msg)
return
}
if (rpcOps.killFlow(runId)) {
output.println("Killed flow $runId", Color.yellow)
output.println("Killed flow $runId", Decoration.bold, Color.yellow)
} else {
output.println("Failed to kill flow $runId", Color.red)
output.println("Failed to kill flow $runId", Decoration.bold, Color.red)
}
} finally {
output.flush()
@ -568,7 +587,7 @@ object InteractiveShell {
// The flow command provides better support and startFlow requires special handling anyway due to
// the generic startFlow RPC interface which offers no type information with which to parse the
// string form of the command.
out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Color.yellow)
out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Decoration.bold, Color.yellow)
return null
} else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) {
return gracefulShutdown(out, cordaRPCOps)
@ -603,12 +622,12 @@ object InteractiveShell {
}
}
} catch (e: StringToMethodCallParser.UnparseableCallException) {
out.println(e.message, Color.red)
out.println(e.message, Decoration.bold, Color.red)
if (e !is StringToMethodCallParser.UnparseableCallException.NoSuchFile) {
out.println("Please try 'man run' to learn what syntax is acceptable")
}
} catch (e: Exception) {
out.println("RPC failed: ${e.rootCause}", Color.red)
out.println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red)
} finally {
InputStreamSerializer.invokeContext = null
InputStreamDeserializer.closeAll()

View File

@ -9,13 +9,15 @@ data class ShellConfiguration(
val cordappsDirectory: Path? = null,
var user: String = "",
var password: String = "",
var permissions: Set<String>? = null,
var localShellAllowExitInSafeMode: Boolean = false,
var localShellUnsafe: Boolean = false,
val hostAndPort: NetworkHostAndPort,
val ssl: ClientRpcSslOptions? = null,
val sshdPort: Int? = null,
val sshHostKeyDirectory: Path? = null,
val noLocalShell: Boolean = false) {
companion object {
const val SSH_PORT = 2222
const val COMMANDS_DIR = "shell-commands"
const val CORDAPPS_DIR = "cordapps"
const val SSHD_HOSTKEY_DIR = "ssh"

View File

@ -31,6 +31,7 @@ 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.Decoration
import org.crsh.text.RenderPrintWriter
import org.junit.Before
import org.junit.Test
@ -235,7 +236,7 @@ class InteractiveShellTest {
@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).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Decoration.bold, Color.red)
verify(printWriter).flush()
}
@ -246,7 +247,7 @@ class InteractiveShellTest {
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
verify(cordaRpcOps).killFlow(runId)
verify(printWriter).println("Failed to kill flow $runId", Color.red)
verify(printWriter).println("Failed to kill flow $runId", Decoration.bold, Color.red)
verify(printWriter).flush()
}
@ -257,7 +258,7 @@ class InteractiveShellTest {
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
verify(cordaRpcOps).killFlow(runId)
verify(printWriter).println("Killed flow $runId", Color.yellow)
verify(printWriter).println("Killed flow $runId", Decoration.bold, Color.yellow)
verify(printWriter).flush()
}
}