diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/aws-cdk-codebuild-project-codeconnections.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/aws-cdk-codebuild-project-codeconnections.assets.json new file mode 100644 index 0000000000000..fef62bc6654d4 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/aws-cdk-codebuild-project-codeconnections.assets.json @@ -0,0 +1,4 @@ +{ + "version": "39.0.0", + "files": {} +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/aws-cdk-codebuild-project-codeconnections.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/aws-cdk-codebuild-project-codeconnections.template.json new file mode 100644 index 0000000000000..942996016e143 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/aws-cdk-codebuild-project-codeconnections.template.json @@ -0,0 +1,200 @@ +{ + "Resources": { + "MyProjectRole9BBE5233": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectRoleDefaultPolicyB19B7C29": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": "codeconnections:UseConnection", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectRoleDefaultPolicyB19B7C29", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + }, + "MyProject39F7B0AE": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Cache": { + "Type": "NO_CACHE" + }, + "EncryptionKey": "alias/aws/s3", + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:7.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "Auth": { + "Resource": "arn:aws:codeconnections:us-east-1:123456789012:connection/84941bd4-6795-4871-99f6-e2e4697138da", + "Type": "CODECONNECTIONS" + }, + "Location": "https://github.com/aws/aws-cdk.git", + "Type": "GITHUB" + }, + "Triggers": { + "Webhook": false + } + }, + "DependsOn": [ + "MyProjectCodeConnectionsPolicyE6B40D02" + ] + }, + "MyProjectCodeConnectionsPolicyE6B40D02": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codeconnections:UseConnection", + "codeconnections:GetConnection", + "codeconnections:GetConnectionToken", + "codestar-connections:UseConnection", + "codestar-connections:GetConnection", + "codestar-connections:GetConnectionToken" + ], + "Effect": "Allow", + "Resource": "arn:aws:codeconnections:us-east-1:123456789012:connection/84941bd4-6795-4871-99f6-e2e4697138da" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectCodeConnectionsPolicyE6B40D02", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/integ.json new file mode 100644 index 0000000000000..e8571171d3b59 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.js.snapshot/integ.json @@ -0,0 +1,10 @@ +{ + "version": "39.0.0", + "testCases": { + "integ-codebuild-project-codeconnections": { + "stacks": [ + "aws-cdk-codebuild-project-codeconnections" + ] + } + } +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.ts new file mode 100644 index 0000000000000..9402045e8c52b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.project-codeconnections.ts @@ -0,0 +1,39 @@ +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as cdk from 'aws-cdk-lib'; +import * as codebuild from 'aws-cdk-lib/aws-codebuild'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codebuild-project-codeconnections'); + +const gitHubSource = codebuild.Source.gitHub({ + owner: 'aws', + repo: 'aws-cdk', + reportBuildStatus: false, +}); + +const project = new codebuild.Project(stack, 'MyProject', { + source: gitHubSource, + grantReportGroupPermissions: false, +}); + +// Override source to use CODECONNECTIONS authentication +const cfnProject = project.node.defaultChild as codebuild.CfnProject; +cfnProject.source = { + type: 'GITHUB', + location: 'https://github.com/aws/aws-cdk.git', + auth: { + type: 'CODECONNECTIONS', + resource: 'arn:aws:codeconnections:us-east-1:123456789012:connection/84941bd4-6795-4871-99f6-e2e4697138da', + }, +}; + +project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codeconnections:UseConnection'], + resources: ['*'], +})); + +new IntegTest(app, 'integ-codebuild-project-codeconnections', { + testCases: [stack], +}); diff --git a/packages/aws-cdk-lib/aws-codebuild/lib/project.ts b/packages/aws-cdk-lib/aws-codebuild/lib/project.ts index 0a533817ee39f..937e0fc3998d9 100644 --- a/packages/aws-cdk-lib/aws-codebuild/lib/project.ts +++ b/packages/aws-cdk-lib/aws-codebuild/lib/project.ts @@ -33,7 +33,7 @@ import * as kms from '../../aws-kms'; import type * as s3 from '../../aws-s3'; import type * as secretsmanager from '../../aws-secretsmanager'; import type { Duration, IResource } from '../../core'; -import { Annotations, ArnFormat, Aws, Lazy, Names, PhysicalName, Reference, Resource, SecretValue, Stack, Token, TokenComparison, Tokenization, UnscopedValidationError, ValidationError } from '../../core'; +import { Annotations, ArnFormat, Aspects, Aws, Lazy, Names, PhysicalName, Reference, Resource, SecretValue, Stack, Token, TokenComparison, Tokenization, UnscopedValidationError, ValidationError } from '../../core'; import type { IArrayBox, IBox } from '../../core/lib/helpers-internal'; import { Box, memoizedGetter } from '../../core/lib/helpers-internal'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; @@ -1241,6 +1241,10 @@ export class Project extends ProjectBase { this.buildImage.bind(this, this, {}); } + // add an explicit dependency between CodeConnections Policy and this Project + // if CODECONNECTIONS / CODESTAR_CONNECTIONS source auth is used + this.addCodeConnectionsPolicyDependency(resource); + this.node.addValidation({ validate: () => this.validateProject() }); } @@ -1597,6 +1601,67 @@ export class Project extends ProjectBase { project.node.addDependency(policy); } + /** + * Adds an explicit dependency on CodeConnections policy if CODECONNECTIONS / CODESTAR_CONNECTIONS source auth is used. + */ + private addCodeConnectionsPolicyDependency(cfnProject: CfnProject): void { + if (!this.role) { + return; + } + + const role = this.role; + const resource = cfnProject; + const project = this; + + Aspects.of(this).add({ + visit: (node: IConstruct): void => { + if (node !== project) { + return; + } + + const source = resource.source as CfnProject.SourceProperty | undefined; + if (!source) { + return; + } + + const auth = source.auth as CfnProject.SourceAuthProperty | undefined; + if (!auth) { + return; + } + + const authType = typeof auth.type === 'string' ? auth.type.toUpperCase() : ''; + if (authType !== 'CODECONNECTIONS' && authType !== 'CODESTAR_CONNECTIONS') { + return; + } + + if (role instanceof iam.Role) { + let connPolicy = project.node.tryFindChild('CodeConnectionsPolicy') as iam.Policy | undefined; + if (!connPolicy) { + const connectionArn = auth.resource || '*'; + connPolicy = new iam.Policy(project, 'CodeConnectionsPolicy', { + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'codeconnections:UseConnection', + 'codeconnections:GetConnection', + 'codeconnections:GetConnectionToken', + 'codestar-connections:UseConnection', + 'codestar-connections:GetConnection', + 'codestar-connections:GetConnectionToken', + ], + resources: [connectionArn], + }), + ], + }); + connPolicy.attachToRole(role); + } + resource.node.addDependency(connPolicy); + } + }, + }); + } + private validateCodePipelineSettings(artifacts: IArtifacts) { const sourceType = this.source.type; const artifactsType = artifacts.type; diff --git a/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts b/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts index e3c2fad69d78e..9bca3c9d0e74c 100644 --- a/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts +++ b/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts @@ -2474,3 +2474,105 @@ test.each([-1, 15])('throws when autoRetryLimit is invalid', (autoRetryLimit) => }); }).toThrow(`autoRetryLimit must be a value between 0 and 10, got ${autoRetryLimit}.`); }); + +test('CodeBuild project with CODECONNECTIONS source auth has DependsOn for role default policy', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'test-owner', + repo: 'test-repo', + webhook: false, + }), + }); + + // Override source to use CODECONNECTIONS auth via L1 escape hatch + const cfnProject = project.node.defaultChild as codebuild.CfnProject; + cfnProject.source = { + type: 'GITHUB', + location: 'https://github.com/test-owner/test-repo.git', + auth: { + type: 'CODECONNECTIONS', + resource: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + }, + }; + + // Add codeconnections permissions to the role (this creates the default policy) + project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codeconnections:UseConnection'], + resources: ['*'], + })); + + // THEN - CfnProject should have DependsOn including the dedicated CodeConnections policy + const template = Template.fromStack(stack); + template.hasResource('AWS::CodeBuild::Project', { + DependsOn: Match.arrayWith([ + Match.stringLikeRegexp('ProjectCodeConnectionsPolicy.*'), + ]), + }); +}); + +test('CodeBuild project with CODESTAR_CONNECTIONS source auth has DependsOn for role default policy', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'test-owner', + repo: 'test-repo', + webhook: false, + }), + }); + + // Override source to use CODESTAR_CONNECTIONS auth via L1 escape hatch + const cfnProject = project.node.defaultChild as codebuild.CfnProject; + cfnProject.source = { + type: 'GITHUB', + location: 'https://github.com/test-owner/test-repo.git', + auth: { + type: 'CODESTAR_CONNECTIONS', + resource: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/test-connection-id', + }, + }; + + project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codestar-connections:UseConnection'], + resources: ['*'], + })); + + // THEN + const template = Template.fromStack(stack); + template.hasResource('AWS::CodeBuild::Project', { + DependsOn: Match.arrayWith([ + Match.stringLikeRegexp('ProjectCodeConnectionsPolicy.*'), + ]), + }); +}); + +test('CodeBuild project without CODECONNECTIONS source auth does not add extra DependsOn', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN - standard GitHub source without CODECONNECTIONS auth + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'test-owner', + repo: 'test-repo', + webhook: false, + }), + }); + + // THEN - CfnProject should NOT have DependsOn for CodeConnectionsPolicy + const template = Template.fromStack(stack); + template.hasResource('AWS::CodeBuild::Project', (res: any) => { + const deps = res.DependsOn || []; + const hasConnPolicyDep = deps.some((d: string) => d.includes('CodeConnectionsPolicy')); + if (hasConnPolicyDep) { + throw new Error(`CodeBuild project without CODECONNECTIONS should not have DependsOn for CodeConnectionsPolicy, but got: ${JSON.stringify(deps)}`); + } + return true; + }); +});