From 3e4f3f339c7ec1a9e024cddd6142eb001a16d2eb Mon Sep 17 00:00:00 2001 From: catborise Date: Wed, 7 Oct 2026 15:01:59 +0300 Subject: [PATCH] feat(installer): idempotent secret-key, settings, perms and nginx primitives --- conf/install-lib.sh | 60 ++++++++++++++++++++++++++++++++++ conf/test_install_lib.sh | 70 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100755 conf/install-lib.sh create mode 100755 conf/test_install_lib.sh diff --git a/conf/install-lib.sh b/conf/install-lib.sh new file mode 100755 index 00000000..128625ef --- /dev/null +++ b/conf/install-lib.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Idempotent installer primitives. Sourcing this file has NO side effects; +# it only defines functions, so it can be unit-tested (conf/test_install_lib.sh). + +# Create /secret_key (mode 600) only if missing or empty; keep an +# existing key. A blank key would make Django fail to boot, so an empty file is +# treated as missing. +wvc_ensure_secret_key() { + local data_dir="$1" python="${2:-python3}" key_file + key_file="$data_dir/secret_key" + mkdir -p "$data_dir" + chmod 700 "$data_dir" + if [ ! -s "$key_file" ]; then + # umask 077 so the key is never group/world readable, even briefly + ( umask 077; "$python" -c 'import secrets; print(secrets.token_urlsafe(50))' > "$key_file" ) + fi + chmod 600 "$key_file" +} + +# Copy settings.py from the template only when it does not already exist, so a +# re-run never overwrites local edits. 0 + "created" on first copy; 1 + "kept" +# when it already exists. +wvc_ensure_settings() { + local template="$1" target="$2" + if [ -f "$target" ]; then + echo "kept" + return 1 + fi + cp "$template" "$target" + echo "created" + return 0 +} + +# Restrict modes so the secret, settings, and db are not world-readable. +wvc_harden_modes() { + local settings="$1" db="$2" data_dir="$3" + [ -e "$settings" ] && chmod 600 "$settings" + [ -e "$db" ] && chmod 600 "$db" + [ -d "$data_dir" ] && chmod 700 "$data_dir" + [ -e "$data_dir/secret_key" ] && chmod 600 "$data_dir/secret_key" + return 0 +} + +# Install the nginx main config only on first run (keeping a one-time backup of +# whatever was there), and always refresh the app's conf.d snippet. A re-run +# therefore never clobbers an operator's edited main nginx.conf. +wvc_install_nginx() { + local main_template="$1" snippet="$2" etc="$3" marker + marker="$etc/.wvc-nginx-installed" + mkdir -p "$etc/conf.d" + # Use a dedicated marker (always created on first install) rather than the + # backup file, which is absent when the host had no nginx.conf to begin with. + if [ ! -f "$marker" ]; then + [ -f "$etc/nginx.conf" ] && cp "$etc/nginx.conf" "$etc/nginx.conf.wvc-orig" + cp "$main_template" "$etc/nginx.conf" + : > "$marker" + fi + cp "$snippet" "$etc/conf.d/webvirtcloud.conf" + return 0 +} diff --git a/conf/test_install_lib.sh b/conf/test_install_lib.sh new file mode 100755 index 00000000..30d9758c --- /dev/null +++ b/conf/test_install_lib.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Unit tests for conf/install-lib.sh. Run: bash conf/test_install_lib.sh +set -u +here="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=/dev/null +. "$here/install-lib.sh" + +fail=0 +check() { if eval "$2"; then echo "ok - $1"; else echo "NOT ok - $1"; fail=1; fi; } + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +# wvc_ensure_secret_key: creates, keeps, mode 600, regenerates when empty +wvc_ensure_secret_key "$tmp/data" +check "secret key created" "[ -s '$tmp/data/secret_key' ]" +check "secret key mode 600" "[ \"\$(stat -c %a '$tmp/data/secret_key')\" = 600 ]" +first="$(cat "$tmp/data/secret_key")" +wvc_ensure_secret_key "$tmp/data" +check "existing key kept" "[ \"\$(cat '$tmp/data/secret_key')\" = \"$first\" ]" +: > "$tmp/data/secret_key" # truncate +wvc_ensure_secret_key "$tmp/data" +check "empty key regenerated" "[ -s '$tmp/data/secret_key' ]" +check "data dir is 700" "[ \"\$(stat -c %a '$tmp/data')\" = 700 ]" + +# wvc_ensure_settings: creates once, keeps on re-run +echo "TEMPLATE" > "$tmp/tpl" +check "ensure_settings returns 0 on create" "wvc_ensure_settings '$tmp/tpl' '$tmp/settings.py'" +echo "EDITED" > "$tmp/settings.py" +check "ensure_settings returns 1 when present" "! wvc_ensure_settings '$tmp/tpl' '$tmp/settings.py'" +check "existing settings kept" "[ \"\$(cat '$tmp/settings.py')\" = EDITED ]" + +# wvc_harden_modes +mkdir -p "$tmp/app/webvirtcloud" "$tmp/app/data" +echo x > "$tmp/app/webvirtcloud/settings.py"; echo x > "$tmp/app/db.sqlite3" +echo k > "$tmp/app/data/secret_key" +wvc_harden_modes "$tmp/app/webvirtcloud/settings.py" "$tmp/app/db.sqlite3" "$tmp/app/data" +check "settings 600" "[ \"\$(stat -c %a '$tmp/app/webvirtcloud/settings.py')\" = 600 ]" +check "db 600" "[ \"\$(stat -c %a '$tmp/app/db.sqlite3')\" = 600 ]" +check "data 700" "[ \"\$(stat -c %a '$tmp/app/data')\" = 700 ]" +check "secret 600" "[ \"\$(stat -c %a '$tmp/app/data/secret_key')\" = 600 ]" + +# wvc_install_nginx: first run installs main + snippet, keeps backup; re-run keeps main +etc="$tmp/etc/nginx"; mkdir -p "$etc" +echo "ORIGINAL" > "$etc/nginx.conf" +echo "MAINTPL" > "$tmp/main_tpl"; echo "SNIPPET" > "$tmp/snippet" +wvc_install_nginx "$tmp/main_tpl" "$tmp/snippet" "$etc" +check "main replaced on first run" "[ \"\$(cat '$etc/nginx.conf')\" = MAINTPL ]" +check "original backed up" "[ \"\$(cat '$etc/nginx.conf.wvc-orig')\" = ORIGINAL ]" +check "snippet installed" "[ \"\$(cat '$etc/conf.d/webvirtcloud.conf')\" = SNIPPET ]" +echo "HANDEDITED" > "$etc/nginx.conf" +echo "SNIPPET2" > "$tmp/snippet" +wvc_install_nginx "$tmp/main_tpl" "$tmp/snippet" "$etc" +check "main NOT replaced on re-run" "[ \"\$(cat '$etc/nginx.conf')\" = HANDEDITED ]" +check "snippet refreshed on re-run" "[ \"\$(cat '$etc/conf.d/webvirtcloud.conf')\" = SNIPPET2 ]" + +# fresh host: no existing main nginx.conf must not error +etc2="$tmp/etc2/nginx"; mkdir -p "$etc2" +wvc_install_nginx "$tmp/main_tpl" "$tmp/snippet" "$etc2" +check "fresh host installs main" "[ \"\$(cat '$etc2/nginx.conf')\" = MAINTPL ]" + +# fresh host (no original config): a second run must NOT clobber operator edits +etc3="$tmp/etc3/nginx"; mkdir -p "$etc3" +wvc_install_nginx "$tmp/main_tpl" "$tmp/snippet" "$etc3" +echo "OP_EDIT" > "$etc3/nginx.conf" +wvc_install_nginx "$tmp/main_tpl" "$tmp/snippet" "$etc3" +check "no-orig second run keeps operator edit" "[ \"\$(cat '$etc3/nginx.conf')\" = OP_EDIT ]" + +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES" +exit "$fail"