From cf30aa1ab35f765114e74c66ec98d847fddbdfe6 Mon Sep 17 00:00:00 2001
From: mamunozgil <miguel.munoz-gil@hft-stuttgart.de>
Date: Mon, 3 Feb 2025 11:12:59 +0100
Subject: [PATCH] Add ssh to the backend container

---
 Dockerfile                                    | 37 ++++++---
 .../dtabackend/utils/RepoUtil.java            | 78 +++++++++++--------
 src/main/resources/application.properties     |  8 +-
 3 files changed, 81 insertions(+), 42 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 6020707..de8ed4c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
-# base image to build a JRE
+# Base image to build a JRE
 FROM eclipse-temurin:21-jdk-alpine as corretto-jdk
 
-# required for strip-debug to work
+# Required for strip-debug to work
 RUN apk add --no-cache binutils
 
 # Build small JRE image
@@ -14,14 +14,17 @@ RUN $JAVA_HOME/bin/jlink \
          --compress=2 \
          --output /customjre
 
-# main app image
+# Main app image
 FROM alpine:latest
 
 ENV JAVA_HOME=/jre
 ENV PATH="${JAVA_HOME}/bin:${PATH}"
-ENV SPRING_CONFIG_ADDITIONAL_LOCATION "file:/data/config/"
+ENV SPRING_CONFIG_ADDITIONAL_LOCATION="file:/data/config/"
 
-# copy JRE from the base image
+# Install OpenSSH client and required dependencies
+RUN apk add --no-cache openssh-client bash
+
+# Copy JRE from the base image
 COPY --from=corretto-jdk /customjre $JAVA_HOME
 
 # Add app user
@@ -35,16 +38,32 @@ ARG BUILD_NUMBER=
 RUN addgroup --gid $GID --system docker
 RUN adduser --no-create-home -u 1000 --ingroup docker --disabled-password $USER
 
-# Prepare environment.
+# Prepare environment
 # Create needed folders
-RUN mkdir /data && \
-    mkdir /data/config && \
-    chown -R $USER /data
+RUN mkdir -p /data /home/$USER/.ssh && \
+    chown -R $USER:$GID /data /home/$USER/.ssh && \
+    chmod 700 /home/$USER/.ssh
 
 VOLUME /data
 
+# Copy SSH keys from build context (MAKE SURE TO ADD THEM TO .dockerignore)
+COPY --chown=1000:$GID id_rsa /home/$USER/.ssh/id_rsa
+COPY --chown=1000:$GID known_hosts /home/$USER/.ssh/known_hosts
+
+# Set proper permissions for SSH keys
+RUN chmod 600 /home/$USER/.ssh/id_rsa && \
+    chmod 644 /home/$USER/.ssh/known_hosts && \
+    chown -R $USER:$GID /home/$USER/.ssh
+
+# Copy application JAR
 COPY --chown=1000:$GID target/dta-backend-$BUILD_NUMBER.jar app.jar
 
+# Switch to non-root user
 USER 1000:$GID
 
+# Set environment variables for SVNKit SSH authentication
+ENV SVN_SSH="/usr/bin/ssh"
+ENV SVN_SSH_KEY="/home/$USER/.ssh/id_rsa"
+
+# Start application
 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java
index 073e055..8f4678c 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/utils/RepoUtil.java
@@ -8,10 +8,7 @@ import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
 import org.springframework.stereotype.Component;
 import org.springframework.util.FileSystemUtils;
-import org.tmatesoft.svn.core.SVNDepth;
-import org.tmatesoft.svn.core.SVNException;
-import org.tmatesoft.svn.core.SVNURL;
-import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
+import org.tmatesoft.svn.core.*;
 import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
 import org.tmatesoft.svn.core.wc.SVNWCUtil;
 import org.tmatesoft.svn.core.wc2.SvnCheckout;
@@ -20,7 +17,6 @@ import org.tmatesoft.svn.core.wc2.SvnTarget;
 
 import java.io.File;
 import java.io.IOException;
-import java.net.URL;
 import java.util.regex.Matcher;
 
 @Component
@@ -35,19 +31,19 @@ public class RepoUtil {
             LOG.error("Invalid repository URL format.");
             return;
         }
-    
+
         String repoUrl = config.group(1);
         String username = config.group(2);
-        String password = config.group(3);
-    
-        LOG.debug(String.format("Cloning repository: %s", repoUrl));
-    
+        String password = config.group(3);  // Not used for SSH authentication
+
+        LOG.debug("Cloning repository: {}", repoUrl);
+
         File targetDirectory = new File(targetPath);
         if (targetDirectory.exists()) {
             LOG.debug("Target directory exists, deleting it.");
             FileSystemUtils.deleteRecursively(targetDirectory);
         }
