mirror of
https://github.com/hashicorp/vault-action.git
synced 2025-11-07 15:16:56 +00:00
Merge f3f9f6b112 into cb841f2c86
This commit is contained in:
commit
6a8151b403
9 changed files with 754 additions and 32 deletions
12
.github/workflows/local-test.yaml
vendored
12
.github/workflows/local-test.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
49
README.md
49
README.md
|
|
@ -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 | | |
|
||||
|
|
|
|||
|
|
@ -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
111
dist/index.js
vendored
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue