diff --git a/pass/secretsmanager_wrapper.sh b/pass/secretsmanager_wrapper.sh new file mode 100755 index 0000000..8f929cc --- /dev/null +++ b/pass/secretsmanager_wrapper.sh @@ -0,0 +1,242 @@ +#!/usr/bin/env bash +#******************************************************************************* +# Copyright (c) 2024 Eclipse Foundation and others. +# This program and the accompanying materials are made available +# under the terms of the Eclipse Public License 2.0 +# which is available at http://www.eclipse.org/legal/epl-v20.html +# SPDX-License-Identifier: EPL-2.0 +#******************************************************************************* + +#TODO: use trap + +# Bash strict-mode +set -o errexit +set -o nounset +set -o pipefail +set +u +IFS=$'\n\t' + +SCRIPT_FOLDER="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" + +# +# Login to the Secretsmanager using the 'vault' command using env variables or local cbi config file +# +sm_login() { + + if ! command -v vault > /dev/null; then + >&2 echo "ERROR: this program requires 'vault' Client, see https://developer.hashicorp.com/vault/install" + exit 1 + fi + + connected() { + echo -e "You are connected to the Secretsmanager: ${VAULT_ADDR} " + vault token lookup + # vault token revoke -self # for testing + } + # Check if user is already authenticated + [ "$(vault token lookup &>/dev/null)" ] && connected && return 0 + + # test VAULT_ADDR + if [ -z "${VAULT_ADDR}" ]; then + VAULT_ADDR="$("${SCRIPT_FOLDER}/../utils/local_config.sh" "get_var" "url" "secretsmanager")" || true + if [ -z "$VAULT_ADDR" ]; then + echo "ERROR: VAULT_ADDR is not set. Please set it in your environment \"export VAULT_ADDR="https://..."\" or in the local cbi config file: ~/.cbi/config." + exit 1 + fi + export VAULT_ADDR + fi + + [ "$(vault token lookup &>/dev/null)" ] && connected && return 0 + + # test VAULT_TOKEN + echo "INFO: Start Auth with VAULT_TOKEN" + vault_token() { + VAULT_TOKEN="$("${SCRIPT_FOLDER}/../utils/local_config.sh" "get_var" "token" "secretsmanager")" || true + if [ -z "$VAULT_TOKEN" ]; then + echo "WARN: VAULT_TOKEN is not set. Please set it in your environment \"export VAULT_TOKEN="..."\" or in the local cbi config file: ~/.cbi/config." + else + export VAULT_TOKEN + fi + } + validate_vault_token() { + if vault token lookup &>/dev/null; then + connected && return 0 + else + echo "WARN: VAULT_TOKEN is not valid $1. Please set it in your environment \"export VAULT_TOKEN=\"...\"\" or in the local cbi config file: ~/.cbi/config." + unset VAULT_TOKEN + fi + } + if [ -z "${VAULT_TOKEN}" ]; then + vault_token + validate_vault_token "from config file" + else + validate_vault_token "from env" + vault_token + validate_vault_token "from config file" + fi + # test login/password + echo "INFO: Start Auth with login/password" + echo "WARN: login/password are optional, prefer using a token" + VAULT_PASSWORD="$("${SCRIPT_FOLDER}/../utils/local_config.sh" "get_var" "password" "secretsmanager")" || true + if [ -z "$VAULT_PASSWORD" ]; then + echo "WARN: VAULT_PASSWORD is not set. Please set it in your environment or in the local cbi config file: ~/.cbi/config." + fi + VAULT_LOGIN="$("${SCRIPT_FOLDER}/../utils/local_config.sh" "get_var" "login" "secretsmanager")" || true + if [ -z "$VAULT_LOGIN" ]; then + echo "WARN: VAULT_LOGIN is not set. Please set it in your environment or in the local cbi config file: ~/.cbi/config." + fi + if [ -z "${VAULT_LOGIN}" ]; then + read -r -p "Username: " VAULT_LOGIN + vault login -method=ldap username="${VAULT_LOGIN}" + elif [ -z "${VAULT_PASSWORD}" ]; then + vault login -method=ldap username="${VAULT_LOGIN}" + else + echo -n "${VAULT_PASSWORD}" | vault login -method=ldap username="${VAULT_LOGIN}" password=- + fi + + [ ! "$(vault token lookup)" ] && echo "ERROR: Unable to login to the Secretsmanager" && exit 1 + + VAULT_TOKEN=$(vault token lookup -format=json | jq -r '.data.id') + export VAULT_TOKEN + connected + return 0 +} + +sm_login + +# +# Usage: sm_read +# NOTE: path is the full path to the secret, including the field name +# +sm_read() { + local mount="${1:-}" + local path="${2:-}" + + local usage="Usage: Usage: sm_read " + local vault_args="-mount=\"${mount}\" \"${path}\"" + + if [ -z "$mount" ]; then + >&2 echo "Error: Mount is required for ${vault_args}. ${usage}" + return 1 + fi + + if [ -z "$path" ]; then + >&2 echo "Error: Path is required for ${vault_args}. ${usage}" + return 1 + fi + + # Check if path is valid: don't start with a slash, at least on slash, does not end with a slash + if [[ ! "$path" =~ ^[^/]+/.+[^/]$ ]]; then + >&2 echo "Error: Path is invalid, slash issue for ${vault_args}. ${usage}" + return 1 + fi + + # Extract secret path and field + local vault_secret_path="${path%/*}" + local field="${path##*/}" + data=$(vault kv get -mount="${mount}" -field="${field}" "${vault_secret_path}" 2>/dev/null) + if [ "$?" != "0" ]; then + >&2 echo "ERROR: vault entry not found: vault kv get -mount=\"${mount}\" -field=\"${field}\" \"${vault_secret_path}\"" + return 1 + fi + echo -n "${data}" + return 0 +} + +# +# Usage: sm_write key1=value1 key2=value2 ... +# NOTE: path is the full path to the secret without the field name +# +sm_write() { + local mount="${1:-}" + local path="${2:-}" + # local fields=${*:3} + shift 2 + + local fields="" + for arg in "$@"; do + if [ -n "$fields" ]; then + fields="${fields} ${arg}" + else + fields="${arg}" + fi + done + + local usage="Usage: sm_write [= | =@ | @]" + + local vault_args="-mount=\"${mount}\" \"${path}\" \"${fields}\"" + + if [ -z "$mount" ]; then + >&2 echo "Error: Mount is required for ${vault_args}. ${usage}" + return 1 + fi + + if [ -z "$path" ]; then + >&2 echo "Error: Path is required for ${vault_args}. ${usage}" + return 1 + fi + + if [ -z "$fields" ]; then + >&2 echo "Error: fields are required for ${vault_args}. ${usage}" + return 1 + fi + + test_file() { + local secrets_file="${1}" + if [ ! -f "$secrets_file" ]; then + >&2 echo "Error: File with secrets not found: ${secrets_file}" + return 1 + fi + if [ ! -s "$secrets_file" ]; then + >&2 echo "Error: Secrets file is empty: ${secrets_file}" + return 1 + fi + } + OLDIFS=$IFS + IFS=' ' + + for field in ${fields}; do + local key="${field%%=*}" + local value="" + if echo "${field}" | grep -q "=" > /dev/null; then + value="${field#*=}" + fi + + if [[ -z "$key" || -z "$value" ]] && [[ "$key" != @* && "$value" != @* ]]; then + >&2 echo "Error: Field key '$key' or value '$value' empty for ${vault_args}" + return 1 + fi + if [[ "$value" == @* ]]; then + local secrets_file="${value#@}" + ! test_file "${secrets_file}" && return 1 + fi + if [[ "$key" == @* ]]; then + local secrets_file="${key#@}" + ! test_file "${secrets_file}" && return 1 + fi + done + IFS=$OLDIFS + + write_to_vault() { + local method=$1 + eval vault kv "${method}" -mount="${mount}" "${path}" "${fields}" &>/dev/null + } + local method="put" + local secret_data + secret_data="$(vault kv get -format="json" -mount="${mount}" "${path}" 2>/dev/null)" || true + if [ -z "${secret_data}" ]; then + >&2 echo "INFO: vault entry not found: add path: ${path}" + write_to_vault "${method}" + return 0 + fi + + secret_value=$(echo "${secret_data}" | jq -r '.data.data') + [ "${secret_value}" != "null" ] && \ + method="patch" + write_to_vault "${method}" + if [ "$?" == "0" ]; then + >&2 echo "INFO: Secret written to Vault: vault kv ${method} -mount=\"${mount}\" \"${path}\" \"${fields}\"" + else + >&2 echo "ERROR: writing secret to Vault: ${vault_args}" + fi +} \ No newline at end of file diff --git a/pass/secretsmanager_wrapper_test.sh b/pass/secretsmanager_wrapper_test.sh new file mode 100755 index 0000000..c4e5b3a --- /dev/null +++ b/pass/secretsmanager_wrapper_test.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash + +#******************************************************************************* +# Copyright (c) 2024 Eclipse Foundation and others. +# This program and the accompanying materials are made available +# under the terms of the Eclipse Public License 2.0 +# which is available at http://www.eclipse.org/legal/epl-v20.html +# SPDX-License-Identifier: EPL-2.0 +#******************************************************************************* + +# Bash strict-mode +set -o errexit +set -o nounset +set -o pipefail + +IFS=$'\n\t' +SCRIPT_FOLDER="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" + +source "${SCRIPT_FOLDER}/secretsmanager_wrapper.sh" + +# Test helper function for assertions +assert_equals() { + local expected="$1" + local actual="$2" + local test_description="$3" + if [ "$expected" != "$actual" ]; then + echo -e "INFO: Test failed: $test_description \u274c" + echo "INFO: Expected: $expected" + echo "INFO: Actual: $actual" + exit 1 + else + echo -e "INFO: $test_description \u2705" + fi +} + +# Function to cleanup vault paths after tests +cleanup_vault() { + local mount="$1" + local path="$2" + # vault kv delete -mount="$mount" "$path" > /dev/null 2>&1 || true + + echo "INFO: Start cleanup for path: ${path}" + metadata=$(vault kv metadata get -mount="${mount}" -format=json "${path}" &> /dev/null) || true + data=$(echo "${metadata}" | jq '.data') + [[ "${data}" == "null" ]] && return + + versions=$(echo "${metadata}" | jq '.data.versions | keys_unsorted[] | tonumber' | tr '\n' ',' | sed 's/\(.*\),/\1 /') + echo "INFO: Path to delete ${path}, versions: ${versions}" + if [[ -z "${versions}" ]]; then + echo -e "WARN: Versions for: ${path} is empty!" + else + vault kv destroy -mount="${mount}" -versions="$versions" "${path}" > /dev/null 2>&1 || true + vault kv metadata delete -mount="${mount}" "${path}" > /dev/null 2>&1 || true + fi + echo -e "INFO: End cleanup for path: ${path}\n" +} + +# Setup for tests +VAULT_MOUNT="cbi" +VAULT_PATH="test/path" +VAULT_KEY_1="key1" +VAULT_VALUE_1="secret_value1" +VAULT_KEY_2="key2" +VAULT_VALUE_2="secret_value2" +VAULT_KEY_3="key3" +VAULT_VALUE_3="secret_value3" +VAULT_FILE_PATH="$(mktemp)" +VAULT_FILE_PATH1="$(mktemp)" +VAULT_FILE_PATH2="$(mktemp)" +VAULT_EMPTY_FILE_PATH="$(mktemp)" +echo '{"file_key_value_test":"file_secret_value"}' > "$VAULT_FILE_PATH" +echo '{"file_key_value_test1":"file_secret_value1"}' > "$VAULT_FILE_PATH1" +echo '{"file_key_value_test2":"file_secret_value2"}' > "$VAULT_FILE_PATH2" + +# Test cases for sm_write and sm_read +test_sm_write_read() { + local result + + # Cleanup before test + cleanup_vault "$VAULT_MOUNT" "$VAULT_PATH" + + ######## Test writing a simple value ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1"="$VAULT_VALUE_1" + result=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_1") + assert_equals "$VAULT_VALUE_1" "$result" "sm_write and sm_read for simple value" + + ######## Test overwriting a simple value ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1"="$VAULT_VALUE_2" + result=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_1") + assert_equals "$VAULT_VALUE_2" "$result" "sm_write and sm_read for overwritting same field" + + ######## Test writing add a new fields ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_2"="$VAULT_VALUE_2" + result=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_2") + assert_equals "$VAULT_VALUE_2" "$result" "sm_write and sm_read for simple value" + + ######## Test writing from stdin ############################################# + echo "$VAULT_VALUE_3" | sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_3=-" + result=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_3") + assert_equals "$VAULT_VALUE_3" "$result" "sm_write and sm_read from stdin" + + ######## Test writing from stdin mixed with value ############################################# + echo "$VAULT_VALUE_2" | sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1=$VAULT_VALUE_3" "$VAULT_KEY_3=-" + result1=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_1") + result2=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_3") + assert_equals "$VAULT_VALUE_3" "$result1" "sm_write and sm_read from stdin mixed with value" + assert_equals "$VAULT_VALUE_2" "$result2" "sm_write and sm_read from stdin mixed with value" + + ######## Test writing patch both fields ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1=$VAULT_VALUE_1" "$VAULT_KEY_2=$VAULT_VALUE_1" + result1=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_2") + result2=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_2") + assert_equals "$VAULT_VALUE_1" "$result1" "sm_write and sm_read patch first key" + assert_equals "$VAULT_VALUE_1" "$result2" "sm_write and sm_read patch second key" + + ######## Test writing a key and a value from a file ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "@$VAULT_FILE_PATH" + result=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/file_key_value_test") + assert_equals "file_secret_value" "$result" "sm_write and sm_read for file key/value content" + + ######## Test writing only a value from a file 1 ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "import_file1=@$VAULT_FILE_PATH" + result=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/import_file1") + assert_equals "$(cat "$VAULT_FILE_PATH")" "$result" "sm_write and sm_read for file content" + + ######## Test writing only a value from a file 2 ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "import_file2=-" < "$VAULT_FILE_PATH" + result=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/import_file2") + assert_equals "$(cat "$VAULT_FILE_PATH")" "$result" "sm_write and sm_read for file content" + + ######## Test writing multiple files ############################################# + sm_write "$VAULT_MOUNT" "$VAULT_PATH" "import_multi_file1=@$VAULT_FILE_PATH1" "import_multi_file2=@$VAULT_FILE_PATH2" + result1=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/import_multi_file1") + result2=$(sm_read "$VAULT_MOUNT" "$VAULT_PATH/import_multi_file2") + assert_equals "$(cat "$VAULT_FILE_PATH1")" "$result1" "sm_write and sm_read for file content1" + assert_equals "$(cat "$VAULT_FILE_PATH2")" "$result2" "sm_write and sm_read for file content2" + + ######## Test conditionnal read ############################################# + if sm_read "$VAULT_MOUNT" "$VAULT_PATH/$VAULT_KEY_1" &> /dev/null ; then + echo -e "INFO: sm_read conditionnal test existing path \u2705" + else + echo -e "ERROR: sm_read conditionnal test existing path \u274c" + fi + + # Cleanup after test + cleanup_vault "$VAULT_MOUNT" "$VAULT_PATH" +} + +# # Test cases for error handling +test_sm_read_errors() { + local result + local message + + ######## Test missing mount ############################################# + result=$(sm_read "" "$VAULT_PATH" 2>&1 || true) + message="Error: Mount is required for -mount=\"\" \"$VAULT_PATH\". Usage: Usage: sm_read " + assert_equals "${message}" "${result}" "sm_read with missing mount" + + ######## Test missing path ############################################# + result=$(sm_read "$VAULT_MOUNT" "" 2>&1 || true) + message="Error: Path is required for -mount=\"$VAULT_MOUNT\" \"\". Usage: Usage: sm_read " + assert_equals "${message}" "${result}" "sm_read with missing path" + + ######## Test path must not start by slash ############################################# + result=$(sm_read "$VAULT_MOUNT" "/XXXXXX" 2>&1 || true) + message="Error: Path is invalid, slash issue for -mount=\"$VAULT_MOUNT\" \"/XXXXXX\". Usage: Usage: sm_read " + assert_equals "${message}" "${result}" "sm_read path must not start by slash" + + ######## Test path must not end by slash ############################################# + result=$(sm_read "$VAULT_MOUNT" "XXXXXX/" 2>&1 || true) + message="Error: Path is invalid, slash issue for -mount=\"$VAULT_MOUNT\" \"XXXXXX/\". Usage: Usage: sm_read " + assert_equals "${message}" "${result}" "sm_read path must not end by slash" + + ######## Test path must have one slash ############################################# + result=$(sm_read "$VAULT_MOUNT" "XXXXXX" 2>&1 || true) + message="Error: Path is invalid, slash issue for -mount=\"$VAULT_MOUNT\" \"XXXXXX\". Usage: Usage: sm_read " + assert_equals "${message}" "${result}" "sm_read path must have one slash" + + ######## Test non exiting path ############################################# + result=$(sm_read "$VAULT_MOUNT" "XXX/XXX" 2>&1 || true) + message="ERROR: vault entry not found: vault kv get -mount=\"$VAULT_MOUNT\" -field=\"XXX\" \"XXX\"" + assert_equals "${message}" "${result}" "sm_read with non existing path" + + ######## Test conditionnal read ############################################# + if ! sm_read "$VAULT_MOUNT" "XXX/XXX" &> /dev/null ; then + echo -e "INFO: sm_read conditionnal test non existing path \u2705" + else + echo -e "ERROR: sm_read conditionnal test non existing path \u274c" + fi +} + +test_sm_write_errors() { + local result + local message + + ######## Test missing mount ############################################# + result=$(sm_write "" "$VAULT_PATH" "$VAULT_KEY_1=$VAULT_VALUE_1" 2>&1 || true) + message="Error: Mount is required for -mount=\"\" \"$VAULT_PATH\" \"$VAULT_KEY_1=$VAULT_VALUE_1\". Usage: sm_write [= | =@ | @]" + assert_equals "${message}" "${result}" "sm_write with missing mount" + + ######## Test missing path ############################################# + result=$(sm_write "$VAULT_MOUNT" "" "$VAULT_KEY_1=$VAULT_VALUE_1" 2>&1 || true) + message="Error: Path is required for -mount=\"$VAULT_MOUNT\" \"\" \"$VAULT_KEY_1=$VAULT_VALUE_1\". Usage: sm_write [= | =@ | @]" + assert_equals "${message}" "${result}" "sm_write with missing path" + + ######## Test missing fields ############################################# + result=$(sm_write "$VAULT_MOUNT" "$VAULT_PATH" 2>&1 || true) + message="Error: fields are required for -mount=\"$VAULT_MOUNT\" \"$VAULT_PATH\" \"\". Usage: sm_write [= | =@ | @]" + assert_equals "${message}" "${result}" "sm_write with missing fields" + + ######## Test missing value in fields #############################################" + result=$(sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1=$VAULT_VALUE_1 $VAULT_KEY_2=" 2>&1 || true) + message="Error: Field key '$VAULT_KEY_2' or value '' empty for -mount=\"$VAULT_MOUNT\" \"$VAULT_PATH\" \"$VAULT_KEY_1=$VAULT_VALUE_1 $VAULT_KEY_2=\"" + assert_equals "${message}" "${result}" "sm_write with missing value in fields" + + ######## Test missing value in fields with only key ref #############################################" + result=$(sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1=$VAULT_VALUE_1 $VAULT_KEY_2" 2>&1 || true) + message="Error: Field key '$VAULT_KEY_2' or value '' empty for -mount=\"$VAULT_MOUNT\" \"$VAULT_PATH\" \"$VAULT_KEY_1=$VAULT_VALUE_1 $VAULT_KEY_2\"" + assert_equals "${message}" "${result}" "sm_write with missing value in fields with only key ref" + + ######## Test missing file for write #############################################" + result=$(sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1=@test.json" 2>&1 || true) + message="Error: File with secrets not found: test.json" + assert_equals "${message}" "${result}" "sm_write with missing file" + + ######## Test missing file without key for write #############################################" + result=$(sm_write "$VAULT_MOUNT" "$VAULT_PATH" "@test.json" 2>&1 || true) + message="Error: File with secrets not found: test.json" + assert_equals "${message}" "${result}" "sm_write with missing file without key " + + ######## Test empty file for write #############################################" + result=$(sm_write "$VAULT_MOUNT" "$VAULT_PATH" "$VAULT_KEY_1=@${VAULT_EMPTY_FILE_PATH}" 2>&1 || true) + message="Error: Secrets file is empty: ${VAULT_EMPTY_FILE_PATH}" + assert_equals "${message}" "${result}" "sm_write with empty file" + + ######## Test empty file without key for write #############################################" + result=$(sm_write "$VAULT_MOUNT" "$VAULT_PATH" "@${VAULT_EMPTY_FILE_PATH}" 2>&1 || true) + message="Error: Secrets file is empty: ${VAULT_EMPTY_FILE_PATH}" + assert_equals "${message}" "${result}" "sm_write with empty file without key" +} + +# Run tests +echo -e "Passing tests for sm_read and sm_write...\n" +test_sm_write_read +echo -e "\nFailure tests for sm_read...\n" +test_sm_read_errors +echo -e "\nFailure tests for sm_write....\n" +test_sm_write_errors + +echo -e "\nAll tests passed! \u2705" \ No newline at end of file