From f09a7fe7c931f782f917b1218bb9874ace32517b Mon Sep 17 00:00:00 2001 From: Lemme Date: Tue, 27 Jul 2021 15:36:28 -0400 Subject: [PATCH] Initial check-in of wildcard to get all secrets in path (Issue#234) --- integrationTests/basic/integration.test.js | 40 ++++++++++ .../enterprise/enterprise.test.js | 32 ++++++++ src/action.js | 45 ++++++----- src/secrets.js | 76 +++++++++++++++---- 4 files changed, 160 insertions(+), 33 deletions(-) diff --git a/integrationTests/basic/integration.test.js b/integrationTests/basic/integration.test.js index 2c436c8..c5da93b 100644 --- a/integrationTests/basic/integration.test.js +++ b/integrationTests/basic/integration.test.js @@ -162,6 +162,26 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERSUPERSECRET'); }); + it('get wildcard secrets', async () => { + mockInput(`secret/data/test * ;`); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledTimes(1); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET'); + }); + + it('get wildcard secrets with name prefix', async () => { + mockInput(`secret/data/test * | GROUP_ ;`); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledTimes(1); + + expect(core.exportVariable).toBeCalledWith('GROUP_SECRET', 'SUPERSECRET'); + }); + it('leading slash kvv2', async () => { mockInput('/secret/data/foobar fookv2'); @@ -186,6 +206,26 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERCUSTOMSECRET'); }); + it('get K/V v1 wildcard secrets', async () => { + mockInput(`secret-kv1/test * ;`); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledTimes(1); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'CUSTOMSECRET'); + }); + + it('get K/V v1 wildcard secrets with name prefix', async () => { + mockInput(`secret-kv1/test * | GROUP_ ;`); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledTimes(1); + + expect(core.exportVariable).toBeCalledWith('GROUP_SECRET', 'CUSTOMSECRET'); + }); + it('leading slash kvv1', async () => { mockInput('/secret-kv1/foobar fookv1'); diff --git a/integrationTests/enterprise/enterprise.test.js b/integrationTests/enterprise/enterprise.test.js index b791022..e648f8b 100644 --- a/integrationTests/enterprise/enterprise.test.js +++ b/integrationTests/enterprise/enterprise.test.js @@ -71,6 +71,22 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('TEST_KEY', 'SUPERSECRET_IN_NAMESPACE'); }); + it('get wildcard secrets', async () => { + mockInput('secret/data/test *'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_IN_NAMESPACE'); + }); + + it('get wildcard secrets with name prefix', async () => { + mockInput('secret/data/test * | GROUP_'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('GROUP_SECRET', 'SUPERSECRET_IN_NAMESPACE'); + }); + it('get nested secret', async () => { mockInput('secret/data/nested/test otherSecret'); @@ -102,6 +118,22 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('SECRET', 'CUSTOMSECRET_IN_NAMESPACE'); }); + it('get wildcard secrets from K/V v1', async () => { + mockInput('my-secret/test *'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'CUSTOMSECRET_IN_NAMESPACE'); + }); + + it('get wildcard secrets from K/V v1 with name prefix', async () => { + mockInput('my-secret/test * | GROUP_'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('GROUP_SECRET', 'CUSTOMSECRET_IN_NAMESPACE'); + }); + it('get nested secret from K/V v1', async () => { mockInput('my-secret/nested/test otherSecret'); diff --git a/src/action.js b/src/action.js index d301605..9c92609 100644 --- a/src/action.js +++ b/src/action.js @@ -3,6 +3,25 @@ const core = require('@actions/core'); const command = require('@actions/core/lib/command'); const got = require('got').default; const jsonata = require('jsonata'); +module.exports = {}; +const wildcard = '*'; +module.exports.wildcard = wildcard; + +/** + * Replaces any dot chars to __ and removes non-ascii charts + * @param {string} dataKey + * @param {boolean=} isEnvVar + */ +function normalizeOutputKey(dataKey, isEnvVar = false) { + let outputKey = dataKey + .replace('.', '__').replace(new RegExp('-', 'g'), '').replace(/[^\p{L}\p{N}_-]/gu, ''); + if (isEnvVar) { + outputKey = outputKey.toUpperCase(); + } + return outputKey; +} +module.exports.normalizeOutputKey = normalizeOutputKey; + const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index'); const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes']; @@ -90,6 +109,7 @@ async function exportSecrets() { core.debug(`✔ ${request.path} => outputs.${request.outputVarName}${exportEnv ? ` | env.${request.envVarName}` : ''}`); } }; +module.exports.exportSecrets = exportSecrets; /** @typedef {Object} SecretRequest * @property {string} path @@ -140,7 +160,7 @@ function parseSecretsInput(secretsInput) { const selectorAst = jsonata(selectorQuoted).ast(); const selector = selectorQuoted.replace(new RegExp('"', 'g'), ''); - if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) { + if (selector !== wildcard && (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}"`); } @@ -159,20 +179,7 @@ function parseSecretsInput(secretsInput) { } return output; } - -/** - * Replaces any dot chars to __ and removes non-ascii charts - * @param {string} dataKey - * @param {boolean=} isEnvVar - */ -function normalizeOutputKey(dataKey, isEnvVar = false) { - let outputKey = dataKey - .replace('.', '__').replace(new RegExp('-', 'g'), '').replace(/[^\p{L}\p{N}_-]/gu, ''); - if (isEnvVar) { - outputKey = outputKey.toUpperCase(); - } - return outputKey; -} +module.exports.parseSecretsInput = parseSecretsInput; /** * @param {string} inputKey @@ -198,10 +205,14 @@ function parseHeadersInput(inputKey, inputOptions) { return map; }, new Map()); } - +module.exports.parseHeadersInput = parseHeadersInput; +// restructured module.exports to avoid circular dependency when secrets imports this. +/* module.exports = { exportSecrets, parseSecretsInput, normalizeOutputKey, - parseHeadersInput + parseHeadersInput, + wildcard }; +*/ diff --git a/src/secrets.js b/src/secrets.js index 91cbd40..ce52abf 100644 --- a/src/secrets.js +++ b/src/secrets.js @@ -1,6 +1,5 @@ const jsonata = require("jsonata"); - - +const { normalizeOutputKey, wildcard} = require('./action'); /** * @typedef {Object} SecretRequest * @property {string} path @@ -24,6 +23,7 @@ const jsonata = require("jsonata"); async function getSecrets(secretRequests, client) { const responseCache = new Map(); const results = []; + for (const secretRequest of secretRequests) { let { path, selector } = secretRequest; @@ -38,25 +38,69 @@ async function getSecrets(secretRequests, client) { body = result.body; responseCache.set(requestPath, body); } - if (!selector.match(/.*[\.].*/)) { - selector = '"' + selector + '"' - } - selector = "data." + selector - body = JSON.parse(body) - if (body.data["data"] != undefined) { - selector = "data." + selector - } - const value = selectData(body, selector); - results.push({ - request: secretRequest, - value, - cachedResponse - }); + if (selector == wildcard) { + body = JSON.parse(body); + const keys = body.data; + for (let key in keys) { + let newRequest = Object.assign({},secretRequest); + newRequest.selector = key; + if (secretRequest.selector === secretRequest.outputVarName) { + newRequest.outputVarName = key; + newRequest.envVarName = key; + } + else { + newRequest.outputVarName = secretRequest.outputVarName+key; + newRequest.envVarName = secretRequest.envVarName+key; + } + newRequest.outputVarName = normalizeOutputKey(newRequest.outputVarName); + newRequest.envVarName = normalizeOutputKey(newRequest.envVarName,true); + + selector = key; + + //This code (with exception of parsing body again and using newRequest instead of secretRequest) should match the else code for a single key + if (!selector.match(/.*[\.].*/)) { + selector = '"' + selector + '"' + } + selector = "data." + selector + //body = JSON.parse(body) + if (body.data["data"] != undefined) { + selector = "data." + selector + } + const value = selectData(body, selector); + results.push({ + request: newRequest, + value, + cachedResponse + }); + + // used cachedResponse for first entry in wildcard list and set to true for the rest + cachedResponse = true; + } + + } + else { + if (!selector.match(/.*[\.].*/)) { + selector = '"' + selector + '"' + } + selector = "data." + selector + body = JSON.parse(body) + if (body.data["data"] != undefined) { + selector = "data." + selector + } + + const value = selectData(body, selector); + results.push({ + request: secretRequest, + value, + cachedResponse + }); + } } return results; } + /** * Uses a Jsonata selector retrieve a bit of data from the result * @param {object} data