From 5f58fdc8a48c28eab69b490acb486868d19cbfca Mon Sep 17 00:00:00 2001 From: Adam Bigelow <58624145+a-bigelow@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:35:04 -0400 Subject: [PATCH] feat: customizable `permissions` in `GitHubActionStep` (#1017) This PR adds configuration passthrough for the `permissions` object that `GitHubActionStep` uses under the hood. The default remains `contents: write`. I have also added a snapshot test showing that the change works, and made a small modification to the README. Fixes #731 --- API.md | 32 +++ README.md | 7 + src/pipeline.ts | 2 +- src/steps/github-action-step.ts | 10 +- test/__snapshots__/github.test.ts.snap | 263 +++++++++++++++++++++++++ test/github.test.ts | 101 +++++++++- 6 files changed, 411 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index f110fdb3..62abf895 100644 --- a/API.md +++ b/API.md @@ -441,6 +441,9 @@ If you want to call a GitHub Action in a step, you can utilize the `GitHubAction The `jobSteps` array is placed into the pipeline job at the relevant `jobs..steps` as [documented here](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsteps). +GitHub Actions Job permissions can be modified by passing the `permissions` object to `GitHubActionStep`. +The default set of permissions is simply `contents: write`. + In this example, ```ts @@ -461,6 +464,10 @@ const pipeline = new GitHubWorkflow(app, 'Pipeline', { const stage = new MyStage(app, 'Beta', { env: BETA_ENV }); pipeline.addStage(stage, { pre: [new GitHubActionStep('PreBetaDeployAction', { + permissions: { + idToken: JobPermission.WRITE, + contents: JobPermission.WRITE, + }, jobSteps: [ { name: 'Checkout', @@ -2314,6 +2321,7 @@ const gitHubActionStepProps: GitHubActionStepProps = { ... } | --- | --- | --- | | jobSteps | JobStep[] | The Job steps. | | env | {[ key: string ]: string} | Environment variables to set. | +| permissions | JobPermissions | Permissions for the GitHub Action step. | --- @@ -2341,6 +2349,19 @@ Environment variables to set. --- +##### `permissions`Optional + +```typescript +public readonly permissions: JobPermissions; +``` + +- *Type:* JobPermissions +- *Default:* The job receives 'contents: write' permissions. If you set additional permissions and require 'contents: write', it must be provided in your configuration. + +Permissions for the GitHub Action step. + +--- + ### GitHubCommonProps Common properties to extend both StageProps and AddStageOpts. @@ -5568,6 +5589,7 @@ API. For example, if you want `secondStep` to occur after `firstStep`, call | primaryOutput | aws-cdk-lib.pipelines.FileSet | The primary FileSet produced by this Step. | | env | {[ key: string ]: string} | *No description.* | | jobSteps | JobStep[] | *No description.* | +| permissions | JobPermissions | *No description.* | --- @@ -5668,6 +5690,16 @@ public readonly jobSteps: JobStep[]; --- +##### `permissions`Optional + +```typescript +public readonly permissions: JobPermissions; +``` + +- *Type:* JobPermissions + +--- + ### GitHubWave diff --git a/README.md b/README.md index 50317749..69d965c1 100644 --- a/README.md +++ b/README.md @@ -441,6 +441,9 @@ If you want to call a GitHub Action in a step, you can utilize the `GitHubAction The `jobSteps` array is placed into the pipeline job at the relevant `jobs..steps` as [documented here](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsteps). +GitHub Actions Job permissions can be modified by passing the `permissions` object to `GitHubActionStep`. +The default set of permissions is simply `contents: write`. + In this example, ```ts @@ -461,6 +464,10 @@ const pipeline = new GitHubWorkflow(app, 'Pipeline', { const stage = new MyStage(app, 'Beta', { env: BETA_ENV }); pipeline.addStage(stage, { pre: [new GitHubActionStep('PreBetaDeployAction', { + permissions: { + idToken: JobPermission.WRITE, + contents: JobPermission.WRITE, + }, jobSteps: [ { name: 'Checkout', diff --git a/src/pipeline.ts b/src/pipeline.ts index 0a63e7ee..fc32fb54 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -832,7 +832,7 @@ export class GitHubWorkflow extends PipelineBase { definition: { name: step.id, ...this.renderJobSettingParameters(), - permissions: { + permissions: step.permissions ?? { contents: github.JobPermission.WRITE, }, runsOn: this.runner.runsOn, diff --git a/src/steps/github-action-step.ts b/src/steps/github-action-step.ts index e5bfa414..7033ec4f 100644 --- a/src/steps/github-action-step.ts +++ b/src/steps/github-action-step.ts @@ -1,5 +1,5 @@ import { Step } from 'aws-cdk-lib/pipelines'; -import { JobStep } from '../workflows-model'; +import { JobStep, JobPermissions } from '../workflows-model'; export interface GitHubActionStepProps { /** @@ -11,6 +11,12 @@ export interface GitHubActionStepProps { * Environment variables to set. */ readonly env?: Record; + + /** + * Permissions for the GitHub Action step. + * @default The job receives 'contents: write' permissions. If you set additional permissions and require 'contents: write', it must be provided in your configuration. + */ + readonly permissions?: JobPermissions; } /** @@ -19,10 +25,12 @@ export interface GitHubActionStepProps { export class GitHubActionStep extends Step { public readonly env: Record; public readonly jobSteps: JobStep[]; + public readonly permissions?: JobPermissions; constructor(id: string, props: GitHubActionStepProps) { super(id); this.jobSteps = props.jobSteps; this.env = props.env ?? {}; + this.permissions = props.permissions; } } diff --git a/test/__snapshots__/github.test.ts.snap b/test/__snapshots__/github.test.ts.snap index 78ef0c28..d4589a7c 100644 --- a/test/__snapshots__/github.test.ts.snap +++ b/test/__snapshots__/github.test.ts.snap @@ -527,6 +527,269 @@ jobs: " `; +exports[`pipeline with GitHubSteps customizing permissions 1`] = ` +"# AUTOMATICALLY GENERATED FILE, DO NOT EDIT MANUALLY. +# Generated by AWS CDK and [cdk-pipelines-github](https://github.com/cdklabs/cdk-pipelines-github) + +name: deploy +on: + push: + branches: + - main + workflow_dispatch: {} +jobs: + Build-Build: + name: Synthesize + permissions: + contents: read + id-token: none + runs-on: ubuntu-latest + needs: [] + env: {} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build + run: \\"\\" + - name: Upload cdk.out + uses: actions/upload-artifact@v4 + with: + name: cdk.out + path: cdk.out + Assets-FileAsset1: + name: Publish Assets Assets-FileAsset1 + needs: + - Build-Build + permissions: + contents: read + id-token: none + runs-on: ubuntu-latest + outputs: + asset-hash: \${{ steps.Publish.outputs.asset-hash }} + steps: + - name: Download cdk.out + uses: actions/download-artifact@v4 + with: + name: cdk.out + path: github.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-duration-seconds: 1800 + role-skip-session-tagging: true + aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }} + - id: Publish + name: Publish Assets-FileAsset1 + run: /bin/bash ./cdk.out/assembly-MyStage/publish-Assets-FileAsset1-step.sh + Assets-FileAsset2: + name: Publish Assets Assets-FileAsset2 + needs: + - Build-Build + permissions: + contents: read + id-token: none + runs-on: ubuntu-latest + outputs: + asset-hash: \${{ steps.Publish.outputs.asset-hash }} + steps: + - name: Download cdk.out + uses: actions/download-artifact@v4 + with: + name: cdk.out + path: github.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-duration-seconds: 1800 + role-skip-session-tagging: true + aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }} + - id: Publish + name: Publish Assets-FileAsset2 + run: /bin/bash ./cdk.out/assembly-MyStage/publish-Assets-FileAsset2-step.sh + MyStage-Test-Step: + name: Test-Step + permissions: + id-token: write + contents: write + runs-on: ubuntu-latest + needs: + - Build-Build + env: {} + steps: + - name: Hello World + run: echo hello + MyStage-MyStack-Deploy: + name: Deploy MyStageMyStackD5720EA1 + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + - Assets-FileAsset2 + - MyStage-Test-Step + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + role-duration-seconds: 1800 + role-skip-session-tagging: true + aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: arn:aws:iam::111111111111:role/cdk-hnb659fds-deploy-role-111111111111-us-east-1 + role-external-id: Pipeline + - id: Deploy + uses: aws-actions/aws-cloudformation-github-deploy@v1 + with: + name: MyStage-MyStack + template: https://cdk-hnb659fds-assets-111111111111-us-east-1.s3.us-east-1.amazonaws.com/\${{ + needs.Assets-FileAsset1.outputs.asset-hash }}.json + no-fail-on-empty-changeset: \\"1\\" + role-arn: arn:aws:iam::111111111111:role/cdk-hnb659fds-cfn-exec-role-111111111111-us-east-1 +" +`; + +exports[`pipeline with GitHubSteps default permissions 1`] = ` +"# AUTOMATICALLY GENERATED FILE, DO NOT EDIT MANUALLY. +# Generated by AWS CDK and [cdk-pipelines-github](https://github.com/cdklabs/cdk-pipelines-github) + +name: deploy +on: + push: + branches: + - main + workflow_dispatch: {} +jobs: + Build-Build: + name: Synthesize + permissions: + contents: read + id-token: none + runs-on: ubuntu-latest + needs: [] + env: {} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build + run: \\"\\" + - name: Upload cdk.out + uses: actions/upload-artifact@v4 + with: + name: cdk.out + path: cdk.out + Assets-FileAsset1: + name: Publish Assets Assets-FileAsset1 + needs: + - Build-Build + permissions: + contents: read + id-token: none + runs-on: ubuntu-latest + outputs: + asset-hash: \${{ steps.Publish.outputs.asset-hash }} + steps: + - name: Download cdk.out + uses: actions/download-artifact@v4 + with: + name: cdk.out + path: github.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-duration-seconds: 1800 + role-skip-session-tagging: true + aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }} + - id: Publish + name: Publish Assets-FileAsset1 + run: /bin/bash ./cdk.out/assembly-MyStage/publish-Assets-FileAsset1-step.sh + Assets-FileAsset2: + name: Publish Assets Assets-FileAsset2 + needs: + - Build-Build + permissions: + contents: read + id-token: none + runs-on: ubuntu-latest + outputs: + asset-hash: \${{ steps.Publish.outputs.asset-hash }} + steps: + - name: Download cdk.out + uses: actions/download-artifact@v4 + with: + name: cdk.out + path: github.out + - name: Install + run: npm install --no-save cdk-assets + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-duration-seconds: 1800 + role-skip-session-tagging: true + aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }} + - id: Publish + name: Publish Assets-FileAsset2 + run: /bin/bash ./cdk.out/assembly-MyStage/publish-Assets-FileAsset2-step.sh + MyStage-Test-Step: + name: Test-Step + permissions: + contents: write + runs-on: ubuntu-latest + needs: + - Build-Build + env: {} + steps: + - name: Hello World + run: echo hello + MyStage-MyStack-Deploy: + name: Deploy MyStageMyStackD5720EA1 + permissions: + contents: read + id-token: none + needs: + - Build-Build + - Assets-FileAsset1 + - Assets-FileAsset2 + - MyStage-Test-Step + runs-on: ubuntu-latest + steps: + - name: Authenticate Via GitHub Secrets + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + role-duration-seconds: 1800 + role-skip-session-tagging: true + aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: arn:aws:iam::111111111111:role/cdk-hnb659fds-deploy-role-111111111111-us-east-1 + role-external-id: Pipeline + - id: Deploy + uses: aws-actions/aws-cloudformation-github-deploy@v1 + with: + name: MyStage-MyStack + template: https://cdk-hnb659fds-assets-111111111111-us-east-1.s3.us-east-1.amazonaws.com/\${{ + needs.Assets-FileAsset1.outputs.asset-hash }}.json + no-fail-on-empty-changeset: \\"1\\" + role-arn: arn:aws:iam::111111111111:role/cdk-hnb659fds-cfn-exec-role-111111111111-us-east-1 +" +`; + exports[`pipeline with concurrency 1`] = ` "# AUTOMATICALLY GENERATED FILE, DO NOT EDIT MANUALLY. # Generated by AWS CDK and [cdk-pipelines-github](https://github.com/cdklabs/cdk-pipelines-github) diff --git a/test/github.test.ts b/test/github.test.ts index 317302df..65c6256b 100644 --- a/test/github.test.ts +++ b/test/github.test.ts @@ -4,8 +4,8 @@ import { Stack, Stage } from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { ShellStep } from 'aws-cdk-lib/pipelines'; import { GitHubExampleApp } from './example-app'; -import { withTemporaryDirectory, TestApp } from './testutil'; -import { GitHubWorkflow, JsonPatch, Runner, AwsCredentials } from '../src'; +import { TestApp, withTemporaryDirectory } from './testutil'; +import { AwsCredentials, GitHubActionStep, GitHubStage, GitHubWorkflow, JobPermission, JsonPatch, Runner } from '../src'; const fixtures = join(__dirname, 'fixtures'); @@ -287,6 +287,103 @@ test('pipeline with job settings', () => { }); }); +test('pipeline with GitHubSteps customizing permissions', () => { + withTemporaryDirectory((dir) => { + const pipeline = new GitHubWorkflow(app, 'Pipeline', { + workflowPath: `${dir}/.github/workflows/deploy.yml`, + synth: new ShellStep('Build', { + commands: [], + }), + }); + + const wave = pipeline.addGitHubWave('Test-Wave'); + const stage = new GitHubStage(app, 'MyStage', { + env: { account: '111111111111', region: 'us-east-1' }, + }); + + const stack = new Stack(stage, 'MyStack'); + + new lambda.Function(stack, 'Function', { + code: lambda.Code.fromAsset(fixtures), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + }); + + wave.addStageWithGitHubOptions(stage, { + pre: [new GitHubActionStep('Test-Step', { + permissions: { + idToken: JobPermission.WRITE, + contents: JobPermission.WRITE, + }, + jobSteps: [{ + name: 'Hello World', + run: 'echo hello', + }], + })], + }); + + app.synth(); + + const file = readFileSync(pipeline.workflowPath, 'utf-8'); + + expect(file).toMatchSnapshot(); + expect(file).toContain(`MyStage-Test-Step: + name: Test-Step + permissions: + id-token: write + contents: write`); + }); +}); + +test('pipeline with GitHubSteps default permissions', () => { + withTemporaryDirectory((dir) => { + const pipeline = new GitHubWorkflow(app, 'Pipeline', { + workflowPath: `${dir}/.github/workflows/deploy.yml`, + synth: new ShellStep('Build', { + commands: [], + }), + }); + + const wave = pipeline.addGitHubWave('Test-Wave'); + const stage = new GitHubStage(app, 'MyStage', { + env: { account: '111111111111', region: 'us-east-1' }, + }); + + const stack = new Stack(stage, 'MyStack'); + + new lambda.Function(stack, 'Function', { + code: lambda.Code.fromAsset(fixtures), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + }); + + wave.addStageWithGitHubOptions(stage, { + pre: [new GitHubActionStep('Test-Step', { + jobSteps: [{ + name: 'Hello World', + run: 'echo hello', + }], + })], + }); + + app.synth(); + + const file = readFileSync(pipeline.workflowPath, 'utf-8'); + + expect(file).toMatchSnapshot(); + expect(file).toContain(`MyStage-Test-Step: + name: Test-Step + permissions: + contents: write`); + + expect(file).not.toContain(`MyStage-Test-Step: + name: Test-Step + permissions: + id-token: write + contents: write`); + }); +}); + test('single wave/stage/stack', () => { withTemporaryDirectory((dir) => { const pipeline = new GitHubWorkflow(app, 'Pipeline', {