Merge pull request #135 from 1Password/jill/validate-secret-reference

Add secret ref validation
This commit is contained in:
Jill Regan
2026-02-23 11:42:24 -05:00
committed by GitHub
4 changed files with 90 additions and 2 deletions

View File

@@ -105,6 +105,22 @@ jobs:
shell: bash shell: bash
run: ./tests/assert-env-unset.sh run: ./tests/assert-env-unset.sh
- name: Load secrets (invalid ref - expect failure)
id: load_invalid
continue-on-error: true
uses: ./
env:
BAD_REF: "op://x"
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
with:
export-env: true
- name: Assert invalid ref failed
shell: bash
run: ./tests/assert-invalid-ref-failed.sh
env:
STEP_OUTCOME: ${{ steps.load_invalid.outcome }}
test-connect: test-connect:
name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }}) name: Connect (ubuntu-latest, ${{ matrix.version }}, export-env=${{ matrix.export-env }})
runs-on: ubuntu-latest runs-on: ubuntu-latest

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,10 @@ 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(),
// eslint-disable-next-line @typescript-eslint/naming-convention
Secrets: {
validateSecretReference: jest.fn(),
},
})); }));
beforeEach(() => { beforeEach(() => {
@@ -349,6 +353,39 @@ 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://x";
(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");
}
},
);
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,32 @@ const getEnvVarNamesWithSecretRefs = (): string[] =>
process.env[key]?.startsWith("op://"), process.env[key]?.startsWith("op://"),
); );
const validateSecretRefs = (envNames: string[]): void => {
const invalid: { name: string; message: string }[] = [];
for (const envName of envNames) {
const ref = process.env[envName];
if (!ref) {
continue;
}
try {
Secrets.validateSecretReference(ref);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
invalid.push({ name: envName, message });
}
}
// Throw an error if any secret references are invalid
if (invalid.length > 0) {
const details = invalid
.map(({ name, message }) => `${name}: ${message}`)
.join("; ");
throw new Error(`Invalid secret reference(s): ${details}`);
}
};
const setResolvedSecret = ( const setResolvedSecret = (
envName: string, envName: string,
secretValue: string, secretValue: string,
@@ -108,6 +134,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);

View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -e
if [ "$STEP_OUTCOME" != "failure" ]; then
echo "Expected action to fail on invalid ref, got: $STEP_OUTCOME"
exit 1
fi
echo "Action correctly failed on invalid ref"