Skip to content

Commit

Permalink
feat(aws-cloudformation): add permission management to CreateUpdate a…
Browse files Browse the repository at this point in the history
…nd Delete Stack CodePipeline Actions. (#880)
  • Loading branch information
skinny85 committed Oct 11, 2018
1 parent 3d91c93 commit 8b3ae43
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 38 deletions.
34 changes: 30 additions & 4 deletions packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo
this.role.addToPolicy(new iam.PolicyStatement().addAction('*').addAllResources());
}
}

// Allow the pipeline to pass this actions' role to CloudFormation
// Required by all Actions that perform CFN deployments
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
.addAction('iam:PassRole')
.addResource(this.role.roleArn));
}

/**
Expand Down Expand Up @@ -265,10 +271,6 @@ export class PipelineCreateReplaceChangeSetAction extends PipelineCloudFormation
.addActions('cloudformation:CreateChangeSet', 'cloudformation:DeleteChangeSet', 'cloudformation:DescribeChangeSet')
.addResource(stackArn)
.addCondition('StringEquals', { 'cloudformation:ChangeSetName': props.changeSetName }));
// Allow the pipeline to pass this actions' role to CloudFormation
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
.addAction('iam:PassRole')
.addResource(this.role.roleArn));
}
}

Expand Down Expand Up @@ -318,6 +320,23 @@ export class PipelineCreateUpdateStackAction extends PipelineCloudFormationDeplo
TemplatePath: props.templatePath.location
});
this.addInputArtifact(props.templatePath.artifact);

// permissions are based on best-guess from
// https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html
// and https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awscloudformation.html
const stackArn = stackArnFromName(props.stackName);
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
.addActions(
'cloudformation:DescribeStack*',
'cloudformation:CreateStack',
'cloudformation:UpdateStack',
'cloudformation:DeleteStack', // needed when props.replaceOnFailure is true
'cloudformation:GetTemplate*',
'cloudformation:ValidateTemplate',
'cloudformation:GetStackPolicy',
'cloudformation:SetStackPolicy',
)
.addResource(stackArn));
}
}

Expand All @@ -339,6 +358,13 @@ export class PipelineDeleteStackAction extends PipelineCloudFormationDeployActio
super(parent, id, props, {
ActionMode: 'DELETE_ONLY',
});
const stackArn = stackArnFromName(props.stackName);
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
.addActions(
'cloudformation:DescribeStack*',
'cloudformation:DeleteStack',
)
.addResource(stackArn));
}
}

Expand Down
65 changes: 51 additions & 14 deletions packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import nodeunit = require('nodeunit');
import cloudformation = require('../lib');

export = nodeunit.testCase({
CreateReplaceChangeSet: {
'CreateReplaceChangeSet': {
works(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
Expand All @@ -21,11 +21,7 @@ export = nodeunit.testCase({

_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);

const stackArn = cdk.ArnUtils.fromComponents({
service: 'cloudformation',
resource: 'stack',
resourceName: 'MyStack/*'
});
const stackArn = _stackArn('MyStack');
const changeSetCondition = { StringEquals: { 'cloudformation:ChangeSetName': 'MyChangeSet' } };
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn);
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition);
Expand All @@ -44,7 +40,7 @@ export = nodeunit.testCase({
test.done();
}
},
ExecuteChangeSet: {
'ExecuteChangeSet': {
works(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
Expand All @@ -55,11 +51,7 @@ export = nodeunit.testCase({
stackName: 'MyStack',
});

const stackArn = cdk.ArnUtils.fromComponents({
service: 'cloudformation',
resource: 'stack',
resourceName: 'MyStack/*'
});
const stackArn = _stackArn('MyStack');
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn,
{ StringEquals: { 'cloudformation:ChangeSetName': 'MyChangeSet' } });

Expand All @@ -71,7 +63,44 @@ export = nodeunit.testCase({

test.done();
}
}
},

'the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions'(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const action = new cloudformation.PipelineCreateUpdateStackAction(stack, 'Action', {
stage: new StageDouble({ pipelineRole }),
templatePath: new cpapi.Artifact(stack as any, 'TestArtifact').atPath('some/file'),
stackName: 'MyStack',
});
const stackArn = _stackArn('MyStack');

_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn);
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:CreateStack', stackArn);
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:UpdateStack', stackArn);
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn);

_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);

test.done();
},

'the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions'(test: nodeunit.Test) {
const stack = new cdk.Stack();
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
const action = new cloudformation.PipelineDeleteStackAction(stack, 'Action', {
stage: new StageDouble({ pipelineRole }),
stackName: 'MyStack',
});
const stackArn = _stackArn('MyStack');

_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn);
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn);

_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);

test.done();
},
});

interface PolicyStatementJson {
Expand Down Expand Up @@ -121,7 +150,7 @@ function _assertPermissionGranted(test: nodeunit.Test, statements: PolicyStateme
: '';
const statementsStr = JSON.stringify(cdk.resolve(statements), null, 2);
test.ok(_grantsPermission(statements, action, resource, conditions),
`Expected to find a statement granting ${action} on ${cdk.resolve(resource)}${conditionStr}, found:\n${statementsStr}`);
`Expected to find a statement granting ${action} on ${JSON.stringify(cdk.resolve(resource))}${conditionStr}, found:\n${statementsStr}`);
}

function _grantsPermission(statements: PolicyStatementJson[], action: string, resource: string, conditions?: any) {
Expand All @@ -145,6 +174,14 @@ function _isOrContains(entity: string | string[], value: string): boolean {
return false;
}

function _stackArn(stackName: string): string {
return cdk.ArnUtils.fromComponents({
service: 'cloudformation',
resource: 'stack',
resourceName: `${stackName}/*`,
});
}

class StageDouble implements cpapi.IStage, cpapi.IInternalStage {
public readonly name: string;
public readonly pipelineArn: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@
]
}
},
{
"Action": "iam:PassRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"PipelineDeployPrepareChangesRoleD28C853C",
"Arn"
]
}
},
{
"Action": "cloudformation:DescribeStacks",
"Effect": "Allow",
Expand Down Expand Up @@ -145,16 +155,6 @@
]
}
},
{
"Action": "iam:PassRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"PipelineDeployPrepareChangesRoleD28C853C",
"Arn"
]
}
},
{
"Action": "cloudformation:ExecuteChangeSet",
"Condition": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@
}
]
},
{
"Action": "iam:PassRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"CfnChangeSetRole6F05F6FC",
"Arn"
]
}
},
{
"Action": "cloudformation:DescribeStacks",
"Effect": "Allow",
Expand Down Expand Up @@ -159,16 +169,6 @@
]
]
}
},
{
"Action": "iam:PassRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"CfnChangeSetRole6F05F6FC",
"Arn"
]
}
}
],
"Version": "2012-10-17"
Expand Down

0 comments on commit 8b3ae43

Please sign in to comment.