diff --git a/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactAccount.java b/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactAccount.java index b93eae1bebe..0143c5dc809 100644 --- a/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactAccount.java +++ b/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactAccount.java @@ -26,5 +26,9 @@ public class GitRepoArtifactAccount implements ArtifactAccount { private String username; private String password; - // TODO(ethanfrogers): add support for SSH keys + private String token; + private String sshPrivateKeyFilePath; + private String sshPrivateKeyPassphrase; + private String sshKnownHostsFilePath; + private boolean sshTrustUnknownHosts; } diff --git a/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactCredentials.java b/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactCredentials.java index de0b1f8bfac..c6301c0c0d7 100644 --- a/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactCredentials.java +++ b/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/gitRepo/GitRepoArtifactCredentials.java @@ -16,9 +16,17 @@ package com.netflix.spinnaker.clouddriver.artifacts.gitRepo; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; import com.netflix.spinnaker.clouddriver.artifacts.config.ArtifactCredentials; import com.netflix.spinnaker.kork.artifacts.model.Artifact; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; @@ -35,7 +43,13 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.archive.TgzFormat; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.SshTransport; +import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.util.FS; @Slf4j public class GitRepoArtifactCredentials implements ArtifactCredentials { @@ -44,11 +58,40 @@ public class GitRepoArtifactCredentials implements ArtifactCredentials { @Getter private final String name; private final String username; private final String password; + private final String token; + private final String sshPrivateKeyFilePath; + private final String sshPrivateKeyPassphrase; + private final String sshKnownHostsFilePath; + private final boolean sshTrustUnknownHosts; + private final AuthType authType; + + private enum AuthType { + HTTP, + TOKEN, + SSH, + NONE + } public GitRepoArtifactCredentials(GitRepoArtifactAccount account) { this.name = account.getName(); this.username = account.getUsername(); this.password = account.getPassword(); + this.token = account.getToken(); + this.sshPrivateKeyFilePath = account.getSshPrivateKeyFilePath(); + this.sshPrivateKeyPassphrase = account.getSshPrivateKeyPassphrase(); + this.sshKnownHostsFilePath = account.getSshKnownHostsFilePath(); + this.sshTrustUnknownHosts = account.isSshTrustUnknownHosts(); + + if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) { + authType = AuthType.HTTP; + } else if (!StringUtils.isEmpty(token)) { + authType = AuthType.TOKEN; + } else if (!StringUtils.isEmpty(sshPrivateKeyFilePath)) { + authType = AuthType.SSH; + } else { + authType = AuthType.NONE; + } + ArchiveCommand.registerFormat("tgz", new TgzFormat()); } @@ -58,6 +101,14 @@ public InputStream download(Artifact artifact) throws IOException { Path stagingPath = Paths.get(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()); + if (!isValidReference(repoReference)) { + throw new IOException( + "Artifact reference " + + repoReference + + " is invalid for artifact account with auth type " + + authType); + } + try (Closeable ignored = () -> FileUtils.deleteDirectory(stagingPath.toFile())) { log.info("Cloning git/repo {} into {}", repoReference, stagingPath.toString()); Git localRepository = clone(artifact, stagingPath); @@ -66,12 +117,13 @@ public InputStream download(Artifact artifact) throws IOException { archiveToOutputStream(artifact, localRepository, outputStream); return new ByteArrayInputStream(outputStream.toByteArray()); } catch (GitAPIException e) { - throw new IOException("Failed to clone or archive git/repo " + repoReference, e); + throw new IOException( + "Failed to clone or archive git/repo " + repoReference + ": " + e.getMessage()); } } private Git clone(Artifact artifact, Path stagingPath) throws GitAPIException { - String version = artifactVersion(artifact); + String version = "origin/" + artifactVersion(artifact); String subPath = artifactSubPath(artifact); // TODO(ethanfrogers): add support for clone history depth once jgit supports it @@ -83,7 +135,7 @@ private Git clone(Artifact artifact, Path stagingPath) throws GitAPIException { .call(); CheckoutCommand checkoutCommand = - localRepository.checkout().setName(version).setStartPoint("origin/" + version); + localRepository.checkout().setName(version).setStartPoint(version); if (!StringUtils.isEmpty(subPath)) { checkoutCommand = checkoutCommand.addPath(subPath); @@ -127,12 +179,66 @@ private String artifactVersion(Artifact artifact) { } private CloneCommand addAuthentication(CloneCommand cloneCommand) { - // TODO(ethanfrogers): support github oauth token and ssh authentication - if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) { - return cloneCommand.setCredentialsProvider( - new UsernamePasswordCredentialsProvider(username, password)); + switch (authType) { + case HTTP: + return cloneCommand.setCredentialsProvider( + new UsernamePasswordCredentialsProvider(username, password)); + case TOKEN: + return cloneCommand.setCredentialsProvider( + new UsernamePasswordCredentialsProvider(token, "")); + case SSH: + return configureSshAuth(cloneCommand); + default: + return cloneCommand; } + } - return cloneCommand; + private CloneCommand configureSshAuth(CloneCommand cloneCommand) { + SshSessionFactory sshSessionFactory = + new JschConfigSessionFactory() { + @Override + protected void configure(OpenSshConfig.Host hc, Session session) { + if (sshKnownHostsFilePath == null && sshTrustUnknownHosts) { + session.setConfig("StrictHostKeyChecking", "no"); + } + } + + @Override + protected JSch createDefaultJSch(FS fs) throws JSchException { + JSch defaultJSch = super.createDefaultJSch(fs); + if (!StringUtils.isEmpty(sshPrivateKeyPassphrase)) { + defaultJSch.addIdentity(sshPrivateKeyFilePath, sshPrivateKeyPassphrase); + } else { + defaultJSch.addIdentity(sshPrivateKeyFilePath); + } + + if (sshKnownHostsFilePath != null && sshTrustUnknownHosts) { + log.warn( + "SSH known_hosts file path supplied, ignoring 'sshTrustUnknownHosts' option"); + } + + if (sshKnownHostsFilePath != null) { + defaultJSch.setKnownHosts(sshKnownHostsFilePath); + } + + return defaultJSch; + } + }; + + return cloneCommand.setTransportConfigCallback( + (Transport transport) -> { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(sshSessionFactory); + }); + } + + private boolean isValidReference(String reference) { + if (authType == AuthType.HTTP || authType == AuthType.TOKEN) { + return reference.startsWith("http"); + } + if (authType == AuthType.SSH) { + return reference.startsWith("git@"); + } + return true; } }