From 7709c609789c5e27b757a85817483caadbb5939a Mon Sep 17 00:00:00 2001 From: Rory Date: Mon, 3 Mar 2025 23:31:00 +0200 Subject: [PATCH] feat: added double asterisk wildcard selector to prevent uppercasing of keys before exporting envs (#545) * feat: added double asterisk wildcard selector to prevent uppercasing of keys before exporting envs * chore: update changelog --------- Co-authored-by: John-Michael Faircloth --- CHANGELOG.md | 5 ++++ README.md | 9 +++++++ dist/index.js | 23 +++++++++++------- integrationTests/basic/integration.test.js | 28 +++++++++++++++++++++- src/action.js | 4 ++-- src/constants.js | 8 ++++--- src/secrets.js | 8 ++++--- src/utils.js | 4 ++-- 8 files changed, 69 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd370da..1d7eec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.3.0 (March 3, 2025) + +Features: +* Wildcard secret imports can use `**` to retain case of exported env keys [GH-545](https://github.com/hashicorp/vault-action/pull/545) + ## 3.2.0 (March 3, 2025) Improvements: diff --git a/README.md b/README.md index 8cd6773..b3939f5 100644 --- a/README.md +++ b/README.md @@ -403,6 +403,15 @@ with: secret/data/ci/aws * | MYAPP_ ; ``` +When using the `exportEnv` option all exported keys will be normalized to uppercase. For example, the key `SecretKey` would be exported as `MYAPP_SECRETKEY`. +You can disable uppercase normalization by specifying double asterisks `**` in the selector path: + +```yaml +with: + secrets: | + secret/data/ci/aws ** | MYAPP_ ; +``` + ### KV secrets engine version 2 When accessing secrets from the KV secrets engine version 2, Vault Action diff --git a/dist/index.js b/dist/index.js index 29c5803..adb2590 100644 --- a/dist/index.js +++ b/dist/index.js @@ -18535,7 +18535,7 @@ const command = __nccwpck_require__(7351); const got = (__nccwpck_require__(3061)["default"]); const jsonata = __nccwpck_require__(4245); const { normalizeOutputKey } = __nccwpck_require__(1608); -const { WILDCARD } = __nccwpck_require__(4438); +const { WILDCARD, WILDCARD_UPPERCASE } = __nccwpck_require__(4438); const { auth: { retrieveToken }, secrets: { getSecrets }, pki: { getCertificates } } = __nccwpck_require__(4351); @@ -18752,7 +18752,7 @@ function parseSecretsInput(secretsInput) { const selectorAst = jsonata(selectorQuoted).ast(); const selector = selectorQuoted.replace(new RegExp('"', 'g'), ''); - if (selector !== WILDCARD && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) { + if (selector !== WILDCARD && selector !== WILDCARD_UPPERCASE && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) { throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); } @@ -19005,12 +19005,15 @@ module.exports = { /***/ 4438: /***/ ((module) => { -const WILDCARD = '*'; +const WILDCARD_UPPERCASE = '*'; +const WILDCARD = '**'; module.exports = { - WILDCARD + WILDCARD, + WILDCARD_UPPERCASE, }; + /***/ }), /***/ 4351: @@ -19114,7 +19117,7 @@ module.exports = { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const jsonata = __nccwpck_require__(4245); -const { WILDCARD } = __nccwpck_require__(4438); +const { WILDCARD, WILDCARD_UPPERCASE} = __nccwpck_require__(4438); const { normalizeOutputKey } = __nccwpck_require__(1608); const core = __nccwpck_require__(2186); @@ -19141,6 +19144,7 @@ const core = __nccwpck_require__(2186); async function getSecrets(secretRequests, client, ignoreNotFound) { const responseCache = new Map(); let results = []; + let upperCaseEnv = false; for (const secretRequest of secretRequests) { let { path, selector } = secretRequest; @@ -19174,7 +19178,8 @@ async function getSecrets(secretRequests, client, ignoreNotFound) { body = JSON.parse(body); - if (selector == WILDCARD) { + if (selector === WILDCARD || selector === WILDCARD_UPPERCASE) { + upperCaseEnv = selector === WILDCARD_UPPERCASE; let keys = body.data; if (body.data["data"] != undefined) { keys = keys.data; @@ -19193,7 +19198,7 @@ async function getSecrets(secretRequests, client, ignoreNotFound) { } newRequest.outputVarName = normalizeOutputKey(newRequest.outputVarName); - newRequest.envVarName = normalizeOutputKey(newRequest.envVarName,true); + newRequest.envVarName = normalizeOutputKey(newRequest.envVarName, upperCaseEnv); // JSONata field references containing reserved tokens should // be enclosed in backticks @@ -19302,12 +19307,12 @@ module.exports = { * @param {string} dataKey * @param {boolean=} isEnvVar */ -function normalizeOutputKey(dataKey, isEnvVar = false) { +function normalizeOutputKey(dataKey, upperCase = false) { let outputKey = dataKey .replace(".", "__") .replace(new RegExp("-", "g"), "") .replace(/[^\p{L}\p{N}_-]/gu, ""); - if (isEnvVar) { + if (upperCase) { outputKey = outputKey.toUpperCase(); } return outputKey; diff --git a/integrationTests/basic/integration.test.js b/integrationTests/basic/integration.test.js index d9598cc..3060466 100644 --- a/integrationTests/basic/integration.test.js +++ b/integrationTests/basic/integration.test.js @@ -395,7 +395,7 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('FOO', 'bar'); }); - it('wildcard supports cubbyhole', async () => { + it('wildcard supports cubbyhole with uppercase transform', async () => { mockInput('/cubbyhole/test *'); await exportSecrets(); @@ -405,6 +405,32 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('FOO', 'bar'); expect(core.exportVariable).toBeCalledWith('ZIP', 'zap'); }); + + it('wildcard supports cubbyhole with no change in case', async () => { + mockInput('/cubbyhole/test **'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledTimes(2); + + expect(core.exportVariable).toBeCalledWith('foo', 'bar'); + expect(core.exportVariable).toBeCalledWith('zip', 'zap'); + }); + + it('wildcard supports cubbyhole with mixed case change', async () => { + mockInput(` + /cubbyhole/test * ; + /cubbyhole/test **`); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledTimes(4); + + expect(core.exportVariable).toBeCalledWith('FOO', 'bar'); + expect(core.exportVariable).toBeCalledWith('ZIP', 'zap'); + expect(core.exportVariable).toBeCalledWith('foo', 'bar'); + expect(core.exportVariable).toBeCalledWith('zip', 'zap'); + }); it('caches responses', async () => { mockInput(` diff --git a/src/action.js b/src/action.js index 931530d..3dcf1f4 100644 --- a/src/action.js +++ b/src/action.js @@ -4,7 +4,7 @@ const command = require('@actions/core/lib/command'); const got = require('got').default; const jsonata = require('jsonata'); const { normalizeOutputKey } = require('./utils'); -const { WILDCARD } = require('./constants'); +const { WILDCARD, WILDCARD_UPPERCASE } = require('./constants'); const { auth: { retrieveToken }, secrets: { getSecrets }, pki: { getCertificates } } = require('./index'); @@ -221,7 +221,7 @@ function parseSecretsInput(secretsInput) { const selectorAst = jsonata(selectorQuoted).ast(); const selector = selectorQuoted.replace(new RegExp('"', 'g'), ''); - if (selector !== WILDCARD && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) { + if (selector !== WILDCARD && selector !== WILDCARD_UPPERCASE && (selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) { throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); } diff --git a/src/constants.js b/src/constants.js index b005350..427f2fc 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,5 +1,7 @@ -const WILDCARD = '*'; +const WILDCARD_UPPERCASE = '*'; +const WILDCARD = '**'; module.exports = { - WILDCARD -}; \ No newline at end of file + WILDCARD, + WILDCARD_UPPERCASE, +}; diff --git a/src/secrets.js b/src/secrets.js index 82375c7..02ed1fb 100644 --- a/src/secrets.js +++ b/src/secrets.js @@ -1,5 +1,5 @@ const jsonata = require("jsonata"); -const { WILDCARD } = require("./constants"); +const { WILDCARD, WILDCARD_UPPERCASE} = require("./constants"); const { normalizeOutputKey } = require("./utils"); const core = require('@actions/core'); @@ -26,6 +26,7 @@ const core = require('@actions/core'); async function getSecrets(secretRequests, client, ignoreNotFound) { const responseCache = new Map(); let results = []; + let upperCaseEnv = false; for (const secretRequest of secretRequests) { let { path, selector } = secretRequest; @@ -59,7 +60,8 @@ async function getSecrets(secretRequests, client, ignoreNotFound) { body = JSON.parse(body); - if (selector == WILDCARD) { + if (selector === WILDCARD || selector === WILDCARD_UPPERCASE) { + upperCaseEnv = selector === WILDCARD_UPPERCASE; let keys = body.data; if (body.data["data"] != undefined) { keys = keys.data; @@ -78,7 +80,7 @@ async function getSecrets(secretRequests, client, ignoreNotFound) { } newRequest.outputVarName = normalizeOutputKey(newRequest.outputVarName); - newRequest.envVarName = normalizeOutputKey(newRequest.envVarName,true); + newRequest.envVarName = normalizeOutputKey(newRequest.envVarName, upperCaseEnv); // JSONata field references containing reserved tokens should // be enclosed in backticks diff --git a/src/utils.js b/src/utils.js index b8dd863..0f5e4de 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,12 +3,12 @@ * @param {string} dataKey * @param {boolean=} isEnvVar */ -function normalizeOutputKey(dataKey, isEnvVar = false) { +function normalizeOutputKey(dataKey, upperCase = false) { let outputKey = dataKey .replace(".", "__") .replace(new RegExp("-", "g"), "") .replace(/[^\p{L}\p{N}_-]/gu, ""); - if (isEnvVar) { + if (upperCase) { outputKey = outputKey.toUpperCase(); } return outputKey;