diff --git a/__tests__/buildx/inputs.test.ts b/__tests__/buildx/inputs.test.ts index 15b15fc..6e16f24 100644 --- a/__tests__/buildx/inputs.test.ts +++ b/__tests__/buildx/inputs.test.ts @@ -50,7 +50,7 @@ afterEach(() => { rimraf.sync(tmpDir); }); -describe('getBuildImageID', () => { +describe('resolveBuildImageID', () => { it('matches', async () => { const buildx = new Buildx({ context: new Context() @@ -58,31 +58,31 @@ describe('getBuildImageID', () => { const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9'; const imageIDFile = buildx.inputs.getBuildImageIDFilePath(); await fs.writeFileSync(imageIDFile, imageID); - const expected = buildx.inputs.getBuildImageID(); + const expected = buildx.inputs.resolveBuildImageID(); expect(expected).toEqual(imageID); }); }); -describe('getBuildMetadata', () => { +describe('resolveBuildMetadata', () => { it('matches', async () => { const buildx = new Buildx({ context: new Context() }); const metadataFile = buildx.inputs.getBuildMetadataFilePath(); await fs.writeFileSync(metadataFile, metadata); - const expected = buildx.inputs.getBuildMetadata(); + const expected = buildx.inputs.resolveBuildMetadata(); expect(expected).toEqual(metadata); }); }); -describe('getDigest', () => { +describe('resolveDigest', () => { it('matches', async () => { const buildx = new Buildx({ context: new Context() }); const metadataFile = buildx.inputs.getBuildMetadataFilePath(); await fs.writeFileSync(metadataFile, metadata); - const expected = buildx.inputs.getDigest(); + const expected = buildx.inputs.resolveDigest(); expect(expected).toEqual('sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c'); }); }); @@ -136,7 +136,7 @@ describe('getProvenanceInput', () => { }); }); -describe('getProvenanceAttrs', () => { +describe('resolveProvenanceAttrs', () => { // prettier-ignore test.each([ [ @@ -163,11 +163,11 @@ describe('getProvenanceAttrs', () => { const buildx = new Buildx({ context: new Context() }); - expect(buildx.inputs.getProvenanceAttrs(input)).toEqual(expected); + expect(buildx.inputs.resolveProvenanceAttrs(input)).toEqual(expected); }); }); -describe('generateBuildSecret', () => { +describe('resolveBuildSecret', () => { test.each([ ['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', null], ['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', null], @@ -184,9 +184,9 @@ describe('generateBuildSecret', () => { }); let secret: string; if (file) { - secret = buildx.inputs.generateBuildSecretFile(kvp); + secret = buildx.inputs.resolveBuildSecretFile(kvp); } else { - secret = buildx.inputs.generateBuildSecretString(kvp); + secret = buildx.inputs.resolveBuildSecretString(kvp); } expect(secret).toEqual(`id=${exKey},src=${tmpName}`); expect(fs.readFileSync(tmpName, 'utf-8')).toEqual(exValue); diff --git a/__tests__/github.test.ts b/__tests__/github.test.ts index b98b6f7..08c73a4 100644 --- a/__tests__/github.test.ts +++ b/__tests__/github.test.ts @@ -17,6 +17,7 @@ import {describe, expect, jest, it, beforeEach, afterEach} from '@jest/globals'; import * as fs from 'fs'; import * as path from 'path'; +import * as core from '@actions/core'; import {GitHub} from '../src/github'; import {GitHubRepo} from '../src/types/github'; @@ -101,12 +102,52 @@ describe('actionsRuntimeToken', () => { }); it('empty', async () => { process.env.ACTIONS_RUNTIME_TOKEN = ''; - expect(GitHub.actionsRuntimeToken).toEqual({}); + expect(GitHub.actionsRuntimeToken).toBeUndefined(); + }); + it('malformed', async () => { + process.env.ACTIONS_RUNTIME_TOKEN = 'foo'; + expect(() => { + GitHub.actionsRuntimeToken; + }).toThrowError(); }); it('fixture', async () => { process.env.ACTIONS_RUNTIME_TOKEN = fs.readFileSync(path.join(__dirname, 'fixtures', 'runtimeToken.txt')).toString().trim(); const runtimeToken = GitHub.actionsRuntimeToken; - expect(runtimeToken.ac).toEqual('[{"Scope":"refs/heads/master","Permission":3}]'); - expect(runtimeToken.iss).toEqual('vstoken.actions.githubusercontent.com'); + expect(runtimeToken?.ac).toEqual('[{"Scope":"refs/heads/master","Permission":3}]'); + expect(runtimeToken?.iss).toEqual('vstoken.actions.githubusercontent.com'); + }); +}); + +describe('printActionsRuntimeTokenACs', () => { + const originalEnv = process.env; + beforeEach(() => { + jest.resetModules(); + process.env = { + ...originalEnv + }; + }); + afterEach(() => { + process.env = originalEnv; + }); + it('empty', async () => { + const warnSpy = jest.spyOn(core, 'warning'); + process.env.ACTIONS_RUNTIME_TOKEN = ''; + await GitHub.printActionsRuntimeTokenACs(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(`ACTIONS_RUNTIME_TOKEN not set`); + }); + it('malformed', async () => { + const warnSpy = jest.spyOn(core, 'warning'); + process.env.ACTIONS_RUNTIME_TOKEN = 'foo'; + await GitHub.printActionsRuntimeTokenACs(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(`Cannot parse Actions Runtime Token: Invalid token specified: Cannot read properties of undefined (reading 'replace')`); + }); + it('refs/heads/master', async () => { + const infoSpy = jest.spyOn(core, 'info'); + process.env.ACTIONS_RUNTIME_TOKEN = fs.readFileSync(path.join(__dirname, 'fixtures', 'runtimeToken.txt')).toString().trim(); + await GitHub.printActionsRuntimeTokenACs(); + expect(infoSpy).toHaveBeenCalledTimes(1); + expect(infoSpy).toHaveBeenCalledWith(`refs/heads/master: read/write`); }); }); diff --git a/src/buildx/inputs.ts b/src/buildx/inputs.ts index a5cae52..c07ea05 100644 --- a/src/buildx/inputs.ts +++ b/src/buildx/inputs.ts @@ -36,7 +36,7 @@ export class Inputs { return path.join(this.context.tmpDir(), 'metadata-file').split(path.sep).join(path.posix.sep); } - public getBuildImageID(): string | undefined { + public resolveBuildImageID(): string | undefined { const iidFile = this.getBuildImageIDFilePath(); if (!fs.existsSync(iidFile)) { return undefined; @@ -44,7 +44,7 @@ export class Inputs { return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim(); } - public getBuildMetadata(): string | undefined { + public resolveBuildMetadata(): string | undefined { const metadataFile = this.getBuildMetadataFilePath(); if (!fs.existsSync(metadataFile)) { return undefined; @@ -56,8 +56,8 @@ export class Inputs { return content; } - public getDigest(): string | undefined { - const metadata = this.getBuildMetadata(); + public resolveDigest(): string | undefined { + const metadata = this.resolveBuildMetadata(); if (metadata === undefined) { return undefined; } @@ -68,15 +68,15 @@ export class Inputs { return undefined; } - public generateBuildSecretString(kvp: string): string { - return this.generateBuildSecret(kvp, false); + public resolveBuildSecretString(kvp: string): string { + return this.resolveBuildSecret(kvp, false); } - public generateBuildSecretFile(kvp: string): string { - return this.generateBuildSecret(kvp, true); + public resolveBuildSecretFile(kvp: string): string { + return this.resolveBuildSecret(kvp, true); } - public generateBuildSecret(kvp: string, file: boolean): string { + public resolveBuildSecret(kvp: string, file: boolean): string { const delimiterIndex = kvp.indexOf('='); const key = kvp.substring(0, delimiterIndex); let value = kvp.substring(delimiterIndex + 1); @@ -105,11 +105,11 @@ export class Inputs { return core.getBooleanInput(name) ? `builder-id=${builderID}` : 'false'; } catch (err) { // not a valid boolean, so we assume it's a string - return this.getProvenanceAttrs(input); + return this.resolveProvenanceAttrs(input); } } - public getProvenanceAttrs(input: string): string { + public resolveProvenanceAttrs(input: string): string { if (!input) { return `builder-id=${this.context.provenanceBuilderID}`; } diff --git a/src/github.ts b/src/github.ts index 232f6f4..e7e0404 100644 --- a/src/github.ts +++ b/src/github.ts @@ -15,11 +15,12 @@ */ import {GitHub as Octokit} from '@actions/github/lib/utils'; +import * as core from '@actions/core'; import * as github from '@actions/github'; import {Context} from '@actions/github/lib/context'; import jwt_decode from 'jwt-decode'; -import {GitHubActionsRuntimeToken, GitHubRepo} from './types/github'; +import {GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo} from './types/github'; export interface GitHubOpts { token?: string; @@ -48,8 +49,43 @@ export class GitHub { return process.env.GITHUB_API_URL || 'https://api.github.com'; } - static get actionsRuntimeToken(): GitHubActionsRuntimeToken { + static get actionsRuntimeToken(): GitHubActionsRuntimeToken | undefined { const token = process.env['ACTIONS_RUNTIME_TOKEN'] || ''; - return token ? jwt_decode(token) : {}; + return token ? jwt_decode(token) : undefined; + } + + public static async printActionsRuntimeTokenACs() { + let jwt: GitHubActionsRuntimeToken | undefined; + try { + jwt = GitHub.actionsRuntimeToken; + } catch (e) { + core.warning(`Cannot parse Actions Runtime Token: ${e.message}`); + return; + } + if (!jwt) { + core.warning(`ACTIONS_RUNTIME_TOKEN not set`); + return; + } + try { + >JSON.parse(`${jwt.ac}`).forEach(ac => { + let permission: string; + switch (ac.Permission) { + case 1: + permission = 'read'; + break; + case 2: + permission = 'write'; + break; + case 3: + permission = 'read/write'; + break; + default: + permission = `unimplemented (${ac.Permission})`; + } + core.info(`${ac.Scope}: ${permission}`); + }); + } catch (e) { + core.warning(`Cannot parse Actions Runtime Token Access Controls: ${e.message}`); + } } } diff --git a/src/types/github.ts b/src/types/github.ts index 833a405..a7b9c71 100644 --- a/src/types/github.ts +++ b/src/types/github.ts @@ -29,3 +29,8 @@ export type GitHubRepo = OctoOpenApiTypes['schemas']['repository']; export interface GitHubActionsRuntimeToken extends JwtPayload { ac?: string; } + +export interface GitHubActionsRuntimeTokenAC { + Scope: string; + Permission: number; +}