Support uploaded keys with native ssh
authorDmitry Neverov <dmitry.neverov@gmail.com>
Tue, 5 Dec 2017 11:31:38 +0000 (12:31 +0100)
committerDmitry Neverov <dmitry.neverov@gmail.com>
Tue, 5 Dec 2017 11:31:38 +0000 (12:31 +0100)
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/GitCommandLine.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/UpdaterImpl.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/ScriptGen.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/UnixScriptGen.java
git-agent/src/jetbrains/buildServer/buildTriggers/vcs/git/agent/command/impl/WinScriptGen.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/command/FetchCommandImplTest.java

index ecd8886f081932d9ff5e9e041c4d8ac3d8c94ffc..3ff722364ba2f6b94f3545f215f0471b9bde5ad7 100644 (file)
@@ -18,7 +18,8 @@ package jetbrains.buildServer.buildTriggers.vcs.git.agent;
 
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.openapi.util.SystemInfo;
-import com.intellij.openapi.util.io.FileUtil;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.KeyPair;
 import jetbrains.buildServer.ExecResult;
 import jetbrains.buildServer.LineAwareByteArrayOutputStream;
 import jetbrains.buildServer.agent.BuildInterruptReason;
@@ -27,14 +28,15 @@ import jetbrains.buildServer.buildTriggers.vcs.git.AuthenticationMethod;
 import jetbrains.buildServer.buildTriggers.vcs.git.GitUtils;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.ScriptGen;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.*;
+import jetbrains.buildServer.ssh.TeamCitySshKey;
 import jetbrains.buildServer.ssh.VcsRootSshKeyManager;
