Skip to content

Commit

Permalink
feat(artifacts/gitRepo): support SSH auth (#4052)
Browse files Browse the repository at this point in the history
* feat(artifacts/gitRepo): support ssh auth

add support for ssh as an alternative authentication mechanisim

* feat(artifacts/gitRepo): improve errors for auth

improve UX around errors for authentication. if the accounts
authentication type doesn't support references of a type throw an error
before trying to download it.

* chore(artifacts/gitRepo): remove todo

* feat(artifacts/gitRepo): ssh without passphrase

add support for ssh without a passphrase
  • Loading branch information
ethanfrogers committed Sep 27, 2019
1 parent 8cd088f commit b234aca
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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());
}

Expand All @@ -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);
Expand All @@ -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

Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
}

0 comments on commit b234aca

Please sign in to comment.