Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented wildcard selector (based on #238) #488

Merged
merged 18 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,13 @@ with:
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
```
You can specify a wildcard * for the key name to get all keys in the path. If you provide an output name with the wildcard, the name will be prepended to the key name:

```yaml
with:
secrets: |
secret/data/ci/aws * | MYAPP_ ;
```

## Other Secret Engines

Expand Down
163 changes: 129 additions & 34 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18516,6 +18516,9 @@ const core = __nccwpck_require__(2186);
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 { auth: { retrieveToken }, secrets: { getSecrets } } = __nccwpck_require__(4351);

const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
Expand Down Expand Up @@ -18684,7 +18687,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}"`);
}

Expand All @@ -18704,20 +18707,6 @@ 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;
}

/**
* @param {string} inputKey
* @param {any} inputOptions
Expand Down Expand Up @@ -18746,11 +18735,11 @@ function parseHeadersInput(inputKey, inputOptions) {
module.exports = {
exportSecrets,
parseSecretsInput,
normalizeOutputKey,
parseHeadersInput
parseHeadersInput,
};



/***/ }),

/***/ 4915:
Expand Down Expand Up @@ -18917,6 +18906,17 @@ module.exports = {
};


/***/ }),

/***/ 4438:
/***/ ((module) => {

const WILDCARD = '*';

module.exports = {
WILDCARD
};

/***/ }),

/***/ 4351:
Expand All @@ -18936,8 +18936,8 @@ module.exports = {
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {

const jsonata = __nccwpck_require__(4245);


const { WILDCARD } = __nccwpck_require__(4438);
const { normalizeOutputKey } = __nccwpck_require__(1608);
/**
* @typedef {Object} SecretRequest
* @property {string} path
Expand All @@ -18960,7 +18960,8 @@ const jsonata = __nccwpck_require__(4245);
*/
async function getSecrets(secretRequests, client) {
const responseCache = new Map();
const results = [];
let results = [];

for (const secretRequest of secretRequests) {
let { path, selector } = secretRequest;

Expand All @@ -18983,22 +18984,53 @@ async function getSecrets(secretRequests, client) {
throw error
}
}
if (!selector.match(/.*[\.].*/)) {
selector = '"' + selector + '"'
}
selector = "data." + selector
body = JSON.parse(body)
if (body.data["data"] != undefined) {
selector = "data." + selector
}

const value = await selectData(body, selector);
results.push({
request: secretRequest,
value,
cachedResponse
});
body = JSON.parse(body);

if (selector == WILDCARD) {
let keys = body.data;
if (body.data["data"] != undefined) {
keys = keys.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;

results = await selectAndAppendResults(
selector,
body,
cachedResponse,
newRequest,
results
);
}
}
else {
results = await selectAndAppendResults(
selector,
body,
cachedResponse,
secretRequest,
results
);
}
}

return results;
}

Expand All @@ -19024,12 +19056,75 @@ async function selectData(data, selector) {
return result;
}

/**
* Uses selectData with the selector to get the value and then appends it to the
* results. Returns a new array with all of the results.
* @param {string} selector
* @param {object} body
* @param {object} cachedResponse
* @param {TRequest} secretRequest
* @param {SecretResponse<TRequest>[]} results
* @return {Promise<SecretResponse<TRequest>[]>}
*/
const selectAndAppendResults = async (
selector,
body,
cachedResponse,
secretRequest,
results
) => {
if (!selector.match(/.*[\.].*/)) {
selector = '"' + selector + '"';
}
selector = "data." + selector;

if (body.data["data"] != undefined) {
selector = "data." + selector;
}

const value = await selectData(body, selector);
return [
...results,
{
request: secretRequest,
value,
cachedResponse,
},
];
};

module.exports = {
getSecrets,
selectData
}


/***/ }),

/***/ 1608:
/***/ ((module) => {

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


/***/ }),

/***/ 9491:
Expand Down
59 changes: 59 additions & 0 deletions integrationTests/basic/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,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');
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small suggestion would be to setup a secret with multiple keys to validate that the wildcard operator does indeed picks up all the sub-keys.

For example on l.22:

        await got(`${vaultUrl}/v1/secret/data/test`, {
            method: 'POST',
            headers: {
                'X-Vault-Token': vaultToken,
            },
            json: {
                data: {
                    secret: 'SUPERSECRET',
                    secret_2: 'SUPERSECRET_2',
                },
            },
        });

it('leading slash kvv2', async () => {
mockInput('/secret/data/foobar fookv2');

Expand All @@ -195,6 +215,34 @@ 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('get wildcard nested secret from K/V v1', async () => {
mockInput('secret-kv1/nested/test *');

await exportSecrets();

expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERCUSTOMSECRET');
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then you can modify the test to cover not only that the wildcard is valid & the prefix gets applied, but that multiple sub-keys get picked up as well as desired:

    it('get wildcard secrets', async () => {
        mockInput(`secret/data/test * ;`);

        await exportSecrets();

        expect(core.exportVariable).toBeCalledTimes(2);

        expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
        expect(core.exportVariable).toBeCalledWith('SECRET_2', 'SUPERSECRET_2');
    });

    it('get wildcard secrets with name prefix', async () => {
        mockInput(`secret/data/test * | GROUP_ ;`);

        await exportSecrets();

        expect(core.exportVariable).toBeCalledTimes(2);

        expect(core.exportVariable).toBeCalledWith('GROUP_SECRET', 'SUPERSECRET');
        expect(core.exportVariable).toBeCalledWith('GROUP_SECRET_2', 'SUPERSECRET_2');
    });

it('leading slash kvv1', async () => {
mockInput('/secret-kv1/foobar fookv1');

Expand Down Expand Up @@ -225,6 +273,17 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
});

it('wildcard supports cubbyhole', async () => {
mockInput('/cubbyhole/test *');

await exportSecrets();

expect(core.exportVariable).toBeCalledTimes(2);

expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
expect(core.exportVariable).toBeCalledWith('ZIP', 'zap');
});

it('caches responses', async () => {
mockInput(`
/cubbyhole/test foo ;
Expand Down
32 changes: 32 additions & 0 deletions integrationTests/enterprise/enterprise.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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');

Expand Down Expand Up @@ -103,6 +119,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');
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, would be great to have a multi-value secret as part of the tests.

it('get nested secret from K/V v1', async () => {
mockInput('my-secret/nested/test otherSecret');

Expand Down
Loading