+import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.vcs.VcsException;
+import jetbrains.buildServer.vcs.VcsRoot;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
+import java.io.*;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -117,6 +119,9 @@ public class GitCommandLine extends GeneralCommandLine {
         }
       }
       if (settings.isUseNativeSsh()) {
+        if (!myGitVersion.isLessThan(UpdaterImpl.MIN_GIT_SSH_COMMAND) && authSettings.getAuthMethod() == AuthenticationMethod.TEAMCITY_SSH_KEY) {
+          configureGitSshCommand(authSettings);
+        }
         return CommandUtil.runCommand(this, settings.getTimeout());
       } else {
         SshHandler h = new SshHandler(mySsh, mySshKeyManager, authSettings, this, myTmpDir, myCtx.getSshMacType());
@@ -132,6 +137,74 @@ public class GitCommandLine extends GeneralCommandLine {
   }
 
 
+  private void configureGitSshCommand(final AuthSettings authSettings) throws VcsException {
+    //Git has 2 environment variables related to GIT_SSH and GIT_SSH_COMMAND.
+    //We use GIT_SSH_COMMAND because git resolves the executable specified in it,
+    //i.e. it finds the 'ssh' executable which is not in the PATH on windows be default.
+
+    //We specify the following command:
+    //
+    //  GIT_SSH_COMMAND=ssh -i "<path to decrypted key>" (-o "StrictHostKeyChecking=no")
+    //
+    //The key is decrypted by us because on MacOS ssh seems to ignore the SSH_ASKPASS and
+    //runs the MacOS graphical keychain helper. Disabling it via the -o "KeychainIntegration=no"
+    //option results in the 'Bad configuration option: keychainintegration' error.
+    String keyId = authSettings.getTeamCitySshKeyId();
+    if (keyId != null && mySshKeyManager != null) {
+      VcsRoot root = authSettings.getRoot();
+      if (root != null) {
+        TeamCitySshKey key = mySshKeyManager.getKey(root);
+        if (key != null) {
+          try {
+            final File privateKey = FileUtil.createTempFile(myTmpDir, "key", "", true);
+            addPostAction(new Runnable() {
+              @Override
+              public void run() {
+                FileUtil.delete(privateKey);
+              }
+            });
+            FileUtil.writeFileAndReportErrors(privateKey, new String(key.getPrivateKey()));
+            if (key.isEncrypted()) {
+              KeyPair keyPair = KeyPair.load(new JSch(), privateKey.getAbsolutePath());
+              OutputStream out = null;
+              try {
+                out = new BufferedOutputStream(new FileOutputStream(privateKey));
+                if (!keyPair.decrypt(authSettings.getPassphrase())) {
+                  throw new VcsException("Wrong SSH key passphrase");
+                }
+                keyPair.writePrivateKey(out, null);
+              } catch (Exception e) {
+                FileUtil.delete(privateKey);
+                throw e;
+              } finally {
+                FileUtil.close(out);
+              }
+            }
+            //set permissions to 600, without that ssh client rejects the key on *nix
+            privateKey.setReadable(false, false);
+            privateKey.setReadable(true, true);
+            privateKey.setWritable(false, false);
+            privateKey.setWritable(true, true);
+
+            String privateKeyPath = privateKey.getAbsolutePath().replace('\\', '/');
+
+            StringBuilder gitSshCommand = new StringBuilder();
+            gitSshCommand.append("ssh -i \"").append(privateKeyPath).append("\"");
+            if (authSettings.isIgnoreKnownHosts()) {
+              gitSshCommand.append(" -o \"StrictHostKeyChecking=no\"");
+            }
+            addEnvParam("GIT_SSH_COMMAND", gitSshCommand.toString());
+          } catch (Exception e) {
+            if (e instanceof VcsException)
+              throw (VcsException) e;
+            throw new VcsException(e);
+          }
+        }
+      }
+    }
+  }
+
+
   @NotNull
   public GitVersion getGitVersion() {
     return myGitVersion;
index 6885e7d4802371932cd37993f805f831dc4e323d..4875e01f6c3e3bf183c62dbc4a41f8309d7c64b9 100644 (file)
@@ -61,6 +61,7 @@ public class UpdaterImpl implements Updater {
   private final static GitVersion GIT_WITH_FORCE_SUBMODULE_UPDATE = new GitVersion(1, 7, 6);
   public final static GitVersion GIT_WITH_SPARSE_CHECKOUT = new GitVersion(1, 7, 4);
   public final static GitVersion BROKEN_SPARSE_CHECKOUT = new GitVersion(2, 7, 0);
+  public final static GitVersion MIN_GIT_SSH_COMMAND = new GitVersion(2, 3, 0);//GIT_SSH_COMMAND was introduced in git 2.3.0
   /**
    * Git version supporting an empty credential helper - the only way to disable system/global/local cred helper
    */
index e2ad01aafcf96f5e47370a6606e2dc908a0db3a6..c33dcac8e013f4a139cf33500044ddb5f2b9f6b5 100644 (file)
@@ -23,6 +23,7 @@ import jetbrains.buildServer.buildTriggers.vcs.git.agent.CredentialsHelper;
 import jetbrains.buildServer.util.FileUtil;
 import jetbrains.buildServer.util.StringUtil;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.io.*;
 
@@ -37,6 +38,9 @@ public abstract class ScriptGen {
   @NotNull
   public abstract File generateAskPass(@NotNull AuthSettings authSettings) throws IOException;
 
+  @NotNull
+  public abstract File generateAskPass(@Nullable String password) throws IOException;
+
 
   @NotNull
   public File generateCredentialsHelper() throws IOException {
index fae2de4e8b5599e17359cb184db48dcdf44e8baf..4a2ad30f125b712244f83eeb535a78fbf7eed087 100644 (file)
@@ -20,6 +20,7 @@ import com.intellij.openapi.util.io.FileUtil;
 import jetbrains.buildServer.buildTriggers.vcs.git.AuthSettings;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.ScriptGen;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
 import java.io.FileWriter;
@@ -38,12 +39,19 @@ public class UnixScriptGen extends ScriptGen {
 
   @NotNull
   public File generateAskPass(@NotNull AuthSettings authSettings) throws IOException {
+    return generateAskPass(authSettings.getPassword());
+  }
+
+
+  @NotNull
+  @Override
+  public File generateAskPass(@Nullable String password) throws IOException {
     File script = FileUtil.createTempFile(myTempDir, "pass", "", true);
     PrintWriter out = null;
     try {
       out = new PrintWriter(new FileWriter(script));
       out.println("#!/bin/sh");
-      out.println("printf " + myEscaper.escape(authSettings.getPassword()));
+      out.println("printf " + myEscaper.escape(password));
       if (!script.setExecutable(true))
         throw new IOException("Cannot make askpass script executable");
     } finally {
index 834e333bca1b9d7991505da09800ad4da2b3c34d..8377872620c4e2678f62f87bfa3341a7e49e552a 100644 (file)
@@ -20,6 +20,7 @@ import com.intellij.openapi.util.io.FileUtil;
 import jetbrains.buildServer.buildTriggers.vcs.git.AuthSettings;
 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.ScriptGen;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
 import java.io.FileWriter;
@@ -38,11 +39,16 @@ public class WinScriptGen extends ScriptGen {
 
   @NotNull
   public File generateAskPass(@NotNull AuthSettings authSettings) throws IOException {
+    return generateAskPass(authSettings.getPassword());
+  }
+
+  @NotNull
+  public File generateAskPass(@Nullable String password) throws IOException {
     File script = FileUtil.createTempFile(myTempDir, "pass", ".bat", true);
     PrintWriter out = null;
     try {
       out = new PrintWriter(new FileWriter(script));
-      out.println("@echo " + myEscaper.escape(authSettings.getPassword()));
+      out.println("@echo " + myEscaper.escape(password));
       if (!script.setExecutable(true))
         throw new IOException("Cannot make askpass script executable");
     } finally {
index f97e666e79329eeb29d40a575163adccc3b40cfa..588d70aad0dd01c750c9026490100740174bb671 100644 (file)
@@ -58,6 +58,11 @@ public class FetchCommandImplTest {
         return new File(".");
       }
       @NotNull
+      @Override
+      public File generateAskPass(@NotNull final String password) throws IOException {
+        return new File(".");
+      }
+      @NotNull
       protected String getCredHelperTemplate() {
         return "";
       }