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

initial commit

https://github.com/orgs/community/discussions/65593
This commit is contained in:
Kevin Schoonover 2023-11-05 13:05:16 -08:00
parent 2fb925f14c
commit 2e4d53b28d
6 changed files with 268 additions and 41 deletions

View file

@ -92,6 +92,7 @@ inputs:
runs: runs:
using: 'node16' using: 'node16'
main: 'dist/index.js' main: 'dist/index.js'
post: "dist/cache-save/index.js"
branding: branding:
icon: 'unlock' icon: 'unlock'
color: 'gray-dark' color: 'gray-dark'

View file

@ -8,26 +8,26 @@ services:
ports: ports:
- 8200:8200 - 8200:8200
privileged: true privileged: true
vault-enterprise: # vault-enterprise:
image: hashicorp/vault-enterprise:latest # image: hashicorp/vault-enterprise:latest
environment: # environment:
VAULT_DEV_ROOT_TOKEN_ID: testtoken # VAULT_DEV_ROOT_TOKEN_ID: testtoken
VAULT_LICENSE: ${VAULT_LICENSE_CI} # VAULT_LICENSE: ${VAULT_LICENSE_CI}
ports: # ports:
- 8200:8200 # - 8200:8200
privileged: true # privileged: true
vault-tls: # vault-tls:
image: hashicorp/vault:latest # image: hashicorp/vault:latest
hostname: vault-tls # hostname: vault-tls
environment: # environment:
VAULT_CAPATH: /etc/vault/ca.crt # VAULT_CAPATH: /etc/vault/ca.crt
ports: # ports:
- 8200:8200 # - 8200:8200
privileged: true # privileged: true
volumes: # volumes:
- ${PWD}/integrationTests/e2e-tls/configs:/etc/vault # - ${PWD}/integrationTests/e2e-tls/configs:/etc/vault
- vault-data:/var/lib/vault:rw # - vault-data:/var/lib/vault:rw
entrypoint: vault server -config=/etc/vault/config.hcl # entrypoint: vault server -config=/etc/vault/config.hcl
volumes: volumes:
vault-data: vault-data:

View file

@ -0,0 +1,118 @@
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 { revokeToken, getDefaultOptions, VAULT_TOKEN_STATE } = require('../../src/action');
const { retrieveToken } = require('../../src/auth');
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
describe('authenticate with userpass', () => {
const username = `testUsername`;
const password = `testPassword`;
beforeAll(async () => {
try {
// Verify Connection
await got(`${vaultUrl}/v1/secret/config`, {
headers: {
'X-Vault-Token': vaultToken,
},
});
await got(`${vaultUrl}/v1/secret/data/userpass-test`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken,
},
json: {
data: {
secret: 'SUPERSECRET_WITH_USERPASS',
},
},
});
// Enable userpass
try {
await got(`${vaultUrl}/v1/sys/auth/userpass`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken
},
json: {
type: 'userpass'
},
});
} catch (error) {
const { response } = error;
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
// Userpass might already be enabled from previous test runs
} else {
throw error;
}
}
// Create policies
await got(`${vaultUrl}/v1/sys/policies/acl/userpass-test`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken
},
json: {
"name": "userpass-test",
"policy": `path \"auth/userpass/*\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"auth/userpass/users/${username}\"\n{\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}\n\npath \"secret/data/*\" {\n capabilities = [\"list\"]\n}\npath \"secret/metadata/*\" {\n capabilities = [\"list\"]\n}\n\npath \"secret/data/userpass-test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/userpass-test\" {\n capabilities = [\"read\", \"list\"]\n}\n`
},
});
// Create user
await got(`${vaultUrl}/v1/auth/userpass/users/${username}`, {
method: 'POST',
headers: {
'X-Vault-Token': vaultToken
},
json: {
password: `${password}`,
policies: 'userpass-test'
},
});
} catch (err) {
console.warn('Create user in userpass', err.response.body);
throw err;
}
});
beforeEach(() => {
jest.resetAllMocks();
when(core.getInput)
.calledWith('method', expect.anything())
.mockReturnValueOnce('userpass');
when(core.getInput)
.calledWith('username', expect.anything())
.mockReturnValueOnce(username);
when(core.getInput)
.calledWith('password', expect.anything())
.mockReturnValueOnce(password);
// also queried by revokeToken
when(core.getInput)
.calledWith('url', expect.anything())
.mockReturnValue(`${vaultUrl}`);
when(core.getInput)
.calledWith('revokeToken', expect.anything())
.mockReturnValueOnce('true');
});
it('revoke token', async () => {
const defaultOptions = getDefaultOptions();
const vaultToken = await retrieveToken("userpass", got.extend(defaultOptions));
when(core.getState).calledWith(VAULT_TOKEN_STATE).mockReturnValue(vaultToken);
await revokeToken()
// token is now revoked so we can't revoke again
await expect(revokeToken())
.rejects
.toThrow('failed to revoke vault token. code: ERR_NON_2XX_3XX_RESPONSE, message: Response code 403 (Forbidden), vaultResponse: {"errors":["permission denied"]}');
})
});

View file

