mirror of
https://github.com/hashicorp/vault-action.git
synced 2026-07-26 00:13:16 +03:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb61006b6d | |||
| 14a4a058b4 | |||
| 2d9c2b9f1b | |||
| d27529ebde | |||
| cd5a8995f3 | |||
| 72c092c8af | |||
| 9c1dce9ef6 | |||
| 9866ce3e18 | |||
| 1f5b7d55d8 | |||
| d1655aec40 | |||
| 1d767e3957 | |||
| c253c155ba | |||
| 3a9100e7d5 | |||
| 256bfb9e6a | |||
| 3bbbc68bd0 | |||
| 74bc2a617b | |||
| 76780d43f5 | |||
| 46540966f1 | |||
| cc5270ec14 | |||
| 130d1f5f4f | |||
| d34ee148bc | |||
| 77bab83f42 | |||
| 5e3dd4f01b | |||
| 7318a98db7 | |||
| b08bc4993d | |||
| 579f9fd8c2 | |||
| 1226471c04 | |||
| fdaeeffa26 | |||
| 0f409d4023 | |||
| 8fa61e9099 | |||
| 132f1c6930 | |||
| f558cc7838 | |||
| d0e05af6a3 | |||
| 1f8e723e55 | |||
| 32d00a142f | |||
| 32838a0d48 | |||
| ed59bea637 | |||
| 2537991e61 | |||
| 7d98524254 | |||
| f380d921ae | |||
| ae2f303e37 | |||
| 55a11671e0 | |||
| ad04ab9377 | |||
| 2dcbd17a34 | |||
| d2b5111993 | |||
| f5817917e5 | |||
| bbf40c0251 | |||
| cebb457349 | |||
| 6ee4dd3797 | |||
| a502b7fa66 | |||
| abba4a3980 | |||
| e025870ee9 | |||
| 4f536680b6 | |||
| fd9b31c94c | |||
| 8e9712e63b | |||
| 25737aea2b | |||
| c2b108a1cb | |||
| 7c940db65a | |||
| d9ec4185f7 | |||
| 203b1c7ae9 | |||
| 0580f85e79 | |||
| b222012f94 | |||
| 40fb8d7236 | |||
| 4aed62f922 | |||
| 4679f8aa3c | |||
| 2f64a97498 | |||
| 25c4aec690 | |||
| c14a190aaa | |||
| 843e7fa30a | |||
| 876cdcfdd3 | |||
| 67281159df | |||
| 61dd38f491 | |||
| 168f9696e8 | |||
| c502100fbe | |||
| b8c90c7243 | |||
| d5a69ceca1 | |||
| f3e4110f8d | |||
| 8c00f7e4fd | |||
| ab957d2aec | |||
| 0451f06f9f | |||
| b6210c5a51 | |||
| 09740f679f | |||
| 72c7a899ca | |||
| 0cf3bd6a39 | |||
| 0723fd7348 | |||
| 0405c26bc9 | |||
| 2eea93c25e | |||
| 4d9cf786d6 | |||
| 575042000a | |||
| cc8ad16bec | |||
| cd1614aee3 | |||
| f3a53220d9 | |||
| 3526e1be65 | |||
| f60544fbda | |||
| 9e8f22534f | |||
| 8417c61f8a | |||
| 0cca23248c | |||
| 8dad53ed10 | |||
| 937d792a86 | |||
| 5e5c06a3c8 | |||
| ff547e1516 | |||
| 1d4917ec58 |
@@ -17,7 +17,7 @@ The yaml of the `vault-action` step, with any sensitive information masked or re
|
|||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Log Output**
|
**Log Output**
|
||||||
For the most verbose logs, [add a secret called `ACTIONS_STEP_DEBUG` with the value `true`](https://github.com/actions/toolkit/blob/master/docs/action-debugging.md). Then, re-run the workflow if possible and post the *raw logs* for the step here with any sensitive information masked or removed.
|
For the most verbose logs, [add a secret called `ACTIONS_STEP_DEBUG` with the value `true`](https://github.com/actions/toolkit/blob/main/docs/action-debugging.md). Then, re-run the workflow if possible and post the *raw logs* for the step here with any sensitive information masked or removed.
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
+57
-30
@@ -1,22 +1,25 @@
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, reopened, synchronize]
|
types: [opened, reopened, synchronize]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
with:
|
||||||
node-version: ''
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||||
|
with:
|
||||||
|
node-version: '16.14.0'
|
||||||
|
|
||||||
- name: Setup NPM Cache
|
- name: Setup NPM Cache
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -36,17 +39,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- name: Run docker-compose
|
- name: Run docker-compose
|
||||||
run: docker-compose up -d vault
|
run: docker-compose up -d vault
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ''
|
node-version: '16.14.0'
|
||||||
|
|
||||||
- name: Setup NPM Cache
|
- name: Setup NPM Cache
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -70,17 +75,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- name: Run docker-compose
|
- name: Run docker-compose
|
||||||
run: docker-compose up -d vault-enterprise
|
run: docker-compose up -d vault-enterprise
|
||||||
|
env:
|
||||||
|
VAULT_LICENSE_CI: ${{ secrets.VAULT_LICENSE_CI }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ''
|
node-version: '16.14.0'
|
||||||
|
|
||||||
- name: Setup NPM Cache
|
- name: Setup NPM Cache
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -93,7 +102,7 @@ jobs:
|
|||||||
- name: NPM Build
|
- name: NPM Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: NPM Run test:intergration:enterprise
|
- name: NPM Run test:integration:enterprise
|
||||||
run: npm run test:integration:enterprise
|
run: npm run test:integration:enterprise
|
||||||
env:
|
env:
|
||||||
VAULT_HOST: localhost
|
VAULT_HOST: localhost
|
||||||
@@ -104,17 +113,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- name: Run docker-compose
|
- name: Run docker-compose
|
||||||
run: docker-compose up -d vault
|
run: docker-compose up -d vault
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ''
|
node-version: '16.14.0'
|
||||||
|
|
||||||
- name: Setup NPM Cache
|
- name: Setup NPM Cache
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -164,25 +175,42 @@ jobs:
|
|||||||
/cubbyhole/test zip | NAMED_CUBBYSECRET ;
|
/cubbyhole/test zip | NAMED_CUBBYSECRET ;
|
||||||
|
|
||||||
- name: Verify Vault Action Outputs
|
- name: Verify Vault Action Outputs
|
||||||
run: npm run test:e2e
|
run: npm run test:integration:e2e
|
||||||
env:
|
env:
|
||||||
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
|
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
|
||||||
|
|
||||||
|
- name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 1/2)
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
url: http://localhost:8200/
|
||||||
|
token: testtoken
|
||||||
|
secrets: |
|
||||||
|
secret/data/test secret | SUBSEQUENT_TEST_SECRET;
|
||||||
|
- name: Test Vault Action Overwrites Env Vars In Subsequent Action (part 2/2)
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
url: http://localhost:8200/
|
||||||
|
token: testtoken
|
||||||
|
secrets: |
|
||||||
|
secret/data/subsequent-test secret | SUBSEQUENT_TEST_SECRET;
|
||||||
|
|
||||||
e2e-tls:
|
e2e-tls:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- name: Run docker-compose
|
- name: Run docker-compose
|
||||||
run: docker-compose up -d vault-tls
|
run: docker-compose up -d vault-tls
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: ''
|
node-version: '16.14.0'
|
||||||
|
|
||||||
- name: Setup NPM Cache
|
- name: Setup NPM Cache
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -255,20 +283,20 @@ jobs:
|
|||||||
clientKey: ${{ secrets.VAULT_CLIENT_KEY }}
|
clientKey: ${{ secrets.VAULT_CLIENT_KEY }}
|
||||||
|
|
||||||
- name: Verify Vault Action Outputs
|
- name: Verify Vault Action Outputs
|
||||||
run: npm run test:e2e-tls
|
run: npm run test:integration:e2e-tls
|
||||||
env:
|
env:
|
||||||
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
|
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
|
||||||
|
|
||||||
# Removing publish step for now.
|
# Removing publish step for now.
|
||||||
# publish:
|
# publish:
|
||||||
# if: github.event_name == 'push' && contains(github.ref, 'master')
|
# if: github.event_name == 'push' && contains(github.ref, 'main')
|
||||||
# runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
# needs: [build, integration, e2e]
|
# needs: [build, integration, e2e]
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v1
|
# - uses: actions/checkout@v1
|
||||||
# - uses: actions/setup-node@v1
|
# - uses: actions/setup-node@v3
|
||||||
# with:
|
# with:
|
||||||
# node-version: ''
|
# node-version: '16.14.0'
|
||||||
# - name: setup npm cache
|
# - name: setup npm cache
|
||||||
# uses: actions/cache@v1
|
# uses: actions/cache@v1
|
||||||
# with:
|
# with:
|
||||||
@@ -279,9 +307,8 @@ jobs:
|
|||||||
# - name: npm install
|
# - name: npm install
|
||||||
# run: npm ci
|
# run: npm ci
|
||||||
# - name: release
|
# - name: release
|
||||||
# if: success() && endsWith(github.ref, 'master')
|
# if: success() && endsWith(github.ref, 'main')
|
||||||
# run: npx semantic-release
|
# run: npx semantic-release
|
||||||
# env:
|
# env:
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
name: JIRA Sync
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, closed, deleted, reopened]
|
types: [opened, closed, deleted, reopened]
|
||||||
@@ -5,83 +6,12 @@ on:
|
|||||||
types: [opened, closed, reopened]
|
types: [opened, closed, reopened]
|
||||||
issue_comment: # Also triggers when commenting on a PR from the conversation view
|
issue_comment: # Also triggers when commenting on a PR from the conversation view
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
name: Jira Sync
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
uses: hashicorp/vault-workflows-common/.github/workflows/jira.yaml@main
|
||||||
name: Jira sync
|
secrets:
|
||||||
steps:
|
JIRA_SYNC_BASE_URL: ${{ secrets.JIRA_SYNC_BASE_URL }}
|
||||||
- name: Check if community user
|
JIRA_SYNC_USER_EMAIL: ${{ secrets.JIRA_SYNC_USER_EMAIL }}
|
||||||
if: github.event.action == 'opened'
|
JIRA_SYNC_API_TOKEN: ${{ secrets.JIRA_SYNC_API_TOKEN }}
|
||||||
id: vault-team-role
|
with:
|
||||||
run: |
|
teams-array: '["applications-eco"]'
|
||||||
TEAM=vault
|
|
||||||
ROLE="$(hub api orgs/hashicorp/teams/${TEAM}/memberships/${{ github.actor }} | jq -r '.role | select(.!=null)')"
|
|
||||||
if [[ -n ${ROLE} ]]; then
|
|
||||||
echo "Actor ${{ github.actor }} is a ${TEAM} team member, skipping ticket creation"
|
|
||||||
else
|
|
||||||
echo "Actor ${{ github.actor }} is not a ${TEAM} team member"
|
|
||||||
fi
|
|
||||||
echo "::set-output name=role::${ROLE}"
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.JIRA_SYNC_GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login
|
|
||||||
uses: atlassian/gajira-login@v2.0.0
|
|
||||||
env:
|
|
||||||
JIRA_BASE_URL: ${{ secrets.JIRA_SYNC_BASE_URL }}
|
|
||||||
JIRA_USER_EMAIL: ${{ secrets.JIRA_SYNC_USER_EMAIL }}
|
|
||||||
JIRA_API_TOKEN: ${{ secrets.JIRA_SYNC_API_TOKEN }}
|
|
||||||
|
|
||||||
- name: Preprocess
|
|
||||||
if: github.event.action == 'opened' || github.event.action == 'created'
|
|
||||||
id: preprocess
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then
|
|
||||||
echo "::set-output name=type::PR"
|
|
||||||
else
|
|
||||||
echo "::set-output name=type::ISS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create ticket
|
|
||||||
if: github.event.action == 'opened' && !steps.vault-team-role.outputs.role
|
|
||||||
uses: tomhjp/gh-action-jira-create@v0.2.0
|
|
||||||
with:
|
|
||||||
project: VAULT
|
|
||||||
issuetype: "GH Issue"
|
|
||||||
summary: "${{ github.event.repository.name }} [${{ steps.preprocess.outputs.type }} #${{ github.event.issue.number || github.event.pull_request.number }}]: ${{ github.event.issue.title || github.event.pull_request.title }}"
|
|
||||||
description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created from GitHub Action for ${{ github.event.issue.html_url || github.event.pull_request.html_url }} from ${{ github.actor }}_"
|
|
||||||
# customfield_10089 is Issue Link custom field
|
|
||||||
# customfield_10091 is team custom field
|
|
||||||
extraFields: '{"fixVersions": [{"name": "TBD"}], "customfield_10091": ["ecosystem", "runtime"], "customfield_10089": "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"}'
|
|
||||||
|
|
||||||
- name: Search
|
|
||||||
if: github.event.action != 'opened'
|
|
||||||
id: search
|
|
||||||
uses: tomhjp/gh-action-jira-search@v0.2.1
|
|
||||||
with:
|
|
||||||
# cf[10089] is Issue Link custom field
|
|
||||||
jql: 'project = "VAULT" and issuetype = "GH Issue" and cf[10089]="${{ github.event.issue.html_url || github.event.pull_request.html_url }}"'
|
|
||||||
|
|
||||||
- name: Sync comment
|
|
||||||
if: github.event.action == 'created' && steps.search.outputs.issue
|
|
||||||
uses: tomhjp/gh-action-jira-comment@v0.2.0
|
|
||||||
with:
|
|
||||||
issue: ${{ steps.search.outputs.issue }}
|
|
||||||
comment: "${{ github.actor }} ${{ github.event.review.state || 'commented' }}:\n\n${{ github.event.comment.body || github.event.review.body }}\n\n${{ github.event.comment.html_url || github.event.review.html_url }}"
|
|
||||||
|
|
||||||
- name: Close ticket
|
|
||||||
if: (github.event.action == 'closed' || github.event.action == 'deleted') && steps.search.outputs.issue
|
|
||||||
uses: atlassian/gajira-transition@v2.0.1
|
|
||||||
with:
|
|
||||||
issue: ${{ steps.search.outputs.issue }}
|
|
||||||
transition: Done
|
|
||||||
|
|
||||||
- name: Reopen ticket
|
|
||||||
if: github.event.action == 'reopened' && steps.search.outputs.issue
|
|
||||||
uses: atlassian/gajira-transition@v2.0.1
|
|
||||||
with:
|
|
||||||
issue: ${{ steps.search.outputs.issue }}
|
|
||||||
transition: "To Do"
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# This is a sample workflow to help test contributions
|
||||||
|
# Change the branch name, url and token to fit with your own environment
|
||||||
|
|
||||||
|
# Use 'on: push' instead of 'on: local-test' if you wish to run the test on github
|
||||||
|
# If running locally with act, run the workflow with 'act local-test'
|
||||||
|
|
||||||
|
# Don't forget to revert the file changes and invalidate any tokens that were committed before opening a pull-request
|
||||||
|
on: local-test
|
||||||
|
|
||||||
|
name: local-test
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: local-test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Import Secrets
|
||||||
|
uses: hashicorp/vault-action@YOUR_BRANCH_NAME
|
||||||
|
with:
|
||||||
|
url: http://localhost:8200
|
||||||
|
method: token
|
||||||
|
token: testtoken
|
||||||
|
secrets: |
|
||||||
|
secret/data/test secret | SAMPLE_SECRET;
|
||||||
@@ -1,5 +1,95 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 2.6.0 (June 7, 2023)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* Add ability to set the `vault_token` output to contain the Vault token after authentication [GH-441](https://github.com/hashicorp/vault-action/pull/441)
|
||||||
|
* Add support for userpass and ldap authentication methods [GH-440](https://github.com/hashicorp/vault-action/pull/440)
|
||||||
|
* Define an output, `errorMessage`, for vault-action's error messages so subsequent steps can read the errors [GH-446](https://github.com/hashicorp/vault-action/pull/446)
|
||||||
|
|
||||||
|
Bugs:
|
||||||
|
|
||||||
|
* Handle undefined response in getSecrets error handler [GH-431](https://github.com/hashicorp/vault-action/pull/431)
|
||||||
|
|
||||||
|
## 2.5.0 (Jan 26th, 2023)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* Adds ability to automatically decode secrets from base64, hex, and utf8 encodings. [GH-408](https://github.com/hashicorp/vault-action/pull/408)
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
* Improves error messages for Vault authentication failures [GH-409](https://github.com/hashicorp/vault-action/pull/409)
|
||||||
|
* bump jest from 28.1.1 to 29.3.1 [GH-397](https://github.com/hashicorp/vault-action/pull/397)
|
||||||
|
* bump @types/jest from 28.1.3 to 29.2.6 [GH-397](https://github.com/hashicorp/vault-action/pull/397), [GH-413](https://github.com/hashicorp/vault-action/pull/413)
|
||||||
|
* bump jsrsasign from 10.5.27 to 10.6.1 [GH-401](https://github.com/hashicorp/vault-action/pull/401)
|
||||||
|
* bump json5 from 2.2.1 to 2.2.3 [GH-404](https://github.com/hashicorp/vault-action/pull/404)
|
||||||
|
* bump minimatch from 3.0.4 to 3.1.2 [GH-410](https://github.com/hashicorp/vault-action/pull/410)
|
||||||
|
|
||||||
|
## 2.4.3 (Nov 8th, 2022)
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
* bump jest-when from 3.5.1 to 3.5.2 [GH-388](https://github.com/hashicorp/vault-action/pull/388)
|
||||||
|
* bump semantic-release from 19.0.3 to 19.0.5 [GH-360](https://github.com/hashicorp/vault-action/pull/360)
|
||||||
|
* bump jsrsasign from 10.5.25 to 10.5.27 [GH-358](https://github.com/hashicorp/vault-action/pull/358)
|
||||||
|
* bump @actions/core from 1.9.0 to 1.10.0 [GH-371](https://github.com/hashicorp/vault-action/pull/371)
|
||||||
|
* update runtime to node16 for action [GH-375](https://github.com/hashicorp/vault-action/pull/375)
|
||||||
|
|
||||||
|
## 2.4.2 (Aug 15, 2022)
|
||||||
|
|
||||||
|
Bugs:
|
||||||
|
|
||||||
|
* Errors due to replication delay for tokens will now be retried [GH-333](https://github.com/hashicorp/vault-action/pull/333)
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
* bump got from 11.5.1 to 11.8.5 [GH-344](https://github.com/hashicorp/vault-action/pull/344)
|
||||||
|
|
||||||
|
## 2.4.1 (April 28th, 2022)
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
* Make secrets parameter optional [GH-299](https://github.com/hashicorp/vault-action/pull/299)
|
||||||
|
* auth/jwt: make "role" input optional [GH-291](https://github.com/hashicorp/vault-action/pull/291)
|
||||||
|
* Write a better error message when secret not found [GH-306](https://github.com/hashicorp/vault-action/pull/306)
|
||||||
|
* bump jest-when from 2.7.2 to 3.5.1 [GH-294](https://github.com/hashicorp/vault-action/pull/294)
|
||||||
|
* bump node-fetch from 2.6.1 to 2.6.7 [GH-308](https://github.com/hashicorp/vault-action/pull/308)
|
||||||
|
* bump @types/jest from 26.0.23 to 27.4.1 [GH-297](https://github.com/hashicorp/vault-action/pull/297)
|
||||||
|
* bump trim-off-newlines from 1.0.1 to 1.0.3 [GH-309](https://github.com/hashicorp/vault-action/pull/309)
|
||||||
|
* bump moment from 2.28.0 to 2.29.2 [GH-304](https://github.com/hashicorp/vault-action/pull/304)
|
||||||
|
* bump @types/got from 9.6.11 to 9.6.12 [GH-266](https://github.com/hashicorp/vault-action/pull/266)
|
||||||
|
|
||||||
|
## 2.4.0 (October 21st, 2021)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* GitHub provided JWT auth is now supported [GH-257](https://github.com/hashicorp/vault-action/pull/257)
|
||||||
|
|
||||||
|
## 2.3.1 (August 23rd, 2021)
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
* bump normalize-url from 4.5.0 to 4.5.1 [GH-227](https://github.com/hashicorp/vault-action/pull/227)
|
||||||
|
* bump path-parse from 1.0.6 to 1.0.7 [GH-239](https://github.com/hashicorp/vault-action/pull/239)
|
||||||
|
|
||||||
|
## 2.3.0 (June 23rd, 2021)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* K8s auth method is now supported [GH-218](https://github.com/hashicorp/vault-action/pull/218)
|
||||||
|
* Custom auth method mount points is configurable [GH-218](https://github.com/hashicorp/vault-action/pull/218)
|
||||||
|
|
||||||
|
## 2.2.0 (May 6th, 2021)
|
||||||
|
|
||||||
|
Security:
|
||||||
|
* multi-line secrets are now properly masked in logs [GH-208](https://github.com/hashicorp/vault-action/pull/208)
|
||||||
|
[CVE-2021-32074](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-32074)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* JWT auth method is now supported [GH-188](https://github.com/hashicorp/vault-action/pull/188)
|
||||||
|
|
||||||
|
## 2.1.2 (January 21st, 2021)
|
||||||
|
|
||||||
|
Bugs:
|
||||||
|
* fixed bug where newlines were being rendered for multi-line secrets [GH-173](https://github.com/hashicorp/vault-action/pull/173)
|
||||||
|
|
||||||
## 2.1.1 (December 15th, 2020)
|
## 2.1.1 (December 15th, 2020)
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|||||||
@@ -10,20 +10,30 @@ A helper action for easily pulling secrets from HashiCorp Vault™.
|
|||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [Example Usage](#example-usage)
|
- [Vault GitHub Action](#vault-github-action)
|
||||||
- [Authentication method](#authentication-method)
|
- [Example Usage](#example-usage)
|
||||||
- [Key Syntax](#key-syntax)
|
- [Authentication Methods](#authentication-methods)
|
||||||
|
- [JWT with GitHub OIDC Tokens](#jwt-with-github-oidc-tokens)
|
||||||
|
- [AppRole](#approle)
|
||||||
|
- [Token](#token)
|
||||||
|
- [GitHub](#github)
|
||||||
|
- [JWT with OIDC Provider](#jwt-with-oidc-provider)
|
||||||
|
- [Kubernetes](#kubernetes)
|
||||||
|
- [Userpass](#userpass)
|
||||||
|
- [Ldap](#ldap)
|
||||||
|
- [Other Auth Methods](#other-auth-methods)
|
||||||
|
- [Key Syntax](#key-syntax)
|
||||||
- [Simple Key](#simple-key)
|
- [Simple Key](#simple-key)
|
||||||
- [Set Output Variable Name](#set-output-variable-name)
|
- [Set Output Variable Name](#set-output-variable-name)
|
||||||
- [Multiple Secrets](#multiple-secrets)
|
- [Multiple Secrets](#multiple-secrets)
|
||||||
- [Nested Secrets](#nested-secrets)
|
- [Other Secret Engines](#other-secret-engines)
|
||||||
- [Other Secret Engines](#other-secret-engines)
|
- [Adding Extra Headers](#adding-extra-headers)
|
||||||
- [Adding Extra Headers](#adding-extra-headers)
|
- [HashiCorp Cloud Platform or Vault Enterprise](#hashicorp-cloud-platform-or-vault-enterprise)
|
||||||
- [Vault Enterprise Features](#vault-enterprise-features)
|
|
||||||
- [Namespace](#namespace)
|
- [Namespace](#namespace)
|
||||||
- [Reference](#reference)
|
- [Reference](#reference)
|
||||||
- [Masking - Hiding Secrets from Logs](#masking---hiding-secrets-from-logs)
|
- [Masking - Hiding Secrets from Logs](#masking---hiding-secrets-from-logs)
|
||||||
- [Normalization](#normalization)
|
- [Normalization](#normalization)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
<!-- /TOC -->
|
<!-- /TOC -->
|
||||||
|
|
||||||
@@ -36,11 +46,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# ...
|
# ...
|
||||||
- name: Import Secrets
|
- name: Import Secrets
|
||||||
uses: hashicorp/vault-action@v2.1.1
|
id: import-secrets
|
||||||
|
uses: hashicorp/vault-action@v2
|
||||||
with:
|
with:
|
||||||
url: https://vault.mycompany.com:8200
|
url: https://vault.mycompany.com:8200
|
||||||
token: ${{ secrets.VaultToken }}
|
token: ${{ secrets.VAULT_TOKEN }}
|
||||||
caCertificate: ${{ secrets.VAULTCA }}
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
|
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
|
||||||
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
|
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
|
||||||
@@ -48,45 +59,250 @@ jobs:
|
|||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authentication method
|
Retrieved secrets are available as environment variables or outputs for subsequent steps:
|
||||||
|
|
||||||
While most workflows will likely use a vault token, you can also use an `approle` to authenticate with Vault. You can configure which by using the `method` parameter:
|
|
||||||
|
|
||||||
- **token**: (by default) you must provide a `token` parameter
|
|
||||||
```yaml
|
```yaml
|
||||||
...
|
#...
|
||||||
with:
|
- name: Step following 'Import Secrets'
|
||||||
url: https://vault.mycompany.com:8200
|
run: |
|
||||||
token: ${{ secrets.VaultToken }}
|
ACCESS_KEY_ID = "${{ env.AWS_ACCESS_KEY_ID }}"
|
||||||
caCertificate: ${{ secrets.VAULTCA }}
|
SECRET_ACCESS_KEY = "${{ steps.import-secrets.outputs.AWS_SECRET_ACCESS_KEY }}"
|
||||||
|
# ...
|
||||||
```
|
```
|
||||||
- **approle**: you must provide a `roleId` & `secretId` parameter
|
|
||||||
|
If your project needs a format other than env vars and step outputs, you can use additional steps to transform them into the desired format.
|
||||||
|
For example, a common pattern is to save all the secrets in a JSON file:
|
||||||
|
```yaml
|
||||||
|
#...
|
||||||
|
- name: Step following 'Import Secrets'
|
||||||
|
run: |
|
||||||
|
touch secrets.json
|
||||||
|
echo "${{ toJson(steps.import-secrets.outputs) }}" >> secrets.json
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Which with our example would yield a file containing:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ACCESS_KEY_ID": "MY_KEY_ID",
|
||||||
|
"SECRET_ACCESS_KEY": "MY_SECRET_KEY",
|
||||||
|
"NPM_TOKEN": "MY_NPM_TOKEN"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that all secrets are masked so programs need to read the file themselves otherwise all values will be replaced with a `***` placeholder.
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication Methods
|
||||||
|
|
||||||
|
Consider using a [Vault authentication method](https://www.vaultproject.io/docs/auth) such as the JWT auth method with
|
||||||
|
[GitHub OIDC tokens](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) or the AppRole auth method. You can configure which by using the `method` parameter.
|
||||||
|
|
||||||
|
### JWT with GitHub OIDC Tokens
|
||||||
|
|
||||||
|
You can configure trust between a GitHub Actions workflow
|
||||||
|
and Vault using the
|
||||||
|
[GitHub's OIDC provider](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect).
|
||||||
|
Each GitHub Actions workflow receives an auto-generated OIDC token with claims
|
||||||
|
to establish the identity of the workflow.
|
||||||
|
|
||||||
|
__Vault Configuration__
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click to toggle instructions for configuring Vault.</summary>
|
||||||
|
|
||||||
|
Set up Vault with the [JWT auth method](https://www.vaultproject.io/api/auth/jwt#configure).
|
||||||
|
Pass the following parameters to your auth method configuration:
|
||||||
|
|
||||||
|
- `oidc_discovery_url`: `https://token.actions.githubusercontent.com`
|
||||||
|
- `bound_issuer`: `https://token.actions.githubusercontent.com`
|
||||||
|
|
||||||
|
|
||||||
|
Configure a [Vault role](https://www.vaultproject.io/api/auth/jwt#create-role) for the auth method.
|
||||||
|
|
||||||
|
- `role_type`: `jwt`
|
||||||
|
|
||||||
|
- `bound_audiences`: `"https://github.com/<org>"`. Update this parameter if
|
||||||
|
you change the `aud` claim in the GitHub OIDC token via the
|
||||||
|
`jwtGithubAudience` parameter in the action config.
|
||||||
|
|
||||||
|
- `user_claim`: Set this to a claim name (e.g., `repository`) in the
|
||||||
|
[GitHub OIDC token](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
|
||||||
|
|
||||||
|
- `bound_claims` OR `bound_subject`: match on [GitHub subject claims](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims).
|
||||||
|
|
||||||
|
- For wildcard (non-exact) matches, use `bound_claims`.
|
||||||
|
|
||||||
|
- `bound_claims_type`: `glob`
|
||||||
|
|
||||||
|
- `bound_claims`: JSON object. Maps one or more claim names to corresponding wildcard values.
|
||||||
|
```json
|
||||||
|
{"sub": "repo:<orgName>/*"}
|
||||||
|
```
|
||||||
|
|
||||||
|
- For exact matches, use `bound_subject`.
|
||||||
|
|
||||||
|
- `bound_claims_type`: `string`
|
||||||
|
|
||||||
|
- `bound_subject`: Must exactly match the `sub` claim in the OIDC token.
|
||||||
|
```plaintext
|
||||||
|
repo:<orgName/repoName>:ref:refs/heads/branchName
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
__GitHub Actions Workflow__
|
||||||
|
|
||||||
|
In the GitHub Actions workflow, the workflow needs permissions to read contents
|
||||||
|
and write the ID token.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
retrieve-secret:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
```
|
||||||
|
|
||||||
|
In the action, provide the name of the Vault role you created to the `role` parameter.
|
||||||
|
You can optionally set the `jwtGithubAudience` parameter to change the `aud`
|
||||||
|
claim from its default.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
...
|
|
||||||
with:
|
with:
|
||||||
url: https://vault.mycompany.com:8200
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
|
role: <Vault JWT Auth Role Name>
|
||||||
|
method: jwt
|
||||||
|
jwtGithubAudience: sigstore # set the GitHub token's aud claim
|
||||||
|
```
|
||||||
|
|
||||||
|
### AppRole
|
||||||
|
|
||||||
|
The [AppRole auth method](https://www.vaultproject.io/docs/auth/approle) allows
|
||||||
|
your GitHub Actions workflow to authenticate to Vault with a pre-defined role.
|
||||||
|
Set the role ID and secret ID as GitHub secrets and pass them to the
|
||||||
|
`roleId` and `secretId` parameters.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
method: approle
|
method: approle
|
||||||
roleId: ${{ secrets.roleId }}
|
roleId: ${{ secrets.VAULT_ROLE_ID }}
|
||||||
secretId: ${{ secrets.secretId }}
|
secretId: ${{ secrets.VAULT_SECRET_ID }}
|
||||||
caCertificate: ${{ secrets.VAULTCA }}
|
|
||||||
```
|
```
|
||||||
- **github**: you must provide the github token as `githubToken`
|
|
||||||
|
### Token
|
||||||
|
|
||||||
|
For the default method of authenticating to Vault,
|
||||||
|
use a [Vault token](https://www.vaultproject.io/docs/concepts/tokens).
|
||||||
|
Set the Vault token as a GitHub secret and pass
|
||||||
|
it to the `token` parameter.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
...
|
|
||||||
with:
|
with:
|
||||||
url: https://vault.mycompany.com:8200
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
|
token: ${{ secrets.VAULT_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub
|
||||||
|
|
||||||
|
The [GitHub auth method](https://www.vaultproject.io/docs/auth/github)
|
||||||
|
requires `read:org` permissions for authentication. The auto-generated `GITHUB_TOKEN`
|
||||||
|
created for projects does not have these permissions and GitHub does not allow this
|
||||||
|
token's permissions to be modified. A new GitHub Token secret must be created with
|
||||||
|
`read:org` permissions to use this authentication method.
|
||||||
|
|
||||||
|
Pass the GitHub token as a GitHub secret into the `githubToken` parameter.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
method: github
|
method: github
|
||||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
caCertificate: ${{ secrets.VAULTCA }}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If any other method is specified and you provide an `authPayload`, the action will attempt to `POST` to `auth/${method}/login` with the provided payload and parse out the client token.
|
### JWT with OIDC Provider
|
||||||
|
|
||||||
|
You can configure trust between your own OIDC Provider and Vault
|
||||||
|
with the JWT auth method. Provide a `role` & `jwtPrivateKey` parameters,
|
||||||
|
additionally you can pass `jwtKeyPassword` & `jwtTtl` parameters
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
|
method: jwt
|
||||||
|
role: <Vault JWT Auth Role Name>
|
||||||
|
jwtPrivateKey: ${{ secrets.JWT_PRIVATE_KEY }}
|
||||||
|
jwtKeyPassword: ${{ secrets.JWT_KEY_PASS }}
|
||||||
|
jwtTtl: 3600 # 1 hour, default value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
Consider the [Kubernetes auth method](https://www.vaultproject.io/docs/auth/kubernetes)
|
||||||
|
when using self-hosted runners on Kubernetes. You must provide the `role` parameter
|
||||||
|
for the Vault role associated with the Kubernetes auth method.
|
||||||
|
You can optionally override the `kubernetesTokenPath` parameter for
|
||||||
|
custom-mounted serviceAccounts.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
|
method: kubernetes
|
||||||
|
role: <Vault Kubernetes Auth Role Name>
|
||||||
|
kubernetesTokenPath: /var/run/secrets/kubernetes.io/serviceaccount/token # default token path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Userpass
|
||||||
|
|
||||||
|
The [Userpass auth method](https://developer.hashicorp.com/vault/docs/auth/userpass) allows
|
||||||
|
your GitHub Actions workflow to authenticate to Vault with a username and password.
|
||||||
|
Set the username and password as GitHub secrets and pass them to the
|
||||||
|
`username` and `password` parameters.
|
||||||
|
|
||||||
|
This is not the same as ldap or okta auth methods.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
|
method: userpass
|
||||||
|
username: ${{ secrets.VAULT_USERNAME }}
|
||||||
|
password: ${{ secrets.VAULT_PASSWORD }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ldap
|
||||||
|
|
||||||
|
The [LDAP auth method](https://developer.hashicorp.com/vault/docs/auth/ldap) allows
|
||||||
|
your GitHub Actions workflow to authenticate to Vault with a username and password inturn verfied with ldap servers.
|
||||||
|
Set the username and password as GitHub secrets and pass them to the
|
||||||
|
`username` and `password` parameters.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
url: https://vault.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
|
method: ldap
|
||||||
|
username: ${{ secrets.VAULT_USERNAME }}
|
||||||
|
password: ${{ secrets.VAULT_PASSWORD }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Auth Methods
|
||||||
|
|
||||||
|
If any other method is specified and you provide an `authPayload`, the action will
|
||||||
|
attempt to `POST` to `auth/${method}/login` with the provided payload and parse out the client token.
|
||||||
|
|
||||||
## Key Syntax
|
## Key Syntax
|
||||||
|
|
||||||
The `secrets` parameter is a set of multiple secret requests separated by the `;` character.
|
The `secrets` parameter is a set of multiple secret requests separated by the `;` character.
|
||||||
|
|
||||||
Each secret request consists of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name.
|
Each secret request consists of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name.
|
||||||
|
Note that the selector is using [JSONata](https://docs.jsonata.org/overview.html) and certain characters in keys may need to be escaped.
|
||||||
|
|
||||||
```raw
|
```raw
|
||||||
{{ Secret Path }} {{ Secret Key or Selector }} | {{ Env/Output Variable Name }}
|
{{ Secret Path }} {{ Secret Key or Selector }} | {{ Env/Output Variable Name }}
|
||||||
@@ -117,7 +333,6 @@ steps:
|
|||||||
# Import config...
|
# Import config...
|
||||||
- name: Sensitive Operation
|
- name: Sensitive Operation
|
||||||
run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'"
|
run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
_**Note:** If you'd like to only use outputs and disable automatic environment variables, you can set the `exportEnv` option to `false`._
|
_**Note:** If you'd like to only use outputs and disable automatic environment variables, you can set the `exportEnv` option to `false`._
|
||||||
@@ -210,11 +425,16 @@ with:
|
|||||||
|
|
||||||
This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to Vault.
|
This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to Vault.
|
||||||
|
|
||||||
## Vault Enterprise Features
|
## HashiCorp Cloud Platform or Vault Enterprise
|
||||||
|
|
||||||
|
If you are using [HCP Vault](https://cloud.hashicorp.com/products/vault)
|
||||||
|
or Vault Enterprise, you may need additional parameters in
|
||||||
|
your GitHub Actions workflow.
|
||||||
|
|
||||||
### Namespace
|
### Namespace
|
||||||
|
|
||||||
If you need to retrieve secrets from a specific Vault namespace, all that's required is an additional parameter specifying the namespace.
|
If you need to retrieve secrets from a specific Vault namespace, set the `namespace`
|
||||||
|
parameter specifying the namespace. In HCP Vault, the namespace defaults to `admin`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
@@ -223,10 +443,10 @@ steps:
|
|||||||
uses: hashicorp/vault-action
|
uses: hashicorp/vault-action
|
||||||
with:
|
with:
|
||||||
url: https://vault-enterprise.mycompany.com:8200
|
url: https://vault-enterprise.mycompany.com:8200
|
||||||
|
caCertificate: ${{ secrets.VAULT_CA_CERT }}
|
||||||
method: token
|
method: token
|
||||||
caCertificate: ${{ secrets.VAULTCA }}
|
token: ${{ secrets.VAULT_TOKEN }}
|
||||||
token: ${{ secrets.VaultToken }}
|
namespace: admin
|
||||||
namespace: ns1
|
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
|
secret/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
|
||||||
secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
|
secret/ci/aws secretKey | AWS_SECRET_ACCESS_KEY ;
|
||||||
@@ -240,17 +460,27 @@ Here are all the inputs available through `with`:
|
|||||||
| Input | Description | Default | Required |
|
| Input | Description | Default | Required |
|
||||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
|
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
|
||||||
| `url` | The URL for the vault endpoint | | ✔ |
|
| `url` | The URL for the vault endpoint | | ✔ |
|
||||||
| `secrets` | A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details | | ✔ |
|
| `secrets` | A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details | | |
|
||||||
| `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | |
|
| `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | |
|
||||||
| `method` | The method to use to authenticate with Vault. | `token` | |
|
| `method` | The method to use to authenticate with Vault. | `token` | |
|
||||||
|
| `role` | Vault role for specified auth method | | |
|
||||||
|
| `path` | Custom vault path, if the auth method was enabled at a different path | | |
|
||||||
| `token` | The Vault Token to be used to authenticate with Vault | | |
|
| `token` | The Vault Token to be used to authenticate with Vault | | |
|
||||||
| `roleId` | The Role Id for App Role authentication | | |
|
| `roleId` | The Role Id for App Role authentication | | |
|
||||||
| `secretId` | The Secret Id for App Role authentication | | |
|
| `secretId` | The Secret Id for App Role authentication | | |
|
||||||
| `githubToken` | The Github Token to be used to authenticate with Vault | | |
|
| `githubToken` | The Github Token to be used to authenticate with Vault | | |
|
||||||
|
| `jwtPrivateKey` | Base64 encoded Private key to sign JWT | | |
|
||||||
|
| `jwtKeyPassword` | Password for key stored in jwtPrivateKey (if needed) | | |
|
||||||
|
| `jwtGithubAudience` | Identifies the recipient ("aud" claim) that the JWT is intended for |`sigstore`| |
|
||||||
|
| `jwtTtl` | Time in seconds, after which token expires | | 3600 |
|
||||||
|
| `kubernetesTokenPath` | The path to the service-account secret with the jwt token for kubernetes based authentication |`/var/run/secrets/kubernetes.io/serviceaccount/token` | |
|
||||||
|
| `username` | The username of the user to log in to Vault as. Available to both Userpass and LDAP auth methods | | |
|
||||||
|
| `password` | The password of the user to log in to Vault as. Available to both Userpass and LDAP auth methods | | |
|
||||||
| `authPayload` | The JSON payload to be sent to Vault when using a custom authentication method. | | |
|
| `authPayload` | The JSON payload to be sent to Vault when using a custom authentication method. | | |
|
||||||
| `extraHeaders` | A string of newline separated extra headers to include on every request. | | |
|
| `extraHeaders` | A string of newline separated extra headers to include on every request. | | |
|
||||||
| `exportEnv` | Whether or not export secrets as environment variables. | `true` | |
|
| `exportEnv` | Whether or not export secrets as environment variables. | `true` | |
|
||||||
| `exportToken` | Whether or not export Vault token as environment variables (i.e VAULT_TOKEN). | `false` | |
|
| `exportToken` | Whether or not export Vault token as environment variables (i.e VAULT_TOKEN). | `false` | |
|
||||||
|
| `outputToken` | Whether or not to set the `vault_token` output to contain the Vault token after authentication. | `false` | |
|
||||||
| `caCertificate` | Base64 encoded CA certificate the server certificate was signed with. | | |
|
| `caCertificate` | Base64 encoded CA certificate the server certificate was signed with. | | |
|
||||||
| `clientCertificate` | Base64 encoded client certificate the action uses to authenticate with Vault when mTLS is enabled. | | |
|
| `clientCertificate` | Base64 encoded client certificate the action uses to authenticate with Vault when mTLS is enabled. | | |
|
||||||
| `clientKey` | Base64 encoded client key the action uses to authenticate with Vault when mTLS is enabled. | | |
|
| `clientKey` | Base64 encoded client key the action uses to authenticate with Vault when mTLS is enabled. | | |
|
||||||
@@ -264,3 +494,70 @@ This action uses GitHub Action's built-in masking, so all variables will automat
|
|||||||
## Normalization
|
## Normalization
|
||||||
|
|
||||||
To make it simpler to consume certain secrets as env vars, if no Env/Output Var Name is specified `vault-action` will replace and `.` chars with `__`, remove any other non-letter or number characters. If you're concerned about the result, it's recommended to provide an explicit Output Var Key.
|
To make it simpler to consume certain secrets as env vars, if no Env/Output Var Name is specified `vault-action` will replace and `.` chars with `__`, remove any other non-letter or number characters. If you're concerned about the result, it's recommended to provide an explicit Output Var Key.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you wish to contribute to this project, the following dependencies are recommended for local development:
|
||||||
|
- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to install dependencies, build project and run tests
|
||||||
|
- [docker](https://docs.docker.com/get-docker/) to run the pre-configured vault containers for acceptance tests
|
||||||
|
- [docker-compose](https://docs.docker.com/compose/) to spin up the pre-configured vault containers for acceptance tests
|
||||||
|
- [act](https://github.com/nektos/act) to run the vault-action locally
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
Use npm to install dependencies and build the project:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vault test instance
|
||||||
|
|
||||||
|
The Github Action needs access to a working Vault instance to function.
|
||||||
|
Multiple docker configurations are available via the docker-compose.yml file to run containers compatible with the various acceptance test suites.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ docker-compose up -d vault # Choose one of: vault, vault-enterprise, vault-tls depending on which tests you would like to run
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead of using one of the dockerized instance, you can also use your own local or remote Vault instance by exporting these environment variables:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ export VAULT_HOST=<YOUR VAULT CLUSTER LOCATION> # localhost if undefined
|
||||||
|
$ export VAULT_PORT=<YOUR VAULT PORT> # 8200 if undefined
|
||||||
|
$ export VAULT_TOKEN=<YOUR VAULT TOKEN> # testtoken if undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running unit tests
|
||||||
|
|
||||||
|
Unit tests can be executed at any time with no dependencies or prior setup.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running acceptance tests
|
||||||
|
|
||||||
|
With a succesful build to take your local changes into account and a working Vault instance configured, you can now run acceptance tests to validate if any regressions were introduced.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm run test:integration:basic # Choose one of: basic, enterprise, e2e, e2e-tls
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the action locally
|
||||||
|
|
||||||
|
You can use the [act](https://github.com/nektos/act) command to test your changes locally if desired. Unfortunately it is not currently possible to use uncommitted local changes for a shared workfow. You will still need to push
|
||||||
|
the changes you would like to validate beforehand. Even if a commit is necessary, this is still a more detailed and faster feedback loop than waiting for the action to be executed by Github in a different repository.
|
||||||
|
|
||||||
|
Push your changes into a feature branch.
|
||||||
|
```sh
|
||||||
|
$ git checkout -b my-feature-branch
|
||||||
|
$ git commit -m "testing new changes"
|
||||||
|
$ git push
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit the ./.github/workflows/local-test.yaml file to use your new feature branch. You may have to additionally edit the vault url, token and secret path if you are not using one of the provided containerized instance.
|
||||||
|
Run your feature branch locally.
|
||||||
|
```sh
|
||||||
|
$ act local-test
|
||||||
|
```
|
||||||
+35
-3
@@ -6,7 +6,7 @@ inputs:
|
|||||||
required: true
|
required: true
|
||||||
secrets:
|
secrets:
|
||||||
description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details'
|
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
|
required: false
|
||||||
namespace:
|
namespace:
|
||||||
description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default'
|
description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default'
|
||||||
required: false
|
required: false
|
||||||
@@ -14,6 +14,12 @@ inputs:
|
|||||||
description: 'The method to use to authenticate with Vault.'
|
description: 'The method to use to authenticate with Vault.'
|
||||||
default: 'token'
|
default: 'token'
|
||||||
required: false
|
required: false
|
||||||
|
role:
|
||||||
|
description: 'Vault role for specified auth method'
|
||||||
|
required: false
|
||||||
|
path:
|
||||||
|
description: 'Custom Vault path, if the auth method was mounted at a different path'
|
||||||
|
required: false
|
||||||
token:
|
token:
|
||||||
description: 'The Vault Token to be used to authenticate with Vault'
|
description: 'The Vault Token to be used to authenticate with Vault'
|
||||||
required: false
|
required: false
|
||||||
@@ -26,6 +32,16 @@ inputs:
|
|||||||
githubToken:
|
githubToken:
|
||||||
description: 'The Github Token to be used to authenticate with Vault'
|
description: 'The Github Token to be used to authenticate with Vault'
|
||||||
required: false
|
required: false
|
||||||
|
kubernetesTokenPath:
|
||||||
|
description: 'The path to the Kubernetes service account secret'
|
||||||
|
required: false
|
||||||
|
default: '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||||
|
username:
|
||||||
|
description: 'The username of the user to log in to Vault as. Available to both Userpass and LDAP auth methods'
|
||||||
|
required: false
|
||||||
|
password:
|
||||||
|
description: 'The password of the user to log in to Vault as. Available to both Userpass and LDAP auth methods'
|
||||||
|
required: false
|
||||||
authPayload:
|
authPayload:
|
||||||
description: 'The JSON payload to be sent to Vault when using a custom authentication method.'
|
description: 'The JSON payload to be sent to Vault when using a custom authentication method.'
|
||||||
required: false
|
required: false
|
||||||
@@ -52,9 +68,25 @@ inputs:
|
|||||||
tlsSkipVerify:
|
tlsSkipVerify:
|
||||||
description: 'When set to true, disables verification of the Vault server certificate. Setting this to true in production is not recommended.'
|
description: 'When set to true, disables verification of the Vault server certificate. Setting this to true in production is not recommended.'
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: 'false'
|
||||||
|
jwtPrivateKey:
|
||||||
|
description: 'Base64 encoded Private key to sign JWT'
|
||||||
|
required: false
|
||||||
|
jwtKeyPassword:
|
||||||
|
description: 'Password for key stored in jwtPrivateKey (if needed)'
|
||||||
|
required: false
|
||||||
|
jwtGithubAudience:
|
||||||
|
description: 'Identifies the recipient ("aud" claim) that the JWT is intended for'
|
||||||
|
required: false
|
||||||
|
jwtTtl:
|
||||||
|
description: 'Time in seconds, after which token expires'
|
||||||
|
required: false
|
||||||
|
default: 3600
|
||||||
|
secretEncodingType:
|
||||||
|
description: 'The encoding type of the secret to decode. If not specified, the secret will not be decoded. Supported values: base64, hex, utf8'
|
||||||
|
required: false
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node16'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
branding:
|
branding:
|
||||||
icon: 'unlock'
|
icon: 'unlock'
|
||||||
|
|||||||
Vendored
+10011
-6877
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ services:
|
|||||||
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}
|
||||||
ports:
|
ports:
|
||||||
- 8200:8200
|
- 8200:8200
|
||||||
privileged: true
|
privileged: true
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
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('../../src/action');
|
||||||
|
|
||||||
|
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||||
|
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||||
|
|
||||||
|
describe('authenticate with approle', () => {
|
||||||
|
let roleId;
|
||||||
|
let secretId;
|
||||||
|
beforeAll(async () => {
|
||||||
|
try {
|
||||||
|
// Verify Connection
|
||||||
|
await got(`${vaultUrl}/v1/secret/config`, {
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await got(`${vaultUrl}/v1/secret/data/approle-test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
data: {
|
||||||
|
secret: 'SUPERSECRET_WITH_APPROLE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable approle
|
||||||
|
try {
|
||||||
|
await got(`${vaultUrl}/v1/sys/auth/approle`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
type: 'approle'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const {response} = error;
|
||||||
|
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
|
||||||
|
// Approle might already be enabled from previous test runs
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create policies
|
||||||
|
await got(`${vaultUrl}/v1/sys/policies/acl/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
"name":"test",
|
||||||
|
"policy":"path \"auth/approle/*\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"auth/approle/role/my-role/role-id\"\n{\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}\npath \"auth/approle/role/my-role/secret-id\"\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/approle-test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/approle-test\" {\n capabilities = [\"read\", \"list\"]\n}\n"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create approle
|
||||||
|
await got(`${vaultUrl}/v1/auth/approle/role/my-role`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
policies: 'test'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get role-id
|
||||||
|
const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, {
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
roleId = roldIdResponse.body.data.role_id;
|
||||||
|
|
||||||
|
// Get secret-id
|
||||||
|
const secretIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/secret-id`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
secretId = secretIdResponse.body.data.secret_id;
|
||||||
|
} catch(err) {
|
||||||
|
console.warn('Create approle', err.response.body);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('method', expect.anything())
|
||||||
|
.mockReturnValueOnce('approle');
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('roleId', expect.anything())
|
||||||
|
.mockReturnValueOnce(roleId);
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secretId', expect.anything())
|
||||||
|
.mockReturnValueOnce(secretId);
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('url', expect.anything())
|
||||||
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockInput(secrets) {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secrets', expect.anything())
|
||||||
|
.mockReturnValueOnce(secrets);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('authenticate with approle', async() => {
|
||||||
|
mockInput('secret/data/approle-test secret');
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_APPROLE');
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -8,20 +8,21 @@ const { when } = require('jest-when');
|
|||||||
const { exportSecrets } = require('../../src/action');
|
const { exportSecrets } = require('../../src/action');
|
||||||
|
|
||||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||||
|
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||||
|
|
||||||
describe('integration', () => {
|
describe('integration', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Verify Connection
|
// Verify Connection
|
||||||
await got(`${vaultUrl}/v1/secret/config`, {
|
await got(`${vaultUrl}/v1/secret/config`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await got(`${vaultUrl}/v1/secret/data/test`, {
|
await got(`${vaultUrl}/v1/secret/data/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
@@ -33,7 +34,7 @@ describe('integration', () => {
|
|||||||
await got(`${vaultUrl}/v1/secret/data/nested/test`, {
|
await got(`${vaultUrl}/v1/secret/data/nested/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
@@ -45,7 +46,7 @@ describe('integration', () => {
|
|||||||
await got(`${vaultUrl}/v1/secret/data/foobar`, {
|
await got(`${vaultUrl}/v1/secret/data/foobar`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
@@ -59,7 +60,7 @@ describe('integration', () => {
|
|||||||
await got(`${vaultUrl}/v1/sys/mounts/secret-kv1`, {
|
await got(`${vaultUrl}/v1/sys/mounts/secret-kv1`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
type: 'kv'
|
type: 'kv'
|
||||||
@@ -77,7 +78,7 @@ describe('integration', () => {
|
|||||||
await got(`${vaultUrl}/v1/secret-kv1/test`, {
|
await got(`${vaultUrl}/v1/secret-kv1/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
secret: 'CUSTOMSECRET',
|
secret: 'CUSTOMSECRET',
|
||||||
@@ -87,7 +88,7 @@ describe('integration', () => {
|
|||||||
await got(`${vaultUrl}/v1/secret-kv1/foobar`, {
|
await got(`${vaultUrl}/v1/secret-kv1/foobar`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
fookv1: 'bar',
|
fookv1: 'bar',
|
||||||
@@ -97,7 +98,7 @@ describe('integration', () => {
|
|||||||
await got(`${vaultUrl}/v1/secret-kv1/nested/test`, {
|
await got(`${vaultUrl}/v1/secret-kv1/nested/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
"other-Secret-dash": 'OTHERCUSTOMSECRET',
|
"other-Secret-dash": 'OTHERCUSTOMSECRET',
|
||||||
@@ -109,20 +110,28 @@ describe('integration', () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('url')
|
.calledWith('url', expect.anything())
|
||||||
.mockReturnValueOnce(`${vaultUrl}`);
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('token')
|
.calledWith('token', expect.anything())
|
||||||
.mockReturnValueOnce('testtoken');
|
.mockReturnValueOnce(vaultToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockInput(secrets) {
|
function mockInput(secrets) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('secrets')
|
.calledWith('secrets', expect.anything())
|
||||||
.mockReturnValueOnce(secrets);
|
.mockReturnValueOnce(secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('prints a nice error message when secret not found', async () => {
|
||||||
|
mockInput(`secret/data/test secret ;
|
||||||
|
secret/data/test secret | NAMED_SECRET ;
|
||||||
|
secret/data/notFound kehe | NO_SIR ;`);
|
||||||
|
|
||||||
|
expect(exportSecrets()).rejects.toEqual(Error(`Unable to retrieve result for "secret/data/notFound" because it was not found: {"errors":[]}`));
|
||||||
|
})
|
||||||
|
|
||||||
it('get simple secret', async () => {
|
it('get simple secret', async () => {
|
||||||
mockInput('secret/data/test secret');
|
mockInput('secret/data/test secret');
|
||||||
|
|
||||||
@@ -199,7 +208,7 @@ describe('integration', () => {
|
|||||||
await got(`${vaultUrl}/v1/cubbyhole/test`, {
|
await got(`${vaultUrl}/v1/cubbyhole/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
jest.mock('@actions/core');
|
||||||
|
jest.mock('@actions/core/lib/command');
|
||||||
|
const core = require('@actions/core');
|
||||||
|
const rsasign = require('jsrsasign');
|
||||||
|
const {
|
||||||
|
privateRsaKey,
|
||||||
|
privateRsaKeyBase64,
|
||||||
|
publicRsaKey
|
||||||
|
} = require('./rsa_keys');
|
||||||
|
|
||||||
|
const got = require('got');
|
||||||
|
const { when } = require('jest-when');
|
||||||
|
|
||||||
|
const { exportSecrets } = require('../../src/action');
|
||||||
|
|
||||||
|
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
|
||||||
|
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Github OIDC response mock
|
||||||
|
* @param {string} aud Audience claim
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function mockGithubOIDCResponse(aud= "https://github.com/hashicorp/vault-action") {
|
||||||
|
const alg = 'RS256';
|
||||||
|
const header = { alg: alg, typ: 'JWT' };
|
||||||
|
const now = rsasign.KJUR.jws.IntDate.getNow();
|
||||||
|
const payload = {
|
||||||
|
jti: "unique-id",
|
||||||
|
sub: "repo:hashicorp/vault-action:ref:refs/heads/main",
|
||||||
|
aud,
|
||||||
|
ref: "refs/heads/main",
|
||||||
|
sha: "commit-sha",
|
||||||
|
repository: "hashicorp/vault-action",
|
||||||
|
repository_owner: "hashicorp",
|
||||||
|
run_id: "1",
|
||||||
|
run_number: "1",
|
||||||
|
run_attempt: "1",
|
||||||
|
actor: "github-username",
|
||||||
|
workflow: "Workflow Name",
|
||||||
|
head_ref: "",
|
||||||
|
base_ref: "",
|
||||||
|
event_name: "push",
|
||||||
|
ref_type: "branch",
|
||||||
|
job_workflow_ref: "hashicorp/vault-action/.github/workflows/workflow.yml@refs/heads/main",
|
||||||
|
iss: 'vault-action',
|
||||||
|
iat: now,
|
||||||
|
nbf: now,
|
||||||
|
exp: now + 3600,
|
||||||
|
};
|
||||||
|
const decryptedKey = rsasign.KEYUTIL.getKey(privateRsaKey);
|
||||||
|
return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sign call inside this function takes a while to run, so cache the default JWT in a constant.
|
||||||
|
const defaultGithubJwt = mockGithubOIDCResponse();
|
||||||
|
|
||||||
|
describe('jwt auth', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Verify Connection
|
||||||
|
await got(`${vaultUrl}/v1/secret/config`, {
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await got(`${vaultUrl}/v1/sys/auth/jwt`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
type: 'jwt'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const {response} = error;
|
||||||
|
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
|
||||||
|
// Auth method might already be enabled from previous test runs
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await got(`${vaultUrl}/v1/sys/policy/reader`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
policy: `
|
||||||
|
path "*" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await got(`${vaultUrl}/v1/auth/jwt/config`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
jwt_validation_pubkeys: publicRsaKey,
|
||||||
|
default_role: "default"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await got(`${vaultUrl}/v1/auth/jwt/role/default`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
role_type: 'jwt',
|
||||||
|
bound_audiences: null,
|
||||||
|
bound_claims: {
|
||||||
|
iss: 'vault-action'
|
||||||
|
},
|
||||||
|
user_claim: 'iss',
|
||||||
|
policies: ['reader']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await got(`${vaultUrl}/v1/secret/data/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
data: {
|
||||||
|
secret: 'SUPERSECRET',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticate with private key', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('url', expect.anything())
|
||||||
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('method', expect.anything())
|
||||||
|
.mockReturnValueOnce('jwt');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('jwtPrivateKey', expect.anything())
|
||||||
|
.mockReturnValueOnce(privateRsaKeyBase64);
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('role', expect.anything())
|
||||||
|
.mockReturnValueOnce('default');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secrets', expect.anything())
|
||||||
|
.mockReturnValueOnce('secret/data/test secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully authenticates', async () => {
|
||||||
|
await exportSecrets();
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticate with Github OIDC', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await got(`${vaultUrl}/v1/auth/jwt/role/default-sigstore`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
role_type: 'jwt',
|
||||||
|
bound_audiences: null,
|
||||||
|
bound_claims: {
|
||||||
|
iss: 'vault-action',
|
||||||
|
aud: 'sigstore',
|
||||||
|
},
|
||||||
|
user_claim: 'iss',
|
||||||
|
policies: ['reader']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('url', expect.anything())
|
||||||
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('method', expect.anything())
|
||||||
|
.mockReturnValueOnce('jwt');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('jwtPrivateKey', expect.anything())
|
||||||
|
.mockReturnValueOnce('');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secrets', expect.anything())
|
||||||
|
.mockReturnValueOnce('secret/data/test secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully authenticates', async () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('role', expect.anything())
|
||||||
|
.mockReturnValueOnce('default');
|
||||||
|
|
||||||
|
when(core.getIDToken)
|
||||||
|
.calledWith(undefined)
|
||||||
|
.mockReturnValueOnce(defaultGithubJwt);
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully authenticates with `jwtGithubAudience` set to `sigstore`', async () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('role', expect.anything())
|
||||||
|
.mockReturnValueOnce('default-sigstore');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('jwtGithubAudience', expect.anything())
|
||||||
|
.mockReturnValueOnce('sigstore');
|
||||||
|
|
||||||
|
when(core.getIDToken)
|
||||||
|
.calledWith(expect.anything())
|
||||||
|
.mockReturnValueOnce(mockGithubOIDCResponse('sigstore'));
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('successfully authenticates as default role without specifying it', async () => {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('role', expect.anything())
|
||||||
|
.mockReturnValueOnce(null);
|
||||||
|
|
||||||
|
when(core.getIDToken)
|
||||||
|
.calledWith(undefined)
|
||||||
|
.mockReturnValueOnce(defaultGithubJwt);
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET');
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
const privateRsaKey = `
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEArcch89X6VuWj/CQtVfaCXUl0Pcv8IJRgICN8X+3zFNrbiTdh
|
||||||
|
kTtrOhdkbEU5VaW6aQiCXX5+4C1T2sXXXT682XJhIjKepyX3aY50Fh59pLCciwAK
|
||||||
|
c5wPy3PVMOhup15u9reiQKxps1SNrqVLyZNjha83qbN9IJvQcMnQghAjjPUeGPMa
|
||||||
|
MMzG1GOnuPOWIiM2kxqRpbugwwTyuepPnakmfkWqVtMIRprPLY6d3liDIUSSRZ7o
|
||||||
|
6vbmgeF+9U4DyaimKVNngrmi+mW0OnyH1eJYLrJEY9tZaRF8xraMZiOcBcyAt6S/
|
||||||
|
TS29HttJ6+zlhcWx34fItEZ8jA5gzhTmspOY8QIDAQABAoIBAQCncXT5qnipOmSk
|
||||||
|
E4fLiNdcY+aplN+/1Lg6v3acSH8s3SUkNkTA1+wd8WRGHv171VCk3BohVD2UbJib
|
||||||
|
+H3nzwfQzjFh7jyI+kBHaYfZuE+AXNy54rQvaXSeqWIG2i+k/Y0WFSM2BetjbFmI
|
||||||
|
qqU3+dive4G69sPeo8RYqV1LtZlLu11j1K1sptcmMi75/cFAB6/uURapNLI978sr
|
||||||
|
cIaOV2BbLs4Yk7ji4YAtpvL+mky9KF56QAsLspBKgsU/Oxy5FkgFORPlaVn9qBFv
|
||||||
|
cdXBsZOWFnZ2+F/OA55WJeQoyO8E9l5+N8TGKpzXbmkUWyKiqbN+AviZYrK6KPxi
|
||||||
|
zXS4SH0xAoGBANUDIhoUTM+u6dJze/i/sSe90k6UOBsSxA8Q7rOfksda4F0JEBPl
|
||||||
|
l4kbfmMVMEeIJrHNaFRE8r/p/J8sQCg4w/wDB5LEdHGaxf3b2Fbai+QHuElrbOXP
|
||||||
|
vQ/UOaaMQKFJlwlfOHpbDzXM3bMdGwfT6DCCoQyrAvVRE/x8veXJ9Fl/AoGBANDZ
|
||||||
|
B+sjiVbIjbWA9debx/QeEjoLB68Pi8DleCOgbtF7c4jJPPDRLz0fce16ePVFpiw4
|
||||||
|
Mu1E8QQdMTxWY+Y4ERNPwXj//PhD5xDfWYdRJ6IgKFK1bqKIwm7BmJn3WLD2JH2J
|
||||||
|
mLR0Wfa7M8OmWBbAS8fe5NvubeqERmbMs+f+eeWPAoGAR4Cgvt5XllNhm8o2MB6w
|
||||||
|
qeV3JfdtCfF3rJMDfXowPAkOTUyQgA1Om7CF8V6YcTqLup13yunGDpPNv+SLuLSt
|
||||||
|
XPfrX+HgMI5Crd9RNH5x/N52hvavfEkKbrjPjU+BFmLsdzHmdHQCnA2j0c8QVsVU
|
||||||
|
KIyA4Q66lHxd2CBLYkozYqMCgYAxiZkoPBiifhWm3LzzdF78V3mpTN54tq5Ghed2
|
||||||
|
Q+KlS6v+4QTUdjnHPMMwOiGgw/GDgZ0KzJSCjk4UasVtYRUjyIIyqj2dwbV4OhIp
|
||||||
|
V6WX/hqya5ifcuLzlHYW5yWha/EB2fZfr017ibHgkX9Jfjk7YnJUfHyT6OYuEhYG
|
||||||
|
TEUrnwKBgHztVFIi0vwELwhVCr37pFzKTTmm7G+SYH2hnvL2o3eCNAxSoE8/+vuP
|
||||||
|
qHxd6MME9OqeuY9s3eimsTuhSxnMN348v3Tr/FnA/VIeEMyDZyPqso1pFylpUnHn
|
||||||
|
67hv/xqXT3+/MHq6AgVWXjwTgn8XNRDfXmHrBIztq6Kzo/kLmthY
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
`;
|
||||||
|
|
||||||
|
const privateRsaKeyBase64 = Buffer.from(privateRsaKey).toString('base64');
|
||||||
|
|
||||||
|
const publicRsaKey = `
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArcch89X6VuWj/CQtVfaC
|
||||||
|
XUl0Pcv8IJRgICN8X+3zFNrbiTdhkTtrOhdkbEU5VaW6aQiCXX5+4C1T2sXXXT68
|
||||||
|
2XJhIjKepyX3aY50Fh59pLCciwAKc5wPy3PVMOhup15u9reiQKxps1SNrqVLyZNj
|
||||||
|
ha83qbN9IJvQcMnQghAjjPUeGPMaMMzG1GOnuPOWIiM2kxqRpbugwwTyuepPnakm
|
||||||
|
fkWqVtMIRprPLY6d3liDIUSSRZ7o6vbmgeF+9U4DyaimKVNngrmi+mW0OnyH1eJY
|
||||||
|
LrJEY9tZaRF8xraMZiOcBcyAt6S/TS29HttJ6+zlhcWx34fItEZ8jA5gzhTmspOY
|
||||||
|
8QIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
`;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
privateRsaKey,
|
||||||
|
privateRsaKeyBase64,
|
||||||
|
publicRsaKey
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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('../../src/action');
|
||||||
|
|
||||||
|
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);
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('url', expect.anything())
|
||||||
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockInput(secrets) {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secrets', expect.anything())
|
||||||
|
.mockReturnValueOnce(secrets);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('authenticate with userpass', async() => {
|
||||||
|
mockInput('secret/data/userpass-test secret');
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_USERPASS');
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -9,5 +9,6 @@ describe('e2e', () => {
|
|||||||
expect(process.env.OTHERALTSECRET).toBe("OTHERCUSTOMSECRET");
|
expect(process.env.OTHERALTSECRET).toBe("OTHERCUSTOMSECRET");
|
||||||
expect(process.env.FOO).toBe("bar");
|
expect(process.env.FOO).toBe("bar");
|
||||||
expect(process.env.NAMED_CUBBYSECRET).toBe("zap");
|
expect(process.env.NAMED_CUBBYSECRET).toBe("zap");
|
||||||
|
expect(process.env.SUBSEQUENT_TEST_SECRET).toBe("SUBSEQUENT_TEST_SECRET");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
const got = require('got');
|
const got = require('got');
|
||||||
|
|
||||||
const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
||||||
|
const vaultToken = `${process.env.VAULT_TOKEN}` === undefined ? `${process.env.VAULT_TOKEN}` : "testtoken";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// Verify Connection
|
// Verify Connection
|
||||||
await got(`http://${vaultUrl}/v1/secret/config`, {
|
await got(`http://${vaultUrl}/v1/secret/config`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await got(`http://${vaultUrl}/v1/secret/data/test`, {
|
await got(`http://${vaultUrl}/v1/secret/data/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
@@ -26,7 +27,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
|||||||
await got(`http://${vaultUrl}/v1/secret/data/nested/test`, {
|
await got(`http://${vaultUrl}/v1/secret/data/nested/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
@@ -38,7 +39,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
|||||||
await got(`http://${vaultUrl}/v1/sys/mounts/my-secret`, {
|
await got(`http://${vaultUrl}/v1/sys/mounts/my-secret`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
type: 'kv'
|
type: 'kv'
|
||||||
@@ -48,7 +49,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
|||||||
await got(`http://${vaultUrl}/v1/my-secret/test`, {
|
await got(`http://${vaultUrl}/v1/my-secret/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
altSecret: 'CUSTOMSECRET',
|
altSecret: 'CUSTOMSECRET',
|
||||||
@@ -58,7 +59,7 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
|||||||
await got(`http://${vaultUrl}/v1/my-secret/nested/test`, {
|
await got(`http://${vaultUrl}/v1/my-secret/nested/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
otherAltSecret: 'OTHERCUSTOMSECRET',
|
otherAltSecret: 'OTHERCUSTOMSECRET',
|
||||||
@@ -68,13 +69,25 @@ const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
|
|||||||
await got(`http://${vaultUrl}/v1/cubbyhole/test`, {
|
await got(`http://${vaultUrl}/v1/cubbyhole/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
zip: 'zap',
|
zip: 'zap',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await got(`http://${vaultUrl}/v1/secret/data/subsequent-test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Vault-Token': vaultToken,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
data: {
|
||||||
|
secret: 'SUBSEQUENT_TEST_SECRET',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { when } = require('jest-when');
|
|||||||
const { exportSecrets } = require('../../src/action');
|
const { exportSecrets } = require('../../src/action');
|
||||||
|
|
||||||
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`;
|
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`;
|
||||||
|
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
|
||||||
|
|
||||||
describe('integration', () => {
|
describe('integration', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -15,7 +16,7 @@ describe('integration', () => {
|
|||||||
// Verify Connection
|
// Verify Connection
|
||||||
await got(`${vaultUrl}/v1/secret/config`, {
|
await got(`${vaultUrl}/v1/secret/config`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,15 +44,15 @@ describe('integration', () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('url')
|
.calledWith('url', expect.anything())
|
||||||
.mockReturnValueOnce(`${vaultUrl}`);
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('token')
|
.calledWith('token', expect.anything())
|
||||||
.mockReturnValueOnce('testtoken');
|
.mockReturnValueOnce(vaultToken);
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('namespace')
|
.calledWith('namespace', expect.anything())
|
||||||
.mockReturnValueOnce('ns1');
|
.mockReturnValueOnce('ns1');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ describe('authenticate with approle', () => {
|
|||||||
// Verify Connection
|
// Verify Connection
|
||||||
await got(`${vaultUrl}/v1/secret/config`, {
|
await got(`${vaultUrl}/v1/secret/config`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ describe('authenticate with approle', () => {
|
|||||||
await got(`${vaultUrl}/v1/sys/auth/approle`, {
|
await got(`${vaultUrl}/v1/sys/auth/approle`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
'X-Vault-Namespace': 'ns2',
|
'X-Vault-Namespace': 'ns2',
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
@@ -157,7 +158,7 @@ describe('authenticate with approle', () => {
|
|||||||
await got(`${vaultUrl}/v1/sys/policies/acl/test`, {
|
await got(`${vaultUrl}/v1/sys/policies/acl/test`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
'X-Vault-Namespace': 'ns2',
|
'X-Vault-Namespace': 'ns2',
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
@@ -170,7 +171,7 @@ describe('authenticate with approle', () => {
|
|||||||
await got(`${vaultUrl}/v1/auth/approle/role/my-role`, {
|
await got(`${vaultUrl}/v1/auth/approle/role/my-role`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
'X-Vault-Namespace': 'ns2',
|
'X-Vault-Namespace': 'ns2',
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
@@ -181,7 +182,7 @@ describe('authenticate with approle', () => {
|
|||||||
// Get role-id
|
// Get role-id
|
||||||
const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, {
|
const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
'X-Vault-Namespace': 'ns2',
|
'X-Vault-Namespace': 'ns2',
|
||||||
},
|
},
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
@@ -192,7 +193,7 @@ describe('authenticate with approle', () => {
|
|||||||
const secretIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/secret-id`, {
|
const secretIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/secret-id`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
'X-Vault-Namespace': 'ns2',
|
'X-Vault-Namespace': 'ns2',
|
||||||
},
|
},
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
@@ -211,16 +212,16 @@ describe('authenticate with approle', () => {
|
|||||||
.calledWith('method', expect.anything())
|
.calledWith('method', expect.anything())
|
||||||
.mockReturnValueOnce('approle');
|
.mockReturnValueOnce('approle');
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('roleId')
|
.calledWith('roleId', expect.anything())
|
||||||
.mockReturnValueOnce(roleId);
|
.mockReturnValueOnce(roleId);
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('secretId')
|
.calledWith('secretId', expect.anything())
|
||||||
.mockReturnValueOnce(secretId);
|
.mockReturnValueOnce(secretId);
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('url')
|
.calledWith('url', expect.anything())
|
||||||
.mockReturnValueOnce(`${vaultUrl}`);
|
.mockReturnValueOnce(`${vaultUrl}`);
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('namespace')
|
.calledWith('namespace', expect.anything())
|
||||||
.mockReturnValueOnce('ns2');
|
.mockReturnValueOnce('ns2');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ async function enableNamespace(name) {
|
|||||||
await got(`${vaultUrl}/v1/sys/namespaces/${name}`, {
|
await got(`${vaultUrl}/v1/sys/namespaces/${name}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -256,7 +257,7 @@ async function enableEngine(path, namespace, version) {
|
|||||||
await got(`${vaultUrl}/v1/sys/mounts/${path}`, {
|
await got(`${vaultUrl}/v1/sys/mounts/${path}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
'X-Vault-Namespace': namespace,
|
'X-Vault-Namespace': namespace,
|
||||||
},
|
},
|
||||||
json: { type: 'kv', config: {}, options: { version }, generate_signing_key: true },
|
json: { type: 'kv', config: {}, options: { version }, generate_signing_key: true },
|
||||||
@@ -277,7 +278,7 @@ async function writeSecret(engine, path, namespace, version, data) {
|
|||||||
await got(`${vaultUrl}/v1/${secretPath}`, {
|
await got(`${vaultUrl}/v1/${secretPath}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Vault-Token': 'testtoken',
|
'X-Vault-Token': vaultToken,
|
||||||
'X-Vault-Namespace': namespace,
|
'X-Vault-Namespace': namespace,
|
||||||
},
|
},
|
||||||
json: secretPayload
|
json: secretPayload
|
||||||
@@ -286,18 +287,6 @@ async function writeSecret(engine, path, namespace, version, data) {
|
|||||||
|
|
||||||
function mockInput(secrets) {
|
function mockInput(secrets) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('secrets')
|
.calledWith('secrets', expect.anything())
|
||||||
.mockReturnValueOnce(secrets);
|
.mockReturnValueOnce(secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockEngineName(name) {
|
|
||||||
when(core.getInput)
|
|
||||||
.calledWith('path')
|
|
||||||
.mockReturnValueOnce(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mockVersion(version) {
|
|
||||||
when(core.getInput)
|
|
||||||
.calledWith('kv-version')
|
|
||||||
.mockReturnValueOnce(version);
|
|
||||||
}
|
|
||||||
|
|||||||
Generated
+7435
-15083
File diff suppressed because it is too large
Load Diff
+10
-21
@@ -8,23 +8,13 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:integration:basic": "jest -c integrationTests/basic/jest.config.js",
|
"test:integration:basic": "jest -c integrationTests/basic/jest.config.js",
|
||||||
"test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js",
|
"test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js",
|
||||||
"test:e2e": "jest -c integrationTests/e2e/jest.config.js",
|
"test:integration:e2e": "jest -c integrationTests/e2e/jest.config.js",
|
||||||
"test:e2e-tls": "jest -c integrationTests/e2e-tls/jest.config.js"
|
"test:integration:e2e-tls": "jest -c integrationTests/e2e-tls/jest.config.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"dist/**/*"
|
"dist/**/*"
|
||||||
],
|
],
|
||||||
"release": {
|
|
||||||
"branch": "master",
|
|
||||||
"plugins": [
|
|
||||||
"@semantic-release/commit-analyzer",
|
|
||||||
"@semantic-release/release-notes-generator",
|
|
||||||
"@semantic-release/github",
|
|
||||||
"@semantic-release/npm"
|
|
||||||
],
|
|
||||||
"ci": false
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/hashicorp/vault-action.git"
|
"url": "git+https://github.com/hashicorp/vault-action.git"
|
||||||
@@ -44,19 +34,18 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/hashicorp/vault-action#readme",
|
"homepage": "https://github.com/hashicorp/vault-action#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"got": "^11.5.1",
|
"got": "^11.8.5",
|
||||||
"jsonata": "^1.8.2"
|
"jsonata": "^2.0.3",
|
||||||
|
"jsrsasign": "^10.8.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@actions/core": ">=1 <2"
|
"@actions/core": ">=1 <2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/core": "^1.2.3",
|
"@actions/core": "^1.10.0",
|
||||||
"@types/got": "^9.6.11",
|
"@vercel/ncc": "^0.36.1",
|
||||||
"@types/jest": "^26.0.13",
|
"jest": "^29.5.0",
|
||||||
"@zeit/ncc": "^0.22.3",
|
"jest-when": "^3.5.2",
|
||||||
"jest": "^26.4.2",
|
"mock-http-server": "^1.4.5"
|
||||||
"jest-when": "^2.7.2",
|
|
||||||
"semantic-release": "^17.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-8
@@ -5,18 +5,22 @@ const got = require('got').default;
|
|||||||
const jsonata = require('jsonata');
|
const jsonata = require('jsonata');
|
||||||
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
|
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
|
||||||
|
|
||||||
const AUTH_METHODS = ['approle', 'token', 'github'];
|
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
|
||||||
|
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
|
||||||
|
|
||||||
async function exportSecrets() {
|
async function exportSecrets() {
|
||||||
const vaultUrl = core.getInput('url', { required: true });
|
const vaultUrl = core.getInput('url', { required: true });
|
||||||
const vaultNamespace = core.getInput('namespace', { required: false });
|
const vaultNamespace = core.getInput('namespace', { required: false });
|
||||||
const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
|
const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
|
||||||
const exportEnv = core.getInput('exportEnv', { required: false }) != '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 exportToken = (core.getInput('exportToken', { required: false }) || 'false').toLowerCase() != 'false';
|
||||||
|
|
||||||
const secretsInput = core.getInput('secrets', { required: true });
|
const secretsInput = core.getInput('secrets', { required: false });
|
||||||
const secretRequests = parseSecretsInput(secretsInput);
|
const secretRequests = parseSecretsInput(secretsInput);
|
||||||
|
|
||||||
|
const secretEncodingType = core.getInput('secretEncodingType', { required: false });
|
||||||
|
|
||||||
const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
|
const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
|
||||||
const authPayload = core.getInput('authPayload', { required: false });
|
const authPayload = core.getInput('authPayload', { required: false });
|
||||||
if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) {
|
if (!AUTH_METHODS.includes(vaultMethod) && !authPayload) {
|
||||||
@@ -26,7 +30,15 @@ async function exportSecrets() {
|
|||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
prefixUrl: vaultUrl,
|
prefixUrl: vaultUrl,
|
||||||
headers: {},
|
headers: {},
|
||||||
https: {}
|
https: {},
|
||||||
|
retry: {
|
||||||
|
statusCodes: [
|
||||||
|
...got.defaults.options.retry.statusCodes,
|
||||||
|
// Vault returns 412 when the token in use hasn't yet been replicated
|
||||||
|
// to the performance replica queried. See issue #332.
|
||||||
|
412,
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tlsSkipVerify = (core.getInput('tlsSkipVerify', { required: false }) || 'false').toLowerCase() != 'false';
|
const tlsSkipVerify = (core.getInput('tlsSkipVerify', { required: false }) || 'false').toLowerCase() != 'false';
|
||||||
@@ -58,11 +70,14 @@ async function exportSecrets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions));
|
const vaultToken = await retrieveToken(vaultMethod, got.extend(defaultOptions));
|
||||||
|
core.setSecret(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) {
|
||||||
|
core.setOutput('vault_token', `${vaultToken}`);
|
||||||
|
}
|
||||||
if (exportToken === true) {
|
if (exportToken === true) {
|
||||||
command.issue('add-mask', vaultToken);
|
|
||||||
core.exportVariable('VAULT_TOKEN', `${vaultToken}`);
|
core.exportVariable('VAULT_TOKEN', `${vaultToken}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +88,28 @@ async function exportSecrets() {
|
|||||||
|
|
||||||
const results = await getSecrets(requests, client);
|
const results = await getSecrets(requests, client);
|
||||||
|
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const { value, request, cachedResponse } = result;
|
// Output the result
|
||||||
|
|
||||||
|
var value = result.value;
|
||||||
|
const request = result.request;
|
||||||
|
const cachedResponse = result.cachedResponse;
|
||||||
|
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
core.debug('ℹ using cached response');
|
core.debug('ℹ using cached response');
|
||||||
}
|
}
|
||||||
command.issue('add-mask', value);
|
|
||||||
|
// if a secret is encoded, decode it
|
||||||
|
if (ENCODING_TYPES.includes(secretEncodingType)) {
|
||||||
|
value = Buffer.from(value, secretEncodingType).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of value.replace(/\r/g, '').split('\n')) {
|
||||||
|
if (line.length > 0) {
|
||||||
|
core.setSecret(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (exportEnv) {
|
if (exportEnv) {
|
||||||
core.exportVariable(request.envVarName, `${value}`);
|
core.exportVariable(request.envVarName, `${value}`);
|
||||||
}
|
}
|
||||||
@@ -87,7 +118,7 @@ async function exportSecrets() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @typedef {Object} SecretRequest
|
/** @typedef {Object} SecretRequest
|
||||||
* @property {string} path
|
* @property {string} path
|
||||||
* @property {string} envVarName
|
* @property {string} envVarName
|
||||||
* @property {string} outputVarName
|
* @property {string} outputVarName
|
||||||
@@ -99,6 +130,10 @@ async function exportSecrets() {
|
|||||||
* @param {string} secretsInput
|
* @param {string} secretsInput
|
||||||
*/
|
*/
|
||||||
function parseSecretsInput(secretsInput) {
|
function parseSecretsInput(secretsInput) {
|
||||||
|
if (!secretsInput) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const secrets = secretsInput
|
const secrets = secretsInput
|
||||||
.split(';')
|
.split(';')
|
||||||
.filter(key => !!key)
|
.filter(key => !!key)
|
||||||
|
|||||||
+89
-11
@@ -2,12 +2,12 @@ jest.mock('got');
|
|||||||
jest.mock('@actions/core');
|
jest.mock('@actions/core');
|
||||||
jest.mock('@actions/core/lib/command');
|
jest.mock('@actions/core/lib/command');
|
||||||
|
|
||||||
|
const command = require('@actions/core/lib/command');
|
||||||
const core = require('@actions/core');
|
const core = require('@actions/core');
|
||||||
const got = require('got');
|
const got = require('got');
|
||||||
const {
|
const {
|
||||||
exportSecrets,
|
exportSecrets,
|
||||||
parseSecretsInput,
|
parseSecretsInput,
|
||||||
parseResponse,
|
|
||||||
parseHeadersInput
|
parseHeadersInput
|
||||||
} = require('./action');
|
} = require('./action');
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ describe('parseSecretsInput', () => {
|
|||||||
describe('parseHeaders', () => {
|
describe('parseHeaders', () => {
|
||||||
it('parses simple header', () => {
|
it('parses simple header', () => {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('extraHeaders')
|
.calledWith('extraHeaders', undefined)
|
||||||
.mockReturnValueOnce('TEST: 1');
|
.mockReturnValueOnce('TEST: 1');
|
||||||
const result = parseHeadersInput('extraHeaders');
|
const result = parseHeadersInput('extraHeaders');
|
||||||
expect(Array.from(result)).toContainEqual(['test', '1']);
|
expect(Array.from(result)).toContainEqual(['test', '1']);
|
||||||
@@ -101,7 +101,7 @@ describe('parseHeaders', () => {
|
|||||||
|
|
||||||
it('parses simple header with whitespace', () => {
|
it('parses simple header with whitespace', () => {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('extraHeaders')
|
.calledWith('extraHeaders', undefined)
|
||||||
.mockReturnValueOnce(`
|
.mockReturnValueOnce(`
|
||||||
TEST: 1
|
TEST: 1
|
||||||
`);
|
`);
|
||||||
@@ -111,7 +111,7 @@ describe('parseHeaders', () => {
|
|||||||
|
|
||||||
it('parses multiple headers', () => {
|
it('parses multiple headers', () => {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('extraHeaders')
|
.calledWith('extraHeaders', undefined)
|
||||||
.mockReturnValueOnce(`
|
.mockReturnValueOnce(`
|
||||||
TEST: 1
|
TEST: 1
|
||||||
FOO: bAr
|
FOO: bAr
|
||||||
@@ -123,7 +123,7 @@ describe('parseHeaders', () => {
|
|||||||
|
|
||||||
it('parses null response', () => {
|
it('parses null response', () => {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('extraHeaders')
|
.calledWith('extraHeaders', undefined)
|
||||||
.mockReturnValueOnce(null);
|
.mockReturnValueOnce(null);
|
||||||
const result = parseHeadersInput('extraHeaders');
|
const result = parseHeadersInput('extraHeaders');
|
||||||
expect(Array.from(result)).toHaveLength(0);
|
expect(Array.from(result)).toHaveLength(0);
|
||||||
@@ -135,29 +135,29 @@ describe('exportSecrets', () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('url')
|
.calledWith('url', expect.anything())
|
||||||
.mockReturnValueOnce('http://vault:8200');
|
.mockReturnValueOnce('http://vault:8200');
|
||||||
|
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('token')
|
.calledWith('token', expect.anything())
|
||||||
.mockReturnValueOnce('EXAMPLE');
|
.mockReturnValueOnce('EXAMPLE');
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockInput(key) {
|
function mockInput(key) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('secrets')
|
.calledWith('secrets', expect.anything())
|
||||||
.mockReturnValueOnce(key);
|
.mockReturnValueOnce(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockVersion(version) {
|
function mockVersion(version) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('kv-version')
|
.calledWith('kv-version', expect.anything())
|
||||||
.mockReturnValueOnce(version);
|
.mockReturnValueOnce(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockExtraHeaders(headerString) {
|
function mockExtraHeaders(headerString) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('extraHeaders')
|
.calledWith('extraHeaders', expect.anything())
|
||||||
.mockReturnValueOnce(headerString);
|
.mockReturnValueOnce(headerString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,10 +180,21 @@ describe('exportSecrets', () => {
|
|||||||
|
|
||||||
function mockExportToken(doExport) {
|
function mockExportToken(doExport) {
|
||||||
when(core.getInput)
|
when(core.getInput)
|
||||||
.calledWith('exportToken')
|
.calledWith('exportToken', expect.anything())
|
||||||
.mockReturnValueOnce(doExport);
|
.mockReturnValueOnce(doExport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockOutputToken(doOutput) {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('outputToken', expect.anything())
|
||||||
|
.mockReturnValueOnce(doOutput);
|
||||||
|
}
|
||||||
|
function mockEncodeType(doEncode) {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secretEncodingType', expect.anything())
|
||||||
|
.mockReturnValueOnce(doEncode);
|
||||||
|
}
|
||||||
|
|
||||||
it('simple secret retrieval', async () => {
|
it('simple secret retrieval', async () => {
|
||||||
mockInput('test key');
|
mockInput('test key');
|
||||||
mockVaultData({
|
mockVaultData({
|
||||||
@@ -196,6 +207,19 @@ describe('exportSecrets', () => {
|
|||||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('encoded secret retrieval', async () => {
|
||||||
|
mockInput('test key');
|
||||||
|
mockVaultData({
|
||||||
|
key: 'MQ=='
|
||||||
|
});
|
||||||
|
mockEncodeType('base64');
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||||
|
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||||
|
});
|
||||||
|
|
||||||
it('intl secret retrieval', async () => {
|
it('intl secret retrieval', async () => {
|
||||||
mockInput('测试 测试');
|
mockInput('测试 测试');
|
||||||
mockVaultData({
|
mockVaultData({
|
||||||
@@ -294,4 +318,58 @@ describe('exportSecrets', () => {
|
|||||||
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
expect(core.exportVariable).toBeCalledWith('KEY', '1');
|
||||||
expect(core.setOutput).toBeCalledWith('key', '1');
|
expect(core.setOutput).toBeCalledWith('key', '1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('single-line secret gets masked', async () => {
|
||||||
|
mockInput('test key');
|
||||||
|
mockVaultData({
|
||||||
|
key: 'secret'
|
||||||
|
});
|
||||||
|
mockExportToken("false")
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.setSecret).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
expect(core.setSecret).toBeCalledWith('secret');
|
||||||
|
expect(core.setOutput).toBeCalledWith('key', 'secret');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multi-line secret gets masked for each line', async () => {
|
||||||
|
const multiLineString = `a multi-line string
|
||||||
|
|
||||||
|
with blank lines
|
||||||
|
|
||||||
|
`
|
||||||
|
mockInput('test key');
|
||||||
|
mockVaultData({
|
||||||
|
key: multiLineString
|
||||||
|
});
|
||||||
|
mockExportToken("false")
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.setSecret).toBeCalledTimes(3); // 1 for each non-empty line.
|
||||||
|
|
||||||
|
expect(core.setSecret).toBeCalledWith('a multi-line string');
|
||||||
|
expect(core.setSecret).toBeCalledWith('with blank lines');
|
||||||
|
expect(core.setOutput).toBeCalledWith('key', multiLineString);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('export only Vault token, no secrets', async () => {
|
||||||
|
mockExportToken("true")
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.exportVariable).toBeCalledTimes(1);
|
||||||
|
expect(core.exportVariable).toBeCalledWith('VAULT_TOKEN', 'EXAMPLE');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('output only Vault token, no secrets', async () => {
|
||||||
|
mockOutputToken("true")
|
||||||
|
|
||||||
|
await exportSecrets();
|
||||||
|
|
||||||
|
expect(core.setOutput).toBeCalledTimes(1);
|
||||||
|
expect(core.setOutput).toBeCalledWith('vault_token', 'EXAMPLE');
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
+84
-6
@@ -1,22 +1,64 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
const core = require('@actions/core');
|
const core = require('@actions/core');
|
||||||
|
const rsasign = require('jsrsasign');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { default: got } = require('got');
|
||||||
|
|
||||||
|
const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||||
/***
|
/***
|
||||||
* Authenticate with Vault and retrieve a Vault token that can be used for requests.
|
* Authenticate with Vault and retrieve a Vault token that can be used for requests.
|
||||||
* @param {string} method
|
* @param {string} method
|
||||||
* @param {import('got').Got} client
|
* @param {import('got').Got} client
|
||||||
*/
|
*/
|
||||||
async function retrieveToken(method, client) {
|
async function retrieveToken(method, client) {
|
||||||
|
let path = core.getInput('path', { required: false }) || method;
|
||||||
|
path = `v1/auth/${path}/login`
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'approle': {
|
case 'approle': {
|
||||||
const vaultRoleId = core.getInput('roleId', { required: true });
|
const vaultRoleId = core.getInput('roleId', { required: true });
|
||||||
const vaultSecretId = core.getInput('secretId', { required: true });
|
const vaultSecretId = core.getInput('secretId', { required: true });
|
||||||
return await getClientToken(client, method, { role_id: vaultRoleId, secret_id: vaultSecretId });
|
return await getClientToken(client, method, path, { role_id: vaultRoleId, secret_id: vaultSecretId });
|
||||||
}
|
}
|
||||||
case 'github': {
|
case 'github': {
|
||||||
const githubToken = core.getInput('githubToken', { required: true });
|
const githubToken = core.getInput('githubToken', { required: true });
|
||||||
return await getClientToken(client, method, { token: githubToken });
|
return await getClientToken(client, method, path, { token: githubToken });
|
||||||
}
|
}
|
||||||
|
case 'jwt': {
|
||||||
|
/** @type {string} */
|
||||||
|
let jwt;
|
||||||
|
const role = core.getInput('role', { required: false });
|
||||||
|
const privateKeyRaw = core.getInput('jwtPrivateKey', { required: false });
|
||||||
|
const privateKey = Buffer.from(privateKeyRaw, 'base64').toString();
|
||||||
|
const keyPassword = core.getInput('jwtKeyPassword', { required: false });
|
||||||
|
const tokenTtl = core.getInput('jwtTtl', { required: false }) || '3600'; // 1 hour
|
||||||
|
const githubAudience = core.getInput('jwtGithubAudience', { required: false });
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
jwt = await core.getIDToken(githubAudience)
|
||||||
|
} else {
|
||||||
|
jwt = generateJwt(privateKey, keyPassword, Number(tokenTtl));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getClientToken(client, method, path, { jwt: jwt, role: role });
|
||||||
|
}
|
||||||
|
case 'kubernetes': {
|
||||||
|
const role = core.getInput('role', { required: true })
|
||||||
|
const tokenPath = core.getInput('kubernetesTokenPath', { required: false }) || defaultKubernetesTokenPath
|
||||||
|
const data = fs.readFileSync(tokenPath, 'utf8')
|
||||||
|
if (!(role && data) && data != "") {
|
||||||
|
throw new Error("Role Name must be set and a kubernetes token must set")
|
||||||
|
}
|
||||||
|
return await getClientToken(client, method, path, { jwt: data, role: role })
|
||||||
|
}
|
||||||
|
case 'userpass':
|
||||||
|
case 'ldap': {
|
||||||
|
const username = core.getInput('username', { required: true });
|
||||||
|
const password = core.getInput('password', { required: true });
|
||||||
|
path = path + `/${username}`
|
||||||
|
return await getClientToken(client, method, path, { password: password })
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
if (!method || method === 'token') {
|
if (!method || method === 'token') {
|
||||||
return core.getInput('token', { required: true });
|
return core.getInput('token', { required: true });
|
||||||
@@ -26,19 +68,46 @@ async function retrieveToken(method, client) {
|
|||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw Error('When using a custom authentication method, you must provide the payload');
|
throw Error('When using a custom authentication method, you must provide the payload');
|
||||||
}
|
}
|
||||||
return await getClientToken(client, method, JSON.parse(payload.trim()));
|
return await getClientToken(client, method, path, JSON.parse(payload.trim()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Generates signed Json Web Token with specified private key and ttl
|
||||||
|
* @param {string} privateKey
|
||||||
|
* @param {string} keyPassword
|
||||||
|
* @param {number} ttl
|
||||||
|
*/
|
||||||
|
function generateJwt(privateKey, keyPassword, ttl) {
|
||||||
|
const alg = 'RS256';
|
||||||
|
const header = { alg: alg, typ: 'JWT' };
|
||||||
|
const now = rsasign.KJUR.jws.IntDate.getNow();
|
||||||
|
const payload = {
|
||||||
|
iss: 'vault-action',
|
||||||
|
iat: now,
|
||||||
|
nbf: now,
|
||||||
|
exp: now + ttl,
|
||||||
|
event: process.env.GITHUB_EVENT_NAME,
|
||||||
|
workflow: process.env.GITHUB_WORKFLOW,
|
||||||
|
sha: process.env.GITHUB_SHA,
|
||||||
|
actor: process.env.GITHUB_ACTOR,
|
||||||
|
repository: process.env.GITHUB_REPOSITORY,
|
||||||
|
ref: process.env.GITHUB_REF
|
||||||
|
};
|
||||||
|
const decryptedKey = rsasign.KEYUTIL.getKey(privateKey, keyPassword);
|
||||||
|
return rsasign.KJUR.jws.JWS.sign(alg, JSON.stringify(header), JSON.stringify(payload), decryptedKey);
|
||||||
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Call the appropriate login endpoint and parse out the token in the response.
|
* Call the appropriate login endpoint and parse out the token in the response.
|
||||||
* @param {import('got').Got} client
|
* @param {import('got').Got} client
|
||||||
* @param {string} method
|
* @param {string} method
|
||||||
|
* @param {string} path
|
||||||
* @param {any} payload
|
* @param {any} payload
|
||||||
*/
|
*/
|
||||||
async function getClientToken(client, method, payload) {
|
async function getClientToken(client, method, path, payload) {
|
||||||
/** @type {'json'} */
|
/** @type {'json'} */
|
||||||
const responseType = 'json';
|
const responseType = 'json';
|
||||||
var options = {
|
var options = {
|
||||||
@@ -46,10 +115,19 @@ async function getClientToken(client, method, payload) {
|
|||||||
responseType,
|
responseType,
|
||||||
};
|
};
|
||||||
|
|
||||||
core.debug(`Retrieving Vault Token from v1/auth/${method}/login endpoint`);
|
core.debug(`Retrieving Vault Token from ${path} endpoint`);
|
||||||
|
|
||||||
/** @type {import('got').Response<VaultLoginResponse>} */
|
/** @type {import('got').Response<VaultLoginResponse>} */
|
||||||
const response = await client.post(`v1/auth/${method}/login`, options);
|
let response;
|
||||||
|
try {
|
||||||
|
response = await client.post(`${path}`, options);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof got.HTTPError) {
|
||||||
|
throw Error(`failed to retrieve vault token. code: ${err.code}, message: ${err.message}, vaultResponse: ${JSON.stringify(err.response.body)}`)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
if (response && response.body && response.body.auth && response.body.auth.client_token) {
|
if (response && response.body && response.body.auth && response.body.auth.client_token) {
|
||||||
core.debug('✔ Vault Token successfully retrieved');
|
core.debug('✔ Vault Token successfully retrieved');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
jest.mock('got');
|
||||||
|
jest.mock('@actions/core');
|
||||||
|
jest.mock('@actions/core/lib/command');
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
stat: jest.fn().mockResolvedValue(null),
|
||||||
|
promises: {
|
||||||
|
access: jest.fn().mockResolvedValue(null),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const core = require('@actions/core');
|
||||||
|
const got = require('got');
|
||||||
|
const fs = require("fs")
|
||||||
|
const { when } = require('jest-when');
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
retrieveToken
|
||||||
|
} = require('./auth');
|
||||||
|
|
||||||
|
|
||||||
|
function mockInput(name, key) {
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith(name, expect.anything())
|
||||||
|
.mockReturnValueOnce(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockApiResponse() {
|
||||||
|
const response = { body: { auth: { client_token: testToken, renewable: true, policies: [], accessor: "accessor" } } }
|
||||||
|
got.post = jest.fn()
|
||||||
|
got.post.mockReturnValue(response)
|
||||||
|
}
|
||||||
|
const testToken = "testoken";
|
||||||
|
|
||||||
|
describe("test retrival for token", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("test retrival with approle", async () => {
|
||||||
|
const method = 'approle'
|
||||||
|
mockApiResponse()
|
||||||
|
const testRoleId = "testRoleId"
|
||||||
|
const testSecretId = "testSecretId"
|
||||||
|
mockInput("roleId", testRoleId)
|
||||||
|
mockInput("secretId", testSecretId)
|
||||||
|
const token = await retrieveToken(method, got)
|
||||||
|
expect(token).toEqual(testToken)
|
||||||
|
const payload = got.post.mock.calls[0][1].json
|
||||||
|
expect(payload).toEqual({ role_id: testRoleId, secret_id: testSecretId })
|
||||||
|
const url = got.post.mock.calls[0][0]
|
||||||
|
expect(url).toContain('approle')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("test retrival with github token", async () => {
|
||||||
|
const method = 'github'
|
||||||
|
mockApiResponse()
|
||||||
|
const githubToken = "githubtoken"
|
||||||
|
mockInput("githubToken", githubToken)
|
||||||
|
const token = await retrieveToken(method, got)
|
||||||
|
expect(token).toEqual(testToken)
|
||||||
|
const payload = got.post.mock.calls[0][1].json
|
||||||
|
expect(payload).toEqual({ token: githubToken })
|
||||||
|
const url = got.post.mock.calls[0][0]
|
||||||
|
expect(url).toContain('github')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("test retrival with kubernetes", async () => {
|
||||||
|
const method = 'kubernetes'
|
||||||
|
const jwtToken = "someJwtToken"
|
||||||
|
const testRole = "testRole"
|
||||||
|
const testTokenPath = "testTokenPath"
|
||||||
|
const testPath = 'differentK8sPath'
|
||||||
|
mockApiResponse()
|
||||||
|
mockInput("kubernetesTokenPath", testTokenPath)
|
||||||
|
mockInput("role", testRole)
|
||||||
|
mockInput("path", testPath)
|
||||||
|
fs.readFileSync = jest.fn()
|
||||||
|
fs.readFileSync.mockReturnValueOnce(jwtToken)
|
||||||
|
const token = await retrieveToken(method, got)
|
||||||
|
expect(token).toEqual(testToken)
|
||||||
|
const payload = got.post.mock.calls[0][1].json
|
||||||
|
expect(payload).toEqual({ jwt: jwtToken, role: testRole })
|
||||||
|
const url = got.post.mock.calls[0][0]
|
||||||
|
expect(url).toContain('differentK8sPath')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,6 +5,7 @@ const { exportSecrets } = require('./action');
|
|||||||
try {
|
try {
|
||||||
await core.group('Get Vault Secrets', exportSecrets);
|
await core.group('Get Vault Secrets', exportSecrets);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
core.setOutput("errorMessage", error.message);
|
||||||
core.setFailed(error.message);
|
core.setFailed(error.message);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
jest.mock('@actions/core');
|
||||||
|
|
||||||
|
const core = require('@actions/core');
|
||||||
|
const ServerMock = require("mock-http-server");
|
||||||
|
const { exportSecrets } = require("./action");
|
||||||
|
const { when } = require('jest-when');
|
||||||
|
|
||||||
|
describe('exportSecrets retries', () => {
|
||||||
|
var server = new ServerMock({ host: "127.0.0.1", port: 0 });
|
||||||
|
var calls = 0;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
calls = 0;
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('token', expect.anything())
|
||||||
|
.mockReturnValueOnce('EXAMPLE');
|
||||||
|
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('secrets', expect.anything())
|
||||||
|
.mockReturnValueOnce("kv/mysecret key");
|
||||||
|
|
||||||
|
server.start(() => {
|
||||||
|
expect(server.getHttpPort()).not.toBeNull();
|
||||||
|
when(core.getInput)
|
||||||
|
.calledWith('url', expect.anything())
|
||||||
|
.mockReturnValueOnce('http://127.0.0.1:' + server.getHttpPort());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach((done) => {
|
||||||
|
server.stop(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockStatusCodes(statusCodes) {
|
||||||
|
server.on({
|
||||||
|
path: '/v1/kv/mysecret',
|
||||||
|
reply: {
|
||||||
|
status: function() {
|
||||||
|
let status = statusCodes[calls];
|
||||||
|
calls += 1;
|
||||||
|
return status;
|
||||||
|
},
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: function() {
|
||||||
|
return JSON.stringify({ data: {"key": "value"} })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('retries on 412 status code', (done) => {
|
||||||
|
mockStatusCodes([412, 200])
|
||||||
|
exportSecrets().then(() => {
|
||||||
|
expect(calls).toEqual(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries on 500 status code', (done) => {
|
||||||
|
mockStatusCodes([500, 200])
|
||||||
|
exportSecrets().then(() => {
|
||||||
|
expect(calls).toEqual(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+16
-8
@@ -34,9 +34,17 @@ async function getSecrets(secretRequests, client) {
|
|||||||
body = responseCache.get(requestPath);
|
body = responseCache.get(requestPath);
|
||||||
cachedResponse = true;
|
cachedResponse = true;
|
||||||
} else {
|
} else {
|
||||||
const result = await client.get(requestPath);
|
try {
|
||||||
body = result.body;
|
const result = await client.get(requestPath);
|
||||||
responseCache.set(requestPath, body);
|
body = result.body;
|
||||||
|
responseCache.set(requestPath, body);
|
||||||
|
} catch (error) {
|
||||||
|
const {response} = error;
|
||||||
|
if (response?.statusCode === 404) {
|
||||||
|
throw Error(`Unable to retrieve result for "${path}" because it was not found: ${response.body.trim()}`)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!selector.match(/.*[\.].*/)) {
|
if (!selector.match(/.*[\.].*/)) {
|
||||||
selector = '"' + selector + '"'
|
selector = '"' + selector + '"'
|
||||||
@@ -47,7 +55,7 @@ async function getSecrets(secretRequests, client) {
|
|||||||
selector = "data." + selector
|
selector = "data." + selector
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = selectData(body, selector);
|
const value = await selectData(body, selector);
|
||||||
results.push({
|
results.push({
|
||||||
request: secretRequest,
|
request: secretRequest,
|
||||||
value,
|
value,
|
||||||
@@ -62,18 +70,18 @@ async function getSecrets(secretRequests, client) {
|
|||||||
* @param {object} data
|
* @param {object} data
|
||||||
* @param {string} selector
|
* @param {string} selector
|
||||||
*/
|
*/
|
||||||
function selectData(data, selector) {
|
async function selectData(data, selector) {
|
||||||
const ata = jsonata(selector);
|
const ata = jsonata(selector);
|
||||||
let result = JSON.stringify(ata.evaluate(data));
|
let result = JSON.stringify(await ata.evaluate(data));
|
||||||
// Compat for custom engines
|
// Compat for custom engines
|
||||||
if (!result && ((ata.ast().type === "path" && ata.ast()['steps'].length === 1) || ata.ast().type === "string") && selector !== 'data' && 'data' in data) {
|
if (!result && ((ata.ast().type === "path" && ata.ast()['steps'].length === 1) || ata.ast().type === "string") && selector !== 'data' && 'data' in data) {
|
||||||
result = JSON.stringify(jsonata(`data.${selector}`).evaluate(data));
|
result = JSON.stringify(await jsonata(`data.${selector}`).evaluate(data));
|
||||||
} else if (!result) {
|
} else if (!result) {
|
||||||
throw Error(`Unable to retrieve result for ${selector}. No match data was found. Double check your Key or Selector.`);
|
throw Error(`Unable to retrieve result for ${selector}. No match data was found. Double check your Key or Selector.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.startsWith(`"`)) {
|
if (result.startsWith(`"`)) {
|
||||||
result = result.substring(1, result.length - 1);
|
result = JSON.parse(result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user