From 51401d3173d9c762dfe8adea7b9d866998f7e82b Mon Sep 17 00:00:00 2001 From: Mike Cowgill Date: Mon, 12 Nov 2018 22:18:29 -0800 Subject: [PATCH] feat(app-delivery) IAM policy for deploy stack * The changeset and apply changeset can now apply role IAM permissions, and CloudFormation Capabilities * Updated CloudFormationCapabilities enum to include `None` * User must set adminPermissions boolean for pipeline action * app-delivery defaults pipelin-action capabilities to AnonymousIAM * Document updates for proper build stage configuration * Fixes #1151 BREAKING CHANGE: `CloudFormationCapabilities.IAM` renamed to `CloudFormation.AnonymousIAM` and `PipelineCloudFormationDeployActionProps.capabilities?: CloudFormationCapabilities[]` has been changed to `PipelineCloudFormationDeployActionProps.capabilities?: CloudFormationCapabilities` no longer an array. `PipelineCloudFormationDeployActionProps.fullPermissions?:` has been renamed to `PipelineCloudFormationDeployActionProps.adminPermissions:` and is required instead of optional. --- packages/@aws-cdk/app-delivery/README.md | 36 ++- .../lib/pipeline-deploy-stack-action.ts | 82 +++++- packages/@aws-cdk/app-delivery/package.json | 6 +- .../@aws-cdk/app-delivery/test/integ.cicd.ts | 6 +- .../test/test.pipeline-deploy-stack-action.ts | 233 +++++++++++++++++- .../lib/pipeline-actions.ts | 43 ++-- .../test/test.pipeline-actions.ts | 7 +- ...g.cfn-template-from-repo.lit.expected.json | 16 +- .../test/integ.cfn-template-from-repo.lit.ts | 2 +- .../test/integ.lambda-pipeline.expected.json | 14 +- .../test/integ.pipeline-cfn-cross-region.ts | 1 + .../test/integ.pipeline-cfn.expected.json | 16 +- .../test/integ.pipeline-cfn.ts | 1 + ...uild-multiple-inputs-outputs.expected.json | 14 +- ...g.pipeline-code-commit-build.expected.json | 14 +- .../integ.pipeline-code-commit.expected.json | 14 +- .../integ.pipeline-code-deploy.expected.json | 14 +- .../test/integ.pipeline-events.expected.json | 30 +-- .../test.cloudformation-pipeline-actions.ts | 8 +- .../aws-codepipeline/test/test.pipeline.ts | 2 + 20 files changed, 461 insertions(+), 98 deletions(-) diff --git a/packages/@aws-cdk/app-delivery/README.md b/packages/@aws-cdk/app-delivery/README.md index f6790ea3e4302..94d5e29aaa546 100644 --- a/packages/@aws-cdk/app-delivery/README.md +++ b/packages/@aws-cdk/app-delivery/README.md @@ -52,9 +52,17 @@ const source = new codepipeline.GitHubSourceAction(pipelineStack, 'GitHub', { /* ... */ }); const project = new codebuild.PipelineProject(pipelineStack, 'CodeBuild', { - /* ... */ + /** + * Choose an environment configuration that meets your use case. For NodeJS + * this might be + * environment: { + * buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_NODEJS_10_1_0, + * }, + */ }); -const synthesizedApp = project.outputArtifact; +const buildStage = pipeline.addStage('build'); +const buildAction = project.addBuildToPipeline(buildStage, 'CodeBuild'); +const synthesizedApp = buildAction.outputArtifact; // Optionally, self-update the pipeline stack const selfUpdateStage = pipeline.addStage('SelfUpdate'); @@ -69,26 +77,44 @@ const deployStage = pipeline.addStage('Deploy'); const serviceStackA = new MyServiceStackA(app, 'ServiceStackA', { /* ... */ }); const serviceStackB = new MyServiceStackB(app, 'ServiceStackB', { /* ... */ }); // Add actions to deploy the stacks in the deploy stage: -new cicd.PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', { +const deployServiceAAction = new cicd.PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', { stage: deployStage, stack: serviceStackA, inputArtifact: synthesizedApp, + // See the note below for details about this option. + adminPermissions: false, }); -new cicd.PipelineDeployStackAction(pipelineStack, 'DeployServiceStackB', { + +// Add the necessary permissions for you service deploy action. This role is +// is passed to CloudFormation and needs the permissions necessary to deploy +// stack. Alternatively you can enable [Administrator](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_job-functions.html#jf_administrator) permissions above, +// users should understand the privileged nature of this role. +deployServiceAAction.addToRolePolicy( + // new iam.PolicyStatement(). + // ... addAction('actions that you need'). + // add resource +); +const deployServiceBAction = new cicd.PipelineDeployStackAction(pipelineStack, 'DeployServiceStackB', { stage: deployStage, stack: serviceStackB, inputArtifact: synthesizedApp, createChangeSetRunOrder: 998, -}); + adminPermissions: true, // no need to modify the role with admin +}); ``` #### `buildspec.yml` +The repository can contain a file at the root level named `buildspec.yml`, or +you can in-line the buildspec. Note that `buildspec.yaml` is not compatible. + The `PipelineDeployStackAction` expects it's `inputArtifact` to contain the result of synthesizing a CDK App using the `cdk synth -o ` command. For example, a *TypeScript* or *Javascript* CDK App can add the following `buildspec.yml` at the root of the repository configured in the `Source` stage: +Example contents of `buildspec.yml`. + ```yml version: 0.2 phases: diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index 8105d354771a8..f13834de16804 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -1,6 +1,6 @@ - import cfn = require('@aws-cdk/aws-cloudformation'); import codepipeline = require('@aws-cdk/aws-codepipeline-api'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); @@ -41,6 +41,47 @@ export interface PipelineDeployStackActionProps { * @default ``createChangeSetRunOrder + 1`` */ executeChangeSetRunOrder?: number; + + /** + * IAM role to assume when deploying changes. + * + * If not specified, a fresh role is created. The role is created with zero + * permissions unless `adminPermissions` is true, in which case the role will have + * admin permissions. + * + * @default A fresh role with admin or no permissions (depending on the value of `adminPermissions`). + */ + role?: iam.Role; + + /** + * Acknowledge certain changes made as part of deployment + * + * For stacks that contain certain resources, explicit acknowledgement that AWS CloudFormation + * might create or update those resources. For example, you must specify AnonymousIAM if your + * stack template contains AWS Identity and Access Management (IAM) resources. For more + * information + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities + * @default AnonymousIAM, unless `adminPermissions` is true + */ + capabilities?: cfn.CloudFormationCapabilities; + + /** + * Whether to grant admin permissions to CloudFormation while deploying this template. + * + * Setting this to `true` affects the defaults for `role` and `capabilities`, if you + * don't specify any alternatives. + * + * The default role that will be created for you will have admin (i.e., `*`) + * permissions on all resources, and the deployment will have named IAM + * capabilities (i.e., able to create all IAM resources). + * + * This is a shorthand that you can use if you fully trust the templates that + * are deployed in this pipeline. If you want more fine-grained permissions, + * use `addToRolePolicy` and `capabilities` to control what the CloudFormation + * deployment is allowed to do. + */ + adminPermissions: boolean; } /** @@ -52,6 +93,12 @@ export interface PipelineDeployStackActionProps { * CodePipeline is hosted. */ export class PipelineDeployStackAction extends cdk.Construct { + + /** + * The role used by CloudFormation for the deploy action + */ + public readonly role: iam.Role; + private readonly stack: cdk.Stack; constructor(parent: cdk.Construct, id: string, props: PipelineDeployStackActionProps) { @@ -72,13 +119,18 @@ export class PipelineDeployStackAction extends cdk.Construct { this.stack = props.stack; const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet'; - new cfn.PipelineCreateReplaceChangeSetAction(this, 'ChangeSet', { + const capabilities = cfnCapabilities(props.adminPermissions, props.capabilities); + const changeSetAction = new cfn.PipelineCreateReplaceChangeSetAction(this, 'ChangeSet', { changeSetName, runOrder: createChangeSetRunOrder, stackName: props.stack.name, stage: props.stage, templatePath: props.inputArtifact.atPath(`${props.stack.name}.template.yaml`), + adminPermissions: props.adminPermissions, + role: props.role, + capabilities, }); + this.role = changeSetAction.role; new cfn.PipelineExecuteChangeSetAction(this, 'Execute', { changeSetName, @@ -97,4 +149,30 @@ export class PipelineDeployStackAction extends cdk.Construct { } return result; } + + /** + * Add policy statements to the role deploying the stack. + * + * This role is passed to CloudFormation and must have the IAM permissions + * necessary to deploy the stack or you can grant this role `adminPermissions` + * by using that option during creation. If you do not grant + * `adminPermissions` you need to identify the proper statements to add to + * this role based on the CloudFormation Resources in your stack. + */ + public addToRolePolicy(statement: iam.PolicyStatement) { + this.role.addToPolicy(statement); + } +} + +function cfnCapabilities(adminPermissions: boolean, capabilities?: cfn.CloudFormationCapabilities): cfn.CloudFormationCapabilities { + if (adminPermissions && capabilities === undefined) { + // admin true default capability to NamedIAM + return cfn.CloudFormationCapabilities.NamedIAM; + } else if (capabilities === undefined) { + // else capabilities are undefined set AnonymousIAM + return cfn.CloudFormationCapabilities.AnonymousIAM; + } else { + // else capabilities are defined use them + return capabilities; + } } diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index b536a57472dca..eaf1846bc0ea0 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -35,10 +35,12 @@ "@aws-cdk/aws-cloudformation": "^0.18.0", "@aws-cdk/aws-codebuild": "^0.18.0", "@aws-cdk/aws-codepipeline-api": "^0.18.0", + "@aws-cdk/aws-iam": "^0.18.0", "@aws-cdk/cdk": "^0.18.0", "@aws-cdk/cx-api": "^0.18.0" }, "devDependencies": { + "@aws-cdk/assert": "^0.18.0", "@aws-cdk/aws-codepipeline": "^0.18.0", "@aws-cdk/aws-s3": "^0.18.0", "cdk-build-tools": "^0.18.0", @@ -62,7 +64,9 @@ "cdk" ], "peerDependencies": { + "@aws-cdk/aws-cloudformation": "^0.18.0", "@aws-cdk/aws-codepipeline-api": "^0.18.0", + "@aws-cdk/aws-iam": "^0.18.0", "@aws-cdk/cdk": "^0.18.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts index 3f7f81636f052..cb884da4973d9 100644 --- a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts +++ b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts @@ -1,3 +1,4 @@ +import cfn = require('@aws-cdk/aws-cloudformation'); import code = require('@aws-cdk/aws-codepipeline'); import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); @@ -16,13 +17,16 @@ const source = new code.GitHubSourceAction(stack, 'GitHub', { oauthToken: new cdk.Secret('DummyToken'), pollForSourceChanges: true, }); +const stage = pipeline.addStage('Deploy'); new cicd.PipelineDeployStackAction(stack, 'DeployStack', { - stage: pipeline.addStage('Deploy'), + stage, stack, changeSetName: 'CICD-ChangeSet', createChangeSetRunOrder: 10, executeChangeSetRunOrder: 999, inputArtifact: source.outputArtifact, + adminPermissions: false, + capabilities: cfn.CloudFormationCapabilities.None, }); app.run(); diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index 5357783f50052..fa8bf00924d48 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -1,11 +1,21 @@ +import cfn = require('@aws-cdk/aws-cloudformation'); +import codebuild = require('@aws-cdk/aws-codebuild'); import code = require('@aws-cdk/aws-codepipeline'); import api = require('@aws-cdk/aws-codepipeline-api'); +import iam = require('@aws-cdk/aws-iam'); +import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); import fc = require('fast-check'); import nodeunit = require('nodeunit'); + +import { countResources, expect, haveResource, isSuperObject } from '@aws-cdk/assert'; import { PipelineDeployStackAction } from '../lib/pipeline-deploy-stack-action'; +interface SelfUpdatingPipeline { + synthesizedApp: api.Artifact; + pipeline: code.Pipeline; +} const accountId = fc.array(fc.integer(0, 9), 12, 12).map(arr => arr.join()); export = nodeunit.testCase({ @@ -25,6 +35,7 @@ export = nodeunit.testCase({ inputArtifact: fakeAction.outputArtifact, stack: new cdk.Stack(app, 'DeployedStack', { env: { account: stackAccount } }), stage: pipeline.addStage('DeployStage'), + adminPermissions: false, }); }, 'Cross-environment deployment is not supported'); } @@ -51,6 +62,7 @@ export = nodeunit.testCase({ inputArtifact: fakeAction.outputArtifact, stack: new cdk.Stack(app, 'DeployedStack'), stage: pipeline.addStage('DeployStage'), + adminPermissions: false, }); }, 'createChangeSetRunOrder must be < executeChangeSetRunOrder'); } @@ -58,7 +70,188 @@ export = nodeunit.testCase({ ); test.done(); }, + 'users can supply CloudFormation capabilities'(test: nodeunit.Test) { + const pipelineStack = getTestStack(); + const stackWithNoCapability = new cdk.Stack(undefined, 'NoCapStack', + { env: { account: '123456789012', region: 'us-east-1' } }); + + const stackWithAnonymousCapability = new cdk.Stack(undefined, 'AnonymousIAM', + { env: { account: '123456789012', region: 'us-east-1' } }); + + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); + + const pipeline = selfUpdatingStack.pipeline; + const selfUpdateStage = pipeline.addStage('SelfUpdate'); + new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { + stage: selfUpdateStage, + stack: pipelineStack, + inputArtifact: selfUpdatingStack.synthesizedApp, + capabilities: cfn.CloudFormationCapabilities.NamedIAM, + adminPermissions: false, + }); + new PipelineDeployStackAction(pipelineStack, 'DeployStack', { + stage: selfUpdateStage, + stack: stackWithNoCapability, + inputArtifact: selfUpdatingStack.synthesizedApp, + capabilities: cfn.CloudFormationCapabilities.None, + adminPermissions: false, + }); + new PipelineDeployStackAction(pipelineStack, 'DeployStack2', { + stage: selfUpdateStage, + stack: stackWithAnonymousCapability, + inputArtifact: selfUpdatingStack.synthesizedApp, + capabilities: cfn.CloudFormationCapabilities.AnonymousIAM, + adminPermissions: false, + }); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "TestStack", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_NAMED_IAM", + } + }))); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "AnonymousIAM", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_IAM", + } + }))); + expect(pipelineStack).notTo(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "NoCapStack", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_NAMED_IAM", + } + }))); + expect(pipelineStack).notTo(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "NoCapStack", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_IAM", + } + }))); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "NoCapStack", + ActionMode: "CHANGE_SET_REPLACE", + } + }))); + test.done(); + }, + 'users can use admin permissions'(test: nodeunit.Test) { + const pipelineStack = getTestStack(); + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); + const pipeline = selfUpdatingStack.pipeline; + const selfUpdateStage = pipeline.addStage('SelfUpdate'); + new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { + stage: selfUpdateStage, + stack: pipelineStack, + inputArtifact: selfUpdatingStack.synthesizedApp, + adminPermissions: true, + }); + expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: '*', + Effect: 'Allow', + Resource: '*', + } + ], + } + })); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "TestStack", + ActionMode: "CHANGE_SET_REPLACE", + Capabilities: "CAPABILITY_NAMED_IAM", + } + }))); + test.done(); + }, + 'users can supply a role for deploy action'(test: nodeunit.Test) { + const pipelineStack = getTestStack(); + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); + + const pipeline = selfUpdatingStack.pipeline; + const role = new iam.Role(pipelineStack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com'), + }); + const selfUpdateStage = pipeline.addStage('SelfUpdate'); + const deployAction = new PipelineDeployStackAction(pipelineStack, 'SelfUpdatePipeline', { + stage: selfUpdateStage, + stack: pipelineStack, + inputArtifact: selfUpdatingStack.synthesizedApp, + adminPermissions: false, + role + }); + test.deepEqual(role.id, deployAction.role.id); + test.done(); + }, + 'users can specify IAM permissions for the deploy action'(test: nodeunit.Test) { + // GIVEN // + const pipelineStack = getTestStack(); + + // the fake stack to deploy + const emptyStack = getTestStack(); + + const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); + const pipeline = selfUpdatingStack.pipeline; + + // WHEN // + // this our app/service/infra to deploy + const deployStage = pipeline.addStage('Deploy'); + const deployAction = new PipelineDeployStackAction(pipelineStack, 'DeployServiceStackA', { + stage: deployStage, + stack: emptyStack, + inputArtifact: selfUpdatingStack.synthesizedApp, + adminPermissions: false, + }); + // we might need to add permissions + deployAction.addToRolePolicy( new iam.PolicyStatement(). + addActions( + 'ec2:AuthorizeSecurityGroupEgress', + 'ec2:AuthorizeSecurityGroupIngress', + 'ec2:DeleteSecurityGroup', + 'ec2:DescribeSecurityGroups', + 'ec2:CreateSecurityGroup', + 'ec2:RevokeSecurityGroupEgress', + 'ec2:RevokeSecurityGroupIngress' + ). + addAllResources()); + + // THEN // + // there should be 3 policies 1. CodePipeline, 2. Codebuild, 3. + // ChangeSetDeploy Action + expect(pipelineStack).to(countResources('AWS::IAM::Policy', 3)); + expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ec2:AuthorizeSecurityGroupEgress', + 'ec2:AuthorizeSecurityGroupIngress', + 'ec2:DeleteSecurityGroup', + 'ec2:DescribeSecurityGroups', + 'ec2:CreateSecurityGroup', + 'ec2:RevokeSecurityGroupEgress', + 'ec2:RevokeSecurityGroupIngress' + ], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + Roles: [ + { + Ref: 'DeployServiceStackAChangeSetRoleA1245536', + }, + ], + })); + test.done(); + }, 'rejects stacks with assets'(test: nodeunit.Test) { fc.assert( fc.property( @@ -74,12 +267,13 @@ export = nodeunit.testCase({ inputArtifact: fakeAction.outputArtifact, stack: deployedStack, stage: pipeline.addStage('DeployStage'), + adminPermissions: false, }); for (let i = 0 ; i < assetCount ; i++) { deployedStack.addMetadata(cxapi.ASSET_METADATA, {}); } test.deepEqual(action.validate(), - [`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]); + [`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]); } ) ); @@ -101,3 +295,40 @@ class FakeAction extends api.Action { this.outputArtifact = new api.Artifact(this, 'OutputArtifact'); } } + +function getTestStack(): cdk.Stack { + return new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); +} + +function createSelfUpdatingStack(pipelineStack: cdk.Stack): SelfUpdatingPipeline { + const pipeline = new code.Pipeline(pipelineStack, 'CodePipeline', { + restartExecutionOnUpdate: true, + }); + + // simple source + const bucket = s3.Bucket.import( pipeline, 'PatternBucket', { bucketArn: 'arn:aws:s3:::totally-fake-bucket' }); + new s3.PipelineSourceAction(pipeline, 'S3Source', { + bucket, + bucketKey: 'the-great-key', + stage: pipeline.addStage('source'), + }); + + const project = new codebuild.PipelineProject(pipelineStack, 'CodeBuild'); + const buildStage = pipeline.addStage('build'); + const buildAction = project.addBuildToPipeline(buildStage, 'CodeBuild'); + const synthesizedApp = buildAction.outputArtifact; + return {synthesizedApp, pipeline}; +} + +function hasPipelineAction(expectedAction: any): (props: any) => boolean { + return (props: any) => { + for (const stage of props.Stages) { + for (const action of stage.Actions) { + if (isSuperObject(action, expectedAction)) { + return true; + } + } + } + return false; + }; +} diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index 169cf8045eb6e..6478fb822cdf9 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -118,10 +118,10 @@ export interface PipelineCloudFormationDeployActionProps extends PipelineCloudFo * IAM role to assume when deploying changes. * * If not specified, a fresh role is created. The role is created with zero - * permissions unless `fullPermissions` is true, in which case the role will have + * permissions unless `adminPermissions` is true, in which case the role will have * full permissions. * - * @default A fresh role with full or no permissions (depending on the value of `fullPermissions`). + * @default A fresh role with full or no permissions (depending on the value of `adminPermissions`). */ role?: iam.Role; @@ -129,13 +129,13 @@ export interface PipelineCloudFormationDeployActionProps extends PipelineCloudFo * Acknowledge certain changes made as part of deployment * * For stacks that contain certain resources, explicit acknowledgement that AWS CloudFormation - * might create or update those resources. For example, you must specify CAPABILITY_IAM if your - * stack template contains AWS Identity and Access Management (IAM) resources. For more - * information, see [Acknowledging IAM Resources in AWS CloudFormation Templates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities). - * - * @default No capabitilities passed, unless `fullPermissions` is true + * might create or update those resources. For example, you must specify `AnonymousIAM` or `NamedIAM` + * if your stack template contains AWS Identity and Access Management (IAM) resources. For more + * information + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities + * @default None, unless `adminPermissions` is true */ - capabilities?: CloudFormationCapabilities[]; + capabilities?: CloudFormationCapabilities; /** * Whether to grant full permissions to CloudFormation while deploying this template. @@ -151,10 +151,8 @@ export interface PipelineCloudFormationDeployActionProps extends PipelineCloudFo * are deployed in this pipeline. If you want more fine-grained permissions, * use `addToRolePolicy` and `capabilities` to control what the CloudFormation * deployment is allowed to do. - * - * @default false */ - fullPermissions?: boolean; + adminPermissions: boolean; /** * Input artifact to use for template parameters values and stack policy. @@ -198,12 +196,11 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo public readonly role: iam.Role; constructor(parent: cdk.Construct, id: string, props: PipelineCloudFormationDeployActionProps, configuration: any) { - const capabilities = props.fullPermissions && props.capabilities === undefined ? [CloudFormationCapabilities.NamedIAM] : props.capabilities; - + const capabilities = props.adminPermissions && props.capabilities === undefined ? CloudFormationCapabilities.NamedIAM : props.capabilities; super(parent, id, props, { ...configuration, - // This must be a string, so flatten the list to a comma-separated string. - Capabilities: (capabilities && capabilities.join(',')) || undefined, + // None evaluates to empty string which is falsey and results in undefined + Capabilities: capabilities && capabilities.toString() || undefined, RoleArn: new cdk.Token(() => this.role.roleArn), ParameterOverrides: cdk.CloudFormationJSON.stringify(props.parameterOverrides), TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined, @@ -217,7 +214,7 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com') }); - if (props.fullPermissions) { + if (props.adminPermissions) { this.role.addToPolicy(new iam.PolicyStatement().addAction('*').addAllResources()); } } @@ -356,8 +353,9 @@ export enum CloudFormationCapabilities { * Capability to create anonymous IAM resources * * Pass this capability if you're only creating anonymous resources. + * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities */ - IAM = 'CAPABILITY_IAM', + AnonymousIAM = 'CAPABILITY_IAM', /** * Capability to create named IAM resources. @@ -366,8 +364,17 @@ export enum CloudFormationCapabilities { * names. * * `CloudFormationCapabilities.NamedIAM` implies `CloudFormationCapabilities.IAM`; you don't have to pass both. + * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities + */ + NamedIAM = 'CAPABILITY_NAMED_IAM', + + /** + * No IAM Capabilities + * + * Pass this capability if you wish to block the creation IAM resources. + * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities */ - NamedIAM = 'CAPABILITY_NAMED_IAM' + None = '', } /** diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 63677172d192a..37da55382672f 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -17,7 +17,8 @@ export = nodeunit.testCase({ stage, changeSetName: 'MyChangeSet', stackName: 'MyStack', - templatePath: artifact.atPath('path/to/file') + templatePath: artifact.atPath('path/to/file'), + adminPermissions: false, }); _assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn); @@ -50,6 +51,7 @@ export = nodeunit.testCase({ stage, changeSetName: 'MyChangeSet', stackName: 'StackA', + adminPermissions: false, templatePath: artifact.atPath('path/to/file') }); @@ -57,6 +59,7 @@ export = nodeunit.testCase({ stage, changeSetName: 'MyChangeSet', stackName: 'StackB', + adminPermissions: false, templatePath: artifact.atPath('path/to/other/file') }); @@ -162,6 +165,7 @@ export = nodeunit.testCase({ stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }), templatePath: new cpapi.Artifact(stack as any, 'TestArtifact').atPath('some/file'), stackName: 'MyStack', + adminPermissions: false, replaceOnFailure: true, }); const stackArn = _stackArn('MyStack'); @@ -181,6 +185,7 @@ export = nodeunit.testCase({ const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cloudformation.PipelineDeleteStackAction(stack, 'Action', { stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }), + adminPermissions: false, stackName: 'MyStack', }); const stackArn = _stackArn('MyStack'); diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.expected.json index 552b0ee5903aa..3bf3e525fb34a 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.expected.json @@ -162,12 +162,6 @@ "PipelineC660917D": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "PipelineRoleD68726F7", @@ -269,7 +263,13 @@ ], "Name": "Deploy" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } }, "DependsOn": [ "PipelineRoleD68726F7", @@ -322,4 +322,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.ts index 4ac08dbc0ab9a..42593fd8ecdfb 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.ts @@ -30,7 +30,7 @@ new cfn.PipelineCreateReplaceChangeSetAction(prodStage, 'PrepareChanges', { stage: prodStage, stackName, changeSetName, - fullPermissions: true, + adminPermissions: true, templatePath: source.outputArtifact.atPath('template.yaml'), }); diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json index c8b43657dfa5c..acce08906cec8 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json @@ -118,12 +118,6 @@ "PipelineC660917D": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "PipelineRoleD68726F7", @@ -181,7 +175,13 @@ ], "Name": "Lambda" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } }, "DependsOn": [ "PipelineRoleD68726F7", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.ts index ea1748b395ec5..7b4958488eb02 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.ts @@ -30,6 +30,7 @@ new cloudformation.PipelineCreateUpdateStackAction(stack, 'CFN_Deploy', { stage: cfnStage, stackName: 'aws-cdk-codepipeline-cross-region-deploy-stack', templatePath: sourceAction.outputArtifact.atPath('template.yml'), + adminPermissions: false, region, }); diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json index 2481e51936f1f..2b8e29dac54f5 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json @@ -147,12 +147,6 @@ "PipelineC660917D": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "PipelineRoleD68726F7", @@ -221,7 +215,13 @@ ], "Name": "CFN" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } }, "DependsOn": [ "PipelineRoleD68726F7", @@ -254,4 +254,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.ts index b0a1bc7f25daf..e0d09f1f35e4c 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.ts @@ -37,6 +37,7 @@ new cfn.PipelineCreateReplaceChangeSetAction(stack, 'DeployCFN', { stackName, role, templatePath: source.outputArtifact.atPath('test.yaml'), + adminPermissions: false, }); app.run(); diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json index 3c94a18f54246..f49b262f994b0 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json @@ -222,12 +222,6 @@ "PipelineC660917D": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "MyBucketF68F3FF0" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "PipelineRoleD68726F7", @@ -355,7 +349,13 @@ ], "Name": "Build" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "MyBucketF68F3FF0" + }, + "Type": "S3" + } }, "DependsOn": [ "PipelineRoleD68726F7", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json index c0083e6923c60..b5baa08eaeba2 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json @@ -124,12 +124,6 @@ "PipelineC660917D": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "PipelineRoleD68726F7", @@ -219,7 +213,13 @@ ], "Name": "build" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } }, "DependsOn": [ "PipelineRoleD68726F7", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit.expected.json index 94dca9257ab14..9797b450a707c 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit.expected.json @@ -160,12 +160,6 @@ "PipelineC660917D": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "PipelineArtifactsBucket22248F97" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "PipelineRoleD68726F7", @@ -221,7 +215,13 @@ ], "Name": "build" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } }, "DependsOn": [ "PipelineRoleD68726F7", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json index af9cc411c88b1..1b434c57ffcd5 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json @@ -258,12 +258,6 @@ "PipelineC660917D": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "CodeDeployPipelineIntegTest9F618D61" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "PipelineRoleD68726F7", @@ -328,7 +322,13 @@ ], "Name": "Deploy" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "CodeDeployPipelineIntegTest9F618D61" + }, + "Type": "S3" + } }, "DependsOn": [ "PipelineRoleD68726F7", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json index 47d2022609c36..d35667e8992a3 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json @@ -103,12 +103,6 @@ "MyPipelineAED38ECF": { "Type": "AWS::CodePipeline::Pipeline", "Properties": { - "ArtifactStore": { - "Location": { - "Ref": "MyPipelineArtifactsBucket727923DD" - }, - "Type": "S3" - }, "RoleArn": { "Fn::GetAtt": [ "MyPipelineRoleC0D47CA4", @@ -177,7 +171,13 @@ ], "Name": "Build" } - ] + ], + "ArtifactStore": { + "Location": { + "Ref": "MyPipelineArtifactsBucket727923DD" + }, + "Type": "S3" + } }, "DependsOn": [ "MyPipelineRoleC0D47CA4", @@ -238,7 +238,7 @@ ] } }, - "SourceOnSourceStateChangeEF8EB16D": { + "MyPipelineOnActionStateChange3B9E6332": { "Type": "AWS::Events::Rule", "Properties": { "EventPattern": { @@ -276,6 +276,12 @@ "detail": { "stage": [ "Source" + ], + "action": [ + "CodeCommitSource" + ], + "state": [ + "STARTED" ] } }, @@ -290,7 +296,7 @@ ] } }, - "MyPipelineOnActionStateChange3B9E6332": { + "SourceOnSourceStateChangeEF8EB16D": { "Type": "AWS::Events::Rule", "Properties": { "EventPattern": { @@ -328,12 +334,6 @@ "detail": { "stage": [ "Source" - ], - "action": [ - "CodeCommitSource" - ], - "state": [ - "STARTED" ] } }, 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 214f63e90140f..3b09a4d98d2cf 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 @@ -64,7 +64,8 @@ export = { changeSetName, role: changeSetExecRole, templatePath: new ArtifactPath(buildAction.outputArtifact, 'template.yaml'), - templateConfiguration: new ArtifactPath(buildAction.outputArtifact, 'templateConfig.json') + templateConfiguration: new ArtifactPath(buildAction.outputArtifact, 'templateConfig.json'), + adminPermissions: false, }); new PipelineExecuteChangeSetAction(stack, 'ExecuteChangeSetProd', { @@ -205,7 +206,7 @@ export = { stage: stack.deployStage, stackName: 'MyStack', templatePath: stack.source.outputArtifact.atPath('template.yaml'), - fullPermissions: true, + adminPermissions: true, }); const roleId = "PipelineDeployCreateUpdateRole515CB7D4"; @@ -259,6 +260,7 @@ export = { stackName: 'MyStack', templatePath: stack.source.outputArtifact.atPath('template.yaml'), outputFileName: 'CreateResponse.json', + adminPermissions: false, }); // THEN: Action has output artifacts @@ -290,6 +292,7 @@ export = { stackName: 'MyStack', templatePath: stack.source.outputArtifact.atPath('template.yaml'), replaceOnFailure: true, + adminPermissions: false, }); // THEN: Action has output artifacts @@ -322,6 +325,7 @@ export = { stage: stack.deployStage, stackName: 'MyStack', templatePath: stack.source.outputArtifact.atPath('template.yaml'), + adminPermissions: false, parameterOverrides: { RepoName: stack.repo.repositoryName } diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 301995f1dcd5b..2d76f24b936a4 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -394,12 +394,14 @@ export = { templatePath: sourceAction.outputArtifact.atPath('template.yaml'), stackName: 'SomeStack', region: pipelineRegion, + adminPermissions: false, }); new cloudformation.PipelineCreateUpdateStackAction(stack, 'Action2', { stage: stage2, templatePath: sourceAction.outputArtifact.atPath('template.yaml'), stackName: 'OtherStack', region: 'us-east-1', + adminPermissions: false, }); new cloudformation.PipelineExecuteChangeSetAction(stack, 'Action3', { stage: stage2,