mirror of
https://github.com/1Password/load-secrets-action.git
synced 2026-06-21 14:23:48 +00:00
Add workload identy feature
This commit is contained in:
@@ -20,6 +20,12 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
OP_SERVICE_ACCOUNT_TOKEN:
|
OP_SERVICE_ACCOUNT_TOKEN:
|
||||||
required: true
|
required: true
|
||||||
|
OP_WORKLOAD_ID:
|
||||||
|
required: true
|
||||||
|
OP_ENVIRONMENT_ID:
|
||||||
|
required: true
|
||||||
|
OP_INTEGRATION_KEY:
|
||||||
|
required: true
|
||||||
VAULT:
|
VAULT:
|
||||||
description: "1Password vault name or UUID"
|
description: "1Password vault name or UUID"
|
||||||
required: true
|
required: true
|
||||||
@@ -248,3 +254,77 @@ jobs:
|
|||||||
- name: Assert removed secrets [exported env]
|
- name: Assert removed secrets [exported env]
|
||||||
if: ${{ matrix.export-env }}
|
if: ${{ matrix.export-env }}
|
||||||
run: ./tests/assert-env-unset.sh
|
run: ./tests/assert-env-unset.sh
|
||||||
|
|
||||||
|
test-workload-identity:
|
||||||
|
name: Workload Identity (ubuntu-latest, export-env=${{ matrix.export-env }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Workload Identity exchanges the GitHub OIDC token for 1Password access,
|
||||||
|
# so the job needs permission to request an OIDC token.
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
export-env: [true, false]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ inputs.ref }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build actions
|
||||||
|
run: npm run build:all
|
||||||
|
|
||||||
|
# No ./configure step and no op:// references: Workload Identity authenticates
|
||||||
|
# via OIDC and loads all variables from the configured 1Password environment.
|
||||||
|
- name: Load secrets
|
||||||
|
id: load_secrets
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
export-env: ${{ matrix.export-env }}
|
||||||
|
env:
|
||||||
|
OP_WORKLOAD_ID: ${{ secrets.OP_WORKLOAD_ID }}
|
||||||
|
OP_ENVIRONMENT_ID: ${{ secrets.OP_ENVIRONMENT_ID }}
|
||||||
|
OP_INTEGRATION_KEY: ${{ secrets.OP_INTEGRATION_KEY }}
|
||||||
|
|
||||||
|
- name: Assert test secret values [step output]
|
||||||
|
if: ${{ !matrix.export-env }}
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
ANOTHER_TEST: ${{ steps.load_secrets.outputs.ANOTHER_TEST }}
|
||||||
|
SUPER_SECRET: ${{ steps.load_secrets.outputs.SUPER_SECRET }}
|
||||||
|
TEST_SECRET: ${{ steps.load_secrets.outputs.TEST_SECRET }}
|
||||||
|
run: ./tests/assert-workload-identity.sh
|
||||||
|
|
||||||
|
- name: Assert test secret values [exported env]
|
||||||
|
if: ${{ matrix.export-env }}
|
||||||
|
shell: bash
|
||||||
|
run: ./tests/assert-workload-identity.sh
|
||||||
|
|
||||||
|
- name: Remove secrets [exported env]
|
||||||
|
if: ${{ matrix.export-env }}
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
unset-previous: true
|
||||||
|
|
||||||
|
- name: Assert removed secrets [exported env]
|
||||||
|
if: ${{ matrix.export-env }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for var in ANOTHER_TEST SUPER_SECRET TEST_SECRET; do
|
||||||
|
if [ -n "$(printenv "$var")" ]; then
|
||||||
|
echo "Expected secret $var to be unset"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ jobs:
|
|||||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||||
|
OP_WORKLOAD_ID: ${{ secrets.OP_WORKLOAD_ID }}
|
||||||
|
OP_ENVIRONMENT_ID: ${{ secrets.OP_ENVIRONMENT_ID }}
|
||||||
|
OP_INTEGRATION_KEY: ${{ secrets.OP_INTEGRATION_KEY }}
|
||||||
VAULT: ${{ secrets.VAULT }}
|
VAULT: ${{ secrets.VAULT }}
|
||||||
|
|
||||||
# Post comment on fork PRs after /ok-to-test
|
# Post comment on fork PRs after /ok-to-test
|
||||||
|
|||||||
@@ -88,6 +88,33 @@ When loading SSH keys, you can specify the format using the `ssh-format` query p
|
|||||||
|
|
||||||
For more details on secret reference syntax, see the [1Password CLI documentation](https://developer.1password.com/docs/cli/secret-reference-syntax/#ssh-format-parameter).
|
For more details on secret reference syntax, see the [1Password CLI documentation](https://developer.1password.com/docs/cli/secret-reference-syntax/#ssh-format-parameter).
|
||||||
|
|
||||||
|
## 🧪 Workload Identity (private beta)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Workload Identity is in **private beta**. It's available to invited participants only. [Contact 1Password](https://developer.1password.com/joinslack) if you're interested in joining the beta.
|
||||||
|
|
||||||
|
Instead of a Service Account token or Connect credentials, you can authenticate using Workload Identity, which exchanges your GitHub Actions OIDC token for short-lived 1Password access — no long-lived secret to store. To use it, set all three of the following environment variables (and do not set `OP_SERVICE_ACCOUNT_TOKEN` or the Connect variables):
|
||||||
|
|
||||||
|
```yml
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
hello-world:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write # required for the action to request a GitHub OIDC token
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Load secret
|
||||||
|
id: load_secrets
|
||||||
|
uses: 1password/load-secrets-action@v5beta
|
||||||
|
env:
|
||||||
|
OP_WORKLOAD_ID: ${{ vars.OP_WORKLOAD_ID }}
|
||||||
|
OP_ENVIRONMENT_ID: ${{ vars.OP_ENVIRONMENT_ID }}
|
||||||
|
OP_INTEGRATION_KEY: ${{ secrets.OP_INTEGRATION_KEY }}
|
||||||
|
```
|
||||||
|
|
||||||
|
When Workload Identity is configured, secrets are loaded directly from your environment's variables. You don't need to specify individual `op://` secret references. If only some of the three variables are set, or if they're combined with another authentication method, the action fails with a configuration error.
|
||||||
|
|
||||||
## 💙 Community & Support
|
## 💙 Community & Support
|
||||||
|
|
||||||
- File an [issue](https://github.com/1Password/load-secrets-action/issues) for bugs and feature requests.
|
- File an [issue](https://github.com/1Password/load-secrets-action/issues) for bugs and feature requests.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const jestConfig = {
|
|||||||
"^@actions/core$": "<rootDir>/__mocks__/actions-core.ts",
|
"^@actions/core$": "<rootDir>/__mocks__/actions-core.ts",
|
||||||
"^@actions/tool-cache$": "<rootDir>/__mocks__/actions-tool-cache.ts",
|
"^@actions/tool-cache$": "<rootDir>/__mocks__/actions-tool-cache.ts",
|
||||||
"^@actions/exec$": "<rootDir>/__mocks__/actions-exec.ts",
|
"^@actions/exec$": "<rootDir>/__mocks__/actions-exec.ts",
|
||||||
|
"^@1password/sdk$": "<rootDir>/__mocks__/1password-sdk.ts",
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
".ts": [
|
".ts": [
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
+2931
-33
File diff suppressed because one or more lines are too long
Generated
+16
@@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@1password/op-js": "^0.1.11",
|
"@1password/op-js": "^0.1.11",
|
||||||
|
"@1password/sdk": "0.5.0-beta.1",
|
||||||
"@actions/core": "^3.0.0",
|
"@actions/core": "^3.0.0",
|
||||||
"@actions/exec": "^3.0.0",
|
"@actions/exec": "^3.0.0",
|
||||||
"@actions/tool-cache": "^4.0.0",
|
"@actions/tool-cache": "^4.0.0",
|
||||||
@@ -72,6 +73,21 @@
|
|||||||
"prettier": "^2.0.0 || ^3.0.0"
|
"prettier": "^2.0.0 || ^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@1password/sdk": {
|
||||||
|
"version": "0.5.0-beta.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@1password/sdk/-/sdk-0.5.0-beta.1.tgz",
|
||||||
|
"integrity": "sha512-GY1kcn86qkb39jt20AyOftEu5Tw/Kyq4f84GOHXKRjur4TvqvzdhapynBBosRcBL+kBrc+E8cx7Tp7GEfqAomw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@1password/sdk-core": "0.5.0-beta.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@1password/sdk-core": {
|
||||||
|
"version": "0.5.0-beta.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@1password/sdk-core/-/sdk-core-0.5.0-beta.1.tgz",
|
||||||
|
"integrity": "sha512-61Q2n0kKYXBVAbW5ZVFqtbK1KX3lUfFi8wdsv+UjIVtbFd+X1GpFbLFs+nPtPgX+Z7oc2tTN/czK0S9Cz4oF/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/1Password/load-secrets-action#readme",
|
"homepage": "https://github.com/1Password/load-secrets-action#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@1password/sdk": "0.5.0-beta.1",
|
||||||
"@1password/op-js": "^0.1.11",
|
"@1password/op-js": "^0.1.11",
|
||||||
"@actions/core": "^3.0.0",
|
"@actions/core": "^3.0.0",
|
||||||
"@actions/exec": "^3.0.0",
|
"@actions/exec": "^3.0.0",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const createClient = jest.fn();
|
||||||
@@ -11,4 +11,5 @@ module.exports = {
|
|||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
addPath: jest.fn(),
|
addPath: jest.fn(),
|
||||||
isDebug: jest.fn(() => false),
|
isDebug: jest.fn(() => false),
|
||||||
|
getIDToken: jest.fn(() => Promise.resolve("mock-oidc-token")),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,8 @@ export const envConnectToken = "OP_CONNECT_TOKEN";
|
|||||||
export const envServiceAccountToken = "OP_SERVICE_ACCOUNT_TOKEN";
|
export const envServiceAccountToken = "OP_SERVICE_ACCOUNT_TOKEN";
|
||||||
export const envManagedVariables = "OP_MANAGED_VARIABLES";
|
export const envManagedVariables = "OP_MANAGED_VARIABLES";
|
||||||
export const envFilePath = "OP_ENV_FILE";
|
export const envFilePath = "OP_ENV_FILE";
|
||||||
|
export const envWorkloadId = "OP_WORKLOAD_ID";
|
||||||
|
export const envEnvironmentId = "OP_ENVIRONMENT_ID";
|
||||||
|
export const envIntegrationKey = "OP_INTEGRATION_KEY";
|
||||||
|
|
||||||
export const authErr = `Authentication error with environment variables: you must set either 1) ${envServiceAccountToken}, or 2) both ${envConnectHost} and ${envConnectToken}.`;
|
export const authErr = `Authentication error with environment variables: you must set either 1) ${envServiceAccountToken}, or 2) both ${envConnectHost} and ${envConnectToken}.`;
|
||||||
|
|||||||
+31
-14
@@ -2,7 +2,13 @@ import dotenv from "dotenv";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { validateCli } from "@1password/op-js";
|
import { validateCli } from "@1password/op-js";
|
||||||
import { installCliOnGithubActionRunner } from "./op-cli-installer";
|
import { installCliOnGithubActionRunner } from "./op-cli-installer";
|
||||||
import { loadSecrets, unsetPrevious, validateAuth } from "./utils";
|
import {
|
||||||
|
getWorkloadIdentityConfig,
|
||||||
|
loadSecrets,
|
||||||
|
unsetPrevious,
|
||||||
|
validateAuth,
|
||||||
|
} from "./utils";
|
||||||
|
import { loadSecretsFromSDK } from "./sdk-client";
|
||||||
import { envFilePath } from "./constants";
|
import { envFilePath } from "./constants";
|
||||||
|
|
||||||
const loadSecretsAction = async () => {
|
const loadSecretsAction = async () => {
|
||||||
@@ -16,21 +22,32 @@ const loadSecretsAction = async () => {
|
|||||||
unsetPrevious();
|
unsetPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that a proper authentication configuration is set for the CLI
|
const workloadConfig = getWorkloadIdentityConfig();
|
||||||
validateAuth();
|
|
||||||
|
|
||||||
// Set environment variables from OP_ENV_FILE
|
if (workloadConfig) {
|
||||||
const file = process.env[envFilePath];
|
await loadSecretsFromSDK(
|
||||||
if (file) {
|
workloadConfig.workloadId,
|
||||||
core.info(`Loading environment variables from file: ${file}`);
|
workloadConfig.environmentId,
|
||||||
dotenv.config({ path: file });
|
workloadConfig.integrationKey,
|
||||||
|
shouldExportEnv,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Validate that a proper authentication configuration is set for the CLI
|
||||||
|
validateAuth();
|
||||||
|
|
||||||
|
// Set environment variables from OP_ENV_FILE
|
||||||
|
const file = process.env[envFilePath];
|
||||||
|
if (file) {
|
||||||
|
core.info(`Loading environment variables from file: ${file}`);
|
||||||
|
dotenv.config({ path: file });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download and install the CLI
|
||||||
|
await installCLI();
|
||||||
|
|
||||||
|
// Load secrets
|
||||||
|
await loadSecrets(shouldExportEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download and install the CLI
|
|
||||||
await installCLI();
|
|
||||||
|
|
||||||
// Load secrets
|
|
||||||
await loadSecrets(shouldExportEnv);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// It's possible for the Error constructor to be modified to be anything
|
// It's possible for the Error constructor to be modified to be anything
|
||||||
// in JavaScript, so the following code accounts for this possibility.
|
// in JavaScript, so the following code accounts for this possibility.
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import { createClient } from "@1password/sdk";
|
||||||
|
import { envManagedVariables } from "./constants";
|
||||||
|
import { getOIDCToken, loadSecretsFromSDK } from "./sdk-client";
|
||||||
|
|
||||||
|
jest.mock("@1password/sdk");
|
||||||
|
|
||||||
|
const mockGetVariables = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(createClient as jest.Mock).mockResolvedValue({
|
||||||
|
environments: {
|
||||||
|
getVariables: mockGetVariables,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOIDCToken", () => {
|
||||||
|
it("delegates to core.getIDToken", async () => {
|
||||||
|
(core.getIDToken as jest.Mock).mockResolvedValue("oidc-token");
|
||||||
|
|
||||||
|
await expect(getOIDCToken("test-audience")).resolves.toBe("oidc-token");
|
||||||
|
expect(core.getIDToken).toHaveBeenCalledWith("test-audience");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadSecretsFromSDK", () => {
|
||||||
|
const workloadId = "workload-uuid";
|
||||||
|
const environmentId = "environment-uuid";
|
||||||
|
const integrationKey = "integration-key";
|
||||||
|
|
||||||
|
const variables = [
|
||||||
|
{ name: "DOCKERHUB_USERNAME", value: "myuser" },
|
||||||
|
{ name: "DOCKERHUB_TOKEN", value: "mypassword" },
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetVariables.mockResolvedValue({ variables });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets secrets as step outputs by default", async () => {
|
||||||
|
await loadSecretsFromSDK(
|
||||||
|
workloadId,
|
||||||
|
environmentId,
|
||||||
|
integrationKey,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(core.setOutput).toHaveBeenCalledWith(
|
||||||
|
"DOCKERHUB_USERNAME",
|
||||||
|
"myuser",
|
||||||
|
);
|
||||||
|
expect(core.setOutput).toHaveBeenCalledWith(
|
||||||
|
"DOCKERHUB_TOKEN",
|
||||||
|
"mypassword",
|
||||||
|
);
|
||||||
|
expect(core.exportVariable).not.toHaveBeenCalledWith(
|
||||||
|
"DOCKERHUB_USERNAME",
|
||||||
|
"myuser",
|
||||||
|
);
|
||||||
|
expect(core.setSecret).toHaveBeenCalledWith("myuser");
|
||||||
|
expect(core.setSecret).toHaveBeenCalledWith("mypassword");
|
||||||
|
expect(core.exportVariable).not.toHaveBeenCalledWith(
|
||||||
|
envManagedVariables,
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exports secrets as environment variables when shouldExportEnv is true", async () => {
|
||||||
|
await loadSecretsFromSDK(
|
||||||
|
workloadId,
|
||||||
|
environmentId,
|
||||||
|
integrationKey,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||||
|
"DOCKERHUB_USERNAME",
|
||||||
|
"myuser",
|
||||||
|
);
|
||||||
|
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||||
|
"DOCKERHUB_TOKEN",
|
||||||
|
"mypassword",
|
||||||
|
);
|
||||||
|
expect(core.setOutput).not.toHaveBeenCalled();
|
||||||
|
expect(core.exportVariable).toHaveBeenCalledWith(
|
||||||
|
envManagedVariables,
|
||||||
|
"DOCKERHUB_USERNAME,DOCKERHUB_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when secret value is empty string", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetVariables.mockResolvedValue({
|
||||||
|
variables: [{ name: "EMPTY_SECRET", value: "" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets empty string as step output", async () => {
|
||||||
|
await loadSecretsFromSDK(
|
||||||
|
workloadId,
|
||||||
|
environmentId,
|
||||||
|
integrationKey,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(core.setOutput).toHaveBeenCalledWith("EMPTY_SECRET", "");
|
||||||
|
expect(core.setSecret).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets empty string as environment variable", async () => {
|
||||||
|
await loadSecretsFromSDK(
|
||||||
|
workloadId,
|
||||||
|
environmentId,
|
||||||
|
integrationKey,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(core.exportVariable).toHaveBeenCalledWith("EMPTY_SECRET", "");
|
||||||
|
expect(core.setSecret).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not export OP_MANAGED_VARIABLES when no variables are returned", async () => {
|
||||||
|
mockGetVariables.mockResolvedValue({ variables: [] });
|
||||||
|
|
||||||
|
await loadSecretsFromSDK(
|
||||||
|
workloadId,
|
||||||
|
environmentId,
|
||||||
|
integrationKey,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(core.exportVariable).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import { createClient } from "@1password/sdk";
|
||||||
|
import { version } from "../package.json";
|
||||||
|
import { envManagedVariables } from "./constants";
|
||||||
|
|
||||||
|
export const getOIDCToken = async (audience: string): Promise<string> =>
|
||||||
|
core.getIDToken(audience);
|
||||||
|
|
||||||
|
export const loadSecretsFromSDK = async (
|
||||||
|
workloadId: string,
|
||||||
|
environmentId: string,
|
||||||
|
integrationKey: string,
|
||||||
|
shouldExportEnv: boolean,
|
||||||
|
): Promise<void> => {
|
||||||
|
|
||||||
|
// Temporary fix: strip base64 padding from integrationKey — this will eventually be handled by the SDK core itself
|
||||||
|
integrationKey = integrationKey.replace(/=+$/, "");
|
||||||
|
|
||||||
|
const client = await createClient({
|
||||||
|
integrationName: "1Password GitHub Action",
|
||||||
|
integrationVersion: version,
|
||||||
|
oidcFetcher: getOIDCToken,
|
||||||
|
workloadDetails: {
|
||||||
|
customerManagedSecret: integrationKey,
|
||||||
|
workloadUuid: workloadId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
core.info("Authenticated with Workload Identity.");
|
||||||
|
|
||||||
|
const { variables } = await client.environments.getVariables(environmentId);
|
||||||
|
|
||||||
|
const envNames: string[] = [];
|
||||||
|
for (const { name, value } of variables) {
|
||||||
|
core.info(`Populating variable: ${name}`);
|
||||||
|
if (shouldExportEnv) {
|
||||||
|
core.exportVariable(name, value);
|
||||||
|
} else {
|
||||||
|
core.setOutput(name, value);
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
core.setSecret(value);
|
||||||
|
}
|
||||||
|
envNames.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldExportEnv && envNames.length > 0) {
|
||||||
|
core.exportVariable(envManagedVariables, envNames.join());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import * as exec from "@actions/exec";
|
|||||||
import { read, setClientInfo } from "@1password/op-js";
|
import { read, setClientInfo } from "@1password/op-js";
|
||||||
import {
|
import {
|
||||||
extractSecret,
|
extractSecret,
|
||||||
|
getWorkloadIdentityConfig,
|
||||||
loadSecrets,
|
loadSecrets,
|
||||||
unsetPrevious,
|
unsetPrevious,
|
||||||
validateAuth,
|
validateAuth,
|
||||||
@@ -11,8 +12,11 @@ import {
|
|||||||
authErr,
|
authErr,
|
||||||
envConnectHost,
|
envConnectHost,
|
||||||
envConnectToken,
|
envConnectToken,
|
||||||
|
envEnvironmentId,
|
||||||
|
envIntegrationKey,
|
||||||
envManagedVariables,
|
envManagedVariables,
|
||||||
envServiceAccountToken,
|
envServiceAccountToken,
|
||||||
|
envWorkloadId,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
|
||||||
jest.mock("@1password/op-js");
|
jest.mock("@1password/op-js");
|
||||||
@@ -66,6 +70,68 @@ describe("validateAuth", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getWorkloadIdentityConfig", () => {
|
||||||
|
const testWorkloadId = "workload-id";
|
||||||
|
const testEnvironmentId = "environment-id";
|
||||||
|
const testIntegrationKey = "integration-key";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env[envWorkloadId] = "";
|
||||||
|
process.env[envEnvironmentId] = "";
|
||||||
|
process.env[envIntegrationKey] = "";
|
||||||
|
process.env[envConnectHost] = "";
|
||||||
|
process.env[envConnectToken] = "";
|
||||||
|
process.env[envServiceAccountToken] = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when no variables are set", () => {
|
||||||
|
expect(getWorkloadIdentityConfig()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the config when all variables are set", () => {
|
||||||
|
process.env[envWorkloadId] = testWorkloadId;
|
||||||
|
process.env[envEnvironmentId] = testEnvironmentId;
|
||||||
|
process.env[envIntegrationKey] = testIntegrationKey;
|
||||||
|
|
||||||
|
expect(getWorkloadIdentityConfig()).toEqual({
|
||||||
|
workloadId: testWorkloadId,
|
||||||
|
environmentId: testEnvironmentId,
|
||||||
|
integrationKey: testIntegrationKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when only some variables are set", () => {
|
||||||
|
process.env[envWorkloadId] = testWorkloadId;
|
||||||
|
|
||||||
|
expect(getWorkloadIdentityConfig).toThrow(
|
||||||
|
/Incomplete Workload Identity configuration/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when combined with Connect credentials", () => {
|
||||||
|
process.env[envWorkloadId] = testWorkloadId;
|
||||||
|
process.env[envEnvironmentId] = testEnvironmentId;
|
||||||
|
process.env[envIntegrationKey] = testIntegrationKey;
|
||||||
|
process.env[envConnectHost] = "https://localhost:8000";
|
||||||
|
process.env[envConnectToken] = "token";
|
||||||
|
|
||||||
|
expect(getWorkloadIdentityConfig).toThrow(
|
||||||
|
/Conflicting authentication configuration/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when combined with a service account token", () => {
|
||||||
|
process.env[envWorkloadId] = testWorkloadId;
|
||||||
|
process.env[envEnvironmentId] = testEnvironmentId;
|
||||||
|
process.env[envIntegrationKey] = testIntegrationKey;
|
||||||
|
process.env[envServiceAccountToken] = "ops_token";
|
||||||
|
|
||||||
|
expect(getWorkloadIdentityConfig).toThrow(
|
||||||
|
/Conflicting authentication configuration/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("extractSecret", () => {
|
describe("extractSecret", () => {
|
||||||
const envTestSecretEnv = "TEST_SECRET";
|
const envTestSecretEnv = "TEST_SECRET";
|
||||||
const testSecretRef = "op://vault/item/secret";
|
const testSecretRef = "op://vault/item/secret";
|
||||||
|
|||||||
@@ -8,8 +8,53 @@ import {
|
|||||||
envConnectToken,
|
envConnectToken,
|
||||||
envServiceAccountToken,
|
envServiceAccountToken,
|
||||||
envManagedVariables,
|
envManagedVariables,
|
||||||
|
envWorkloadId,
|
||||||
|
envEnvironmentId,
|
||||||
|
envIntegrationKey,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
|
||||||
|
export interface WorkloadIdentityConfig {
|
||||||
|
workloadId: string;
|
||||||
|
environmentId: string;
|
||||||
|
integrationKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the Workload Identity configuration when all variables are set,
|
||||||
|
// or null when none are set (so the CLI auth path can be used instead).
|
||||||
|
// Throws if the configuration is only partially set, or if it is combined
|
||||||
|
// with the CLI auth methods (Connect / service account).
|
||||||
|
export const getWorkloadIdentityConfig = (): WorkloadIdentityConfig | null => {
|
||||||
|
const workloadId = process.env[envWorkloadId];
|
||||||
|
const environmentId = process.env[envEnvironmentId];
|
||||||
|
const integrationKey = process.env[envIntegrationKey];
|
||||||
|
|
||||||
|
// None set: fall back to the CLI auth path.
|
||||||
|
if (!workloadId && !environmentId && !integrationKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some but not all set: configuration is incomplete.
|
||||||
|
if (!workloadId || !environmentId || !integrationKey) {
|
||||||
|
throw new Error(
|
||||||
|
`Incomplete Workload Identity configuration. To use Workload Identity, set all of ${envWorkloadId}, ${envEnvironmentId}, and ${envIntegrationKey}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workload Identity is fully configured, so it must not be combined with the
|
||||||
|
// CLI auth methods (Connect / service account), which are mutually exclusive.
|
||||||
|
if (
|
||||||
|
process.env[envConnectHost] ||
|
||||||
|
process.env[envConnectToken] ||
|
||||||
|
process.env[envServiceAccountToken]
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Conflicting authentication configuration: Workload Identity cannot be combined with Connect (${envConnectHost}/${envConnectToken}) or a service account (${envServiceAccountToken}). Set only one authentication method.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { workloadId, environmentId, integrationKey };
|
||||||
|
};
|
||||||
|
|
||||||
export const validateAuth = (): void => {
|
export const validateAuth = (): void => {
|
||||||
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
|
const isConnect = process.env[envConnectHost] && process.env[envConnectToken];
|
||||||
const isServiceAccount = process.env[envServiceAccountToken];
|
const isServiceAccount = process.env[envServiceAccountToken];
|
||||||
|
|||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Asserts the secrets loaded via Workload Identity.
|
||||||
|
|
||||||
|
assert_env_equals() {
|
||||||
|
if [ "$(printenv $1)" != "$2" ]; then
|
||||||
|
echo -e "Expected $1 to be set to:\n$2\nBut got:\n$(printenv $1)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_env_equals "ANOTHER_TEST" "anothertest123"
|
||||||
|
assert_env_equals "SUPER_SECRET" "supersecret"
|
||||||
|
assert_env_equals "TEST_SECRET" "thisisatest"
|
||||||
Reference in New Issue
Block a user