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