From b683ac6eabee4c3d6c27579544a87c62bf1aba73 Mon Sep 17 00:00:00 2001 From: Rado Smogura Date: Mon, 17 Dec 2018 05:39:52 -0800 Subject: [PATCH] =?UTF-8?q?feat(aws-codepipeline):=20support=20for=20pipel?= =?UTF-8?q?ine=20action=E2=80=99s=20service=20role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In realation to https://github.com/awslabs/aws-cdk/issues/49 The action’s service roles is a role which will be assumed by pipeline during execution of this action. The pipeline action’s service role can be used to perform more advanced configuration, when i.e. elevation of permissions is required, or when fine grained access control may be required. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html This commit is motivated by enabling cross-account deployments, for which service role’s will be used as jump role to assume one used by Cloud Formation in target account. --- .../lib/pipeline-actions.ts | 1 + .../aws-codepipeline-api/lib/action.ts | 20 ++ .../@aws-cdk/aws-codepipeline/lib/stage.ts | 1 + ...ipeline-cfn-wtih-action-role.expected.json | 251 ++++++++++++++++++ .../integ.pipeline-cfn-wtih-action-role.ts | 36 +++ .../test.cloudformation-pipeline-actions.ts | 47 ++++ 6 files changed, 356 insertions(+) create mode 100644 packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.expected.json create mode 100644 packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.ts diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index e8e1ee3fcbfe7..8eb4d05e90073 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -61,6 +61,7 @@ export abstract class PipelineCloudFormationAction extends codepipeline.Action { super(parent, id, { stage: props.stage, runOrder: props.runOrder, + actionRole: props.actionRole, region: props.region, artifactBounds: { minInputs: 0, diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts index 0f6ef1754787d..f38c79b1b4d4a 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -151,6 +151,15 @@ export interface CommonActionConstructProps { * The Pipeline Stage to add this Action to. */ stage: IStage; + + /** + * The service role that is assumed during execution of action. + * This role is not mandatory, however more advanced configuration + * may require specifying it. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html + */ + actionRole?: iam.IRole; } /** @@ -210,6 +219,15 @@ export abstract class Action extends cdk.Construct { */ public readonly configuration?: any; + /** + * The service role that is assumed during execution of action. + * This role is not mandatory, however more advanced configuration + * may require specifying it. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html + */ + public readonly actionRole?: iam.IRole; + /** * The order in which AWS CodePipeline runs this action. * For more information, see the AWS CodePipeline User Guide. @@ -223,6 +241,7 @@ export abstract class Action extends cdk.Construct { private readonly _actionInputArtifacts = new Array(); private readonly _actionOutputArtifacts = new Array(); + private readonly artifactBounds: ActionArtifactBounds; private readonly stage: IStage; @@ -240,6 +259,7 @@ export abstract class Action extends cdk.Construct { this.artifactBounds = props.artifactBounds; this.runOrder = props.runOrder === undefined ? 1 : props.runOrder; this.stage = props.stage; + this.actionRole = props.actionRole; this.stage._internal._attachAction(this); } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index bd1faa6398c80..8eb3455cda4d5 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -162,6 +162,7 @@ export class Stage extends cdk.Construct implements cpapi.IStage, cpapi.IInterna configuration: action.configuration, outputArtifacts: action._outputArtifacts.map(a => ({ name: a.name })), runOrder: action.runOrder, + roleArn: action.actionRole ? action.actionRole.roleArn : undefined }; } diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.expected.json new file mode 100644 index 0000000000000..9d182230514d2 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.expected.json @@ -0,0 +1,251 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + } + }, + "MyPipelineRoleC0D47CA4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyPipelineRoleDefaultPolicy34F09EFA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CFNDeployRole68D5E8D3", + "Arn" + ] + } + }, + { + "Action": [ + "cloudformation:CreateStack", + "cloudformation:DescribeStack*", + "cloudformation:GetStackPolicy", + "cloudformation:GetTemplate*", + "cloudformation:SetStackPolicy", + "cloudformation:UpdateStack", + "cloudformation:ValidateTemplate" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":stack/aws-cdk-codepipeline-cross-region-deploy-stack/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyPipelineRoleDefaultPolicy34F09EFA", + "Roles": [ + { + "Ref": "MyPipelineRoleC0D47CA4" + } + ] + } + }, + "MyPipelineAED38ECF": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "MyPipelineRoleC0D47CA4", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "MyBucketF68F3FF0" + }, + "S3ObjectKey": "some/path", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "S3", + "OutputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecloudformationcrossregionwithactionroleMyBucketS30423514B" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "aws-cdk-codepipeline-cross-region-deploy-stack", + "ActionMode": "CREATE_UPDATE", + "TemplatePath": "Artifact_awscdkcodepipelinecloudformationcrossregionwithactionroleMyBucketS30423514B::template.yml", + "RoleArn": { + "Fn::GetAtt": [ + "CFNDeployRole68D5E8D3", + "Arn" + ] + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecloudformationcrossregionwithactionroleMyBucketS30423514B" + } + ], + "Name": "CFN_Deploy", + "OutputArtifacts": [], + "RoleArn": "arn:aws:iam::000000000000:role/action-role", + "RunOrder": 1 + } + ], + "Name": "CFN" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "MyBucketF68F3FF0" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "MyPipelineRoleC0D47CA4", + "MyPipelineRoleDefaultPolicy34F09EFA" + ] + }, + "CFNDeployRole68D5E8D3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.ts new file mode 100644 index 0000000000000..dea21bccc9b33 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-wtih-action-role.ts @@ -0,0 +1,36 @@ +import cloudformation = require('@aws-cdk/aws-cloudformation'); +import iam = require('@aws-cdk/aws-iam'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import codepipeline = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-cloudformation-cross-region-with-action-role', {}); + +const bucket = new s3.Bucket(stack, 'MyBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.Destroy, +}); + +const pipeline = new codepipeline.Pipeline(stack, 'MyPipeline', { + artifactBucket: bucket, +}); + +const sourceStage = pipeline.addStage('Source'); +const sourceAction = bucket.addToPipeline(sourceStage, 'S3', { + bucketKey: 'some/path', +}); + +const cfnStage = pipeline.addStage('CFN'); +new cloudformation.PipelineCreateUpdateStackAction(stack, 'CFN_Deploy', { + stage: cfnStage, + stackName: 'aws-cdk-codepipeline-cross-region-deploy-stack', + templatePath: sourceAction.outputArtifact.atPath('template.yml'), + adminPermissions: false, + actionRole: iam.Role.import(stack, 'DeployCFNActionRole', { + roleArn: 'arn:aws:iam::000000000000:role/action-role' + }) +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts index a33581084b12a..989d4b4f80632 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.cloudformation-pipeline-actions.ts @@ -356,6 +356,53 @@ export = { })); test.done(); + }, + + 'Action service role is passed to template'(test: Test) { + const stack = new TestFixture(); + + const importedRole = Role.import(stack, 'ImportedRole', { + roleArn: 'arn:aws:iam::000000000000:role/action-role' + }); + const freshRole = new Role(stack, 'FreshRole', { + assumedBy: new ServicePrincipal('magicservice') + }); + + new PipelineExecuteChangeSetAction(stack.pipeline, 'ImportedRoleAction', { + actionRole: importedRole, + changeSetName: 'magicSet', + stackName: 'magicStack', + stage: stack.deployStage + }); + + new PipelineExecuteChangeSetAction(stack.pipeline, 'FreshRoleAction', { + actionRole: freshRole, + changeSetName: 'magicSet', + stackName: 'magicStack', + stage: stack.deployStage + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [{ + "Name": "Source" /* don't care about the rest */ + }, { + "Name": "Deploy", + "Actions": [{ + "Name": "ImportedRoleAction", + "RoleArn": "arn:aws:iam::000000000000:role/action-role" + }, { + "Name": "FreshRoleAction", + "RoleArn": { + "Fn::GetAtt": [ + "FreshRole472F6E18", + "Arn" + ] + } + }] + }] + })); + + test.done(); } };