From 405ebad792942c91d1ed60502df5b8cdc5ae7a70 Mon Sep 17 00:00:00 2001 From: Chris Fraser Date: Mon, 16 Jan 2017 12:53:06 -0500 Subject: [PATCH] Implement configuration options for ssh passphrase and strict host key checking (#535) * add strictHostKeyChecking config option * Add passphrase configuration for SSH connections * test passphrase property is pulled from the environment * test that a if a passphrase is confgured a PassphraseCredentialsProvider is added to the git command. --- .../JGitEnvironmentRepository.java | 24 ++-- .../server/support/AbstractScmAccessor.java | 24 ++++ .../PassphraseCredentialsProvider.java | 87 ++++++++++++ ...EnvironmentRepositoryIntegrationTests.java | 21 +++ .../JGitEnvironmentRepositoryTests.java | 130 +++++++++++++++++- 5 files changed, 274 insertions(+), 12 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/PassphraseCredentialsProvider.java diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java index e624f1fa..e4e354c6 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java @@ -45,6 +45,7 @@ import org.eclipse.jgit.transport.*; import org.eclipse.jgit.transport.OpenSshConfig.Host; import org.eclipse.jgit.util.FileUtils; import org.springframework.beans.factory.InitializingBean; +import org.springframework.cloud.config.server.support.PassphraseCredentialsProvider; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.UrlResource; import org.springframework.util.Assert; @@ -150,6 +151,7 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository public void afterPropertiesSet() throws Exception { Assert.state(getUri() != null, "You need to configure a uri for the git repository"); + initialize(); if (this.cloneOnStart) { initClonedRepository(); } @@ -287,10 +289,7 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository setTimeout(fetch); try { - if (hasText(getUsername())) { - setCredentialsProvider(fetch); - } - + setCredentialsProvider(fetch); FetchResult result = fetch.call(); if(result.getTrackingRefUpdates() != null && result.getTrackingRefUpdates().size() > 0) { this.logger.info("Fetched for remote " + label + " and found " + result.getTrackingRefUpdates().size() @@ -385,9 +384,7 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository CloneCommand clone = this.gitFactory.getCloneCommandByCloneRepository() .setURI(getUri()).setDirectory(getBasedir()); setTimeout(clone); - if (hasText(getUsername())) { - setCredentialsProvider(clone); - } + setCredentialsProvider(clone); try { return clone.call(); } @@ -409,11 +406,11 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository } private void initialize() { - if (getUri().startsWith("file:") && !this.initialized) { + if (!this.initialized) { SshSessionFactory.setInstance(new JschConfigSessionFactory() { @Override protected void configure(Host hc, Session session) { - session.setConfig("StrictHostKeyChecking", "no"); + session.setConfig("StrictHostKeyChecking", isStrictHostKeyChecking() ? "yes" : "no"); } }); this.initialized = true; @@ -421,8 +418,13 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository } private void setCredentialsProvider(TransportCommand cmd) { - cmd.setCredentialsProvider( - new UsernamePasswordCredentialsProvider(getUsername(), getPassword())); + if (hasText(getUsername())) { + cmd.setCredentialsProvider( + new UsernamePasswordCredentialsProvider(getUsername(), getPassword())); + } else if (hasText(getPassphrase())) { + cmd.setCredentialsProvider( + new PassphraseCredentialsProvider(getPassphrase())); + } } private void setTimeout(TransportCommand pull) { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/AbstractScmAccessor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/AbstractScmAccessor.java index 751fd881..dd07b085 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/AbstractScmAccessor.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/AbstractScmAccessor.java @@ -64,6 +64,14 @@ public class AbstractScmAccessor implements ResourceLoaderAware { * Password for authentication with remote repository. */ private String password; + /** + * Passphrase for unlocking your ssh private key. + */ + private String passphrase; + /** + * Reject incoming SSH host keys from remote servers not in the known host list. + */ + private boolean strictHostKeyChecking; /** * Search paths to use within local working copy. By default searches only the root. */ @@ -159,6 +167,22 @@ public class AbstractScmAccessor implements ResourceLoaderAware { this.password = password; } + public String getPassphrase() { + return passphrase; + } + + public void setPassphrase(String passphrase) { + this.passphrase = passphrase; + } + + public boolean isStrictHostKeyChecking() { + return strictHostKeyChecking; + } + + public void setStrictHostKeyChecking(boolean strictHostKeyChecking) { + this.strictHostKeyChecking = strictHostKeyChecking; + } + protected File getWorkingDirectory() { if (this.uri.startsWith("file:")) { try { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/PassphraseCredentialsProvider.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/PassphraseCredentialsProvider.java new file mode 100644 index 00000000..9e27088f --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/PassphraseCredentialsProvider.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.support; + +import org.eclipse.jgit.errors.UnsupportedCredentialItem; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.URIish; + +public class PassphraseCredentialsProvider extends CredentialsProvider { + public static final String PROMPT = "Passphrase for"; + private final String passphrase; + + /** + * Initialize the provider with a the ssh passphrase. + * + * @param passphrase + */ + public PassphraseCredentialsProvider(String passphrase) { + super(); + this.passphrase = passphrase; + } + + /** + * {@inheritDoc} + * @return + */ + @Override + public boolean isInteractive() { + return false; + } + + /** + * {@inheritDoc} + * @return + */ + @Override + public boolean supports(CredentialItem... items) { + for (final CredentialItem item : items) { + if (item instanceof CredentialItem.StringType && item.getPromptText().startsWith(PROMPT)) { + continue; + } else { + return false; + } + } + return true; + } + + /** + * Ask for the credential items to be populated with the passphrase. + * + * @param uri + * the URI of the remote resource that needs authentication. + * @param items + * the items the application requires to complete authentication. + * @return {@code true} if the request was successful and values were + * supplied; {@code false} if the user canceled the request and did + * not supply all requested values. + * @throws UnsupportedCredentialItem + * if one of the items supplied is not supported. + */ + @Override + public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem { + for (final CredentialItem item : items) { + if (item instanceof CredentialItem.StringType && item.getPromptText().startsWith(PROMPT)) { + ((CredentialItem.StringType) item).setValue(passphrase); + continue; + } + throw new UnsupportedCredentialItem(uri, item.getClass().getName() + ":" + item.getPromptText()); + } + return true; + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java index 27834415..9ce581df 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryIntegrationTests.java @@ -483,6 +483,27 @@ public class JGitEnvironmentRepositoryIntegrationTests { return localRef.getObjectId().getName(); } + public void passphrase() throws IOException { + String uri = ConfigServerTestUtils.prepareLocalRepo("config-repo"); + final String passphrase = "thisismypassphrase"; + this.context = new SpringApplicationBuilder(TestConfiguration.class).web(false) + .run("--spring.cloud.config.server.git.uri=" + uri, + "--spring.cloud.config.server.git.passphrase=" + passphrase); + JGitEnvironmentRepository repository = this.context.getBean(JGitEnvironmentRepository.class); + assertThat(repository.getPassphrase(), Matchers.containsString(passphrase)); + } + + @Test + public void strictHostKeyChecking() throws IOException { + String uri = ConfigServerTestUtils.prepareLocalRepo("config-repo"); + final boolean strictHostKeyChecking = true; + this.context = new SpringApplicationBuilder(TestConfiguration.class).web(false) + .run("--spring.cloud.config.server.git.uri=" + uri, + "--spring.cloud.config.server.git.strict-host-key-checking=" + strictHostKeyChecking); + JGitEnvironmentRepository repository = this.context.getBean(JGitEnvironmentRepository.class); + assertEquals(repository.isStrictHostKeyChecking(), strictHostKeyChecking); + } + @Configuration @EnableConfigurationProperties(ConfigServerProperties.class) @Import({ PropertyPlaceholderAutoConfiguration.class, diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java index 9607be17..0714d649 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java @@ -24,6 +24,7 @@ import java.util.List; import org.eclipse.jgit.api.CheckoutCommand; +import com.jcraft.jsch.Session; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; @@ -40,19 +41,34 @@ import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.transport.FetchResult; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.server.support.PassphraseCredentialsProvider; import org.springframework.cloud.config.server.test.ConfigServerTestUtils; import org.springframework.core.env.StandardEnvironment; +import java.lang.reflect.Method; + +import static junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.*; @@ -69,6 +85,9 @@ public class JGitEnvironmentRepositoryTests { private File basedir = new File("target/config"); + @Rule + public final ExpectedException exception = ExpectedException.none(); + @Before public void init() throws Exception { String uri = ConfigServerTestUtils.prepareLocalRepo(); @@ -177,6 +196,8 @@ public class JGitEnvironmentRepositoryTests { assertVersion(environment); } + + @Test public void uriWithHostOnly() throws Exception { this.repository.setUri("git://localhost"); @@ -517,6 +538,113 @@ public class JGitEnvironmentRepositoryTests { assertFalse("baseDir should be deleted when clone fails", this.basedir.exists()); } + @Test + public void usernamePasswordShouldSetCredentials() throws Exception { + Git mockGit = mock(Git.class); + MockCloneCommand mockCloneCommand = new MockCloneCommand(mockGit); + + JGitEnvironmentRepository envRepository = new JGitEnvironmentRepository(this.environment); + envRepository.setGitFactory(new MockGitFactory(mockGit, mockCloneCommand)); + envRepository.setUri("git+ssh://git@somegitserver/somegitrepo"); + envRepository.setBasedir(new File("./mybasedir")); + final String username = "someuser"; + final String password = "mypassword"; + envRepository.setUsername(username); + envRepository.setPassword(password); + envRepository.setCloneOnStart(true); + envRepository.afterPropertiesSet(); + + assertTrue(mockCloneCommand.getCredentialsProvider() instanceof UsernamePasswordCredentialsProvider); + + CredentialsProvider provider = mockCloneCommand.getCredentialsProvider(); + CredentialItem.Username usernameCredential = new CredentialItem.Username(); + CredentialItem.Password passwordCredential = new CredentialItem.Password(); + assertTrue(provider.supports(usernameCredential)); + assertTrue(provider.supports(passwordCredential)); + + provider.get(new URIish(), usernameCredential); + assertEquals(usernameCredential.getValue(), username); + provider.get(new URIish(), passwordCredential); + assertEquals(String.valueOf(passwordCredential.getValue()), password); + } + + @Test + public void passphraseShouldSetCredentials() throws Exception { + final String passphrase = "mypassphrase"; + Git mockGit = mock(Git.class); + MockCloneCommand mockCloneCommand = new MockCloneCommand(mockGit); + + JGitEnvironmentRepository envRepository = new JGitEnvironmentRepository(this.environment); + envRepository.setGitFactory(new MockGitFactory(mockGit, mockCloneCommand)); + envRepository.setUri("git+ssh://git@somegitserver/somegitrepo"); + envRepository.setBasedir(new File("./mybasedir")); + envRepository.setPassphrase(passphrase); + envRepository.setCloneOnStart(true); + envRepository.afterPropertiesSet(); + + assertTrue(mockCloneCommand.hasPassphraseCredentialsProvider()); + + CredentialsProvider provider = mockCloneCommand.getCredentialsProvider(); + assertFalse(provider.isInteractive()); + + CredentialItem.StringType stringCredential = new CredentialItem.StringType(PassphraseCredentialsProvider.PROMPT, true); + + assertTrue(provider.supports(stringCredential)); + provider.get(new URIish(), stringCredential); + assertEquals(stringCredential.getValue(), passphrase); + } + + @Test + public void strictHostKeyCheckShouldCheck() throws Exception { + String uri = "git+ssh://git@somegitserver/somegitrepo"; + SshSessionFactory.setInstance(null); + JGitEnvironmentRepository envRepository = new JGitEnvironmentRepository(this.environment); + envRepository.setUri(uri); + envRepository.setBasedir(new File("./mybasedir")); + envRepository.setStrictHostKeyChecking(true); + envRepository.setCloneOnStart(true); + try { + // this will throw but we don't care about connecting. + envRepository.afterPropertiesSet(); + } catch (Exception e) { + final OpenSshConfig.Host hc = OpenSshConfig.get(FS.detect()).lookup("github.com"); + JschConfigSessionFactory factory = (JschConfigSessionFactory) SshSessionFactory.getInstance(); + // There's no public method that can be used to inspect the ssh configuration, so we'll reflect + // the configure method to allow us to check that the config property is set as expected. + Method configure = factory.getClass().getDeclaredMethod("configure", OpenSshConfig.Host.class, Session.class ); + configure.setAccessible(true); + Session session = mock(Session.class); + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); + configure.invoke(factory, hc, session ); + verify(session).setConfig(keyCaptor.capture(), valueCaptor.capture()); + configure.setAccessible(false); + assertTrue("yes".equals(valueCaptor.getValue())); + } + } + + class MockCloneCommand extends CloneCommand { + private Git mockGit; + + public MockCloneCommand(Git mockGit) { + this.mockGit = mockGit; + } + + @Override + public Git call() throws GitAPIException, InvalidRemoteException { + return mockGit; + } + + public boolean hasPassphraseCredentialsProvider() { + return credentialsProvider instanceof PassphraseCredentialsProvider; + } + + public CredentialsProvider getCredentialsProvider() { + return credentialsProvider; + } + } + + class MockGitFactory extends JGitEnvironmentRepository.JGitFactory { private Git mockGit;