mirror of
https://github.com/hashicorp/vault-action.git
synced 2025-11-08 15:46:56 +00:00
parent
2fb925f14c
commit
2e4d53b28d
6 changed files with 268 additions and 41 deletions
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
118
integrationTests/basic/revoke.test.js
Normal file
118
integrationTests/basic/revoke.test.js
Normal 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"]}');
|
||||||
|
})
|
||||||
|
});
|
||||||
101
src/action.js
101
src/action.js
|
|
@ -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;
|
||||||
|
|
@ -56,12 +44,12 @@ async function exportSecrets() {
|
||||||
|
|
||||||
const clientCertificateRaw = core.getInput('clientCertificate', { required: false });
|
const clientCertificateRaw = core.getInput('clientCertificate', { required: false });
|
||||||
if (clientCertificateRaw != null) {
|
if (clientCertificateRaw != null) {
|
||||||
defaultOptions.https.certificate = Buffer.from(clientCertificateRaw, 'base64').toString();
|
defaultOptions.https.certificate = Buffer.from(clientCertificateRaw, 'base64').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientKeyRaw = core.getInput('clientKey', { required: false });
|
const clientKeyRaw = core.getInput('clientKey', { required: false });
|
||||||
if (clientKeyRaw != null) {
|
if (clientKeyRaw != null) {
|
||||||
defaultOptions.https.key = Buffer.from(clientKeyRaw, 'base64').toString();
|
defaultOptions.https.key = Buffer.from(clientKeyRaw, 'base64').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [headerName, headerValue] of extraHeaders) {
|
for (const [headerName, headerValue] of extraHeaders) {
|
||||||
|
|
@ -72,13 +60,37 @@ 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);
|
||||||
|
|
||||||
if (outputToken === true) {
|
if (outputToken === true) {
|
||||||
core.setOutput('vault_token', `${vaultToken}`);
|
core.setOutput('vault_token', `${vaultToken}`);
|
||||||
}
|
}
|
||||||
if (exportToken === true) {
|
if (exportToken === true) {
|
||||||
core.exportVariable('VAULT_TOKEN', `${vaultToken}`);
|
core.exportVariable('VAULT_TOKEN', `${vaultToken}`);
|
||||||
|
|
@ -134,7 +146,7 @@ async function exportSecrets() {
|
||||||
*/
|
*/
|
||||||
function parseSecretsInput(secretsInput) {
|
function parseSecretsInput(secretsInput) {
|
||||||
if (!secretsInput) {
|
if (!secretsInput) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const secrets = secretsInput
|
const secrets = secretsInput
|
||||||
|
|
@ -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
11
src/revoke.js
Normal 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
38
src/revoke.test.js
Normal 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])
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue