Files
artex/reset-password.sh
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

236 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# ARTEX 관리자 비밀번호 재설정 스크립트
#
# 로그인 사용자 이름은 ARTEX 로 고정입니다. 비밀번호는 bcrypt 해시로 데이터베이스 settings 테이블의
# auth.password_hash 키에 저장됩니다. 이 스크립트는 데이터베이스에 접속한 뒤 pgcrypto 로 DB 안에서 bcrypt 해시를 생성해
# 그 키에 다시 씁니다. 백엔드 로그인 검증(golang.org/x/crypto/bcrypt)과 완전히 호환됩니다.
#
# 배포 방식 두 가지:
# local (기본값) -- 호스트에서 psql 로 데이터베이스에 직접 접속합니다. 접속 정보는 다음 우선순위로 가져옵니다:
# 명령행 인자 > --dsn/$ARTEX_PG_DSN > config.json 의 database.*
# docker -- `docker compose exec`(또는 `docker exec`)로 postgres
# 컨테이너 안에서 psql 을 실행합니다(compose 는 기본적으로 5432 를 호스트에 노출하지 않아 컨테이너 안에서 실행합니다).
#
# 사용 예:
# ./reset-password.sh # 로컬, config.json/환경을 자동으로 읽고 새 비밀번호를 대화형으로 입력
# ./reset-password.sh -p 'NewPass!' # 로컬, 새 비밀번호를 직접 지정
# ./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 배포(.env 의 POSTGRES_* 를 읽음)
# ./reset-password.sh -m docker -c pg컨테이너명 --exec docker
#
# 보안: 새 비밀번호는 환경 변수 + psql \getenv 로 전달되며(프로세스 argv 에 들어가지 않습니다), :'var' 로
# 자동 이스케이프됩니다(SQL 인젝션 방지). 데이터베이스 비밀번호는 PGPASSWORD 로 전달되어 마찬가지로 argv 에 들어가지 않습니다.
# =============================================================================
set -euo pipefail
PASS_KEY="auth.password_hash"
BCRYPT_COST=10
MODE="" # local | docker(비우면 자동 판정)
DSN=""
HOST="" PORT="" USER="" DBPASS="" DBNAME="" SSLMODE=""
CONFIG=""
CONTAINER="" # docker 모드의 postgres 서비스/컨테이너명(기본값 postgres)
EXEC_KIND="" # compose | docker(docker 모드에서 어떤 exec 를 쓸지. 비우면 자동)
NEWPASS=""
ASSUME_YES=0
die() { echo "오류: $*" >&2; exit 1; }
info() { echo "· $*" >&2; }
usage() { sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }
# ---- 인자 파싱 -------------------------------------------------------------
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 "알 수 없는 인자입니다: $1(-h 로 사용법 확인)" ;;
esac
done
# ---- config.json 에서 database.* 읽기(local 모드이고 접속 정보를 명시하지 않았을 때만) -----
# python3 로 파싱하는 것을 우선합니다(견고함). python3 가 없으면 grep 으로 대체합니다(config.json 이 정돈된 분리 필드라서).
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)
# dsn 을 직접 주거나 필드를 나눠 줄 수 있습니다
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
# 아주 단순한 후순위: 키마다 grep(값은 문자열 또는 숫자)
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
}
# ---- 모드 자동 판정 ---------------------------------------------------------
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 "배포 모드: $MODE"
# ---- 새 비밀번호 수집 -----------------------------------------------------------
if [[ -z "$NEWPASS" ]]; then
read -r -s -p "새 비밀번호 입력(사용자 이름은 ARTEX 로 고정):" NEWPASS; echo >&2
[[ -n "$NEWPASS" ]] || die "비밀번호는 비워 둘 수 없습니다"
read -r -s -p "확인을 위해 다시 입력:" NEWPASS2; echo >&2
[[ "$NEWPASS" == "$NEWPASS2" ]] || die "두 번 입력한 값이 일치하지 않습니다"
fi
[[ -n "$NEWPASS" ]] || die "비밀번호는 비워 둘 수 없습니다"
# 환경 변수로 비밀번호를 psql 에 전달합니다(\getenv 로 읽으며 argv/ps 에 들어가지 않습니다)
export ARTEX_RESET_NEWPASS="$NEWPASS"
# DB 안에서 bcrypt 를 생성해 upsert 합니다. 비밀번호는 :'newpw' 로 자동 이스케이프됩니다. CREATE EXTENSION 은 멱등이며,
# 데이터베이스 역할에 확장 생성 권한이 없으면 여기서 오류가 납니다(안내는 아래 run 의 실패 분기 참조).
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
)
# ---- 실행 -----------------------------------------------------------------
if [[ "$MODE" == "local" ]]; then
# 접속 정보 우선순위: 명령행 > --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 "$cfg 에서 데이터베이스 설정을 읽습니다"
apply_config_fields < <(read_config_json "$cfg")
fi
fi
command -v psql >/dev/null 2>&1 || die "이 컴퓨터에서 psql 을 찾을 수 없습니다(postgresql-client 를 설치하거나 -m docker 를 사용하세요)"
declare -a PSQL_ARGS=()
if [[ -n "$DSN" ]]; then
PSQL_ARGS=("$DSN")
target="$DSN"
else
[[ -n "$USER" ]] || die "데이터베이스 사용자(-U)가 없거나 유효한 config.json/DSN 이 없습니다"
[[ -n "$DBNAME" ]] || die "데이터베이스 이름(-d)이 없거나 유효한 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"
if [[ "$ASSUME_YES" -ne 1 ]]; then
read -r -p "이 데이터베이스에서 ARTEX 비밀번호를 재설정할까요? [y/N]" ans
[[ "$ans" == "y" || "$ans" == "Y" ]] || die "취소했습니다"
fi
if ! printf '%s\n' "$SQL" | psql "${PSQL_ARGS[@]}" -v ON_ERROR_STOP=1 -q >/dev/null; then
die "쓰기에 실패했습니다. pgcrypto 권한 부족·누락 오류가 나면 확장 생성 권한이 있는 역할을 쓰거나 먼저 CREATE EXTENSION pgcrypto 를 직접 실행하세요."
fi
else
# ---- docker ----
command -v docker >/dev/null 2>&1 || die "docker 를 찾을 수 없습니다"
CONTAINER="${CONTAINER:-postgres}"
# exec 방식 선택: docker compose exec(서비스명)를 우선하고, 아니면 docker exec(컨테이너명)
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 자격 증명: 명령행을 우선, 다음 .env 의 POSTGRES_*, 마지막으로 compose 기본값(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 "대상: 컨테이너 $CONTAINER 안 psql -U $DUSER -d $DNAME(exec=$EXEC_KIND)"
if [[ "$ASSUME_YES" -ne 1 ]]; then
read -r -p "이 컨테이너 데이터베이스에서 ARTEX 비밀번호를 재설정할까요? [y/N]" ans
[[ "$ans" == "y" || "$ans" == "Y" ]] || die "취소했습니다"
fi
# -e 는 이름만 주고 값은 주지 않습니다 → 현재 환경에서 상속하므로 비밀번호가 docker 명령 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 "쓰기에 실패했습니다. 컨테이너명(-c), 데이터베이스 계정(.env 의 POSTGRES_*), 역할의 pgcrypto 권한을 확인하세요."
fi
fi
unset ARTEX_RESET_NEWPASS
echo "✓ ARTEX 관리자 비밀번호를 재설정했습니다. 사용자 이름 ARTEX + 새 비밀번호로 로그인하세요(서비스를 재시작할 필요가 없습니다)."