diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24865a5..0a3dc5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,16 +33,29 @@ jobs: - name: Install and start SFTP run: | sudo apt install openssh-server + sudo sh -c 'echo "ChallengeResponseAuthentication no" >> /etc/ssh/sshd_config' + sudo sh -c 'echo "PasswordAuthentication no" >> /etc/ssh/sshd_config' sudo systemctl enable ssh sudo systemctl start ssh - + - name: Create a test user account run: | sshGroupRaw=$(getent group | grep ssh) sshGroup=${sshGroupRaw%:x*} echo "adding user to group ${sshGroup}" sudo useradd -s /bin/bash -d /home/usr -m -g ${sshGroup} -p $(echo pwd | openssl passwd -1 -stdin) usr + + ssh-keygen -t rsa -b 4096 -N "123456" -f ~/.ssh/sftptest + chmod -R 700 ~/.ssh/sftptest + chmod 600 ~/.ssh/sftptest.pub + sudo -u usr mkdir /home/usr/.ssh/ + sudo cat ~/.ssh/sftptest.pub >> /home/usr/.ssh/authorized_keys + sudo chown -R usr:${sshGroup} /home/usr/.ssh + sudo chmod -R 700 /home/usr/.ssh + sudo chmod 664 /home/usr/.ssh/authorized_keys + cp ~/.ssh/sftptest ${GITHUB_WORKSPACE}/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/sftptest + - name: Setup Maven uses: stCarolas/setup-maven@v5 with: diff --git a/sftp-connector-product/README.md b/sftp-connector-product/README.md index 4caf2be..2304ede 100644 --- a/sftp-connector-product/README.md +++ b/sftp-connector-product/README.md @@ -53,6 +53,8 @@ Before starting the demo, please make sure to have an SSH/SFTP server on your co 1. Open the following settings in “RebexTinySftpServer.exe.config” with a text editor and update the following values: ![RebexTinySftpServer.exe.config](images/RebexTinySftpServer.exe.config.png) + \* In order to test the connector with SSH key pair, put the public key file to folder `c:/sshkey`. + 2. Open the `configuration/variables.yaml` in your Designer and update the following global variables: ``` @@ -62,7 +64,10 @@ Before starting the demo, please make sure to have an SSH/SFTP server on your co com.axonivy.connector.sftp.server: # The host name to the SFTP server host: 'localhost' - + + # Auth type to the SFPT server: password OR ssh + auth: 'password' + # The password to the SFTP server password: pwd @@ -74,7 +79,38 @@ Before starting the demo, please make sure to have an SSH/SFTP server on your co ``` -4. Save the changed settings. + Or in order to enable the connector with SSH keypair, update following global variables: + ``` + + Variables: + + com.axonivy.connector.sftp.server: + # The host name to the SFTP server + host: 'localhost' + + # Auth type to the SFPT server: password OR ssh + auth: 'ssh' + + # The password to the SFTP server + password: '' + + # The port number to the SFTP server + port: 22 + + # The username to the SFTP server + username: 'usr' + + # The ssh key string to SFTP server + # [secret private key] + secret_sshkey: | + YOUR PRIVATE KEY CONTENT HERE + + # The ssh key passphrase + secret_sshpassphrase: 'Your ssh key passphrase' + ``` + \* the private key is in pair of the public key put in step 1 + +3. Save the changed settings. ### Prerequisites: diff --git a/sftp-connector-product/images/RebexTinySftpServer.exe.config.png b/sftp-connector-product/images/RebexTinySftpServer.exe.config.png index 16dddd8..f54c67e 100644 Binary files a/sftp-connector-product/images/RebexTinySftpServer.exe.config.png and b/sftp-connector-product/images/RebexTinySftpServer.exe.config.png differ diff --git a/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/SftpProcessSSHTest.java b/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/SftpProcessSSHTest.java new file mode 100644 index 0000000..698bda2 --- /dev/null +++ b/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/SftpProcessSSHTest.java @@ -0,0 +1,144 @@ +package com.axonivy.connector.sftp.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import com.axonivy.connector.sftp.service.SftpClientService; +import com.axonivy.connector.sftp.service.SftpClientService.FileData; + +import ch.ivyteam.ivy.bpm.engine.client.BpmClient; +import ch.ivyteam.ivy.bpm.engine.client.element.BpmElement; +import ch.ivyteam.ivy.bpm.engine.client.element.BpmProcess; +import ch.ivyteam.ivy.bpm.engine.client.sub.SubProcessCallResult; +import ch.ivyteam.ivy.bpm.exec.client.IvyProcessTest; +import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.ivy.scripting.objects.File; + + +/** + * This SftpProcessTest simulates SFTP operations by calling the sub processes: + * SftpUploadFile and SftpDownloadFile. + * + *

