#!/bin/bash # The wizard's Backups step, on the apply side. # # scripts/dev/lp-backup-setup-test # # Two things the step submits, and the reason each matters: # # backup_mode Automatic or Manual. Manual has to REMOVE a schedule # that is already installed, not merely decline to add # one, or answering "Manual" changes nothing. # backup_locations Destinations. A password arrives as a REFERENCE, never a # value: the whole wizard payload is base64'd into a task's # command string, and tasks are recorded in # frontend/data/tasks/*.json — 0644, in a world-readable # directory. A backup repository password sent that way is # readable by any local account. # # So the case that matters most is the last one: what reaches the config file is # the secret, and what was in the payload never was. REPO="$(cd "$(dirname "$0")/../.." && pwd)" BASE="$(mktemp -d "${TMPDIR:-/tmp}/lp-bksetup-XXXXXX")" trap 'rm -rf "$BASE"' EXIT command -v jq >/dev/null 2>&1 || { echo " SKIP jq not installed"; exit 0; } fail=0 chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; } # Extract the block under test from setup_apply.sh so edits there are what break # this, rather than a copy drifting out of date. # From the mode comment to the line that hands off to the legacy single- # destination path, which is where the new block ends. BLOCK=$(awk '/^ # Automatic or Manual\./{f=1} f{print} /^ backup_dest=""$/{exit}' \ "$REPO/scripts/setup/setup_apply.sh") BLOCK="$BLOCK fi" [[ -n "$BLOCK" ]] || { echo " FAIL could not extract the backup block from setup_apply.sh"; exit 1; } run() { # run local payload="$1" cat > "$BASE/run.sh" < "\$configs_dir/backup/backup_general" isSuccessful(){ echo "OK: \$*"; }; isNotice(){ echo "NOTE: \$*"; }; isError(){ echo "ERR: \$*"; } crontabSetupBackupScheduler(){ echo "SCHED_CALLED"; } locationAdd(){ echo "LOCADD:\$1:\$2" >> "$BASE/calls"; echo 7; } # The real one returns an existing file; the block skips a destination whose # config is missing, which is correct behaviour and not what is under test here. backupLocationConfig(){ local f="$BASE/loc.\$1.config"; : > "\$f"; echo "\$f"; } engineInitLocation(){ echo "INIT:\$1" >> "$BASE/calls"; return 0; } webuiSecretResolve(){ echo "RESOLVE:\$1" >> "$BASE/calls"; printf 'the-actual-password'; } updateConfigOption(){ echo "SET:\$1=\$2" >> "$BASE/calls"; } backup_dest="" # Parsed earlier in setup_apply, outside the extracted region. backup_mode=\$(echo "\$payload" | jq -r '.backup_mode // ""') $(printf '%s' "$BLOCK") EOS : > "$BASE/calls" bash "$BASE/run.sh" 2>&1 } echo "--- Manual is applied and the scheduler is re-run ---" out=$(run '{"backup_mode":"manual"}') chk "mode written" "$(grep -c 'SET:CFG_BACKUP_MODE=manual' "$BASE/calls")" "1" chk "scheduler re-run" "$(grep -c 'SCHED_CALLED' <<< "$out")" "1" echo "--- a new remote destination ---" out=$(run '{"backup_mode":"automatic","backup_locations":[{"name":"Offsite","type":"sftp","ssh_host":"h.example.org","ssh_user":"lp","ssh_path":"/srv/lp","password_ref":"secret:deadbeefcafe"}]}') chk "location created" "$(grep -c 'LOCADD:Offsite:sftp' "$BASE/calls")" "1" chk "host set" "$(grep -c 'SET:CFG_BACKUP_LOC_7_SSH_HOST=h.example.org' "$BASE/calls")" "1" chk "user set" "$(grep -c 'SET:CFG_BACKUP_LOC_7_SSH_USER=lp' "$BASE/calls")" "1" chk "enabled" "$(grep -c 'SET:CFG_BACKUP_LOC_7_ENABLED=true' "$BASE/calls")" "1" chk "repo initialised" "$(grep -c 'INIT:7' "$BASE/calls")" "1" echo "--- the password: a reference in, the secret out ---" chk "reference redeemed" "$(grep -c 'RESOLVE:secret:deadbeefcafe' "$BASE/calls")" "1" chk "secret written" "$(grep -c 'SET:CFG_BACKUP_LOC_7_PASSWORD=the-actual-password' "$BASE/calls")" "1" chk "reference never written as the value" \ "$(grep -c 'SET:CFG_BACKUP_LOC_7_PASSWORD=secret:' "$BASE/calls")" "0" echo "--- editing the destination that already exists ---" out=$(run '{"backup_locations":[{"idx":1,"name":"Local disk","type":"local","path":"/mnt/usb/lp"}]}') chk "no new location" "$(grep -c 'LOCADD' "$BASE/calls")" "0" chk "path set on 1" "$(grep -c 'SET:CFG_BACKUP_LOC_1_PATH=/mnt/usb/lp' "$BASE/calls")" "1" chk "switched to custom" "$(grep -c 'SET:CFG_BACKUP_LOC_1_PATH_MODE=custom' "$BASE/calls")" "1" echo "--- an unusable reference must not blank the password ---" out=$(run '{"backup_locations":[{"idx":1,"type":"local","password_ref":"secret:gone"}]}' ) # webuiSecretResolve is stubbed to succeed, so re-stub it as failing for this case cat > "$BASE/run2.sh" < "$BASE/calls" out=$(bash "$BASE/run2.sh" 2>&1) chk "password untouched" "$(grep -c 'SET:CFG_BACKUP_LOC_1_PASSWORD' "$BASE/calls")" "0" chk "and it says so" "$(grep -c 'Could not read the password' <<< "$out")" "1" echo "" if (( fail )); then echo "FAILED"; exit 1; fi echo "All backup-setup checks passed."