diff --git a/build.gradle b/build.gradle index e4d6f93..83ac66a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ subprojects { group = 'org.testeditor.web' ext.versions = [ - dropwizard: '1.2.0', + dropwizard: '1.2.2', xtext: '2.13.0' ] diff --git a/org.testeditor.web.dropwizard.testing/src/main/java/org/testeditor/web/dropwizard/testing/files/FileTestUtils.xtend b/org.testeditor.web.dropwizard.testing/src/main/java/org/testeditor/web/dropwizard/testing/files/FileTestUtils.xtend index bd95eb4..966062d 100644 --- a/org.testeditor.web.dropwizard.testing/src/main/java/org/testeditor/web/dropwizard/testing/files/FileTestUtils.xtend +++ b/org.testeditor.web.dropwizard.testing/src/main/java/org/testeditor/web/dropwizard/testing/files/FileTestUtils.xtend @@ -25,12 +25,12 @@ class FileTestUtils { return Files.asCharSource(file, UTF_8).read } - def void write(File parent, String child, String contents) { + def File write(File parent, String child, String contents) { val fileToWrite = new File(parent, child) Files.createParentDirs(fileToWrite) fileToWrite.createNewFile // will not override existing file Files.asCharSink(fileToWrite, UTF_8).write(contents) + return fileToWrite } - } diff --git a/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextApplication.xtend b/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextApplication.xtend index aa45b1e..0c9b5f5 100644 --- a/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextApplication.xtend +++ b/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextApplication.xtend @@ -46,7 +46,7 @@ abstract class XtextApplication extends Dropwizard } protected def void initializeXtextIndex(T configuration, Environment environment) { - gitService.init(configuration.localRepoFileRoot, configuration.remoteRepoUrl) + gitService.init(configuration.localRepoFileRoot, configuration.remoteRepoUrl, configuration.privateKeyLocation, configuration.knownHostsLocation) indexUpdater.addToIndex(new File(configuration.localRepoFileRoot)) } diff --git a/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextConfiguration.xtend b/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextConfiguration.xtend index e6a78b7..58701fb 100644 --- a/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextConfiguration.xtend +++ b/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/dropwizard/xtext/XtextConfiguration.xtend @@ -9,9 +9,15 @@ import org.testeditor.web.dropwizard.DropwizardApplicationConfiguration public class XtextConfiguration extends DropwizardApplicationConfiguration { @NotEmpty @JsonProperty - String localRepoFileRoot + String localRepoFileRoot = 'repo' @NotEmpty @JsonProperty String remoteRepoUrl + @JsonProperty + String privateKeyLocation + + @JsonProperty + String knownHostsLocation + } diff --git a/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/xtext/index/persistence/GitService.xtend b/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/xtext/index/persistence/GitService.xtend index e526fa5..f6cc148 100644 --- a/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/xtext/index/persistence/GitService.xtend +++ b/org.testeditor.web.dropwizard.xtext/src/main/java/org/testeditor/web/xtext/index/persistence/GitService.xtend @@ -1,13 +1,22 @@ package org.testeditor.web.xtext.index.persistence import com.google.common.annotations.VisibleForTesting +import com.jcraft.jsch.JSch +import com.jcraft.jsch.JSchException +import com.jcraft.jsch.Session import java.io.File import java.util.List import javax.inject.Singleton import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.GitCommand +import org.eclipse.jgit.api.TransportCommand import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.transport.JschConfigSessionFactory +import org.eclipse.jgit.transport.OpenSshConfig.Host +import org.eclipse.jgit.transport.SshTransport import org.eclipse.jgit.treewalk.CanonicalTreeParser +import org.eclipse.jgit.util.FS import org.eclipse.xtend.lib.annotations.Accessors import org.slf4j.LoggerFactory @@ -29,12 +38,17 @@ class GitService { @Accessors(PUBLIC_GETTER) File projectFolder - + + String privateKeyLocation + String knownHostsLocation + /** * initialize this git service. either open the existing git and pull, or clone the remote repo */ - def void init(String localRepoFileRoot, String remoteRepoUrl) { + def void init(String localRepoFileRoot, String remoteRepoUrl, String privateKeyLocation, String knownHostsLocation) { logger.info("Initializing with localRepoFileRoot='{}', remoteRepoUrl='{}'.", localRepoFileRoot, remoteRepoUrl) + this.privateKeyLocation = privateKeyLocation + this.knownHostsLocation = knownHostsLocation projectFolder = verifyIsFolderOrNonExistent(localRepoFileRoot) if (isExistingGitRepository(projectFolder)) { openRepository(projectFolder, remoteRepoUrl) @@ -43,6 +57,10 @@ class GitService { cloneRepository(projectFolder, remoteRepoUrl) } } + + def void init(String localRepoFileRoot, String remoteRepoUrl) { + init(localRepoFileRoot, remoteRepoUrl, null, null) + } private def File verifyIsFolderOrNonExistent(String localRepoFileRoot) { val file = new File(localRepoFileRoot) @@ -59,8 +77,16 @@ class GitService { return folder.exists && new File(folder, DOT_GIT).exists } + /** + * configure transport commands with ssh credentials (if configured for this dropwizard app) + */ + def > GitCommand configureTransport(TransportCommand command) { + command.setSshSessionFactory + return command + } + def void pull() { - git.pull.call + git.pull.configureTransport.call } def ObjectId getHeadTree() { @@ -89,7 +115,12 @@ class GitService { } private def void cloneRepository(File projectFolder, String remoteRepoUrl) { - git = Git.cloneRepository.setDirectory(projectFolder).setURI(remoteRepoUrl).call + val cloneCommand = Git.cloneRepository => [ + setURI(remoteRepoUrl) + setSshSessionFactory + setDirectory(projectFolder) + ] + git = cloneCommand.call } @VisibleForTesting @@ -116,4 +147,40 @@ class GitService { return config.getString(CONFIG_REMOTE_SECTION, DEFAULT_REMOTE_NAME, CONFIG_KEY_URL) } + private def > void setSshSessionFactory(TransportCommand command) { + + val sshSessionFactory = new JschConfigSessionFactory { + + override protected void configure(Host host, Session session) { + logger.info('''HashKnownHosts = «session.getConfig('HashKnownHosts')»''') + logger.info('''StrictHostKeyChecking = «session.getConfig('StrictHostKeyChecking')»''') + } + + // provide custom private key location (if not located at ~/.ssh/id_rsa) + // provide custom known hosts file location (if not located at ~/.ssh/known_hosts) + // see also http://www.codeaffine.com/2014/12/09/jgit-authentication/ + override protected JSch createDefaultJSch(FS fs) throws JSchException { + val defaultJSch = super.createDefaultJSch(fs) + if (!privateKeyLocation.isNullOrEmpty) { + defaultJSch.addIdentity(privateKeyLocation) + } + if (!knownHostsLocation.isNullOrEmpty) { + defaultJSch.knownHosts = knownHostsLocation + defaultJSch.hostKeyRepository.hostKey.forEach [ + logger.info('''host = «host», type = «type», key = «key», fingerprint = «getFingerPrint(defaultJSch)»''') + ] + } + return defaultJSch + } + + } + + command.transportConfigCallback = [ transport | + if (transport instanceof SshTransport) { + transport.sshSessionFactory = sshSessionFactory + } + ] + + } + } diff --git a/org.testeditor.web.dropwizard.xtext/src/test/java/org/testeditor/web/xtext/index/persistence/GitServiceInitTest.xtend b/org.testeditor.web.dropwizard.xtext/src/test/java/org/testeditor/web/xtext/index/persistence/GitServiceInitTest.xtend index 0132376..afeca87 100644 --- a/org.testeditor.web.dropwizard.xtext/src/test/java/org/testeditor/web/xtext/index/persistence/GitServiceInitTest.xtend +++ b/org.testeditor.web.dropwizard.xtext/src/test/java/org/testeditor/web/xtext/index/persistence/GitServiceInitTest.xtend @@ -2,12 +2,81 @@ package org.testeditor.web.xtext.index.persistence import java.io.File import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.errors.TransportException import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.transport.URIish +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.rules.TemporaryFolder class GitServiceInitTest extends AbstractGitTest { + + @Rule public TemporaryFolder keyfiles = new TemporaryFolder + @Rule public ExpectedException expectedException = ExpectedException.none + + @Test + def void cloneTriesToUsePrivateKeyIfConfigured() { + // given + val invalidPrivateKey = write(keyfiles.root, 'invalid-private-key-file', 'invalid-private-key-content') + expectedException.expect(TransportException) + expectedException.expectMessage('invalid privatekey') + + // when + gitService.init(localRepoRoot.path, 'git@git.example.com:test-editor/test-editor-examples.git', invalidPrivateKey.absolutePath, null) + + // then (expected exception is thrown) + } + + @Test + def void cloneUsesPrivateKeyIfConfiguredButFailsOnHostThen() { + // given + val dummyButValidPrivateKey = write(keyfiles.root, 'dummy-private-key-file', + ''' + -----BEGIN RSA PRIVATE KEY BLOCK----- + + lQHYBFo7o+MBBADJg6nDraGCWwCCs4+J+VZP94htAXOgzY3LekOumSH55ywNPluM + gc5FPljiCS+UNEl1yYk+oFshClXhVSevtur/mXgUYck9V8n81fOlJBKPowJ/KiC5 + KdHRJX7SdUEvK0UymNsIEIhAyqGCT/9OcpIZSJy0mJoaY50da4rxaod9KQARAQAB + AAP9FJmqGX/phTWfsrzVIqoOTHR7SjFzdu/XMVhEDQLzOeSLrfrpnvkyHgVb+WzY + +VHzDzXVcEUyVl6uctpNXqWDa66dgbP/Cwwtjs8JT1ws919/9HuXtnC1mCvyTwHv + zi4AA9I9dARvWc/urMzW1ywW+Xhf96qnX0sPvivw1YxwmQUCANluybZP0Wu+srTc + 4GFJtyZZwfPZZUWmApeo9CQ+VqQcVxs4OmvlO3BKsh+hVjOJdpTdfV7tzbpcp5DD + TmeduqsCAO1CDFDi1LaW5lnKEHNlOMKv5d94BRJXW+nd9UjXVPm1U8tVZ1tfXZIo + x4VZWIPIPdmvs1X8Xa/FkLqbJ0LsZ3sCAM+4mfcCuuRwUiiqyGPTh1LQ2aWFtWSU + 0Svw9y7RqSJhgCsNcJxpgQDUl7cQcQ0LgaGTMlvyzZ825xDHUi07dhek0bQjdGVz + dGVkaXRvciA8dGVzdGVkaXRvckBleGFtcGxlLmNvbT6IuAQTAQIAIgUCWjuj4wIb + AwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQWwCvCdlYmPf8XAP7BXlqNYgn + D73QU6Rixk0txWF2gi4R+VuHPsxuM0LhzHNh1MKsrCAyIdAGUdQUzQNI1d3Z7UdG + 3uFUKRzdoHigc4yRjq2imN5DZm+xONtkt1y2tDeu9e0XkaOlsIazS4HzbKeJvd+n + AChJdHV8UAxjjm43lm2AOm6Wm5e98eFD5o2dAdgEWjuj4wEEALU7tZQj3+36+Z4I + jkoQjgTTz5Q/hnY7i8kpr1iCQkd4mR5mYOsTtDzaSa4R/pBMArrPB4p3x4JiDIL5 + QaA+LtaSsQTXDSd8NuESWSxgDGg2fr+J9U2HT9TmeZR2XMUoARr2QBr2uQuJJrwF + Qa9cDfl9F+yqvIlCdcpNVQoIryh7ABEBAAEAA/9PnsPPKVOfwbsYarnYYB2EkWmI + v7/bAZ4P6nhWciOcMqdSa7f4jteIRH5KMy2bR0mLuJifhK/p4BmPEOJ7+9WnQcGr + YqnuJ6lFn3fud/aANjGepsE3+Re4qJSfIWQtUuDdpQyjvyuIShkVck2G3YbMJX0O + lE2iNhVjkCtVcHgQQQIA0O5tPKdXxzku2wjOt/6zXJKaASyySzaEeK88J0+nq4ow + 4R69rFseNd5aCSapSQz27I63unt4UsXr1zkGbYKPPwIA3g/doOT8byVE4Z97UagP + ExhrBp2DLAapjZwu/9ppCRs418uS0XNp005cnq8tzyp25YbSxAtgJJymJb3JdVrT + xQIAnBOxCGac6AH/Ypt6vtJeHp81OxN6ADPq4hOLU1e0jRHz/wAXjXTj+mQ611NE + BqqozKixwRyM8VtLM6xcDoszNZyxiJ8EGAECAAkFAlo7o+MCGwwACgkQWwCvCdlY + mPf7wAP/WUXIjMWRUj+fc9BwXNwuMkNMvCrgv0vlknB8nRqAClE+kchTIALU3Ejb + oeH/IcZ9lEnLC80eTnh8AuY+iAnCAN54udblx4x1xz7NwXZq6e8KVoHC7KtoM2ho + EOEXryHZ6kNpO+cMSyey6xPA6zZR2yNY0fgDHdh8mVzgghR6c/o= + =KbQq + -----END RSA PRIVATE KEY BLOCK----- + ''') + expectedException.expect(TransportException) + expectedException.expectMessage('unknown host') + + // when + gitService.init(localRepoRoot.path, 'git@git.example.com:test-editor/test-editor-examples.git', dummyButValidPrivateKey.absolutePath, null) + + // then (expected exception is thrown) + } + + @Test def void clonesRemoteRepositoryWhenUninitialized() {