Skip to content

Commit 390baf1

Browse files
authored
fix(codebuild): correctly handle permissions for Projects inside VPC. (#2662)
A CodeBuild Project needs to have appropriate EC2 permissions on creation when it uses a VPC. However, the default Policy that a Project Role has depends on the Project itself (for CloudWatch Logs permissions). Because of that, add a dependency between the Policy containing the EC2 permissions and the Project. BREAKING CHANGE: the method addToRoleInlinePolicy in CodeBuild's Project class has been removed. Fixes #2651 Fixes #2652
1 parent d229836 commit 390baf1

File tree

4 files changed

+87
-42
lines changed

4 files changed

+87
-42
lines changed

packages/@aws-cdk/aws-codebuild/README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ import s3 = require('@aws-cdk/aws-s3');
7777
const bucket = new s3.Bucket(this, 'MyBucket');
7878
new codebuild.Project(this, 'MyProject', {
7979
source: codebuild.Source.s3({
80-
bucket: bucket,
80+
bucket,
8181
path: 'path/to/file.zip',
8282
}),
8383
});
@@ -138,7 +138,10 @@ With S3 caching, the cache is stored in an S3 bucket which is available from mul
138138

139139
```typescript
140140
new codebuild.Project(this, 'Project', {
141-
source: new codebuild.CodePipelineSource(),
141+
source: codebuild.Source.bitBucket({
142+
owner: 'awslabs',
143+
repo: 'aws-cdk',
144+
}),
142145
cache: codebuild.Cache.bucket(new Bucket(this, 'Bucket'))
143146
});
144147
```
@@ -153,7 +156,9 @@ With local caching, the cache is stored on the codebuild instance itself. CodeBu
153156

154157
```typescript
155158
new codebuild.Project(this, 'Project', {
156-
source: new codebuild.CodePipelineSource(),
159+
source: codebuild.Source.gitHubEnterprise({
160+
httpsCloneUrl: 'https://my-github-enterprise.com/owner/repo',
161+
}),
157162
cache: codebuild.Cache.local(LocalCacheMode.DockerLayer, LocalCacheMode.Custom)
158163
});
159164
```
@@ -262,10 +267,10 @@ with their identifier.
262267

263268
So, a buildspec for the above Project could look something like this:
264269

265-
```ts
270+
```typescript
266271
const project = new codebuild.Project(this, 'MyProject', {
267272
// secondary sources and artifacts as above...
268-
buildSpec: {
273+
buildSpec: codebuild.BuildSpec.fromObject({
269274
version: '0.2',
270275
phases: {
271276
build: {
@@ -285,7 +290,7 @@ const project = new codebuild.Project(this, 'MyProject', {
285290
},
286291
},
287292
},
288-
},
293+
}),
289294
});
290295
```
291296

@@ -330,8 +335,10 @@ const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup1', {
330335
groupName: 'MySecurityGroup',
331336
vpc: vpc,
332337
});
333-
new Project(stack, 'MyProject', {
334-
buildScript: new assets.ZipDirectoryAsset(stack, 'Bundle', { path: 'script_bundle' }),
338+
new codebuild.Project(stack, 'MyProject', {
339+
buildSpec: codebuild.BuildSpec.fromObject({
340+
// ...
341+
}),
335342
securityGroups: [securityGroup],
336343
vpc: vpc
337344
});

packages/@aws-cdk/aws-codebuild/lib/project.ts

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ecr = require('@aws-cdk/aws-ecr');
55
import events = require('@aws-cdk/aws-events');
66
import iam = require('@aws-cdk/aws-iam');
77
import kms = require('@aws-cdk/aws-kms');
8-
import { Aws, Construct, IResource, Lazy, PhysicalName, Resource, ResourceIdentifiers, Stack } from '@aws-cdk/cdk';
8+
import { Aws, CfnResource, Construct, IResource, Lazy, PhysicalName, Resource, ResourceIdentifiers, Stack } from '@aws-cdk/cdk';
99
import { IArtifacts } from './artifacts';
1010
import { BuildSpec } from './build-spec';
1111
import { Cache } from './cache';
@@ -699,6 +699,8 @@ export class Project extends ProjectBase {
699699
vpcConfig: this.configureVpc(props),
700700
});
701701

702+
this.addVpcRequiredPermissions(props, resource);
703+
702704
const resourceIdentifiers = new ResourceIdentifiers(this, {
703705
arn: resource.attrArn,
704706
name: resource.refAsString,
@@ -718,20 +720,6 @@ export class Project extends ProjectBase {
718720
}
719721
}
720722

721-
/**
722-
* Add a permission only if there's a policy attached.
723-
* @param statement The permissions statement to add
724-
*/
725-
public addToRoleInlinePolicy(statement: iam.PolicyStatement) {
726-
if (this.role) {
727-
const policy = new iam.Policy(this, 'PolicyDocument', {
728-
policyName: 'CodeBuildEC2Policy',
729-
statements: [statement]
730-
});
731-
this.role.attachInlinePolicy(policy);
732-
}
733-
}
734-
735723
/**
736724
* Adds a secondary source to the Project.
737725
*
@@ -869,31 +857,55 @@ export class Project extends ProjectBase {
869857
}
870858
this._connections = new ec2.Connections({ securityGroups });
871859

872-
this.addToRoleInlinePolicy(new iam.PolicyStatement({
873-
resources: ['*'],
874-
actions: [
875-
'ec2:CreateNetworkInterface', 'ec2:DescribeNetworkInterfaces',
876-
'ec2:DeleteNetworkInterface', 'ec2:DescribeSubnets',
877-
'ec2:DescribeSecurityGroups', 'ec2:DescribeDhcpOptions',
878-
'ec2:DescribeVpcs']
879-
}));
880-
this.addToRolePolicy(new iam.PolicyStatement({
860+
return {
861+
vpcId: props.vpc.vpcId,
862+
subnets: props.vpc.selectSubnets(props.subnetSelection).subnetIds,
863+
securityGroupIds: this.connections.securityGroups.map(s => s.securityGroupId)
864+
};
865+
}
866+
867+
private addVpcRequiredPermissions(props: ProjectProps, project: CfnProject): void {
868+
if (!props.vpc || !this.role) {
869+
return;
870+
}
871+
872+
this.role.addToPolicy(new iam.PolicyStatement({
881873
resources: [`arn:aws:ec2:${Aws.region}:${Aws.accountId}:network-interface/*`],
882874
actions: ['ec2:CreateNetworkInterfacePermission'],
883875
conditions: {
884876
StringEquals: {
885-
"ec2:Subnet": props.vpc
877+
'ec2:Subnet': props.vpc
886878
.selectSubnets(props.subnetSelection).subnetIds
887879
.map(si => `arn:aws:ec2:${Aws.region}:${Aws.accountId}:subnet/${si}`),
888-
"ec2:AuthorizedService": "codebuild.amazonaws.com"
889-
}
890-
}
880+
'ec2:AuthorizedService': 'codebuild.amazonaws.com'
881+
},
882+
},
891883
}));
892-
return {
893-
vpcId: props.vpc.vpcId,
894-
subnets: props.vpc.selectSubnets(props.subnetSelection).subnetIds,
895-
securityGroupIds: this.connections.securityGroups.map(s => s.securityGroupId)
896-
};
884+
885+
const policy = new iam.Policy(this, 'PolicyDocument', {
886+
policyName: 'CodeBuildEC2Policy',
887+
statements: [
888+
new iam.PolicyStatement({
889+
resources: ['*'],
890+
actions: [
891+
'ec2:CreateNetworkInterface',
892+
'ec2:DescribeNetworkInterfaces',
893+
'ec2:DeleteNetworkInterface',
894+
'ec2:DescribeSubnets',
895+
'ec2:DescribeSecurityGroups',
896+
'ec2:DescribeDhcpOptions',
897+
'ec2:DescribeVpcs',
898+
],
899+
}),
900+
],
901+
});
902+
this.role.attachInlinePolicy(policy);
903+
904+
// add an explicit dependency between the EC2 Policy and this Project -
905+
// otherwise, creating the Project fails,
906+
// as it requires these permissions to be already attached to the Project's Role
907+
const cfnPolicy = policy.node.findChild('Resource') as CfnResource;
908+
project.addDependsOn(cfnPolicy);
897909
}
898910

899911
private validateCodePipelineSettings(artifacts: IArtifacts) {

packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,10 @@
423423
"Ref": "MyVPCAFB07A31"
424424
}
425425
}
426-
}
426+
},
427+
"DependsOn": [
428+
"MyProjectPolicyDocument646EE0F2"
429+
]
427430
}
428431
}
429432
}

packages/@aws-cdk/aws-codebuild/test/test.project.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert';
2+
import ec2 = require('@aws-cdk/aws-ec2');
3+
import iam = require('@aws-cdk/aws-iam');
24
import { Bucket } from '@aws-cdk/aws-s3';
35
import cdk = require('@aws-cdk/cdk');
46
import { Test } from 'nodeunit';
@@ -232,4 +234,25 @@ export = {
232234

233235
test.done();
234236
},
237+
238+
'can use an imported Role for a Project within a VPC'(test: Test) {
239+
const stack = new cdk.Stack();
240+
241+
const importedRole = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::1234567890:role/service-role/codebuild-bruiser-service-role');
242+
const vpc = new ec2.Vpc(stack, 'Vpc');
243+
244+
new codebuild.Project(stack, 'Project', {
245+
source: codebuild.Source.gitHubEnterprise({
246+
httpsCloneUrl: 'https://mygithub-enterprise.com/myuser/myrepo',
247+
}),
248+
role: importedRole,
249+
vpc,
250+
});
251+
252+
expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', {
253+
// no need to do any assertions
254+
}));
255+
256+
test.done();
257+
},
235258
};

0 commit comments

Comments
 (0)