Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for assuming IAM roles #12

Merged
merged 1 commit into from
Jan 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ work
.project
build
.fbExcludeFilterFile

# files osx plops down sometimes
.DS_Store
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,51 @@
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient;
import com.amazonaws.services.securitytoken.model.AssumeRoleRequest;
import com.amazonaws.services.securitytoken.model.AssumeRoleResult;
import com.cloudbees.plugins.credentials.CredentialsDescriptor;
import com.cloudbees.plugins.credentials.CredentialsScope;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.ProxyConfiguration;
import hudson.Util;
import hudson.util.FormValidation;
import hudson.util.Secret;
import jenkins.model.Jenkins;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

import java.net.HttpURLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;

public class AWSCredentialsImpl extends BaseAmazonWebServicesCredentials implements AmazonWebServicesCredentials {

private static final Logger LOGGER = Logger.getLogger(BaseAmazonWebServicesCredentials.class.getName());

public static final int STS_CREDENTIALS_DURATION_SECONDS = 3600;
private final String accessKey;

private final Secret secretKey;

private final String iamRoleArn;
private final String iamMfaSerialNumber;

@DataBoundConstructor
public AWSCredentialsImpl(@CheckForNull CredentialsScope scope, @CheckForNull String id,
@CheckForNull String accessKey, @CheckForNull String secretKey, @CheckForNull String description) {
@CheckForNull String accessKey, @CheckForNull String secretKey, @CheckForNull String description,
@CheckForNull String iamRoleArn, @CheckForNull String iamMfaSerialNumber) {
super(scope, id, description);
this.accessKey = Util.fixNull(accessKey);
this.secretKey = Secret.fromString(secretKey);
this.iamRoleArn = Util.fixNull(iamRoleArn);
this.iamMfaSerialNumber = Util.fixNull(iamMfaSerialNumber);
}

public String getAccessKey() {
Expand All @@ -74,16 +85,66 @@ public Secret getSecretKey() {
return secretKey;
}

public String getIamRoleArn() {
return iamRoleArn;
}

public String getIamMfaSerialNumber() {
return iamMfaSerialNumber;
}

public boolean requiresToken() {
return !StringUtils.isBlank(iamMfaSerialNumber);
}

public AWSCredentials getCredentials() {
return new BasicAWSCredentials(accessKey, secretKey.getPlainText());
AWSCredentials initialCredentials = new BasicAWSCredentials(accessKey, secretKey.getPlainText());

if (StringUtils.isBlank(iamRoleArn)) {
return initialCredentials;
} else {
AssumeRoleRequest assumeRequest = createAssumeRoleRequest(iamRoleArn);

AssumeRoleResult assumeResult = new AWSSecurityTokenServiceClient(initialCredentials).assumeRole(assumeRequest);

return new BasicSessionCredentials(
assumeResult.getCredentials().getAccessKeyId(),
assumeResult.getCredentials().getSecretAccessKey(),
assumeResult.getCredentials().getSessionToken());
}
}

public AWSCredentials getCredentials(String mfaToken) {
AWSCredentials initialCredentials = new BasicAWSCredentials(accessKey, secretKey.getPlainText());

AssumeRoleRequest assumeRequest = createAssumeRoleRequest(iamRoleArn)
.withSerialNumber(iamMfaSerialNumber)
.withTokenCode(mfaToken);

AssumeRoleResult assumeResult = new AWSSecurityTokenServiceClient(initialCredentials).assumeRole(assumeRequest);

return new BasicSessionCredentials(
assumeResult.getCredentials().getAccessKeyId(),
assumeResult.getCredentials().getSecretAccessKey(),
assumeResult.getCredentials().getSessionToken());
}

public void refresh() {
// no-op
}

public String getDisplayName() {
return accessKey;
if (StringUtils.isBlank(iamRoleArn)) {
return accessKey;
}
return accessKey + ":" + iamRoleArn;
}

private static AssumeRoleRequest createAssumeRoleRequest(@QueryParameter("iamRoleArn") String iamRoleArn) {
return new AssumeRoleRequest()
.withRoleArn(iamRoleArn)
.withDurationSeconds(STS_CREDENTIALS_DURATION_SECONDS)
.withRoleSessionName(Jenkins.getActiveInstance().getDisplayName());
}

@Extension
Expand All @@ -94,31 +155,62 @@ public String getDisplayName() {
return Messages.AWSCredentialsImpl_DisplayName();
}

public FormValidation doCheckSecretKey(@QueryParameter("accessKey") final String accessKey, @QueryParameter
final String value) {
if (StringUtils.isBlank(accessKey) && StringUtils.isBlank(value)) {
public FormValidation doCheckSecretKey(@QueryParameter("accessKey") final String accessKey,
@QueryParameter("iamRoleArn") final String iamRoleArn,
@QueryParameter("iamMfaSerialNumber") final String iamMfaSerialNumber,
@QueryParameter("iamMfaToken") final String iamMfaToken,
@QueryParameter final String secretKey) {
if (StringUtils.isBlank(accessKey) && StringUtils.isBlank(secretKey)) {
return FormValidation.ok();
}
if (StringUtils.isBlank(accessKey)) {
return FormValidation.error(Messages.AWSCredentialsImpl_SpecifyAccessKeyId());
}
if (StringUtils.isBlank(value)) {
if (StringUtils.isBlank(secretKey)) {
return FormValidation.error(Messages.AWSCredentialsImpl_SpecifySecretAccessKey());
}

ProxyConfiguration proxy = Jenkins.getActiveInstance().proxy;
ClientConfiguration clientConfiguration = new ClientConfiguration();
ClientConfiguration clientConfiguration = new ClientConfiguration();
if(proxy != null) {
clientConfiguration.setProxyHost(proxy.name);
clientConfiguration.setProxyPort(proxy.port);
clientConfiguration.setProxyUsername(proxy.getUserName());
clientConfiguration.setProxyPassword(proxy.getPassword());
}

AmazonEC2 ec2 = new AmazonEC2Client(
new BasicAWSCredentials(accessKey, Secret.fromString(value).getPlainText()),
clientConfiguration);


AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, Secret.fromString(secretKey).getPlainText());

// If iamRoleArn is specified, swap out the credentials.
if (!StringUtils.isBlank(iamRoleArn)) {

AssumeRoleRequest assumeRequest = createAssumeRoleRequest(iamRoleArn);

if(!StringUtils.isBlank(iamMfaSerialNumber)) {
if(StringUtils.isBlank(iamMfaToken)) {
return FormValidation.error(Messages.AWSCredentialsImpl_SpecifyMFAToken());
}
assumeRequest = assumeRequest
.withSerialNumber(iamMfaSerialNumber)
.withTokenCode(iamMfaToken);
}

try {
AssumeRoleResult assumeResult = new AWSSecurityTokenServiceClient(awsCredentials).assumeRole(assumeRequest);

awsCredentials = new BasicSessionCredentials(
assumeResult.getCredentials().getAccessKeyId(),
assumeResult.getCredentials().getSecretAccessKey(),
assumeResult.getCredentials().getSessionToken());
} catch(AmazonServiceException e) {
LOGGER.log(Level.WARNING, "Unable to assume role [" + iamRoleArn + "] with request [" + assumeRequest + "]", e);
return FormValidation.error(Messages.AWSCredentialsImpl_NotAbleToAssumeRole());
}

}

AmazonEC2 ec2 = new AmazonEC2Client(awsCredentials,clientConfiguration);

// TODO better/smarter validation of the credentials instead of verifying the permission on EC2.READ in us-east-1
String region = "us-east-1";
try {
Expand All @@ -140,5 +232,4 @@ public FormValidation doCheckSecretKey(@QueryParameter("accessKey") final String
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,24 @@
-->

<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:st="jelly:stapler">
<f:entry title="${%Access Key ID}">
<f:textbox field="accessKey"/>
<st:include page="id-and-description" class="${descriptor.clazz}"/>
<f:entry title="${%Access Key ID}" field="accessKey">
<f:textbox/>
</f:entry>
<f:entry title="${%Secret Access Key}">
<f:password field="secretKey"/>
<f:entry title="${%Secret Access Key}" field="secretKey">
<f:password/>
</f:entry>
<st:include page="id-and-description" class="${descriptor.clazz}"/>
<f:section title="IAM Role Support">
<f:advanced>
<f:entry title="${%IAM Role To Use}" field="iamRoleArn">
<f:textbox/>
</f:entry>
<f:entry title="${%MFA Serial Number}" field="iamMfaSerialNumber">
<f:textbox/>
</f:entry>
<f:entry title="${%MFA Token}" field="iamMfaToken">
<f:textbox/>
</f:entry>
</f:advanced>
</f:section>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<p>The identifier for an MFA device. Either a serial number for hardware MFA devices, or an ARN for virtual devices.</p>
<p>This is only required if the trust policy of the role being assumed includes a condition that requires MFA authentication..</p>
<p>Specify a serial number such as "GAHT12345678" for hardware MFA devices.</p>
<p>Specify an ARN such as "arn:aws:iam::123456789012:mfa/user" for virtual MFA devices.</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
This is a one-time token from the MFA device to validate that it is configured correctly. This is not persisted in any way.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
An ARN specifying the IAM role to assume. The format should be something like: "arn:aws:iam::123456789012:role/MyIAMRoleName".
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
AWSCredentialsImpl_DisplayName=AWS Credentials
AWSCredentialsImpl.SpecifyAccessKeyId=Please specify the Access Key ID
AWSCredentialsImpl.SpecifySecretAccessKey=Please specify the Secret Access Key
AWSCredentialsImpl.NotAbleToAssumeRole=There was an error assuming the specified IAM role, a MFA may be required by your organization
AWSCredentialsImpl.SpecifyMFAToken=If the MFA Serial Number/ARN is specified, then a one time token is necessary to validate these credentials
AWSCredentialsImpl.CredentialsValidWithAccessToNZones=These credentials are valid and have access to {0} availability zones
AWSCredentialsImpl.CredentialsValidWithoutAccessToAwsServiceInZone=These credentials are valid but do not have access to the "{0}" service in the region "{1}". This message is not a problem if you need to access to other services or to other regions. Message: "{2}"
AWSCredentialsImpl.CredentialsInValid=These credentials are NOT valid: "{0}"