feat(clients): add client details and secure config export

This commit is contained in:
2026-10-13 00:52:56 +03:00
parent 2e40d5bd1a
commit 2bec75aa9d
6 changed files with 527 additions and 22 deletions
+313 -1
View File
@@ -11,7 +11,7 @@ source "${PROJECT_DIR}/wg-manager.sh"
case $# in
0) ;;
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names|show-client|config-export|qr|path-safety|no-secret-default-output) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
*) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;;
esac
test_group=${1-}
@@ -830,6 +830,7 @@ transaction_builder_symlink() {
run_m4_tests() {
local config_file="${TEST_ROOT}/m4.conf" fixture="${TEST_ROOT}/wg0.conf" runtime_file="${TEST_ROOT}/runtime.dump" status output before
unset TRANSACTION_FINALIZER TRANSACTION_ROLLBACK_ACTIVE ADD_PUBLISH_READY_FIFO ADD_PUBLISH_RELEASE_FIFO ADD_PUBLISH_PID_FILE ADD_PUBLISH_READY_INDEX ADD_PUBLISH_FAIL ADD_PUBLISH_RACE TRANSACTION_ADD_STAGE_FAIL TRANSACTION_ADD_DIRECTORY_FAIL
cp -- "${PROJECT_DIR}/tests/fixtures/m3/source.conf" "$fixture"
write_m2_config "$config_file" "$fixture"
export WG_MANAGER_CONFIG="$config_file" WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/backups" WG_MANAGER_LOG_DIR="${TEST_ROOT}/logs" WG_MANAGER_METADATA_DIR="${TEST_ROOT}/metadata" WG_MANAGER_ARCHIVE_DIR="${TEST_ROOT}/archive" WG_MANAGER_LOCK_FILE="${TEST_ROOT}/lock" WG_MANAGER_NOW=1700000100 WG_MANAGER_TEST_MODE=1 WG_MOCK_INTERFACE=wg0
@@ -1234,6 +1235,312 @@ EOF
esac
}
run_m5_tests() {
local config_file="${TEST_ROOT}/m5.conf" fixture="${TEST_ROOT}/wg0.conf" runtime_file="${TEST_ROOT}/runtime.dump" status output expected qr_path mode
cp -- "${PROJECT_DIR}/tests/fixtures/m3/source.conf" "$fixture"
write_m2_config "$config_file" "$fixture"
export WG_MANAGER_CONFIG="$config_file" WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/backups" WG_MANAGER_LOG_DIR="${TEST_ROOT}/logs" WG_MANAGER_METADATA_DIR="${TEST_ROOT}/metadata" WG_MANAGER_ARCHIVE_DIR="${TEST_ROOT}/archive" WG_MANAGER_LOCK_FILE="${TEST_ROOT}/lock" WG_MANAGER_TEST_MODE=1 WG_MOCK_INTERFACE=wg0
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\nFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=\t(none)\tvpn.example.invalid:51820\t192.0.2.3/32\t0\t0\t0\t0\n' >"$runtime_file"
rm -rf -- "${TEST_ROOT}/dump-sequence"; mkdir -p "${TEST_ROOT}/dump-sequence"; cp -- "$runtime_file" "${TEST_ROOT}/dump-sequence/1"; cp -- "${PROJECT_DIR}/tests/fixtures/m3/runtime.dump" "${TEST_ROOT}/dump-sequence/2"
export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_DUMP_SEQUENCE_DIR="${TEST_ROOT}/dump-sequence"
unset WG_SYNC_FAIL WG_SYNC_FAIL_ONCE_FILE WG_SYNC_FAIL_ON_CALL WG_SYNC_FAIL_COUNTER_FILE WG_QUICK_STRIP_FAIL QR_MOCK_FAIL TRANSACTION_ADD_STAGE_FAIL TRANSACTION_ADD_DIRECTORY_FAIL ADD_PUBLISH_FAIL ADD_PUBLISH_RACE
set_runtime_paths; load_config; rm -rf -- "$METADATA_DIR" "$CLIENT_DIR"; mkdir -p "$METADATA_DIR" "$CLIENT_DIR"
add_client alice >/dev/null
: >"$TEST_MOCK_LOG"
case $test_group in
''|show-client|no-secret-default-output)
begin_test 'show exposes only non-secret managed details'
set +e; output=$(show_client alice 2>&1); status=$?; set -e
assert_equal 0 "$status" 'show succeeds'
assert_contains 'Имя: alice' "$output" 'show name'
assert_contains 'Путь конфигурации:' "$output" 'show validated path'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' <<<"$output" || true)" 'show without secrets'
local cli_stdout="${TEST_ROOT}/cli.stdout" cli_stderr="${TEST_ROOT}/cli.stderr"
set +e; "${PROJECT_DIR}/wg-manager.sh" show alice >"$cli_stdout" 2>"$cli_stderr"; status=$?; set -e
assert_equal 0 "$status" 'CLI show succeeds'
assert_contains 'Имя: alice' "$(<"$cli_stdout")" 'CLI show name'
assert_contains 'Публичный ключ:' "$(<"$cli_stdout")" 'CLI show public key'
assert_contains 'Адреса:' "$(<"$cli_stdout")" 'CLI show addresses'
assert_contains 'Endpoint:' "$(<"$cli_stdout")" 'CLI show endpoint'
assert_contains 'Последний handshake:' "$(<"$cli_stdout")" 'CLI show handshake'
assert_contains 'RX:' "$(<"$cli_stdout")" 'CLI show RX'
assert_contains 'TX:' "$(<"$cli_stdout")" 'CLI show TX'
assert_contains 'Режим:' "$(<"$cli_stdout")" 'CLI show mode'
assert_contains 'Создан:' "$(<"$cli_stdout")" 'CLI show creation time'
assert_contains 'Происхождение: managed' "$(<"$cli_stdout")" 'CLI show origin'
assert_equal '' "$(<"$cli_stderr")" 'CLI show stderr clean'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' "$cli_stdout" || true)" 'CLI show secret-free'
assert_equal '' "$(grep -F $'\033' "$cli_stdout" || true)" 'CLI show has no ANSI'
for cli_args in 'show' 'config' 'qr'; do
set +e; "${PROJECT_DIR}/wg-manager.sh" $cli_args >"$cli_stdout" 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" "CLI ${cli_args} missing name"
done
set +e; "${PROJECT_DIR}/wg-manager.sh" show absent >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'CLI missing client name validation'
set +e; "${PROJECT_DIR}/wg-manager.sh" show alice extra >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'CLI show extra argument'
set +e; "${PROJECT_DIR}/wg-manager.sh" qr alice extra >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'CLI qr extra argument'
set +e; "${PROJECT_DIR}/wg-manager.sh" config alice --show --show >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'CLI duplicate config show'
set +e; "${PROJECT_DIR}/wg-manager.sh" show fixture >"$cli_stdout" 2>"$cli_stderr"; status=$?; set -e
assert_equal 0 "$status" 'CLI show legacy peer succeeds'
assert_contains 'Происхождение: legacy' "$(<"$cli_stdout")" 'legacy origin shown'
assert_equal '' "$(grep -F 'Путь конфигурации:' "$cli_stdout" || true)" 'legacy has no config path'
set +e; "${PROJECT_DIR}/wg-manager.sh" config fixture >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'legacy config export rejected'
set +e; "${PROJECT_DIR}/wg-manager.sh" qr fixture >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'legacy QR rejected'
begin_test 'ambiguous legacy and mismatched identities never grant access'
cp -- "${PROJECT_DIR}/tests/fixtures/m3/source.conf" "$fixture"
cat >>"$fixture" <<'EOF'
[Peer]
### Client fixture
PublicKey = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=
AllowedIPs = 192.0.2.3/32
EOF
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\nFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=\t(none)\tvpn.example.invalid:51820\t192.0.2.3/32\t0\t0\t0\t0\n' >"$runtime_file"
unset WG_MOCK_DUMP_SEQUENCE_DIR
export WG_MOCK_DUMP_FILE="$runtime_file"
rm -f -- "${METADATA_DIR}/alice.meta"
set +e; "${PROJECT_DIR}/wg-manager.sh" show fixture >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'duplicate legacy name is ambiguous'
assert_equal '' "$(grep -E 'EEEE|GGGG|PrivateKey|PresharedKey|sentinel' "$cli_stderr" || true)" 'ambiguous identity diagnostic is secret-free'
write_alice_metadata
replace_config_line "${METADATA_DIR}/alice.meta" 'IPV4=' 'IPV4=192.0.2.99'
set +e; "${PROJECT_DIR}/wg-manager.sh" show alice >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'mismatched metadata address is rejected'
set +e; "${PROJECT_DIR}/wg-manager.sh" config alice >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'mismatched metadata config is rejected'
write_alice_metadata
replace_config_line "${METADATA_DIR}/alice.meta" 'PUBLIC_KEY=' 'PUBLIC_KEY=HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH='
set +e; "${PROJECT_DIR}/wg-manager.sh" qr alice >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'mismatched metadata key QR is rejected'
cp -- "${PROJECT_DIR}/tests/fixtures/m3/source.conf" "$fixture"
write_alice_metadata
replace_config_line "${METADATA_DIR}/alice.meta" 'PUBLIC_KEY=' 'PUBLIC_KEY=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF='
replace_config_line "${METADATA_DIR}/alice.meta" 'IPV4=' 'IPV4=192.0.2.3'
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t0\t0\t0\nFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=\t(none)\tvpn.example.invalid:51820\t192.0.2.3/32\t0\t0\t0\t0\n' >"$runtime_file"
set +e; "${PROJECT_DIR}/wg-manager.sh" show alice >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'runtime-only peer cannot use metadata path'
set +e; "${PROJECT_DIR}/wg-manager.sh" config alice >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'runtime-only peer config is rejected'
set +e; "${PROJECT_DIR}/wg-manager.sh" qr alice >/dev/null 2>"$cli_stderr"; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'runtime-only peer QR is rejected'
set -e
if [[ $- == *e* ]]; then runtime_errexit=1; else runtime_errexit=0; fi
assert_equal 1 "$runtime_errexit" 'errexit enabled before runtime load'
load_runtime_state
if [[ $- == *e* ]]; then runtime_errexit=1; else runtime_errexit=0; fi
assert_equal 1 "$runtime_errexit" 'errexit preserved after enabled runtime load'
set +e
if [[ $- == *e* ]]; then runtime_errexit=1; else runtime_errexit=0; fi
assert_equal 0 "$runtime_errexit" 'errexit disabled before runtime load'
load_runtime_state
if [[ $- == *e* ]]; then runtime_errexit=1; else runtime_errexit=0; fi
assert_equal 0 "$runtime_errexit" 'errexit preserved after disabled runtime load'
set -e
write_alice_metadata
rm -f -- "${CLIENT_DIR}/alice.conf"
set +e; output=$(show_client alice 2>&1); status=$?; set -e
assert_equal 0 "$status" 'show tolerates unavailable client config'
assert_contains '(недоступен)' "$output" 'show marks unavailable config'
if [[ $test_group == no-secret-default-output ]]; then assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' "$TEST_MOCK_LOG" || true)" 'default mock log without secrets'; fi
return
;;
config-export)
begin_test 'config prints path by default and content only with --show'
expected="${CLIENT_DIR}/alice.conf"
output=$(export_client_config alice)
assert_equal "$expected" "$output" 'default config export path'
local cli_path_bin="${TEST_ROOT}/config-path" cli_out="${TEST_ROOT}/config.stdout" cli_err="${TEST_ROOT}/config.stderr"
mkdir -p -- "$cli_path_bin"
for utility in wg cat stat sort paste date; do ln -s -- "$(command -v "$utility")" "$cli_path_bin/$utility"; done
set +e; PATH="$cli_path_bin" /bin/bash "${PROJECT_DIR}/wg-manager.sh" config alice >"$cli_out" 2>"$cli_err"; status=$?; set -e
assert_equal 0 "$status" 'fresh CLI config succeeds without gum or qrencode'
assert_equal "$expected" "$(<"$cli_out")" 'fresh CLI config prints one exact path'
assert_equal '' "$(<"$cli_err")" 'fresh CLI config stderr is empty'
assert_equal '' "$(grep -F $'\033' "$cli_out" || true)" 'fresh CLI config has no ANSI'
set +e; PATH="$cli_path_bin" /bin/bash "${PROJECT_DIR}/wg-manager.sh" config alice --show >"$cli_out" 2>"$cli_err"; status=$?; set -e
assert_equal 0 "$status" 'fresh CLI explicit config succeeds'
assert_equal "$(<"${PROJECT_DIR}/tests/fixtures/m5/managed-client.conf")" "$(<"$cli_out")" 'fresh CLI explicit config exact bytes'
assert_equal '' "$(<"$cli_err")" 'fresh CLI explicit config stderr is empty'
set +e; PATH="$cli_path_bin" /bin/bash "${PROJECT_DIR}/wg-manager.sh" config alice --bad >"$cli_out" 2>"$cli_err"; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'fresh CLI unknown config flag rejected'
set +e; output=$(export_client_config alice 1); status=$?; set -e
assert_equal 0 "$status" 'explicit full export'
assert_equal "$(<"${PROJECT_DIR}/tests/fixtures/m5/managed-client.conf")" "$output" 'explicit config content matches fixture'
set +e; export_client_config alice 2 >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_USAGE" "$status" 'invalid export mode rejected'
rm -f -- "${CLIENT_DIR}/alice.conf"
set +e; output=$(export_client_config alice 1 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'missing export config rejected'
assert_contains 'Файл конфигурации клиента' "$output" 'missing export diagnostic'
printf 'placeholder\n' >"${CLIENT_DIR}/alice.conf"; chmod 600 "${CLIENT_DIR}/alice.conf"
open_validated_client_config() { return "$EXIT_OPERATION"; }
set +e; output=$(export_client_config alice 1 2>&1); status=$?; set -e
assert_equal "$EXIT_OPERATION" "$status" 'export open failure status preserved'
assert_contains 'безопасно открыть' "$output" 'export open failure diagnostic'
return
;;
qr)
begin_test 'qr uses exact argv and stdin without logging config'
export QR_MOCK_EXPECTED_FILE="${CLIENT_DIR}/alice.conf"
: >"$TEST_MOCK_LOG"
output=$(qr_client alice)
assert_contains 'MOCK-QR' "$output" 'mock QR output'
assert_equal 1 "$(grep -c '^qrencode -t ansiutf8$' "$TEST_MOCK_LOG" || true)" 'exact QR invocation count'
assert_equal 'qrencode -t ansiutf8' "$(tail -n 1 "$TEST_MOCK_LOG")" 'exact QR argv'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' "$TEST_MOCK_LOG" || true)" 'QR log without secrets'
export QR_MOCK_FAIL=1
set +e; qr_client alice >"${TEST_ROOT}/qr-fail.stdout" 2>"${TEST_ROOT}/qr-fail.stderr"; status=$?; set -e
unset QR_MOCK_FAIL
assert_equal "$EXIT_OPERATION" "$status" 'QR failure status'
assert_equal '' "$(<"${TEST_ROOT}/qr-fail.stdout")" 'QR failure stdout is empty'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG|\[Interface\]|\[Peer\]' "${TEST_ROOT}/qr-fail.stderr" || true)" 'QR failure diagnostic is secret-free'
local saved_path=$PATH
mkdir -p -- "${TEST_ROOT}/empty-path"
ln -s -- "${PROJECT_DIR}/tests/mocks/bin/wg" "${TEST_ROOT}/empty-path/wg"
ln -s -- "$(command -v cat)" "${TEST_ROOT}/empty-path/cat"
ln -s -- "$(command -v stat)" "${TEST_ROOT}/empty-path/stat"
ln -s -- "$(command -v sort)" "${TEST_ROOT}/empty-path/sort"
ln -s -- "$(command -v paste)" "${TEST_ROOT}/empty-path/paste"
ln -s -- "$(command -v date)" "${TEST_ROOT}/empty-path/date"
PATH="${TEST_ROOT}/empty-path"
set +e; qr_client alice >"${TEST_ROOT}/qr-missing.stdout" 2>"${TEST_ROOT}/qr-missing.stderr"; status=$?; set -e
PATH=$saved_path
assert_equal "$EXIT_DEPENDENCY" "$status" 'missing qrencode dependency status'
assert_equal "${CLIENT_DIR}/alice.conf" "$(<"${TEST_ROOT}/qr-missing.stdout")" 'missing qrencode safe path only on stdout'
assert_contains 'qrencode не найден' "$(<"${TEST_ROOT}/qr-missing.stderr")" 'missing qrencode diagnostic on stderr'
assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|EEEE|GGGG' "${TEST_ROOT}/qr-missing.stderr" || true)" 'missing QR diagnostic is secret-free'
open_validated_client_config() { return "$EXIT_VALIDATION"; }
set +e; output=$(qr_client alice 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'QR identity failure status preserved'
assert_contains 'безопасно открыть' "$output" 'QR identity failure diagnostic'
unset QR_MOCK_EXPECTED_FILE
return
;;
path-safety)
begin_test 'unsafe or permissive config paths are rejected'
cp -- "${METADATA_DIR}/alice.meta" "${TEST_ROOT}/alice-meta.base"
cp -- "${TEST_ROOT}/alice-meta.base" "${METADATA_DIR}/alice.meta"
printf 'NAME=alice\nPUBLIC_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nIPV4=192.0.2.2\nCREATED_AT=1700000000\nCONFIG_PATH=%s\nconf\nCLIENT_MODE=full-tunnel\nMANAGED=1\n' "$CLIENT_DIR/alice" >"${METADATA_DIR}/alice.meta"
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'metadata embedded newline/control record rejected'
assert_equal '' "$(printf '%s' "$output" | grep -E 'sentinel|PrivateKey|PresharedKey|BBBB|EEEE|GGGG' || true)" 'newline metadata diagnostic is secret-free'
cp -- "${TEST_ROOT}/alice-meta.base" "${METADATA_DIR}/alice.meta"
local real_stat
real_stat=$(command -v stat)
stat() {
if [[ ${2:-} == '%u:%g' && ${4:-} == "${METADATA_DIR}/alice.meta" ]]; then
printf '999:999\n'
return 0
fi
"$real_stat" "$@"
}
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
unset -f stat
assert_equal "$EXIT_VALIDATION" "$status" 'metadata owner mismatch is rejected'
assert_contains 'Владелец' "$output" 'metadata owner mismatch diagnostic'
assert_equal '' "$(printf '%s' "$output" | grep -E 'PrivateKey|PresharedKey|BBBB|EEEE|GGGG' || true)" 'owner mismatch diagnostic is secret-free'
real_stat=$(command -v stat)
stat() {
if [[ ${2:-} == '%a' && ${4:-} == "${CLIENT_DIR}/alice.conf" ]]; then
return 1
fi
"$real_stat" "$@"
}
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
unset -f stat
assert_equal "$EXIT_OPERATION" "$status" 'config stat failure is operation error'
assert_contains 'режим конфигурации клиента' "$output" 'config stat failure diagnostic'
assert_equal '' "$(printf '%s' "$output" | grep -E 'PrivateKey|PresharedKey|BBBB|EEEE|GGGG' || true)" 'config stat diagnostic is secret-free'
real_stat=$(command -v stat)
stat() {
if [[ ${2:-} == '%a' && ${4:-} == "${METADATA_DIR}/alice.meta" ]]; then
return 1
fi
"$real_stat" "$@"
}
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
unset -f stat
assert_equal "$EXIT_OPERATION" "$status" 'metadata stat failure is operation error'
assert_contains 'режим метаданных клиента' "$output" 'metadata stat failure diagnostic'
assert_equal '' "$(printf '%s' "$output" | grep -E 'PrivateKey|PresharedKey|BBBB|EEEE|GGGG' || true)" 'metadata stat diagnostic is secret-free'
cp -- "${TEST_ROOT}/alice-meta.base" "${METADATA_DIR}/alice.meta"
for unsafe_path in "${CLIENT_DIR}/nested/alice.conf" "${CLIENT_DIR}/alice file.conf" "${CLIENT_DIR}/-alice.conf" "${CLIENT_DIR}/alice\$'\\n'conf"; do
cp -- "${TEST_ROOT}/alice-meta.base" "${METADATA_DIR}/alice.meta"
replace_config_line "${METADATA_DIR}/alice.meta" 'CONFIG_PATH=' "CONFIG_PATH=${unsafe_path}"
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" "unsafe path rejected: ${unsafe_path@Q}"
assert_equal '' "$(printf '%s' "$output" | grep -E 'sentinel|PrivateKey|PresharedKey|EEEE|GGGG' || true)" 'unsafe path diagnostic is secret-free'
done
cp -- "${TEST_ROOT}/alice-meta.base" "${METADATA_DIR}/alice.meta"
mkdir -p -- "${CLIENT_DIR}/nested"
printf 'sentinel\n' >"${CLIENT_DIR}/nested/alice.conf"
replace_config_line "${METADATA_DIR}/alice.meta" 'CONFIG_PATH=' "CONFIG_PATH=${CLIENT_DIR}/nested/alice.conf"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'nested client path rejected'
rm -f -- "${CLIENT_DIR}/nested/alice.conf"; rmdir -- "${CLIENT_DIR}/nested"
cp -- "${TEST_ROOT}/alice-meta.base" "${METADATA_DIR}/alice.meta"
rm -- "${CLIENT_DIR}/alice.conf"; mkdir -- "${CLIENT_DIR}/alice.conf"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'directory client target rejected'
rmdir -- "${CLIENT_DIR}/alice.conf"
printf 'placeholder\n' >"${CLIENT_DIR}/alice.conf"; chmod 600 "${CLIENT_DIR}/alice.conf"
cp -- "${TEST_ROOT}/alice-meta.base" "${METADATA_DIR}/alice.meta"
mv -- "$CLIENT_DIR" "${TEST_ROOT}/clients-real"
ln -s -- "${TEST_ROOT}/clients-real" "$CLIENT_DIR"
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'symlink client directory rejected'
rm -- "$CLIENT_DIR"; mv -- "${TEST_ROOT}/clients-real" "$CLIENT_DIR"
mv -- "$METADATA_DIR" "${TEST_ROOT}/metadata-real"
ln -s -- "${TEST_ROOT}/metadata-real" "$METADATA_DIR"
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'symlink metadata directory rejected'
rm -- "$METADATA_DIR"; mv -- "${TEST_ROOT}/metadata-real" "$METADATA_DIR"
chmod 755 "$CLIENT_DIR" "$METADATA_DIR"
set +e; output=$(export_client_config alice 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'permissive client directories rejected'
chmod 700 "$CLIENT_DIR" "$METADATA_DIR"
replace_config_line "${METADATA_DIR}/alice.meta" 'CONFIG_PATH=' 'CONFIG_PATH="${TEST_ROOT}/outside.conf"'
set +e; config_path=$(export_client_config alice 2>&1); status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'outside path rejected'
sed -i "s#CONFIG_PATH=.*#CONFIG_PATH=${CLIENT_DIR}/alice.conf#" "${METADATA_DIR}/alice.meta"
chmod 644 "${CLIENT_DIR}/alice.conf"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'permissive config rejected'
chmod 600 "${CLIENT_DIR}/alice.conf"
chmod 644 "${METADATA_DIR}/alice.meta"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'permissive metadata rejected'
chmod 600 "${METADATA_DIR}/alice.meta"
ln -- "${METADATA_DIR}/alice.meta" "${TEST_ROOT}/alice-meta-hardlink"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'hardlinked metadata rejected'
rm -f -- "${TEST_ROOT}/alice-meta-hardlink"
ln -- "${CLIENT_DIR}/alice.conf" "${TEST_ROOT}/alice-hardlink.conf"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'hardlinked config rejected'
rm -f -- "${TEST_ROOT}/alice-hardlink.conf"
mv -- "${CLIENT_DIR}/alice.conf" "${CLIENT_DIR}/alice-real.conf"
ln -s -- "${CLIENT_DIR}/alice-real.conf" "${CLIENT_DIR}/alice.conf"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'symlink config rejected'
rm -- "${CLIENT_DIR}/alice.conf"
mv -- "${CLIENT_DIR}/alice-real.conf" "${CLIENT_DIR}/alice.conf"
printf 'sentinel\n' >"${TEST_ROOT}/outside-meta"
mv -- "${METADATA_DIR}/alice.meta" "${METADATA_DIR}/alice.real.meta"
rm -f -- "${METADATA_DIR}/alice.real.meta"
ln -s -- "${TEST_ROOT}/outside-meta" "${METADATA_DIR}/alice.meta"
set +e; export_client_config alice >/dev/null 2>&1; status=$?; set -e
assert_equal "$EXIT_VALIDATION" "$status" 'symlink metadata rejected'
return
;;
esac
}
run_m3_tests() {
local config_file="${TEST_ROOT}/m3.conf" fixture="${TEST_ROOT}/wg0.conf" runtime_file="${TEST_ROOT}/runtime.dump" status output before after backup_count manifest_content parent_exit parent_int parent_term
cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture"
@@ -2037,6 +2344,8 @@ fi
if [[ $test_group == config ]]; then
run_config_tests
elif [[ $test_group == show-client || $test_group == config-export || $test_group == qr || $test_group == path-safety || $test_group == no-secret-default-output ]]; then
run_m5_tests
elif [[ $test_group == parser || $test_group == status || $test_group == list || $test_group == list-redirected || $test_group == list-deterministic ]]; then
run_m2_focused "$test_group"
elif [[ $test_group == transaction || $test_group == rollback || $test_group == locking || $test_group == backups || $test_group == mutation-preserves-source || $test_group == secret-modes ]]; then
@@ -2052,6 +2361,9 @@ else
run_m2_tests
run_m3_tests
run_m4_tests
test_group=show-client
run_m5_tests
test_group=''
fi
finish_tests