From 9aa9eed1d28f232aebc24988a5816c1ea4ff4a3a 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` * Require user must set adminPermissions boolean for pipeline action * 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 | 38 +++- .../lib/pipeline-deploy-stack-action.ts | 68 +++++- packages/@aws-cdk/app-delivery/package.json | 4 + .../@aws-cdk/app-delivery/test/integ.cicd.ts | 6 +- .../test/test.pipeline-deploy-stack-action.ts | 207 +++++++++++++++++- .../lib/pipeline-actions.ts | 55 +++-- .../test/test.pipeline-actions.ts | 7 +- 7 files changed, 359 insertions(+), 26 deletions(-) diff --git a/packages/@aws-cdk/app-delivery/README.md b/packages/@aws-cdk/app-delivery/README.md index f6790ea3e4302..1a7e238e3137e 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,48 @@ 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, }); +deployServiceBAction.role.addToPolicy( + // new iam.PolicyStatement(). + // ... addAction('actions that you need'). + // add resource +); ``` #### `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..104c2ca42bca5 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 CAPABILITY_IAM 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,17 @@ export class PipelineDeployStackAction extends cdk.Construct { this.stack = props.stack; const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet'; - new cfn.PipelineCreateReplaceChangeSetAction(this, 'ChangeSet', { + 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: props.capabilities, }); + this.role = changeSetAction.role; new cfn.PipelineExecuteChangeSetAction(this, 'Execute', { changeSetName, @@ -97,4 +148,17 @@ 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); + } } diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 6f7a33020876a..bcfd6201766cf 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.17.0", "@aws-cdk/aws-codebuild": "^0.17.0", "@aws-cdk/aws-codepipeline-api": "^0.17.0", + "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0", "@aws-cdk/cx-api": "^0.17.0" }, "devDependencies": { + "@aws-cdk/assert": "^0.17.0", "@aws-cdk/aws-codepipeline": "^0.17.0", "@aws-cdk/aws-s3": "^0.17.0", "cdk-build-tools": "^0.17.0", @@ -62,7 +64,9 @@ "cdk" ], "peerDependencies": { + "@aws-cdk/aws-cloudformation": "^0.17.0", "@aws-cdk/aws-codepipeline-api": "^0.17.0", + "@aws-cdk/aws-iam": "^0.17.0", "@aws-cdk/cdk": "^0.17.0" } } diff --git a/packages/@aws-cdk/app-delivery/test/integ.cicd.ts b/packages/@aws-cdk/app-delivery/test/integ.cicd.ts index bf6948588f17e..68423c2bc628b 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'); @@ -15,13 +16,16 @@ const source = new code.GitHubSourceAction(stack, 'GitHub', { repo: 'aws-cdk', oauthToken: new cdk.Secret('DummyToken'), }); +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..79442b2e97966 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,162 @@ 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 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, + }); + expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + Configuration: { + StackName: "TestStack", + 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_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 supply enable full 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: '*', + } + ], + } + })); + 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(). + addAction('ec2:AuthorizeSecurityGroupEgress'). + addAction('ec2:AuthorizeSecurityGroupIngress'). + addAction('ec2:DeleteSecurityGroup'). + addAction('ec2:DescribeSecurityGroups'). + addAction('ec2:CreateSecurityGroup'). + addAction('ec2:RevokeSecurityGroupEgress'). + addAction('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 +241,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 +269,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..0b15293c1cc75 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 AnonymousIAM, 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,12 @@ 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 = cfnCapabilities(props.adminPermissions, 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, + // If None set to undefined so nothing is sent + Capabilities: capabilities === CloudFormationCapabilities.None ? undefined : capabilities.toString(), RoleArn: new cdk.Token(() => this.role.roleArn), ParameterOverrides: cdk.CloudFormationJSON.stringify(props.parameterOverrides), TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined, @@ -217,7 +215,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 +354,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 +365,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 = '', } /** @@ -494,3 +502,16 @@ function stackArnFromProps(props: { stackName: string, region?: string }): strin resourceName: `${props.stackName}/*` }); } + +function cfnCapabilities(adminPermissions: boolean, capabilities?: CloudFormationCapabilities): CloudFormationCapabilities { + if (adminPermissions && capabilities === undefined) { + // admin true default capability to NamedIAM + return CloudFormationCapabilities.NamedIAM; + } else if (capabilities === undefined) { + // else capabilities are undefined set AnonymousIAM + return CloudFormationCapabilities.AnonymousIAM; + } else { + // else capabilities are defined use them + return capabilities; + } +} 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 978303b4f308f..391971fae2fe4 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');