diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 0fb8136fb8de5..0b820f6a2a89a 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -6,7 +6,7 @@ import ecr = require('@aws-cdk/aws-ecr'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); -import { Aws, Construct, IResource, Lazy, PhysicalName, Resource, ResourceIdentifiers, Stack } from '@aws-cdk/cdk'; +import { Aws, CfnResource, Construct, IResource, Lazy, PhysicalName, Resource, ResourceIdentifiers, Stack } from '@aws-cdk/cdk'; import { BuildArtifacts, CodePipelineBuildArtifacts, NoBuildArtifacts } from './artifacts'; import { BuildSpec, mergeBuildSpecs } from './build-spec'; import { Cache } from './cache'; @@ -705,6 +705,8 @@ export class Project extends ProjectBase { vpcConfig: this.configureVpc(props), }); + this.addVpcRequiredPermissions(props, resource); + const resourceIdentifiers = new ResourceIdentifiers(this, { arn: resource.projectArn, name: resource.projectName, @@ -738,20 +740,6 @@ export class Project extends ProjectBase { } } - /** - * Add a permission only if there's a policy attached. - * @param statement The permissions statement to add - */ - public addToRoleInlinePolicy(statement: iam.PolicyStatement) { - if (this.role) { - const policy = new iam.Policy(this, 'PolicyDocument', { - policyName: 'CodeBuildEC2Policy', - statements: [statement] - }); - this.role.attachInlinePolicy(policy); - } - } - /** * Adds a secondary source to the Project. * @@ -888,31 +876,56 @@ export class Project extends ProjectBase { }); this._securityGroups = [securityGroup]; } - this.addToRoleInlinePolicy(new iam.PolicyStatement({ - resources: ['*'], - actions: [ - 'ec2:CreateNetworkInterface', 'ec2:DescribeNetworkInterfaces', - 'ec2:DeleteNetworkInterface', 'ec2:DescribeSubnets', - 'ec2:DescribeSecurityGroups', 'ec2:DescribeDhcpOptions', - 'ec2:DescribeVpcs'] - })); - this.addToRolePolicy(new iam.PolicyStatement({ + + return { + vpcId: props.vpc.vpcId, + subnets: props.vpc.selectSubnets(props.subnetSelection).subnetIds, + securityGroupIds: this._securityGroups.map(s => s.securityGroupId) + }; + } + + private addVpcRequiredPermissions(props: ProjectProps, project: CfnProject): void { + if (!props.vpc || !this.role) { + return; + } + + this.role.addToPolicy(new iam.PolicyStatement({ resources: [`arn:aws:ec2:${Aws.region}:${Aws.accountId}:network-interface/*`], actions: ['ec2:CreateNetworkInterfacePermission'], conditions: { StringEquals: { - "ec2:Subnet": props.vpc + 'ec2:Subnet': props.vpc .selectSubnets(props.subnetSelection).subnetIds .map(si => `arn:aws:ec2:${Aws.region}:${Aws.accountId}:subnet/${si}`), - "ec2:AuthorizedService": "codebuild.amazonaws.com" - } - } + 'ec2:AuthorizedService': 'codebuild.amazonaws.com' + }, + }, })); - return { - vpcId: props.vpc.vpcId, - subnets: props.vpc.selectSubnets(props.subnetSelection).subnetIds, - securityGroupIds: this._securityGroups.map(s => s.securityGroupId) - }; + + const policy = new iam.Policy(this, 'PolicyDocument', { + policyName: 'CodeBuildEC2Policy', + statements: [ + new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + ], + }), + ], + }); + this.role.attachInlinePolicy(policy); + + // add an explicit dependency between the EC2 Policy and this Project - + // otherwise, creating the Project fails, + // as it requires these permissions to be already attached to the Project's Role + const cfnPolicy = policy.node.findChild('Resource') as CfnResource; + project.addDependsOn(cfnPolicy); } private parseArtifacts(props: ProjectProps) { diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json index 35f355bfeb053..0ebb733a366b0 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json @@ -524,7 +524,10 @@ "Ref": "MyVPCAFB07A31" } } - } + }, + "DependsOn": [ + "MyProjectPolicyDocument646EE0F2" + ] } }, "Parameters": { @@ -541,4 +544,4 @@ "Description": "Artifact hash for asset \"aws-cdk-codebuild-project-vpc/Bundle\"" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index 8f7372d58b471..95110422960ab 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -1,5 +1,7 @@ import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert'; import assets = require('@aws-cdk/assets'); +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); import { Bucket } from '@aws-cdk/aws-s3'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; @@ -266,4 +268,26 @@ export = { test.done(); }, + + 'can use an imported Role for a Project within a VPC'(test: Test) { + const stack = new cdk.Stack(); + + const importedRole = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::1234567890:role/service-role/codebuild-bruiser-service-role'); + const vpc = new ec2.Vpc(stack, 'Vpc'); + + new codebuild.Project(stack, 'Project', { + source: new codebuild.GitHubEnterpriseSource({ + httpsCloneUrl: 'https://mygithub-enterprise.com/myuser/myrepo', + }), + artifacts: new codebuild.NoBuildArtifacts(), + role: importedRole, + vpc, + }); + + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + // no need to do any assertions + })); + + test.done(); + }, };