fix: Final-Review-Findings (GUI-Login, FK-Kaskaden, Validierung, Duplikat-Recheck, Views)

F1: GUI-Login-Hash gegen $-Korruption gehaertet. .env-Werte einfach-gequotet
    geschrieben + Sanity-Check nach Sourcing. Zusaetzlich systemd-Ebene:
    podman escapt $->$$ in Environment=, systemd 252 kollabiert das nicht ->
    sed-Post-Processing der Unit-Dateien. Live-.env: GUI-Passwort rotiert.
F2: Explizite Dependent-Deletes gegen FK-500 (delete_scenario/loan), 409 bei
    bestaetigtem Import-Delete. Test-Engine erzwingt PRAGMA foreign_keys=ON.
F3: Enum/Range-Validierung (Literal + Field ge/le) fuer Recurring, Loan,
    Modifier inkl. PATCH-Pfade.
F4: Confirm nur fuer draft-Statements (sonst 409) + Dedup-Re-Check gegen
    bestaetigte Buchungen vor dem Promoten.
F5: v_balance_history/v_balance_total addieren pro Konto den opening_balance
    des fruehesten bestaetigten Statements (approved deviation vom Plan-SQL).
F6: patch_scenario Namenskollision -> 409; create_transaction unbekanntes
    Konto -> 404 (statt 500).
F7: Generierte systemd-Unit-Dateien chmod 600 (enthalten Env-Secrets).
F8: Globaler htmx:responseError-Handler in base.html.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 20:11:31 +02:00
parent b3b40a5982
commit cf9282c716
10 changed files with 265 additions and 31 deletions

View File

@@ -53,15 +53,26 @@ GRAFANA_DASHBOARDS_DIR="$FINANCE_DIR/grafana/dashboards"
# unchanged, so re-running this script never rotates secrets under existing
# data.
if [ ! -f "$ENV_FILE" ]; then
# Compute all values into variables first, then write them SINGLE-QUOTED.
# The GUI password hash has the form salt$digest; if written unquoted, the
# later `set -a; . "$ENV_FILE"` would shell-expand the "$digest" part and
# corrupt the hash, breaking GUI login. Single quotes make every value a
# literal string on sourcing.
GUI_PASSWORD=$(openssl rand -base64 12)
POSTGRES_PASSWORD_VAL=$(openssl rand -hex 16)
FINANCE_READ_PASSWORD_VAL=$(openssl rand -hex 16)
FB_API_KEY_VAL=$(openssl rand -hex 32)
FB_SESSION_SECRET_VAL=$(openssl rand -hex 32)
FB_GUI_PASSWORD_HASH_VAL=$(python3 -c "import hashlib,secrets,sys;s=secrets.token_hex(16);pw=sys.argv[1];print(s+'\$'+hashlib.pbkdf2_hmac('sha256',pw.encode(),s.encode(),200000).hex())" "$GUI_PASSWORD")
GRAFANA_ADMIN_PASSWORD_VAL=$(openssl rand -base64 12)
cat > "$ENV_FILE" <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 16)
FINANCE_READ_PASSWORD=$(openssl rand -hex 16)
FB_API_KEY=$(openssl rand -hex 32)
FB_SESSION_SECRET=$(openssl rand -hex 32)
FB_GUI_USER=admin
FB_GUI_PASSWORD_HASH=$(python3 -c "import hashlib,secrets;s=secrets.token_hex(16);print(s+'\$'+hashlib.pbkdf2_hmac('sha256',b'$GUI_PASSWORD',s.encode(),200000).hex())")
GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 12)
POSTGRES_PASSWORD='$POSTGRES_PASSWORD_VAL'
FINANCE_READ_PASSWORD='$FINANCE_READ_PASSWORD_VAL'
FB_API_KEY='$FB_API_KEY_VAL'
FB_SESSION_SECRET='$FB_SESSION_SECRET_VAL'
FB_GUI_USER='admin'
FB_GUI_PASSWORD_HASH='$FB_GUI_PASSWORD_HASH_VAL'
GRAFANA_ADMIN_PASSWORD='$GRAFANA_ADMIN_PASSWORD_VAL'
EOF
chmod 600 "$ENV_FILE"
echo "NEU ERZEUGT: GUI-Login admin / $GUI_PASSWORD (jetzt notieren!)"
@@ -69,6 +80,16 @@ EOF
fi
set -a; . "$ENV_FILE"; set +a
# Sanity check: the GUI password hash must survive shell sourcing intact. An
# unquoted value containing a literal '$' gets mangled by parameter expansion,
# which would silently break GUI login. Require the canonical salt$digest form
# (32 hex chars, '$', 64 hex chars).
if ! printf '%s' "$FB_GUI_PASSWORD_HASH" | grep -Eq '^[0-9a-f]{32}\$[0-9a-f]{64}$'; then
echo "ERROR: FB_GUI_PASSWORD_HASH is malformed after sourcing $ENV_FILE." >&2
echo " All values in $ENV_FILE must be single-quoted." >&2
exit 1
fi
# Stop existing systemd-managed pod if present, to avoid conflicts on rerun
echo "Stopping systemd-managed pod 'pod-$POD_NAME.service' if it exists..."
if systemctl --user list-units --type=service --all 2>/dev/null | \
@@ -169,6 +190,24 @@ cd "$USER_SYSTEMD_DIR"
podman generate systemd --name --new --files "$POD_NAME"
echo "Generated systemd service files (rc=$?)"
# The --new units embed the full `podman run` commands, including container
# environment secrets (POSTGRES_PASSWORD, GF_SECURITY_ADMIN_PASSWORD,
# FB_GUI_PASSWORD_HASH, ...). Post-process each generated unit file:
# 1. chmod 600 so only the owner can read the embedded secrets.
# 2. Collapse '$$' -> '$' on Environment= lines. `podman generate systemd`
# escapes every '$' as '$$' (correct for ExecStart command lines, where
# systemd performs variable expansion). But systemd does NOT un-escape
# '$$' inside Environment= *values* - it passes them through literally.
# The GUI password hash has the form salt$digest, so without this fix the
# container receives salt$$digest and GUI login fails. Restrict the
# substitution to Environment= lines so ExecStart escaping stays intact.
for svc in "pod-${POD_NAME}.service" "container-${DB_CTR_NAME}.service" \
"container-${API_CTR_NAME}.service" "container-${GRAFANA_CTR_NAME}.service"; do
[ -f "$svc" ] || continue
chmod 600 "$svc"
sed -i '/^Environment=/ s/\$\$/$/g' "$svc"
done
# Stop & remove live pod and containers
podman pod stop --ignore --time 15 "$POD_NAME"
podman pod rm -f --ignore "$POD_NAME"