Add secret ref validation

This commit is contained in:
Jill Regan
2026-02-18 17:24:55 -05:00
parent 24235f3b6b
commit 6911316fe3
2 changed files with 80 additions and 3 deletions

View File

@@ -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", () => {

View File

@@ -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);