diff --git a/.github/workflows/test-ethd.yml b/.github/workflows/test-ethd.yml index aa29d42eb..694e10442 100644 --- a/.github/workflows/test-ethd.yml +++ b/.github/workflows/test-ethd.yml @@ -36,7 +36,7 @@ jobs: - name: Set up Docker on macOS if: ${{ startsWith(matrix.os, 'macos-') }} uses: douglascamata/setup-docker-macos-action@d5ccc6aae0ce23e7700154f5e63cc53e6433ac48 - - name: Install Expect + - name: Prerequisites on Ubuntu if: ${{ startsWith(matrix.os, 'ubuntu-') }} run: sudo apt-get install -y expect whiptail - name: Prerequisites on macOS diff --git a/ethd b/ethd index fd928b892..b6afac591 100755 --- a/ethd +++ b/ethd @@ -6,6 +6,14 @@ __project_name="Eth Docker" __app_name="Ethereum node" __sample_service="consensus" __min_env_version=55 +__min_ubuntu=22 +__suggest_ubuntu="24.04 or 22.04." +__upgrade_ubuntu="24.04: https://gist.github.com/yorickdowne/94f1e5538007f4c9d3da7b22b0dc28a4" +__min_debian=12 +__suggest_debian="13 or 12." +__upgrade_debian="13: https://gist.github.com/yorickdowne/3cecc7b424ce241b173510e36754af47" +__target_pg=18 +__stat_mode_param=() __docker_exe="docker" __old_docker=0 __docker_sudo="" @@ -21,13 +29,6 @@ __compose_upgraded=0 __distro="" __os_major_version="" __os_minor_version="" -__target_pg=18 -__min_ubuntu=22 -__suggest_ubuntu="24.04 or 22.04." -__upgrade_ubuntu="24.04: https://gist.github.com/yorickdowne/94f1e5538007f4c9d3da7b22b0dc28a4" -__min_debian=12 -__suggest_debian="13 or 12." -__upgrade_debian="13: https://gist.github.com/yorickdowne/3cecc7b424ce241b173510e36754af47" __eol_os=0 __non_interactive=0 __debug=0 @@ -52,10 +53,13 @@ __command="" __params="" __me=./ethd __auto_sudo="" +__chmod_sudo="" __cannot_sudo=0 __as_owner="" +__new_owner="" __owner="" __owner_group="" +__group_can_write=0 __value="" # This is a global to hand a result back from __get_value_from_env __free_space=0 __docker_dir="/var/lib/docker" @@ -196,26 +200,115 @@ __handle_docker() { } -__handle_root() { +# Allow users to set their own umask like 077, and make it x0x if we are going to rely on group write +__adjust_umask_for_group() { + local current_umask + local new_umask + + current_umask=$(umask) + + # split into 3 or 4 digits safely, set group to 0 + case ${#current_umask} in + 3) # ugo form + u=${current_umask:0:1} + g=0 + o=${current_umask:2:1} + new_umask="${u}${g}${o}" + ;; + 4) # sugo form + s=${current_umask:0:1} + u=${current_umask:1:1} + g=0 + o=${current_umask:3:1} + new_umask="${s}${u}${g}${o}" + ;; + esac + + umask "${new_umask}" +} + + +# Who owns the directory, what permissions do they have, and do we need to sudo -u ${__owner} when creating files +# Also, can we sudo +# Assume owner has rw on everything +# EUID is owner of directory: No umask adjustment, no sudo +# EUID is not owner of directory, but directory is group-writeable and EUID is in the group that can write: +# change umask to not touch group perms +# Neither: sudo -u ${__owner} +# __owner and __owner_group are set at script entry and have the user:group owners of the directory +__handle_ownership() { local g - local found=0 + local can_sudo=0 + local is_in_group=0 + local perms + local group_w + local sudo_group + local reason_msg="" + local action_msg="" - if [[ "${EUID}" != $(id -u "${__owner}") ]]; then - __as_owner="sudo -u ${__owner}" +# shellcheck disable=SC2012 + perms=$(ls -ld . | awk '{print $1}') + + group_w="${perms:5:1}" + if [[ "${group_w}" != "-" ]]; then + __group_can_write=1 fi + for g in $(id -nG); do + if [[ "${g}" = "${__owner_group}" ]]; then + is_in_group=1 + break + fi + done + + if [[ "$OSTYPE" = "darwin"* ]]; then + sudo_group="admin" + __stat_mode_param=("-f" "%Lp") + else + sudo_group="sudo" + __stat_mode_param=("-c" "%a") + fi +# Figure out whether the user can sudo + __cannot_sudo=1 if [[ "${EUID}" -ne 0 ]]; then for g in $(id -nG); do - if [[ "${g}" =~ ^(sudo|admin)$ ]]; then + if [[ "${g}" = "${sudo_group}" ]]; then __auto_sudo="sudo" - found=1 + __cannot_sudo=0 break fi done + else # root always can + __cannot_sudo=0 + fi + + if [[ "${EUID}" = $(id -u "${__owner}") ]]; then # No adjustments needed + __as_owner="" + __new_owner="${__owner}" + elif [[ "${__group_can_write}" -eq 1 && "${is_in_group}" -eq 1 ]]; then + __adjust_umask_for_group # Adjust umask to make sure files are group-writable + __as_owner="" + __new_owner="$(id -nu ${EUID})" + if [[ "${__cannot_sudo}" -eq 0 ]]; then + __chmod_sudo="sudo" + fi + else + __as_owner="sudo -u ${__owner}" + __new_owner="${__owner}" + __chmod_sudo="sudo" fi - if [[ "${EUID}" -ne 0 && "${found}" -eq 0 ]]; then - __cannot_sudo=1 + if [[ -n "${__as_owner}" && "${__cannot_sudo}" -eq 1 ]]; then # Have to sudo but can't + if [[ "${is_in_group}" -eq 1 ]]; then + reason_msg="and all of its groups " + action_msg="or give write permissions to ${__owner_group}, " + fi + echo "The $(dirname "$(realpath "${BASH_SOURCE[0]}")") directory is owned by ${__owner}, the script runs as $(id -nu), and that user ${reason_msg}cannot write files in \ +this directory, nor run \"sudo\"." + echo "This means ${__me} cannot modify or create files, which keeps it from working." + echo "Please run ${__me} as ${__owner}, ${action_msg}or make $(id -nu) part of the ${sudo_group} group." + echo "Aborting." + exit 1 fi } @@ -473,127 +566,79 @@ __check_compose_version() { __prep_conffiles() { -# Create prometheus.yml if it doesn't exist - if [[ ! -f ./prometheus/prometheus.yml ]]; then - ${__as_owner} cp ./prometheus/prometheus.yml.sample ./prometheus/prometheus.yml - fi -# Make sure all yml under prometheus dir are readable - if find prometheus -maxdepth 0 \! -perm -o+rx | grep -q .; then - ${__as_owner} chmod o+rx prometheus - fi - if find prometheus -type f -name '*.yml' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+r prometheus/*.yml - fi + local env + local file + local dir + local mode + local -A create_files=( + [./prometheus/prometheus.yml]="./prometheus/prometheus.yml.sample" + [./alloy/loki-write.alloy]="./alloy/loki-write.alloy.sample" + [./alloy/prometheus-write.alloy]="./alloy/prometheus-write.alloy.sample" + [./alloy/alloy-logs.alloy]="./alloy/alloy-logs.alloy.sample" + [./ssv-config/config.yaml]="./ssv-config/config.yaml.sample" + [./ssv-config/dkg-config.yaml]="./ssv-config/dkg-config.yaml.sample" + [./commit-boost/cb-config.toml]="./commit-boost/cb-config.toml.sample" + [./tempo/tempo.yaml]="./tempo/tempo.yaml.sample" + [./tempo/overrides.yaml]="./tempo/overrides.yaml.sample" + ) + local -A chmod_dirs=( + [./prometheus]="*.yml" + [./alloy]="*.alloy" + [./alloy-obol]="*.alloy" + [./ssv-config]="*.yaml" + [./.eth/dkg_output]="*" + [./commit-boost]="cb-config.toml" + [./tempo]="*.yaml" + [./loki]="*.yml" + [./siren]="*.sh" + ) -# Create alloy files if they do not exist - if [[ ! -f "alloy/loki-write.alloy" ]]; then - ${__as_owner} cp alloy/loki-write.alloy.sample alloy/loki-write.alloy - fi - if [[ ! -f "alloy/prometheus-write.alloy" ]]; then - ${__as_owner} cp alloy/prometheus-write.alloy.sample alloy/prometheus-write.alloy - fi - if [[ ! -f "alloy/alloy-logs.alloy" ]]; then - ${__as_owner} cp alloy/alloy-logs.alloy.sample alloy/alloy-logs.alloy - fi -# Make sure alloy files are readable - if find alloy -maxdepth 0 \! -perm -o+rx | grep -q .; then - ${__as_owner} chmod o+rx alloy - fi - if find alloy -type f -name '*.alloy' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+r alloy/*.alloy - fi -# Make sure alloy-obol files are readable - if find alloy-obol -maxdepth 0 \! -perm -o+rx | grep -q .; then - ${__as_owner} chmod o+rx alloy-obol - fi - if find alloy-obol -type f -name '*.alloy' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+r alloy-obol/*.alloy - fi +# Create configuration files if they do not exist + for file in "${!create_files[@]}"; do + if [[ ! -f "${file}" ]]; then + mode=$(${__as_owner} stat "${__stat_mode_param[@]}" "${create_files[${file}]}") + ${__as_owner} install -g "${__owner_group}" -o "${__new_owner}" -m "${mode}" "${create_files[${file}]}" "${file}" + fi + done -# Create SSV config.yaml if it doesn't exist - if [[ ! -f "ssv-config/config.yaml" ]]; then - ${__as_owner} cp ssv-config/config.yaml.sample ssv-config/config.yaml - fi - if [[ ! -f "ssv-config/dkg-config.yaml" ]]; then - ${__as_owner} cp ssv-config/dkg-config.yaml.sample ssv-config/dkg-config.yaml - fi -# Make sure ssv-config yaml files are readable - if find ssv-config -maxdepth 0 \! -perm -o+rx | grep -q .; then - ${__as_owner} chmod o+rx ssv-config - fi - if find ssv-config -type f -name '*.yaml' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+r ssv-config/*.yaml - fi -# Make sure local user owns the dkg output dir and everything in it -if find .eth/dkg_output \( \! -user "${__owner}" -o \! -group "${__owner_group}" \) -print -quit | grep -q .; then +# Make sure directory owner owns the SSV dkg output dir and everything in it. ssvlabs/ssv-dkg runs as root + if ${__as_owner} find ./.eth/dkg_output \( \! -user "${__owner}" -o \! -group "${__owner_group}" \) -print -quit | grep -q .; then if [[ "${__cannot_sudo}" -eq 0 ]]; then echo "Fixing ownership of .eth/dkg_output" ${__auto_sudo} chown -R "${__owner}:${__owner_group}" .eth/dkg_output - ${__as_owner} chmod u=rwx,go=rx .eth/dkg_output + ${__chmod_sudo} chmod u+rwx,o+rx .eth/dkg_output else echo "Ownership of .eth/dkg_output should be fixed, but this user can't sudo" fi fi -# Make sure the dkg output dir and its subdirs are readable - if find .eth/dkg_output -type d \! -perm -o+rx | grep -q .; then + +# Adjust "other" read permissions for files that are bind-mounted + for dir in "${!chmod_dirs[@]}"; do # shellcheck disable=SC2086 - find .eth/dkg_output -type d -exec ${__as_owner} chmod o+rx {} \; - fi -# Make sure the contents of the dkg output dir are readable - if find .eth/dkg_output -type f \! -perm -o+rx | grep -q .; then + ${__chmod_sudo} find "${dir}" -type d \! -perm -o+rx -exec chmod o+rx {} + # shellcheck disable=SC2086 - find .eth/dkg_output -type f -exec ${__as_owner} chmod o+rx {} \; - fi - -# Create cb-config.toml if it doesn't exist - if [[ ! -f "commit-boost/cb-config.toml" ]]; then - ${__as_owner} cp commit-boost/cb-config.toml.sample commit-boost/cb-config.toml - fi -# Make sure cb-config.toml is readable - if find commit-boost -type f -name 'cb-config.toml' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+r commit-boost/cb-config.toml - fi - -# Create tempo.yaml if it doesn't exist - if [[ ! -f "tempo/tempo.yaml" ]]; then - ${__as_owner} cp tempo/tempo.yaml.sample tempo/tempo.yaml - fi - if [[ ! -f "tempo/overrides.yaml" ]]; then - ${__as_owner} cp tempo/overrides.yaml.sample tempo/overrides.yaml - fi -# Make sure tempo files are readable - if find tempo -maxdepth 0 \! -perm -o+rx | grep -q .; then - ${__as_owner} chmod o+rx tempo - fi - if find tempo -type f -name '*.yaml' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+r tempo/*.yaml - fi -# Make sure loki files are readable - if find loki -maxdepth 0 \! -perm -o+rx | grep -q .; then - ${__as_owner} chmod o+rx loki - fi - if find loki -type f -name '*.yml' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+r loki/*.yml - fi -# Make sure Siren entrypoint is executable - if find siren -maxdepth 0 \! -perm -o+rx | grep -q .; then - ${__as_owner} chmod o+rx siren - fi - if find siren -type f -name '*.sh' \! -perm -o+r | grep -q .; then - ${__as_owner} chmod o+rx siren/*.sh - fi + ${__chmod_sudo} find "${dir}" -type f -name "${chmod_dirs[${dir}]}" \! -perm -o+r -exec chmod o+r {} + + done -# Make sure local user owns .env - if find . -name .env \( \! -user "${__owner}" -o \! -group "${__owner_group}" \) -print -quit | grep -q ./.env; then +# Make sure directory owner owns .env, or it's owned by directory group and group is writable + if ! ${__as_owner} test -f "${__env_file}" || ${__as_owner} find . -maxdepth 1 -name "${__env_file}" -user "${__owner}" -print -quit | grep -q "${__env_file}" \ + || { ${__as_owner} find . -maxdepth 1 -name "${__env_file}" -group "${__owner_group}" -print -quit | grep -q "${__env_file}" \ + && [[ "${__group_can_write}" -eq 1 ]]; }; then + true # Nothing to do + else if [[ "${__cannot_sudo}" -eq 0 ]]; then - echo "Fixing ownership of .env" - ${__auto_sudo} chown -R "${__owner}:${__owner_group}" .env - ${__auto_sudo} chmod -R 644 .env - if [[ -f .env.tmp ]]; then - ${__auto_sudo} rm -f .env.tmp + echo "Fixing ownership of ${__env_file}" + ${__auto_sudo} chown -R "${__owner}:${__owner_group}" "${__env_file}" + if [[ ${__group_can_write} -eq 1 ]]; then + ${__chmod_sudo} chmod ug+rw "${__env_file}" + else + ${__chmod_sudo} chmod u+rw "${__env_file}" + fi + if [[ -f ${__env_file}.tmp ]]; then + ${__auto_sudo} rm -f "${__env_file}".tmp fi else - echo "Ownership of .env should be fixed, but this user can't sudo" + echo "Ownership of ${__env_file} should be fixed, but this user can't sudo" fi fi } @@ -863,7 +908,7 @@ EOF } -install() { +install_command() { local yn if [[ ! "${__distro}" =~ (ubuntu|debian) ]]; then @@ -1650,6 +1695,7 @@ __update_value_in_env() { local env_file="$3" local escaped_value local awk_exe + local mode if [[ "$OSTYPE" = "darwin"* ]]; then awk_exe="gawk" # Built-in awk can't handle multi-line. Does require brew install gawk @@ -1721,7 +1767,9 @@ __update_value_in_env() { # Print all lines if not in the target variable block { print } ' "${env_file}" | ${__as_owner} tee "${env_file}.tmp" >/dev/null - ${__as_owner} mv "${env_file}.tmp" "${env_file}" + mode=$(${__as_owner} stat "${__stat_mode_param[@]}" "${env_file}") + ${__as_owner} install -g "${__owner_group}" -o "${__new_owner}" -m "${mode}" "${env_file}.tmp" "${env_file}" + ${__as_owner} rm -f "${env_file}.tmp" else # Variable does not exist, append it printf "%s=%s\n" "${var_name}" "${escaped_value}" | ${__as_owner} tee -a "${env_file}" >/dev/null @@ -3697,7 +3745,7 @@ __get_github_release() { | cut -d : -f 2,3 \ | tr -d \" \ | ${__as_owner} wget -qi- -O "${dirname}/${targetname}" \ - && ${__as_owner} chmod +x "${dirname}/${targetname}" \ + && ${__chmod_sudo} chmod +x "${dirname}/${targetname}" \ || echo "-> Could not download the latest version of '${suffix}' from github '${repo}'." fi } @@ -5325,12 +5373,15 @@ __query_dkg() { __handle_error() { + local exitstatus=$1 + local line=$2 + local calling_line=$3 + if [[ ! $- =~ e ]]; then # set +e, do nothing return 0 fi - local exitstatus=$1 if [[ "${exitstatus}" -eq 0 ]]; then return 0 fi @@ -5346,7 +5397,7 @@ __handle_error() { elif [[ "${__during_config}" -eq 1 && "${exitstatus}" -eq 1 ]]; then echo "Canceled config wizard." else - echo "${__me} terminated with exit code ${exitstatus} on line $2" + echo "${__me} terminated with exit code ${exitstatus} on line $line, called from $calling_line" if [[ -n "${__command}" ]]; then echo "This happened during ${__me} ${__command} ${__params}" fi @@ -6124,8 +6175,7 @@ if [[ ! -f ~/.profile ]] || ! grep -q "alias ethd" ~/.profile; then __me="./${__me}" fi -trap '__handle_error $? $LINENO' ERR -trap '__handle_error $? $LINENO' EXIT +trap '__handle_error $? $LINENO ${BASH_LINENO[0]}' ERR EXIT if [[ "$#" -eq 0 || "$*" = "--help" || "$*" = "-h" || "$*" = "update --help" || "$*" = "update -h" ]]; then help "$@" @@ -6153,7 +6203,7 @@ __command="$1" shift __params=$* -__handle_root +__handle_ownership __determine_distro __prep_conffiles @@ -6161,7 +6211,7 @@ __check_for_snap # Don't check for Docker before it's installed if [[ "${__command}" = "install" ]]; then - ${__command} "$@" + install_command "$@" exit "$?" fi diff --git a/tests/test-multiuser.sh b/tests/test-multiuser.sh new file mode 100755 index 000000000..7e2463e65 --- /dev/null +++ b/tests/test-multiuser.sh @@ -0,0 +1,613 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +__users='{ + "alice": { + "groups": ["sudo", "docker", "test-ethd-admins"] + }, + "bob": { + "groups": ["docker", "test-ethd-admins"] + }, + "charlie": { + "groups": ["sudo", "docker"] + }, + "eve": { + "groups": ["docker", "test-ethd-admins"] + } +}' +__admin_group="test-ethd-admins" + +declare -a __config_files=( + "prometheus/prometheus.yml" + "alloy/loki-write.alloy" + "alloy/prometheus-write.alloy" + "alloy/alloy-logs.alloy" + "ssv-config/config.yaml" + "ssv-config/dkg-config.yaml" + "commit-boost/cb-config.toml" + "tempo/tempo.yaml" + "tempo/overrides.yaml" +) + +__initial_owner="" +__error_count=0 +__temp_dir="/tmp/ethd_test_dir" + +# This hard-codes the user and group names. +__test_parameters='[ + { + "owner": "alice:alice", + "user": "alice", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,go=r", + "exec_file_permissions": "u=rwx,go=rx", + "dir_permissions": "u=rwx,go=rx", + "setgid": "g-s", + "expected_file_owner": "alice:alice", + "expected_env_owner": "alice:alice", + "expected_config_permissions": "644", + "expected_env_permissions": "644", + "should_succeed": "true" + }, + { + "owner": "alice:alice", + "user": "alice", + "umask": "077", + "runfirst": "", + "file_permissions": "u=rw,go=", + "exec_file_permissions": "u=rwx,go=", + "dir_permissions": "u=rwx,go=", + "setgid": "g-s", + "expected_file_owner": "alice:alice", + "expected_env_owner": "alice:alice", + "expected_config_permissions": "604", + "expected_env_permissions": "600", + "should_succeed": "true" + }, + { + "owner": "alice:alice", + "user": "root", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,go=r", + "exec_file_permissions": "u=rwx,go=rx", + "dir_permissions": "u=rwx,go=rx", + "setgid": "g-s", + "expected_file_owner": "alice:alice", + "expected_env_owner": "alice:alice", + "expected_config_permissions": "644", + "expected_env_permissions": "644", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "alice", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=r", + "exec_file_permissions": "u=rwx,g=rwx,o=rx", + "dir_permissions": "u=rwx,g=rwx,o=rx", + "setgid": "g-s", + "expected_file_owner": "alice:test-ethd-admins", + "expected_env_owner": "alice:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "664", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "alice", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=r", + "exec_file_permissions": "u=rwx,g=rwx,o=rx", + "dir_permissions": "u=rwx,g=rwx,o=rx", + "setgid": "g+s", + "expected_file_owner": "alice:test-ethd-admins", + "expected_env_owner": "alice:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "664", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "alice", + "umask": "077", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=", + "exec_file_permissions": "u=rwx,g=rwx,o=", + "dir_permissions": "u=rwx,g=rwx,o=", + "setgid": "g-s", + "expected_file_owner": "alice:test-ethd-admins", + "expected_env_owner": "alice:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "660", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "alice", + "umask": "077", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=", + "exec_file_permissions": "u=rwx,g=rwx,o=", + "dir_permissions": "u=rwx,g=rwx,o=", + "setgid": "g+s", + "expected_file_owner": "alice:test-ethd-admins", + "expected_env_owner": "alice:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "660", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "bob", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=r", + "exec_file_permissions": "u=rwx,g=rwx,o=rx", + "dir_permissions": "u=rwx,g=rwx,o=rx", + "setgid": "g-s", + "expected_file_owner": "bob:test-ethd-admins", + "expected_env_owner": "bob:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "664", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "bob", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=r", + "exec_file_permissions": "u=rwx,g=rwx,o=rx", + "dir_permissions": "u=rwx,g=rwx,o=rx", + "setgid": "g+s", + "expected_file_owner": "bob:test-ethd-admins", + "expected_env_owner": "bob:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "664", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "bob", + "umask": "077", + "runfirst": "alice", + "file_permissions": "u=rw,g=rw,o=", + "exec_file_permissions": "u=rwx,g=rwx,o=", + "dir_permissions": "u=rwx,g=rwx,o=", + "setgid": "g-s", + "expected_file_owner": "alice:test-ethd-admins", + "expected_env_owner": "bob:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "660", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "bob", + "umask": "077", + "runfirst": "alice", + "file_permissions": "u=rw,g=rw,o=", + "exec_file_permissions": "u=rwx,g=rwx,o=", + "dir_permissions": "u=rwx,g=rwx,o=", + "setgid": "g+s", + "expected_file_owner": "alice:test-ethd-admins", + "expected_env_owner": "bob:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "660", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "root", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=r", + "exec_file_permissions": "u=rwx,g=rwx,o=rx", + "dir_permissions": "u=rwx,g=rwx,o=rx", + "setgid": "g-s", + "expected_file_owner": "eve:test-ethd-admins", + "expected_env_owner": "eve:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "664", + "should_succeed": "true" + }, + { + "owner": "eve:test-ethd-admins", + "user": "bob", + "umask": "077", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=", + "exec_file_permissions": "u=rwx,g=rwx,o=", + "dir_permissions": "u=rwx,g=rwx,o=", + "setgid": "g-s", + "expected_file_owner": "bob:test-ethd-admins", + "expected_env_owner": "bob:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "660", + "should_succeed": "false" + }, + { + "owner": "eve:test-ethd-admins", + "user": "charlie", + "umask": "022", + "runfirst": "", + "file_permissions": "u=rw,g=rw,o=r", + "exec_file_permissions": "u=rwx,g=rwx,o=rx", + "dir_permissions": "u=rwx,g=rwx,o=r", + "setgid": "g-s", + "expected_file_owner": "alice:test-ethd-admins", + "expected_env_owner": "alice:test-ethd-admins", + "expected_config_permissions": "664", + "expected_env_permissions": "664", + "should_succeed": "false" + } +]' + + +__handle_error() { + local exitstatus=$1 + local lineno=$2 + + if [[ ! $- =~ e ]]; then +# set +e, do nothing + return 0 + fi + + echo + echo "Test script terminated with exit code $exitstatus on line $lineno" +} + + +__create_users() { + local user + local group + local sudoers_file + + if ! getent group "$__admin_group" >/dev/null; then + echo "Creating $__admin_group group" + sudo groupadd "$__admin_group" + fi + + for user in $(jq -r 'keys[]' <<< "$__users"); do + # Create user if it doesn't exist + if ! id -u "$user" >/dev/null 2>&1; then + echo "Creating user $user" + sudo useradd -m -s /bin/bash "$user" + for group in $(jq -r --arg u "$user" '.[$u].groups[]' <<< "$__users"); do + sudo usermod -aG "$group" "$user" + if [[ "$group" == "sudo" ]]; then + # Passwordless sudo via sudoers.d + sudoers_file="/etc/sudoers.d/$user" + echo "$user ALL=(ALL) NOPASSWD:ALL" | sudo tee "$sudoers_file" >/dev/null + sudo chmod 0440 "$sudoers_file" + fi + done + fi + done +} + + +__delete_users() { + local user + local group + local sudoers_file + + echo + for user in $(jq -r 'keys[]' <<< "$__users"); do + # Only act if the user exists + if id -u "$user" >/dev/null 2>&1; then + echo "Deleting user $user" + for group in $(jq -r --arg u "$user" '.[$u].groups[]' <<< "$__users"); do + sudo gpasswd -d "$user" "$group" >/dev/null 2>&1 || true + if [[ "$group" == "sudo" ]]; then + # Remove sudoers file if present + sudoers_file="/etc/sudoers.d/$user" + [[ -f "$sudoers_file" ]] && sudo rm -f "$sudoers_file" + fi + done + # Delete user (and home directory) + sudo userdel -r "$user" 2>/dev/null || sudo userdel "$user" + fi + done + + if getent group "$__admin_group" >/dev/null; then + echo "Deleting $__admin_group group" + sudo groupdel "$__admin_group" + fi +} + + +__check_os() { + if [[ ! -f /etc/os-release ]]; then + echo "ERROR: This script is designed to be run on Linux, but /etc/os-release doesn't exist" + echo "Aborting" + exit 0 + fi + . /etc/os-release + if [[ ! "$ID" =~ (debian|ubuntu) ]]; then + echo "ERROR: This script is designed to be run on Debian or Ubuntu, and you don't appear to be running either." + echo "You are on $ID" + echo "Aborting" + exit 0 + fi +} + + +__check_workdir() { + if [[ ! -f "ethd" ]]; then + echo "ERROR: This script is designed to be run while inside the top-level Eth Docker directory" + echo "\"ethd\" not found, the script was called from $(pwd)" + echo "Aborting" + exit 0 + fi +} + + +__check_prereqs() { + for arg in "$@"; do + if ! dpkg-query -W -f='${Status}' "$arg" 2>/dev/null | grep -q "ok installed"; then + echo "Installing $arg" + sudo apt-get update && sudo apt-get -y install "$arg" + fi + done +} + + +__warn_creation() { + local yn + + echo "This test script will first create, then at the end delete, these users" + jq -r 'keys[]' <<< "$__users" + echo "It will also create and delete a $__admin_group group" + echo + read -rp "Are you sure you wish to continue? (y/N)" yn + + case $yn in + [Yy]) return;; + *) echo "Aborting"; exit 0;; + esac +} + + +__delete_config_files() { + local file + + for file in "${__config_files[@]}"; do + if sudo test -f "$__temp_dir/$file"; then + sudo rm -f "$__temp_dir/$file" + fi + done +} + + +# Set ownership and permissions for the test +__prep_temp_directory() { + local owner="$1" + local file_permissions="$2" + local exec_file_permissions="$3" + local dir_permissions="$4" + local setgid="$5" + + echo + echo "Preparing temporary directory for test" + sudo mkdir -p "$__temp_dir" + sudo cp -a . "$__temp_dir"/ + sudo chown -R "$owner" "$__temp_dir" + sudo chmod "$setgid" "$__temp_dir" + sudo find "$__temp_dir" -type d -exec chmod "$dir_permissions" {} + + sudo find "$__temp_dir" -type f -perm /111 -exec chmod "$exec_file_permissions" {} + + sudo find "$__temp_dir" -type f ! -perm /111 -exec chmod "$file_permissions" {} + +} + + +__delete_temp_directory() { + sudo rm -rf "$__temp_dir" +} + + +__check_config_files() { + local file + local expected_owner="$1" + local expected_permissions="$2" + + for file in "${__config_files[@]}"; do + if sudo test -f "$__temp_dir/$file"; then + actual_owner=$(sudo stat -c '%U:%G' "$__temp_dir/$file") + actual_permissions=$(sudo stat -c '%a' "$__temp_dir/$file") + + if [[ "$actual_owner" != "$expected_owner" ]]; then + echo "ERROR: $__temp_dir/$file has owner $actual_owner but expected $expected_owner" + __error_count=$((__error_count + 1)) + fi + if [[ "$actual_permissions" != "$expected_permissions" ]]; then + echo "ERROR: $__temp_dir/$file has permissions $actual_permissions but expected $expected_permissions" + __error_count=$((__error_count + 1)) + fi + else + echo "ERROR: Expected config file $__temp_dir/$file does not exist" + __error_count=$((__error_count + 1)) + fi + done +} + + +__check_other_read_perms() { + local -A read_perm_files=( + [prometheus]="*.yml" + [alloy]="*.alloy" + [alloy-obol]="*.alloy" + [ssv-config]="*.yaml" + [.eth/dkg_output]="*" + [commit-boost]="cb-config.toml" + [tempo]="*.yaml" + [loki]="*.yml" + [siren]="*.sh" + ) + local dir + + for dir in "${!read_perm_files[@]}"; do + if sudo find "$__temp_dir/$dir" -type d \! -perm -o+rx -print -quit | grep -q .; then + echo "ERROR: There are directories in $__temp_dir/$dir that do not have \"other\" read and execute permissions but should" + __error_count=$((__error_count + 1)) + fi + if sudo find "$__temp_dir/$dir" -type f -name "${read_perm_files[${dir}]}" \! -perm -o+r -print -quit | grep -q .; then + echo "ERROR: There are files in $__temp_dir/$dir that do not have \"other\" read permissions but should" + __error_count=$((__error_count + 1)) + fi + done +} + + +__check_env_file() { + local env_file=".env" + local expected_owner="$1" + local expected_permissions="$2" + + if sudo test -f "$__temp_dir/$env_file"; then + actual_owner=$(sudo stat -c '%U:%G' "$__temp_dir/$env_file") + actual_permissions=$(sudo stat -c '%a' "$__temp_dir/$env_file") + + if [[ "$actual_owner" != "$expected_owner" ]]; then + echo "ERROR: $__temp_dir/$env_file has owner $actual_owner but expected $expected_owner" + __error_count=$((__error_count + 1)) + fi + if [[ "$actual_permissions" != "$expected_permissions" ]]; then + echo "ERROR: $__temp_dir/$env_file has permissions $actual_permissions but expected $expected_permissions" + __error_count=$((__error_count + 1)) + fi + else + echo "ERROR: Expected env file $__temp_dir/$env_file does not exist" + __error_count=$((__error_count + 1)) + fi +} + + +__run_tests() { + local user + local owner + local umask_val + local runfirst + local file_permissions + local exec_file_permissions + local dir_permissions + local setgid + local expected_file_owner + local expected_env_owner + local expected_config_permissions + local expected_env_permissions + local should_succeed + local result + local output + + while IFS= read -r obj; do # From jq after "done" + user=$(jq -r '.user' <<< "$obj") + owner=$(jq -r '.owner' <<< "$obj") + umask_val=$(jq -r '.umask' <<< "$obj") + runfirst=$(jq -r '.runfirst' <<< "$obj") + file_permissions=$(jq -r '.file_permissions' <<< "$obj") + exec_file_permissions=$(jq -r '.exec_file_permissions' <<< "$obj") + dir_permissions=$(jq -r '.dir_permissions' <<< "$obj") + setgid=$(jq -r '.setgid' <<< "$obj") + expected_file_owner=$(jq -r '.expected_file_owner' <<< "$obj") + expected_config_permissions=$(jq -r '.expected_config_permissions' <<< "$obj") + expected_env_owner=$(jq -r '.expected_env_owner' <<< "$obj") + expected_env_permissions=$(jq -r '.expected_env_permissions' <<< "$obj") + should_succeed=$(jq -r '.should_succeed' <<< "$obj") + + __error_count=0 + echo + echo "Running test with these parameters" + cat <&1") + result=$? + if [[ "$result" -ne 0 ]]; then + echo "ERROR: \"ethd space\" failed to run with user $runfirst" + echo "Output was:" + echo "$output" + echo "Stopping here so this can be investigated. Note users have not been deleted yet, so you can investigate the temp directory at $__temp_dir" + exit 1 + fi + fi + output=$(sudo -u "$user" bash -c "cd $__temp_dir && umask $umask_val && ./ethd space 2>&1") + result=$? + set -e + if [[ "$result" -ne 0 && "$should_succeed" == "true" ]]; then + echo "ERROR: \"ethd space\" failed to run with user $user" + echo "Output was:" + echo "$output" + echo "Stopping here so this can be investigated. Note users have not been deleted yet, so you can investigate the temp directory at $__temp_dir" + exit 1 + elif [[ "$result" -eq 0 && "$should_succeed" == "false" ]]; then + echo "ERROR: \"ethd space\" ran successfully with user $user, but was expected to fail" + echo "Output was:" + echo "$output" + echo "Stopping here so this can be investigated. Note users have not been deleted yet, so you can investigate the temp directory at $__temp_dir" + exit 1 + elif [[ "$result" -ne 0 && "$should_succeed" == "false" ]]; then + echo "Success: \"ethd space\" failed to run with user $user, as expected" + elif [[ "$result" -eq 0 && "$should_succeed" == "true" ]]; then + echo "Success: \"ethd space\" ran successfully with user $user" + __check_config_files "$expected_file_owner" "$expected_config_permissions" + __check_other_read_perms + __check_env_file "$expected_env_owner" "$expected_env_permissions" + if [[ "$__error_count" -eq 0 ]]; then + echo "Success: All permissions are correct for user $user on the config files and .env file" + else + echo "ERROR: There were $__error_count permission errors for user $user on the config files and/or .env file" + echo "Stopping here so this can be investigated. Note users have not been deleted yet, so you can investigate the temp directory at $__temp_dir" + exit 1 + fi + set +e + output=$(sudo -u "$user" bash -c "cd $__temp_dir && umask $umask_val && ./ethd space 2>&1") + result=$? + set -e + if [[ "$output" == *"Fixing ownership of .env"* ]]; then + echo "ERROR: \"ethd space\" output contains \"Fixing ownership of .env\" for user $user" + echo "Output was:" + echo "$output" + echo "Stopping here so this can be investigated. Note users have not been deleted yet, so you can investigate the temp directory at $__temp_dir" + exit 1 + fi + if [[ "$result" -ne 0 ]]; then + echo "ERROR: \"ethd space\" failed to run when checking for idempotence with user $user" + echo "Output was:" + echo "$output" + echo "Stopping here so this can be investigated. Note users have not been deleted yet, so you can investigate the temp directory at $__temp_dir" + exit 1 + else + echo "Success: \"ethd space\" ran successfully when checking for idempotence with user $user" + fi + fi + + __delete_temp_directory + done < <(jq -c '.[]' <<< "$__test_parameters") +} + +trap '__handle_error $? ${BASH_LINENO[0]}' ERR + +__check_os +__check_workdir +__check_prereqs jq +__warn_creation +__create_users +__run_tests +__delete_users diff --git a/tests/testing-ethd.md b/tests/testing-ethd.md index 6962d1e9c..db2c1ef43 100644 --- a/tests/testing-ethd.md +++ b/tests/testing-ethd.md @@ -83,3 +83,34 @@ Test that on Hoodi and Mainnet, verify that `deposit-cli.yml` is added to `CORE_ Lido Obol can't be tested without a live Obol cluster, but run through it as far as possible to rule out obvious issues Lido SSV is identical to SSV + +## Multi-user test paths + +tests/test-multiuser.sh encodes the below tests + +Tests the code with directory ownership `eve:test-ethd-admins`; `eve`, `alice` and `bob` part of the `test-ethd-admins` group, `alice` part +of the `sudo` group and `bob` not, and setgid set or not on the directory, `g+s` and `g-s`. `charlie` is not in `node-admins`, and should be able +to succeed via `sudo -u eve` + +Also test a regular `alice:alice` setup of eth-docker, and that the code works well in that case. + +Test scenarios +- dir `alice:alice` `g-s` and 775/664 permissions, `alice` umask 022 +- dir `alice:alice` `g-s` and 700/600 permissions, `alice` with umask 077 +- dir `alice:alice` `g-s` and 775/664 permissions, `root` umask 022 +- dir `eve:test-ethd-admins` `g-s` and 775/664 permissions, `alice` with umask 022 +- dir `eve:test-ethd-admins` `g+s` and 775/664 permissions, `alice` with umask 022 +- dir `eve:test-ethd-admins` `g-s` and 770/660 permissions, `alice` with umask 077 +- dir `eve:test-ethd-admins` `g+s` and 770/660 permissions, `alice` with umask 077 +- dir `eve:test-ethd-admins` `g-s` and 775/664 permissions, `bob` umask 022 (can't sudo) +- dir `eve:test-ethd-admins` `g+s` and 775/664 permissions, `bob` umask 022 (can't sudo) +- dir `eve:test-ethd-admins` `g-s` and 770/660 permissions, `bob` umask 077 (can't sudo) after `alice` first runs +- dir `eve:test-ethd-admins` `g+s` and 770/660 permissions, `bob` umask 077 (can't sudo) after `alice` first runs +- dir `eve:test-ethd-admins` `g-s` and 775/664 permissions, `root` umask 022 +- dir `eve:test-ethd-admins` `g-s` and 770/660 permissions, `bob` umask 077 (can't sudo) without `alice` first run, should fail +- dir `eve:test-ethd-admins` `g-s` and 775/664 permissions, `charlie` (can sudo), should fail because user can't cd in + +- `./ethd space` and check `.env` ownership and permissions. Should be `user:user` when solo, `user:owner-group` when running user's group does have write rights, with group write rights kept even with umask 077 on the user, `owner:owner-group` when running user's group doesn't have write rights (invoke sudo) +- Likewise config files, same ownership expectations, and o+r permissions +- `./ethd space` a second time, no message that `.env` permissions are being fixed should be seen +- Ditto check ownership and permissions of bind-mounted files in alloy, alloy-obol, prometheus, loki, tempo, ssv-config. They need to be `other` readable.