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.dependency_checker_version = '5.2.0'
ext.commons_collections_version = '4.3' ext.commons_collections_version = '4.3'
ext.beanutils_version = '1.9.3' ext.beanutils_version = '1.9.3'
ext.crash_version = '810d2b774b85d4938be01b9b65e29e4fddbc450b' ext.crash_version = '1.7.1'
ext.jsr305_version = constants.getProperty("jsr305Version") ext.jsr305_version = constants.getProperty("jsr305Version")
ext.shiro_version = '1.4.1' ext.shiro_version = '1.4.1'
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') 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.CordaRPCConnection
import net.corda.client.rpc.GracefulReconnect import net.corda.client.rpc.GracefulReconnect
import net.corda.client.rpc.MaxRpcRetryException import net.corda.client.rpc.MaxRpcRetryException
import net.corda.client.rpc.PermissionException
import net.corda.client.rpc.RPCConnection import net.corda.client.rpc.RPCConnection
import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCException
import net.corda.client.rpc.internal.ReconnectingCordaRPCOps.ReconnectingRPCConnection.CurrentState.CLOSED 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. // Deliberately not logging full stack trace as it will be full of internal stacktraces.
log.debug { "Exception upon establishing connection: ${ex.message}" } 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 -> { 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`_. 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`_). 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 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 * ``run shutdown`` will shut the node down immediately
Output Formats Output Formats
********************** **************
You can choose the format in which the output of the commands will be shown. 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 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). 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. 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_P2P_USER = "SystemUsers/Node"
const val NODE_RPC_USER = "SystemUsers/NodeRPC" const val NODE_RPC_USER = "SystemUsers/NodeRPC"
const val PEER_USER = "SystemUsers/Peer" 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 INTERNAL_PREFIX = "internal."
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue
const val P2P_PREFIX = "p2p.inbound." 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.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.config.rpc.NodeRpcOptions 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.shell.toShellConfig
import net.corda.node.services.config.shouldInitCrashShell import net.corda.node.services.config.shouldInitCrashShell
import net.corda.node.services.events.NodeSchedulerService 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.FlowMonitor
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.SingleThreadedStateMachineManager 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.StateMachineManager
import net.corda.node.services.statemachine.StateMachineManagerInternal
import net.corda.node.services.transactions.BasicVerifierFactoryService import net.corda.node.services.transactions.BasicVerifierFactoryService
import net.corda.node.services.transactions.DeterministicVerifierFactoryService import net.corda.node.services.transactions.DeterministicVerifierFactoryService
import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryTransactionVerifierService
@ -534,6 +533,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
shellConfiguration.sshdPort?.let { shellConfiguration.sshdPort?.let {
log.info("Binding Shell SSHD server on port $it.") 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) 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.MB
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.SecurityConfiguration 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.shouldInitCrashShell
import net.corda.node.services.config.shouldStartLocalShell import net.corda.node.services.config.shouldStartLocalShell
import net.corda.node.services.messaging.ArtemisMessagingServer 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.node.utilities.errorAndTerminate
import net.corda.nodeapi.internal.ArtemisMessagingClient import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent 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.ShutdownHook
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.bridging.BridgeControlListener
@ -353,7 +354,7 @@ open class Node(configuration: NodeConfiguration,
val securityManager = with(RPCSecurityManagerImpl(securityManagerConfig, cacheFactory)) { val securityManager = with(RPCSecurityManagerImpl(securityManagerConfig, cacheFactory)) {
if (configuration.shouldStartLocalShell()) RPCSecurityManagerWithAdditionalUser(this, 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) { val messageBroker = if (!configuration.messagingServerExternal) {

View File

@ -53,6 +53,8 @@ interface NodeConfiguration {
val lazyBridgeStart: Boolean val lazyBridgeStart: Boolean
val detectPublicIp: Boolean get() = false val detectPublicIp: Boolean get() = false
val sshd: SSHDConfiguration? val sshd: SSHDConfiguration?
val localShellAllowExitInSafeMode: Boolean
val localShellUnsafe: Boolean
val database: DatabaseConfig val database: DatabaseConfig
val noLocalShell: Boolean get() = false val noLocalShell: Boolean get() = false
val transactionCacheSizeBytes: Long get() = defaultTransactionCacheSize 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 // TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration
override val additionalNodeInfoPollingFrequencyMsec: Long = Defaults.additionalNodeInfoPollingFrequencyMsec, override val additionalNodeInfoPollingFrequencyMsec: Long = Defaults.additionalNodeInfoPollingFrequencyMsec,
override val sshd: SSHDConfiguration? = Defaults.sshd, 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), override val database: DatabaseConfig = Defaults.database(devMode),
private val transactionCacheSizeMegaBytes: Int? = Defaults.transactionCacheSizeMegaBytes, private val transactionCacheSizeMegaBytes: Int? = Defaults.transactionCacheSizeMegaBytes,
private val attachmentContentCacheSizeMegaBytes: Int? = Defaults.attachmentContentCacheSizeMegaBytes, private val attachmentContentCacheSizeMegaBytes: Int? = Defaults.attachmentContentCacheSizeMegaBytes,
@ -98,6 +100,8 @@ data class NodeConfigurationImpl(
const val detectPublicIp: Boolean = false const val detectPublicIp: Boolean = false
val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis() val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis()
val sshd: SSHDConfiguration? = null val sshd: SSHDConfiguration? = null
const val localShellAllowExitInSafeMode: Boolean = true
const val localShellUnsafe: Boolean = false
val transactionCacheSizeMegaBytes: Int? = null val transactionCacheSizeMegaBytes: Int? = null
val attachmentContentCacheSizeMegaBytes: Int? = null val attachmentContentCacheSizeMegaBytes: Int? = null
const val attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound 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 lazyBridgeStart by boolean().optional().withDefaultValue(Defaults.lazyBridgeStart)
private val detectPublicIp by boolean().optional().withDefaultValue(Defaults.detectPublicIp) private val detectPublicIp by boolean().optional().withDefaultValue(Defaults.detectPublicIp)
private val sshd by nested(SSHDConfigurationSpec).optional() 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 database by nested(DatabaseConfigSpec).optional()
private val noLocalShell by boolean().optional().withDefaultValue(Defaults.noLocalShell) private val noLocalShell by boolean().optional().withDefaultValue(Defaults.noLocalShell)
private val attachmentCacheBound by long().optional().withDefaultValue(Defaults.attachmentCacheBound) private val attachmentCacheBound by long().optional().withDefaultValue(Defaults.attachmentCacheBound)
@ -103,6 +105,8 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
lazyBridgeStart = configuration[lazyBridgeStart], lazyBridgeStart = configuration[lazyBridgeStart],
detectPublicIp = configuration[detectPublicIp], detectPublicIp = configuration[detectPublicIp],
sshd = configuration[sshd], sshd = configuration[sshd],
localShellAllowExitInSafeMode = configuration[localShellAllowExitInSafeMode],
localShellUnsafe = configuration[localShellUnsafe],
database = database, database = database,
noLocalShell = configuration[noLocalShell], noLocalShell = configuration[noLocalShell],
attachmentCacheBound = configuration[attachmentCacheBound], 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.core.internal.div
import net.corda.node.internal.clientSslOptionsCompatibleWith import net.corda.node.internal.clientSslOptionsCompatibleWith
import net.corda.node.services.config.NodeConfiguration 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
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR
@ -15,7 +13,10 @@ fun NodeConfiguration.toShellConfig() = ShellConfiguration(
commandsDirectory = this.baseDirectory / COMMANDS_DIR, commandsDirectory = this.baseDirectory / COMMANDS_DIR,
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR, cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
user = INTERNAL_SHELL_USER, user = INTERNAL_SHELL_USER,
password = ArtemisMessagingComponent.internalShellPassword, password = internalShellPassword,
permissions = internalShellPermissions(!this.localShellUnsafe),
localShellAllowExitInSafeMode = this.localShellAllowExitInSafeMode,
localShellUnsafe = this.localShellUnsafe,
hostAndPort = this.rpcOptions.address, hostAndPort = this.rpcOptions.address,
ssl = clientSslOptionsCompatibleWith(this.rpcOptions), ssl = clientSslOptionsCompatibleWith(this.rpcOptions),
sshdPort = this.sshd?.port, 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.core.utilities.NetworkHostAndPort
import net.corda.node.internal.artemis.BrokerJaasLoginModule import net.corda.node.internal.artemis.BrokerJaasLoginModule
import net.corda.node.internal.artemis.SecureArtemisConfiguration 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.BrokerRpcSslOptions
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent 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 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 -> val rolesAdderOnLogin = RolesAdderOnLogin(addRPCRoleToUsers) { username ->
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#" to setOf(nodeInternalRole, restrictedRole( "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#" to setOf(nodeInternalRole, restrictedRole(
"${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username", "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username",

View File

@ -35,12 +35,12 @@ dependencies {
compile project(':client:jackson') compile project(':client:jackson')
// CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy. // 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.slf4j", module: "slf4j-jdk14"
exclude group: "org.bouncycastle" 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.slf4j", module: "slf4j-jdk14"
exclude group: "org.bouncycastle" exclude group: "org.bouncycastle"
} }

View File

@ -55,7 +55,7 @@ public class FlowShellCommand extends InteractiveShellCommand {
ANSIProgressRenderer ansiProgressRenderer, ANSIProgressRenderer ansiProgressRenderer,
ObjectMapper om) { ObjectMapper om) {
if (name == null) { 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; return;
} }
String inp = input == null ? "" : String.join(" ", input).trim(); 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.Man;
import org.crsh.cli.Usage; import org.crsh.cli.Usage;
import org.crsh.text.Color; import org.crsh.text.Color;
import org.crsh.text.Decoration;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -27,7 +28,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
logger.info("Executing command \"hash-lookup\"."); logger.info("Executing command \"hash-lookup\".");
if (txIdHash == null) { 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; return;
} }
@ -38,7 +39,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
try { try {
txIdHashParsed = SecureHash.parse(txIdHash); txIdHashParsed = SecureHash.parse(txIdHash);
} catch (IllegalArgumentException e) { } 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; return;
} }
@ -53,7 +54,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
SecureHash found = match.get(); SecureHash found = match.get();
out.println("Found a matching transaction with Id: " + found.toString()); out.println("Found a matching transaction with Id: " + found.toString());
} else { } 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.ANSIProgressRenderer
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
import org.crsh.command.InvocationContext import org.crsh.command.InvocationContext
import org.crsh.command.ShellSafety
import org.crsh.console.jline.JLineProcessor import org.crsh.console.jline.JLineProcessor
import org.crsh.console.jline.TerminalFactory import org.crsh.console.jline.TerminalFactory
import org.crsh.console.jline.console.ConsoleReader 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.ShellFactory
import org.crsh.shell.impl.command.ExternalResolver import org.crsh.shell.impl.command.ExternalResolver
import org.crsh.text.Color import org.crsh.text.Color
import org.crsh.text.Decoration
import org.crsh.text.RenderPrintWriter import org.crsh.text.RenderPrintWriter
import org.crsh.util.InterruptHandler import org.crsh.util.InterruptHandler
import org.crsh.util.Utils import org.crsh.util.Utils
@ -87,6 +89,8 @@ import kotlin.concurrent.thread
// TODO: Resurrect or reimplement the mail plugin. // TODO: Resurrect or reimplement the mail plugin.
// TODO: Make it notice new shell commands added after the node started. // TODO: Make it notice new shell commands added after the node started.
const val STANDALONE_SHELL_PERMISSION = "ALL"
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
object InteractiveShell { object InteractiveShell {
private val log = LoggerFactory.getLogger(javaClass) private val log = LoggerFactory.getLogger(javaClass)
@ -128,14 +132,21 @@ object InteractiveShell {
rpcConn = connection rpcConn = connection
connection.proxy as InternalCordaRPCOps 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 shellConfiguration = configuration
InteractiveShell.classLoader = classLoader InteractiveShell.classLoader = classLoader
val runSshDaemon = configuration.sshdPort != null 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() val config = Properties()
if (runSshDaemon) { if (runSshDaemon) {
// Enable SSH access. Note: these have to be strings, even though raw object assignments also work. // 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", "Commands to extract information about checkpoints stored within the node",
CheckpointShellCommand::class.java 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 = {}) { 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 { fun start(config: Properties, localUserName: String = "", localUserPassword: String = ""): Shell {
val classLoader = this.javaClass.classLoader val classLoader = this.javaClass.classLoader
val classpathDriver = ClassPathMountFactory(classLoader) val classpathDriver = ClassPathMountFactory(classLoader)
@ -243,7 +261,8 @@ object InteractiveShell {
this.config = config this.config = config
start(context) start(context)
ops = makeRPCOps(rpcOps, localUserName, localUserPassword) 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 { val matches = try {
rpcOps.registeredFlows().filter { nameFragment in it } rpcOps.registeredFlows().filter { nameFragment in it }
} catch (e: PermissionException) { } catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red) output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
return return
} }
if (matches.isEmpty()) { 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 return
} else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) { } else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) {
output.println("Ambiguous name provided, please be more specific. Your options are:") 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 return
} }
@ -364,10 +383,10 @@ object InteractiveShell {
} }
output.println("Flow completed with result: ${stateObservable.returnValue.get()}") output.println("Flow completed with result: ${stateObservable.returnValue.get()}")
} catch (e: NoApplicableConstructor) { } catch (e: NoApplicableConstructor) {
output.println("No matching constructor found:", Color.red) output.println("No matching constructor found:", Decoration.bold, Color.red)
e.errors.forEach { output.println("- $it", Color.red) } e.errors.forEach { output.println("- $it", Decoration.bold, Color.red) }
} catch (e: PermissionException) { } catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red) output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// ignoring it as already logged by the progress handler subscriber // ignoring it as already logged by the progress handler subscriber
} finally { } finally {
@ -421,7 +440,7 @@ object InteractiveShell {
val runId = try { val runId = try {
inputObjectMapper.readValue(id, StateMachineRunId::class.java) inputObjectMapper.readValue(id, StateMachineRunId::class.java)
} catch (e: JsonMappingException) { } 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) log.error("Failed to parse flow ID", e)
return return
} }
@ -429,14 +448,14 @@ object InteractiveShell {
if (id.length < uuidStringSize) { 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. " + 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." "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) log.warn(msg)
return return
} }
if (rpcOps.killFlow(runId)) { if (rpcOps.killFlow(runId)) {
output.println("Killed flow $runId", Color.yellow) output.println("Killed flow $runId", Decoration.bold, Color.yellow)
} else { } else {
output.println("Failed to kill flow $runId", Color.red) output.println("Failed to kill flow $runId", Decoration.bold, Color.red)
} }
} finally { } finally {
output.flush() output.flush()
@ -568,7 +587,7 @@ object InteractiveShell {
// The flow command provides better support and startFlow requires special handling anyway due to // 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 // the generic startFlow RPC interface which offers no type information with which to parse the
// string form of the command. // 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 return null
} else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) { } else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) {
return gracefulShutdown(out, cordaRPCOps) return gracefulShutdown(out, cordaRPCOps)
@ -603,12 +622,12 @@ object InteractiveShell {
} }
} }
} catch (e: StringToMethodCallParser.UnparseableCallException) { } catch (e: StringToMethodCallParser.UnparseableCallException) {
out.println(e.message, Color.red) out.println(e.message, Decoration.bold, Color.red)
if (e !is StringToMethodCallParser.UnparseableCallException.NoSuchFile) { if (e !is StringToMethodCallParser.UnparseableCallException.NoSuchFile) {
out.println("Please try 'man run' to learn what syntax is acceptable") out.println("Please try 'man run' to learn what syntax is acceptable")
} }
} catch (e: Exception) { } catch (e: Exception) {
out.println("RPC failed: ${e.rootCause}", Color.red) out.println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red)
} finally { } finally {
InputStreamSerializer.invokeContext = null InputStreamSerializer.invokeContext = null
InputStreamDeserializer.closeAll() InputStreamDeserializer.closeAll()

View File

@ -9,13 +9,15 @@ data class ShellConfiguration(
val cordappsDirectory: Path? = null, val cordappsDirectory: Path? = null,
var user: String = "", var user: String = "",
var password: String = "", var password: String = "",
var permissions: Set<String>? = null,
var localShellAllowExitInSafeMode: Boolean = false,
var localShellUnsafe: Boolean = false,
val hostAndPort: NetworkHostAndPort, val hostAndPort: NetworkHostAndPort,
val ssl: ClientRpcSslOptions? = null, val ssl: ClientRpcSslOptions? = null,
val sshdPort: Int? = null, val sshdPort: Int? = null,
val sshHostKeyDirectory: Path? = null, val sshHostKeyDirectory: Path? = null,
val noLocalShell: Boolean = false) { val noLocalShell: Boolean = false) {
companion object { companion object {
const val SSH_PORT = 2222
const val COMMANDS_DIR = "shell-commands" const val COMMANDS_DIR = "shell-commands"
const val CORDAPPS_DIR = "cordapps" const val CORDAPPS_DIR = "cordapps"
const val SSHD_HOSTKEY_DIR = "ssh" 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 net.corda.testing.internal.DEV_ROOT_CA
import org.crsh.command.InvocationContext import org.crsh.command.InvocationContext
import org.crsh.text.Color import org.crsh.text.Color
import org.crsh.text.Decoration
import org.crsh.text.RenderPrintWriter import org.crsh.text.RenderPrintWriter
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -235,7 +236,7 @@ class InteractiveShellTest {
@Test @Test
fun killFlowWithNonsenseID() { fun killFlowWithNonsenseID() {
InteractiveShell.killFlowById("nonsense", printWriter, cordaRpcOps, om) 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() verify(printWriter).flush()
} }
@ -246,7 +247,7 @@ class InteractiveShellTest {
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om) InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
verify(cordaRpcOps).killFlow(runId) 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() verify(printWriter).flush()
} }
@ -257,7 +258,7 @@ class InteractiveShellTest {
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om) InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
verify(cordaRpcOps).killFlow(runId) 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() verify(printWriter).flush()
} }
} }