mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
parent
96e6313fbd
commit
e6f9b46584
@ -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')
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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."
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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],
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user