Use JGit URIish to parse SSH URIs

Use JSR-303 cross field validation for `SshUriProperties`
Rebase with 1.3.x Dalston
This commit is contained in:
Ollie Hughes
2017-07-12 17:41:04 +01:00
parent acee8a1b10
commit 67cdfab7fa
19 changed files with 572 additions and 250 deletions

View File

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

View File

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

View File

@@ -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<? extends Payload>[] payload() default {};
}

View File

@@ -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<HostKeyAlgoSupported, SshUriProperties> {
private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git.";
private final SshPropertyValidator sshPropertyValidator = new SshPropertyValidator();
private static final Set<String> 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<Boolean> validationResults = new HashSet<>();
List<SshUriProperties> 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;
}
}

View File

@@ -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<? extends Payload>[] payload() default {};
}

View File

@@ -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<HostKeyAndAlgoBothExist, SshUriProperties> {
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<Boolean> validationResults = new HashSet<>();
List<SshUriProperties> 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;
}
}

View File

@@ -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<? extends Payload>[] payload() default {};
}

View File

@@ -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<PrivateKeyIsValid, SshUriProperties> {
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<Boolean> validationResults = new HashSet<>();
List<SshUriProperties> 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;
}
}
}

View File

@@ -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<String, SshUriProperties> 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);
}
}

View File

@@ -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<String> 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<SshUriProperties> extractRepoProperties(SshUriProperties sshUriProperties) {
List<SshUriProperties> allRepoProperties = new ArrayList<>();
allRepoProperties.add(sshUriProperties);
Map<String, SshUriProperties> 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));
}
}
}

View File

@@ -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<String, SshUriProperties> repos = new HashMap<>();
public SshUriProperties(String uri, String hostKeyAlgorithm, String hostKey, String privateKey, String username, String password, boolean ignoreLocalSshSettings, boolean strictHostKeyChecking, Map<String, SshUriProperties> repos) {
public SshUriProperties(String uri, String hostKeyAlgorithm, String hostKey, String privateKey, boolean ignoreLocalSshSettings, boolean strictHostKeyChecking, Map<String, SshUriProperties> 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<String, SshUriProperties> 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 + ")";
}
}
}

View File

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

View File

@@ -18,4 +18,3 @@ server:
port: 8888
management:
context_path: /admin

View File

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

View File

@@ -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<HostKey> 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<String, SshUriProperties> 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);
}

View File

@@ -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<ConstraintViolation<SshUriProperties>> 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<ConstraintViolation<SshUriProperties>> 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<ConstraintViolation<SshUriProperties>> 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<ConstraintViolation<SshUriProperties>> 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<ConstraintViolation<SshUriProperties>> 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<ConstraintViolation<SshUriProperties>> 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<ConstraintViolation<SshUriProperties>> 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<ConstraintViolation<SshUriProperties>> constraintViolations = validator.validate(httpsUri);
assertThat(constraintViolations, hasSize(0));
}
}

View File

@@ -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<String, SshUriProperties> 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<String, SshUriProperties> sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname();
@@ -106,12 +116,26 @@ public class SshUriPropertyProcessorTest {
}
@Test
public void testNoSshUriPropertiess() {
public void testNoSshUriProperties() {
SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(new SshUriProperties());
Map<String, SshUriProperties> sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname();
assertThat(sshKeysByHostname.values(), hasSize(0));
}
@Test
public void testInvalidUriDoesNotAddEntry() {
SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(SshUriProperties.builder().uri("invalid_uri").build());
Map<String, SshUriProperties> 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<String, SshUriProperties> 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<String, SshUriProperties> repos = new HashMap<>();
repos.put("repo2", repoProperties);
mainRepoProperties.setRepos(repos);
private void addRepoProperties(SshUriProperties mainRepoProperties, SshUriProperties repoProperties, String repoName) {
if (mainRepoProperties.getRepos() == null) {
Map<String, SshUriProperties> 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)));