diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildAccount.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildAccount.java index 5f8bc43ea..5de4f7c76 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildAccount.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildAccount.java @@ -16,26 +16,36 @@ package com.netflix.spinnaker.igor.codebuild; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; import com.amazonaws.services.codebuild.AWSCodeBuildClient; import com.amazonaws.services.codebuild.AWSCodeBuildClientBuilder; import com.amazonaws.services.codebuild.model.BatchGetBuildsRequest; import com.amazonaws.services.codebuild.model.Build; import com.amazonaws.services.codebuild.model.StartBuildRequest; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient; +import java.util.Objects; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; /** Generates authenticated requests to AWS CodeBuild API for a single configured account */ @RequiredArgsConstructor public class AwsCodeBuildAccount { private final AWSCodeBuildClient client; - public AwsCodeBuildAccount(String accessKeyId, String secretAccessKey, String region) { - BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKeyId, secretAccessKey); + @Autowired AWSSecurityTokenServiceClient stsClient; + + public AwsCodeBuildAccount(String accountId, String assumeRole, String region) { + STSAssumeRoleSessionCredentialsProvider credentialsProvider = + new STSAssumeRoleSessionCredentialsProvider.Builder( + getRoleArn(accountId, assumeRole), "spinnaker-session") + .withStsClient(stsClient) + .build(); + + // TODO: Add client-side rate limiting to avoid getting throttled if necessary this.client = (AWSCodeBuildClient) AWSCodeBuildClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) + .withCredentials(credentialsProvider) .withRequestHandlers(new AwsCodeBuildRequestHandler()) .withRegion(region) .build(); @@ -48,4 +58,19 @@ public Build startBuild(StartBuildRequest request) { public Build getBuild(String buildId) { return client.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds().get(0); } + + private String getRoleArn(String accountId, String assumeRole) { + String assumeRoleValue = Objects.requireNonNull(assumeRole, "assumeRole"); + if (!assumeRoleValue.startsWith("arn:")) { + /** + * GovCloud and China regions need to have the full arn passed because of differing formats + * Govcloud: arn:aws-us-gov:iam China: arn:aws-cn:iam + */ + assumeRoleValue = + String.format( + "arn:aws:iam::%s:%s", + Objects.requireNonNull(accountId, "accountId"), assumeRoleValue); + } + return assumeRoleValue; + } } diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandler.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandler.java index d68e6fd8e..a71700649 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandler.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandler.java @@ -17,18 +17,37 @@ package com.netflix.spinnaker.igor.codebuild; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.Request; import com.amazonaws.Response; import com.amazonaws.handlers.RequestHandler2; -import com.amazonaws.services.codebuild.model.AWSCodeBuildException; import com.netflix.spinnaker.igor.exceptions.BuildJobError; +import com.netflix.spinnaker.security.AuthenticatedRequest; import lombok.extern.slf4j.Slf4j; @Slf4j public class AwsCodeBuildRequestHandler extends RequestHandler2 { + @Override + public AmazonWebServiceRequest beforeMarshalling(AmazonWebServiceRequest request) { + final String userAgent = + String.format( + "spinnaker-user/%s spinnaker-executionId/%s", + AuthenticatedRequest.getSpinnakerUser().orElse("unknown"), + AuthenticatedRequest.getSpinnakerExecutionId().orElse("unknown")); + + final AmazonWebServiceRequest cloned = request.clone(); + + cloned.getRequestClientOptions().appendUserAgent(userAgent); + return super.beforeMarshalling(cloned); + } + @Override public void afterError(Request request, Response response, Exception e) { - if (e instanceof AWSCodeBuildException) { + if (e instanceof AmazonServiceException + && ((AmazonServiceException) e) + .getErrorType() + .equals(AmazonServiceException.ErrorType.Client)) { log.warn(e.getMessage()); throw new BuildJobError(e.getMessage()); } else { diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildConfig.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildConfig.java index 34ea747b0..e83a11207 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildConfig.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildConfig.java @@ -16,6 +16,9 @@ package com.netflix.spinnaker.igor.config; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; import com.netflix.spinnaker.igor.codebuild.AwsCodeBuildAccount; import com.netflix.spinnaker.igor.codebuild.AwsCodeBuildAccountRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -36,10 +39,17 @@ AwsCodeBuildAccountRepository awsCodeBuildAccountRepository( .forEach( a -> { AwsCodeBuildAccount account = - new AwsCodeBuildAccount( - a.getAccessKeyId(), a.getSecretAccessKey(), a.getRegion()); + new AwsCodeBuildAccount(a.getAccountId(), a.getAssumeRole(), a.getRegion()); accounts.addAccount(a.getName(), account); }); return accounts; } + + @Bean + AWSSecurityTokenServiceClient awsSecurityTokenServiceClient() { + return (AWSSecurityTokenServiceClient) + AWSSecurityTokenServiceClientBuilder.standard() + .withCredentials(DefaultAWSCredentialsProviderChain.getInstance()) + .build(); + } } diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildProperties.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildProperties.java index d26b576e8..108c0aab7 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildProperties.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/AwsCodeBuildProperties.java @@ -29,7 +29,7 @@ public class AwsCodeBuildProperties { public static class Account { private String name; private String region; - private String accessKeyId; - private String secretAccessKey; + private String accountId; + private String assumeRole; } } diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandlerSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandlerSpec.groovy index efe3be4e7..b4d34708b 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandlerSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/codebuild/AwsCodeBuildRequestHandlerSpec.groovy @@ -16,6 +16,7 @@ package com.netflix.spinnaker.igor.codebuild +import com.amazonaws.AmazonServiceException import com.amazonaws.DefaultRequest import com.amazonaws.Response import com.amazonaws.services.codebuild.model.InvalidInputException @@ -29,9 +30,10 @@ class AwsCodeBuildRequestHandlerSpec extends Specification { def request = new DefaultRequest(new StartBuildRequest(), "codebuild") def response = new Response(new StartBuildResult(), null) - def "should throw BuildJobError in case of AWSCodeBuildException"() { + def "should throw BuildJobError in case of a client exception"() { when: def exception = new InvalidInputException("err msg") + exception.setErrorType(AmazonServiceException.ErrorType.Client) handler.afterError(request, response, exception) then: BuildJobError err = thrown()