Skip to content

Commit

Permalink
Add support for assuming IAM roles
Browse files Browse the repository at this point in the history
  • Loading branch information
mattstep committed Jan 4, 2017
1 parent 99f9ca6 commit d12ab1d
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 24 deletions.
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}"

0 comments on commit d12ab1d

Please sign in to comment.