diff --git a/bin/main/com/plugin/sshjplugin/SSHJBuilder$BuilderException.class b/bin/main/com/plugin/sshjplugin/SSHJBuilder$BuilderException.class new file mode 100644 index 0000000..d1856e8 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/SSHJBuilder$BuilderException.class differ diff --git a/bin/main/com/plugin/sshjplugin/SSHJBuilder.class b/bin/main/com/plugin/sshjplugin/SSHJBuilder.class new file mode 100644 index 0000000..76fea2b Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/SSHJBuilder.class differ diff --git a/bin/main/com/plugin/sshjplugin/SSHJFileCopierPlugin.class b/bin/main/com/plugin/sshjplugin/SSHJFileCopierPlugin.class new file mode 100644 index 0000000..66de106 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/SSHJFileCopierPlugin.class differ diff --git a/bin/main/com/plugin/sshjplugin/SSHJNodeExecutorPlugin$ExtractFailure.class b/bin/main/com/plugin/sshjplugin/SSHJNodeExecutorPlugin$ExtractFailure.class new file mode 100644 index 0000000..a8a822f Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/SSHJNodeExecutorPlugin$ExtractFailure.class differ diff --git a/bin/main/com/plugin/sshjplugin/SSHJNodeExecutorPlugin.class b/bin/main/com/plugin/sshjplugin/SSHJNodeExecutorPlugin.class new file mode 100644 index 0000000..99ccf73 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/SSHJNodeExecutorPlugin.class differ diff --git a/bin/main/com/plugin/sshjplugin/model/SSHJAuthentication.class b/bin/main/com/plugin/sshjplugin/model/SSHJAuthentication.class new file mode 100644 index 0000000..d547ce3 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/model/SSHJAuthentication.class differ diff --git a/bin/main/com/plugin/sshjplugin/model/SSHJBase.class b/bin/main/com/plugin/sshjplugin/model/SSHJBase.class new file mode 100644 index 0000000..40431ea Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/model/SSHJBase.class differ diff --git a/bin/main/com/plugin/sshjplugin/model/SSHJConnection$AuthenticationType.class b/bin/main/com/plugin/sshjplugin/model/SSHJConnection$AuthenticationType.class new file mode 100644 index 0000000..6d0e0ce Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/model/SSHJConnection$AuthenticationType.class differ diff --git a/bin/main/com/plugin/sshjplugin/model/SSHJConnection.class b/bin/main/com/plugin/sshjplugin/model/SSHJConnection.class new file mode 100644 index 0000000..57f68fc Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/model/SSHJConnection.class differ diff --git a/bin/main/com/plugin/sshjplugin/model/SSHJConnectionParameters.class b/bin/main/com/plugin/sshjplugin/model/SSHJConnectionParameters.class new file mode 100644 index 0000000..acd93c9 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/model/SSHJConnectionParameters.class differ diff --git a/bin/main/com/plugin/sshjplugin/model/SSHJExec.class b/bin/main/com/plugin/sshjplugin/model/SSHJExec.class new file mode 100644 index 0000000..373c44d Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/model/SSHJExec.class differ diff --git a/bin/main/com/plugin/sshjplugin/model/SSHJScp.class b/bin/main/com/plugin/sshjplugin/model/SSHJScp.class new file mode 100644 index 0000000..65e9b0e Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/model/SSHJScp.class differ diff --git a/bin/main/com/plugin/sshjplugin/sudo/SudoCommand.class b/bin/main/com/plugin/sshjplugin/sudo/SudoCommand.class new file mode 100644 index 0000000..92eadc8 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/sudo/SudoCommand.class differ diff --git a/bin/main/com/plugin/sshjplugin/sudo/SudoCommandBuilder.class b/bin/main/com/plugin/sshjplugin/sudo/SudoCommandBuilder.class new file mode 100644 index 0000000..6a10b2f Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/sudo/SudoCommandBuilder.class differ diff --git a/bin/main/com/plugin/sshjplugin/util/PropertyResolver.class b/bin/main/com/plugin/sshjplugin/util/PropertyResolver.class new file mode 100644 index 0000000..2e6bf09 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/util/PropertyResolver.class differ diff --git a/bin/main/com/plugin/sshjplugin/util/SSHJSecretBundleUtil.class b/bin/main/com/plugin/sshjplugin/util/SSHJSecretBundleUtil.class new file mode 100644 index 0000000..234b1e7 Binary files /dev/null and b/bin/main/com/plugin/sshjplugin/util/SSHJSecretBundleUtil.class differ diff --git a/bin/test/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy b/bin/test/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy new file mode 100644 index 0000000..6cd3ccf --- /dev/null +++ b/bin/test/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy @@ -0,0 +1,528 @@ +package com.plugin.sshjplugin + +import com.dtolabs.rundeck.core.common.NodeEntryImpl +import com.dtolabs.rundeck.core.data.BaseDataContext +import com.dtolabs.rundeck.core.execution.ExecutionContextImpl +import com.dtolabs.rundeck.core.execution.ExecutionListener +import com.dtolabs.rundeck.core.execution.ExecutionListenerOverride +import com.dtolabs.rundeck.core.common.Framework +import com.dtolabs.rundeck.core.storage.ResourceMeta +import com.dtolabs.rundeck.core.storage.StorageTree +import com.dtolabs.rundeck.core.utils.PropertyLookup +import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.connection.channel.direct.Session +import net.schmizz.sshj.transport.Transport +import org.rundeck.storage.api.Resource +import spock.lang.Specification +import com.dtolabs.rundeck.core.common.ProjectManager +import com.dtolabs.rundeck.core.common.IRundeckProject + +class SSHJNodeExecutorPluginSpec extends Specification { + + def getContext(Properties properties,def rundeckFramework, def logger) { + + def dataContext = [ + config: ["RD_TEST": "Value"] + ] + + def storage = Mock(StorageTree) { + getResource('keys/password') >> Mock(Resource) { + getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('test.'.bytes) + 7L + } + } + } + + getResource('keys/node.key') >> Mock(Resource) { + getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('-----BEGIN OPENSSH PRIVATE KEY-----'.bytes) + 7L + } + } + } + } + + def framework = Mock(Framework) { + getFrameworkProjectMgr() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + getPropertyLookup() >> PropertyLookup.create(properties) + getProjectManager() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + } + + ExecutionContextImpl.builder() + .framework(framework) + .executionListener(logger) + .storageTree(storage) + .dataContext(new BaseDataContext(dataContext)) + .frameworkProject("test") + .build() + } + + + def "authenticate using node password"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + def context = getContext(properties, rundeckFramework, logger) + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"password", + "ssh-password-storage-path":"keys/password", + "sudo-password-storage-path":"keys/password", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } + 1 * logger.log(3, "Authenticating using password: keys/password") + result!=null + result.success + + } + + def "authenticate using node key"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + def context = getContext(properties,rundeckFramework, logger) + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"privateKey", + "ssh-key-storage-path":"keys/node.key", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } + 1 * logger.log(3, "Authenticating using private key") + result!=null + result.success + + } + + + def "authenticate using password project level"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) { + hasProperty('project.ssh-authentication') >> true + getProperty('project.ssh-authentication') >> "password" + hasProperty('project.ssh-password-storage-path') >> true + getProperty('project.ssh-password-storage-path')>> "keys/password" + } + + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + def context = getContext(properties,rundeckFramework, logger) + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } + 1 * logger.log(3, "Authenticating using password: keys/password") + result!=null + result.success + + } + + def "authenticate using key project level"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) { + hasProperty('project.ssh-authentication') >> true + getProperty('project.ssh-authentication') >> "privateKey" + hasProperty('project.ssh-key-storage-path') >> true + getProperty('project.ssh-key-storage-path')>> "keys/node.key" + } + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + def context = getContext(properties,rundeckFramework, logger) + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } + 1 * logger.log(3, "Authenticating using private key") + result!=null + result.success + + + + } + + + def "error getting key"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + + def dataContext = [ + config: ["RD_TEST": "Value"] + ] + + def storage = Mock(StorageTree) { + getResource('keys/password') >> {throw new Exception("Cannot get password")} + getResource('keys/node.key') >> {throw new Exception("Cannot get key")} + } + + def framework = Mock(Framework) { + getFrameworkProjectMgr() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + getPropertyLookup() >> PropertyLookup.create(properties) + getProjectManager() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + } + + def context = ExecutionContextImpl.builder() + .framework(framework) + .executionListener(logger) + .storageTree(storage) + .dataContext(new BaseDataContext(dataContext)) + .frameworkProject("test") + .build() + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"privateKey", + "ssh-key-storage-path":"keys/node.key", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + !result.success + result.failureMessage=="Failed to read SSH Key Storage stored at path: keys/node.key" + + + } + + + def "error getting password"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + + def dataContext = [ + config: ["RD_TEST": "Value"] + ] + + def storage = Mock(StorageTree) { + getResource('keys/password') >> {throw new Exception("Cannot get password")} + getResource('keys/node.key') >> {throw new Exception("Cannot get key")} + } + + def framework = Mock(Framework) { + getFrameworkProjectMgr() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + getPropertyLookup() >> PropertyLookup.create(properties) + getProjectManager() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + } + + def context = ExecutionContextImpl.builder() + .framework(framework) + .executionListener(logger) + .storageTree(storage) + .dataContext(new BaseDataContext(dataContext)) + .frameworkProject("test") + .build() + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"password", + "ssh-password-storage-path":"keys/password", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + !result.success + result.failureMessage=="Failed to read SSH Password stored at path: keys/password" + + + } + + def "error getting key passphrase"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + + def dataContext = [ + config: ["RD_TEST": "Value"] + ] + + def storage = Mock(StorageTree) { + getResource('keys/password') >> {throw new Exception("Cannot get password")} + getResource('keys/node.key') >> Mock(Resource) { + getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('test.'.bytes) + 7L + } + } + } + } + + def framework = Mock(Framework) { + getFrameworkProjectMgr() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + getPropertyLookup() >> PropertyLookup.create(properties) + getProjectManager() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + } + + def context = ExecutionContextImpl.builder() + .framework(framework) + .executionListener(logger) + .storageTree(storage) + .dataContext(new BaseDataContext(dataContext)) + .frameworkProject("test") + .build() + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"privateKey", + "ssh-key-storage-path":"keys/node.key", + "ssh-key-passphrase-storage-path":"keys/password", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + !result.success + result.failureMessage=="Failed to read SSH Passphrase stored at path: keys/password" + + + } + +} diff --git a/bin/test/com/plugin/sshjplugin/model/SSHJAuthenticationTest.groovy b/bin/test/com/plugin/sshjplugin/model/SSHJAuthenticationTest.groovy new file mode 100644 index 0000000..6464904 --- /dev/null +++ b/bin/test/com/plugin/sshjplugin/model/SSHJAuthenticationTest.groovy @@ -0,0 +1,196 @@ +package com.plugin.sshjplugin.model + +import com.dtolabs.rundeck.plugins.PluginLogger +import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile +import net.schmizz.sshj.Config +import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.common.Factory +import net.schmizz.sshj.transport.Transport +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider +import spock.lang.Specification + + +class SSHJAuthenticationTest extends Specification { + + def "authenticate with private key on filesystem no passphrase"() { + given: + SSHClient sshClient = Mock(SSHClient) + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyFilePath() >> "keys/rundeck/storage" + 0 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_, _) + 1 * sshClient.loadKeys(_) + 0 * sshClient.loadKeys(_, _) + } + + def "authenticate with private key on filesystem with passphrase"() { + given: + String passphraseStoragePath = "keys/rundeck/storage/passphrase" + + SSHClient sshClient = Mock(SSHClient) + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyFilePath() >> "keys/rundeck/storage" + 1 * getPrivateKeyPassphraseStoragePath() >> passphraseStoragePath + 1 * getPrivateKeyPassphrase(passphraseStoragePath) >> "pass" + 0 * getPrivateKeyStorage(_) + } + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 0 * sshClient.loadKeys(_) + 1 * sshClient.authPublickey(_, _) + } + + + + def "authenticate with private key Rundeck storage no passphrase"() { + given: + String keyStoragePath = "keys/rundeck/storage" + SSHClient sshClient = Mock(SSHClient){ + getTransport() >> Mock(Transport){ + getConfig() >> Mock(Config){ + getFileKeyProviderFactories() >> providerFactoriesList() + } + } + } + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyStoragePath() >> keyStoragePath + 1 * getPrivateKeyStorage(keyStoragePath) >> "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCBHpyIOm\n" + + "Y45NDeuHxAzGCUAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKUYWSH2YrYUH3IA\n" + + "t40IcM0ykM03oFRI7m+5jEK+fE4LAAAAoBIhOVxejwLYvIKIBgNQOe0j8h2nnz/+sEYUDc\n" + + "ug6KrlPxQ7kuL67It/Tb7IxAGzVWT3g3fkQMGNU/8uxRHAf5fQC9aYValFPr21g7I39OqR\n" + + "MbPXHnD8a+DwAw3ArakcZigzWqncuX5cuBgpr5+x/iXWAz0lAHJH1d5HaIsoy1K6VmMR+b\n" + + "GN7ixrjWwMVBM+Lv8DdRN5UnniX5grj6M8P0A=\n" + + "-----END OPENSSH PRIVATE KEY-----" + 0 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_,_) + + } + + def "authenticate with private key Rundeck storage and passphrase"() { + given: + String keyStoragePath = "keys/rundeck/storage" + String passphraseStoragePath = "keys/rundeck/storage/passphrase" + SSHClient sshClient = Mock(SSHClient){ + getTransport() >> Mock(Transport){ + getConfig() >> Mock(Config){ + getFileKeyProviderFactories() >> providerFactoriesList() + } + } + } + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getPrivateKeyPassphraseStoragePath() >> passphraseStoragePath + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyStoragePath() >> keyStoragePath + 1 * getPrivateKeyStorage(keyStoragePath) >> "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCBHpyIOm\n" + + "Y45NDeuHxAzGCUAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKUYWSH2YrYUH3IA\n" + + "t40IcM0ykM03oFRI7m+5jEK+fE4LAAAAoBIhOVxejwLYvIKIBgNQOe0j8h2nnz/+sEYUDc\n" + + "ug6KrlPxQ7kuL67It/Tb7IxAGzVWT3g3fkQMGNU/8uxRHAf5fQC9aYValFPr21g7I39OqR\n" + + "MbPXHnD8a+DwAw3ArakcZigzWqncuX5cuBgpr5+x/iXWAz0lAHJH1d5HaIsoy1K6VmMR+b\n" + + "GN7ixrjWwMVBM+Lv8DdRN5UnniX5grj6M8P0A=\n" + + "-----END OPENSSH PRIVATE KEY-----" + 1 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_,_) + } + + + def "if local storage path and rundeck storage path are provided, key stored on rundeck will be used"() { + given: + String keyStoragePath = "keys/rundeck/storage" + String fileSystemStoragePath = "user/key" + String passphraseStoragePath = "keys/rundeck/storage/passphrase" + SSHClient sshClient = Mock(SSHClient){ + getTransport() >> Mock(Transport){ + getConfig() >> Mock(Config){ + getFileKeyProviderFactories() >> providerFactoriesList() + } + } + } + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getPrivateKeyFilePath() >> fileSystemStoragePath + 1 * getPrivateKeyPassphraseStoragePath() >> passphraseStoragePath + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyStoragePath() >> keyStoragePath + 1 * getPrivateKeyStorage(keyStoragePath) >> "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCBHpyIOm\n" + + "Y45NDeuHxAzGCUAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKUYWSH2YrYUH3IA\n" + + "t40IcM0ykM03oFRI7m+5jEK+fE4LAAAAoBIhOVxejwLYvIKIBgNQOe0j8h2nnz/+sEYUDc\n" + + "ug6KrlPxQ7kuL67It/Tb7IxAGzVWT3g3fkQMGNU/8uxRHAf5fQC9aYValFPr21g7I39OqR\n" + + "MbPXHnD8a+DwAw3ArakcZigzWqncuX5cuBgpr5+x/iXWAz0lAHJH1d5HaIsoy1K6VmMR+b\n" + + "GN7ixrjWwMVBM+Lv8DdRN5UnniX5grj6M8P0A=\n" + + "-----END OPENSSH PRIVATE KEY-----" + 1 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_,_) + 0 * sshClient.loadKeys(_) + 0 * sshClient.loadKeys(_, _) + } + + + + private static List> providerFactoriesList() { + List> namedList = new ArrayList<>(); + namedList.add(new Factory.Named(){ + @Override + FileKeyProvider create() { + return new OpenSSHKeyV1KeyFile(); + } + @Override + String getName() { + return "OpenSSHKeyV1KeyFile" + } + }) + return namedList; + } + + + } + diff --git a/src/main/java/com/plugin/sshjplugin/SSHJNodeExecutorPlugin.java b/src/main/java/com/plugin/sshjplugin/SSHJNodeExecutorPlugin.java index d39c88c..a966337 100644 --- a/src/main/java/com/plugin/sshjplugin/SSHJNodeExecutorPlugin.java +++ b/src/main/java/com/plugin/sshjplugin/SSHJNodeExecutorPlugin.java @@ -256,9 +256,9 @@ public NodeExecutorResult executeCommand(ExecutionContext context, String[] comm sshexec.execute(sshClient); success = true; } catch (Exception e) { - final ExtractFailure extractJschFailure = extractFailure(e, node, commandtimeout, contimeout, context.getFramework()); - errormsg = extractJschFailure.getErrormsg(); - failureReason = extractJschFailure.getReason(); + final ExtractFailure extractFailure = extractFailure(e, node, commandtimeout, contimeout, context.getFramework()); + errormsg = extractFailure.getErrormsg(); + failureReason = extractFailure.getReason(); context.getExecutionListener().log( 3, String.format( diff --git a/src/main/java/com/plugin/sshjplugin/model/SSHJAuthentication.java b/src/main/java/com/plugin/sshjplugin/model/SSHJAuthentication.java index 9440887..967c1bc 100644 --- a/src/main/java/com/plugin/sshjplugin/model/SSHJAuthentication.java +++ b/src/main/java/com/plugin/sshjplugin/model/SSHJAuthentication.java @@ -2,16 +2,20 @@ import com.dtolabs.rundeck.plugins.PluginLogger; import com.plugin.sshjplugin.SSHJBuilder; import net.schmizz.sshj.SSHClient; -import net.schmizz.sshj.userauth.keyprovider.KeyProvider; -import java.io.File; -import java.io.IOException; +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; +import net.schmizz.sshj.userauth.keyprovider.KeyFormat; +import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil; +import net.schmizz.sshj.userauth.password.PasswordUtils; + +import java.io.*; public class SSHJAuthentication { SSHJConnection.AuthenticationType authenticationType; String username; String password; - String privateKeyFile; + String privateKeyContent; String passphrase; PluginLogger logger; SSHJConnection connectionParameters; @@ -28,52 +32,61 @@ void authenticate(final SSHClient ssh) throws IOException { switch (authenticationType) { case privateKey: - try{ - privateKeyFile = connectionParameters.getPrivateKeyPath(); - logger.log(3, "Authenticating using private key"); - - } catch (IOException e) { - logger.log(0, "Failed to get SSH key: " + e.getMessage()); - } - + logger.log(3, "Authenticating using private key"); + String privateKeyStoragePath = connectionParameters.getPrivateKeyStoragePath(); + String privateKeyFileSystemPath = connectionParameters.getPrivateKeyFilePath(); String passphrasePath = connectionParameters.getPrivateKeyPassphraseStoragePath(); - try{ - passphrase = connectionParameters.getPrivateKeyPassphrase(passphrasePath); - } catch (IOException e) { - logger.log(0, "Failed to read SSH Passphrase stored at path: " + passphrasePath); - } + FileKeyProvider keys = null; - KeyProvider key = null; - if (null != privateKeyFile && !"".equals(privateKeyFile)) { - if (!new File(privateKeyFile).exists()) { - throw new SSHJBuilder.BuilderException("SSH Keyfile does not exist: " + privateKeyFile); + if(passphrasePath != null){ + try{ + passphrase = connectionParameters.getPrivateKeyPassphrase(passphrasePath); + } catch (Exception e) { + throw new SSHJBuilder.BuilderException("Failed to read SSH Passphrase stored at path: " + passphrasePath); + } + } + if(privateKeyFileSystemPath != null && privateKeyStoragePath == null){ + logger.log(3, "[sshj-debug] Using SSH Keyfile: " + privateKeyFileSystemPath); + if (passphrase == null) { + keys = (FileKeyProvider) ssh.loadKeys(privateKeyFileSystemPath); + } else { + keys = (FileKeyProvider) ssh.loadKeys(privateKeyFileSystemPath, passphrase); + logger.log(3, "[sshj-debug] Using Passphrase: " + passphrasePath); } - logger.log(3, "[sshj-debug] Using ssh keyfile: " + privateKeyFile); } + if(privateKeyStoragePath != null){ + logger.log(3, "[sshj-debug] Using SSH Storage key: " + privateKeyStoragePath); + try{ + privateKeyContent = connectionParameters.getPrivateKeyStorage(privateKeyStoragePath); + } catch (Exception e) { + throw new SSHJBuilder.BuilderException("Failed to read SSH Key Storage stored at path: " + privateKeyStoragePath); + } + KeyFormat format = KeyProviderUtil.detectKeyFileFormat(privateKeyContent,true); + keys = Factory.Named.Util.create(ssh.getTransport().getConfig().getFileKeyProviderFactories(), format.toString()); - if (passphrase == null) { - key = ssh.loadKeys(privateKeyFile); - } else { - key = ssh.loadKeys(privateKeyFile, passphrase); + if(keys != null ){ + if (passphrase == null) { + keys.init(new StringReader(privateKeyContent), null); + } else { + logger.log(3, "[sshj-debug] Using Passphrase: " + passphrasePath); + keys.init(new StringReader(privateKeyContent), PasswordUtils.createOneOff(passphrase.toCharArray())); + } + } } - ssh.authPublickey(username, key); + ssh.authPublickey(username, keys); break; case password: String passwordPath = connectionParameters.getPasswordStoragePath(); - if(passwordPath!=null){ + if(passwordPath != null){ logger.log(3, "Authenticating using password: " + passwordPath); } try{ password = connectionParameters.getPassword(passwordPath); - } catch (IOException e) { - logger.log(0, "Failed to read SSH Password stored at path: " + passwordPath); + } catch (Exception e) { + throw new SSHJBuilder.BuilderException("Failed to read SSH Password stored at path: " + passwordPath); } - if (password != null) { - ssh.authPassword(username, password); - }else{ - throw new SSHJBuilder.BuilderException("SSH password wasn't set, please define a password"); - } + ssh.authPassword(username, password); break; } } diff --git a/src/main/java/com/plugin/sshjplugin/model/SSHJConnection.java b/src/main/java/com/plugin/sshjplugin/model/SSHJConnection.java index 1d5cfa3..54d4cd1 100644 --- a/src/main/java/com/plugin/sshjplugin/model/SSHJConnection.java +++ b/src/main/java/com/plugin/sshjplugin/model/SSHJConnection.java @@ -21,8 +21,12 @@ static enum AuthenticationType { InputStream getPrivateKeyStorageData(String path); + String getPrivateKeyStorage(String path) throws IOException; + String getPasswordStoragePath(); + String getPrivateKeyFilePath(); + String getPassword(String path) throws IOException; String getSudoPasswordStoragePath(); diff --git a/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java b/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java index 3c45c44..17f9114 100644 --- a/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java +++ b/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java @@ -59,7 +59,7 @@ public String getPrivateKeyPath() throws IOException { }else{ - privateKeyFile = getPrivateKeyfilePath(); + privateKeyFile = getPrivateKeyFilePath(); context.getExecutionListener().log(3, "[sshj-debug] Using ssh keyfile: " + privateKeyFile); } @@ -69,7 +69,6 @@ public String getPrivateKeyPath() throws IOException { @Override public String getPrivateKeyStoragePath(){ - String path = propertyResolver.resolve(SSHJNodeExecutorPlugin.NODE_ATTR_SSH_KEY_RESOURCE); if (path == null && framework.hasProperty(Constants.SSH_KEYRESOURCE_PROP)) { //return default framework level @@ -85,15 +84,20 @@ public String getPrivateKeyStoragePath(){ @Override public InputStream getPrivateKeyStorageData(String path){ try { - InputStream sshKey = propertyResolver.getPrivateKeyStorageData(path); - return sshKey; + return propertyResolver.getPrivateKeyStorageData(path); } catch (IOException e) { throw new RuntimeException(e); } } - String getPrivateKeyfilePath() { + @Override + public String getPrivateKeyStorage(String path) throws IOException { + return propertyResolver.getPrivateKeyStorage(path); + } + + @Override + public String getPrivateKeyFilePath() { String path = propertyResolver.resolve(SSHJNodeExecutorPlugin.NODE_ATTR_SSH_KEYPATH); if (path == null && framework.hasProperty(Constants.SSH_KEYPATH_PROP)) { //return default framework level diff --git a/src/main/java/com/plugin/sshjplugin/model/SSHJExec.java b/src/main/java/com/plugin/sshjplugin/model/SSHJExec.java index 8462c57..3caf74d 100644 --- a/src/main/java/com/plugin/sshjplugin/model/SSHJExec.java +++ b/src/main/java/com/plugin/sshjplugin/model/SSHJExec.java @@ -53,6 +53,7 @@ public void execute(SSHClient ssh) { pluginLogger.log(3, "["+getPluginName()+"] starting session" ); session = ssh.startSession(); + session.allocateDefaultPTY(); pluginLogger.log(3, "["+getPluginName()+"] setting environments" ); @@ -84,6 +85,8 @@ public void execute(SSHClient ssh) { } String sudoPassword = this.getSshjConnection().getSudoPassword(sudoPasswordPath); + pluginLogger.log(3, "["+getPluginName()+"] using sudoPassword value of: " + sudoPassword ); + SudoCommand sudoCommandRunner = new SudoCommandBuilder() .sudoPromptPattern(this.getSshjConnection().getSudoPromptPattern()) .sudoPassword(sudoPassword) diff --git a/src/main/java/com/plugin/sshjplugin/sudo/SudoCommand.java b/src/main/java/com/plugin/sshjplugin/sudo/SudoCommand.java index b895ee8..394ebd5 100644 --- a/src/main/java/com/plugin/sshjplugin/sudo/SudoCommand.java +++ b/src/main/java/com/plugin/sshjplugin/sudo/SudoCommand.java @@ -64,6 +64,8 @@ public void setLogger(PluginLogger logger) { public String runSudoCommand(String command) throws IOException { + logger.log(3, "Entered runSudoCommand"); + Expect expect = new ExpectBuilder() .withOutput(outputStream) .withInputs(inputStream, errorStream) @@ -77,10 +79,13 @@ public String runSudoCommand(String command) throws IOException { .withTimeout(30000, TimeUnit.SECONDS) .build(); + logger.log(3, "Looking for pattern :" + PROMPT_PATTERN); expect.expect(regexp(PROMPT_PATTERN)); + //expect.sendLine("stty -echo"); //expect.interact(); + logger.log(3, "Issuing command :" + command); expect.sendLine(command); logger.log(3, "SUDO command enabled"); @@ -90,6 +95,7 @@ public String runSudoCommand(String command) throws IOException { expect.expect(contains(sudoPromptPattern)); expect.sendLine(sudoPassword); + logger.log(3, "Looking for pattern :" + PROMPT_PATTERN); expect.expect(regexp(PROMPT_PATTERN)); expect.sendLine("echo $?"); diff --git a/src/main/java/com/plugin/sshjplugin/util/PropertyResolver.java b/src/main/java/com/plugin/sshjplugin/util/PropertyResolver.java index f167258..31c670a 100644 --- a/src/main/java/com/plugin/sshjplugin/util/PropertyResolver.java +++ b/src/main/java/com/plugin/sshjplugin/util/PropertyResolver.java @@ -69,6 +69,22 @@ public InputStream getPrivateKeyStorageData(String path) throws IOException { } + public String getPrivateKeyStorage(String path) throws IOException { + //expand properties in path + if (path != null && path.contains("${")) { + path = DataContextUtils.replaceDataReferencesInString(path, context.getDataContext()); + } + if (null == path) { + return null; + } + + ResourceMeta contents = context.getStorageTree().getResource(path).getContents(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + contents.writeContent(byteArrayOutputStream); + return byteArrayOutputStream.toString(); + + } + public String getStoragePath(String property) { String path = resolve(property); @@ -83,8 +99,7 @@ public String getPasswordFromPath(String path) throws IOException { ResourceMeta contents = context.getStorageTree().getResource(path).getContents(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); contents.writeContent(byteArrayOutputStream); - String password = new String(byteArrayOutputStream.toByteArray()); - return password; + return byteArrayOutputStream.toString(); } public String nonBlank(final String input) { diff --git a/src/test/groovy/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy b/src/test/groovy/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy index 34b63dd..6cd3ccf 100644 --- a/src/test/groovy/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy +++ b/src/test/groovy/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy @@ -10,6 +10,7 @@ import com.dtolabs.rundeck.core.storage.ResourceMeta import com.dtolabs.rundeck.core.storage.StorageTree import com.dtolabs.rundeck.core.utils.PropertyLookup import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.connection.channel.direct.Session import net.schmizz.sshj.transport.Transport import org.rundeck.storage.api.Resource import spock.lang.Specification @@ -20,7 +21,6 @@ class SSHJNodeExecutorPluginSpec extends Specification { def getContext(Properties properties,def rundeckFramework, def logger) { - def dataContext = [ config: ["RD_TEST": "Value"] ] @@ -38,7 +38,7 @@ class SSHJNodeExecutorPluginSpec extends Specification { getResource('keys/node.key') >> Mock(Resource) { getContents() >> Mock(ResourceMeta) { writeContent(_) >> { args -> - args[0].write('test.'.bytes) + args[0].write('-----BEGIN OPENSSH PRIVATE KEY-----'.bytes) 7L } } @@ -102,10 +102,25 @@ class SSHJNodeExecutorPluginSpec extends Specification { ]) when: - plugin.executeCommand(context, command, node) + def result = plugin.executeCommand(context, command, node) then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } 1 * logger.log(3, "Authenticating using password: keys/password") + result!=null + result.success } @@ -145,10 +160,25 @@ class SSHJNodeExecutorPluginSpec extends Specification { ]) when: - plugin.executeCommand(context, command, node) + def result = plugin.executeCommand(context, command, node) then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } 1 * logger.log(3, "Authenticating using private key") + result!=null + result.success } @@ -190,10 +220,25 @@ class SSHJNodeExecutorPluginSpec extends Specification { ]) when: - plugin.executeCommand(context, command, node) + def result = plugin.executeCommand(context, command, node) then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } 1 * logger.log(3, "Authenticating using password: keys/password") + result!=null + result.success } @@ -233,10 +278,250 @@ class SSHJNodeExecutorPluginSpec extends Specification { ]) when: - plugin.executeCommand(context, command, node) + def result = plugin.executeCommand(context, command, node) then: + 1 * client.connect(_) + 1 * client.isConnected() >> true + 1 * client.startSession()>>Mock(Session){ + exec(_)>>Mock(Session.Command){ + getExitStatus()>>0 + getInputStream()>>Mock(InputStream){ + read(_)>>-1 + } + getErrorStream()>>Mock(InputStream){ + read(_)>>-1 + } + } + } 1 * logger.log(3, "Authenticating using private key") + result!=null + result.success + + + + } + + + def "error getting key"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + + def dataContext = [ + config: ["RD_TEST": "Value"] + ] + + def storage = Mock(StorageTree) { + getResource('keys/password') >> {throw new Exception("Cannot get password")} + getResource('keys/node.key') >> {throw new Exception("Cannot get key")} + } + + def framework = Mock(Framework) { + getFrameworkProjectMgr() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + getPropertyLookup() >> PropertyLookup.create(properties) + getProjectManager() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + } + + def context = ExecutionContextImpl.builder() + .framework(framework) + .executionListener(logger) + .storageTree(storage) + .dataContext(new BaseDataContext(dataContext)) + .frameworkProject("test") + .build() + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"privateKey", + "ssh-key-storage-path":"keys/node.key", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + !result.success + result.failureMessage=="Failed to read SSH Key Storage stored at path: keys/node.key" + + + } + + + def "error getting password"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + + def dataContext = [ + config: ["RD_TEST": "Value"] + ] + + def storage = Mock(StorageTree) { + getResource('keys/password') >> {throw new Exception("Cannot get password")} + getResource('keys/node.key') >> {throw new Exception("Cannot get key")} + } + + def framework = Mock(Framework) { + getFrameworkProjectMgr() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + getPropertyLookup() >> PropertyLookup.create(properties) + getProjectManager() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + } + + def context = ExecutionContextImpl.builder() + .framework(framework) + .executionListener(logger) + .storageTree(storage) + .dataContext(new BaseDataContext(dataContext)) + .frameworkProject("test") + .build() + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"password", + "ssh-password-storage-path":"keys/password", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + !result.success + result.failureMessage=="Failed to read SSH Password stored at path: keys/password" + + + } + + def "error getting key passphrase"(){ + + given: + + String[] command = ["ls -lrt"] + + def logger = Mock(ExecutionListener) { + createOverride() >> Mock(ExecutionListenerOverride) + } + + def rundeckFramework = Mock(IRundeckProject) + def properties = new Properties() + properties.setProperty("fwkprop","fwkvalue") + + def dataContext = [ + config: ["RD_TEST": "Value"] + ] + + def storage = Mock(StorageTree) { + getResource('keys/password') >> {throw new Exception("Cannot get password")} + getResource('keys/node.key') >> Mock(Resource) { + getContents() >> Mock(ResourceMeta) { + writeContent(_) >> { args -> + args[0].write('test.'.bytes) + 7L + } + } + } + } + + def framework = Mock(Framework) { + getFrameworkProjectMgr() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + getPropertyLookup() >> PropertyLookup.create(properties) + getProjectManager() >> Mock(ProjectManager) { + getFrameworkProject(_) >> rundeckFramework + } + } + + def context = ExecutionContextImpl.builder() + .framework(framework) + .executionListener(logger) + .storageTree(storage) + .dataContext(new BaseDataContext(dataContext)) + .frameworkProject("test") + .build() + + SSHClient client = Mock(SSHClient){ + getTransport()>>Mock(Transport){ + getConfig()>>SSHJDefaultConfig.init().getConfig() + } + } + + def plugin = new SSHJNodeExecutorPlugin() + plugin.sshClient = client + + def node = new NodeEntryImpl("test") + node.setAttributes(["username":"test", + "osFamily":"linux", + "hostname":"localhost", + "ssh-connect-timeout":"3", + "ssh-command-timeout":"3", + "ssh-authentication":"privateKey", + "ssh-key-storage-path":"keys/node.key", + "ssh-key-passphrase-storage-path":"keys/password", + "sudo-command-enabled":"true" + ]) + + when: + def result = plugin.executeCommand(context, command, node) + + then: + !result.success + result.failureMessage=="Failed to read SSH Passphrase stored at path: keys/password" + } diff --git a/src/test/groovy/com/plugin/sshjplugin/model/SSHJAuthenticationTest.groovy b/src/test/groovy/com/plugin/sshjplugin/model/SSHJAuthenticationTest.groovy new file mode 100644 index 0000000..6464904 --- /dev/null +++ b/src/test/groovy/com/plugin/sshjplugin/model/SSHJAuthenticationTest.groovy @@ -0,0 +1,196 @@ +package com.plugin.sshjplugin.model + +import com.dtolabs.rundeck.plugins.PluginLogger +import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile +import net.schmizz.sshj.Config +import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.common.Factory +import net.schmizz.sshj.transport.Transport +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider +import spock.lang.Specification + + +class SSHJAuthenticationTest extends Specification { + + def "authenticate with private key on filesystem no passphrase"() { + given: + SSHClient sshClient = Mock(SSHClient) + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyFilePath() >> "keys/rundeck/storage" + 0 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_, _) + 1 * sshClient.loadKeys(_) + 0 * sshClient.loadKeys(_, _) + } + + def "authenticate with private key on filesystem with passphrase"() { + given: + String passphraseStoragePath = "keys/rundeck/storage/passphrase" + + SSHClient sshClient = Mock(SSHClient) + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyFilePath() >> "keys/rundeck/storage" + 1 * getPrivateKeyPassphraseStoragePath() >> passphraseStoragePath + 1 * getPrivateKeyPassphrase(passphraseStoragePath) >> "pass" + 0 * getPrivateKeyStorage(_) + } + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 0 * sshClient.loadKeys(_) + 1 * sshClient.authPublickey(_, _) + } + + + + def "authenticate with private key Rundeck storage no passphrase"() { + given: + String keyStoragePath = "keys/rundeck/storage" + SSHClient sshClient = Mock(SSHClient){ + getTransport() >> Mock(Transport){ + getConfig() >> Mock(Config){ + getFileKeyProviderFactories() >> providerFactoriesList() + } + } + } + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyStoragePath() >> keyStoragePath + 1 * getPrivateKeyStorage(keyStoragePath) >> "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCBHpyIOm\n" + + "Y45NDeuHxAzGCUAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKUYWSH2YrYUH3IA\n" + + "t40IcM0ykM03oFRI7m+5jEK+fE4LAAAAoBIhOVxejwLYvIKIBgNQOe0j8h2nnz/+sEYUDc\n" + + "ug6KrlPxQ7kuL67It/Tb7IxAGzVWT3g3fkQMGNU/8uxRHAf5fQC9aYValFPr21g7I39OqR\n" + + "MbPXHnD8a+DwAw3ArakcZigzWqncuX5cuBgpr5+x/iXWAz0lAHJH1d5HaIsoy1K6VmMR+b\n" + + "GN7ixrjWwMVBM+Lv8DdRN5UnniX5grj6M8P0A=\n" + + "-----END OPENSSH PRIVATE KEY-----" + 0 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_,_) + + } + + def "authenticate with private key Rundeck storage and passphrase"() { + given: + String keyStoragePath = "keys/rundeck/storage" + String passphraseStoragePath = "keys/rundeck/storage/passphrase" + SSHClient sshClient = Mock(SSHClient){ + getTransport() >> Mock(Transport){ + getConfig() >> Mock(Config){ + getFileKeyProviderFactories() >> providerFactoriesList() + } + } + } + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getPrivateKeyPassphraseStoragePath() >> passphraseStoragePath + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyStoragePath() >> keyStoragePath + 1 * getPrivateKeyStorage(keyStoragePath) >> "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCBHpyIOm\n" + + "Y45NDeuHxAzGCUAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKUYWSH2YrYUH3IA\n" + + "t40IcM0ykM03oFRI7m+5jEK+fE4LAAAAoBIhOVxejwLYvIKIBgNQOe0j8h2nnz/+sEYUDc\n" + + "ug6KrlPxQ7kuL67It/Tb7IxAGzVWT3g3fkQMGNU/8uxRHAf5fQC9aYValFPr21g7I39OqR\n" + + "MbPXHnD8a+DwAw3ArakcZigzWqncuX5cuBgpr5+x/iXWAz0lAHJH1d5HaIsoy1K6VmMR+b\n" + + "GN7ixrjWwMVBM+Lv8DdRN5UnniX5grj6M8P0A=\n" + + "-----END OPENSSH PRIVATE KEY-----" + 1 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_,_) + } + + + def "if local storage path and rundeck storage path are provided, key stored on rundeck will be used"() { + given: + String keyStoragePath = "keys/rundeck/storage" + String fileSystemStoragePath = "user/key" + String passphraseStoragePath = "keys/rundeck/storage/passphrase" + SSHClient sshClient = Mock(SSHClient){ + getTransport() >> Mock(Transport){ + getConfig() >> Mock(Config){ + getFileKeyProviderFactories() >> providerFactoriesList() + } + } + } + PluginLogger pluginLogger = Mock(PluginLogger) + SSHJConnection connectionParameters = Mock(SSHJConnection){ + 1 * getPrivateKeyFilePath() >> fileSystemStoragePath + 1 * getPrivateKeyPassphraseStoragePath() >> passphraseStoragePath + 1 * getAuthenticationType() >> SSHJConnection.AuthenticationType.privateKey + 1 * getPrivateKeyStoragePath() >> keyStoragePath + 1 * getPrivateKeyStorage(keyStoragePath) >> "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCBHpyIOm\n" + + "Y45NDeuHxAzGCUAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKUYWSH2YrYUH3IA\n" + + "t40IcM0ykM03oFRI7m+5jEK+fE4LAAAAoBIhOVxejwLYvIKIBgNQOe0j8h2nnz/+sEYUDc\n" + + "ug6KrlPxQ7kuL67It/Tb7IxAGzVWT3g3fkQMGNU/8uxRHAf5fQC9aYValFPr21g7I39OqR\n" + + "MbPXHnD8a+DwAw3ArakcZigzWqncuX5cuBgpr5+x/iXWAz0lAHJH1d5HaIsoy1K6VmMR+b\n" + + "GN7ixrjWwMVBM+Lv8DdRN5UnniX5grj6M8P0A=\n" + + "-----END OPENSSH PRIVATE KEY-----" + 1 * getPrivateKeyPassphrase(_) + 0 * getPrivateKeyStorage(_) + } + + SSHJAuthentication auth = new SSHJAuthentication(connectionParameters,pluginLogger) + + when: + auth.authenticate(sshClient) + + then: + 1 * sshClient.authPublickey(_,_) + 0 * sshClient.loadKeys(_) + 0 * sshClient.loadKeys(_, _) + } + + + + private static List> providerFactoriesList() { + List> namedList = new ArrayList<>(); + namedList.add(new Factory.Named(){ + @Override + FileKeyProvider create() { + return new OpenSSHKeyV1KeyFile(); + } + @Override + String getName() { + return "OpenSSHKeyV1KeyFile" + } + }) + return namedList; + } + + + } +