diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e3ac7e..9e49f71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,39 @@ jobs: VAULT_PORT: ${{ job.services.vault.ports[8200] }} CI: true + test-ent: + runs-on: ubuntu-latest + + services: + vault: + image: hashicorp/vault-enterprise:1.3.0_ent + ports: + - 8200/tcp + env: + VAULT_DEV_ROOT_TOKEN_ID: testtoken + options: --cap-add=IPC_LOCK + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js 10.x + uses: actions/setup-node@v1 + with: + node-version: 10.x + - name: npm install + run: npm ci + - name: npm build + run: npm run build + - name: npm run test + run: npm run test + env: + CI: true + - name: npm run test:integration-ent + run: npm run test:integration-ent + env: + VAULT_HOST: localhost + VAULT_PORT: ${{ job.services.vault.ports[8200] }} + CI: true + e2e: runs-on: ubuntu-latest diff --git a/README.md b/README.md index f17d567..8d3e5fe 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,25 @@ with: ci/aws secretKey | AWS_SECRET_ACCESS_KEY ``` +### Namespace + +This action could be use with namespace Vault Enterprise feature. You can specify namespace in request : + +```yaml +steps: + # ... + - name: Import Secrets + uses: RichiCoder1/vault-action + with: + url: https://vault-enterprise.mycompany.com:8200 + token: ${{ secrets.VaultToken }} + namespace: ns1 + secrets: | + ci/aws accessKey | AWS_ACCESS_KEY_ID ; + ci/aws secretKey | AWS_SECRET_ACCESS_KEY ; + ci npm_token +``` + ## Masking This action uses Github Action's built in masking, so all variables will automatically be masked if printed to the console or to logs. diff --git a/action.js b/action.js index a8c9a98..5b136cf 100644 --- a/action.js +++ b/action.js @@ -5,17 +5,23 @@ const got = require('got'); async function exportSecrets() { const vaultUrl = core.getInput('url', { required: true }); const vaultToken = core.getInput('token', { required: true }); + const vaultNamespace = core.getInput('namespace', { required: false }); const secretsInput = core.getInput('secrets', { required: true }); const secrets = parseSecretsInput(secretsInput); for (const secret of secrets) { const { secretPath, outputName, secretKey } = secret; - const result = await got(`${vaultUrl}/v1/secret/data/${secretPath}`, { + const requestOptions = { headers: { 'X-Vault-Token': vaultToken - } - }); + }}; + + if (vaultNamespace != null){ + requestOptions.headers["X-Vault-Namespace"] = vaultNamespace + } + + const result = await got(`${vaultUrl}/v1/secret/data/${secretPath}`, requestOptions); const parsedResponse = JSON.parse(result.body); const vaultKeyData = parsedResponse.data; @@ -91,4 +97,4 @@ module.exports = { exportSecrets, parseSecretsInput, normalizeOutputKey -}; \ No newline at end of file +}; diff --git a/action.yml b/action.yml index 1e5366d..e6f702a 100644 --- a/action.yml +++ b/action.yml @@ -10,9 +10,12 @@ inputs: 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: true + namespace: + description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default' + required: false runs: using: 'node12' main: 'dist/index.js' branding: icon: 'unlock' - color: 'gray-dark' \ No newline at end of file + color: 'gray-dark' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..041702d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +# Start vault server locally +# You can run integration tests against server by running +# `VAULT_HOST=localhost VAULT_PORT=8200 CI=true npm run test:integration-ent` +version: "3.0" +services: + vault: + image: hashicorp/vault-enterprise:1.3.0_ent + environment: + VAULT_DEV_ROOT_TOKEN_ID: testtoken + ports: + - 8200:8200 + privileged: true \ No newline at end of file diff --git a/integration-ent/integration.test.js b/integration-ent/integration.test.js new file mode 100644 index 0000000..ede9c59 --- /dev/null +++ b/integration-ent/integration.test.js @@ -0,0 +1,131 @@ +jest.mock('@actions/core'); +jest.mock('@actions/core/lib/command'); +const core = require('@actions/core'); + +const got = require('got'); +const { when } = require('jest-when'); + +const { exportSecrets } = require('../action'); + +describe('integration', () => { + + beforeAll(async () => { + // Verify Connection + await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/config`, { + headers: { + 'X-Vault-Token': 'testtoken', + }, + }); + + // Create namespace + await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/sys/namespaces/ns1`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: true, + }); + + // Enable secret engine + await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/sys/mounts/secret`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns1', + }, + body: {"path":"secret","type":"kv","config":{},"options":{"version":2},"generate_signing_key":true}, + json: true, + }); + + await got(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}/v1/secret/data/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns1', + }, + body: { + data: { + secret: "SUPERSECRET_IN_NAMESPACE", + }, + }, + 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', + 'X-Vault-Namespace': 'ns1', + }, + body: { + data: { + otherSecret: "OTHERSUPERSECRET_IN_NAMESPACE", + }, + }, + json: true, + }); + + + + }) + beforeEach(() => { + jest.resetAllMocks(); + + when(core.getInput) + .calledWith('url') + .mockReturnValue(`http://${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`); + + when(core.getInput) + .calledWith('token') + .mockReturnValue('testtoken'); + + when(core.getInput) + .calledWith('namespace') + .mockReturnValue('ns1'); + }); + + function mockInput(secrets) { + when(core.getInput) + .calledWith('secrets') + .mockReturnValue(secrets); + } + + it('get simple secret', async () => { + mockInput('test secret') + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_IN_NAMESPACE'); + }); + + it('re-map secret', async () => { + mockInput('test secret | TEST_KEY') + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('TEST_KEY', 'SUPERSECRET_IN_NAMESPACE'); + }); + + it('get nested secret', async () => { + mockInput('nested/test otherSecret') + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET_IN_NAMESPACE'); + }); + + it('get multiple secrets', async () => { + mockInput(` + test secret ; + test secret | NAMED_SECRET ; + nested/test otherSecret ;`); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledTimes(3); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_IN_NAMESPACE'); + expect(core.exportVariable).toBeCalledWith('NAMED_SECRET', 'SUPERSECRET_IN_NAMESPACE'); + expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET_IN_NAMESPACE'); + }); +}); \ No newline at end of file diff --git a/integration-ent/jest.config.js b/integration-ent/jest.config.js new file mode 100644 index 0000000..03d15be --- /dev/null +++ b/integration-ent/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + verbose: true +}; diff --git a/jest.config.js b/jest.config.js index 2355f35..df9fa1c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - testPathIgnorePatterns: ['/node_modules/', '/integration/', '/e2e/'], + testPathIgnorePatterns: ['/node_modules/', '/integration/', '/e2e/','/integration-ent'], }; diff --git a/package.json b/package.json index bcfc78b..c294fa6 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "ncc build index.js -o dist", "test": "jest", "test:integration": "jest -c integration/jest.config.js", + "test:integration-ent": "jest -c integration-ent/jest.config.js", "test:e2e": "jest -c e2e/jest.config.js" }, "release": {