Add secret ref validation
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as exec from "@actions/exec";
|
import * as exec from "@actions/exec";
|
||||||
import { read, setClientInfo } from "@1password/op-js";
|
import { read, setClientInfo } from "@1password/op-js";
|
||||||
import { createClient } from "@1password/sdk";
|
import { createClient, Secrets } from "@1password/sdk";
|
||||||
import {
|
import {
|
||||||
extractSecret,
|
extractSecret,
|
||||||
loadSecrets,
|
loadSecrets,
|
||||||
@@ -25,6 +25,9 @@ jest.mock("@actions/exec", () => ({
|
|||||||
jest.mock("@1password/op-js");
|
jest.mock("@1password/op-js");
|
||||||
jest.mock("@1password/sdk", () => ({
|
jest.mock("@1password/sdk", () => ({
|
||||||
createClient: jest.fn(),
|
createClient: jest.fn(),
|
||||||
|
Secrets: {
|
||||||
|
validateSecretReference: jest.fn(),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -170,11 +173,24 @@ describe("loadSecrets when using Connect", () => {
|
|||||||
expect(core.exportVariable).not.toHaveBeenCalled();
|
expect(core.exportVariable).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails with clear message when a secret reference is invalid", async () => {
|
||||||
|
(Secrets.validateSecretReference as jest.Mock).mockImplementationOnce(
|
||||||
|
() => {
|
||||||
|
throw new Error("invalid reference format");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
process.env.MOCK_SECRET = "op://bad/invalid-ref";
|
||||||
|
|
||||||
|
await expect(loadSecrets(true)).rejects.toThrow(
|
||||||
|
"Invalid secret reference(s): MOCK_SECRET",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe("core.exportVariable", () => {
|
describe("core.exportVariable", () => {
|
||||||
it("is called when shouldExportEnv is true", async () => {
|
it("is called when shouldExportEnv is true", async () => {
|
||||||
await loadSecrets(true);
|
await loadSecrets(true);
|
||||||
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledTimes(1);
|
expect(core.exportVariable).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is not called when shouldExportEnv is false", async () => {
|
it("is not called when shouldExportEnv is false", async () => {
|
||||||
@@ -334,6 +350,40 @@ describe("loadSecrets when using Service Account", () => {
|
|||||||
expect(core.setSecret).toHaveBeenCalledTimes(3);
|
expect(core.setSecret).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("secret reference validation", () => {
|
||||||
|
it("fails with clear message when a secret reference is invalid", async () => {
|
||||||
|
process.env.MY_SECRET = "op://invalid/ref/form";
|
||||||
|
(Secrets.validateSecretReference as jest.Mock).mockImplementationOnce(
|
||||||
|
() => {
|
||||||
|
throw new Error("invalid reference format");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(loadSecrets(true)).rejects.toThrow(
|
||||||
|
"Invalid secret reference(s): MY_SECRET",
|
||||||
|
);
|
||||||
|
expect(mockResolve).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates all refs before resolving any secrets", async () => {
|
||||||
|
process.env.MY_SECRET = "op://vault/item/field";
|
||||||
|
process.env.OTHER = "op://vault/other/item";
|
||||||
|
(Secrets.validateSecretReference as jest.Mock).mockImplementation(
|
||||||
|
(ref: string) => {
|
||||||
|
if (ref === "op://vault/other/item") {
|
||||||
|
throw new Error("invalid");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
mockResolve.mockResolvedValue("value1");
|
||||||
|
|
||||||
|
await expect(loadSecrets(false)).rejects.toThrow(
|
||||||
|
"Invalid secret reference(s): OTHER",
|
||||||
|
);
|
||||||
|
expect(mockResolve).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("unsetPrevious", () => {
|
describe("unsetPrevious", () => {
|
||||||
|
|||||||
29
src/utils.ts
29
src/utils.ts
@@ -1,7 +1,7 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as exec from "@actions/exec";
|
import * as exec from "@actions/exec";
|
||||||
import { read, setClientInfo, semverToInt } from "@1password/op-js";
|
import { read, setClientInfo, semverToInt } from "@1password/op-js";
|
||||||
import { createClient } from "@1password/sdk";
|
import { createClient, Secrets } from "@1password/sdk";
|
||||||
import { version } from "../package.json";
|
import { version } from "../package.json";
|
||||||
import {
|
import {
|
||||||
authErr,
|
authErr,
|
||||||
@@ -37,6 +37,29 @@ export const getEnvVarNamesWithSecretRefs = (): string[] =>
|
|||||||
process.env[key]?.startsWith("op://"),
|
process.env[key]?.startsWith("op://"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const validateSecretRefs = (envNames: string[]): void => {
|
||||||
|
const invalid: string[] = [];
|
||||||
|
|
||||||
|
for (const envName of envNames) {
|
||||||
|
const ref = process.env[envName];
|
||||||
|
if (!ref) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Secrets.validateSecretReference(ref);
|
||||||
|
} catch {
|
||||||
|
invalid.push(envName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw an error if any secret references are invalid
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
const names = invalid.join(", ");
|
||||||
|
throw new Error(`Invalid secret reference(s): ${names}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setResolvedSecret = (
|
const setResolvedSecret = (
|
||||||
envName: string,
|
envName: string,
|
||||||
secretValue: string,
|
secretValue: string,
|
||||||
@@ -102,6 +125,8 @@ const loadSecretsViaConnect = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const envs = res.stdout.replace(/\n+$/g, "").split(/\r?\n/);
|
const envs = res.stdout.replace(/\n+$/g, "").split(/\r?\n/);
|
||||||
|
validateSecretRefs(envs);
|
||||||
|
|
||||||
for (const envName of envs) {
|
for (const envName of envs) {
|
||||||
extractSecret(envName, shouldExportEnv);
|
extractSecret(envName, shouldExportEnv);
|
||||||
}
|
}
|
||||||
@@ -119,6 +144,8 @@ const loadSecretsViaServiceAccount = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateSecretRefs(envs);
|
||||||
|
|
||||||
const token = process.env[envServiceAccountToken];
|
const token = process.env[envServiceAccountToken];
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error(authErr);
|
throw new Error(authErr);
|
||||||
|
|||||||
Reference in New Issue
Block a user