Skip to content

Commit 8b3ae43

Browse files
authored
feat(aws-cloudformation): add permission management to CreateUpdate and Delete Stack CodePipeline Actions. (#880)
1 parent 3d91c93 commit 8b3ae43

File tree

4 files changed

+101
-38
lines changed

4 files changed

+101
-38
lines changed

packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo
211211
this.role.addToPolicy(new iam.PolicyStatement().addAction('*').addAllResources());
212212
}
213213
}
214+
215+
// Allow the pipeline to pass this actions' role to CloudFormation
216+
// Required by all Actions that perform CFN deployments
217+
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
218+
.addAction('iam:PassRole')
219+
.addResource(this.role.roleArn));
214220
}
215221

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

@@ -318,6 +320,23 @@ export class PipelineCreateUpdateStackAction extends PipelineCloudFormationDeplo
318320
TemplatePath: props.templatePath.location
319321
});
320322
this.addInputArtifact(props.templatePath.artifact);
323+
324+
// permissions are based on best-guess from
325+
// https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html
326+
// and https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awscloudformation.html
327+
const stackArn = stackArnFromName(props.stackName);
328+
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
329+
.addActions(
330+
'cloudformation:DescribeStack*',
331+
'cloudformation:CreateStack',
332+
'cloudformation:UpdateStack',
333+
'cloudformation:DeleteStack', // needed when props.replaceOnFailure is true
334+
'cloudformation:GetTemplate*',
335+
'cloudformation:ValidateTemplate',
336+
'cloudformation:GetStackPolicy',
337+
'cloudformation:SetStackPolicy',
338+
)
339+
.addResource(stackArn));
321340
}
322341
}
323342

@@ -339,6 +358,13 @@ export class PipelineDeleteStackAction extends PipelineCloudFormationDeployActio
339358
super(parent, id, props, {
340359
ActionMode: 'DELETE_ONLY',
341360
});
361+
const stackArn = stackArnFromName(props.stackName);
362+
props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
363+
.addActions(
364+
'cloudformation:DescribeStack*',
365+
'cloudformation:DeleteStack',
366+
)
367+
.addResource(stackArn));
342368
}
343369
}
344370

packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import nodeunit = require('nodeunit');
66
import cloudformation = require('../lib');
77

88
export = nodeunit.testCase({
9-
CreateReplaceChangeSet: {
9+
'CreateReplaceChangeSet': {
1010
works(test: nodeunit.Test) {
1111
const stack = new cdk.Stack();
1212
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
@@ -21,11 +21,7 @@ export = nodeunit.testCase({
2121

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

24-
const stackArn = cdk.ArnUtils.fromComponents({
25-
service: 'cloudformation',
26-
resource: 'stack',
27-
resourceName: 'MyStack/*'
28-
});
24+
const stackArn = _stackArn('MyStack');
2925
const changeSetCondition = { StringEquals: { 'cloudformation:ChangeSetName': 'MyChangeSet' } };
3026
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn);
3127
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition);
@@ -44,7 +40,7 @@ export = nodeunit.testCase({
4440
test.done();
4541
}
4642
},
47-
ExecuteChangeSet: {
43+
'ExecuteChangeSet': {
4844
works(test: nodeunit.Test) {
4945
const stack = new cdk.Stack();
5046
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
@@ -55,11 +51,7 @@ export = nodeunit.testCase({
5551
stackName: 'MyStack',
5652
});
5753

58-
const stackArn = cdk.ArnUtils.fromComponents({
59-
service: 'cloudformation',
60-
resource: 'stack',
61-
resourceName: 'MyStack/*'
62-
});
54+
const stackArn = _stackArn('MyStack');
6355
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn,
6456
{ StringEquals: { 'cloudformation:ChangeSetName': 'MyChangeSet' } });
6557

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

7264
test.done();
7365
}
74-
}
66+
},
67+
68+
'the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions'(test: nodeunit.Test) {
69+
const stack = new cdk.Stack();
70+
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
71+
const action = new cloudformation.PipelineCreateUpdateStackAction(stack, 'Action', {
72+
stage: new StageDouble({ pipelineRole }),
73+
templatePath: new cpapi.Artifact(stack as any, 'TestArtifact').atPath('some/file'),
74+
stackName: 'MyStack',
75+
});
76+
const stackArn = _stackArn('MyStack');
77+
78+
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn);
79+
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:CreateStack', stackArn);
80+
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:UpdateStack', stackArn);
81+
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn);
82+
83+
_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);
84+
85+
test.done();
86+
},
87+
88+
'the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions'(test: nodeunit.Test) {
89+
const stack = new cdk.Stack();
90+
const pipelineRole = new RoleDouble(stack, 'PipelineRole');
91+
const action = new cloudformation.PipelineDeleteStackAction(stack, 'Action', {
92+
stage: new StageDouble({ pipelineRole }),
93+
stackName: 'MyStack',
94+
});
95+
const stackArn = _stackArn('MyStack');
96+
97+
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn);
98+
_assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn);
99+
100+
_assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn);
101+
102+
test.done();
103+
},
75104
});
76105

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

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

177+
function _stackArn(stackName: string): string {
178+
return cdk.ArnUtils.fromComponents({
179+
service: 'cloudformation',
180+
resource: 'stack',
181+
resourceName: `${stackName}/*`,
182+
});
183+
}
184+
148185
class StageDouble implements cpapi.IStage, cpapi.IInternalStage {
149186
public readonly name: string;
150187
public readonly pipelineArn: string;

packages/@aws-cdk/aws-codepipeline/test/integ.cfn-template-from-repo.lit.expected.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@
7676
]
7777
}
7878
},
79+
{
80+
"Action": "iam:PassRole",
81+
"Effect": "Allow",
82+
"Resource": {
83+
"Fn::GetAtt": [
84+
"PipelineDeployPrepareChangesRoleD28C853C",
85+
"Arn"
86+
]
87+
}
88+
},
7989
{
8090
"Action": "cloudformation:DescribeStacks",
8191
"Effect": "Allow",
@@ -145,16 +155,6 @@
145155
]
146156
}
147157
},
148-
{
149-
"Action": "iam:PassRole",
150-
"Effect": "Allow",
151-
"Resource": {
152-
"Fn::GetAtt": [
153-
"PipelineDeployPrepareChangesRoleD28C853C",
154-
"Arn"
155-
]
156-
}
157-
},
158158
{
159159
"Action": "cloudformation:ExecuteChangeSet",
160160
"Condition": {

packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn.expected.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@
9191
}
9292
]
9393
},
94+
{
95+
"Action": "iam:PassRole",
96+
"Effect": "Allow",
97+
"Resource": {
98+
"Fn::GetAtt": [
99+
"CfnChangeSetRole6F05F6FC",
100+
"Arn"
101+
]
102+
}
103+
},
94104
{
95105
"Action": "cloudformation:DescribeStacks",
96106
"Effect": "Allow",
@@ -159,16 +169,6 @@
159169
]
160170
]
161171
}
162-
},
163-
{
164-
"Action": "iam:PassRole",
165-
"Effect": "Allow",
166-
"Resource": {
167-
"Fn::GetAtt": [
168-
"CfnChangeSetRole6F05F6FC",
169-
"Arn"
170-
]
171-
}
172172
}
173173
],
174174
"Version": "2012-10-17"

0 commit comments

Comments
 (0)