@ -8,27 +8,13 @@ const { WILDCARD } = require('./constants');
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index'); const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
const VAULT_TOKEN_STATE = "VAULT_TOKEN";
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass']; const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
const ENCODING_TYPES = ['base64', 'hex', 'utf8']; const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
async function exportSecrets() {
function getDefaultOptions() {
const vaultUrl = core.getInput('url', { required: true }); const vaultUrl = core.getInput('url', { required: true });
const vaultNamespace = core.getInput('namespace', { required: false });
const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
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 secretsInput = core.getInput('secrets', { required: false });
const secretRequests = parseSecretsInput(secretsInput);
const secretEncodingType = core.getInput('secretEncodingType', { required: false });
const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
const authPayload = core.getInput('authPayload', { required: false });
if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) {
throw Error(`Sorry, the provided authentication method ${vaultMethod} is not currently supported and no custom authPayload was provided.`);
}
const defaultOptions = { const defaultOptions = {
prefixUrl: vaultUrl, prefixUrl: vaultUrl,
@ -44,6 +30,8 @@ async function exportSecrets() {
} }
} }
const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
const vaultNamespace = core.getInput('namespace', { required: false });
const tlsSkipVerify = (core.getInput('tlsSkipVerify', { required: false }) || 'false').toLowerCase() != 'false'; const tlsSkipVerify = (core.getInput('tlsSkipVerify', { required: false }) || 'false').toLowerCase() != 'false';
if (tlsSkipVerify === true) { if (tlsSkipVerify === true) {
defaultOptions.https.rejectUnauthorized = false; defaultOptions.https.rejectUnauthorized = false;
@ -72,8 +60,32 @@ async function exportSecrets() {
defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace; defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace;
} }
return defaultOptions
}
async function exportSecrets() {
const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
const revokeToken = core.getInput("revokeToken", { required: false }) !== 'false'
const outputToken = (core.getInput('outputToken', { required: false }) || 'false').toLowerCase() != 'false';
const exportToken = (core.getInput('exportToken', { required: false }) || 'false').toLowerCase() != 'false';
const secretsInput = core.getInput('secrets', { required: false });
const secretRequests = parseSecretsInput(secretsInput);
const secretEncodingType = core.getInput('secretEncodingType', { required: false });
const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
const authPayload = core.getInput('authPayload', { required: false });
if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) {
throw Error(`Sorry, the provided authentication method ${vaultMethod} is not currently supported and no custom authPayload was provided.`);
}
const defaultOptions = getDefaultOptions();
const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions)); const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions));
core.setSecret(vaultToken) core.setSecret(vaultToken)
if (revokeToken) {
core.saveState(VAULT_TOKEN_STATE, vaultToken)
}
defaultOptions.headers['X-Vault-Token'] = vaultToken; defaultOptions.headers['X-Vault-Token'] = vaultToken;
const client = got.extend(defaultOptions); const client = got.extend(defaultOptions);
@ -219,9 +231,56 @@ function parseHeadersInput(inputKey, inputOptions) {
}, new Map()); }, new Map());
} }
async function revokeToken() {
const token = core.getState(VAULT_TOKEN_STATE)
if (!token || token === "") {
core.debug(`provided token in state (${VAULT_TOKEN_STATE}) is empty. skipping...`)
return
}
core.setSecret(token)
const defaultOptions = getDefaultOptions();
defaultOptions.headers['X-Vault-Token'] = token;
const client = got.extend(defaultOptions);
try {
await revokeClientToken(client)
} catch (err) {
throw err
}
}
/**
* @param {import('got').Got} client
*/
async function revokeClientToken(client) {
const path = "v1/auth/token/revoke-self"
/** @type {'json'} */
const responseType = 'json';
var options = {
responseType,
};
core.debug(`Revoking Vault Token from ${path} endpoint`);
let response;
try {
response = await client.post(path, options);
} catch (err) {
if (err instanceof got.HTTPError) {
throw Error(`failed to revoke vault token. code: ${err.code}, message: ${err.message}, vaultResponse: ${JSON.stringify(err.response.body)}`)
} else {
throw err
}
}
core.debug('✔ Vault Token successfully revoked');
}
module.exports = { module.exports = {
exportSecrets, exportSecrets,
parseSecretsInput, parseSecretsInput,
parseHeadersInput, parseHeadersInput,
getDefaultOptions,
revokeToken,
VAULT_TOKEN_STATE
}; };

11
src/revoke.js Normal file
View file

@ -0,0 +1,11 @@
const core = require('@actions/core');
const { revokeToken } = require('./action');
(async () => {
try {
await revokeToken()
} catch (error) {
core.setOutput("errorMessage", error.message);
core.setFailed(error.message);
}
})();

38
src/revoke.test.js Normal file
View file

@ -0,0 +1,38 @@
jest.mock('got');
jest.mock('@actions/core');
const core = require('@actions/core');
const got = require('got');
const { when } = require('jest-when');
const {
revokeClientToken
} = require('./revoke');
function mockApiResponse() {
const response = ""
got.post = jest.fn()
got.post.mockReturnValue(response)
}
function mockApiFailure() {
const response = { "errors": ["permission denied"] }
got.post = jest.fn()
got.post.mockReturnValue(response)
}
describe("test revoke for token", () => {
beforeEach(() => {
jest.resetAllMocks();
});
it("test revoke with success", async () => {
mockApiResponse()
await revokeClientToken(got)
console.log(got.post.mock.calls[0][1])
})
it("test revoke with error", async () => {
mockApiFailure()
await revokeClientToken(got)
console.log(got.post.mock.calls[0][1])
})
})