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.
This commit is contained in:
Chris Fraser
2017-01-16 12:53:06 -05:00
committed by Spencer Gibb
parent 4e24a1f38e
commit 405ebad792
5 changed files with 274 additions and 12 deletions

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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<String> keyCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> 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;