Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pty into session and more sudo command logging #19

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file added bin/main/com/plugin/sshjplugin/SSHJBuilder.class
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
528 changes: 528 additions & 0 deletions bin/test/com/plugin/sshjplugin/SSHJNodeExecutorPluginSpec.groovy

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions bin/test/com/plugin/sshjplugin/model/SSHJAuthenticationTest.groovy
Original file line number Diff line number Diff line change
@@ -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<Factory.Named<FileKeyProvider>> providerFactoriesList() {
List<Factory.Named<FileKeyProvider>> namedList = new ArrayList<>();
namedList.add(new Factory.Named<FileKeyProvider>(){
@Override
FileKeyProvider create() {
return new OpenSSHKeyV1KeyFile();
}
@Override
String getName() {
return "OpenSSHKeyV1KeyFile"
}
})
return namedList;
}


}

Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
83 changes: 48 additions & 35 deletions src/main/java/com/plugin/sshjplugin/model/SSHJAuthentication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/plugin/sshjplugin/model/SSHJConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public String getPrivateKeyPath() throws IOException {

}else{

privateKeyFile = getPrivateKeyfilePath();
privateKeyFile = getPrivateKeyFilePath();
context.getExecutionListener().log(3, "[sshj-debug] Using ssh keyfile: " + privateKeyFile);
}

Expand All @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/plugin/sshjplugin/model/SSHJExec.java
Original file line number Diff line number Diff line change
Expand Up @@ -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" );

Expand Down Expand Up @@ -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)
Expand Down
Loading