From ec10b5e25781bdbe71515457c817094dd5b9948c Mon Sep 17 00:00:00 2001 From: Richard Simpson Date: Thu, 20 Feb 2020 11:13:47 -0600 Subject: [PATCH] feat: add ability to retrieve secrets via ouputs --- README.md | 47 ++++++++++++++++++++++++++++++++++++++++----- action.js | 53 ++++++++++++++++++++++++++++++++++++--------------- dist/index.js | 53 ++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 118 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 81ef182..5cd07de 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The `secrets` parameter is a set of multiple secret requests separated by the `; Each secret request is comprised of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name. ```raw -{{ Secret Path }} {{ Secret Key }} | {{ Output Environment Variable Name }} +{{ Secret Path }} {{ Secret Key }} | {{ Output Variable Name }} ``` ### Simple Key @@ -64,15 +64,28 @@ with: secrets: ci npmToken ``` -`vault-action` will automatically normalize the given data key, and output: +`vault-action` will automatically normalize the given secret selector key, and set the follow as environment variables for the following steps in the current job: ```bash NPMTOKEN=somelongtoken ``` -### Set Environment Variable Name +You can also access the secret via ouputs: -However, if you want to set it to a specific environmental variable, say `NPM_TOKEN`, you could do this instead: +```yaml +steps: + # ... + - name: Import Secrets + id: secrets + # Import config... + - name: Sensitive Operation + run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'" + +``` + +### Set Output Variable Name + +However, if you want to set it to a specific name, say `NPM_TOKEN`, you could do this instead: ```yaml with: @@ -85,6 +98,17 @@ With that, `vault-action` will now use your requested name and output: NPM_TOKEN=somelongtoken ``` +```yaml +steps: + # ... + - name: Import Secrets + id: secrets + # Import config... + - name: Sensitive Operation + run: "my-cli --token '${{ steps.secrets.outputs.NPM_TOKEN }}'" + +``` + ### Multiple Secrets This action can take multi-line input, so say you had your AWS keys stored in a path and wanted to retrieve both of them. You can do: @@ -147,7 +171,20 @@ with: Resulting in: ```bash -FOO=bar MY_KEY=zap +FOO=bar +MY_KEY=zap +``` + +```yaml +steps: + # ... + - name: Import Secrets + id: secrets + # Import config... + - name: Sensitive Operation + run: "my-cli --token '${{ steps.secrets.outputs.foo }}'" + - name: Another Sensitive Operation + run: "my-cli --token '${{ steps.secrets.outputs.MY_KEY }}'" ``` Secrets pulled from the same `Secret Path` are cached by default. So if you, for example, are using the `aws` engine and retrieve a key, only a single key for a given path is returned. diff --git a/action.js b/action.js index cc36793..d40cc88 100644 --- a/action.js +++ b/action.js @@ -1,4 +1,8 @@ +// @ts-check + +// @ts-ignore const core = require('@actions/core'); +// @ts-ignore const command = require('@actions/core/lib/command'); const got = require('got'); @@ -36,6 +40,7 @@ async function exportSecrets() { options.headers["X-Vault-Namespace"] = vaultNamespace; } + /** @type {any} */ const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options); if (result && result.body && result.body.auth && result.body.auth.client_token) { vaultToken = result.body.auth.client_token; @@ -64,7 +69,7 @@ async function exportSecrets() { const responseCache = new Map(); for (const secretRequest of secretRequests) { - const { secretPath, outputName, secretSelector, isJSONPath } = secretRequest; + const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest; const requestOptions = { headers: { 'X-Vault-Token': vaultToken @@ -98,13 +103,22 @@ async function exportSecrets() { let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion; const secretData = getResponseData(body, dataDepth); - const value = selectData(secretData, secretSelector); + const value = selectData(secretData, secretSelector, isJSONPath); command.issue('add-mask', value); - core.exportVariable(outputName, `${value}`); - core.debug(`✔ ${secretPath} => ${outputName}`); + core.exportVariable(envVarName, `${value}`); + core.setOutput(outputVarName, `${value}`); + core.debug(`✔ ${secretPath} => outputs.${outputVarName} | env.${envVarName}`); } }; +/** @typedef {Object} SecretRequest + * @property {string} secretPath + * @property {string} envVarName + * @property {string} outputVarName + * @property {string} secretSelector + * @property {boolean} isJSONPath +*/ + /** * Parses a secrets input string into key paths and their resulting environment variable name. * @param {string} secretsInput @@ -116,18 +130,18 @@ function parseSecretsInput(secretsInput) { .map(key => key.trim()) .filter(key => key.length !== 0); - /** @type {{ secretPath: string; outputName: string; dataKey: string; }[]} */ + /** @type {SecretRequest[]} */ const output = []; for (const secret of secrets) { let path = secret; - let outputName = null; + let outputVarName = null; const renameSigilIndex = secret.lastIndexOf('|'); if (renameSigilIndex > -1) { path = secret.substring(0, renameSigilIndex).trim(); - outputName = secret.substring(renameSigilIndex + 1).trim(); + outputVarName = secret.substring(renameSigilIndex + 1).trim(); - if (outputName.length < 1) { + if (outputVarName.length < 1) { throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`); } } @@ -138,21 +152,29 @@ function parseSecretsInput(secretsInput) { .filter(part => part.length !== 0); if (pathParts.length !== 2) { - throw Error(`You must provide a valid path and key. Input: "${secret}"`) + throw Error(`You must provide a valid path and key. Input: "${secret}"`); } const [secretPath, secretSelector] = pathParts; - // If we're not using a mapped name, normalize the key path into a variable name. - if (!outputName) { - outputName = normalizeOutputKey(secretSelector); + const isJSONPath = secretSelector.includes('.'); + + if (isJSONPath && !outputVarName) { + throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); + } + + let envVarName = outputVarName; + if (!outputVarName) { + outputVarName = secretSelector; + envVarName = normalizeOutputKey(outputVarName); } output.push({ secretPath, - outputName, + envVarName, + outputVarName, secretSelector, - isJSONPath: secretSelector.startsWith('$') + isJSONPath }); } return output; @@ -161,7 +183,7 @@ function parseSecretsInput(secretsInput) { /** * Parses a JSON response and returns the secret data * @param {string} responseBody - * @param {number} kvVersion + * @param {number} dataLevel */ function getResponseData(responseBody, dataLevel) { let secretData = JSON.parse(responseBody); @@ -193,6 +215,7 @@ function normalizeOutputKey(dataKey) { return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase(); } +// @ts-ignore function parseBoolInput(input) { if (input === null || input === undefined || input.trim() === '') { return null; diff --git a/dist/index.js b/dist/index.js index 3951f95..a232389 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4839,7 +4839,11 @@ module.exports = require("fs"); /***/ 751: /***/ (function(module, __unusedexports, __webpack_require__) { +// @ts-check + +// @ts-ignore const core = __webpack_require__(470); +// @ts-ignore const command = __webpack_require__(431); const got = __webpack_require__(77); @@ -4877,6 +4881,7 @@ async function exportSecrets() { options.headers["X-Vault-Namespace"] = vaultNamespace; } + /** @type {any} */ const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options); if (result && result.body && result.body.auth && result.body.auth.client_token) { vaultToken = result.body.auth.client_token; @@ -4905,7 +4910,7 @@ async function exportSecrets() { const responseCache = new Map(); for (const secretRequest of secretRequests) { - const { secretPath, outputName, secretSelector, isJSONPath } = secretRequest; + const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest; const requestOptions = { headers: { 'X-Vault-Token': vaultToken @@ -4939,13 +4944,22 @@ async function exportSecrets() { let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion; const secretData = getResponseData(body, dataDepth); - const value = selectData(secretData, secretSelector); + const value = selectData(secretData, secretSelector, isJSONPath); command.issue('add-mask', value); - core.exportVariable(outputName, `${value}`); - core.debug(`✔ ${secretPath} => ${outputName}`); + core.exportVariable(envVarName, `${value}`); + core.setOutput(outputVarName, `${value}`); + core.debug(`✔ ${secretPath} => outputs.${outputVarName} | env.${envVarName}`); } }; +/** @typedef {Object} SecretRequest + * @property {string} secretPath + * @property {string} envVarName + * @property {string} outputVarName + * @property {string} secretSelector + * @property {boolean} isJSONPath +*/ + /** * Parses a secrets input string into key paths and their resulting environment variable name. * @param {string} secretsInput @@ -4957,18 +4971,18 @@ function parseSecretsInput(secretsInput) { .map(key => key.trim()) .filter(key => key.length !== 0); - /** @type {{ secretPath: string; outputName: string; dataKey: string; }[]} */ + /** @type {SecretRequest[]} */ const output = []; for (const secret of secrets) { let path = secret; - let outputName = null; + let outputVarName = null; const renameSigilIndex = secret.lastIndexOf('|'); if (renameSigilIndex > -1) { path = secret.substring(0, renameSigilIndex).trim(); - outputName = secret.substring(renameSigilIndex + 1).trim(); + outputVarName = secret.substring(renameSigilIndex + 1).trim(); - if (outputName.length < 1) { + if (outputVarName.length < 1) { throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`); } } @@ -4979,21 +4993,29 @@ function parseSecretsInput(secretsInput) { .filter(part => part.length !== 0); if (pathParts.length !== 2) { - throw Error(`You must provide a valid path and key. Input: "${secret}"`) + throw Error(`You must provide a valid path and key. Input: "${secret}"`); } const [secretPath, secretSelector] = pathParts; - // If we're not using a mapped name, normalize the key path into a variable name. - if (!outputName) { - outputName = normalizeOutputKey(secretSelector); + const isJSONPath = secretSelector.includes('.'); + + if (isJSONPath && !outputVarName) { + throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); + } + + let envVarName = outputVarName; + if (!outputVarName) { + outputVarName = secretSelector; + envVarName = normalizeOutputKey(outputVarName); } output.push({ secretPath, - outputName, + envVarName, + outputVarName, secretSelector, - isJSONPath: secretSelector.startsWith('$') + isJSONPath }); } return output; @@ -5002,7 +5024,7 @@ function parseSecretsInput(secretsInput) { /** * Parses a JSON response and returns the secret data * @param {string} responseBody - * @param {number} kvVersion + * @param {number} dataLevel */ function getResponseData(responseBody, dataLevel) { let secretData = JSON.parse(responseBody); @@ -5034,6 +5056,7 @@ function normalizeOutputKey(dataKey) { return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase(); } +// @ts-ignore function parseBoolInput(input) { if (input === null || input === undefined || input.trim() === '') { return null;