From acee8a1b103f3469a575df3ca42eeb39818dbb65 Mon Sep 17 00:00:00 2001 From: Ollie Hughes Date: Thu, 6 Jul 2017 16:21:09 +0100 Subject: [PATCH 1/3] Git SSH configuration using properties By default, Spring Cloud Config Server uses SSH configuration files such as ~/.ssh/known_hosts and /etc/ssh/ssh_config when connecting to Git repositories using an SSH URI. In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. For cases such as these, SSH configuration can be set using Java properties. In order to activate property based SSH configuration, the property spring.cloud.config.server.git.ignoreLocalSshSettings must be set to true --- docs/pom.xml | 4 +- .../main/asciidoc/spring-cloud-config.adoc | 72 ++++- .../config/ConfigServerAutoConfiguration.java | 2 +- .../server/config/TransportConfiguration.java | 101 +++++++ .../JGitEnvironmentRepository.java | 22 -- .../ssh/PropertyBasedSshSessionFactory.java | 75 ++++++ .../server/ssh/SshPropertyValidator.java | 110 ++++++++ .../config/server/ssh/SshUriProperties.java | 251 ++++++++++++++++++ .../server/ssh/SshUriPropertyProcessor.java | 76 ++++++ ...ransportConfigurationIntegrationTests.java | 229 ++++++++++++++++ .../JGitEnvironmentRepositoryTests.java | 38 --- .../PropertyBasedSshSessionFactoryTest.java | 160 +++++++++++ .../server/ssh/SshPropertyValidatorTest.java | 185 +++++++++++++ .../ssh/SshUriPropertyProcessorTest.java | 138 ++++++++++ .../src/test/resources/ssh/key | 27 ++ .../resources/ssh/ssh-nested-settings.yml | 41 +++ .../resources/ssh/ssh-private-key-block.yml | 35 +++ .../resources/ssh/ssh-private-key-newline.yml | 10 + 18 files changed, 1513 insertions(+), 63 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java create mode 100644 spring-cloud-config-server/src/test/resources/ssh/key create mode 100644 spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml create mode 100644 spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml create mode 100644 spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml diff --git a/docs/pom.xml b/docs/pom.xml index 38eabee8..b41839ae 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,8 +6,10 @@ org.springframework.cloud spring-cloud-config - 1.3.2.BUILD-SNAPSHOT + 1.4.0.BUILD-SNAPSHOT + .. + pom Spring Cloud Config Docs Spring Cloud Docs diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 503bb6f0..55146702 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -360,7 +360,77 @@ by the default credential provider chain. AWS EC2 instances may use http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html[IAM Roles for EC2 Instances]. Note: The aws-java-sdk-core jar is an optional dependency. If the aws-java-sdk-core jar is not on your -classpath, then the AWS Code Commit credential provider will not be created regardless of the git server URI. +classpath, then the AWS Code Commit credential provider will not be created regardless of the git server URI. + +===== Git SSH configuration using properties + +By default, Spring Cloud Config Server uses SSH configuration files such as `~/.ssh/known_hosts` and `/etc/ssh/ssh_config` when connecting to Git repositories using an SSH URI. +In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. For cases such as these, SSH configuration can be set using +Java properties. In order to activate property based SSH configuration, the property `spring.cloud.config.server.git.ignoreLocalSshSettings` must be set to `true`. +Example: + +[source,yaml] +---- + spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo1.git + ignoreLocalSshSettings: true + hostKey: someHostKey + hostKeyAlgorithm: ssh-rsa + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX + IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF + ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud + 1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i + oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W + DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd + fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b + BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG + EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj + 5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8 + +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe + pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG + ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ + xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW + dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi + PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX + VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z + FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R + gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4 + VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV + cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee + KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N + CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs + q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J + 69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT + -----END RSA PRIVATE KEY----- + +---- + +.SSH Configuration properties +|=== +|Property Name |Remarks + +|*ignoreLocalSshSettings* +|If true, use property based SSH config instead of file based + +|*privateKey* +|Valid SSH private key. Must be set if `ignoreLocalSshSettings` is true and Git URI is SSH format + +|*hostKey* +|Valid SSH host key. Must be set if `hostKeyAlgorithm` is also set + +|*hostKeyAlgorithm* +|One of `ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384 ,ecdsa-sha2-nistp521`. Must be set if `hostKey` is also set + +|*strictHostKeyChecking* +|`true` or `false`. If false, ignore errors with host key +|=== + ===== Placeholders in Git Search Paths diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java index b1dff1ca..8a7a4140 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java @@ -29,7 +29,7 @@ import org.springframework.context.annotation.Import; @ConditionalOnBean(ConfigServerConfiguration.Marker.class) @EnableConfigurationProperties(ConfigServerProperties.class) @Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class, - ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class }) + ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class, TransportConfiguration.class }) public class ConfigServerAutoConfiguration { } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java new file mode 100644 index 00000000..f5199cd1 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015 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.config; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.transport.*; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.server.ssh.PropertyBasedSshSessionFactory; +import org.springframework.cloud.config.server.ssh.SshUriProperties; +import org.springframework.cloud.config.server.ssh.SshUriPropertyProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configure a callback to set up a property based SSH settings before running a transport command (such as clone or fetch) + * + * @author Ollie Hughes + */ +@Configuration +@EnableConfigurationProperties(SshUriProperties.class) +public class TransportConfiguration { + + @ConditionalOnMissingBean(TransportConfigCallback.class) + @Bean + public TransportConfigCallback propertiesBasedSshTransportCallback(final SshUriProperties sshUriProperties) { + if(sshUriProperties.isIgnoreLocalSshSettings()) { + return new PropertiesBasedSshTransportConfigCallback(sshUriProperties); + } + else return new FileBasedSshTransportConfigCallback(sshUriProperties); + } + + /** + * Configure JGit transport command to use a SSH session factory that is configured using properties defined + * in {@link SshUriProperties} + */ + public static class PropertiesBasedSshTransportConfigCallback implements TransportConfigCallback { + + private SshUriProperties sshUriProperties; + + public PropertiesBasedSshTransportConfigCallback(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + public SshUriProperties getSshUriProperties() { + return sshUriProperties; + } + + @Override + public void configure(Transport transport) { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory( + new PropertyBasedSshSessionFactory( + new SshUriPropertyProcessor(sshUriProperties).getSshKeysByHostname(), new JSch())); + } + } + + /** + * Configure JGit transport command to use a default SSH session factory based on local machines SSH config. + * Allow strict host key checking to be set. + */ + public static class FileBasedSshTransportConfigCallback implements TransportConfigCallback { + + private SshUriProperties sshUriProperties; + + public FileBasedSshTransportConfigCallback(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + public SshUriProperties getSshUriProperties() { + return sshUriProperties; + } + + @Override + public void configure(Transport transport) { + SshSessionFactory.setInstance(new JschConfigSessionFactory() { + @Override + protected void configure(OpenSshConfig.Host hc, Session session) { + session.setConfig("StrictHostKeyChecking", + sshUriProperties.isStrictHostKeyChecking() ? "yes" : "no"); + } + }); + } + } +} 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 c28370de..8df63428 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 @@ -21,7 +21,6 @@ import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; - import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; @@ -43,9 +42,6 @@ import org.eclipse.jgit.errors.NoRemoteRepositoryException; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; -import org.eclipse.jgit.transport.JschConfigSessionFactory; -import org.eclipse.jgit.transport.OpenSshConfig.Host; -import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.TagOpt; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.util.FileUtils; @@ -56,8 +52,6 @@ import org.springframework.core.io.UrlResource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.jcraft.jsch.Session; - import static org.springframework.util.StringUtils.hasText; /** @@ -81,8 +75,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository */ private int timeout = 5; - private boolean initialized; - /** * Flag to indicate that the repository should be cloned on startup (not on * demand). Generally leads to slower startup but faster first query. @@ -174,7 +166,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository @Override public void afterPropertiesSet() throws Exception { Assert.state(getUri() != null, "You need to configure a uri for the git repository"); - initialize(); if (this.cloneOnStart) { initClonedRepository(); } @@ -184,7 +175,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository * Get the working directory ready. */ public String refresh(String label) { - initialize(); Git git = null; try { git = createGitClient(); @@ -422,18 +412,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository } } - private void initialize() { - if (!this.initialized) { - SshSessionFactory.setInstance(new JschConfigSessionFactory() { - @Override - protected void configure(Host hc, Session session) { - session.setConfig("StrictHostKeyChecking", isStrictHostKeyChecking() ? "yes" : "no"); - } - }); - this.initialized = true; - } - } - private void configureCommand(TransportCommand command) { command.setTimeout(this.timeout); if (this.transportConfigCallback != null) { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java new file mode 100644 index 00000000..93493a43 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 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.ssh; + +import com.jcraft.jsch.HostKey; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig.Host; +import org.eclipse.jgit.util.Base64; +import org.eclipse.jgit.util.FS; + +import java.util.Map; + + +/** + * In a cloud environment local SSH config files such as `.known_hosts` may not be suitable for providing + * configuration settings due to ephemeral filesystems. This flag enables SSH config to be provided as application + * properties + * @author William Tran + * @author Ollie Hughes + */ +public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { + + private final Map sshKeysByHostname; + private final JSch jSch; + + public PropertyBasedSshSessionFactory(Map sshKeysByHostname, JSch jSch) { + this.sshKeysByHostname = sshKeysByHostname; + this.jSch = jSch; + } + + @Override + protected void configure(Host hc, Session session) { + SshUriProperties sshProperties = sshKeysByHostname.get(hc.getHostName()); + String hostKeyAlgorithm = sshProperties.getHostKeyAlgorithm(); + if (hostKeyAlgorithm != null) { + session.setConfig("server_host_key", hostKeyAlgorithm); + } + if (sshProperties.getHostKey() == null || !sshProperties.isStrictHostKeyChecking()) { + session.setConfig("StrictHostKeyChecking", "no"); + } else { + session.setConfig("StrictHostKeyChecking", "yes"); + } + } + + @Override + protected Session createSession(Host hc, String user, String host, int port, FS fs) throws JSchException { + if (sshKeysByHostname.containsKey(host)) { + SshUriProperties sshUriProperties = sshKeysByHostname.get(host); + jSch.addIdentity(host, sshUriProperties.getPrivateKey().getBytes(), null, null); + if (sshUriProperties.getHostKey() != null) { + HostKey hostkey = new HostKey(host, Base64.decode(sshUriProperties.getHostKey())); + jSch.getHostKeyRepository().add(hostkey, null); + } + return jSch.getSession(user, host, port); + } + throw new JSchException("no keys configured for hostname " + host); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java new file mode 100644 index 00000000..011effe7 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java @@ -0,0 +1,110 @@ +/* + * Copyright 2015 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.ssh; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.KeyPair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import java.util.*; + +import static java.lang.String.format; +import static org.springframework.util.StringUtils.hasText; + +/** + * Validate SSH related properties + * + * @author Ollie Hughes + */ +@Component +@EnableConfigurationProperties(SshUriProperties.class) +public class SshPropertyValidator { + + private final SshUriProperties sshUriProperties; + private final JSch jsch = new JSch(); + private static final Set VALID_HOST_KEY_ALGORITHMS = new LinkedHashSet<>(Arrays.asList( + "ssh-dss","ssh-rsa","ecdsa-sha2-nistp256","ecdsa-sha2-nistp384","ecdsa-sha2-nistp521")); + private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git."; + + @Autowired + public SshPropertyValidator(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + static boolean isSshUri(Object uri) { + return uri != null && (uri.toString().startsWith("ssh") || uri.toString().startsWith("git")); + } + + @PostConstruct + public void validateSshConfigurationProperties() { + List allRepoProperties = new ArrayList<>(); + allRepoProperties.add(sshUriProperties); + Map repos = sshUriProperties.getRepos(); + if (repos != null) { + allRepoProperties.addAll(repos.values()); + } + for (SshUriProperties repoProperties : allRepoProperties) { + if(isSshUri(repoProperties.getUri()) && sshUriProperties.isIgnoreLocalSshSettings()){ + validatePrivateKeyPresent(); + validatePrivateKeyFormat(); + validateAlgorithmSpecifiedWhenHostKeySet(); + validateHostKeySpecifiedWhenAlgorithmSet(); + validateHostKeyAlgorithmSupported(); + } + } + } + + protected void validatePrivateKeyFormat() { + try { + KeyPair.load(jsch, sshUriProperties.getPrivateKey().getBytes(), null); + } catch (JSchException e) { + throw new IllegalStateException(format("Property '%sprivateKey' contains an invalid value", GIT_PROPERTY_PREFIX)); + } + } + + protected void validateHostKeyAlgorithmSupported() { + if (hasText(sshUriProperties.getHostKeyAlgorithm())) { + Assert.state(VALID_HOST_KEY_ALGORITHMS.contains(sshUriProperties.getHostKeyAlgorithm()), + format("Property '%shostKeyAlgorithm' must be one of %s", GIT_PROPERTY_PREFIX, VALID_HOST_KEY_ALGORITHMS)); + } + } + + protected void validatePrivateKeyPresent() { + Assert.state(sshUriProperties.getPrivateKey() != null, + format("Property '%sprivateKey' must be set when '%signoreLocalSshSettings' is set to 'true'", GIT_PROPERTY_PREFIX, GIT_PROPERTY_PREFIX)); + } + + protected void validateHostKeySpecifiedWhenAlgorithmSet() { + if (hasText(sshUriProperties.getHostKeyAlgorithm())) { + Assert.state(hasText(sshUriProperties.getHostKey()), + format("Property '%shostKey' must be set when 'hostKeyAlgorithm' is specified", GIT_PROPERTY_PREFIX)); + } + } + + protected void validateAlgorithmSpecifiedWhenHostKeySet() { + if (hasText(sshUriProperties.getHostKey())) { + Assert.state(hasText(sshUriProperties.getHostKeyAlgorithm()), + format("Property '%shostKeyAlgorithm' must be set when 'hostKey' is specified", GIT_PROPERTY_PREFIX)); + } + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java new file mode 100644 index 00000000..8d036d5a --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java @@ -0,0 +1,251 @@ +/* + * Copyright 2015 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.ssh; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Data container for property based SSH config + * + * @author Ollie Hughes + */ +@ConfigurationProperties("spring.cloud.config.server.git") +public class SshUriProperties { + private String uri; + private String hostKeyAlgorithm; + private String hostKey; + private String privateKey; + private String username; + private String password; + private boolean ignoreLocalSshSettings; + private boolean strictHostKeyChecking = true; + + private Map repos = new HashMap<>(); + + public SshUriProperties(String uri, String hostKeyAlgorithm, String hostKey, String privateKey, String username, String password, boolean ignoreLocalSshSettings, boolean strictHostKeyChecking, Map repos) { + this.uri = uri; + this.hostKeyAlgorithm = hostKeyAlgorithm; + this.hostKey = hostKey; + this.privateKey = privateKey; + this.username = username; + this.password = password; + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + this.strictHostKeyChecking = strictHostKeyChecking; + this.repos = repos; + } + + public SshUriProperties() { + } + + public static SshUriPropertiesBuilder builder() { + return new SshUriPropertiesBuilder(); + } + + public boolean isSshUri() { + return uri != null && !uri.startsWith("http"); + } + + public String getHostname() { + if (getUri() == null) { + return null; + } + + if (getUri().matches("^[a-z]+://.*")) { + return UriComponentsBuilder.fromUriString(uri).build().getHost(); + } + else if (getUri().indexOf('@') < getUri().indexOf(':')) { + return getUri().substring(getUri().indexOf('@') + 1, uri.indexOf(':')); + } + else if (getUri().startsWith("ssh:") && getUri().indexOf('@') > 0) { + String postAt = getUri().substring(getUri().indexOf('@') + 1); + return postAt.substring(0, postAt.indexOf(":")); + } + else return null; + } + + public String getUri() { + return this.uri; + } + + public String getHostKeyAlgorithm() { + return this.hostKeyAlgorithm; + } + + public String getHostKey() { + return this.hostKey; + } + + public String getPrivateKey() { + return this.privateKey; + } + + public String getUsername() { + return this.username; + } + + public String getPassword() { + return this.password; + } + + public boolean isIgnoreLocalSshSettings() { + return this.ignoreLocalSshSettings; + } + + public boolean isStrictHostKeyChecking() { + return this.strictHostKeyChecking; + } + + public Map getRepos() { + return this.repos; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setHostKeyAlgorithm(String hostKeyAlgorithm) { + this.hostKeyAlgorithm = hostKeyAlgorithm; + } + + public void setHostKey(String hostKey) { + this.hostKey = hostKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setIgnoreLocalSshSettings(boolean ignoreLocalSshSettings) { + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + } + + public void setStrictHostKeyChecking(boolean strictHostKeyChecking) { + this.strictHostKeyChecking = strictHostKeyChecking; + } + + public void setRepos(Map repos) { + this.repos = repos; + } + + @Override + public int hashCode() { + return Objects.hash(uri, hostKeyAlgorithm, hostKey, privateKey, username, password, ignoreLocalSshSettings, strictHostKeyChecking); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SshUriProperties other = (SshUriProperties) obj; + return Objects.equals(this.uri, other.uri) + && Objects.equals(this.hostKeyAlgorithm, other.hostKeyAlgorithm) + && Objects.equals(this.hostKey, other.hostKey) + && Objects.equals(this.privateKey, other.privateKey) + && Objects.equals(this.username, other.username) + && Objects.equals(this.password, other.password) + && Objects.equals(this.ignoreLocalSshSettings, other.ignoreLocalSshSettings) + && Objects.equals(this.strictHostKeyChecking, other.strictHostKeyChecking); + } + + public String toString() { + return "org.springframework.cloud.config.server.ssh.SshUriProperties(uri=" + this.getUri() + " hostKeyAlgorithm=" + this.getHostKeyAlgorithm() + ", hostKey=" + this.getHostKey() + ", privateKey=" + this.getPrivateKey() + ", username=" + this.getUsername() + ", password=" + this.getPassword() + ", ignoreLocalSshSettings=" + this.isIgnoreLocalSshSettings() + ", strictHostKeyChecking=" + this.isStrictHostKeyChecking() + ", repos=" + this.getRepos() + ")"; + } + + public static class SshUriPropertiesBuilder { + private String uri; + private String hostKeyAlgorithm; + private String hostKey; + private String privateKey; + private String username; + private String password; + private boolean ignoreLocalSshSettings; + private boolean strictHostKeyChecking = true; + private Map repos; + + SshUriPropertiesBuilder() { + } + + public SshUriProperties.SshUriPropertiesBuilder uri(String uri) { + this.uri = uri; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder hostKeyAlgorithm(String hostKeyAlgorithm) { + this.hostKeyAlgorithm = hostKeyAlgorithm; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder hostKey(String hostKey) { + this.hostKey = hostKey; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder privateKey(String privateKey) { + this.privateKey = privateKey; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder username(String username) { + this.username = username; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder password(String password) { + this.password = password; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder ignoreLocalSshSettings(boolean ignoreLocalSshSettings) { + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder strictHostKeyChecking(boolean strictHostKeyChecking) { + this.strictHostKeyChecking = strictHostKeyChecking; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder repos(Map repos) { + this.repos = repos; + return this; + } + + public SshUriProperties build() { + return new SshUriProperties(uri, hostKeyAlgorithm, hostKey, privateKey, username, password, ignoreLocalSshSettings, strictHostKeyChecking, repos); + } + + public String toString() { + return "org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriPropertiesBuilder(uri=" + this.uri + "hostKeyAlgorithm=" + this.hostKeyAlgorithm + ", hostKey=" + this.hostKey + ", privateKey=" + this.privateKey + ", username=" + this.username + ", password=" + this.password + ", ignoreLocalSshSettings=" + this.ignoreLocalSshSettings + ", strictHostKeyChecking=" + this.strictHostKeyChecking + ", repos=" + this.repos + ")"; + } + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java new file mode 100644 index 00000000..791dcda9 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 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.ssh; + +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.cloud.config.server.ssh.SshPropertyValidator.isSshUri; + +/** + * Check if Git repo properties refer to an SSH based transport then filter and extract the properties + * @author William Tran + * @author Ollie Hughes + */ +public class SshUriPropertyProcessor { + + private final SshUriProperties sshUriProperties; + + public SshUriPropertyProcessor(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + public Map getSshKeysByHostname() { + return extractNestedProperties(sshUriProperties); + } + + private Map extractNestedProperties(SshUriProperties uriProperties) { + Map sshUriPropertyMap = new HashMap<>(); + String parentUri = uriProperties.getUri(); + if (isSshUri(parentUri) && getHostname(parentUri) != null) { + sshUriPropertyMap.put(getHostname(parentUri), uriProperties); + } + Map repos = uriProperties.getRepos(); + if(repos != null) { + for (SshUriProperties repoProperties : repos.values()) { + String repoUri = repoProperties.getUri(); + if (isSshUri(repoUri) && getHostname(repoUri) != null) { + sshUriPropertyMap.put(getHostname(repoUri), repoProperties); + } + } + } + return sshUriPropertyMap; + } + + private String getHostname(String uri) { + if (uri == null) { + return null; + } + else if (uri.matches("^[a-z]+://.*")) { + return UriComponentsBuilder.fromUriString(uri).build().getHost(); + } + else if (uri.indexOf('@') < uri.indexOf(':')) { + return uri.substring(uri.indexOf('@') + 1, uri.indexOf(':')); + } + else if (uri.startsWith("ssh:") && uri.indexOf('@') > 0) { + String postAt = uri.substring(uri.indexOf('@') + 1); + return postAt.substring(0, postAt.indexOf(":")); + } + else return null; + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java new file mode 100644 index 00000000..b22b3166 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2015 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; + +import com.jcraft.jsch.Session; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.util.FS; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.config.server.config.TransportConfiguration; +import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository; +import org.springframework.cloud.config.server.ssh.SshPropertyValidator; +import org.springframework.cloud.config.server.ssh.SshUriProperties; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.lang.reflect.Method; + +import static junit.framework.TestCase.assertTrue; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Integration tests for property based SSH config support + * @author Ollie Hughes + */ +public class TransportConfigurationIntegrationTests { + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.config.name:ssh/ssh-private-key-block",}) + @ActiveProfiles({"test", "git"}) + public static class PropertyBasedCallbackTest { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void propertyBasedTransportCallbackIsConfigured() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.PropertiesBasedSshTransportConfigCallback.class))); + } + } + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.config.name:ssh/ssh-private-key-newline" + }) + @ActiveProfiles({"test", "git"}) + public static class PrivateKeyPropertyWithLineBreaks { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void privateKeyPropertyWithLineBreaks() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.PropertiesBasedSshTransportConfigCallback.class))); + + TransportConfiguration.PropertiesBasedSshTransportConfigCallback configCallback = + (TransportConfiguration.PropertiesBasedSshTransportConfigCallback) transportConfigCallback; + assertThat(configCallback.getSshUriProperties().getPrivateKey(), is(equalTo(TestProperties.TEST_PRIVATE_KEY_1))); + } + } + + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.config.name:ssh/ssh-nested-settings" + }) + @ActiveProfiles({"test", "git"}) + public static class SshPropertiesWithinNestedRepo { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void sshPropertiesWithinNestedRepo() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.PropertiesBasedSshTransportConfigCallback.class))); + + TransportConfiguration.PropertiesBasedSshTransportConfigCallback configCallback = + (TransportConfiguration.PropertiesBasedSshTransportConfigCallback) transportConfigCallback; + SshUriProperties sshUriProperties = configCallback.getSshUriProperties(); + assertThat(sshUriProperties.getPrivateKey(), is(equalTo(TestProperties.TEST_PRIVATE_KEY_1))); + + assertThat(sshUriProperties.getRepos().get("repo1"), is(notNullValue())); + assertThat(sshUriProperties.getRepos().get("repo1").getPrivateKey(), is(equalTo(TestProperties.TEST_PRIVATE_KEY_2))); + } + } + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.cloud.config.server.git.uri=git@gitserver.com:team/repo.git", + "spring.cloud.config.server.git.ignoreLocalSshSettings=false",}) + @ActiveProfiles({"test", "git"}) + public static class FileBasedCallbackTest { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void fileBasedTransportCallbackIsConfigured() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.FileBasedSshTransportConfigCallback.class))); + } + + + @Test + public void strictHostKeyCheckShouldCheck() throws Exception { + String uri = "git+ssh://git@somegitserver/somegitrepo"; + SshSessionFactory.setInstance(null); + jGitEnvironmentRepository.setUri(uri); + jGitEnvironmentRepository.setBasedir(new File("./mybasedir")); + assertTrue(jGitEnvironmentRepository.isStrictHostKeyChecking()); + jGitEnvironmentRepository.setCloneOnStart(true); + try { + // this will throw but we don't care about connecting. + jGitEnvironmentRepository.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())); + } + } + } + + private static class TestProperties { + private static final String TEST_PRIVATE_KEY_1 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\n" + + "yyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\n" + + "uP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\n" + + "ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\n" + + "TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n" + + "+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\n" + + "jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\n" + + "lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\n" + + "xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n" + + "4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n" + + "7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n" + + "3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\n" + + "q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\n" + + "mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\n" + + "lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\n" + + "ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\n" + + "ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\n" + + "BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\n" + + "zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\n" + + "uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n" + + "54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\n" + + "E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n" + + "4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n" + + "/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n" + + "2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n" + + "-----END RSA PRIVATE KEY-----"; + + private static final String TEST_PRIVATE_KEY_2 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX\n" + + "IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF\n" + + "ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud\n" + + "1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i\n" + + "oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W\n" + + "DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd\n" + + "fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b\n" + + "BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG\n" + + "EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj\n" + + "5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8\n" + + "+AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe\n" + + "pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG\n" + + "ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ\n" + + "xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW\n" + + "dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi\n" + + "PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX\n" + + "VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z\n" + + "FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R\n" + + "gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4\n" + + "VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV\n" + + "cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee\n" + + "KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N\n" + + "CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs\n" + + "q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J\n" + + "69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT\n" + + "-----END RSA PRIVATE KEY-----\n"; + } +} 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 ef105132..0d67d4ac 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 @@ -33,7 +33,6 @@ import static org.mockito.Mockito.when; import java.io.File; import java.io.IOException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -60,19 +59,14 @@ import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; -import org.eclipse.jgit.transport.JschConfigSessionFactory; -import org.eclipse.jgit.transport.OpenSshConfig; -import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.TrackingRefUpdate; 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.AwsCodeCommitCredentialProvider; import org.springframework.cloud.config.server.support.GitCredentialsProviderFactory; @@ -677,38 +671,6 @@ public class JGitEnvironmentRepositoryTests { } - @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")); - assertTrue(envRepository.isStrictHostKeyChecking()); - 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())); - } - } - @Test public void shouldPrintStacktraceIfDebugEnabled() throws Exception { final Log mockLogger = mock(Log.class); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java new file mode 100644 index 00000000..fbcb3461 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2015 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.ssh; + +import com.jcraft.jsch.*; +import org.eclipse.jgit.transport.OpenSshConfig.Host; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit tests for property based SSH config processor + * @author William Tran + * @author Ollie Hughes + */ +@RunWith(MockitoJUnitRunner.class) +public class PropertyBasedSshSessionFactoryTest { + + private static final String HOST_KEY = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMzCa0AcNbahUFjFYJHIilhJOhKFHuDOOuY+/HqV9kALftitwNYo6dQ+tC9IK5JVZCZfqKfDWVMxspcPDf9eMoE="; + private static final String HOST_KEY_ALGORITHM = "ecdsa-sha2-nistp256"; + private static final String PRIVATE_KEY = getResourceAsString("/ssh/key"); + private PropertyBasedSshSessionFactory factory; + @Mock + private Host hc; + @Mock + private Session session; + @Mock + private JSch jSch; + @Mock + private HostKeyRepository hostKeyRepository; + + @Test + public void strictHostKeyCheckingIsOptional() { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("ssh://gitlab.example.local:3322/somerepo.git") + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.configure(hc, session); + + verify(session).setConfig("StrictHostKeyChecking", "no"); + verifyNoMoreInteractions(session); + } + + @Test + public void strictHostKeyCheckingIsUsed() { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("ssh://gitlab.example.local:3322/somerepo.git") + .hostKey(HOST_KEY) + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.configure(hc, session); + + verify(session).setConfig("StrictHostKeyChecking", "yes"); + verifyNoMoreInteractions(session); + } + + @Test + public void hostKeyAlgorithmIsSpecified() { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("ssh://gitlab.example.local:3322/somerepo.git") + .hostKeyAlgorithm(HOST_KEY_ALGORITHM) + .hostKey(HOST_KEY) + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.configure(hc, session); + verify(session).setConfig("server_host_key", HOST_KEY_ALGORITHM); + verify(session).setConfig("StrictHostKeyChecking", "yes"); + verifyNoMoreInteractions(session); + } + + @Test + public void privateKeyIsUsed() throws Exception { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("git@gitlab.example.local:someorg/somerepo.git") + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.createSession(hc, null, sshKey.getHostname(), 22, null); + verify(jSch).addIdentity("gitlab.example.local", PRIVATE_KEY.getBytes(), null, null); + } + + @Test + public void hostKeyIsUsed() throws Exception { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("git@gitlab.example.local:someorg/somerepo.git") + .hostKey(HOST_KEY) + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.createSession(hc, null, sshKey.getHostname(), 22, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(HostKey.class); + verify(hostKeyRepository).add(captor.capture(), any(UserInfo.class)); + HostKey hostKey = captor.getValue(); + Assert.assertEquals("gitlab.example.local", hostKey.getHost()); + Assert.assertEquals(HOST_KEY, hostKey.getKey()); + } + + private void setupSessionFactory(SshUriProperties sshKey) { + Map sshKeysByHostname = new HashMap<>(); + sshKeysByHostname.put(sshKey.getHostname(), sshKey); + factory = new PropertyBasedSshSessionFactory(sshKeysByHostname, jSch) ; + when(hc.getHostName()).thenReturn(sshKey.getHostname()); + when(jSch.getHostKeyRepository()).thenReturn(hostKeyRepository); + } + + public static String getResourceAsString(String path) { + try { + Resource resource = new ClassPathResource(path); + try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { + StringBuilder builder = new StringBuilder(); + String line = ""; + while ((line = br.readLine()) != null) { + builder.append(line).append('\n'); + } + return builder.toString(); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java new file mode 100644 index 00000000..3fe69c98 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java @@ -0,0 +1,185 @@ +/* + * Copyright 2015 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.ssh; + +import org.junit.Test; + +import static org.mockito.Mockito.*; + +/** + * Unit tests for property based SSH config validators + * + * @author Ollie Hughes + */ +public class SshPropertyValidatorTest { + + private static final String SSH_URI = "git@gitserver.com:team/repo1.git"; + + private static final String VALID_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\n" + + "yyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\n" + + "uP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\n" + + "ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\n" + + "TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n" + + "+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\n" + + "jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\n" + + "lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\n" + + "xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n" + + "4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n" + + "7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n" + + "3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\n" + + "q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\n" + + "mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\n" + + "lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\n" + + "ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\n" + + "ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\n" + + "BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\n" + + "zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\n" + + "uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n" + + "54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\n" + + "E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n" + + "4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n" + + "/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n" + + "2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n" + + "-----END RSA PRIVATE KEY-----"; + + private static final String VALID_HOST_KEY = "AAAAB3NzaC1yc2EAAAADAQABAAABAQDg6/W/5cbk/npvzpae7ZEa54F4rkwh2V3NiuqVZ5hWr+8O4/6SmrS7yBvRHAFeAJNb0LOCjE/7tjd1fqUx+QU1ATCtwkOhuwG8Ubzkx23mMZlrwEvx7XEfBoLN7Lw9fXjWDtTTgFB1AxCQ2pGGiNG0QCwyA4HViDHVU+ibwkRlzuDJG0tnp5Qpo3DXkHwFNdqWNfVrIZ6q2xbyeoJjKjnR215T0ehmuWFmKqG+uMNe/LQ6IOiK0F5+gr7rgPxNLAYYqyhraAnBeHn5gapsSzYJmFpoAHWvN7OUwHcJ88D9qUkKi4VKxYiuK69u3z825Xj2cLTfj9JiHCfV8cTo9GL"; + + @Test + public void supportedParametersSuccesful() throws Exception { + SshUriProperties validSettings = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey(VALID_PRIVATE_KEY) + .hostKey(VALID_HOST_KEY) + .hostKeyAlgorithm("ssh-rsa") + .build(); + + SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(validSettings)); + sshPropertyValidator.validateSshConfigurationProperties(); + verify(sshPropertyValidator, times(1)).validatePrivateKeyFormat(); + verify(sshPropertyValidator, times(1)).validateAlgorithmSpecifiedWhenHostKeySet(); + verify(sshPropertyValidator, times(1)).validatePrivateKeyPresent(); + verify(sshPropertyValidator, times(1)).validateHostKeyAlgorithmSupported(); + verify(sshPropertyValidator, times(1)).validateHostKeySpecifiedWhenAlgorithmSet(); + } + + @Test(expected = IllegalStateException.class) + public void invalidPrivateKeyFails() throws Exception { + + SshUriProperties invalidKey = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(invalidKey); + sshPropertyValidator.validateSshConfigurationProperties(); + + } + + @Test(expected = IllegalStateException.class) + public void missingPrivateKeyFails() throws Exception { + + SshUriProperties missingKey = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingKey); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test(expected = IllegalStateException.class) + public void hostKeyWithMissingAlgoFails() throws Exception { + + SshUriProperties missingAlgo = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .hostKey("some_host") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingAlgo); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test(expected = IllegalStateException.class) + public void algoWithMissingHostKeyFails() throws Exception { + + SshUriProperties missingHostKey = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .hostKeyAlgorithm("some_host_algo") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingHostKey); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test(expected = IllegalStateException.class) + public void unsupportedAlgoFails() throws Exception { + + SshUriProperties unsupportedAlgo = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .hostKey("some_host_key") + .hostKeyAlgorithm("unsupported") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(unsupportedAlgo); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test + public void validatorNotRunIfIgnoreLocalSettingsFalse() throws Exception { + + SshUriProperties useLocal = (SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(false) + .privateKey("invalid_key") + .build()); + + SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(useLocal)); + sshPropertyValidator.validateSshConfigurationProperties(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyFormat(); + verify(sshPropertyValidator, times(0)).validateAlgorithmSpecifiedWhenHostKeySet(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyPresent(); + verify(sshPropertyValidator, times(0)).validateHostKeyAlgorithmSupported(); + verify(sshPropertyValidator, times(0)).validateHostKeySpecifiedWhenAlgorithmSet(); + } + + @Test + public void validatorNotRunIfHttpsUri() throws Exception { + + SshUriProperties httpsUri = (SshUriProperties.builder() + .uri("https://somerepo.com/team/project.git") + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .build()); + + SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(httpsUri)); + sshPropertyValidator.validateSshConfigurationProperties(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyFormat(); + verify(sshPropertyValidator, times(0)).validateAlgorithmSpecifiedWhenHostKeySet(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyPresent(); + verify(sshPropertyValidator, times(0)).validateHostKeyAlgorithmSupported(); + verify(sshPropertyValidator, times(0)).validateHostKeySpecifiedWhenAlgorithmSet(); + } +} \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java new file mode 100644 index 00000000..cc3457b0 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2015 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.ssh; + +import org.eclipse.jgit.transport.SshSessionFactory; +import org.junit.After; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +/** + * Unit tests for property based SSH config processor + * @author William Tran + * @author Ollie Hughes + */ +public class SshUriPropertyProcessorTest { + + private static final String PRIVATE_KEY1 = "privateKey"; + private static final String HOST_KEY1 = "hostKey"; + private static final String ALGO1 = "ssh-rsa"; + private static final String URI1 = "git@gitlab.test.local:wtran/my-repo"; + private static final String HOST1 = "gitlab.test.local"; + private static final String PRIVATE_KEY2 = "privateKey2"; + private static final String URI2 = "git@gitlab2.test.local:wtran/my-repo"; + private static final String HOST2 = "gitlab2.test.local"; + + @After + public void cleanup() { + SshSessionFactory.setInstance(null); + } + + @Test + public void testSingleSshUriProperties() { + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(mainRepoPropertiesFixture()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + + assertThat(sshKeysByHostname.values(), hasSize(1)); + + SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + assertMainRepo(sshKey); + } + + @Test + public void testMultipleSshUriPropertiess() { + SshUriProperties sshUriProperties = mainRepoPropertiesFixture(); + addRepoProperties(sshUriProperties, SshUriProperties.builder() + .uri(URI2) + .privateKey(PRIVATE_KEY2) + .build()); + + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); + + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + + + SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + assertMainRepo(sshKey); + + sshKey = sshKeysByHostname.get(HOST2); + + assertThat(sshKeysByHostname.values(), hasSize(2)); + + assertThat(sshKey.getHostname(), is(equalTo(HOST2))); + + assertThat(sshKey.getHostKeyAlgorithm(), is(nullValue())); + + assertThat(sshKey.getHostKey(), is(nullValue())); + + assertThat(sshKey.getPrivateKey(), is(equalTo(PRIVATE_KEY2))); + } + + @Test + public void testSameHostnameDifferentKeysFirstOneWins() { + SshUriProperties sshUriProperties = mainRepoPropertiesFixture(); + addRepoProperties(sshUriProperties, SshUriProperties.builder().uri(URI1) + .privateKey(PRIVATE_KEY1) + .hostKey(HOST_KEY1) + .hostKeyAlgorithm(ALGO1) + .build()); + + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + + assertThat(sshKeysByHostname.values(), hasSize(1)); + + SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + assertMainRepo(sshKey); + } + + @Test + public void testNoSshUriPropertiess() { + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(new SshUriProperties()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + assertThat(sshKeysByHostname.values(), hasSize(0)); + } + + private SshUriProperties mainRepoPropertiesFixture() { + + return SshUriProperties.builder() + .uri(URI1) + .hostKeyAlgorithm(ALGO1) + .hostKey(HOST_KEY1) + .privateKey(PRIVATE_KEY1) + .build(); + } + + private void addRepoProperties(SshUriProperties mainRepoProperties, SshUriProperties repoProperties) { + Map repos = new HashMap<>(); + repos.put("repo2", repoProperties); + mainRepoProperties.setRepos(repos); + } + + private void assertMainRepo(SshUriProperties sshKey) { + assertThat(sshKey.getHostname(), is(equalTo(HOST1))); + assertThat(sshKey.getHostKeyAlgorithm(), is(equalTo(ALGO1))); + assertThat(sshKey.getHostKey(), is(equalTo(HOST_KEY1))); + assertThat(sshKey.getPrivateKey(), is(equalTo(PRIVATE_KEY1))); + } + +} diff --git a/spring-cloud-config-server/src/test/resources/ssh/key b/spring-cloud-config-server/src/test/resources/ssh/key new file mode 100644 index 00000000..f1586c20 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwQTXywKTzJEYXQ/LOR/hDciu4tJ1Av8zQQqi4ekL2hnOUzA0 +XecMmoYp31j3aWbT8RFNaUxFObfG7uGUWG21NlciqN7NtRkzTceygwBl25J9UEIo +iSHkS2arMhSIQ/B5TIFmL8fucsMUzqZ0LFwd2NR5k+BL5Ip/g6Gp6NpY2lNlxG3W +EaI/ee73vp8fJd9xge1o5Y/WFjCdP2TVfBuMTzswtjooakhQMg16c3YxcpK3Utbc +J4gaPDAGSd9eu5YuxSnTyC4s+FMPsKF3zEu+kTb5iutiCvO4lFdvlhURN2WBecRt +Am4nrMHEmLp3Yy440UTSOAzUUdxlfyltxvdOnQIDAQABAoIBABpQtbb4z/u44HB3 +ocZVAVLmxGkqNkfFUVCRyakxI6uFjyqqTMlsobxDhFvt/jVBjq20JTCw9FMtKhMR +cC8qLeWNtaz+S19gYy08h6ryl/B6pVkW2/3uP/jDs51tTTKuC2uRzMPA51KAJEsU +SIxvtqYEOdseXlp3Q3FHzilv82M4dKQCUXO/BbYV1jtgGHsJIJIcJhb8JIuXGScq +krm+GrzXpejF8FjpEFJ+VRbh/wjbMqUD5/9KimtJ9tXK5MKaxZ2aerYVONrPIbRH +Ij2tKS2o+cEBL9b0E2GHSGQyzs+g4+eTmsf56O8MWAoh5uib3pKXi84wxWi1ETQd ++NIGuUECgYEA9VZuTCbJVEriqptZKw8BNn7xIlXkmABEWAmIlo8U4NodjP/WIyIB +FtwYS4jUxfJhLdgs8mzb7O+EY6F+lusQBcAViRfk6a0VL9qrzYAY21d1X0hRtt4t +NKixjQ1hESao/CQDOonsYIqolXlDsvYxDX0tWXkVjiXPVk71oPtaF80CgYEAyWhT +j1v6XtFQT4KAQ10pQEbXBet9ae4M6WALAghdROMY60zX6XdXNGugycy3T9s9q712 +gxglhhhQO+RpEtaVOXvOzZv5aczrDqPwYOX1jJbIaOT4Iv2szjI518akRTef+ANW +t+mETUBf+qYKhW91lfSmi5PUPrWBs5z8j9qcohECgYEA1XgfAKO/ClkeYNRLAexM +Yd6Scg3KmXeh//hLoLVetUcT71usN2WfDQT1HsQwXwxHAlzWvxl5QvAlgYtDWo2t +o1M/acogpDK1/K2IT4bmGfQ/bGjAsR0guHEQDGtXylSZNMshBA5+XoJHrceV4GHW +zKXej7uLxeOgRLjvgOMzwQECgYAWSjJ9OiuyVK3zrritO+E12OExKYe+snbBWTy+ +o9hWqF94ow9+KhP/nC22R6696sCfgh9ckM6OUaCHkFbEm/T14KrZfHqU5/XXr/MJ +dkm/nmxdl4GStcprI4ndRIGnm2lb1FrDjddziT1WCww9CkHUL7hC2EB0FDhffW9C +KGqPUQKBgQCd85DH751a6EKN8dyKUyf3sP8BraTsc26DFk3q+vfhzSyJF4wxIVWa +kmjx6kfGTu0+ED0UXWi5LnMSvne4rGU1j3rQyHs71t6JLVcFl8j5i8g/n91FA87C +EIni0JgBvgbDNlM04mMFE6K0u5+Rt3Nx/yywW/w9nQUrzNio7MP+8w== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml new file mode 100644 index 00000000..28b09e86 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml @@ -0,0 +1,41 @@ +spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo1.git + ignoreLocalSshSettings: true + privateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\nyyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\nuP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\nayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\nTOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\njfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\nlDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\nxdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\nq4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\nmRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\nlKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\nZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\nZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\nBT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\nzf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\nuC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\nE1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n-----END RSA PRIVATE KEY-----" + repos: + repo1: + uri: git@gitserver.com:team/repo2.git + hostKey: someHostKey + hostKeyAlgorithm: ssh-rsa + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX + IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF + ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud + 1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i + oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W + DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd + fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b + BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG + EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj + 5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8 + +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe + pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG + ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ + xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW + dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi + PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX + VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z + FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R + gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4 + VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV + cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee + KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N + CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs + q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J + 69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT + -----END RSA PRIVATE KEY----- diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml new file mode 100644 index 00000000..3db045f3 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml @@ -0,0 +1,35 @@ +spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo.git + ignoreLocalSshSettings: true + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI + yyxp38YcuZG9cUDBAxby+K2TKmwHaC1Wf1QTwbPuCRdIPrDwRz+FLoegm3iDLCmn + uP6rjZDneYsqfU1sSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km + ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq + TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir + +diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk + jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt + lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI + xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V + 4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG + 7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu + 3reKgm0CgYEA03Sx8JgF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8 + q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq + mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa + lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M + ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv + ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit + BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO + zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw + uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF + 54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS + E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx + 4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr + /syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq + 2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ== + -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml new file mode 100644 index 00000000..399ba87a --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml @@ -0,0 +1,10 @@ +spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo.git + ignoreLocalSshSettings: true + privateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\nyyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\nuP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\nayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\nTOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\njfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\nlDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\nxdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\nq4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\nmRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\nlKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\nZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\nZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\nBT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\nzf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\nuC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\nE1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n-----END RSA PRIVATE KEY-----" + hostKey: somekey + hostKeyAlgorithm: ssh-rsa \ No newline at end of file From 67cdfab7fa427bac60e8760ffe9d8daf185d5a96 Mon Sep 17 00:00:00 2001 From: Ollie Hughes Date: Wed, 12 Jul 2017 17:41:04 +0100 Subject: [PATCH 2/3] Use JGit URIish to parse SSH URIs Use JSR-303 cross field validation for `SshUriProperties` Rebase with 1.3.x Dalston --- docs/pom.xml | 2 +- .../main/asciidoc/spring-cloud-config.adoc | 10 +- .../server/config/TransportConfiguration.java | 10 +- .../JGitEnvironmentRepository.java | 1 + .../server/ssh/HostKeyAlgoSupported.java | 39 ++++++++ .../ssh/HostKeyAlgoSupportedValidator.java | 77 +++++++++++++++ .../server/ssh/HostKeyAndAlgoBothExist.java | 38 +++++++ .../ssh/HostKeyAndAlgoBothExistValidator.java | 84 ++++++++++++++++ .../config/server/ssh/PrivateKeyIsValid.java | 24 +++++ .../server/ssh/PrivateKeyValidator.java | 89 +++++++++++++++++ .../ssh/PropertyBasedSshSessionFactory.java | 15 +-- .../server/ssh/SshPropertyValidator.java | 91 +++++------------ .../config/server/ssh/SshUriProperties.java | 99 +++---------------- .../server/ssh/SshUriPropertyProcessor.java | 22 ++--- .../src/main/resources/configserver.yml | 1 - .../config/TransportConfigurationTest.java | 54 ++++++++++ .../PropertyBasedSshSessionFactoryTest.java | 9 +- .../server/ssh/SshPropertyValidatorTest.java | 85 ++++++++-------- .../ssh/SshUriPropertyProcessorTest.java | 72 ++++++++++---- 19 files changed, 572 insertions(+), 250 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupported.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExist.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExistValidator.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyIsValid.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java diff --git a/docs/pom.xml b/docs/pom.xml index b41839ae..abc036ed 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-config - 1.4.0.BUILD-SNAPSHOT + 1.3.2.BUILD-SNAPSHOT .. diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 55146702..f4ab6f8a 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -151,8 +151,8 @@ This repository implementation maps the `{label}` parameter of the HTTP resource to a git label (commit id, branch name or tag). If the git branch or tag name contains a slash ("/") then the label in the HTTP URL should be specified with the special string "(\_)" instead (to -avoid ambiguity with other URL paths). For example, if the label is -`foo/bar`, replacing the slash would result in a label that looks like +avoid ambiguity with other URL paths). For example, if the label is +`foo/bar`, replacing the slash would result in a label that looks like `foo(_)bar`. Be careful with the brackets in the URL if you are using a command line client like curl (e.g. escape them from the shell with quotes ''). @@ -351,7 +351,7 @@ https://git-codecommit.${AWS_REGION}.amazonaws.com/${repopath}. If you provide a username and password with an AWS CodeCommit URI, then these must be the http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html[AWS accessKeyId and secretAccessKey] to be used to access the repository. If you do not specify a username and password, -then the accessKeyId and secretAccessKey will be retrieved using the +then the accessKeyId and secretAccessKey will be retrieved using the http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html[AWS Default Credential Provider Chain]. If your Git URI matches the CodeCommit URI pattern (above) then you must provide @@ -364,7 +364,7 @@ classpath, then the AWS Code Commit credential provider will not be created rega ===== Git SSH configuration using properties -By default, Spring Cloud Config Server uses SSH configuration files such as `~/.ssh/known_hosts` and `/etc/ssh/ssh_config` when connecting to Git repositories using an SSH URI. +By default, the JGit library used by Spring Cloud Config Server uses SSH configuration files such as `~/.ssh/known_hosts` and `/etc/ssh/ssh_config` when connecting to Git repositories using an SSH URI. In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. For cases such as these, SSH configuration can be set using Java properties. In order to activate property based SSH configuration, the property `spring.cloud.config.server.git.ignoreLocalSshSettings` must be set to `true`. Example: @@ -416,7 +416,7 @@ Example: |Property Name |Remarks |*ignoreLocalSshSettings* -|If true, use property based SSH config instead of file based +|If true, use property based SSH config instead of file based. Must be set at as `spring.cloud.config.server.git.ignoreLocalSshSettings`, *not* inside a repository definition. |*privateKey* |Valid SSH private key. Must be set if `ignoreLocalSshSettings` is true and Git URI is SSH format diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java index f5199cd1..a8d3721c 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java @@ -64,10 +64,12 @@ public class TransportConfiguration { @Override public void configure(Transport transport) { - SshTransport sshTransport = (SshTransport) transport; - sshTransport.setSshSessionFactory( - new PropertyBasedSshSessionFactory( - new SshUriPropertyProcessor(sshUriProperties).getSshKeysByHostname(), new JSch())); + if (transport instanceof SshTransport) { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory( + new PropertyBasedSshSessionFactory( + new SshUriPropertyProcessor(sshUriProperties).getSshKeysByHostname(), new JSch())); + } } } 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 8df63428..7ad63742 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 @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; + import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupported.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupported.java new file mode 100644 index 00000000..68fae7d0 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupported.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 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.ssh; + +import org.springframework.validation.annotation.Validated; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Beans annotated with {@link HostKeyAlgoSupported} and {@link Validated} will have the constraints applied. + * + * @author Ollie Hughes + **/ +@Constraint(validatedBy = HostKeyAlgoSupportedValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface HostKeyAlgoSupported { + String message() default "{HostKeyAlgoSupported.message}"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java new file mode 100644 index 00000000..335fe3a4 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015 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.ssh; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.springframework.validation.annotation.Validated; + +import static java.lang.String.format; +import static org.springframework.cloud.config.server.ssh.SshPropertyValidator.isSshUri; +import static org.springframework.util.StringUtils.hasText; + +/** + * JSR-303 Cross Field validator that ensures that a {@link SshUriProperties} bean for the constraints: + * - If host key algo is supported + * + * Beans annotated with {@link HostKeyAlgoSupported} and {@link Validated} will have the constraints applied. + * + * @author Ollie Hughes + */ +public class HostKeyAlgoSupportedValidator implements ConstraintValidator { + private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git."; + private final SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(); + private static final Set VALID_HOST_KEY_ALGORITHMS = new LinkedHashSet<>(Arrays.asList( + "ssh-dss","ssh-rsa","ecdsa-sha2-nistp256","ecdsa-sha2-nistp384","ecdsa-sha2-nistp521")); + + @Override + public void initialize(HostKeyAlgoSupported constrainAnnotation) { + //No special initialization of validator required + } + + @Override + public boolean isValid(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + Set validationResults = new HashSet<>(); + List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); + + for (SshUriProperties extractedProperty : extractedProperties) { + if (sshUriProperties.isIgnoreLocalSshSettings() && isSshUri(extractedProperty.getUri())) { + validationResults.add(isHostKeySpecifiedWhenAlgorithmSet(extractedProperty, context)); + } + } + return !validationResults.contains(false); + } + + private boolean isHostKeySpecifiedWhenAlgorithmSet(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + if (hasText(sshUriProperties.getHostKeyAlgorithm()) + && !VALID_HOST_KEY_ALGORITHMS.contains(sshUriProperties.getHostKeyAlgorithm())) { + + context.buildConstraintViolationWithTemplate( + format("Property '%shostKeyAlgorithm' must be one of %s", GIT_PROPERTY_PREFIX, VALID_HOST_KEY_ALGORITHMS)) + .addConstraintViolation(); + return false; + } + return true; + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExist.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExist.java new file mode 100644 index 00000000..b0464526 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExist.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 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.ssh; + +import org.springframework.validation.annotation.Validated; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Beans annotated with {@link HostKeyAndAlgoBothExist} and {@link Validated} will have the constraints applied. + * @author Ollie Hughes + */ +@Constraint(validatedBy = HostKeyAndAlgoBothExistValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface HostKeyAndAlgoBothExist { + String message() default "{HostKeyAndAlgoBothExist.message}"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExistValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExistValidator.java new file mode 100644 index 00000000..1c4439ca --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAndAlgoBothExistValidator.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 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.ssh; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.springframework.validation.annotation.Validated; + +import static java.lang.String.format; +import static org.springframework.cloud.config.server.ssh.SshPropertyValidator.isSshUri; +import static org.springframework.util.StringUtils.hasText; + +/** + * JSR-303 Cross Field validator that ensures that a {@link SshUriProperties} bean for the constraints: + * - If host key is set then host key algo must also be set + * - If host key algo is set then host key must also be set + * + * Beans annotated with {@link HostKeyAndAlgoBothExist} and {@link Validated} will have the constraints applied. + * + * @author Ollie Hughes + */ +public class HostKeyAndAlgoBothExistValidator implements ConstraintValidator { + private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git."; + private final SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(); + + @Override + public void initialize(HostKeyAndAlgoBothExist constrainAnnotation) { + //No special initialization of validator required + } + + @Override + public boolean isValid(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + Set validationResults = new HashSet<>(); + List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); + + for (SshUriProperties extractedProperty : extractedProperties) { + if (sshUriProperties.isIgnoreLocalSshSettings() && isSshUri(extractedProperty.getUri())) { + validationResults.add( + isAlgorithmSpecifiedWhenHostKeySet(extractedProperty, context) + && isHostKeySpecifiedWhenAlgorithmSet(extractedProperty, context)); + } + } + return !validationResults.contains(false); + } + + private boolean isHostKeySpecifiedWhenAlgorithmSet(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + if (hasText(sshUriProperties.getHostKeyAlgorithm()) && !hasText(sshUriProperties.getHostKey())) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate( + format("Property '%shostKey' must be set when '%shostKeyAlgorithm' is specified", GIT_PROPERTY_PREFIX, GIT_PROPERTY_PREFIX)) + .addConstraintViolation(); + return false; + } + return true; + } + + private boolean isAlgorithmSpecifiedWhenHostKeySet(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + if (hasText(sshUriProperties.getHostKey()) && !hasText(sshUriProperties.getHostKeyAlgorithm())) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate( + format("Property '%shostKeyAlgorithm' must be set when '%shostKey' is specified", GIT_PROPERTY_PREFIX, GIT_PROPERTY_PREFIX)) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyIsValid.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyIsValid.java new file mode 100644 index 00000000..6a0a831b --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyIsValid.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.config.server.ssh; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +import org.springframework.validation.annotation.Validated; + +/** +* Beans annotated with {@link PrivateKeyValidator} and {@link Validated} will have the constraints applied. +* +* @author Ollie Hughes +*/ +@Constraint(validatedBy = PrivateKeyValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface PrivateKeyIsValid { + String message() default "{PrivateKeyIsValid.message}"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java new file mode 100644 index 00000000..900de013 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PrivateKeyValidator.java @@ -0,0 +1,89 @@ +/* + * Copyright 2015 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.ssh; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.KeyPair; + +import org.springframework.validation.annotation.Validated; + +import static java.lang.String.format; +import static org.springframework.cloud.config.server.ssh.SshPropertyValidator.isSshUri; +import static org.springframework.util.StringUtils.hasText; + +/** + * JSR-303 Cross Field validator that ensures that a {@link SshUriProperties} bean for the constraints: + * - Private key is present and can be correctly parsed using {@link com.jcraft.jsch.KeyPair} + * + * Beans annotated with {@link PrivateKeyValidator} and {@link Validated} will have the constraints applied. + * + * @author Ollie Hughes + */ +public class PrivateKeyValidator implements ConstraintValidator { + private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git."; + private final SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(); + + @Override + public void initialize(PrivateKeyIsValid constrainAnnotation) { + //No special initialization of validator required + } + + @Override + public boolean isValid(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + Set validationResults = new HashSet<>(); + List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); + + for (SshUriProperties extractedProperty : extractedProperties) { + if (sshUriProperties.isIgnoreLocalSshSettings() && isSshUri(extractedProperty.getUri())) { + validationResults.add( + isPrivateKeyPresent(extractedProperty, context) + && isPrivateKeyFormatCorrect(extractedProperty, context)); + } + } + return !validationResults.contains(false); + + } + + private boolean isPrivateKeyPresent(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + if (!hasText(sshUriProperties.getPrivateKey())) { + context.buildConstraintViolationWithTemplate( + format("Property '%shostKey' must be set when '%shostKeyAlgorithm' is specified", GIT_PROPERTY_PREFIX, GIT_PROPERTY_PREFIX)) + .addConstraintViolation(); + return false; + } + return true; + } + + private boolean isPrivateKeyFormatCorrect(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + try { + KeyPair.load(new JSch(), sshUriProperties.getPrivateKey().getBytes(), null); + return true; + } catch (JSchException e) { + context.buildConstraintViolationWithTemplate( + format("Property '%sprivateKey' contains is not a valid private key", GIT_PROPERTY_PREFIX)) + .addConstraintViolation(); + return false; + } + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java index 93493a43..20a4d701 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java @@ -15,6 +15,8 @@ */ package org.springframework.cloud.config.server.ssh; +import java.util.Map; + import com.jcraft.jsch.HostKey; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; @@ -24,9 +26,6 @@ import org.eclipse.jgit.transport.OpenSshConfig.Host; import org.eclipse.jgit.util.Base64; import org.eclipse.jgit.util.FS; -import java.util.Map; - - /** * In a cloud environment local SSH config files such as `.known_hosts` may not be suitable for providing * configuration settings due to ephemeral filesystems. This flag enables SSH config to be provided as application @@ -36,6 +35,10 @@ import java.util.Map; */ public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { + private static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; + private static final String YES_OPTION = "yes"; + private static final String NO_OPTION = "no"; + private static final String SERVER_HOST_KEY = "server_host_key"; private final Map sshKeysByHostname; private final JSch jSch; @@ -49,12 +52,12 @@ public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { SshUriProperties sshProperties = sshKeysByHostname.get(hc.getHostName()); String hostKeyAlgorithm = sshProperties.getHostKeyAlgorithm(); if (hostKeyAlgorithm != null) { - session.setConfig("server_host_key", hostKeyAlgorithm); + session.setConfig(SERVER_HOST_KEY, hostKeyAlgorithm); } if (sshProperties.getHostKey() == null || !sshProperties.isStrictHostKeyChecking()) { - session.setConfig("StrictHostKeyChecking", "no"); + session.setConfig(STRICT_HOST_KEY_CHECKING, NO_OPTION); } else { - session.setConfig("StrictHostKeyChecking", "yes"); + session.setConfig(STRICT_HOST_KEY_CHECKING, YES_OPTION); } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java index 011effe7..b289587c 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java @@ -16,18 +16,16 @@ package org.springframework.cloud.config.server.ssh; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.KeyPair; -import org.springframework.beans.factory.annotation.Autowired; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.transport.URIish; + import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; -import org.springframework.util.Assert; -import javax.annotation.PostConstruct; -import java.util.*; - -import static java.lang.String.format; import static org.springframework.util.StringUtils.hasText; /** @@ -39,72 +37,31 @@ import static org.springframework.util.StringUtils.hasText; @EnableConfigurationProperties(SshUriProperties.class) public class SshPropertyValidator { - private final SshUriProperties sshUriProperties; - private final JSch jsch = new JSch(); - private static final Set VALID_HOST_KEY_ALGORITHMS = new LinkedHashSet<>(Arrays.asList( - "ssh-dss","ssh-rsa","ecdsa-sha2-nistp256","ecdsa-sha2-nistp384","ecdsa-sha2-nistp521")); - private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git."; + protected static boolean isSshUri(Object uri) { + if(uri != null) { + try { + URIish urIish = new URIish(uri.toString()); + String scheme = urIish.getScheme(); + if(scheme == null && hasText(urIish.getHost()) && hasText(urIish.getUser())) { + //JGit returns null if using SCP URI but user and host will be populated + return true; + } + return scheme != null && !scheme.matches("^(http|https)$"); - @Autowired - public SshPropertyValidator(SshUriProperties sshUriProperties) { - this.sshUriProperties = sshUriProperties; - } - - static boolean isSshUri(Object uri) { - return uri != null && (uri.toString().startsWith("ssh") || uri.toString().startsWith("git")); + } catch (URISyntaxException e) { + return false; + } + } + return false; } - @PostConstruct - public void validateSshConfigurationProperties() { + protected List extractRepoProperties(SshUriProperties sshUriProperties) { List allRepoProperties = new ArrayList<>(); allRepoProperties.add(sshUriProperties); Map repos = sshUriProperties.getRepos(); if (repos != null) { allRepoProperties.addAll(repos.values()); } - for (SshUriProperties repoProperties : allRepoProperties) { - if(isSshUri(repoProperties.getUri()) && sshUriProperties.isIgnoreLocalSshSettings()){ - validatePrivateKeyPresent(); - validatePrivateKeyFormat(); - validateAlgorithmSpecifiedWhenHostKeySet(); - validateHostKeySpecifiedWhenAlgorithmSet(); - validateHostKeyAlgorithmSupported(); - } - } + return allRepoProperties; } - - protected void validatePrivateKeyFormat() { - try { - KeyPair.load(jsch, sshUriProperties.getPrivateKey().getBytes(), null); - } catch (JSchException e) { - throw new IllegalStateException(format("Property '%sprivateKey' contains an invalid value", GIT_PROPERTY_PREFIX)); - } - } - - protected void validateHostKeyAlgorithmSupported() { - if (hasText(sshUriProperties.getHostKeyAlgorithm())) { - Assert.state(VALID_HOST_KEY_ALGORITHMS.contains(sshUriProperties.getHostKeyAlgorithm()), - format("Property '%shostKeyAlgorithm' must be one of %s", GIT_PROPERTY_PREFIX, VALID_HOST_KEY_ALGORITHMS)); - } - } - - protected void validatePrivateKeyPresent() { - Assert.state(sshUriProperties.getPrivateKey() != null, - format("Property '%sprivateKey' must be set when '%signoreLocalSshSettings' is set to 'true'", GIT_PROPERTY_PREFIX, GIT_PROPERTY_PREFIX)); - } - - protected void validateHostKeySpecifiedWhenAlgorithmSet() { - if (hasText(sshUriProperties.getHostKeyAlgorithm())) { - Assert.state(hasText(sshUriProperties.getHostKey()), - format("Property '%shostKey' must be set when 'hostKeyAlgorithm' is specified", GIT_PROPERTY_PREFIX)); - } - } - - protected void validateAlgorithmSpecifiedWhenHostKeySet() { - if (hasText(sshUriProperties.getHostKey())) { - Assert.state(hasText(sshUriProperties.getHostKeyAlgorithm()), - format("Property '%shostKeyAlgorithm' must be set when 'hostKey' is specified", GIT_PROPERTY_PREFIX)); - } - } - } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java index 8d036d5a..5c8124d9 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java @@ -15,12 +15,11 @@ */ package org.springframework.cloud.config.server.ssh; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.web.util.UriComponentsBuilder; - import java.util.HashMap; import java.util.Map; -import java.util.Objects; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; /** * Data container for property based SSH config @@ -28,25 +27,25 @@ import java.util.Objects; * @author Ollie Hughes */ @ConfigurationProperties("spring.cloud.config.server.git") +@Validated +@PrivateKeyIsValid +@HostKeyAndAlgoBothExist +@HostKeyAlgoSupported public class SshUriProperties { + private String privateKey; private String uri; private String hostKeyAlgorithm; private String hostKey; - private String privateKey; - private String username; - private String password; private boolean ignoreLocalSshSettings; private boolean strictHostKeyChecking = true; private Map repos = new HashMap<>(); - public SshUriProperties(String uri, String hostKeyAlgorithm, String hostKey, String privateKey, String username, String password, boolean ignoreLocalSshSettings, boolean strictHostKeyChecking, Map repos) { + public SshUriProperties(String uri, String hostKeyAlgorithm, String hostKey, String privateKey, boolean ignoreLocalSshSettings, boolean strictHostKeyChecking, Map repos) { this.uri = uri; this.hostKeyAlgorithm = hostKeyAlgorithm; this.hostKey = hostKey; this.privateKey = privateKey; - this.username = username; - this.password = password; this.ignoreLocalSshSettings = ignoreLocalSshSettings; this.strictHostKeyChecking = strictHostKeyChecking; this.repos = repos; @@ -59,28 +58,6 @@ public class SshUriProperties { return new SshUriPropertiesBuilder(); } - public boolean isSshUri() { - return uri != null && !uri.startsWith("http"); - } - - public String getHostname() { - if (getUri() == null) { - return null; - } - - if (getUri().matches("^[a-z]+://.*")) { - return UriComponentsBuilder.fromUriString(uri).build().getHost(); - } - else if (getUri().indexOf('@') < getUri().indexOf(':')) { - return getUri().substring(getUri().indexOf('@') + 1, uri.indexOf(':')); - } - else if (getUri().startsWith("ssh:") && getUri().indexOf('@') > 0) { - String postAt = getUri().substring(getUri().indexOf('@') + 1); - return postAt.substring(0, postAt.indexOf(":")); - } - else return null; - } - public String getUri() { return this.uri; } @@ -97,14 +74,6 @@ public class SshUriProperties { return this.privateKey; } - public String getUsername() { - return this.username; - } - - public String getPassword() { - return this.password; - } - public boolean isIgnoreLocalSshSettings() { return this.ignoreLocalSshSettings; } @@ -133,14 +102,6 @@ public class SshUriProperties { this.privateKey = privateKey; } - public void setUsername(String username) { - this.username = username; - } - - public void setPassword(String password) { - this.password = password; - } - public void setIgnoreLocalSshSettings(boolean ignoreLocalSshSettings) { this.ignoreLocalSshSettings = ignoreLocalSshSettings; } @@ -153,32 +114,12 @@ public class SshUriProperties { this.repos = repos; } - @Override - public int hashCode() { - return Objects.hash(uri, hostKeyAlgorithm, hostKey, privateKey, username, password, ignoreLocalSshSettings, strictHostKeyChecking); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - final SshUriProperties other = (SshUriProperties) obj; - return Objects.equals(this.uri, other.uri) - && Objects.equals(this.hostKeyAlgorithm, other.hostKeyAlgorithm) - && Objects.equals(this.hostKey, other.hostKey) - && Objects.equals(this.privateKey, other.privateKey) - && Objects.equals(this.username, other.username) - && Objects.equals(this.password, other.password) - && Objects.equals(this.ignoreLocalSshSettings, other.ignoreLocalSshSettings) - && Objects.equals(this.strictHostKeyChecking, other.strictHostKeyChecking); + public void addRepo(String repoName, SshUriProperties properties) { + this.repos.put(repoName, properties); } public String toString() { - return "org.springframework.cloud.config.server.ssh.SshUriProperties(uri=" + this.getUri() + " hostKeyAlgorithm=" + this.getHostKeyAlgorithm() + ", hostKey=" + this.getHostKey() + ", privateKey=" + this.getPrivateKey() + ", username=" + this.getUsername() + ", password=" + this.getPassword() + ", ignoreLocalSshSettings=" + this.isIgnoreLocalSshSettings() + ", strictHostKeyChecking=" + this.isStrictHostKeyChecking() + ", repos=" + this.getRepos() + ")"; + return "org.springframework.cloud.config.server.ssh.SshUriProperties(uri=" + this.getUri() + " hostKeyAlgorithm=" + this.getHostKeyAlgorithm() + ", hostKey=" + this.getHostKey() + ", privateKey=" + this.getPrivateKey() + ", ignoreLocalSshSettings=" + this.isIgnoreLocalSshSettings() + ", strictHostKeyChecking=" + this.isStrictHostKeyChecking() + ", repos=" + this.getRepos() + ")"; } public static class SshUriPropertiesBuilder { @@ -186,8 +127,6 @@ public class SshUriProperties { private String hostKeyAlgorithm; private String hostKey; private String privateKey; - private String username; - private String password; private boolean ignoreLocalSshSettings; private boolean strictHostKeyChecking = true; private Map repos; @@ -215,16 +154,6 @@ public class SshUriProperties { return this; } - public SshUriProperties.SshUriPropertiesBuilder username(String username) { - this.username = username; - return this; - } - - public SshUriProperties.SshUriPropertiesBuilder password(String password) { - this.password = password; - return this; - } - public SshUriProperties.SshUriPropertiesBuilder ignoreLocalSshSettings(boolean ignoreLocalSshSettings) { this.ignoreLocalSshSettings = ignoreLocalSshSettings; return this; @@ -241,11 +170,11 @@ public class SshUriProperties { } public SshUriProperties build() { - return new SshUriProperties(uri, hostKeyAlgorithm, hostKey, privateKey, username, password, ignoreLocalSshSettings, strictHostKeyChecking, repos); + return new SshUriProperties(uri, hostKeyAlgorithm, hostKey, privateKey, ignoreLocalSshSettings, strictHostKeyChecking, repos); } public String toString() { - return "org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriPropertiesBuilder(uri=" + this.uri + "hostKeyAlgorithm=" + this.hostKeyAlgorithm + ", hostKey=" + this.hostKey + ", privateKey=" + this.privateKey + ", username=" + this.username + ", password=" + this.password + ", ignoreLocalSshSettings=" + this.ignoreLocalSshSettings + ", strictHostKeyChecking=" + this.strictHostKeyChecking + ", repos=" + this.repos + ")"; + return "org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriPropertiesBuilder(uri=" + this.uri + "hostKeyAlgorithm=" + this.hostKeyAlgorithm + ", hostKey=" + this.hostKey + ", privateKey=" + this.privateKey + ", ignoreLocalSshSettings=" + this.ignoreLocalSshSettings + ", strictHostKeyChecking=" + this.strictHostKeyChecking + ", repos=" + this.repos + ")"; } } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java index 791dcda9..ca004475 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java @@ -15,11 +15,13 @@ */ package org.springframework.cloud.config.server.ssh; -import org.springframework.web.util.UriComponentsBuilder; +import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; +import org.eclipse.jgit.transport.URIish; + import static org.springframework.cloud.config.server.ssh.SshPropertyValidator.isSshUri; /** @@ -57,20 +59,12 @@ public class SshUriPropertyProcessor { return sshUriPropertyMap; } - private String getHostname(String uri) { - if (uri == null) { + protected static String getHostname(String uri) { + try { + URIish urIish = new URIish(uri); + return urIish.getHost(); + } catch (URISyntaxException e) { return null; } - else if (uri.matches("^[a-z]+://.*")) { - return UriComponentsBuilder.fromUriString(uri).build().getHost(); - } - else if (uri.indexOf('@') < uri.indexOf(':')) { - return uri.substring(uri.indexOf('@') + 1, uri.indexOf(':')); - } - else if (uri.startsWith("ssh:") && uri.indexOf('@') > 0) { - String postAt = uri.substring(uri.indexOf('@') + 1); - return postAt.substring(0, postAt.indexOf(":")); - } - else return null; } } diff --git a/spring-cloud-config-server/src/main/resources/configserver.yml b/spring-cloud-config-server/src/main/resources/configserver.yml index 176cc464..ee3218a8 100644 --- a/spring-cloud-config-server/src/main/resources/configserver.yml +++ b/spring-cloud-config-server/src/main/resources/configserver.yml @@ -18,4 +18,3 @@ server: port: 8888 management: context_path: /admin - \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java new file mode 100644 index 00000000..649eab13 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 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.config; + +import org.eclipse.jgit.api.TransportConfigCallback; +import org.junit.Test; +import org.springframework.cloud.config.server.ssh.SshUriProperties; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.instanceOf; +/** +* @author Ollie Hughes +*/ +public class TransportConfigurationTest { + @Test + public void propertiesBasedSshTransportCallbackCreated() throws Exception { + SshUriProperties ignoreLocalSettings = SshUriProperties.builder() + .uri("user@gitrepo.com:proj/repo") + .ignoreLocalSshSettings(true) + .build(); + TransportConfiguration transportConfiguration = new TransportConfiguration(); + TransportConfigCallback transportConfigCallback = transportConfiguration.propertiesBasedSshTransportCallback(ignoreLocalSettings); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.PropertiesBasedSshTransportConfigCallback.class))); + } + + @Test + public void fileBasedSshTransportCallbackCreated() throws Exception { + SshUriProperties dontIgnoreLocalSettings = SshUriProperties.builder() + .uri("user@gitrepo.com:proj/repo") + .ignoreLocalSshSettings(false) + .build(); + + TransportConfiguration transportConfiguration = new TransportConfiguration(); + TransportConfigCallback transportConfigCallback = transportConfiguration.propertiesBasedSshTransportCallback(dontIgnoreLocalSettings); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.FileBasedSshTransportConfigCallback.class))); + + } + +} \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java index fbcb3461..2836886c 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java @@ -23,7 +23,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -113,7 +112,7 @@ public class PropertyBasedSshSessionFactoryTest { .build(); setupSessionFactory(sshKey); - factory.createSession(hc, null, sshKey.getHostname(), 22, null); + factory.createSession(hc, null, SshUriPropertyProcessor.getHostname(sshKey.getUri()), 22, null); verify(jSch).addIdentity("gitlab.example.local", PRIVATE_KEY.getBytes(), null, null); } @@ -126,7 +125,7 @@ public class PropertyBasedSshSessionFactoryTest { .build(); setupSessionFactory(sshKey); - factory.createSession(hc, null, sshKey.getHostname(), 22, null); + factory.createSession(hc, null, SshUriPropertyProcessor.getHostname(sshKey.getUri()), 22, null); ArgumentCaptor captor = ArgumentCaptor.forClass(HostKey.class); verify(hostKeyRepository).add(captor.capture(), any(UserInfo.class)); HostKey hostKey = captor.getValue(); @@ -136,9 +135,9 @@ public class PropertyBasedSshSessionFactoryTest { private void setupSessionFactory(SshUriProperties sshKey) { Map sshKeysByHostname = new HashMap<>(); - sshKeysByHostname.put(sshKey.getHostname(), sshKey); + sshKeysByHostname.put(SshUriPropertyProcessor.getHostname(sshKey.getUri()), sshKey); factory = new PropertyBasedSshSessionFactory(sshKeysByHostname, jSch) ; - when(hc.getHostName()).thenReturn(sshKey.getHostname()); + when(hc.getHostName()).thenReturn(SshUriPropertyProcessor.getHostname(sshKey.getUri())); when(jSch.getHostKeyRepository()).thenReturn(hostKeyRepository); } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java index 3fe69c98..7ce36ba6 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java @@ -16,9 +16,17 @@ package org.springframework.cloud.config.server.ssh; +import org.junit.BeforeClass; import org.junit.Test; -import static org.mockito.Mockito.*; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; /** * Unit tests for property based SSH config validators @@ -58,6 +66,13 @@ public class SshPropertyValidatorTest { "-----END RSA PRIVATE KEY-----"; private static final String VALID_HOST_KEY = "AAAAB3NzaC1yc2EAAAADAQABAAABAQDg6/W/5cbk/npvzpae7ZEa54F4rkwh2V3NiuqVZ5hWr+8O4/6SmrS7yBvRHAFeAJNb0LOCjE/7tjd1fqUx+QU1ATCtwkOhuwG8Ubzkx23mMZlrwEvx7XEfBoLN7Lw9fXjWDtTTgFB1AxCQ2pGGiNG0QCwyA4HViDHVU+ibwkRlzuDJG0tnp5Qpo3DXkHwFNdqWNfVrIZ6q2xbyeoJjKjnR215T0ehmuWFmKqG+uMNe/LQ6IOiK0F5+gr7rgPxNLAYYqyhraAnBeHn5gapsSzYJmFpoAHWvN7OUwHcJ88D9qUkKi4VKxYiuK69u3z825Xj2cLTfj9JiHCfV8cTo9GL"; + private static Validator validator; + + @BeforeClass + public static void setUpValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } @Test public void supportedParametersSuccesful() throws Exception { @@ -69,16 +84,12 @@ public class SshPropertyValidatorTest { .hostKeyAlgorithm("ssh-rsa") .build(); - SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(validSettings)); - sshPropertyValidator.validateSshConfigurationProperties(); - verify(sshPropertyValidator, times(1)).validatePrivateKeyFormat(); - verify(sshPropertyValidator, times(1)).validateAlgorithmSpecifiedWhenHostKeySet(); - verify(sshPropertyValidator, times(1)).validatePrivateKeyPresent(); - verify(sshPropertyValidator, times(1)).validateHostKeyAlgorithmSupported(); - verify(sshPropertyValidator, times(1)).validateHostKeySpecifiedWhenAlgorithmSet(); + Set> constraintViolations = validator.validate(validSettings); + assertThat(constraintViolations, hasSize(0)); + } - @Test(expected = IllegalStateException.class) + @Test public void invalidPrivateKeyFails() throws Exception { SshUriProperties invalidKey = SshUriProperties.builder() @@ -87,12 +98,12 @@ public class SshPropertyValidatorTest { .privateKey("invalid_key") .build(); - SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(invalidKey); - sshPropertyValidator.validateSshConfigurationProperties(); + Set> constraintViolations = validator.validate(invalidKey); + assertThat(constraintViolations, hasSize(1)); } - @Test(expected = IllegalStateException.class) + @Test public void missingPrivateKeyFails() throws Exception { SshUriProperties missingKey = SshUriProperties.builder() @@ -100,51 +111,51 @@ public class SshPropertyValidatorTest { .ignoreLocalSshSettings(true) .build(); - SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingKey); - sshPropertyValidator.validateSshConfigurationProperties(); + Set> constraintViolations = validator.validate(missingKey); + assertThat(constraintViolations, hasSize(1)); } - @Test(expected = IllegalStateException.class) + @Test public void hostKeyWithMissingAlgoFails() throws Exception { SshUriProperties missingAlgo = SshUriProperties.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) - .privateKey("invalid_key") + .privateKey(VALID_PRIVATE_KEY) .hostKey("some_host") .build(); - SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingAlgo); - sshPropertyValidator.validateSshConfigurationProperties(); + Set> constraintViolations = validator.validate(missingAlgo); + assertThat(constraintViolations, hasSize(1)); } - @Test(expected = IllegalStateException.class) + @Test public void algoWithMissingHostKeyFails() throws Exception { SshUriProperties missingHostKey = SshUriProperties.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) - .privateKey("invalid_key") - .hostKeyAlgorithm("some_host_algo") + .privateKey(VALID_PRIVATE_KEY) + .hostKeyAlgorithm("ssh-rsa") .build(); - SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingHostKey); - sshPropertyValidator.validateSshConfigurationProperties(); + Set> constraintViolations = validator.validate(missingHostKey); + assertThat(constraintViolations, hasSize(1)); } - @Test(expected = IllegalStateException.class) + @Test public void unsupportedAlgoFails() throws Exception { SshUriProperties unsupportedAlgo = SshUriProperties.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) - .privateKey("invalid_key") + .privateKey(VALID_PRIVATE_KEY) .hostKey("some_host_key") .hostKeyAlgorithm("unsupported") .build(); - SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(unsupportedAlgo); - sshPropertyValidator.validateSshConfigurationProperties(); + Set> constraintViolations = validator.validate(unsupportedAlgo); + assertThat(constraintViolations, hasSize(1)); } @Test @@ -156,13 +167,9 @@ public class SshPropertyValidatorTest { .privateKey("invalid_key") .build()); - SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(useLocal)); - sshPropertyValidator.validateSshConfigurationProperties(); - verify(sshPropertyValidator, times(0)).validatePrivateKeyFormat(); - verify(sshPropertyValidator, times(0)).validateAlgorithmSpecifiedWhenHostKeySet(); - verify(sshPropertyValidator, times(0)).validatePrivateKeyPresent(); - verify(sshPropertyValidator, times(0)).validateHostKeyAlgorithmSupported(); - verify(sshPropertyValidator, times(0)).validateHostKeySpecifiedWhenAlgorithmSet(); + Set> constraintViolations = validator.validate(useLocal); + assertThat(constraintViolations, hasSize(0)); + } @Test @@ -174,12 +181,8 @@ public class SshPropertyValidatorTest { .privateKey("invalid_key") .build()); - SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(httpsUri)); - sshPropertyValidator.validateSshConfigurationProperties(); - verify(sshPropertyValidator, times(0)).validatePrivateKeyFormat(); - verify(sshPropertyValidator, times(0)).validateAlgorithmSpecifiedWhenHostKeySet(); - verify(sshPropertyValidator, times(0)).validatePrivateKeyPresent(); - verify(sshPropertyValidator, times(0)).validateHostKeyAlgorithmSupported(); - verify(sshPropertyValidator, times(0)).validateHostKeySpecifiedWhenAlgorithmSet(); + Set> constraintViolations = validator.validate(httpsUri); + assertThat(constraintViolations, hasSize(0)); + } } \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java index cc3457b0..3051abf2 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java @@ -36,11 +36,14 @@ public class SshUriPropertyProcessorTest { private static final String PRIVATE_KEY1 = "privateKey"; private static final String HOST_KEY1 = "hostKey"; private static final String ALGO1 = "ssh-rsa"; - private static final String URI1 = "git@gitlab.test.local:wtran/my-repo"; - private static final String HOST1 = "gitlab.test.local"; + private static final String URI1 = "ollie@gitlab1.test.local:project/my-repo"; + private static final String HOST1 = "gitlab1.test.local"; private static final String PRIVATE_KEY2 = "privateKey2"; - private static final String URI2 = "git@gitlab2.test.local:wtran/my-repo"; + private static final String URI2 = "ssh://git@gitlab2.test.local/wtran/my-repo"; private static final String HOST2 = "gitlab2.test.local"; + private static final String PRIVATE_KEY3 = "privateKey3"; + private static final String URI3 = "git+ssh://git@gitlab3.test.local/wtran/my-repo"; + private static final String HOST3 = "gitlab3.test.local"; @After public void cleanup() { @@ -64,27 +67,34 @@ public class SshUriPropertyProcessorTest { addRepoProperties(sshUriProperties, SshUriProperties.builder() .uri(URI2) .privateKey(PRIVATE_KEY2) - .build()); + .build(), "repo2"); + addRepoProperties(sshUriProperties, SshUriProperties.builder() + .uri(URI3) + .privateKey(PRIVATE_KEY3) + .build(), "repo3"); SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + assertThat(sshKeysByHostname.values(), hasSize(3)); - SshUriProperties sshKey = sshKeysByHostname.get(HOST1); - assertMainRepo(sshKey); + SshUriProperties sshKey1 = sshKeysByHostname.get(HOST1); + assertMainRepo(sshKey1); - sshKey = sshKeysByHostname.get(HOST2); + SshUriProperties sshKey2 = sshKeysByHostname.get(HOST2); - assertThat(sshKeysByHostname.values(), hasSize(2)); + assertThat(SshUriPropertyProcessor.getHostname(sshKey2.getUri()), is(equalTo(HOST2))); + assertThat(sshKey2.getHostKeyAlgorithm(), is(nullValue())); + assertThat(sshKey2.getHostKey(), is(nullValue())); + assertThat(sshKey2.getPrivateKey(), is(equalTo(PRIVATE_KEY2))); - assertThat(sshKey.getHostname(), is(equalTo(HOST2))); + SshUriProperties sshKey3 = sshKeysByHostname.get(HOST3); - assertThat(sshKey.getHostKeyAlgorithm(), is(nullValue())); - - assertThat(sshKey.getHostKey(), is(nullValue())); - - assertThat(sshKey.getPrivateKey(), is(equalTo(PRIVATE_KEY2))); + assertThat(SshUriPropertyProcessor.getHostname(sshKey3.getUri()), is(equalTo(HOST3))); + assertThat(sshKey3.getHostKeyAlgorithm(), is(nullValue())); + assertThat(sshKey3.getHostKey(), is(nullValue())); + assertThat(sshKey3.getPrivateKey(), is(equalTo(PRIVATE_KEY3))); } @Test @@ -94,7 +104,7 @@ public class SshUriPropertyProcessorTest { .privateKey(PRIVATE_KEY1) .hostKey(HOST_KEY1) .hostKeyAlgorithm(ALGO1) - .build()); + .build(), "repo2"); SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); @@ -106,12 +116,26 @@ public class SshUriPropertyProcessorTest { } @Test - public void testNoSshUriPropertiess() { + public void testNoSshUriProperties() { SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(new SshUriProperties()); Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); assertThat(sshKeysByHostname.values(), hasSize(0)); } + @Test + public void testInvalidUriDoesNotAddEntry() { + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(SshUriProperties.builder().uri("invalid_uri").build()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + assertThat(sshKeysByHostname.values(), hasSize(0)); + } + + @Test + public void testHttpsUriDoesNotAddEntry() { + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(SshUriProperties.builder().uri("https://user@github.com/proj/repo.git").build()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + assertThat(sshKeysByHostname.values(), hasSize(0)); + } + private SshUriProperties mainRepoPropertiesFixture() { return SshUriProperties.builder() @@ -122,14 +146,20 @@ public class SshUriPropertyProcessorTest { .build(); } - private void addRepoProperties(SshUriProperties mainRepoProperties, SshUriProperties repoProperties) { - Map repos = new HashMap<>(); - repos.put("repo2", repoProperties); - mainRepoProperties.setRepos(repos); + private void addRepoProperties(SshUriProperties mainRepoProperties, SshUriProperties repoProperties, String repoName) { + if (mainRepoProperties.getRepos() == null) { + Map repos = new HashMap<>(); + repos.put(repoName, repoProperties); + mainRepoProperties.setRepos(repos); + } + else { + mainRepoProperties.addRepo(repoName, repoProperties); + } } private void assertMainRepo(SshUriProperties sshKey) { - assertThat(sshKey.getHostname(), is(equalTo(HOST1))); + assertThat(sshKey, is(notNullValue())); + assertThat(SshUriPropertyProcessor.getHostname(sshKey.getUri()), is(equalTo(HOST1))); assertThat(sshKey.getHostKeyAlgorithm(), is(equalTo(ALGO1))); assertThat(sshKey.getHostKey(), is(equalTo(HOST_KEY1))); assertThat(sshKey.getPrivateKey(), is(equalTo(PRIVATE_KEY1))); From 537f6ce64860b3bcd6437709ece82b7c5bd7e71f Mon Sep 17 00:00:00 2001 From: Ollie Hughes Date: Thu, 20 Jul 2017 12:02:31 +0100 Subject: [PATCH 3/3] Differentiate between SSH settings found at the top level of a Git configuration object and those found as a map under the Repos property. Do this by introducing common base class SshUri that holds common properties and SshUriNestedRepoProperties, that is used to contain properties in the repos property map. This avoids Boot from guarding against a potential infinite deserialization loop. --- .../ssh/HostKeyAlgoSupportedValidator.java | 6 +- .../ssh/HostKeyAndAlgoBothExistValidator.java | 8 +- .../server/ssh/PrivateKeyValidator.java | 8 +- .../ssh/PropertyBasedSshSessionFactory.java | 8 +- .../server/ssh/SshPropertyValidator.java | 6 +- .../cloud/config/server/ssh/SshUri.java | 150 ++++++++++++++++++ .../config/server/ssh/SshUriProperties.java | 145 ++--------------- .../server/ssh/SshUriPropertyProcessor.java | 11 +- .../config/TransportConfigurationTest.java | 5 +- .../PropertyBasedSshSessionFactoryTest.java | 14 +- .../server/ssh/SshPropertyValidatorTest.java | 16 +- .../ssh/SshUriPropertyProcessorTest.java | 58 +++---- 12 files changed, 232 insertions(+), 203 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUri.java diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java index 335fe3a4..d75d2e48 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/HostKeyAlgoSupportedValidator.java @@ -52,9 +52,9 @@ public class HostKeyAlgoSupportedValidator implements ConstraintValidator validationResults = new HashSet<>(); - List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); + List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); - for (SshUriProperties extractedProperty : extractedProperties) { + for (SshUri extractedProperty : extractedProperties) { if (sshUriProperties.isIgnoreLocalSshSettings() && isSshUri(extractedProperty.getUri())) { validationResults.add(isHostKeySpecifiedWhenAlgorithmSet(extractedProperty, context)); } @@ -62,7 +62,7 @@ public class HostKeyAlgoSupportedValidator implements ConstraintValidator validationResults = new HashSet<>(); - List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); + List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); - for (SshUriProperties extractedProperty : extractedProperties) { + for (SshUri extractedProperty : extractedProperties) { if (sshUriProperties.isIgnoreLocalSshSettings() && isSshUri(extractedProperty.getUri())) { validationResults.add( isAlgorithmSpecifiedWhenHostKeySet(extractedProperty, context) @@ -60,7 +60,7 @@ public class HostKeyAndAlgoBothExistValidator implements ConstraintValidator validationResults = new HashSet<>(); - List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); + List extractedProperties = sshPropertyValidator.extractRepoProperties(sshUriProperties); - for (SshUriProperties extractedProperty : extractedProperties) { + for (SshUri extractedProperty : extractedProperties) { if (sshUriProperties.isIgnoreLocalSshSettings() && isSshUri(extractedProperty.getUri())) { validationResults.add( isPrivateKeyPresent(extractedProperty, context) @@ -65,7 +65,7 @@ public class PrivateKeyValidator implements ConstraintValidator sshKeysByHostname; + private final Map sshKeysByHostname; private final JSch jSch; - public PropertyBasedSshSessionFactory(Map sshKeysByHostname, JSch jSch) { + public PropertyBasedSshSessionFactory(Map sshKeysByHostname, JSch jSch) { this.sshKeysByHostname = sshKeysByHostname; this.jSch = jSch; } @Override protected void configure(Host hc, Session session) { - SshUriProperties sshProperties = sshKeysByHostname.get(hc.getHostName()); + SshUri sshProperties = sshKeysByHostname.get(hc.getHostName()); String hostKeyAlgorithm = sshProperties.getHostKeyAlgorithm(); if (hostKeyAlgorithm != null) { session.setConfig(SERVER_HOST_KEY, hostKeyAlgorithm); @@ -64,7 +64,7 @@ public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { @Override protected Session createSession(Host hc, String user, String host, int port, FS fs) throws JSchException { if (sshKeysByHostname.containsKey(host)) { - SshUriProperties sshUriProperties = sshKeysByHostname.get(host); + SshUri sshUriProperties = sshKeysByHostname.get(host); jSch.addIdentity(host, sshUriProperties.getPrivateKey().getBytes(), null, null); if (sshUriProperties.getHostKey() != null) { HostKey hostkey = new HostKey(host, Base64.decode(sshUriProperties.getHostKey())); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java index b289587c..6b1b1475 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java @@ -55,10 +55,10 @@ public class SshPropertyValidator { return false; } - protected List extractRepoProperties(SshUriProperties sshUriProperties) { - List allRepoProperties = new ArrayList<>(); + protected List extractRepoProperties(SshUriProperties sshUriProperties) { + List allRepoProperties = new ArrayList<>(); allRepoProperties.add(sshUriProperties); - Map repos = sshUriProperties.getRepos(); + Map repos = sshUriProperties.getRepos(); if (repos != null) { allRepoProperties.addAll(repos.values()); } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUri.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUri.java new file mode 100644 index 00000000..109d6a31 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUri.java @@ -0,0 +1,150 @@ +package org.springframework.cloud.config.server.ssh; + +import org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriNestedRepoProperties; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Base class that contains configuration properties for Git SSH properties + * + * @author Ollie Hughes + */ +public abstract class SshUri { + private String privateKey; + private String uri; + private String hostKeyAlgorithm; + private String hostKey; + private boolean ignoreLocalSshSettings; + private boolean strictHostKeyChecking = true; + + public static SshUriPropertiesBuilder builder() { + return new SshUriPropertiesBuilder(); + } + + public String getUri() { + return this.uri; + } + + public String getHostKeyAlgorithm() { + return this.hostKeyAlgorithm; + } + + public String getHostKey() { + return this.hostKey; + } + + public String getPrivateKey() { + return this.privateKey; + } + + public boolean isIgnoreLocalSshSettings() { + return this.ignoreLocalSshSettings; + } + + public boolean isStrictHostKeyChecking() { + return this.strictHostKeyChecking; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setHostKeyAlgorithm(String hostKeyAlgorithm) { + this.hostKeyAlgorithm = hostKeyAlgorithm; + } + + public void setHostKey(String hostKey) { + this.hostKey = hostKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public void setIgnoreLocalSshSettings(boolean ignoreLocalSshSettings) { + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + } + + public void setStrictHostKeyChecking(boolean strictHostKeyChecking) { + this.strictHostKeyChecking = strictHostKeyChecking; + } + + public String toString() { + return "org.springframework.cloud.config.server.ssh.SshUriProperties(uri=" + this.getUri() + " hostKeyAlgorithm=" + this.getHostKeyAlgorithm() + ", hostKey=" + this.getHostKey() + ", privateKey=" + this.getPrivateKey() + ", ignoreLocalSshSettings=" + this.isIgnoreLocalSshSettings() + ", strictHostKeyChecking=" + this.isStrictHostKeyChecking() + ",)"; + } + + public static class SshUriPropertiesBuilder { + private String uri; + private String hostKeyAlgorithm; + private String hostKey; + private String privateKey; + private boolean ignoreLocalSshSettings; + private boolean strictHostKeyChecking = true; + private Map repos = new LinkedHashMap<>(); + + SshUriPropertiesBuilder() { + } + + public SshUri.SshUriPropertiesBuilder uri(String uri) { + this.uri = uri; + return this; + } + + public SshUri.SshUriPropertiesBuilder hostKeyAlgorithm(String hostKeyAlgorithm) { + this.hostKeyAlgorithm = hostKeyAlgorithm; + return this; + } + + public SshUri.SshUriPropertiesBuilder hostKey(String hostKey) { + this.hostKey = hostKey; + return this; + } + + public SshUri.SshUriPropertiesBuilder privateKey(String privateKey) { + this.privateKey = privateKey; + return this; + } + + public SshUri.SshUriPropertiesBuilder ignoreLocalSshSettings(boolean ignoreLocalSshSettings) { + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + return this; + } + + public SshUri.SshUriPropertiesBuilder strictHostKeyChecking(boolean strictHostKeyChecking) { + this.strictHostKeyChecking = strictHostKeyChecking; + return this; + } + + public SshUri.SshUriPropertiesBuilder repos(Map repos) { + this.repos = repos; + return this; + } + + public SshUriProperties build() { + SshUriProperties sshUriProperties = new SshUriProperties(); + sshUriProperties.setRepos(repos); + build(sshUriProperties); + return sshUriProperties; + } + + public SshUriNestedRepoProperties buildAsNestedRepo() { + SshUriNestedRepoProperties sshUriNestedRepoProperties = new SshUriNestedRepoProperties(); + build(sshUriNestedRepoProperties); + return sshUriNestedRepoProperties; + } + + private void build(SshUri sshUriNestedRepoProperties) { + sshUriNestedRepoProperties.setUri(uri); + sshUriNestedRepoProperties.setHostKeyAlgorithm(hostKeyAlgorithm); + sshUriNestedRepoProperties.setHostKey(hostKey); + sshUriNestedRepoProperties.setPrivateKey(privateKey); + sshUriNestedRepoProperties.setIgnoreLocalSshSettings(ignoreLocalSshSettings); + sshUriNestedRepoProperties.setStrictHostKeyChecking(strictHostKeyChecking); + } + + public String toString() { + return "org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriPropertiesBuilder(uri=" + this.uri + "hostKeyAlgorithm=" + this.hostKeyAlgorithm + ", hostKey=" + this.hostKey + ", privateKey=" + this.privateKey + ", ignoreLocalSshSettings=" + this.ignoreLocalSshSettings + ", strictHostKeyChecking=" + this.strictHostKeyChecking + ", repos=" + this.repos + ")"; + } + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java index 5c8124d9..c5a25ddb 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java @@ -15,7 +15,7 @@ */ package org.springframework.cloud.config.server.ssh; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -31,150 +31,33 @@ import org.springframework.validation.annotation.Validated; @PrivateKeyIsValid @HostKeyAndAlgoBothExist @HostKeyAlgoSupported -public class SshUriProperties { - private String privateKey; - private String uri; - private String hostKeyAlgorithm; - private String hostKey; - private boolean ignoreLocalSshSettings; - private boolean strictHostKeyChecking = true; +public class SshUriProperties extends SshUri { - private Map repos = new HashMap<>(); + private Map repos = new LinkedHashMap<>(); - public SshUriProperties(String uri, String hostKeyAlgorithm, String hostKey, String privateKey, boolean ignoreLocalSshSettings, boolean strictHostKeyChecking, Map repos) { - this.uri = uri; - this.hostKeyAlgorithm = hostKeyAlgorithm; - this.hostKey = hostKey; - this.privateKey = privateKey; - this.ignoreLocalSshSettings = ignoreLocalSshSettings; - this.strictHostKeyChecking = strictHostKeyChecking; - this.repos = repos; - } - - public SshUriProperties() { - } - - public static SshUriPropertiesBuilder builder() { - return new SshUriPropertiesBuilder(); - } - - public String getUri() { - return this.uri; - } - - public String getHostKeyAlgorithm() { - return this.hostKeyAlgorithm; - } - - public String getHostKey() { - return this.hostKey; - } - - public String getPrivateKey() { - return this.privateKey; - } - - public boolean isIgnoreLocalSshSettings() { - return this.ignoreLocalSshSettings; - } - - public boolean isStrictHostKeyChecking() { - return this.strictHostKeyChecking; - } - - public Map getRepos() { + public Map getRepos() { return this.repos; } - public void setUri(String uri) { - this.uri = uri; - } - - public void setHostKeyAlgorithm(String hostKeyAlgorithm) { - this.hostKeyAlgorithm = hostKeyAlgorithm; - } - - public void setHostKey(String hostKey) { - this.hostKey = hostKey; - } - - public void setPrivateKey(String privateKey) { - this.privateKey = privateKey; - } - - public void setIgnoreLocalSshSettings(boolean ignoreLocalSshSettings) { - this.ignoreLocalSshSettings = ignoreLocalSshSettings; - } - - public void setStrictHostKeyChecking(boolean strictHostKeyChecking) { - this.strictHostKeyChecking = strictHostKeyChecking; - } - - public void setRepos(Map repos) { + public void setRepos(Map repos) { this.repos = repos; } - public void addRepo(String repoName, SshUriProperties properties) { + public void addRepo(String repoName, SshUriProperties.SshUriNestedRepoProperties properties) { this.repos.put(repoName, properties); } + @Override public String toString() { - return "org.springframework.cloud.config.server.ssh.SshUriProperties(uri=" + this.getUri() + " hostKeyAlgorithm=" + this.getHostKeyAlgorithm() + ", hostKey=" + this.getHostKey() + ", privateKey=" + this.getPrivateKey() + ", ignoreLocalSshSettings=" + this.isIgnoreLocalSshSettings() + ", strictHostKeyChecking=" + this.isStrictHostKeyChecking() + ", repos=" + this.getRepos() + ")"; + return super.toString() + "{repos=" + repos + "}"; } - public static class SshUriPropertiesBuilder { - private String uri; - private String hostKeyAlgorithm; - private String hostKey; - private String privateKey; - private boolean ignoreLocalSshSettings; - private boolean strictHostKeyChecking = true; - private Map repos; + /** + * Differentiate between sets of properties that are defined in nested Git repos. + * This is to prevent boot from guarding against a potential infinite deserialization of nested properties. + * This sub class differentiates from {@link SshUriProperties} as it does not contain the self mao + */ + public static class SshUriNestedRepoProperties extends SshUri { - SshUriPropertiesBuilder() { - } - - public SshUriProperties.SshUriPropertiesBuilder uri(String uri) { - this.uri = uri; - return this; - } - - public SshUriProperties.SshUriPropertiesBuilder hostKeyAlgorithm(String hostKeyAlgorithm) { - this.hostKeyAlgorithm = hostKeyAlgorithm; - return this; - } - - public SshUriProperties.SshUriPropertiesBuilder hostKey(String hostKey) { - this.hostKey = hostKey; - return this; - } - - public SshUriProperties.SshUriPropertiesBuilder privateKey(String privateKey) { - this.privateKey = privateKey; - return this; - } - - public SshUriProperties.SshUriPropertiesBuilder ignoreLocalSshSettings(boolean ignoreLocalSshSettings) { - this.ignoreLocalSshSettings = ignoreLocalSshSettings; - return this; - } - - public SshUriProperties.SshUriPropertiesBuilder strictHostKeyChecking(boolean strictHostKeyChecking) { - this.strictHostKeyChecking = strictHostKeyChecking; - return this; - } - - public SshUriProperties.SshUriPropertiesBuilder repos(Map repos) { - this.repos = repos; - return this; - } - - public SshUriProperties build() { - return new SshUriProperties(uri, hostKeyAlgorithm, hostKey, privateKey, ignoreLocalSshSettings, strictHostKeyChecking, repos); - } - - public String toString() { - return "org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriPropertiesBuilder(uri=" + this.uri + "hostKeyAlgorithm=" + this.hostKeyAlgorithm + ", hostKey=" + this.hostKey + ", privateKey=" + this.privateKey + ", ignoreLocalSshSettings=" + this.ignoreLocalSshSettings + ", strictHostKeyChecking=" + this.strictHostKeyChecking + ", repos=" + this.repos + ")"; - } } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java index ca004475..36cbf85a 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Map; import org.eclipse.jgit.transport.URIish; +import org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriNestedRepoProperties; import static org.springframework.cloud.config.server.ssh.SshPropertyValidator.isSshUri; @@ -37,19 +38,19 @@ public class SshUriPropertyProcessor { this.sshUriProperties = sshUriProperties; } - public Map getSshKeysByHostname() { + public Map getSshKeysByHostname() { return extractNestedProperties(sshUriProperties); } - private Map extractNestedProperties(SshUriProperties uriProperties) { - Map sshUriPropertyMap = new HashMap<>(); + private Map extractNestedProperties(SshUriProperties uriProperties) { + Map sshUriPropertyMap = new HashMap<>(); String parentUri = uriProperties.getUri(); if (isSshUri(parentUri) && getHostname(parentUri) != null) { sshUriPropertyMap.put(getHostname(parentUri), uriProperties); } - Map repos = uriProperties.getRepos(); + Map repos = uriProperties.getRepos(); if(repos != null) { - for (SshUriProperties repoProperties : repos.values()) { + for (SshUriNestedRepoProperties repoProperties : repos.values()) { String repoUri = repoProperties.getUri(); if (isSshUri(repoUri) && getHostname(repoUri) != null) { sshUriPropertyMap.put(getHostname(repoUri), repoProperties); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java index 649eab13..40296eef 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/config/TransportConfigurationTest.java @@ -18,6 +18,7 @@ package org.springframework.cloud.config.server.config; import org.eclipse.jgit.api.TransportConfigCallback; import org.junit.Test; +import org.springframework.cloud.config.server.ssh.SshUri; import org.springframework.cloud.config.server.ssh.SshUriProperties; import static org.hamcrest.MatcherAssert.assertThat; @@ -29,7 +30,7 @@ import static org.hamcrest.Matchers.instanceOf; public class TransportConfigurationTest { @Test public void propertiesBasedSshTransportCallbackCreated() throws Exception { - SshUriProperties ignoreLocalSettings = SshUriProperties.builder() + SshUriProperties ignoreLocalSettings = SshUri.builder() .uri("user@gitrepo.com:proj/repo") .ignoreLocalSshSettings(true) .build(); @@ -40,7 +41,7 @@ public class TransportConfigurationTest { @Test public void fileBasedSshTransportCallbackCreated() throws Exception { - SshUriProperties dontIgnoreLocalSettings = SshUriProperties.builder() + SshUriProperties dontIgnoreLocalSettings = SshUri.builder() .uri("user@gitrepo.com:proj/repo") .ignoreLocalSshSettings(false) .build(); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java index 2836886c..cf41ce6d 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java @@ -61,7 +61,7 @@ public class PropertyBasedSshSessionFactoryTest { @Test public void strictHostKeyCheckingIsOptional() { - SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + SshUri sshKey = new SshUriProperties.SshUriPropertiesBuilder() .uri("ssh://gitlab.example.local:3322/somerepo.git") .privateKey(PRIVATE_KEY) .build(); @@ -75,7 +75,7 @@ public class PropertyBasedSshSessionFactoryTest { @Test public void strictHostKeyCheckingIsUsed() { - SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + SshUri sshKey = new SshUriProperties.SshUriPropertiesBuilder() .uri("ssh://gitlab.example.local:3322/somerepo.git") .hostKey(HOST_KEY) .privateKey(PRIVATE_KEY) @@ -90,7 +90,7 @@ public class PropertyBasedSshSessionFactoryTest { @Test public void hostKeyAlgorithmIsSpecified() { - SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + SshUri sshKey = new SshUriProperties.SshUriPropertiesBuilder() .uri("ssh://gitlab.example.local:3322/somerepo.git") .hostKeyAlgorithm(HOST_KEY_ALGORITHM) .hostKey(HOST_KEY) @@ -106,7 +106,7 @@ public class PropertyBasedSshSessionFactoryTest { @Test public void privateKeyIsUsed() throws Exception { - SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + SshUri sshKey = new SshUriProperties.SshUriPropertiesBuilder() .uri("git@gitlab.example.local:someorg/somerepo.git") .privateKey(PRIVATE_KEY) .build(); @@ -118,7 +118,7 @@ public class PropertyBasedSshSessionFactoryTest { @Test public void hostKeyIsUsed() throws Exception { - SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + SshUri sshKey = new SshUriProperties.SshUriPropertiesBuilder() .uri("git@gitlab.example.local:someorg/somerepo.git") .hostKey(HOST_KEY) .privateKey(PRIVATE_KEY) @@ -133,8 +133,8 @@ public class PropertyBasedSshSessionFactoryTest { Assert.assertEquals(HOST_KEY, hostKey.getKey()); } - private void setupSessionFactory(SshUriProperties sshKey) { - Map sshKeysByHostname = new HashMap<>(); + private void setupSessionFactory(SshUri sshKey) { + Map sshKeysByHostname = new HashMap<>(); sshKeysByHostname.put(SshUriPropertyProcessor.getHostname(sshKey.getUri()), sshKey); factory = new PropertyBasedSshSessionFactory(sshKeysByHostname, jSch) ; when(hc.getHostName()).thenReturn(SshUriPropertyProcessor.getHostname(sshKey.getUri())); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java index 7ce36ba6..154c139a 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java @@ -76,7 +76,7 @@ public class SshPropertyValidatorTest { @Test public void supportedParametersSuccesful() throws Exception { - SshUriProperties validSettings = SshUriProperties.builder() + SshUriProperties validSettings = SshUri.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) .privateKey(VALID_PRIVATE_KEY) @@ -92,7 +92,7 @@ public class SshPropertyValidatorTest { @Test public void invalidPrivateKeyFails() throws Exception { - SshUriProperties invalidKey = SshUriProperties.builder() + SshUriProperties invalidKey = SshUri.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) .privateKey("invalid_key") @@ -106,7 +106,7 @@ public class SshPropertyValidatorTest { @Test public void missingPrivateKeyFails() throws Exception { - SshUriProperties missingKey = SshUriProperties.builder() + SshUriProperties missingKey = SshUri.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) .build(); @@ -118,7 +118,7 @@ public class SshPropertyValidatorTest { @Test public void hostKeyWithMissingAlgoFails() throws Exception { - SshUriProperties missingAlgo = SshUriProperties.builder() + SshUriProperties missingAlgo = SshUri.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) .privateKey(VALID_PRIVATE_KEY) @@ -132,7 +132,7 @@ public class SshPropertyValidatorTest { @Test public void algoWithMissingHostKeyFails() throws Exception { - SshUriProperties missingHostKey = SshUriProperties.builder() + SshUriProperties missingHostKey = SshUri.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) .privateKey(VALID_PRIVATE_KEY) @@ -146,7 +146,7 @@ public class SshPropertyValidatorTest { @Test public void unsupportedAlgoFails() throws Exception { - SshUriProperties unsupportedAlgo = SshUriProperties.builder() + SshUriProperties unsupportedAlgo = SshUri.builder() .uri(SSH_URI) .ignoreLocalSshSettings(true) .privateKey(VALID_PRIVATE_KEY) @@ -161,7 +161,7 @@ public class SshPropertyValidatorTest { @Test public void validatorNotRunIfIgnoreLocalSettingsFalse() throws Exception { - SshUriProperties useLocal = (SshUriProperties.builder() + SshUriProperties useLocal = (SshUri.builder() .uri(SSH_URI) .ignoreLocalSshSettings(false) .privateKey("invalid_key") @@ -175,7 +175,7 @@ public class SshPropertyValidatorTest { @Test public void validatorNotRunIfHttpsUri() throws Exception { - SshUriProperties httpsUri = (SshUriProperties.builder() + SshUriProperties httpsUri = (SshUri.builder() .uri("https://somerepo.com/team/project.git") .ignoreLocalSshSettings(true) .privateKey("invalid_key") diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java index 3051abf2..8f7fd49a 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java @@ -16,12 +16,13 @@ package org.springframework.cloud.config.server.ssh; + +import java.util.Map; import org.eclipse.jgit.transport.SshSessionFactory; import org.junit.After; import org.junit.Test; +import org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriNestedRepoProperties; -import java.util.HashMap; -import java.util.Map; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; @@ -53,43 +54,43 @@ public class SshUriPropertyProcessorTest { @Test public void testSingleSshUriProperties() { SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(mainRepoPropertiesFixture()); - Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); assertThat(sshKeysByHostname.values(), hasSize(1)); - SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + SshUri sshKey = sshKeysByHostname.get(HOST1); assertMainRepo(sshKey); } @Test public void testMultipleSshUriPropertiess() { SshUriProperties sshUriProperties = mainRepoPropertiesFixture(); - addRepoProperties(sshUriProperties, SshUriProperties.builder() + addRepoProperties(sshUriProperties, SshUri.builder() .uri(URI2) .privateKey(PRIVATE_KEY2) - .build(), "repo2"); - addRepoProperties(sshUriProperties, SshUriProperties.builder() + .buildAsNestedRepo(), "repo2"); + addRepoProperties(sshUriProperties, SshUri.builder() .uri(URI3) .privateKey(PRIVATE_KEY3) - .build(), "repo3"); + .buildAsNestedRepo(), "repo3"); SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); - Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); assertThat(sshKeysByHostname.values(), hasSize(3)); - SshUriProperties sshKey1 = sshKeysByHostname.get(HOST1); + SshUri sshKey1 = sshKeysByHostname.get(HOST1); assertMainRepo(sshKey1); - SshUriProperties sshKey2 = sshKeysByHostname.get(HOST2); + SshUri sshKey2 = sshKeysByHostname.get(HOST2); assertThat(SshUriPropertyProcessor.getHostname(sshKey2.getUri()), is(equalTo(HOST2))); assertThat(sshKey2.getHostKeyAlgorithm(), is(nullValue())); assertThat(sshKey2.getHostKey(), is(nullValue())); assertThat(sshKey2.getPrivateKey(), is(equalTo(PRIVATE_KEY2))); - SshUriProperties sshKey3 = sshKeysByHostname.get(HOST3); + SshUri sshKey3 = sshKeysByHostname.get(HOST3); assertThat(SshUriPropertyProcessor.getHostname(sshKey3.getUri()), is(equalTo(HOST3))); assertThat(sshKey3.getHostKeyAlgorithm(), is(nullValue())); @@ -100,45 +101,45 @@ public class SshUriPropertyProcessorTest { @Test public void testSameHostnameDifferentKeysFirstOneWins() { SshUriProperties sshUriProperties = mainRepoPropertiesFixture(); - addRepoProperties(sshUriProperties, SshUriProperties.builder().uri(URI1) + addRepoProperties(sshUriProperties, SshUri.builder().uri(URI1) .privateKey(PRIVATE_KEY1) .hostKey(HOST_KEY1) .hostKeyAlgorithm(ALGO1) - .build(), "repo2"); + .buildAsNestedRepo(), "repo2"); SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); - Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); assertThat(sshKeysByHostname.values(), hasSize(1)); - SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + SshUri sshKey = sshKeysByHostname.get(HOST1); assertMainRepo(sshKey); } @Test public void testNoSshUriProperties() { SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(new SshUriProperties()); - Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); assertThat(sshKeysByHostname.values(), hasSize(0)); } @Test public void testInvalidUriDoesNotAddEntry() { - SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(SshUriProperties.builder().uri("invalid_uri").build()); - Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(SshUri.builder().uri("invalid_uri").build()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); assertThat(sshKeysByHostname.values(), hasSize(0)); } @Test public void testHttpsUriDoesNotAddEntry() { - SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(SshUriProperties.builder().uri("https://user@github.com/proj/repo.git").build()); - Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(SshUri.builder().uri("https://user@github.com/proj/repo.git").build()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); assertThat(sshKeysByHostname.values(), hasSize(0)); } private SshUriProperties mainRepoPropertiesFixture() { - return SshUriProperties.builder() + return SshUri.builder() .uri(URI1) .hostKeyAlgorithm(ALGO1) .hostKey(HOST_KEY1) @@ -146,18 +147,11 @@ public class SshUriPropertyProcessorTest { .build(); } - private void addRepoProperties(SshUriProperties mainRepoProperties, SshUriProperties repoProperties, String repoName) { - if (mainRepoProperties.getRepos() == null) { - Map repos = new HashMap<>(); - repos.put(repoName, repoProperties); - mainRepoProperties.setRepos(repos); - } - else { - mainRepoProperties.addRepo(repoName, repoProperties); - } + private void addRepoProperties(SshUriProperties mainRepoProperties, SshUriNestedRepoProperties repoProperties, String repoName) { + mainRepoProperties.addRepo(repoName, repoProperties); } - private void assertMainRepo(SshUriProperties sshKey) { + private void assertMainRepo(SshUri sshKey) { assertThat(sshKey, is(notNullValue())); assertThat(SshUriPropertyProcessor.getHostname(sshKey.getUri()), is(equalTo(HOST1))); assertThat(sshKey.getHostKeyAlgorithm(), is(equalTo(ALGO1)));