From 9e1bb89fa360c756bc351872963a59336803e38f Mon Sep 17 00:00:00 2001 From: Beth Brennan Date: Wed, 15 Feb 2023 14:46:19 -0500 Subject: [PATCH] Validate secrets passed in to reusable workflow job --- .../src/model/converter/job.ts | 11 +++++ .../src/model/converter/job/inputs.ts | 2 +- .../src/model/converter/job/secrets.ts | 43 +++++++++++++++++++ .../src/model/converter/referencedWorkflow.ts | 9 ++++ .../src/model/workflow-template.ts | 3 ++ ...reusable-workflow-job-secrets-required.yml | 1 - ...eusable-workflow-job-secrets-undefined.yml | 1 - .../reader/reusable-workflow-secrets.yml | 1 - 8 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 actions-workflow-parser/src/model/converter/job/secrets.ts diff --git a/actions-workflow-parser/src/model/converter/job.ts b/actions-workflow-parser/src/model/converter/job.ts index 5a7f335..a9d5163 100644 --- a/actions-workflow-parser/src/model/converter/job.ts +++ b/actions-workflow-parser/src/model/converter/job.ts @@ -21,6 +21,8 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token: let steps: Step[] = []; let workflowJobRef: StringToken | undefined; let workflowJobInputs: MappingToken | undefined; + let inheritSecrets = false; + let workflowJobSecrets: MappingToken | undefined; for (const item of token) { const propertyName = item.key.assertString("job property name"); @@ -95,6 +97,12 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token: case "with": workflowJobInputs = item.value.assertMapping("uses-with value"); break; + case "secrets": + if (isString(item.value) && item.value.value === "inherit") { + inheritSecrets = true; + } else { + workflowJobSecrets = item.value.assertMapping("uses-secrets value"); + } } } @@ -108,6 +116,9 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token: ref: workflowJobRef, "input-definitions": undefined, "input-values": workflowJobInputs, + "secret-definitions": undefined, + "secret-values": workflowJobSecrets, + "inherit-secrets": inheritSecrets || undefined, outputs: undefined, concurrency, strategy diff --git a/actions-workflow-parser/src/model/converter/job/inputs.ts b/actions-workflow-parser/src/model/converter/job/inputs.ts index ebd9467..f8d6c57 100644 --- a/actions-workflow-parser/src/model/converter/job/inputs.ts +++ b/actions-workflow-parser/src/model/converter/job/inputs.ts @@ -39,7 +39,7 @@ export function convertWorkflowJobInputs(context: TemplateContext, job: Reusable } } -function createTokenMap(mapping: MappingToken | undefined, description: string): TokenMap | undefined { +export function createTokenMap(mapping: MappingToken | undefined, description: string): TokenMap | undefined { if (!mapping) { return undefined; } diff --git a/actions-workflow-parser/src/model/converter/job/secrets.ts b/actions-workflow-parser/src/model/converter/job/secrets.ts new file mode 100644 index 0000000..bc4c9a5 --- /dev/null +++ b/actions-workflow-parser/src/model/converter/job/secrets.ts @@ -0,0 +1,43 @@ +import {TemplateContext} from "../../../templates/template-context"; +import {NullToken} from "../../../templates/tokens"; +import {ReusableWorkflowJob} from "../../workflow-template"; +import {createTokenMap} from "./inputs"; + +export function convertWorkflowJobSecrets(context: TemplateContext, job: ReusableWorkflowJob) { + // No validation if job passes all secrets + if (!!job["inherit-secrets"]) { + return; + } + + const secretDefinitions = createTokenMap( + job["secret-definitions"]?.assertMapping("workflow job secret definitions"), + "secrets" + ); + + const secretValues = createTokenMap(job["secret-values"]?.assertMapping("workflow job secret values"), "secrets"); + + if (secretDefinitions !== undefined) { + for (const [_, [name, value]] of secretDefinitions) { + if (value instanceof NullToken) { + continue; + } + + const secretSpec = createTokenMap(value.assertMapping(`secret ${name}`), `secret ${name} key`)!; + + const required = secretSpec.get("required")?.[1].assertBoolean(`secret ${name} required`).value; + if (required) { + if (secretValues == undefined || !secretValues.has(name.toLowerCase())) { + context.error(job.ref, `Secret ${name} is required, but not provided while calling.`); + } + } + } + } + + if (secretValues !== undefined) { + for (const [_, [name, value]] of secretValues) { + if (!secretDefinitions?.has(name.toLowerCase())) { + context.error(value, `Invalid secret, ${name} is not defined in the referenced workflow.`); + } + } + } +} diff --git a/actions-workflow-parser/src/model/converter/referencedWorkflow.ts b/actions-workflow-parser/src/model/converter/referencedWorkflow.ts index 9de9b49..00a1627 100644 --- a/actions-workflow-parser/src/model/converter/referencedWorkflow.ts +++ b/actions-workflow-parser/src/model/converter/referencedWorkflow.ts @@ -4,6 +4,7 @@ import {TokenType} from "../../templates/tokens/types"; import {ReusableWorkflowJob} from "../workflow-template"; import {handleTemplateTokenErrors} from "./handle-errors"; import {convertWorkflowJobInputs} from "./job/inputs"; +import {convertWorkflowJobSecrets} from "./job/secrets"; import {convertJobs} from "./jobs"; export function convertReferencedWorkflow( @@ -41,6 +42,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken const event = on.assertString("Reference workflow on value").value; if (event === "workflow_call") { handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job)); + handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job)); return; } break; @@ -52,6 +54,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken const event = eventToken.assertString(`Reference workflow on value ${eventToken}`).value; if (event === "workflow_call") { handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job)); + handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job)); return; } } @@ -69,6 +72,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken if (pair.value.templateTokenType === TokenType.Null) { handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job)); + handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job)); return; } @@ -83,10 +87,15 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken case "outputs": job.outputs = definition.value.assertMapping(`on-workflow_call-${definition.key}`); break; + + case "secrets": + job["secret-definitions"] = definition.value.assertMapping(`on-workflow_call-${definition.key}`); + break; } } handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job)); + handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job)); return; } break; diff --git a/actions-workflow-parser/src/model/workflow-template.ts b/actions-workflow-parser/src/model/workflow-template.ts index 660fdea..ae0ecbb 100644 --- a/actions-workflow-parser/src/model/workflow-template.ts +++ b/actions-workflow-parser/src/model/workflow-template.ts @@ -60,6 +60,9 @@ export type ReusableWorkflowJob = BaseJob & { ref: StringToken; "input-definitions"?: MappingToken; "input-values"?: MappingToken; + "secret-definitions"?: MappingToken; + "secret-values"?: MappingToken; + "inherit-secrets"?: boolean; jobs?: WorkflowJob[]; }; diff --git a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml index 6015a83..1bd2c84 100644 --- a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml +++ b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: diff --git a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml index 97ad0e7..48d9816 100644 --- a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml +++ b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: diff --git a/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml b/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml index 7514011..7714103 100644 --- a/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml +++ b/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: