5
0
Fork 0
mirror of https://github.com/hashicorp/vault-action.git synced 2025-11-07 15:16:56 +00:00
This commit is contained in:
vikas-pundir-learnings 2023-07-16 18:13:11 +00:00 committed by GitHub
commit 6a8151b403
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 754 additions and 32 deletions

View file

@ -46,9 +46,21 @@ jobs:
url: http://localhost:8200
method: token
token: testtoken
secretsMethod: read
secrets: |
secret/data/test-json-string jsonString;
# Write Secret examples
# Write Simple Secret
# secret/data/writetest secret=TEST ;
# Write Mulitple Secrets at one path
# secret/data/writetest secret=TEST secret1=TEST1 secret2=TEST2 ;
# Json String Secret
# secret/data/writetest secret={"url":"https://google.com/hello","key":"EQWQASAMSADAD"};
- name: Check Secrets
run: |
touch secrets.json

View file

@ -26,6 +26,9 @@ A helper action for easily pulling secrets from HashiCorp Vault™.
- [Simple Key](#simple-key)
- [Set Output Variable Name](#set-output-variable-name)
- [Multiple Secrets](#multiple-secrets)
- [Write Secrets](#write-secrets)
- [Write Multiple Secrets](#write-multiple-secrets)
- [Write Json Secrets](#write-json-secrets)
- [Other Secret Engines](#other-secret-engines)
- [Adding Extra Headers](#adding-extra-headers)
- [HashiCorp Cloud Platform or Vault Enterprise](#hashicorp-cloud-platform-or-vault-enterprise)
@ -374,6 +377,51 @@ with:
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
```
### Write Secrets
This action can write secrets to vault, so say you had your AWS access Key and you want them to write to vault. You can provide `secretsMethod: write` and provide the secret data as below:
```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws accessKey=someAccessKey;
```
`vault-action` create the secret at provided vault path. You will get `SUCCESS` in response for you saved secrets.
You can also write the multiple secrets at a single path. You can do:
```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws accessKey=someAccessKey secretKey=someSecretKey;
```
### Write Multiple Secrets
This action can take multi-line input, so say you had your AWS keys to be saved to vault. You can do:
```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws/key accessKey=someAccessKey ;
secret/data/ci/aws/secret secretKey=someAccessKey ;
```
### Write Json Secrets
This action can take json string input as a secret value and save it to vault as a json string. You can do:
```yaml
with:
secretsMethod: write
secrets: |
secret/data/ci/aws/ secret={"accessKey":"someAccessKey","secretKey":"someAccessKey"} ;
```
## Other Secret Engines
Vault Action currently supports retrieving secrets from any engine where secrets
@ -461,6 +509,7 @@ Here are all the inputs available through `with`:
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| `url` | The URL for the vault endpoint | | ✔ |
| `secrets` | A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details | | |
| `secretsMethod` | The secretsMethod indicates if you want to read or write secrets to vault. Supported values are `"read"` and `"write"`. If not provided, `default` is `"read"` | | |
| `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | |
| `method` | The method to use to authenticate with Vault. | `token` | |
| `role` | Vault role for specified auth method | | |

View file

@ -4,6 +4,9 @@ inputs:
url:
description: 'The URL for the vault endpoint'
required: true
secretsMethod:
description: 'The secretsMethod indicates if you want to read or write to vault. Supported values are "read" and "write". If not provided default is "read"'
required: false
secrets:
description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details'
required: false

111
dist/index.js vendored
View file

@ -18516,10 +18516,11 @@ const core = __nccwpck_require__(2186);
const command = __nccwpck_require__(7351);
const got = (__nccwpck_require__(3061)["default"]);
const jsonata = __nccwpck_require__(4245);
const { auth: { retrieveToken }, secrets: { getSecrets } } = __nccwpck_require__(4351);
const { auth: { retrieveToken }, secrets: { getSecrets, writeSecrets } } = __nccwpck_require__(4351);
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
const SECRETS_METHOD = { Read: "read", Write: "write" };
async function exportSecrets() {
const vaultUrl = core.getInput('url', { required: true });
@ -18528,6 +18529,7 @@ async function exportSecrets() {
const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
const outputToken = (core.getInput('outputToken', { required: false }) || 'false').toLowerCase() != 'false';
const exportToken = (core.getInput('exportToken', { required: false }) || 'false').toLowerCase() != 'false';
const secretsMethod = core.getInput('secretsMethod', { required: false });
const secretsInput = core.getInput('secrets', { required: false });
const secretRequests = parseSecretsInput(secretsInput);
@ -18599,7 +18601,18 @@ async function exportSecrets() {
return request;
});
const results = await getSecrets(requests, client);
let results = null;
switch (secretsMethod) {
case SECRETS_METHOD.Read:
results = await getSecrets(requests, client);
break;
case SECRETS_METHOD.Write:
results = await writeSecrets(requests, client);
break;
default:
results = await getSecrets(requests, client);
break;
}
for (const result of results) {
@ -18636,13 +18649,16 @@ async function exportSecrets() {
* @property {string} envVarName
* @property {string} outputVarName
* @property {string} selector
* @property {string} secretsMethod
* @property {Map} secretsData
*/
/**
* Parses a secrets input string into key paths and their resulting environment variable name.
* @param {string} secretsInput
* @param {string} secretsMethod
*/
function parseSecretsInput(secretsInput) {
function parseSecretsInput(secretsInput, secretsMethod) {
if (!secretsInput) {
return []
}
@ -18674,18 +18690,58 @@ function parseSecretsInput(secretsInput) {
.map(part => part.trim())
.filter(part => part.length !== 0);
let path = null;
let selector = '';
let secretsData = new Map();
if(secretsMethod === SECRETS_METHOD.Write) {
if (pathParts.length < 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}
let writeSelectorParts = null;
let finalSelector = [];
for (let index = 0; index < pathParts.length; index++) {
const element = pathParts[index];
if(index == 0) {
path = element;
continue;
}
//if a secret is for write, it should be saperated by "="
writeSelectorParts = element
.split("=")
.map(part => part.trim())
.filter(part => part.length !== 0);
const [writeSelectorKey, writeSelectorValue] = writeSelectorParts;
/** @type {any} */
const selectorAst = jsonata(writeSelectorKey).ast();
const writeSelector = writeSelectorKey.replace(new RegExp('"', 'g'), '');
if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`Write Secret: You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
if(writeSelector !=='\\') {
finalSelector.push(writeSelector);
secretsData.set(writeSelector, writeSelectorValue);
}
}
selector = finalSelector.join('__');
} else {
if (pathParts.length !== 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}
const [path, selectorQuoted] = pathParts;
path = pathParts[0];
const selectorQuoted = pathParts[1];
/** @type {any} */
const selectorAst = jsonata(selectorQuoted).ast();
const selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
if ((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}"`);
throw Error(`Read Secret: You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
}
let envVarName = outputVarName;
@ -18698,7 +18754,9 @@ function parseSecretsInput(secretsInput) {
path,
envVarName,
outputVarName,
selector
selector,
secretsMethod,
secretsData
});
}
return output;
@ -18942,6 +19000,7 @@ const jsonata = __nccwpck_require__(4245);
* @typedef {Object} SecretRequest
* @property {string} path
* @property {string} selector
* @property {Map} secretsData
*/
/**
@ -19002,6 +19061,43 @@ async function getSecrets(secretRequests, client) {
return results;
}
/**
* @template TRequest
* @param {Array<TRequest>} secretRequests
* @param {import('got').Got} client
* @return {Promise<SecretResponse<TRequest>[]>}
*/
async function writeSecrets(secretRequests, client) {
const results = [];
for (const secretRequest of secretRequests) {
let { path, selector, secretsData } = secretRequest;
const requestPath = `v1/${path}`;
let body;
const jsonata = {};
for (const [key, value] of secretsData) {
jsonata[key] = value;
}
try {
const result = await client.post(requestPath,{
json: {
data: jsonata
}
});
body = result.body;
} catch (error) {
throw error
}
//body = JSON.parse(body); //body.request_id
results.push({
request: secretRequest,
value: 'SUCCESS',
cachedResponse: false
});
}
return results;
}
/**
* Uses a Jsonata selector retrieve a bit of data from the result
* @param {object} data
@ -19026,6 +19122,7 @@ async function selectData(data, selector) {
module.exports = {
getSecrets,
writeSecrets,
selectData
}

View file

@ -9,10 +9,12 @@ const { exportSecrets } = require('../../src/action');
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
const secretsMethod = { Read: "read", Write: "write" };
describe('integration', () => {
beforeAll(async () => {
// Verify Connection
console.log('before all');
await got(`${vaultUrl}/v1/secret/config`, {
headers: {
'X-Vault-Token': vaultToken,
@ -124,6 +126,12 @@ describe('integration', () => {
.mockReturnValueOnce(secrets);
}
function mockSecretsMethod(method) {
when(core.getInput)
.calledWith('secretsMethod', expect.anything())
.mockReturnValueOnce(method);
}
it('prints a nice error message when secret not found', async () => {
mockInput(`secret/data/test secret ;
secret/data/test secret | NAMED_SECRET ;
@ -140,6 +148,16 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
});
it('write secret: simple secret', async () => {
mockInput('secret/data/writetest secret=TEST');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUCCESS');
});
it('re-map secret', async () => {
mockInput('secret/data/test secret | TEST_KEY');
@ -148,6 +166,15 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('TEST_KEY', 'SUPERSECRET');
});
it('write secret: re-map secret', async () => {
mockInput('secret/data/writetest secret=TEST | TEST_KEY');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('TEST_KEY', 'SUCCESS');
});
it('get nested secret', async () => {
mockInput(`secret/data/nested/test "other-Secret-dash"`);
@ -171,6 +198,18 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('OTHERSECRETDASH', 'OTHERSUPERSECRET');
});
it('write secrets: multiple secrets', async () => {
mockInput(`
secret/data/writetest secret=TEST ;
secret/data/writetest secret=TEST | NAMED_SECRET ;`);
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(2);
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUCCESS');
expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUCCESS');
});
it('leading slash kvv2', async () => {
mockInput('/secret/data/foobar fookv2');
@ -179,6 +218,15 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('FOOKV2', 'bar');
});
it('write secrets: leading slash kvv2', async () => {
mockInput('/secret/data/foobar fookv2=bar');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('FOOKV2', 'SUCCESS');
});
it('get secret from K/V v1', async () => {
mockInput('secret-kv1/test secret');
@ -187,6 +235,15 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('SECRET', 'CUSTOMSECRET');
});
it('write secrets: secret from K/V v1', async () => {
mockInput('secret-kv1/test secret=CUSTOMSECRET');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUCCESS');
});
it('get nested secret from K/V v1', async () => {
mockInput('secret-kv1/nested/test "other-Secret-dash"');
@ -203,6 +260,15 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('FOOKV1', 'bar');
});
it('write secrets: leading slash kvv1', async () => {
mockInput('/secret-kv1/foobar fookv1=bar');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('FOOKV1', 'SUCCESS');
});
describe('generic engines', () => {
beforeAll(async () => {
await got(`${vaultUrl}/v1/cubbyhole/test`, {
@ -237,5 +303,26 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('FOO', 'bar');
expect(core.exportVariable).toBeCalledWith('ZIP', 'zap');
});
it('write secrets: supports cubbyhole', async () => {
mockInput('/cubbyhole/test foo=foo');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('FOO', 'SUCCESS');
});
it('write secrets: multiple secrets', async () => {
mockInput(`
/cubbyhole/test foo=foo ;
/cubbyhole/test zip=zip`);
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('FOO', 'SUCCESS');
expect(core.exportVariable).toBeCalledWith('ZIP', 'SUCCESS');
});
})
});

View file

@ -9,6 +9,7 @@ const { exportSecrets } = require('../../src/action');
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
const secretsMethod = { Read: "read", Write: "write" };
describe('integration', () => {
beforeAll(async () => {
@ -64,6 +65,16 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_IN_NAMESPACE');
});
it('write secret: simple secret', async () => {
mockInput('secret/data/writetest secret=TEST');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUCCESS');
});
it('re-map secret', async () => {
mockInput('secret/data/test secret | TEST_KEY');
@ -72,6 +83,15 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('TEST_KEY', 'SUPERSECRET_IN_NAMESPACE');
});
it('write secret: re-map secret', async () => {
mockInput('secret/data/writetest secret=TEST | TEST_KEY');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('TEST_KEY', 'SUCCESS');
});
it('get nested secret', async () => {
mockInput('secret/data/nested/test otherSecret');
@ -95,6 +115,18 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET_IN_NAMESPACE');
});
it('write secrets: multiple secrets', async () => {
mockInput(`
secret/data/writetest secret=TEST ;
secret/data/writetest secret=TEST | NAMED_SECRET ;`);
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(2);
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUCCESS');
expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUCCESS');
});
it('get secret from K/V v1', async () => {
mockInput('my-secret/test secret');
@ -103,6 +135,15 @@ describe('integration', () => {
expect(core.exportVariable).toBeCalledWith('SECRET', 'CUSTOMSECRET_IN_NAMESPACE');
});
it('write secrets: secret from K/V v1', async () => {
mockInput('secret-kv1/test secret=CUSTOMSECRET');
mockSecretsMethod(secretsMethod.Write);
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUCCESS');
});
it('get nested secret from K/V v1', async () => {
mockInput('my-secret/nested/test otherSecret');
@ -290,3 +331,10 @@ function mockInput(secrets) {
.calledWith('secrets', expect.anything())
.mockReturnValueOnce(secrets);
}
function mockSecretsMethod(method) {
when(core.getInput)
.calledWith('secretsMethod', expect.anything())
.mockReturnValueOnce(method);
}

View file

@ -3,10 +3,11 @@ const core = require('@actions/core');
const command = require('@actions/core/lib/command');
const got = require('got').default;
const jsonata = require('jsonata');
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
const { auth: { retrieveToken }, secrets: { getSecrets, writeSecrets } } = require('./index');
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
const SECRETS_METHOD = { Read: "read", Write: "write" };
async function exportSecrets() {
const vaultUrl = core.getInput('url', { required: true });
@ -15,9 +16,10 @@ async function exportSecrets() {
const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
const outputToken = (core.getInput('outputToken', { required: false }) || 'false').toLowerCase() != 'false';
const exportToken = (core.getInput('exportToken', { required: false }) || 'false').toLowerCase() != 'false';
const secretsMethod = core.getInput('secretsMethod', { required: false });
const secretsInput = core.getInput('secrets', { required: false });
const secretRequests = parseSecretsInput(secretsInput);
const secretRequests = parseSecretsInput(secretsInput, secretsMethod);
const secretEncodingType = core.getInput('secretEncodingType', { required: false });
@ -86,8 +88,18 @@ async function exportSecrets() {
return request;
});
const results = await getSecrets(requests, client);
let results = null;
switch (secretsMethod) {
case SECRETS_METHOD.Read:
results = await getSecrets(requests, client);
break;
case SECRETS_METHOD.Write:
results = await writeSecrets(requests, client);
break;
default:
results = await getSecrets(requests, client);
break;
}
for (const result of results) {
// Output the result
@ -123,13 +135,16 @@ async function exportSecrets() {
* @property {string} envVarName
* @property {string} outputVarName
* @property {string} selector
* @property {string} secretsMethod
* @property {Map} secretsData
*/
/**
* Parses a secrets input string into key paths and their resulting environment variable name.
* @param {string} secretsInput
* @param {string} secretsMethod
*/
function parseSecretsInput(secretsInput) {
function parseSecretsInput(secretsInput, secretsMethod) {
if (!secretsInput) {
return []
}
@ -161,18 +176,58 @@ function parseSecretsInput(secretsInput) {
.map(part => part.trim())
.filter(part => part.length !== 0);
let path = null;
let selector = '';
let secretsData = new Map();
if(secretsMethod === SECRETS_METHOD.Write) {
if (pathParts.length < 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}
let writeSelectorParts = null;
let finalSelector = [];
for (let index = 0; index < pathParts.length; index++) {
const element = pathParts[index];
if(index == 0) {
path = element;
continue;
}
//if a secret is for write, it should be saperated by "="
writeSelectorParts = element
.split("=")
.map(part => part.trim())
.filter(part => part.length !== 0);
const [writeSelectorKey, writeSelectorValue] = writeSelectorParts;
/** @type {any} */
const selectorAst = jsonata(writeSelectorKey).ast();
const writeSelector = writeSelectorKey.replace(new RegExp('"', 'g'), '');
if ((selectorAst.type !== "path" || selectorAst.steps[0].stages) && selectorAst.type !== "string" && !outputVarName) {
throw Error(`Write Secret: You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
if(writeSelector !=='\\') {
finalSelector.push(writeSelector);
secretsData.set(writeSelector, writeSelectorValue);
}
}
selector = finalSelector.join('__');
} else {
if (pathParts.length !== 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}
const [path, selectorQuoted] = pathParts;
path = pathParts[0];
const selectorQuoted = pathParts[1];
/** @type {any} */
const selectorAst = jsonata(selectorQuoted).ast();
const selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
selector = selectorQuoted.replace(new RegExp('"', 'g'), '');
if ((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}"`);
throw Error(`Read Secret: You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
}
let envVarName = outputVarName;
@ -185,7 +240,9 @@ function parseSecretsInput(secretsInput) {
path,
envVarName,
outputVarName,
selector
selector,
secretsMethod,
secretsData
});
}
return output;

View file

@ -5,6 +5,8 @@ jest.mock('@actions/core/lib/command');
const command = require('@actions/core/lib/command');
const core = require('@actions/core');
const got = require('got');
const secretsMethod = { Read: "read", Write: "write" }
const {
exportSecrets,
parseSecretsInput,
@ -15,12 +17,14 @@ const { when } = require('jest-when');
describe('parseSecretsInput', () => {
it('parses simple secret', () => {
const output = parseSecretsInput('test key');
const output = parseSecretsInput('test key', secretsMethod.Read);
expect(output).toContainEqual({
path: 'test',
selector: 'key',
outputVarName: 'key',
envVarName: 'KEY'
envVarName: 'KEY',
secretsMethod: secretsMethod.Read,
secretsData: new Map()
});
});
@ -29,7 +33,7 @@ describe('parseSecretsInput', () => {
expect(output).toHaveLength(1);
expect(output[0]).toMatchObject({
outputVarName: 'testName',
envVarName: 'testName',
envVarName: 'testName'
});
});
@ -90,6 +94,109 @@ describe('parseSecretsInput', () => {
});
});
describe('write secret: parseSecretsInput', () => {
it('write secret: parses simple secret', () => {
const output = parseSecretsInput('test key=123', secretsMethod.Write);
expect(output).toContainEqual({
path: 'test',
selector: 'key',
outputVarName: 'key',
envVarName: 'KEY',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('key', '123')
});
});
it('write secret: parses mapped secret', () => {
const output = parseSecretsInput('test key=123|testName', secretsMethod.Write);
expect(output).toHaveLength(1);
expect(output[0]).toMatchObject({
outputVarName: 'testName',
envVarName: 'testName',
selector: 'key',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('key', '123')
});
});
it('write secret: fails on invalid mapped name', () => {
expect(() => parseSecretsInput('test key|', secretsMethod.Write))
.toThrowError(`You must provide a value when mapping a secret to a name. Input: "test key|"`)
});
it('write secret: fails on invalid path for mapped', () => {
expect(() => parseSecretsInput('|testName', secretsMethod.Write))
.toThrowError(`You must provide a valid path and key. Input: "|testName"`)
});
it('write secret: parses multiple secrets', () => {
const output = parseSecretsInput('first a=aaa;second b=bbb;', secretsMethod.Write);
expect(output).toHaveLength(2);
expect(output[0]).toMatchObject({
path: 'first',
selector: 'a',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('a', 'aaa')
});
expect(output[1]).toMatchObject({
path: 'second',
selector: 'b',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('b', 'bbb')
});
});
it('write secrets: parses multiple complex secret input', () => {
const output = parseSecretsInput('first a=aaa;second b=bbb|secondName', secretsMethod.Write);
expect(output).toHaveLength(2);
expect(output[0]).toMatchObject({
outputVarName: 'a',
envVarName: 'A',
selector: 'a',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('a', 'aaa')
});
expect(output[1]).toMatchObject({
outputVarName: 'secondName',
envVarName: 'secondName',
selector: 'b',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('b', 'bbb')
});
});
it('write secrets: parses multiline input', () => {
const output = parseSecretsInput(`
first a=aaa;
second b=bbb;
third c=ccc | SOME_C;`, secretsMethod.Write);
expect(output).toHaveLength(3);
expect(output[0]).toMatchObject({
path: 'first',
selector: 'a',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('a', 'aaa')
});
expect(output[1]).toMatchObject({
outputVarName: 'b',
envVarName: 'B',
selector: 'b',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('b', 'bbb')
});
expect(output[2]).toMatchObject({
outputVarName: 'SOME_C',
envVarName: 'SOME_C',
selector: 'c',
secretsMethod: secretsMethod.Write,
secretsData: new Map().set('c', 'ccc')
});
});
});
describe('parseHeaders', () => {
it('parses simple header', () => {
when(core.getInput)
@ -445,3 +552,226 @@ with blank lines
expect(core.setOutput).toBeCalledWith('vault_token', 'EXAMPLE');
})
});
describe('write secrets: exportSecrets', () => {
beforeEach(() => {
jest.resetAllMocks();
when(core.getInput)
.calledWith('url', expect.anything())
.mockReturnValueOnce('http://vault:8200');
when(core.getInput)
.calledWith('token', expect.anything())
.mockReturnValueOnce('EXAMPLE');
});
function mockInput(key) {
when(core.getInput)
.calledWith('secrets', expect.anything())
.mockReturnValueOnce(key);
}
function mockSecretsMethod(method) {
when(core.getInput)
.calledWith('secretsMethod', expect.anything())
.mockReturnValueOnce(method);
}
function mockVersion(version) {
when(core.getInput)
.calledWith('kv-version', expect.anything())
.mockReturnValueOnce(version);
}
function mockExtraHeaders(headerString) {
when(core.getInput)
.calledWith('extraHeaders', expect.anything())
.mockReturnValueOnce(headerString);
}
function mockVaultData(data, version='2') {
switch(version) {
case '1':
got.extend.mockReturnValue({
post: async () => ({ body: JSON.stringify({ data }) })
});
break;
case '2':
got.extend.mockReturnValue({
post: async () => ({ body: JSON.stringify({ data: {
data
} }) })
});
break;
}
}
function mockExportToken(doExport) {
when(core.getInput)
.calledWith('exportToken', expect.anything())
.mockReturnValueOnce(doExport);
}
function mockOutputToken(doOutput) {
when(core.getInput)
.calledWith('outputToken', expect.anything())
.mockReturnValueOnce(doOutput);
}
function mockEncodeType(doEncode) {
when(core.getInput)
.calledWith('secretEncodingType', expect.anything())
.mockReturnValueOnce(doEncode);
}
it('write secrets: simple secret retrieval', async () => {
mockInput('test key=1');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
key: 1
});
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('KEY', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('key', 'SUCCESS');
});
it('write secrets: JSON string secret retrieval', async () => {
const jsonString = '{"x":1,"y":2}';
mockInput('test key={"x":1,"y":2}');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
key: jsonString,
});
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('KEY', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('key', 'SUCCESS');
});
it('write secrets: intl secret retrieval', async () => {
mockInput('测试 测试=1');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
测试: 1
});
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('测试', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('测试', 'SUCCESS');
});
it('write secrets: mapped secret retrieval', async () => {
mockInput('test key=1|TEST_NAME');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
key: 1
});
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('TEST_NAME', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('TEST_NAME', 'SUCCESS');
});
it('write secrets: simple secret retrieval from K/V v1', async () => {
const version = '1';
mockInput('test key=1');
mockSecretsMethod(secretsMethod.Write);
mockExtraHeaders(`
TEST: 1
`);
mockVaultData({
key: 1
});
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('KEY', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('key', 'SUCCESS');
});
it('write secrets: simple secret retrieval with extra headers', async () => {
const version = '1';
mockInput('test key=1');
mockSecretsMethod(secretsMethod.Write);
mockVersion(version);
mockVaultData({
key: 1
}, version);
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('KEY', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('key', 'SUCCESS');
});
it('write secrets: nested secret retrieval', async () => {
mockInput('test key.value=1');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
key: { value: 1 }
});
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('KEY__VALUE', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('key__value', 'SUCCESS');
});
it('write secrets: export Vault token', async () => {
mockInput('test key=1');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
key: 1
});
mockExportToken("true")
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(2);
expect(core.exportVariable).toBeCalledWith('VAULT_TOKEN', 'EXAMPLE');
expect(core.exportVariable).toBeCalledWith('KEY', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('key', 'SUCCESS');
});
it('write secrets: not export Vault token', async () => {
mockInput('test key=1');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
key: 1
});
mockExportToken("false")
await exportSecrets();
expect(core.exportVariable).toBeCalledTimes(1);
expect(core.exportVariable).toBeCalledWith('KEY', 'SUCCESS');
expect(core.setOutput).toBeCalledWith('key', 'SUCCESS');
});
it('write secrets: single-line secret gets masked', async () => {
mockInput('test key=secret');
mockSecretsMethod(secretsMethod.Write);
mockVaultData({
key: 'secret'
});
mockExportToken("false")
await exportSecrets();
expect(core.setSecret).toBeCalledTimes(2);
expect(core.setSecret).toBeCalledWith('SUCCESS');
expect(core.setOutput).toBeCalledWith('key', 'SUCCESS');
})
});

View file

@ -5,6 +5,7 @@ const jsonata = require("jsonata");
* @typedef {Object} SecretRequest
* @property {string} path
* @property {string} selector
* @property {Map} secretsData
*/
/**
@ -65,6 +66,43 @@ async function getSecrets(secretRequests, client) {
return results;
}
/**
* @template TRequest
* @param {Array<TRequest>} secretRequests
* @param {import('got').Got} client
* @return {Promise<SecretResponse<TRequest>[]>}
*/
async function writeSecrets(secretRequests, client) {
const results = [];
for (const secretRequest of secretRequests) {
let { path, selector, secretsData } = secretRequest;
const requestPath = `v1/${path}`;
let body;
const jsonata = {};
for (const [key, value] of secretsData) {
jsonata[key] = value;
}
try {
const result = await client.post(requestPath,{
json: {
data: jsonata
}
});
body = result.body;
} catch (error) {
throw error
}
//body = JSON.parse(body); //body.request_id
results.push({
request: secretRequest,
value: 'SUCCESS',
cachedResponse: false
});
}
return results;
}
/**
* Uses a Jsonata selector retrieve a bit of data from the result
* @param {object} data
@ -89,5 +127,6 @@ async function selectData(data, selector) {
module.exports = {
getSecrets,
writeSecrets,
selectData
}