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

feat: rethink how key retrevial is structued and add e2e test

This commit is contained in:
Richard Simpson 2019-09-20 16:59:05 -05:00
parent 9a7f009394
commit 0b17727b1c
No known key found for this signature in database
GPG key ID: 0CECAF50D013D1E2
10 changed files with 191 additions and 179 deletions

View file

@ -41,8 +41,8 @@ jobs:
node-version: 10.x
- name: npm install
run: npm ci
- name: test connection
run: node quicktest.js
- name: npm run test:e2e
run: npm run test:e2e
env:
VAULT_HOST: localhost
VAULT_PORT: ${{ job.services.vault.ports[8200] }}

View file

@ -16,10 +16,9 @@ jobs:
vaultUrl: https://vault.mycompany.com
vaultToken: ${{ secrets.VaultToken }}
keys: |
ci_key ;
ci/aws > $.accessKey | AWS_ACCESS_KEY_ID ;
ci/aws > $.secretKey | AWS_SECRET_ACCESS_KEY ;
ci/npm_token | NPM_TOKEN
ci/aws accessKey | AWS_ACCESS_KEY_ID ;
ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
ci/npm token | NPM_TOKEN
# ...
```
@ -35,17 +34,17 @@ Each key is comprised of the `path` of they key, and optionally a [`JSONPath`](h
### Simple Key
To retrieve a key `ci/npm_token` that has value `somelongtoken` from vault you could do:
To retrieve a key `npmToken` from path `ci` that has value `somelongtoken` from vault you could do:
```yaml
with:
keys: ci/npm_token
keys: ci npmToken
```
`vault-action` will automatically normalize the given path, and output:
`vault-action` will automatically normalize the given data key, and output:
```bash
CI__NPM_TOKEN=somelongtoken
NPMTOKEN=somelongtoken
```
### Set Environment Variable Name
@ -54,40 +53,24 @@ However, if you want to set it to a specific environmental variable, say `NPM_TO
```yaml
with:
keys: ci/npm_token | NPM_TOKEN
keys: ci npmToken | NPM_TOKEN
```
With that, `vault-action` will now use your request name and output:
With that, `vault-action` will now use your requested name and output:
```bash
NPM_TOKEN=somelongtoken
```
### JSON Key
### Multiple Keys
Say you are storing a set of AWS keys as a JSON document in Vault like so:
```json
{
"accessKey": "AKIAIOSFODNN7EXAMPLE",
"secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
```
And you want to set them to `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` respectively so you could use the AWS CLI:
This action can take multi-line input, so say you had your AWS keys stored in a path and wanted to retrieve both of them. You can do:
```yaml
with:
keys: |
ci/aws > $.accessKey | AWS_ACCESS_KEY_ID ;
ci/aws > $.secretKey | AWS_SECRET_ACCESS_KEY
```
This would output:
```bash
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
ci/aws accessKey | AWS_ACCESS_KEY_ID ;
ci/aws secretKey | AWS_SECRET_ACCESS_KEY
```
## Masking

View file

