5
0
Fork 0
mirror of https://github.com/hashicorp/vault-action.git synced 2025-11-09 16:16:55 +00:00

Initial check-in of wildcard to get all secrets in path (Issue#234)

This commit is contained in:
Lemme 2021-07-27 15:36:28 -04:00
parent 0451f06f9f
commit f09a7fe7c9
4 changed files with 160 additions and 33 deletions

View file

@ -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');

View file

@ -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');

View file

@ -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
};
*/

View file

@ -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