#!/usr/bin/env bash # Raise libeufin-bank debit thresholds for all accounts (and optionally the default). # # Style: like mytops-admin-log scripts/taler-merchant/maintenance/*.sh # dry-run by default; pass --no-dry to apply. # # Where to run: # - on koopa host (preferred): talks to BANK_URL (default http://127.0.0.1:9012) # - or: podman exec -u root -i taler-hacktivism-bank bash -s < this-script -- --no-dry # # Auth (first match wins): # BANK_ADMIN_PASS # /root/bank-admin-password.txt (inside bank container or host root secrets mirror) # $KOOPA_SECRETS/.../bank-admin-password.txt # ../koopa-admin-secrets/koopa/host-root/taler-bank/bank-admin-password.txt (relative to repo) # # Env / flags: # --no-dry apply changes # --amount GOA:N threshold (default: libeufin ceiling GOA:4503599627370496) # --set-default also set DEFAULT_DEBT_LIMIT in bank-overrides.conf (+ bank restart) # --only USER[,USER] only these usernames # BANK_URL default http://127.0.0.1:9012 # BANK_CTR if set and BANK not reachable on host, use podman exec into container # (default: taler-hacktivism-bank when host curl fails) set -euo pipefail echo "[INFO] raise-debit-limits — libeufin-bank account debit_threshold" echo "[INFO] Dry-run unless --no-dry. Tested against bank.hacktivism.ch (GOA)." DRY_RUN=true SET_DEFAULT=false AMOUNT="${AMOUNT:-GOA:4503599627370496}" ONLY_USERS="" BANK_URL="${BANK_URL:-http://127.0.0.1:9012}" BANK_URL="${BANK_URL%/}" BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}" OVERRIDE_CONF="${OVERRIDE_CONF:-/etc/libeufin/bank-overrides.conf}" usage() { sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' exit 0 } while [[ $# -gt 0 ]]; do case "$1" in --no-dry) DRY_RUN=false; echo "[WARN] Dry-run disabled — will PATCH accounts."; shift ;; --set-default) SET_DEFAULT=true; shift ;; --amount) AMOUNT="${2:?}"; shift 2 ;; --only) ONLY_USERS="${2:?}"; shift 2 ;; -h|--help) usage ;; *) echo "[ERROR] unknown arg: $1" >&2 exit 2 ;; esac done if $DRY_RUN; then echo "[INFO] Dry-run active (no HTTP PATCH, no conf write)." else echo "[WARN] APPLY mode amount=${AMOUNT}" fi # --- admin password --- find_admin_pass() { if [[ -n "${BANK_ADMIN_PASS:-}" ]]; then printf '%s' "$BANK_ADMIN_PASS" return 0 fi local f for f in \ /root/bank-admin-password.txt \ "${KOOPA_SECRETS:-}/koopa/host-root/taler-bank/bank-admin-password.txt" \ "$(cd "$(dirname "$0")/../../../.." 2>/dev/null && pwd)/koopa-admin-secrets/koopa/host-root/taler-bank/bank-admin-password.txt" \ "$HOME/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank/bank-admin-password.txt" do [[ -n "$f" && -f "$f" && -r "$f" ]] || continue tr -d '\n' <"$f" return 0 done # hernani@koopa: secret lives in the bank container if command -v podman >/dev/null 2>&1 \ && podman inspect -f '{{.State.Running}}' "${BANK_CTR:-taler-hacktivism-bank}" 2>/dev/null | grep -qx true; then podman exec "${BANK_CTR:-taler-hacktivism-bank}" cat /root/bank-admin-password.txt 2>/dev/null | tr -d '\n' return 0 fi return 1 } ADMIN_PASS="$(find_admin_pass)" || { echo "[ERROR] no admin password (BANK_ADMIN_PASS or bank-admin-password.txt)" >&2 exit 1 } # Prefer host URL; fall back to podman exec curl inside bank container USE_PODMAN=false if ! curl -sf -m 3 "${BANK_URL}/config" >/dev/null 2>&1; then if command -v podman >/dev/null 2>&1 && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then echo "[INFO] ${BANK_URL} not reachable — using podman exec ${BANK_CTR}" USE_PODMAN=true BANK_URL="http://127.0.0.1:9012" else echo "[ERROR] bank not reachable at ${BANK_URL} and container ${BANK_CTR} not running" >&2 exit 1 fi fi bank_curl() { # bank_curl [curl-args...] URL_PATH_or_absolute # Last arg is URL path starting with / or full URL local args=("$@") local n=$((${#args[@]} - 1)) local url="${args[$n]}" unset "args[$n]" if [[ "$url" != http* ]]; then url="${BANK_URL}${url}" fi if $USE_PODMAN; then podman exec -u root -i "$BANK_CTR" curl -sS -m 30 "${args[@]}" "$url" else curl -sS -m 30 "${args[@]}" "$url" fi } bank_curl_code() { # like bank_curl but print HTTP code on stdout after body to fd3... simpler: write body to file local out="$1"; shift local args=("$@") local n=$((${#args[@]} - 1)) local url="${args[$n]}" unset "args[$n]" if [[ "$url" != http* ]]; then url="${BANK_URL}${url}" fi if $USE_PODMAN; then podman exec -u root -i "$BANK_CTR" curl -sS -m 30 -o /tmp/raise-debt-body -w '%{http_code}' "${args[@]}" "$url" else curl -sS -m 30 -o "$out" -w '%{http_code}' "${args[@]}" "$url" fi } echo "[INFO] bank=${BANK_URL} podman=${USE_PODMAN} amount=${AMOUNT}" TOK_JSON=$(bank_curl -u "admin:${ADMIN_PASS}" -H 'Content-Type: application/json' \ -d '{"scope":"readwrite"}' /accounts/admin/token) TOKEN=$(printf '%s' "$TOK_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])') [[ -n "$TOKEN" ]] || { echo "[ERROR] admin token failed: $TOK_JSON" >&2; exit 1; } echo "[INFO] admin token OK" ACCS_JSON=$(bank_curl -H "Authorization: Bearer ${TOKEN}" "/accounts?limit=500") export ACCS_JSON AMOUNT ONLY_USERS DRY_RUN TOKEN export BANK_URL USE_PODMAN BANK_CTR python3 <<'PY' import json, os, sys, subprocess, urllib.request amount = os.environ["AMOUNT"] only = {u.strip() for u in os.environ.get("ONLY_USERS", "").split(",") if u.strip()} dry = os.environ.get("DRY_RUN", "true").lower() in ("1", "true", "yes") token = os.environ["TOKEN"] bank = os.environ["BANK_URL"].rstrip("/") use_podman = os.environ.get("USE_PODMAN", "false").lower() in ("1", "true", "yes") ctr = os.environ.get("BANK_CTR", "taler-hacktivism-bank") accs = json.loads(os.environ["ACCS_JSON"]).get("accounts") or [] if only: accs = [a for a in accs if a.get("username") in only] print(f"[INFO] accounts to process: {len(accs)}") def http_patch(user: str, body: dict) -> int: data = json.dumps(body).encode() url = f"{bank}/accounts/{user}" if use_podman: # avoid putting token in process list longer than needed — still visible cmd = [ "podman", "exec", "-u", "root", "-i", ctr, "curl", "-sS", "-m", "30", "-o", "/dev/null", "-w", "%{http_code}", "-X", "PATCH", "-H", f"Authorization: Bearer {token}", "-H", "Content-Type: application/json", "-d", json.dumps(body), url.replace(bank, "http://127.0.0.1:9012") if bank.startswith("http") else f"http://127.0.0.1:9012/accounts/{user}", ] # always use in-container localhost for podman path cmd[-1] = f"http://127.0.0.1:9012/accounts/{user}" out = subprocess.check_output(cmd, text=True).strip() return int(out) req = urllib.request.Request( url, data=data, method="PATCH", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) try: with urllib.request.urlopen(req, timeout=30) as r: return r.status except Exception as e: code = getattr(e, "code", None) if code is not None: return int(code) print(f"[ERROR] {user}: {e}", file=sys.stderr) return 0 ok = fail = skip = 0 for a in accs: u = a["username"] old = a.get("debit_threshold") or "?" bal = a.get("balance") or {} if old == amount: print(f"[SKIP] {u}: already {amount}") skip += 1 continue if dry: print(f"[DRY] {u}: {old} -> {amount} (balance={bal})") ok += 1 continue code = http_patch(u, {"debit_threshold": amount}) if code in (200, 204): print(f"[EXECUTE] {u}: {old} -> {amount} HTTP {code}") ok += 1 else: print(f"[FAIL] {u}: {old} -> {amount} HTTP {code}") fail += 1 print(f"[INFO] done ok={ok} skip={skip} fail={fail} dry={dry}") sys.exit(1 if fail else 0) PY # Optional: DEFAULT_DEBT_LIMIT in overrides (new accounts) if $SET_DEFAULT; then echo "[INFO] --set-default: DEFAULT_DEBT_LIMIT in ${OVERRIDE_CONF}" if $USE_PODMAN; then conf_path="$OVERRIDE_CONF" if $DRY_RUN; then echo "[DRY] sed DEFAULT_DEBT_LIMIT = ${AMOUNT} in container:${conf_path}" echo "[DRY] restart libeufin-bank serve" else podman exec -u root "$BANK_CTR" bash -lc " set -e conf='$OVERRIDE_CONF' cp -a \"\$conf\" \"\${conf}.bak-\$(date +%Y%m%d%H%M%S)\" if grep -q '^DEFAULT_DEBT_LIMIT' \"\$conf\"; then sed -i 's/^DEFAULT_DEBT_LIMIT = .*/DEFAULT_DEBT_LIMIT = ${AMOUNT}/' \"\$conf\" else echo 'DEFAULT_DEBT_LIMIT = ${AMOUNT}' >>\"\$conf\" fi grep '^DEFAULT_DEBT_LIMIT' \"\$conf\" # restart bank process (manual stack) pids=\$(ps -eo pid=,args= | awk '/libeufin-bank serve/ && !/awk/ {print \$1}') for p in \$pids; do kill \$p 2>/dev/null || true; done sleep 1 runuser -u libeufin-bank -- nohup /usr/bin/libeufin-bank serve -c /etc/libeufin/libeufin-bank.conf \ >>/var/log/libeufin-bank/serve.log 2>&1 & for i in \$(seq 1 30); do curl -sf -m 2 http://127.0.0.1:9012/config >/dev/null && break sleep 0.5 done curl -sS http://127.0.0.1:9012/config | python3 -c 'import sys,json; d=json.load(sys.stdin); print(\"[INFO] default_debit_threshold\", d.get(\"default_debit_threshold\"))' " echo "[EXECUTE] DEFAULT_DEBT_LIMIT + bank restart" fi else if [[ ! -f "$OVERRIDE_CONF" ]]; then echo "[WARN] ${OVERRIDE_CONF} not on host — skip --set-default (use from inside container or with podman path)" elif $DRY_RUN; then echo "[DRY] sed DEFAULT_DEBT_LIMIT = ${AMOUNT} in ${OVERRIDE_CONF}" else cp -a "$OVERRIDE_CONF" "${OVERRIDE_CONF}.bak-$(date +%Y%m%d%H%M%S)" sed -i "s/^DEFAULT_DEBT_LIMIT = .*/DEFAULT_DEBT_LIMIT = ${AMOUNT}/" "$OVERRIDE_CONF" echo "[EXECUTE] wrote ${OVERRIDE_CONF}" grep '^DEFAULT_DEBT_LIMIT' "$OVERRIDE_CONF" || true echo "[WARN] restart libeufin-bank yourself if conf is mounted from host" fi fi fi echo "[INFO] Done."