-    
+
         File checkoutDirectory = targetDirectory;
         if (!subDir.isEmpty()) {
             checkoutDirectory = new File(targetPath + "_checkout");
@@ -56,49 +52,65 @@ public class RepoUtil {
                 FileSystemUtils.deleteRecursively(checkoutDirectory);
             }
         }
-    
+
         try {
             LOG.debug("Preparing clone...");
+
             if (repoUrl.endsWith(".git")) {
+                // GIT Repository Clone
                 CloneCommand cloneCommand = Git.cloneRepository()
                         .setDirectory(checkoutDirectory)
                         .setURI(repoUrl);
-    
+
                 if (!"none".equals(username) && !"none".equals(password)) {
                     LOG.debug("Setting Git credentials.");
                     cloneCommand.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password));
                 }
-    
+
                 LOG.debug("Cloning Git repository...");
                 cloneCommand.call().close();
+
             } else {
+                // SVN Repository Clone (including svn+ssh support)
                 SvnOperationFactory operationFactory = new SvnOperationFactory();
                 try {
-                    URL sourceUrl = new URL(repoUrl);
-                    SVNURL svnUrl = SVNURL.create(sourceUrl.getProtocol(), null, sourceUrl.getHost(), sourceUrl.getPort(), sourceUrl.getPath(), false);
+                    SVNURL svnUrl = SVNURL.parseURIEncoded(repoUrl);
                     SvnCheckout checkout = operationFactory.createCheckout();
-                    checkout.setSingleTarget(SvnTarget.fromFile(new File(targetPath)));
+                    checkout.setSingleTarget(SvnTarget.fromFile(checkoutDirectory));
                     checkout.setSource(SvnTarget.fromURL(svnUrl));
                     checkout.setDepth(SVNDepth.INFINITY);
-    
-                    if (!"none".equals(username) && !"none".equals(password)) {
-                        if (repoUrl.startsWith("https")) {
-                            LOG.debug("Setting SVN credentials for HTTPS.");
-                            operationFactory.setAuthenticationManager(new BasicAuthenticationManager(username, password));
-                        } else if (repoUrl.startsWith("svn+ssh")) {
-                            LOG.debug("Setting SVN credentials for SSH.");
-                            ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(username, password.toCharArray());
-                            operationFactory.setAuthenticationManager(authManager);
+
+                    // **Use SSH authentication if URL starts with svn+ssh://**
+                    if (repoUrl.startsWith("svn+ssh://")) {
+                        LOG.debug("Setting up SSH authentication for SVN...");
+
+                        String sshPrivateKeyPath = "/home/appuser/.ssh/id_rsa"; 
+                        File privateKeyFile = new File(sshPrivateKeyPath);
+
+                        if (!privateKeyFile.exists()) {
+                            LOG.error("SSH private key not found at: {}", sshPrivateKeyPath);
+                            throw new SVNException(SVNErrorMessage.create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE, "SSH key not found"));
                         }
+
+                        ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(
+                            new File("/home/appuser/.ssh/id_rsa"),  // Explicit SSH Key
+                            username,
+                            null,
+                            new File("/home/appuser/.ssh/known_hosts"),  // Explicit Known Hosts
+                            false
+                        );
+
+                        operationFactory.setAuthenticationManager(authManager);
                     }
-    
+
                     LOG.debug("Performing SVN checkout...");
                     checkout.run();
                 } finally {
-                    operationFactory.dispose(); 
+                    operationFactory.dispose();
                 }
             }
-    
+
+            // Handle subdirectory case
             if (!subDir.isEmpty()) {
                 File sourceSubDir = new File(checkoutDirectory, subDir);
                 if (sourceSubDir.exists()) {
@@ -107,10 +119,12 @@ public class RepoUtil {
                     LOG.error("Specified subdirectory does not exist.");
                 }
             }
-    
-            LOG.debug(String.format("Repository cloned from %s to %s", repoUrl, targetDirectory));
+
+            LOG.debug("Repository successfully cloned from {} to {}", repoUrl, targetDirectory);
+
         } catch (IOException | GitAPIException | SVNException e) {
             LOG.error("Error while cloning repository: " + repoUrl, e);
         }
-    }       
+    }
 }
+
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 315cfe1..6b33bca 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -14,4 +14,10 @@ host.tests.tmp.dir=${tests.tmp.dir}
 data.dir=/data
 data.dir.test.folder.name=UnitTests
 
-logging.level.de.hftstuttgart=TRACE
\ No newline at end of file
+logging.level.de.hftstuttgart=TRACE
+
+################################################
+# SSH directory
+################################################
+
+ssh.file=/home/appuser/.ssh/id_rsa 
\ No newline at end of file
-- 
GitLab