diff --git a/build.gradle b/build.gradle index 075abafe3f..76ad445761 100644 --- a/build.gradle +++ b/build.gradle @@ -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') diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt index 3af0ef5744..65d0cd6abf 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/ReconnectingCordaRPCOps.kt @@ -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) } } diff --git a/docs/source/shell.rst b/docs/source/shell.rst index e133d12187..cf57b722fb 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -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. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt index 723c33f778..14052c1789 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt @@ -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." diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 7f39fce87f..8ff4a93093 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -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(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) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index a6cd49e0fa..65e1ca0d98 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -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) { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index f9de2f1391..81440feab0 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -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 diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt index a3dbe02f3d..625e94e476 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt @@ -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 diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt index 32feeb73e0..d59d3c6ad9 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/V1NodeConfigurationSpec.kt @@ -35,6 +35,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification { + return setOf(if (safe) { SAFE_INTERNAL_SHELL_PERMISSION } else { UNSAFE_INTERNAL_SHELL_PERMISSION }) +} + +fun determineUnsafeUsers(config: NodeConfiguration): Set { + var unsafeUsers = HashSet() + 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) { + for (perm in user.permissions) { + if (perm == UNSAFE_INTERNAL_SHELL_PERMISSION) { + unsafeUsers.add(user.username) + return + } + } +} + +private fun checkSecurityForUserList(users: List, unsafeUsers: MutableSet) { + for (user in users) { + checkSecurityPermListForUser(user, unsafeUsers) + } +} + +private fun checkSecurityUsers(config: NodeConfiguration, unsafeUsers: MutableSet) { + val users = config.security?.authService?.dataSource?.users + if (users != null) { + checkSecurityForUserList(users, unsafeUsers) + } +} + diff --git a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt index 0b57818aa0..99da62b510 100644 --- a/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/rpc/RpcBrokerConfiguration.kt @@ -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", diff --git a/tools/shell/build.gradle b/tools/shell/build.gradle index bce5a21798..f76070853b 100644 --- a/tools/shell/build.gradle +++ b/tools/shell/build.gradle @@ -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" } 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 c5bbffe9e4..a99c767e54 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 @@ -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(); diff --git a/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java b/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java index 038e60ce0d..3024d8015a 100644 --- a/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java +++ b/tools/shell/src/main/java/net/corda/tools/shell/HashLookupShellCommand.java @@ -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); } } } 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 3237427c9d..c4e33df84a 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 @@ -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() diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt index 4877b520cc..76fd0f5f88 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/ShellConfiguration.kt @@ -9,13 +9,15 @@ data class ShellConfiguration( val cordappsDirectory: Path? = null, var user: String = "", var password: String = "", + var permissions: Set? = 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" 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 7f7ee989b9..7a3212298f 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 @@ -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() } }