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..986ecea 100644 --- a/src/main/java/com/plugin/sshjplugin/model/SSHJAuthentication.java +++ b/src/main/java/com/plugin/sshjplugin/model/SSHJAuthentication.java @@ -1,17 +1,22 @@ package com.plugin.sshjplugin.model; import com.dtolabs.rundeck.plugins.PluginLogger; +import com.dtolabs.utils.Streams; 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,35 +33,34 @@ void authenticate(final SSHClient ssh) throws IOException { switch (authenticationType) { case privateKey: - try{ - privateKeyFile = connectionParameters.getPrivateKeyPath(); - logger.log(3, "Authenticating using private key"); + logger.log(3, "Authenticating using private key"); - } catch (IOException e) { - logger.log(0, "Failed to get SSH key: " + e.getMessage()); + String privateKeyPath = connectionParameters.getPrivateKeyStoragePath(); + try{ + privateKeyContent = connectionParameters.getPrivateKeyStorage(privateKeyPath); + } catch (Exception e) { + throw new SSHJBuilder.BuilderException("Failed to read SSH Key Storage stored at path: " + privateKeyPath); } String passphrasePath = connectionParameters.getPrivateKeyPassphraseStoragePath(); try{ passphrase = connectionParameters.getPrivateKeyPassphrase(passphrasePath); - } catch (IOException e) { - logger.log(0, "Failed to read SSH Passphrase stored at path: " + passphrasePath); + } catch (Exception e) { + throw new SSHJBuilder.BuilderException("Failed to read SSH Passphrase stored at path: " + passphrasePath); } - KeyProvider key = null; - if (null != privateKeyFile && !"".equals(privateKeyFile)) { - if (!new File(privateKeyFile).exists()) { - throw new SSHJBuilder.BuilderException("SSH Keyfile does not exist: " + privateKeyFile); - } - logger.log(3, "[sshj-debug] Using ssh keyfile: " + privateKeyFile); - } + KeyFormat format = KeyProviderUtil.detectKeyFileFormat(privateKeyContent,true); + FileKeyProvider keys = Factory.Named.Util.create(ssh.getTransport().getConfig().getFileKeyProviderFactories(), format.toString()); + + logger.log(3, "[sshj-debug] Using ssh keyfile: " + privateKeyPath); if (passphrase == null) { - key = ssh.loadKeys(privateKeyFile); + keys.init(new StringReader(privateKeyContent), null); } else { - key = ssh.loadKeys(privateKeyFile, passphrase); + 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(); @@ -65,15 +69,11 @@ void authenticate(final SSHClient ssh) throws IOException { } 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..95935bf 100644 --- a/src/main/java/com/plugin/sshjplugin/model/SSHJConnection.java +++ b/src/main/java/com/plugin/sshjplugin/model/SSHJConnection.java @@ -21,6 +21,8 @@ static enum AuthenticationType { InputStream getPrivateKeyStorageData(String path); + String getPrivateKeyStorage(String path) throws IOException; + String getPasswordStoragePath(); String getPassword(String path) throws IOException; diff --git a/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java b/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java index 3c45c44..ba8ff13 100644 --- a/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java +++ b/src/main/java/com/plugin/sshjplugin/model/SSHJConnectionParameters.java @@ -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,14 +84,18 @@ 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); } } + @Override + public String getPrivateKeyStorage(String path) throws IOException { + return propertyResolver.getPrivateKeyStorage(path); + } + String getPrivateKeyfilePath() { String path = propertyResolver.resolve(SSHJNodeExecutorPlugin.NODE_ATTR_SSH_KEYPATH); if (path == null && framework.hasProperty(Constants.SSH_KEYPATH_PROP)) { 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" + }