Merge pull request #1 from 1Password/env-export-action
Create initial action
This commit is contained in:
10
.github/workflows/lint.yml
vendored
Normal file
10
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
on: pull_request
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: ShellCheck
|
||||||
|
uses: ludeeus/action-shellcheck@1.1.0
|
||||||
51
.github/workflows/test.yml
vendored
Normal file
51
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
on: push
|
||||||
|
name: Run acceptance tests
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Launch 1Password Connect instance
|
||||||
|
env:
|
||||||
|
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||||
|
run: |
|
||||||
|
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
|
||||||
|
docker-compose -f tests/fixtures/docker-compose.yml up -d && sleep 10
|
||||||
|
- name: Configure 1Password Connect
|
||||||
|
uses: ./configure # 1password/load-secrets-action/configure@<version>
|
||||||
|
with:
|
||||||
|
connect-host: http://localhost:8080
|
||||||
|
connect-token: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
|
- name: Load secrets
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
env:
|
||||||
|
SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
|
||||||
|
SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/password
|
||||||
|
UNMASKED_VALUE: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/username
|
||||||
|
- name: Load multiline secret
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
env:
|
||||||
|
MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
|
||||||
|
- name: Print environment variables with masked secrets
|
||||||
|
run: printenv
|
||||||
|
- name: Assert test secret values
|
||||||
|
run: ./tests/assert-env-set.sh
|
||||||
|
- name: Remove secrets
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
with:
|
||||||
|
unset-previous: true
|
||||||
|
- name: Print environment variables with secrets removed
|
||||||
|
run: printenv
|
||||||
|
- name: Assert removed secrets
|
||||||
|
run: ./tests/assert-env-unset.sh
|
||||||
|
- name: Load secret again
|
||||||
|
uses: ./ # 1password/load-secrets-action@<version>
|
||||||
|
env:
|
||||||
|
SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/password
|
||||||
|
SECRET_IN_SECTION: op://v5pz6venw4roosmkzdq2nhpv6u/hrgkzhrlvscomepxlgafb2m3ca/test-section/password
|
||||||
|
MULTILINE_SECRET: op://v5pz6venw4roosmkzdq2nhpv6u/ghtz3jvcc6dqmzc53d3r3eskge/notesPlain
|
||||||
|
- name: Print environment variables with masked secrets
|
||||||
|
run: printenv
|
||||||
|
- name: Assert test secret values again
|
||||||
|
run: ./tests/assert-env-set.sh
|
||||||
17
action.yml
Normal file
17
action.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Load secrets from 1Password
|
||||||
|
description: Make secrets from 1Password Connect available as environment variables in the next steps.
|
||||||
|
author: 1Password
|
||||||
|
branding:
|
||||||
|
icon: lock
|
||||||
|
color: blue
|
||||||
|
inputs:
|
||||||
|
unset-previous:
|
||||||
|
description: Whether to unset environment variables populated by 1Password in earlier job steps
|
||||||
|
default: false
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
export INPUT_UNSET_PREVIOUS=${{ inputs.unset-previous }}
|
||||||
|
${{ github.action_path }}/entrypoint.sh
|
||||||
|
shell: bash
|
||||||
16
configure/action.yml
Normal file
16
configure/action.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: Configure 1Password Connect
|
||||||
|
description: Persist 1Password Connect host and token for use in next steps.
|
||||||
|
author: 1Password
|
||||||
|
inputs:
|
||||||
|
connect-host:
|
||||||
|
description: Your 1Password Connect instance URL
|
||||||
|
connect-token:
|
||||||
|
description: Token to authenticate to your 1Password Connect instance
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
export INPUT_CONNECT_HOST=${{ inputs.connect-host }}
|
||||||
|
export INPUT_CONNECT_TOKEN=${{ inputs.connect-token }}
|
||||||
|
${{ github.action_path }}/entrypoint.sh
|
||||||
|
shell: bash
|
||||||
16
configure/entrypoint.sh
Executable file
16
configure/entrypoint.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Capture Connect configuration in $GITHUB_ENV, giving (optional) inputs
|
||||||
|
# precendence over OP_CONNECT_* environment variables.
|
||||||
|
|
||||||
|
OP_CONNECT_HOST="${INPUT_CONNECT_HOST:-$OP_CONNECT_HOST}"
|
||||||
|
if [ -n "$OP_CONNECT_HOST" ]; then
|
||||||
|
echo "OP_CONNECT_HOST=$OP_CONNECT_HOST" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
OP_CONNECT_TOKEN="${INPUT_CONNECT_TOKEN:-$OP_CONNECT_TOKEN}"
|
||||||
|
if [ -n "$OP_CONNECT_TOKEN" ]; then
|
||||||
|
echo "OP_CONNECT_TOKEN=$OP_CONNECT_TOKEN" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
134
entrypoint.sh
Executable file
134
entrypoint.sh
Executable file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck disable=SC2046,SC2001,SC2086
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$OP_CONNECT_TOKEN" ] || [ -z "$OP_CONNECT_HOST" ]; then
|
||||||
|
echo "\$OP_CONNECT_TOKEN and \$OP_CONNECT_HOST must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
managed_variables_var="OP_MANAGED_VARIABLES"
|
||||||
|
IFS=',' read -r -a managed_variables <<< "$(printenv $managed_variables_var)"
|
||||||
|
|
||||||
|
# Unset all secrets managed by 1Password if `unset-previous` is set.
|
||||||
|
if [ "$INPUT_UNSET_PREVIOUS" == "true" ]; then
|
||||||
|
echo "Unsetting previous values..."
|
||||||
|
|
||||||
|
# Find environment variables that are managed by 1Password.
|
||||||
|
for env_var in "${managed_variables[@]}"; do
|
||||||
|
echo "Unsetting $env_var"
|
||||||
|
unset $env_var
|
||||||
|
|
||||||
|
echo "$env_var=" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Keep the masks, just in case.
|
||||||
|
done
|
||||||
|
|
||||||
|
managed_variables=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Iterate over environment varables to find 1Password references, load the secret values,
|
||||||
|
# and make them available as environment variables in the next steps.
|
||||||
|
IFS=$'\n'
|
||||||
|
for possible_ref in $(printenv | grep "=op://" | grep -v "^#"); do
|
||||||
|
env_var=$(echo "$possible_ref" | cut -d '=' -f1)
|
||||||
|
ref=$(printenv $env_var)
|
||||||
|
|
||||||
|
if [[ ! $ref == "op://"* ]]; then
|
||||||
|
echo "Not really a reference: $ref"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
path=$(echo $ref | sed -e "s/^op:\/\///")
|
||||||
|
if [ $(echo "$path" | tr -cd '/' | wc -c) -lt 2 ]; then
|
||||||
|
echo "Expected path to be in format op://<vault>/<item>[/<section>]/<field>: $ref"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Populating variable: $env_var"
|
||||||
|
|
||||||
|
vault=""
|
||||||
|
item=""
|
||||||
|
section=""
|
||||||
|
field=""
|
||||||
|
i=0
|
||||||
|
IFS="/"
|
||||||
|
for component in $path; do
|
||||||
|
((i+=1))
|
||||||
|
case "$i" in
|
||||||
|
1) vault=$component ;;
|
||||||
|
2) item=$component ;;
|
||||||
|
3) section=$component ;;
|
||||||
|
4) field=$component ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
unset IFS
|
||||||
|
|
||||||
|
# If field is not set, it may have wrongfully been interpreted as the section.
|
||||||
|
if [ -z "$field" ]; then
|
||||||
|
field="$section"
|
||||||
|
section=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Loading item $item from vault $vault..."
|
||||||
|
item_json=$(curl -sSf -H "Content-Type: application/json" -H "Authorization: Bearer $OP_CONNECT_TOKEN" "$OP_CONNECT_HOST/v1/vaults/$vault/items/$item")
|
||||||
|
jq_field_selector=".id == \"$field\" or .label == \"$field\""
|
||||||
|
jq_section_selector=".section == null"
|
||||||
|
|
||||||
|
# If the reference contains a section, edit the jq selector to take that into account.
|
||||||
|
if [ -n "$section" ]; then
|
||||||
|
echo "Looking for section: $section"
|
||||||
|
section_id=$(echo "$item_json" | jq -r ".sections[] | select(.id == \"$section\" or .label == \"$section\") | .id")
|
||||||
|
jq_section_selector=".section.id == \"$section_id\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq_secret_selector="$jq_section_selector and ($jq_field_selector)"
|
||||||
|
|
||||||
|
echo "Looking for field: $field"
|
||||||
|
secret_field_json=$(echo "$item_json" | jq -r "first(.fields[] | select($jq_secret_selector))")
|
||||||
|
|
||||||
|
field_type=$(echo "$secret_field_json" | jq -r '.type')
|
||||||
|
field_purpose=$(echo "$secret_field_json" | jq -r '.purpose')
|
||||||
|
secret_value=$(echo "$secret_field_json" | jq -r '.value')
|
||||||
|
|
||||||
|
if [ -z "$secret_value" ]; then
|
||||||
|
echo "Could not find or access secret $ref"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If the field is marked as concealed or is a note, register a mask
|
||||||
|
# for the secret to prevent accidental log exposure.
|
||||||
|
if [ "$field_type" == "CONCEALED" ] || [ "$field_purpose" == "NOTES" ]; then
|
||||||
|
# To support multiline secrets, escape percent signs and add a mask per line.
|
||||||
|
escaped_mask_value=$(echo "$secret_value" | sed -e 's/%/%25/g')
|
||||||
|
IFS=$'\n'
|
||||||
|
for line in $escaped_mask_value; do
|
||||||
|
if [ "${#line}" -lt 3 ]; then
|
||||||
|
# To avoid false positives and unreadable logs, omit mask for lines that are too short.
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "::add-mask::$line"
|
||||||
|
done
|
||||||
|
unset IFS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# To support multiline secrets, we'll use the heredoc syntax to populate the environment variables.
|
||||||
|
# As the heredoc identifier, we'll use a randomly generated 64-character string,
|
||||||
|
# so that collisions are practically impossible.
|
||||||
|
random_heredoc_identifier=$(openssl rand -hex 16)
|
||||||
|
|
||||||
|
{
|
||||||
|
# Populate env var, using heredoc syntax with generated identifier
|
||||||
|
echo "$env_var<<${random_heredoc_identifier}"
|
||||||
|
echo "$secret_value"
|
||||||
|
echo "${random_heredoc_identifier}"
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
managed_variables+=("$env_var")
|
||||||
|
done
|
||||||
|
unset IFS
|
||||||
|
|
||||||
|
# Add extra env var that lists which secrets are managed by 1Password so that in a later step
|
||||||
|
# these can be unset again.
|
||||||
|
managed_variables_str=$(IFS=','; echo "${managed_variables[*]}")
|
||||||
|
echo "$managed_variables_var=$managed_variables_str" >> $GITHUB_ENV
|
||||||
30
tests/assert-env-set.sh
Executable file
30
tests/assert-env-set.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_env_equals() {
|
||||||
|
if [ "$(printenv $1)" != "$2" ]; then
|
||||||
|
echo -e "Expected $1 to be set to:\n$2\nBut got:\n$(printenv $1)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_env_equals "SECRET" "RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||||
|
|
||||||
|
assert_env_equals "SECRET_IN_SECTION" "RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLCB0aGlzIGlzIGp1c3QgYSBkdW1teSBzZWNyZXQuIFBsZWFzZSBkb24ndCByZXBvcnQgaXQu"
|
||||||
|
|
||||||
|
assert_env_equals "MULTILINE_SECRET" "$(cat << EOF
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
RGVhciBzZWN1cml0eSByZXNlYXJjaGVyLApXaGls
|
||||||
|
ZSB3ZSBkZWVwbHkgYXBwcmVjaWF0ZSB5b3VyIHZp
|
||||||
|
Z2lsYW5jZSBhbmQgZWZmb3J0cyB0byBtYWtlIHRo
|
||||||
|
ZSB3b3JsZCBtb3JlIHNlY3VyZSwgSSdtIGFmcmFp
|
||||||
|
ZCBJIG11c3QgdGVsbCB5b3UgdGhhdCB0aGlzIHZh
|
||||||
|
bHVlIGlzIG5vdCBhIGFjdHVhbCBwcml2YXRlIGtl
|
||||||
|
eS4gCkl0J3MgYSBqdXN0IGEgZHVtbXkgc2VjcmV0
|
||||||
|
IHRoYXQgd2UgdXNlIHRvIHRlc3QgdmFyaW91cyAx
|
||||||
|
UGFzc3dvcmQgc2VjcmV0cyBpbnRlZ3JhdGlvbnMu
|
||||||
|
IApTbyBwbGVhc2UgZG9uJ3QgcmVwb3J0IGl0IQo=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
14
tests/assert-env-unset.sh
Executable file
14
tests/assert-env-unset.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_env_unset() {
|
||||||
|
if [ -n "$(printenv $1)" ]; then
|
||||||
|
echo "Expected secret $1 to be unset"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_env_unset "SECRET"
|
||||||
|
assert_env_unset "SECRET_IN_SECTION"
|
||||||
|
assert_env_unset "MULTILINE_SECRET"
|
||||||
20
tests/fixtures/docker-compose.yml
vendored
Normal file
20
tests/fixtures/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
version: "3.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
op-connect-api:
|
||||||
|
image: 1password/connect-api:latest
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- "$PWD/1password-credentials.json:/home/opuser/.op/1password-credentials.json"
|
||||||
|
- "data:/home/opuser/.op/data"
|
||||||
|
op-connect-sync:
|
||||||
|
image: 1password/connect-sync:latest
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
volumes:
|
||||||
|
- "$PWD/1password-credentials.json:/home/opuser/.op/1password-credentials.json"
|
||||||
|
- "data:/home/opuser/.op/data"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
Reference in New Issue
Block a user