Add AWS CodeCommit authentication support (#548)
The jgit library does not support the credential helpers defined in git configuration files. Since AWS CodeCommit requires a signature used as the password part of authentication, jgit is not able to authenticate to AWS Code Commit without help. These changes calculate the required AWS Code Commit signature value and provide a jgit compatible CredentialsProvider class. Fixes gh-334
This commit is contained in:
committed by
Spencer Gibb
parent
2efe1a4134
commit
ae0adc9322
@@ -307,6 +307,8 @@ start successfully with a misconfigured or invalid configuration source and
|
||||
not detect an error until an application requests configuration from that
|
||||
configuration source.
|
||||
|
||||
===== Authentication
|
||||
|
||||
To use HTTP basic authentication on the remote repository add the
|
||||
"username" and "password" properties separately (not in the URL),
|
||||
e.g.
|
||||
@@ -337,6 +339,28 @@ TIP: If you don't know where your `~/.git` directory is us `git config
|
||||
--global` to manipulate the settings (e.g. `git config --global
|
||||
http.sslVerify false`).
|
||||
|
||||
===== Authentication with AWS CodeCommit
|
||||
|
||||
http://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html[AWS CodeCommit] authentication can also be
|
||||
done. AWS CodeCommit uses an authentication helper when using Git from the command line. This helper is not
|
||||
used with the JGit library, so a JGit CredentialProvider for AWS CodeCommit will be created if the Git
|
||||
URI matches the AWS CodeCommit pattern. AWS CodeCommit URIs always look like
|
||||
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
|
||||
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
|
||||
valid AWS credentials in the username and password, or in one of the locations supported
|
||||
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.
|
||||
|
||||
===== Placeholders in Git Search Paths
|
||||
|
||||
Spring Cloud Config Server also supports a search path with
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
<artifactId>svnkit</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-core</artifactId>
|
||||
<version>1.11.52</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.eclipse.jgit.api.TransportCommand;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.api.errors.RefNotFoundException;
|
||||
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;
|
||||
@@ -89,6 +90,11 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository
|
||||
private JGitEnvironmentRepository.JGitFactory gitFactory = new JGitEnvironmentRepository.JGitFactory();
|
||||
|
||||
private String defaultLabel = DEFAULT_LABEL;
|
||||
|
||||
/**
|
||||
* The credentials provider to use to connect to the Git repository.
|
||||
*/
|
||||
private CredentialsProvider gitCredentialsProvider;
|
||||
|
||||
/**
|
||||
* Flag to indicate that the repository should force pull. If true discard any local
|
||||
@@ -494,4 +500,18 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the gitCredentialsProvider
|
||||
*/
|
||||
public CredentialsProvider getGitCredentialsProvider() {
|
||||
return gitCredentialsProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param gitCredentialsProvider the gitCredentialsProvider to set
|
||||
*/
|
||||
public void setGitCredentialsProvider(CredentialsProvider gitCredentialsProvider) {
|
||||
this.gitCredentialsProvider = gitCredentialsProvider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.Map;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.config.environment.Environment;
|
||||
import org.springframework.cloud.config.server.credentials.GitCredentialsProviderFactory;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -61,6 +62,8 @@ public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
GitCredentialsProviderFactory credentialFactory = new GitCredentialsProviderFactory();
|
||||
super.setGitCredentialsProvider(credentialFactory.createFor(getUri(), getUsername(), getPassword()));
|
||||
super.afterPropertiesSet();
|
||||
for (String name : this.repos.keySet()) {
|
||||
PatternMatchingJGitEnvironmentRepository repo = this.repos.get(name);
|
||||
@@ -74,6 +77,13 @@ public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository
|
||||
if (getTimeout() != 0 && repo.getTimeout() == 0) {
|
||||
repo.setTimeout(getTimeout());
|
||||
}
|
||||
String user = repo.getUsername();
|
||||
String pass = repo.getPassword();
|
||||
if (user == null) {
|
||||
user = getUsername();
|
||||
pass = getPassword();
|
||||
}
|
||||
repo.setGitCredentialsProvider(credentialFactory.createFor(repo.getUri(), user, pass));
|
||||
repo.afterPropertiesSet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
/*
|
||||
* Copyright 2013-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.credentials;
|
||||
|
||||
import static org.springframework.util.StringUtils.hasText;
|
||||
|
||||
import java.net.URI;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
|
||||
import org.eclipse.jgit.transport.CredentialItem;
|
||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.auth.AWSSessionCredentials;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
|
||||
import com.amazonaws.util.ValidationUtils;
|
||||
|
||||
/**
|
||||
* Provides a jgit {@link CredentialsProvider} implementation that can provide
|
||||
* the appropriate credentials to connect to an AWS CodeCommit repository.
|
||||
* <p>
|
||||
* From the command line, you can configure git to use AWS code commit with a
|
||||
* credential helper. However, jgit does not support credential helper commands,
|
||||
* but it does provider a CredentialsProvider abstract class we can extend.
|
||||
* </p>
|
||||
* Connecting to an AWS CodeCommit (codecommit) repository requires an AWS access key
|
||||
* and secret key. These are used to calculate a signature for the git request. The
|
||||
* AWS access key is used as the codecommit username, and the calculated signature
|
||||
* is used as the password. The process for calculating this signature is documented
|
||||
* very well at http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html.
|
||||
* </p>
|
||||
*
|
||||
* @author Don Laidlaw
|
||||
*
|
||||
*/
|
||||
public class AwsCodeCommitCredentialProvider extends CredentialsProvider {
|
||||
|
||||
private static final String SHA_256 = "SHA-256"; //$NON-NLS-1$
|
||||
private static final String UTF8 = "UTF8"; //$NON-NLS-1$
|
||||
private static final String HMAC_SHA256 = "HmacSHA256"; //$NON-NLS-1$
|
||||
private static final char[] hexArray = "0123456789abcdef".toCharArray(); //$NON-NLS-1$
|
||||
|
||||
protected Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* The AWSCredentialsProvider will be used to provide the access key and
|
||||
* secret key if they are not specified.
|
||||
*/
|
||||
private AWSCredentialsProvider awsCredentialProvider;
|
||||
|
||||
/**
|
||||
* If the access and secret keys are provided, then the
|
||||
* AWSCredentialsProvider will not be used. The username is the
|
||||
* awsAccessKeyId.
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* If the access and secret keys are provided, then the AWSCredentialsProvider will
|
||||
* not be used. The password is the awsSecretKey.
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* This credentials provider cannot run interactively.
|
||||
* @return false
|
||||
* @see org.eclipse.jgit.transport.CredentialsProvider#isInteractive()
|
||||
*/
|
||||
@Override
|
||||
public boolean isInteractive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* We support username and password credential items only.
|
||||
* @see org.eclipse.jgit.transport.CredentialsProvider#supports(org.eclipse.jgit.transport.CredentialItem[])
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(CredentialItem... items) {
|
||||
for (CredentialItem i : items) {
|
||||
if (i instanceof CredentialItem.Username) {
|
||||
continue;
|
||||
}
|
||||
else if (i instanceof CredentialItem.Password) {
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AWSCredentials. If an AWSCredentialProvider was specified, use that, otherwise,
|
||||
* create a new AWSCredentialsProvider. If the username and password are provided, then
|
||||
* use those directly as AWSCredentials. Otherwise us the {@link DefaultAWSCredentialsProviderChain}
|
||||
* as is standard with AWS applications.
|
||||
* @return the AWS credentials.
|
||||
*/
|
||||
private AWSCredentials retrieveAwsCredentials() {
|
||||
if (awsCredentialProvider == null) {
|
||||
if (username != null && password != null) {
|
||||
logger.debug("Creating a static AWSCredentialsProvider");
|
||||
awsCredentialProvider = new AWSStaticCredentialsProvider(new BasicAWSCredentials(username, password));
|
||||
} else {
|
||||
logger.debug("Creating a default AWSCredentialsProvider");
|
||||
awsCredentialProvider = new DefaultAWSCredentialsProviderChain();
|
||||
}
|
||||
}
|
||||
return awsCredentialProvider.getCredentials();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the username and password to use for the given uri.
|
||||
* @see org.eclipse.jgit.transport.CredentialsProvider#get(org.eclipse.jgit.transport.URIish, org.eclipse.jgit.transport.CredentialItem[])
|
||||
*/
|
||||
@Override
|
||||
public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
|
||||
String codeCommitPassword;
|
||||
String awsAccessKey;
|
||||
String awsSecretKey;
|
||||
try {
|
||||
AWSCredentials awsCredentials = retrieveAwsCredentials();
|
||||
StringBuilder awsKey = new StringBuilder();
|
||||
awsKey.append(awsCredentials.getAWSAccessKeyId());
|
||||
awsSecretKey = awsCredentials.getAWSSecretKey();
|
||||
if (awsCredentials instanceof AWSSessionCredentials) {
|
||||
AWSSessionCredentials sessionCreds = (AWSSessionCredentials) awsCredentials;
|
||||
if (sessionCreds.getSessionToken() != null) {
|
||||
awsKey.append('%')
|
||||
.append(sessionCreds.getSessionToken());
|
||||
}
|
||||
}
|
||||
awsAccessKey = awsKey.toString();
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Unable to retrieve AWS Credentials", t);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
codeCommitPassword = calculateCodeCommitPassword(uri, awsSecretKey);
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Error calculating the AWS CodeCommit password", t);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (CredentialItem i : items) {
|
||||
if (i instanceof CredentialItem.Username) {
|
||||
((CredentialItem.Username) i).setValue(awsAccessKey);
|
||||
logger.trace("Returning username " + awsAccessKey);
|
||||
continue;
|
||||
}
|
||||
if (i instanceof CredentialItem.Password) {
|
||||
((CredentialItem.Password) i).setValue(codeCommitPassword.toCharArray());
|
||||
logger.trace("Returning password " + codeCommitPassword);
|
||||
continue;
|
||||
}
|
||||
if (i instanceof CredentialItem.StringType && i.getPromptText().equals("Password: ")) { //$NON-NLS-1$
|
||||
((CredentialItem.StringType) i).setValue(codeCommitPassword);
|
||||
logger.trace("Returning password string " + codeCommitPassword);
|
||||
continue;
|
||||
}
|
||||
throw new UnsupportedCredentialItem(uri, i.getClass().getName() + ":" + i.getPromptText()); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the AWS CodeCommit password for the provided URI and AWS secret key.
|
||||
* This uses the algorithm published by AWS at
|
||||
* http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
||||
* @param uri the codecommit repository uri
|
||||
* @param awsSecretKey the aws secret key
|
||||
* @return the password to use in the git request
|
||||
*/
|
||||
protected static String calculateCodeCommitPassword(URIish uri, String awsSecretKey) {
|
||||
String[] split = uri.getHost().split("\\.");
|
||||
if (split.length < 4) {
|
||||
throw new CredentialException("Cannot detect AWS region from URI", null);
|
||||
}
|
||||
String region = split[1];
|
||||
|
||||
Date now = new Date();
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
||||
String dateStamp = dateFormat.format(now);
|
||||
String shortDateStamp = dateStamp.substring(0, 8);
|
||||
|
||||
String codeCommitPassword;
|
||||
try {
|
||||
StringBuilder stringToSign = new StringBuilder();
|
||||
stringToSign.append("AWS4-HMAC-SHA256\n")
|
||||
.append(dateStamp).append("\n")
|
||||
.append(shortDateStamp)
|
||||
.append("/").append(region)
|
||||
.append("/codecommit/aws4_request\n")
|
||||
.append(bytesToHexString(canonicalRequestDigest(uri)));
|
||||
|
||||
byte[] signedRequest = sign(awsSecretKey, shortDateStamp, region, stringToSign.toString());
|
||||
codeCommitPassword = dateStamp + "Z" + bytesToHexString(signedRequest);
|
||||
} catch (Exception e) {
|
||||
throw new CredentialException("Error calculating AWS CodeCommit password", e);
|
||||
}
|
||||
|
||||
return codeCommitPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw out cached data and force retrieval of AWS credentials.
|
||||
* @param uri This parameter is not used in this implementation.
|
||||
*/
|
||||
@Override
|
||||
public void reset(URIish uri) {
|
||||
// Should throw out cached info.
|
||||
// Note that even though the credentials (password) we calculate here is
|
||||
// valid for 15 minutes, we do not cache it. Instead we just re-calculate
|
||||
// it each time we need it. However, the AWSCredentialProvider will cache
|
||||
// its AWSCredentials object.
|
||||
}
|
||||
|
||||
private static byte[] hmacSha256(String data, byte[] key) throws Exception {
|
||||
String algorithm = HMAC_SHA256;
|
||||
Mac mac = Mac.getInstance(algorithm);
|
||||
mac.init(new SecretKeySpec(key, algorithm));
|
||||
return mac.doFinal(data.getBytes(UTF8));
|
||||
}
|
||||
|
||||
private static byte[] sign(String secret, String shortDateStamp, String region, String toSign) throws Exception {
|
||||
byte[] kSecret = ("AWS4" + secret).getBytes(UTF8);
|
||||
byte[] kDate = hmacSha256(shortDateStamp, kSecret);
|
||||
byte[] kRegion = hmacSha256(region, kDate);
|
||||
byte[] kService = hmacSha256("codecommit", kRegion);
|
||||
byte[] kSigning = hmacSha256("aws4_request", kService);
|
||||
return hmacSha256(toSign, kSigning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message digest
|
||||
* @param uri
|
||||
* @return
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
private static byte[] canonicalRequestDigest(URIish uri) throws NoSuchAlgorithmException {
|
||||
StringBuilder canonicalRequest = new StringBuilder();
|
||||
canonicalRequest.append("GIT\n") // codecommit uses GIT as the request method
|
||||
.append(uri.getPath()).append("\n") // URI request path
|
||||
.append("\n") // Query string, always empty for codecommit
|
||||
// Next is canonical headers, codecommit only requires the host header
|
||||
.append("host:").append(uri.getHost()).append("\n")
|
||||
.append("\n") // canonical headers are always terminated by newline
|
||||
.append("host\n"); // The list of canonical headers, only one for codecommit
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance(SHA_256);
|
||||
|
||||
return digest.digest(canonicalRequest.toString().getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to a hex string
|
||||
* @param bytes the bytes
|
||||
* @return a string of hex characters encoding the bytes.
|
||||
*/
|
||||
private static String bytesToHexString(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the awsCredentialProvider
|
||||
*/
|
||||
public AWSCredentialsProvider getAwsCredentialProvider() {
|
||||
return awsCredentialProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param awsCredentialProvider the awsCredentialProvider to set
|
||||
*/
|
||||
public void setAwsCredentialProvider(AWSCredentialsProvider awsCredentialProvider) {
|
||||
this.awsCredentialProvider = awsCredentialProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* This provider can handle uris like https://git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/$REPO
|
||||
* @see org.springframework.cloud.config.server.credentials.GitCredentialsProvider#canHandleUri(java.lang.String)
|
||||
*/
|
||||
public static boolean canHandle(String uri) {
|
||||
if (!hasText(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
URI u = new URI(uri.toLowerCase());
|
||||
if (u.getScheme().equals("https")) {
|
||||
String host = u.getHost();
|
||||
if (host.endsWith(".amazonaws.com") && host.startsWith("git-codecommit.")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// ignore all, we can't handle it
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param username the username to set
|
||||
*/
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the password
|
||||
*/
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param password the password to set
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple implementation of AWSCredentialsProvider that just wraps static AWSCredentials.
|
||||
* AWS Actually provides this class in newer versions of the AWS API.
|
||||
*/
|
||||
public class AWSStaticCredentialsProvider implements AWSCredentialsProvider {
|
||||
|
||||
private final AWSCredentials credentials;
|
||||
|
||||
public AWSStaticCredentialsProvider(AWSCredentials credentials) {
|
||||
this.credentials = ValidationUtils.assertNotNull(credentials, "credentials");
|
||||
}
|
||||
|
||||
public AWSCredentials getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
// Nothing to do for static credentials.
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2013-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.credentials;
|
||||
|
||||
/**
|
||||
* @author Don Laidlaw
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class CredentialException extends RuntimeException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public CredentialException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2013-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.credentials;
|
||||
|
||||
import static org.springframework.util.StringUtils.hasText;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* A CredentialsProvider factory for Git repositories. Can handle AWS CodeCommit
|
||||
* repositories and other repositories with username/password.
|
||||
*
|
||||
* @author Don Laidlaw
|
||||
*
|
||||
*/
|
||||
public class GitCredentialsProviderFactory {
|
||||
protected Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* Enable the AWS Code Commit credentials provider for Git URI's
|
||||
* that match the AWS Code Commit pattern of
|
||||
* https://git-codecommit.${AWS_REGION}.amazonaws.com/${repoPath}.
|
||||
* Enabled by default.
|
||||
*/
|
||||
protected boolean awsCodeCommitEnabled = true;
|
||||
|
||||
/**
|
||||
* Search for a credential provider that will handle the specified URI. If
|
||||
* not found, and the username has text, then create a default using the
|
||||
* provided username and password. Otherwise null.
|
||||
* @param uri the URI of the repository (cannot be null)
|
||||
* @param username the username provided for the repository (may be null)
|
||||
* @param password the password provided for the repository (may be null)
|
||||
* @return the first matched credentials provider or the default or null.
|
||||
*/
|
||||
public CredentialsProvider createFor(String uri, String username, String password) {
|
||||
CredentialsProvider provider = null;
|
||||
if (awsAvailable() && AwsCodeCommitCredentialProvider.canHandle(uri)) {
|
||||
logger.debug("Constructing AwsCodeCommitCredentialProvider for URI " + uri);
|
||||
AwsCodeCommitCredentialProvider aws = new AwsCodeCommitCredentialProvider();
|
||||
aws.setUsername(username);
|
||||
aws.setPassword(password);
|
||||
provider = aws;
|
||||
}
|
||||
else if (hasText(username)) {
|
||||
logger.debug("Constructing UsernamePasswordCredentialsProvider for URI " + uri);
|
||||
provider = new UsernamePasswordCredentialsProvider(username, password.toCharArray());
|
||||
}
|
||||
else {
|
||||
logger.debug("No credentials provider required for URI " + uri);
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the AWS Authentication API is available.
|
||||
* @return true if the com.amazonaws.auth.DefaultAWSCredentialsProviderChain is present,
|
||||
* false otherwise.
|
||||
*/
|
||||
private boolean awsAvailable() {
|
||||
return awsCodeCommitEnabled
|
||||
&& ClassUtils.isPresent("com.amazonaws.auth.DefaultAWSCredentialsProviderChain", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the awsCodeCommitEnabled
|
||||
*/
|
||||
public boolean isAwsCodeCommitEnabled() {
|
||||
return awsCodeCommitEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param awsCodeCommitEnabled the awsCodeCommitEnabled to set
|
||||
*/
|
||||
public void setAwsCodeCommitEnabled(boolean awsCodeCommitEnabled) {
|
||||
this.awsCodeCommitEnabled = awsCodeCommitEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2013-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.credentials;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
|
||||
import org.eclipse.jgit.transport.CredentialItem;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
|
||||
/**
|
||||
* It would be nice to do an integration test, however, this would require
|
||||
* using real AWS credentials. How can we test the credential generation
|
||||
* without real credentials?
|
||||
*
|
||||
* @author don laidlaw
|
||||
*
|
||||
*/
|
||||
public class AwsCodeCommitCredentialsProviderTests {
|
||||
private static final String PASSWORD = "secret";
|
||||
private static final String USER = "test";
|
||||
private static final String AWS_REPO = "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test";
|
||||
private static final String BAD_REPO = "https://amazonaws.com/v1/repos/test";
|
||||
|
||||
private AwsCodeCommitCredentialProvider provider;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
GitCredentialsProviderFactory factory = new GitCredentialsProviderFactory();
|
||||
provider = (AwsCodeCommitCredentialProvider)
|
||||
factory.createFor(AWS_REPO, USER, PASSWORD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basics() {
|
||||
assertNotNull(provider);
|
||||
assertEquals(USER, provider.getUsername());
|
||||
assertEquals(PASSWORD, provider.getPassword());
|
||||
assertFalse(provider.isInteractive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSupportsUsernamePassword() {
|
||||
assertTrue(provider.supports(new CredentialItem[] {
|
||||
new CredentialItem.Username(),
|
||||
new CredentialItem.Password()
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotSupportsOther() {
|
||||
assertFalse(provider.supports(new CredentialItem[] {
|
||||
new CredentialItem.YesNoType("OK To Login?") // this is not ok
|
||||
}));
|
||||
assertFalse(provider.supports(new CredentialItem[] {
|
||||
new CredentialItem.StringType("OK To Login?", true) // this is not ok
|
||||
}));
|
||||
assertFalse(provider.supports(new CredentialItem[] {
|
||||
new CredentialItem.Username(), // this is ok
|
||||
new CredentialItem.Password(), // this is ok
|
||||
new CredentialItem.StringType("OK To Login?", true) // this is not ok
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAwsCredentialsProviderIsNullInitially() {
|
||||
AWSCredentialsProvider awsProvider = provider.getAwsCredentialProvider();
|
||||
assertNull(awsProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAwsCredentialsProviderIsDefinedAfterGet() throws URISyntaxException {
|
||||
AWSCredentialsProvider awsProvider = provider.getAwsCredentialProvider();
|
||||
assertNull(awsProvider);
|
||||
assertTrue(provider.get(new URIish(AWS_REPO), makeCredentialItems()));
|
||||
awsProvider = provider.getAwsCredentialProvider();
|
||||
assertNotNull(awsProvider);
|
||||
assertTrue(awsProvider instanceof AwsCodeCommitCredentialProvider.AWSStaticCredentialsProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadUriReturnsFalse() throws UnsupportedCredentialItem, URISyntaxException {
|
||||
CredentialItem[] credentialItems = makeCredentialItems();
|
||||
assertFalse(provider.get(new URIish(BAD_REPO), credentialItems));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThrowsUnsupportedCredentialException() throws URISyntaxException {
|
||||
CredentialItem[] goodCredentialItems = makeCredentialItems();
|
||||
CredentialItem[] badCredentialItems = new CredentialItem[] {
|
||||
goodCredentialItems[0],
|
||||
goodCredentialItems[1],
|
||||
new CredentialItem.YesNoType("OK?")
|
||||
};
|
||||
try {
|
||||
provider.get(new URIish(AWS_REPO), badCredentialItems);
|
||||
fail("Expected UnsupportedCredentialItem exception");
|
||||
} catch (UnsupportedCredentialItem e) {
|
||||
assertNotNull(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsCredentials() throws URISyntaxException {
|
||||
CredentialItem[] credentialItems = makeCredentialItems();
|
||||
assertTrue(provider.get(new URIish(AWS_REPO), credentialItems));
|
||||
|
||||
String theUsername = ((CredentialItem.Username) credentialItems[0]).getValue();
|
||||
char[] thePassword = ((CredentialItem.Password) credentialItems[1]).getValue();
|
||||
|
||||
assertEquals(USER, theUsername);
|
||||
assertNotNull(thePassword);
|
||||
|
||||
// The password will always begin with a timestamp like
|
||||
// 20161113T121314Z
|
||||
assertTrue(thePassword.length > 16);
|
||||
assertEquals('T', thePassword[8]);
|
||||
assertEquals('Z', thePassword[15]);
|
||||
}
|
||||
|
||||
private CredentialItem[] makeCredentialItems() {
|
||||
CredentialItem[] credentialItems = new CredentialItem[2];
|
||||
credentialItems[0] = new CredentialItem.Username();
|
||||
credentialItems[1] = new CredentialItem.Password();
|
||||
return credentialItems;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2013-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.credentials;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author don laidlaw
|
||||
*
|
||||
*/
|
||||
public class GitCredentialsProviderFactoryTests {
|
||||
private static final String PASSWORD = "secret";
|
||||
private static final String USER = "test";
|
||||
private static final String FILE_REPO = "file:///home/user/repo";
|
||||
private static final String GIT_REPO = "https://github.com/spring-cloud/spring-cloud-config-test";
|
||||
private static final String AWS_REPO = "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test";
|
||||
|
||||
private GitCredentialsProviderFactory factory;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
factory = new GitCredentialsProviderFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateForFileNoUsernameIsNull() {
|
||||
CredentialsProvider provider = factory.createFor(FILE_REPO, null, null);
|
||||
assertNull(provider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateForFileWithUsername() {
|
||||
CredentialsProvider provider = factory.createFor(FILE_REPO, USER, PASSWORD);
|
||||
assertNotNull(provider);
|
||||
assertTrue(provider instanceof UsernamePasswordCredentialsProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateForServerNoUsernameIsNull() {
|
||||
CredentialsProvider provider = factory.createFor(GIT_REPO, null, null);
|
||||
assertNull(provider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateForServerWithUsername() {
|
||||
CredentialsProvider provider = factory.createFor(GIT_REPO, USER, PASSWORD);
|
||||
assertNotNull(provider);
|
||||
assertTrue(provider instanceof UsernamePasswordCredentialsProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateForAwsNoUsername() {
|
||||
CredentialsProvider provider = factory.createFor(AWS_REPO, null, null);
|
||||
assertNotNull(provider);
|
||||
assertTrue(provider instanceof AwsCodeCommitCredentialProvider);
|
||||
AwsCodeCommitCredentialProvider aws = (AwsCodeCommitCredentialProvider) provider;
|
||||
assertNull(aws.getUsername());
|
||||
assertNull(aws.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateForAwsWithUsername() {
|
||||
CredentialsProvider provider = factory.createFor(AWS_REPO, USER, PASSWORD);
|
||||
assertNotNull(provider);
|
||||
assertTrue(provider instanceof AwsCodeCommitCredentialProvider);
|
||||
AwsCodeCommitCredentialProvider aws = (AwsCodeCommitCredentialProvider) provider;
|
||||
assertEquals(USER, aws.getUsername());
|
||||
assertEquals(PASSWORD, aws.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateForAwsDisabled() {
|
||||
factory.setAwsCodeCommitEnabled(false);
|
||||
CredentialsProvider provider = factory.createFor(AWS_REPO, null, null);
|
||||
assertNull(provider);
|
||||
provider = factory.createFor(AWS_REPO, USER, PASSWORD);
|
||||
assertNotNull(provider);
|
||||
assertTrue(provider instanceof UsernamePasswordCredentialsProvider);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testIsAwsCodeCommitEnabled() {
|
||||
assertTrue(factory.isAwsCodeCommitEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetAwsCodeCommitEnabled() {
|
||||
assertTrue(factory.isAwsCodeCommitEnabled());
|
||||
factory.setAwsCodeCommitEnabled(false);
|
||||
assertFalse(factory.isAwsCodeCommitEnabled());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user