diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a9b170..612f183 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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] }} diff --git a/README.md b/README.md index 5464d6a..61a82ba 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/action.js b/action.js index be14f6f..db86e0e 100644 --- a/action.js +++ b/action.js @@ -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} */ + /** @type {Map} */ 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; diff --git a/action.test.js b/action.test.js index 8e399ac..4c916ba 100644 --- a/action.test.js +++ b/action.test.js @@ -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'); - - await exportSecrets(); - - expect(core.exportSecret).toBeCalledWith('TEST', '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'); mockVaultData({ - key: 'SECURE' + key: 1 }); await exportSecrets(); - expect(core.exportSecret).toBeCalledWith('TEST', 'SECURE'); + expect(core.exportSecret).toBeCalledWith('KEY', 1); + }); + + it('mapped key retrieval', async () => { + mockInput('test key|TEST_NAME'); + mockVaultData({ + key: 1 + }); + + await exportSecrets(); + + expect(core.exportSecret).toBeCalledWith('TEST_NAME', 1); }); }); \ No newline at end of file diff --git a/e2e/e2e.test.js b/e2e/e2e.test.js new file mode 100644 index 0000000..c2d5976 --- /dev/null +++ b/e2e/e2e.test.js @@ -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); + }); +}); \ No newline at end of file diff --git a/e2e/jest.config.js b/e2e/jest.config.js new file mode 100644 index 0000000..03d15be --- /dev/null +++ b/e2e/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + verbose: true +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..6cd1ce3 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + testPathIgnorePatterns: ['/node_modules/', '/e2e/'], +}; diff --git a/package-lock.json b/package-lock.json index e779da9..318cd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b18d762..c83724a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/quicktest.js b/quicktest.js deleted file mode 100644 index bcc623c..0000000 --- a/quicktest.js +++ /dev/null @@ -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); -})(); \ No newline at end of file