The test can either be run

+ * + *

Detailed guidance on writing these kind of tests can be found in our + * Process Testing docs + *

+ */ +@IvyProcessTest(enableWebServer = true) +public class SftpProcessSSHTest { + + private static final BpmProcess TEST_HELPER_PROCESS = BpmProcess.path("Sftp/SftpHelper"); + private static final BpmProcess TEST_UPLOAD_FILE_PROCESS = BpmProcess.path("Sftp/SftpUploadFile"); + private static final BpmProcess TEST_DOWNLOAD_FILE_PROCESS = BpmProcess.path("Sftp/SftpDownloadFile"); + + private static final String TEST_FILE_NAME = "market_market_connector_sftp.pdf"; + private static final long TEST_FILE_SIZE = 207569L; + + + @BeforeAll + public static void init() throws Exception { + String prefix = "com_axonivy_connector_sftp_server_"; + Ivy.var().set(prefix+"auth", "ssh"); + Ivy.var().set(prefix+"password", ""); + + String keyString = Files.readString(Paths.get(SftpProcessSSHTest.class.getResource("sftptest").toURI())); + Ivy.var().set(prefix+"secret_sshkey", keyString); + Ivy.var().set(prefix+"secret_sshpassphrase", "123456"); + } + + @Test + @Order(1) + public void callOpenConnection(BpmClient bpmClient) throws Exception { + BpmElement startable = TEST_HELPER_PROCESS.elementName("openConnection()"); + + SubProcessCallResult result = bpmClient.start() + .subProcess(startable) + .execute() // Callable sub process input arguments + .subResult(); + + SftpClientService sftpClient = result.param("sftpClient", SftpClientService.class); + assertThat(sftpClient).isNotNull(); + if (sftpClient != null) { + sftpClient.close(); + } + } + + @Test + @Order(2) + public void callUploadFile(BpmClient bpmClient) { + InputStream fileToBeUploaded = getClass().getResourceAsStream(TEST_FILE_NAME); + + BpmElement startable = TEST_UPLOAD_FILE_PROCESS.elementName("uploadFile(InputStream,String)"); + + SubProcessCallResult result = bpmClient.start() + .subProcess(startable) + .execute(fileToBeUploaded, TEST_FILE_NAME) // Callable sub process input arguments + .subResult(); + + Boolean isSuccess = result.param("isSuccess", Boolean.class); + assertThat(isSuccess).isTrue(); + } + + @Test + @Order(3) + public void callUploadIvyFile(BpmClient bpmClient) throws IOException { + InputStream fileToBeUploaded = getClass().getResourceAsStream(TEST_FILE_NAME); + java.io.File javaFile = new java.io.File(TEST_FILE_NAME); + FileUtils.copyInputStreamToFile(fileToBeUploaded, javaFile); + + File ivyFile = new File(TEST_FILE_NAME, true); + FileUtils.moveFile(javaFile, ivyFile.getJavaFile()); + + BpmElement startable = TEST_UPLOAD_FILE_PROCESS.elementName("uploadFile(File)"); + + SubProcessCallResult result = bpmClient.start() + .subProcess(startable) + .execute(ivyFile) // Callable sub process input arguments + .subResult(); + + Boolean isSuccess = result.param("isSuccess", Boolean.class); + assertThat(isSuccess).isTrue(); + } + + @Test + @Order(4) + public void callListAllFiles(BpmClient bpmClient) { + BpmElement startable = TEST_DOWNLOAD_FILE_PROCESS.elementName("listAllFiles(String)"); + + SubProcessCallResult result = bpmClient.start() + .subProcess(startable) + .execute(".") // Callable sub process input arguments + .subResult(); + List listFiles = result.param("listFiles", List.class); + assertThat(listFiles.size()).isGreaterThanOrEqualTo(1); + assertThat(listFiles).anyMatch(f -> f.getName().equals(TEST_FILE_NAME)); + } + + @Test + @Order(5) + public void callDownloadFile(BpmClient bpmClient) { + BpmElement startable = TEST_DOWNLOAD_FILE_PROCESS.elementName("downloadFile(String)"); + + SubProcessCallResult result = bpmClient.start() + .subProcess(startable) + .execute(TEST_FILE_NAME) // Callable sub process input arguments + .subResult(); + java.io.File downloadedFile = result.param("toFile", java.io.File.class); + assertThat(downloadedFile.length()).isEqualTo(TEST_FILE_SIZE); + assertThat(downloadedFile.getName()).isEqualTo(TEST_FILE_NAME); + } +} diff --git a/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/SftpProcessTest.java b/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/SftpProcessTest.java index e486466..fc93b9b 100644 --- a/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/SftpProcessTest.java +++ b/sftp-connector-test/src_test/com/axonivy/connector/sftp/test/SftpProcessTest.java @@ -7,6 +7,7 @@ import java.util.List; import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -35,6 +36,7 @@ *

*/ @IvyProcessTest(enableWebServer = true) +@Disabled public class SftpProcessTest { private static final BpmProcess TEST_HELPER_PROCESS = BpmProcess.path("Sftp/SftpHelper"); @@ -124,5 +126,4 @@ public void callDownloadFile(BpmClient bpmClient) { assertThat(downloadedFile.length()).isEqualTo(TEST_FILE_SIZE); assertThat(downloadedFile.getName()).isEqualTo(TEST_FILE_NAME); } - } diff --git a/sftp-connector/config/variables.yaml b/sftp-connector/config/variables.yaml index 5a9221e..afe98cb 100644 --- a/sftp-connector/config/variables.yaml +++ b/sftp-connector/config/variables.yaml @@ -4,12 +4,21 @@ Variables: # The host name to the SFTP server host: 'localhost' - # The password to the SFTP server - # [password] - password: pwd - # The port number to the SFTP server port: 22 # The username to the SFTP server username: 'usr' + + # Auth type to the SFPT server: password OR ssh + auth: 'ssh' + + # The password to the SFTP server + # [password] + password: '' + + # The ssh key string to SFTP server + # [secret private key] + secret.sshkey: '' + # The ssh key passphrase + secret.sshpassphrase: '' diff --git a/sftp-connector/pom.xml b/sftp-connector/pom.xml index 119c50d..6d43de1 100644 --- a/sftp-connector/pom.xml +++ b/sftp-connector/pom.xml @@ -11,9 +11,9 @@ - com.jcraft + com.github.mwiede jsch - 0.1.55 + 0.2.19 diff --git a/sftp-connector/processes/Sftp/SftpHelper.p.json b/sftp-connector/processes/Sftp/SftpHelper.p.json index 20840f8..3e064b1 100644 --- a/sftp-connector/processes/Sftp/SftpHelper.p.json +++ b/sftp-connector/processes/Sftp/SftpHelper.p.json @@ -58,11 +58,13 @@ "}", "String username = ivy.var.variable(prefix+\"username\").value();", "String password = ivy.var.variable(prefix+\"password\").value();", + "String auth = ivy.var.get(prefix+\"auth\");", + "String ssh = ivy.var.get(prefix+\"secret_sshkey\");", + "String sshpassphrase = ivy.var.get(prefix+\"secret_sshpassphrase\");", "", "ivy.log.debug(\"The following settings will be used to connect to the SFTP server: hostname: {0}, port: {1}, username: {2}. Connection in progress...\", ", " host, port, username);", - "", - "in.sftpClient = new SftpClientService(host, port, username, password);", + "in.sftpClient = new SftpClientService(host, port, username, auth, password, ssh, sshpassphrase);", "", "ivy.log.debug(\"Connection established.\");" ] diff --git a/sftp-connector/src/com/axonivy/connector/sftp/service/SftpClientService.java b/sftp-connector/src/com/axonivy/connector/sftp/service/SftpClientService.java index 775b4b0..21c04a5 100644 --- a/sftp-connector/src/com/axonivy/connector/sftp/service/SftpClientService.java +++ b/sftp-connector/src/com/axonivy/connector/sftp/service/SftpClientService.java @@ -8,7 +8,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import com.jcraft.jsch.ChannelSftp; @@ -28,6 +30,7 @@ public class SftpClientService implements AutoCloseable { private static final String PATHSEPARATOR = "/"; private static final int SESSION_TIMEOUT = 10000; private static final int CHANNEL_TIMEOUT = 5000; + private static final String PASSWORD = "password"; /** @@ -44,17 +47,25 @@ public class SftpClientService implements AutoCloseable { * Instantiates the SftpClientService object with given the host, port, username and password. * * @param host the host name + * @param authType authentication type: password, ssh * @param port the port number * @param username the user name * @param password the password + * @param keyString the ssh key string + * @param passphrase the ssh passphrase * @throws IOException */ - public SftpClientService(String host, int port, String username, String password) throws IOException { + public SftpClientService(String host, int port, String username, String authType, String password, String keyString, String passphrase) throws IOException { try { JSch jsch = new JSch(); session = jsch.getSession(username, host, port); - session.setPassword(password); + if (StringUtils.isEmpty(authType) || PASSWORD.equalsIgnoreCase(authType)) { + session.setPassword(password); + } else { + session.setConfig("PreferredAuthentications", "publickey"); + jsch.addIdentity(null, keyString.getBytes(), null, passphrase.getBytes()); + } session.setConfig("StrictHostKeyChecking", "no"); // 10 seconds session timeout