diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.github-codeconnections-auth.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.github-codeconnections-auth.ts new file mode 100644 index 0000000000000..ef4f7bff673e3 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-codebuild/test/integ.github-codeconnections-auth.ts @@ -0,0 +1,34 @@ +import * as cdk from 'aws-cdk-lib'; +import * as codebuild from 'aws-cdk-lib/aws-codebuild'; + +class GitHubCodeConnectionsAuthTestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string) { + super(scope, id); + + // Repository-level source with CodeConnections auth + new codebuild.Project(this, 'RepoProject', { + source: codebuild.Source.gitHub({ + owner: 'awslabs', + repo: 'aws-cdk', + connectionArn: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + webhookFilters: [ + codebuild.FilterGroup.inEventOf(codebuild.EventAction.WORKFLOW_JOB_QUEUED), + ], + }), + }); + + // Organization-level source with CodeConnections auth + new codebuild.Project(this, 'OrgProject', { + source: codebuild.Source.gitHub({ + owner: 'awslabs', + connectionArn: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + }), + }); + } +} + +const app = new cdk.App(); + +new GitHubCodeConnectionsAuthTestStack(app, 'codebuild-github-codeconnections-auth'); + +app.synth(); diff --git a/packages/aws-cdk-lib/aws-codebuild/README.md b/packages/aws-cdk-lib/aws-codebuild/README.md index 9e06119f06c48..9ab27a6eedbbd 100644 --- a/packages/aws-cdk-lib/aws-codebuild/README.md +++ b/packages/aws-cdk-lib/aws-codebuild/README.md @@ -99,6 +99,24 @@ Example: aws codebuild import-source-credentials --server-type GITHUB --auth-type PERSONAL_ACCESS_TOKEN --token ``` +Alternatively, you can use a CodeConnections connection for GitHub App authentication: + +```ts +const gitHubSource = codebuild.Source.gitHub({ + owner: 'awslabs', + repo: 'aws-cdk', + connectionArn: 'arn:aws:codeconnections:us-east-1:123456789012:connection/your-connection-id', + webhookFilters: [ + codebuild.FilterGroup + .inEventOf(codebuild.EventAction.WORKFLOW_JOB_QUEUED), + ], +}); +``` + +When `connectionArn` is provided, the source uses CodeConnections (GitHub App) authentication +instead of OAuth or personal access token credentials. The required IAM permissions +for the connection are automatically granted to the project's role. + ### `BitBucketSource` This source type can be used to build code from a BitBucket repository. diff --git a/packages/aws-cdk-lib/aws-codebuild/lib/source.ts b/packages/aws-cdk-lib/aws-codebuild/lib/source.ts index 06c788622f2c3..99c853ca44c85 100644 --- a/packages/aws-cdk-lib/aws-codebuild/lib/source.ts +++ b/packages/aws-cdk-lib/aws-codebuild/lib/source.ts @@ -762,6 +762,20 @@ export interface GitHubSourceProps extends CommonGithubSourceProps { * @default undefined will create an organization webhook */ readonly repo?: string; + + /** + * The ARN of the CodeConnections connection to use for authentication. + * + * When provided, the source will use CodeConnections (GitHub App) authentication + * instead of the default OAuth or personal access token credentials. + * + * The required IAM permissions for the connection will be automatically granted + * to the project's role. + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/connections-github-app.html + * @default - the source will use the default credentials configured for GitHub in the account + */ + readonly connectionArn?: string; } /** @@ -771,20 +785,35 @@ class GitHubSource extends CommonGithubSource { public readonly type = GITHUB_SOURCE_TYPE; private readonly sourceLocation: string; private readonly organization?: string; + private readonly connectionArn?: string; protected readonly webhookFilters: FilterGroup[]; constructor(props: GitHubSourceProps) { super(props); this.organization = props.repo === undefined ? props.owner : undefined; this.webhookFilters = props.webhookFilters ?? (this.organization ? [FilterGroup.inEventOf(EventAction.WORKFLOW_JOB_QUEUED)] : []); this.sourceLocation = this.organization ? 'CODEBUILD_DEFAULT_WEBHOOK_SOURCE_LOCATION' : `https://github.com/${props.owner}/${props.repo}.git`; + this.connectionArn = props.connectionArn; } public bind(_scope: Construct, project: IProject): SourceConfig { + if (this.connectionArn) { + project.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + 'codeconnections:UseConnection', + ], + resources: [this.connectionArn], + })); + } + const superConfig = super.bind(_scope, project); return { sourceProperty: { ...superConfig.sourceProperty, location: this.sourceLocation, + auth: this.connectionArn ? { + type: 'CODECONNECTIONS', + resource: this.connectionArn, + } : undefined, }, sourceVersion: superConfig.sourceVersion, buildTriggers: this.organization 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 036748bd16de4..04f47d67cb464 100644 --- a/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts +++ b/packages/aws-cdk-lib/aws-codebuild/test/project.test.ts @@ -252,6 +252,134 @@ describe('GitHub source', () => { }); }); + test('can create GitHub source with CodeConnections auth', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + repo: 'testrepo', + connectionArn: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + webhookFilters: [ + codebuild.FilterGroup.inEventOf(codebuild.EventAction.WORKFLOW_JOB_QUEUED), + ], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Source: { + Type: 'GITHUB', + Location: 'https://github.com/testowner/testrepo.git', + Auth: { + Type: 'CODECONNECTIONS', + Resource: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + }, + }, + Triggers: { + Webhook: true, + FilterGroups: [ + [ + { + Type: 'EVENT', + Pattern: 'WORKFLOW_JOB_QUEUED', + }, + ], + ], + }, + }); + }); + + test('can create organizational webhook with CodeConnections auth', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + connectionArn: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Source: { + Type: 'GITHUB', + Location: 'CODEBUILD_DEFAULT_WEBHOOK_SOURCE_LOCATION', + Auth: { + Type: 'CODECONNECTIONS', + Resource: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + }, + }, + Triggers: { + Webhook: true, + ScopeConfiguration: { + Name: 'testowner', + }, + FilterGroups: [ + [ + { + Type: 'EVENT', + Pattern: 'WORKFLOW_JOB_QUEUED', + }, + ], + ], + }, + }); + }); + + test('CodeConnections auth grants required IAM permissions', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + repo: 'testrepo', + connectionArn: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'codeconnections:UseConnection', + Effect: 'Allow', + Resource: 'arn:aws:codeconnections:us-east-1:123456789012:connection/test-connection-id', + }), + ]), + }, + }); + }); + + test('GitHub source without connectionArn does not set auth', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + repo: 'testrepo', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Source: { + Type: 'GITHUB', + Location: 'https://github.com/testowner/testrepo.git', + Auth: Match.absent(), + }, + }); + }); + test('can be added to a CodePipeline', () => { const stack = new cdk.Stack(); const project = new codebuild.Project(stack, 'Project', {