mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
- ARTEX_TRUSTED_PROXIES (comma-separated CIDRs, empty by default). When the TCP peer is listed, the login limit reads X-Forwarded-For from the right and keys on the first hop that is not a trusted proxy; otherwise the header is ignored. Without it every client behind a reverse proxy shares one bucket and an attacker can block the admin. Passed through by docker-compose.yml and listed in .env.example. - README: nginx example sends X-Forwarded-For; document the variable; the binary's default listen address is 127.0.0.1:8787. - reset-password.sh refuses passwords under 12 characters, counted in characters like the server.
240 lines
9.9 KiB
Bash
Executable File
240 lines
9.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# ARTEX admin password reset script
|
|
#
|
|
# The login username is fixed as ARTEX; the password is stored as a bcrypt hash in
|
|
# the auth.password_hash key of the settings table. This script uses pgcrypto to
|
|
# generate the bcrypt hash in-database — compatible with the backend (x/crypto/bcrypt).
|
|
#
|
|
# Two deployments:
|
|
# local (default) — host connects to the database directly with psql. Connection
|
|
# info priority: args > --dsn/$ARTEX_PG_DSN > config.json database.*
|
|
# docker — runs psql inside the postgres container via `docker compose
|
|
# exec` (or `docker exec`) (compose does not expose 5432 by default).
|
|
#
|
|
# Usage examples:
|
|
# ./reset-password.sh # local, auto-reads config.json/env, prompts for the new password
|
|
# ./reset-password.sh -p 'New-Passphrase!' # local, new password given directly (at least 12 characters)
|
|
# ./reset-password.sh --dsn postgres://u:p@h:5432/artex
|
|
# ./reset-password.sh -H 127.0.0.1 -P 5433 -U autopentest -W pass -d artex
|
|
# ./reset-password.sh -m docker # docker deployment (reads POSTGRES_* from .env)
|
|
# ./reset-password.sh -m docker -c pg-container-name --exec docker
|
|
#
|
|
# Security: new password via env var + psql \getenv (never in argv), auto-escaped
|
|
# with :'var' (anti-SQL-injection); DB password via PGPASSWORD, also not in argv.
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
PASS_KEY="auth.password_hash"
|
|
BCRYPT_COST=10
|
|
|
|
MODE="" # local | docker (empty = auto-detect)
|
|
DSN=""
|
|
HOST="" PORT="" USER="" DBPASS="" DBNAME="" SSLMODE=""
|
|
CONFIG=""
|
|
CONTAINER="" # postgres service/container name in docker mode (default postgres)
|
|
EXEC_KIND="" # compose | docker (which exec to use in docker mode; empty = auto)
|
|
NEWPASS=""
|
|
ASSUME_YES=0
|
|
|
|
die() { echo "error: $*" >&2; exit 1; }
|
|
info() { echo "· $*" >&2; }
|
|
|
|
usage() { sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }
|
|
|
|
# ---- Argument parsing -------------------------------------------------------
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-m|--mode) MODE="${2:-}"; shift 2 ;;
|
|
--dsn) DSN="${2:-}"; shift 2 ;;
|
|
-H|--host) HOST="${2:-}"; shift 2 ;;
|
|
-P|--port) PORT="${2:-}"; shift 2 ;;
|
|
-U|--user) USER="${2:-}"; shift 2 ;;
|
|
-W|--db-password) DBPASS="${2:-}"; shift 2 ;;
|
|
-d|--dbname) DBNAME="${2:-}"; shift 2 ;;
|
|
--sslmode) SSLMODE="${2:-}"; shift 2 ;;
|
|
--config) CONFIG="${2:-}"; shift 2 ;;
|
|
-c|--container) CONTAINER="${2:-}"; shift 2 ;;
|
|
--exec) EXEC_KIND="${2:-}"; shift 2 ;;
|
|
-p|--new-password) NEWPASS="${2:-}"; shift 2 ;;
|
|
-y|--yes) ASSUME_YES=1; shift ;;
|
|
-h|--help) usage ;;
|
|
*) die "unknown argument: $1 (see -h for usage)" ;;
|
|
esac
|
|
done
|
|
|
|
# ---- Read database.* from config.json (local mode only, and only when no explicit connection is given) -----
|
|
# Prefer python3 for parsing (robust); fall back to grep when python3 is missing (config.json is a flat per-key file).
|
|
read_config_json() {
|
|
local path="$1"
|
|
[[ -f "$path" ]] || return 1
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 - "$path" <<'PY'
|
|
import json, sys
|
|
try:
|
|
d = json.load(open(sys.argv[1])).get("database", {})
|
|
except Exception:
|
|
sys.exit(1)
|
|
# Supports a direct dsn, or individual fields
|
|
if d.get("dsn"):
|
|
print("DSN\t" + d["dsn"]); sys.exit(0)
|
|
for k in ("host","port","user","password","dbname","sslmode"):
|
|
if d.get(k) is not None:
|
|
print(k.upper() + "\t" + str(d[k]))
|
|
PY
|
|
else
|
|
# Minimal fallback: grep key by key (values are strings or numbers)
|
|
local k
|
|
for k in host port user password dbname sslmode; do
|
|
local v
|
|
v=$(grep -oE "\"$k\"[[:space:]]*:[[:space:]]*(\"[^\"]*\"|[0-9]+)" "$path" 2>/dev/null \
|
|
| head -1 | sed -E "s/.*:[[:space:]]*//; s/^\"//; s/\"$//") || true
|
|
[[ -n "$v" ]] && echo -e "${k^^}\t$v"
|
|
done
|
|
fi
|
|
}
|
|
|
|
apply_config_fields() {
|
|
local line key val
|
|
while IFS=$'\t' read -r key val; do
|
|
[[ -z "$key" ]] && continue
|
|
case "$key" in
|
|
DSN) [[ -z "$DSN" ]] && DSN="$val" ;;
|
|
HOST) [[ -z "$HOST" ]] && HOST="$val" ;;
|
|
PORT) [[ -z "$PORT" ]] && PORT="$val" ;;
|
|
USER) [[ -z "$USER" ]] && USER="$val" ;;
|
|
PASSWORD) [[ -z "$DBPASS" ]] && DBPASS="$val" ;;
|
|
DBNAME) [[ -z "$DBNAME" ]] && DBNAME="$val" ;;
|
|
SSLMODE) [[ -z "$SSLMODE" ]] && SSLMODE="$val" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ---- Auto-detect mode ---------------------------------------------------------
|
|
if [[ -z "$MODE" ]]; then
|
|
if [[ -n "$DSN$HOST$USER$DBNAME" || -n "${ARTEX_PG_DSN:-}" || -f "${CONFIG:-config.json}" ]]; then
|
|
MODE="local"
|
|
elif command -v docker >/dev/null 2>&1 && [[ -f docker-compose.yml ]]; then
|
|
MODE="docker"
|
|
else
|
|
MODE="local"
|
|
fi
|
|
fi
|
|
info "deployment mode: $MODE"
|
|
|
|
# ---- Collect the new password -------------------------------------------------
|
|
if [[ -z "$NEWPASS" ]]; then
|
|
read -r -s -p "Enter new password (username is fixed as ARTEX): " NEWPASS; echo >&2
|
|
[[ -n "$NEWPASS" ]] || die "password cannot be empty"
|
|
read -r -s -p "Enter again to confirm: " NEWPASS2; echo >&2
|
|
[[ "$NEWPASS" == "$NEWPASS2" ]] || die "the two entries do not match"
|
|
fi
|
|
[[ -n "$NEWPASS" ]] || die "password cannot be empty"
|
|
# Same minimum as the server (minPasswordRunes in server/auth.go), counted in
|
|
# characters like the server: UTF-8 bytes minus continuation bytes, whatever the locale.
|
|
NEWPASS_CHARS=$(printf '%s' "$NEWPASS" | LC_ALL=C tr -d '\200-\277' | wc -c)
|
|
(( NEWPASS_CHARS >= 12 )) || die "password must be at least 12 characters"
|
|
|
|
# Hand the password to psql via environment variable (read with \getenv, never in argv/ps)
|
|
export ARTEX_RESET_NEWPASS="$NEWPASS"
|
|
|
|
# Generate bcrypt in-database and upsert; the password is auto-escaped with :'newpw'. CREATE EXTENSION is idempotent;
|
|
# if the database role lacks permission to create extensions it errors here (see the hint in the run failure branch below).
|
|
SQL=$(cat <<SQL
|
|
\\set ON_ERROR_STOP on
|
|
\\getenv newpw ARTEX_RESET_NEWPASS
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
INSERT INTO settings(key, value)
|
|
VALUES ('$PASS_KEY', crypt(:'newpw', gen_salt('bf', $BCRYPT_COST)))
|
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
|
|
SQL
|
|
)
|
|
|
|
# ---- Execute ------------------------------------------------------------------
|
|
if [[ "$MODE" == "local" ]]; then
|
|
# Connection info priority: command line > --dsn/$ARTEX_PG_DSN > config.json
|
|
if [[ -z "$DSN" && -z "$HOST$USER$DBNAME" ]]; then
|
|
[[ -n "${ARTEX_PG_DSN:-}" ]] && DSN="$ARTEX_PG_DSN"
|
|
fi
|
|
if [[ -z "$DSN" && -z "$HOST$USER$DBNAME" ]]; then
|
|
cfg="${CONFIG:-config.json}"
|
|
if [[ -f "$cfg" ]]; then
|
|
info "reading database config from $cfg"
|
|
apply_config_fields < <(read_config_json "$cfg")
|
|
fi
|
|
fi
|
|
|
|
command -v psql >/dev/null 2>&1 || die "psql not found on this machine (install postgresql-client, or use -m docker instead)"
|
|
|
|
declare -a PSQL_ARGS=()
|
|
if [[ -n "$DSN" ]]; then
|
|
PSQL_ARGS=("$DSN")
|
|
target="$DSN"
|
|
else
|
|
[[ -n "$USER" ]] || die "missing database user (-U) or a valid config.json/DSN"
|
|
[[ -n "$DBNAME" ]] || die "missing database name (-d) or a valid config.json/DSN"
|
|
HOST="${HOST:-127.0.0.1}"; PORT="${PORT:-5432}"; SSLMODE="${SSLMODE:-disable}"
|
|
PSQL_ARGS=(-h "$HOST" -p "$PORT" -U "$USER" -d "$DBNAME")
|
|
[[ -n "$SSLMODE" ]] && export PGSSLMODE="$SSLMODE"
|
|
[[ -n "$DBPASS" ]] && export PGPASSWORD="$DBPASS"
|
|
target="$USER@$HOST:$PORT/$DBNAME"
|
|
fi
|
|
|
|
info "target database: $target"
|
|
if [[ "$ASSUME_YES" -ne 1 ]]; then
|
|
read -r -p "Reset the ARTEX password in this database? [y/N] " ans
|
|
[[ "$ans" == "y" || "$ans" == "Y" ]] || die "cancelled"
|
|
fi
|
|
|
|
if ! printf '%s\n' "$SQL" | psql "${PSQL_ARGS[@]}" -v ON_ERROR_STOP=1 -q >/dev/null; then
|
|
die "write failed. If it reports a pgcrypto permission/missing error, use a role allowed to create extensions, or run CREATE EXTENSION pgcrypto manually first."
|
|
fi
|
|
|
|
else
|
|
# ---- docker ----
|
|
command -v docker >/dev/null 2>&1 || die "docker not found"
|
|
CONTAINER="${CONTAINER:-postgres}"
|
|
|
|
# Choose the exec method: prefer docker compose exec (service name), otherwise docker exec (container name)
|
|
if [[ -z "$EXEC_KIND" ]]; then
|
|
if docker compose version >/dev/null 2>&1 && [[ -f docker-compose.yml ]]; then
|
|
EXEC_KIND="compose"
|
|
else
|
|
EXEC_KIND="docker"
|
|
fi
|
|
fi
|
|
|
|
# psql credentials inside the container: command line first, then POSTGRES_* from .env, then compose defaults (artex)
|
|
if [[ -f .env ]]; then
|
|
# shellcheck disable=SC1091
|
|
set -a; . ./.env; set +a
|
|
fi
|
|
DUSER="${USER:-${POSTGRES_USER:-artex}}"
|
|
DNAME="${DBNAME:-${POSTGRES_DB:-artex}}"
|
|
[[ -n "$DBPASS" ]] && export PGPASSWORD="$DBPASS"
|
|
[[ -z "${PGPASSWORD:-}" && -n "${POSTGRES_PASSWORD:-}" ]] && export PGPASSWORD="$POSTGRES_PASSWORD"
|
|
|
|
info "target: psql -U $DUSER -d $DNAME inside container $CONTAINER (exec=$EXEC_KIND)"
|
|
if [[ "$ASSUME_YES" -ne 1 ]]; then
|
|
read -r -p "Reset the ARTEX password in this container database? [y/N] " ans
|
|
[[ "$ans" == "y" || "$ans" == "Y" ]] || die "cancelled"
|
|
fi
|
|
|
|
# -e with a name only (no value) → inherited from the current environment, so the password never appears in the docker command argv.
|
|
declare -a EXEC_CMD
|
|
if [[ "$EXEC_KIND" == "compose" ]]; then
|
|
EXEC_CMD=(docker compose exec -T -e ARTEX_RESET_NEWPASS -e PGPASSWORD "$CONTAINER"
|
|
psql -U "$DUSER" -d "$DNAME" -v ON_ERROR_STOP=1 -q)
|
|
else
|
|
EXEC_CMD=(docker exec -i -e ARTEX_RESET_NEWPASS -e PGPASSWORD "$CONTAINER"
|
|
psql -U "$DUSER" -d "$DNAME" -v ON_ERROR_STOP=1 -q)
|
|
fi
|
|
|
|
if ! printf '%s\n' "$SQL" | "${EXEC_CMD[@]}" >/dev/null; then
|
|
die "write failed. Check the container name (-c), the database account (POSTGRES_* in .env), and that the role has pgcrypto permission."
|
|
fi
|
|
fi
|
|
|
|
unset ARTEX_RESET_NEWPASS
|
|
echo "✓ ARTEX admin password has been reset. Log in with username ARTEX + the new password (no service restart needed)."
|