@ -2,7 +2,6 @@
// @ts-ignore
const core = require('@actions/core');
const got = require('got');
const jq = require('jsonpath');
async function exportSecrets() {
const vaultUrl = core.getInput('vaultUrl', { required: true });
@ -12,21 +11,19 @@ async function exportSecrets() {
const keys = parseKeysInput(keysInput);
for (const key of keys) {
const [keyPath, { name, query }] = key;
const result = await got(`${vaultUrl}/secret/data/${keyPath}`, {
const [keyPath, { outputName, dataKey }] = key;
const result = await got(`${vaultUrl}/v1/secret/data/${keyPath}`, {
headers: {
'Accept': 'application/json',
'X-Vault-Token': vaultToken
}
});
const parsedResponse = JSON.parse(result.body);
let value = parsedResponse.data;
if (query) {
value = jq.value(value, query);
}
core.exportSecret(name, value);
core.debug(`${keyPath} => ${name}`);
const vaultKeyData = parsedResponse.data;
const versionData = vaultKeyData.data;
const value = versionData[dataKey];
core.exportSecret(outputName, value);
core.debug(`${keyPath} => ${outputName}`);
}
};
@ -41,51 +38,41 @@ function parseKeysInput(keys) {
.map(key => key.trim())
.filter(key => key.length !== 0);
/** @type {Map<string, { name: string; query: string; }>} */
/** @type {Map<string, { outputName: string; dataKey: string; }>} */
const output = new Map();
for (const keyPair of keyPairs) {
let path = keyPair;
let mappedName = null;
let query = null;
let outputName = null;
const renameSigilIndex = keyPair.lastIndexOf('|');
if (renameSigilIndex > -1) {
path = keyPair.substring(0, renameSigilIndex).trim();
mappedName = keyPair.substring(renameSigilIndex + 1).trim();
outputName = keyPair.substring(renameSigilIndex + 1).trim();
if (mappedName.length < 1) {
if (outputName.length < 1) {
throw Error(`You must provide a value when mapping a secret to a name. Input: "${keyPair}"`);
}
}
const pathPair = path;
const querySigilIndex = pathPair.indexOf('>');
if (querySigilIndex > -1) {
path = pathPair.substring(0, querySigilIndex).trim();
query = pathPair.substring(querySigilIndex + 1).trim();
const pathParts = path
.split(/\s+/)
.map(part => part.trim())
.filter(part => part.length !== 0);
try {
const expression = jq.parse(query);
if (expression.length === 0) {
throw Error(`Invalid query expression provided "${query}" from "${keyPair}".`);
}
} catch (_) {
throw Error(`Invalid query expression provided "${query}" from "${keyPair}".`);
}
if (pathParts.length !== 2) {
throw Error(`You must provide a valid path and key. Input: "${keyPair}"`)
}
if (path.length === 0) {
throw Error(`You must provide a valid path. Input: "${keyPair}"`)
}
const [secretPath, dataKey] = pathParts;
// If we're not using a mapped name, normalize the key path into a variable name.
if (!mappedName) {
mappedName = normalizeKeyName(path);
if (!outputName) {
outputName = normalizeKeyName(dataKey);
}
output.set(path, {
name: mappedName,
query
output.set(secretPath, {
outputName,
dataKey
});
}
return output;

View file

@ -12,55 +12,33 @@ const { when } = require('jest-when');
describe('parseKeysInput', () => {
it('parses simple key', () => {
const output = parseKeysInput('test');
const output = parseKeysInput('test key');
expect(output.has('test')).toBeTruthy();
expect(output.get('test')).toMatchObject({
name: 'TEST',
query: null
outputName: 'KEY',
dataKey: 'key'
});
});
it('parses mapped key', () => {
const output = parseKeysInput('test|testName');
const output = parseKeysInput('test key|testName');
expect(output.get('test')).toMatchObject({
name: 'testName',
query: null
outputName: 'testName'
});
});
it('fails on invalid mapped name', () => {
expect(() => parseKeysInput('test|'))
.toThrowError(`You must provide a value when mapping a secret to a name. Input: "test|"`)
expect(() => parseKeysInput('test key|'))
.toThrowError(`You must provide a value when mapping a secret to a name. Input: "test key|"`)
});
it('fails on invalid path for mapped', () => {
expect(() => parseKeysInput('|testName'))
.toThrowError(`You must provide a valid path. Input: "|testName"`)
});
it('parses queried key', () => {
const output = parseKeysInput('test>$.test');
expect(output.get('test')).toMatchObject({
name: 'TEST',
query: '$.test'
});
});
it('fails on invalid query', () => {
expect(() => parseKeysInput('test>#'))
.toThrowError(`Invalid query expression provided "#" from "test>#".`)
});
it('parses queried and mapped key', () => {
const output = parseKeysInput('test>$.test|testName');
expect(output.get('test')).toMatchObject({
name: 'testName',
query: '$.test'
});
.toThrowError(`You must provide a valid path and key. Input: "|testName"`)
});
it('parses multiple keys', () => {
const output = parseKeysInput('first;second;');
const output = parseKeysInput('first a;second b;');
expect(output.size).toBe(2);
expect(output.has('first')).toBeTruthy();
@ -68,20 +46,19 @@ describe('parseKeysInput', () => {
});
it('parses multiple complex keys', () => {
const output = parseKeysInput('first;second|secondName;third>$.third');
const output = parseKeysInput('first a;second b|secondName');
expect(output.size).toBe(3);
expect(output.has('first')).toBeTruthy();
expect(output.get('second')).toMatchObject({
name: 'secondName'
expect(output.size).toBe(2);
expect(output.get('first')).toMatchObject({
dataKey: 'a'
});
expect(output.get('third')).toMatchObject({
query: '$.third'
expect(output.get('second')).toMatchObject({
outputName: 'secondName'
});
});
it('parses multiline input', () => {
const output = parseKeysInput('\nfirst;\nsecond;\n');
const output = parseKeysInput('\nfirst a;\nsecond b;\n');
expect(output.size).toBe(2);
expect(output.has('first')).toBeTruthy();
@ -96,7 +73,7 @@ describe('exportSecrets', () => {
when(core.getInput)
.calledWith('vaultUrl')
.mockReturnValue('https://vault');
.mockReturnValue('http://vault:8200');
when(core.getInput)
.calledWith('vaultToken')
@ -119,31 +96,24 @@ describe('exportSecrets', () => {
}
it('simple key retrieval', async () => {
mockInput('test');
mockVaultData('1');
mockInput('test key');
mockVaultData({
key: 1
});
await exportSecrets();
expect(core.exportSecret).toBeCalledWith('TEST', '1');
expect(core.exportSecret).toBeCalledWith('KEY', 1);
});
it('mapped key retrieval', async () => {
mockInput('test|TEST_NAME');
mockVaultData('1');
await exportSecrets();
expect(core.exportSecret).toBeCalledWith('TEST_NAME', '1');
});
it('queried data retrieval', async () => {
mockInput('test > $.key');
mockInput('test key|TEST_NAME');
mockVaultData({
key: 'SECURE'
key: 1
});
await exportSecrets();
expect(core.exportSecret).toBeCalledWith('TEST', 'SECURE');
expect(core.exportSecret).toBeCalledWith('TEST_NAME', 1);
});
});

93
e2e/e2e.test.js Normal file
View file

@ -0,0 +1,93 @@
jest.mock('@actions/core');
const core = require('@actions/core');
const got = require('got');
const { when } = require('jest-when');
const { exportSecrets } = require('../action');
describe('e2e', () => {
beforeAll(async () => {
console.debug(`Testing against: http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret`)
// Verify Connection
await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/config`, {
headers: {
'X-Vault-Token': 'testtoken',
},
});
await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/data/test`, {
method: 'POST',
headers: {
'X-Vault-Token': 'testtoken',
},
body: {
data: {
a: 1,
b: 2,
c: 3,
},
},
json: true,
});
await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/data/nested/test`, {
method: 'POST',
headers: {
'X-Vault-Token': 'testtoken',
},
body: {
data: {
e: 4,
f: 5,
g: 6,
},
},
json: true,
});
})
beforeEach(() => {
jest.resetAllMocks();
when(core.getInput)
.calledWith('vaultUrl')
.mockReturnValue(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`);
when(core.getInput)
.calledWith('vaultToken')
.mockReturnValue('testtoken');
});
function mockInput(key) {
when(core.getInput)
.calledWith('keys')
.mockReturnValue(key);
}
it('get simple secret', async () => {
mockInput('test a')
await exportSecrets();
expect(core.exportSecret).toBeCalledWith('A', 1);
});
it('re-map secret', async () => {
mockInput('test a | TEST_KEY')
await exportSecrets();
expect(core.exportSecret).toBeCalledWith('TEST_KEY', 1);
});
it('get nested secret', async () => {
mockInput('nested/test e')
await exportSecrets();
expect(core.exportSecret).toBeCalledWith('E', 4);
});
});

3
e2e/jest.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
verbose: true
};

3
jest.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/e2e/'],
};

56
package-lock.json generated
View file

@ -1181,7 +1181,8 @@
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
"defer-to-connect": {
"version": "1.0.2",
@ -1352,6 +1353,7 @@
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz",
"integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==",
"dev": true,
"requires": {
"esprima": "^3.1.3",
"estraverse": "^4.2.0",
@ -1363,24 +1365,22 @@
"esprima": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
"integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
"integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
"dev": true
}
}
},
"esprima": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz",
"integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs="
},
"estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
"exec-sh": {
"version": "0.3.2",
@ -1571,7 +1571,8 @@
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"fb-watchman": {
"version": "2.0.0",
@ -3268,16 +3269,6 @@
"minimist": "^1.2.0"
}
},
"jsonpath": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.0.2.tgz",
"integrity": "sha512-rmzlgFZiQPc6q4HDyK8s9Qb4oxBnI5sF61y/Co5PV0lc3q2bIuRsNdueVbhoSHdKM4fxeimphOAtfz47yjCfeA==",
"requires": {
"esprima": "1.2.2",
"static-eval": "2.0.2",
"underscore": "1.7.0"
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@ -3326,6 +3317,7 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
"dev": true,
"requires": {
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2"
@ -3802,6 +3794,7 @@
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
"integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
"dev": true,
"requires": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.4",
@ -3961,7 +3954,8 @@
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
"dev": true
},
"prepend-http": {
"version": "2.0.0",
@ -4459,7 +4453,8 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-resolve": {
"version": "0.5.2",
@ -4554,14 +4549,6 @@
"integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==",
"dev": true
},
"static-eval": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz",
"integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==",
"requires": {
"escodegen": "^1.8.1"
}
},
"static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@ -4804,6 +4791,7 @@
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
"dev": true,
"requires": {
"prelude-ls": "~1.1.2"
}
@ -4819,11 +4807,6 @@
"source-map": "~0.6.1"
}
},
"underscore": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
"integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk="
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -5010,7 +4993,8 @@
"wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
"dev": true
},
"wrap-ansi": {
"version": "5.1.0",

View file

@ -4,7 +4,8 @@
"description": "A Github Action that allows you to consume vault secrets as secure environment variables.",
"main": "index.js",
"scripts": {
"test": "jest"
"test": "jest",
"test:e2e": "jest -c e2e/jest.config.js"
},
"repository": {
"type": "git",
@ -26,8 +27,7 @@
"homepage": "https://github.com/RichiCoder1/vault-action#readme",
"dependencies": {
"@actions/core": "^1.1.1",
"got": "^9.6.0",
"jsonpath": "^1.0.2"
"got": "^9.6.0"
},
"devDependencies": {
"@types/jest": "^24.0.18",

View file

@ -1,11 +0,0 @@
const got = require('got');
(async () => {
const result = await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/config`, {
headers: {
'X-Vault-Token': 'testtoken'
}
});
console.log(result.body);
})();