From 18a5da61d445c7042e076e7655c2ed06ec5e7ad6 Mon Sep 17 00:00:00 2001 From: Edgars Jasmans Date: Fri, 1 Sep 2017 02:05:34 +0300 Subject: [PATCH] Added additional options for GIT SSH configuration property configuration (#775) Abilty to specify known_hosts file Added option to specify SSH 'PreferredAuthentications' property to override SSH server priorities for authentication methods. fixes gh-101 --- .../main/asciidoc/spring-cloud-config.adoc | 6 ++ .../server/ssh/KnownHostsFileIsValid.java | 40 +++++++++++ .../server/ssh/KnownHostsFileValidator.java | 54 +++++++++++++++ .../ssh/PropertyBasedSshSessionFactory.java | 10 ++- .../cloud/config/server/ssh/SshUri.java | 69 ++++++++++++++++++- .../config/server/ssh/SshUriProperties.java | 3 +- .../PropertyBasedSshSessionFactoryTest.java | 33 ++++++++- .../server/ssh/SshPropertyValidatorTest.java | 23 ++++++- 8 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileIsValid.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileValidator.java diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index f4ab6f8a..f9f19e85 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -429,6 +429,12 @@ Example: |*strictHostKeyChecking* |`true` or `false`. If false, ignore errors with host key + +|*knownHostsFile* +|Location of custom .known_hosts file + +|*preferredAuthentications* +|Override server authentication method order. This should allow evade login prompts if server has keyboard-interactive authentication before `publickey` method. |=== diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileIsValid.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileIsValid.java new file mode 100644 index 00000000..f11af222 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileIsValid.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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 KnownHostsFileIsValid} and {@link Validated} will have the constraints applied. + * + * @author Edgars Jasmans + **/ +@Constraint(validatedBy = KnownHostsFileValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface KnownHostsFileIsValid { + String message() default "{KnownHostsFileIsValid.message}"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileValidator.java new file mode 100644 index 00000000..281cf37c --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/KnownHostsFileValidator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 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.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.io.File; + +import static java.lang.String.*; + +/** + * JSR-303 Cross Field validator that ensures that a {@link SshUriProperties} bean for the constraints: + * - Verifies that known hosts file exists + *

+ * Beans annotated with {@link KnownHostsFileIsValid} and {@link Validated} will have the constraints applied. + * + * @author Edgars Jasmans + */ +public class KnownHostsFileValidator implements ConstraintValidator { + + @Override + public void initialize(KnownHostsFileIsValid knownHostsFileIsValid) { + // No initialization required + } + + @Override + public boolean isValid(SshUriProperties sshUriProperties, ConstraintValidatorContext context) { + String knownHostsFile = sshUriProperties.getKnownHostsFile(); + if (knownHostsFile != null && !new File(knownHostsFile).exists()) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate( + format("File '%s' specified in property 'spring.cloud.config.server.git.knownHostsFile' could not be located", knownHostsFile)) + .addConstraintViolation(); + return false; + } + return true; + } +} 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 0934ccec..0b3d2eb4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 - 2017 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. @@ -36,6 +36,7 @@ import org.eclipse.jgit.util.FS; public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { private static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; + private static final String PREFERRED_AUTHENTICATIONS = "PreferredAuthentications"; private static final String YES_OPTION = "yes"; private static final String NO_OPTION = "no"; private static final String SERVER_HOST_KEY = "server_host_key"; @@ -59,6 +60,10 @@ public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { } else { session.setConfig(STRICT_HOST_KEY_CHECKING, YES_OPTION); } + String preferredAuthentications = sshProperties.getPreferredAuthentications(); + if (preferredAuthentications != null) { + session.setConfig(PREFERRED_AUTHENTICATIONS, preferredAuthentications); + } } @Override @@ -66,6 +71,9 @@ public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { if (sshKeysByHostname.containsKey(host)) { SshUri sshUriProperties = sshKeysByHostname.get(host); jSch.addIdentity(host, sshUriProperties.getPrivateKey().getBytes(), null, null); + if (sshUriProperties.getKnownHostsFile() != null) { + jSch.setKnownHosts(sshUriProperties.getKnownHostsFile()); + } if (sshUriProperties.getHostKey() != null) { HostKey hostkey = new HostKey(host, Base64.decode(sshUriProperties.getHostKey())); jSch.getHostKeyRepository().add(hostkey, null); 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 index 109d6a31..a7211160 100644 --- 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 @@ -1,7 +1,24 @@ +/* + * Copyright 2017 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.cloud.config.server.ssh.SshUriProperties.SshUriNestedRepoProperties; +import javax.validation.constraints.Pattern; import java.util.LinkedHashMap; import java.util.Map; @@ -15,6 +32,9 @@ public abstract class SshUri { private String uri; private String hostKeyAlgorithm; private String hostKey; + private String knownHostsFile; + @Pattern(regexp = "([\\w -]+,)*([\\w -]+)") + private String preferredAuthentications; private boolean ignoreLocalSshSettings; private boolean strictHostKeyChecking = true; @@ -34,6 +54,14 @@ public abstract class SshUri { return this.hostKey; } + public String getKnownHostsFile() { + return this.knownHostsFile; + } + + public String getPreferredAuthentications() { + return this.preferredAuthentications; + } + public String getPrivateKey() { return this.privateKey; } @@ -58,6 +86,14 @@ public abstract class SshUri { this.hostKey = hostKey; } + public void setKnownHostsFile(String knownHostsFile) { + this.knownHostsFile = knownHostsFile; + } + + public void setPreferredAuthentications(String preferredAuthentications) { + this.preferredAuthentications = preferredAuthentications; + } + public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } @@ -71,7 +107,14 @@ public abstract class SshUri { } 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() + ",)"; + return "org.springframework.cloud.config.server.ssh.SshUriProperties(uri=" + this.getUri() + + " hostKeyAlgorithm=" + this.getHostKeyAlgorithm() + + ", hostKey=" + this.getHostKey() + + ", privateKey=" + this.getPrivateKey() + + ", ignoreLocalSshSettings=" + this.isIgnoreLocalSshSettings() + + ", knownHostsFile=" + this.getKnownHostsFile() + + ", preferredAuthentications=" + this.getPreferredAuthentications() + + ", strictHostKeyChecking=" + this.isStrictHostKeyChecking() + ",)"; } public static class SshUriPropertiesBuilder { @@ -79,6 +122,8 @@ public abstract class SshUri { private String hostKeyAlgorithm; private String hostKey; private String privateKey; + private String knownHostsFile; + private String preferredAuthentications; private boolean ignoreLocalSshSettings; private boolean strictHostKeyChecking = true; private Map repos = new LinkedHashMap<>(); @@ -106,6 +151,16 @@ public abstract class SshUri { return this; } + public SshUri.SshUriPropertiesBuilder knownHostsFile(String knownHostsFile) { + this.knownHostsFile = knownHostsFile; + return this; + } + + public SshUri.SshUriPropertiesBuilder preferredAuthentications(String preferredAuthentications) { + this.preferredAuthentications = preferredAuthentications; + return this; + } + public SshUri.SshUriPropertiesBuilder ignoreLocalSshSettings(boolean ignoreLocalSshSettings) { this.ignoreLocalSshSettings = ignoreLocalSshSettings; return this; @@ -139,12 +194,22 @@ public abstract class SshUri { sshUriNestedRepoProperties.setHostKeyAlgorithm(hostKeyAlgorithm); sshUriNestedRepoProperties.setHostKey(hostKey); sshUriNestedRepoProperties.setPrivateKey(privateKey); + sshUriNestedRepoProperties.setKnownHostsFile(knownHostsFile); + sshUriNestedRepoProperties.setPreferredAuthentications(preferredAuthentications); 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 + ")"; + return "org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriPropertiesBuilder(uri=" + this.uri + + "hostKeyAlgorithm=" + this.hostKeyAlgorithm + + ", hostKey=" + this.hostKey + + ", privateKey=" + this.privateKey + + ", knownHostsFile=" + this.knownHostsFile + + ", preferredAuthentications=" + this.preferredAuthentications + + ", 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 c5a25ddb..7aa9879d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 - 2017 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. @@ -31,6 +31,7 @@ import org.springframework.validation.annotation.Validated; @PrivateKeyIsValid @HostKeyAndAlgoBothExist @HostKeyAlgoSupported +@KnownHostsFileIsValid public class SshUriProperties extends SshUri { private Map repos = new LinkedHashMap<>(); 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 45fb97ba..ad7280f7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 - 2017 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. @@ -138,6 +138,37 @@ public class PropertyBasedSshSessionFactoryTest { Assert.assertEquals(HOST_KEY, hostKey.getKey()); } + @Test + public void preferredAuthenticationsIsSpecified() { + SshUri sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("ssh://gitlab.example.local:3322/somerepo.git") + .privateKey(PRIVATE_KEY) + .preferredAuthentications("password,keyboard-interactive") + .build(); + setupSessionFactory(sshKey); + + factory.configure(hc, session); + verify(session).setConfig("PreferredAuthentications", "password,keyboard-interactive"); + verify(session).setConfig("StrictHostKeyChecking", "no"); + verifyNoMoreInteractions(session); + } + + @Test + public void customKnownHostsFileIsUsed() throws Exception { + SshUri sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("git@gitlab.example.local:someorg/somerepo.git") + .privateKey(PRIVATE_KEY) + .knownHostsFile("/ssh/known_hosts") + .build(); + setupSessionFactory(sshKey); + + factory.createSession(hc, null, SshUriPropertyProcessor.getHostname(sshKey.getUri()), 22, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + + verify(jSch).setKnownHosts(captor.capture()); + Assert.assertEquals("/ssh/known_hosts", captor.getValue()); + } + private void setupSessionFactory(SshUri sshKey) { Map sshKeysByHostname = new HashMap<>(); sshKeysByHostname.put(SshUriPropertyProcessor.getHostname(sshKey.getUri()), sshKey); 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 154c139a..942f96ec 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015 - 2017 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. @@ -185,4 +185,25 @@ public class SshPropertyValidatorTest { assertThat(constraintViolations, hasSize(0)); } + + @Test + public void preferredAuthenticationsIsValidated() throws Exception { + SshUriProperties sshUriProperties = new SshUriProperties(); + assertThat(validator.validate(sshUriProperties), hasSize(0)); + + sshUriProperties.setPreferredAuthentications("keyboard-interactive, public-key ,kerberos"); + assertThat(validator.validate(sshUriProperties), hasSize(0)); + + sshUriProperties.setPreferredAuthentications(",,"); + assertThat(validator.validate(sshUriProperties), hasSize(1)); + } + + @Test + public void knowHostsFileIsValidated() throws Exception { + SshUriProperties sshUriProperties = new SshUriProperties(); + assertThat(validator.validate(sshUriProperties), hasSize(0)); + + sshUriProperties.setKnownHostsFile("non-existing.file"); + assertThat(validator.validate(sshUriProperties), hasSize(1)); + } } \ No newline at end of file