#!/bin/bash
# bj-onec-admin — вход 1С в базу PostgreSQL bj для проверки поручений в 1С (через root-демон bj-keymedia).
#   access — логин onec_bj с паролем, вход в базу bj только с IP сервера 1С и только по TLS:
#            свои строки в начале pg_hba.conf (# bj-onec), PostgreSQL слушает адрес сервера bj,
#            с которого виден сервер 1С (перезапуск PostgreSQL — только если этот адрес не слушался),
#            порт 5432 в ufw для IP 1С, если ufw включён. BJ_EXTRA: {"password":"…","ips":"10.0.0.5/32,…"}
#   revoke — закрыть вход: NOLOGIN, строки # bj-onec из pg_hba.conf и правила ufw убрать
# Права на объекты onec.* выдаёт сам bj (он их владелец).
set -uo pipefail
q() { python3 -c "import json,sys;print(json.dumps(sys.argv[1],ensure_ascii=False))" "$1"; }
ok() { echo "{\"ok\":true,\"output\":$(q "$1")${2:-}}"; exit 0; }
err() { echo "{\"ok\":false,\"error\":$(q "$1")}"; exit 0; }
x() { python3 -c "import json,os,sys;print(json.loads(os.environ.get('BJ_EXTRA') or '{}').get(sys.argv[1],''))" "$1" 2>/dev/null; }
cd /
psqlq() { runuser -u postgres -- psql -X -q -At -v ON_ERROR_STOP=1 "$@"; }
LOGIN=onec_bj
DB=bj
MARK='# bj-onec'

hba=$(psqlq -d postgres -c 'show hba_file' 2>&1) || err "PostgreSQL недоступен: $hba"
[ -f "$hba" ] || err "не найден pg_hba.conf ($hba)"

# hba_set "строки" — заменить наш блок (пусто — убрать) в начале pg_hba.conf
hba_set() {
  [ -f "$hba.bak-bj-onec" ] || cp -a "$hba" "$hba.bak-bj-onec"
  python3 - "$hba" "$MARK" "$1" <<'PY' || return 1
import sys
p, mark, new = sys.argv[1], sys.argv[2], sys.argv[3]
old = [l for l in open(p).read().split("\n") if not l.rstrip().endswith(mark)]
add = [l for l in new.split("\n") if l.strip()]
open(p, "w").write("\n".join(add + old).rstrip("\n") + "\n")
PY
}

case "${1:-}" in
  access)
    pw=$(x password); ips=$(x ips)
    [[ "$pw" =~ ^[A-Za-z0-9]{20,64}$ ]] || err "пароль: 20–64 латинских букв и цифр"
    list=()
    for ip in ${ips//,/ }; do
      [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/(2[4-9]|3[0-2])$ ]] || err "недопустимый адрес 1С: $ip"
      list+=("$ip")
    done
    [ ${#list[@]} -gt 0 ] || err "не указан IP-адрес сервера 1С"
    [ "$(psqlq -d postgres -c 'show ssl')" = on ] || err "в PostgreSQL выключено шифрование (ssl = off) — 1С подключается только по TLS"
    psqlq -d postgres -c "select 1 from pg_database where datname='$DB'" | grep -q 1 || err "нет базы $DB"
    # логин: только вход, без прав на создание чего-либо; пароль — через переменную psql (не в командной строке)
    printf '%s\n' \
      "DO \$\$BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$LOGIN') THEN CREATE ROLE $LOGIN; END IF; END\$\$;" \
      "ALTER ROLE $LOGIN WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS CONNECTION LIMIT 5 PASSWORD :'pw';" \
      "ALTER ROLE $LOGIN SET statement_timeout = '30s';" \
      "ALTER ROLE $LOGIN SET search_path = onec;" \
      | psqlq -d postgres -v pw="$pw" >/dev/null 2>&1 || err "логин $LOGIN не заведён"
    # в схеме public базы bj создавать объекты может только bj (у PUBLIC право снимаем)
    psqlq -d "$DB" -c "GRANT USAGE, CREATE ON SCHEMA public TO bj" -c "REVOKE CREATE ON SCHEMA public FROM PUBLIC" >/dev/null 2>&1 \
      || err "права на схему public в базе $DB не изменены"
    lines=""
    for ip in "${list[@]}"; do lines+="hostssl $DB $LOGIN $ip md5 $MARK"$'\n'; done
    lines+="host all $LOGIN 0.0.0.0/0 reject $MARK"$'\n'"host all $LOGIN ::/0 reject $MARK"
    hba_set "$lines" || err "не удалось записать $hba"
    # адрес сервера bj, с которого виден сервер 1С
    first=${list[0]%/*}
    src=$(ip -o route get "$first" 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | head -1)
    [ -n "$src" ] || err "нет маршрута до сервера 1С ($first)"
    cur=$(psqlq -d postgres -c 'show listen_addresses')
    restart=no
    case ",${cur// /}," in
      *,\*,*|*,"$src",*|*,0.0.0.0,*) ;;
      *) new="${cur// /},$src"
         printf '%s\n' "ALTER SYSTEM SET listen_addresses = :'v';" | psqlq -d postgres -v v="$new" >/dev/null 2>&1 || err "listen_addresses не изменён"
         restart=yes ;;
    esac
    if [ "$restart" = yes ]; then
      systemctl restart postgresql >/dev/null 2>&1 || err "PostgreSQL не перезапустился (listen_addresses = $new)"
      for i in $(seq 1 30); do psqlq -d postgres -c 'select 1' >/dev/null 2>&1 && break; sleep 1; done
    else
      psqlq -d postgres -c 'select pg_reload_conf()' >/dev/null 2>&1 || err "PostgreSQL не перечитал pg_hba.conf"
    fi
    fw=""
    if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
      for ip in "${list[@]}"; do ufw allow from "$ip" to any port 5432 proto tcp comment bj-onec >/dev/null 2>&1; done
      fw="; порт 5432 открыт в ufw"
    fi
    port=$(psqlq -d postgres -c 'show port')
    note="логин $LOGIN, вход с ${list[*]} по TLS, сервер $src:$port"
    [ "$restart" = yes ] && note+="; PostgreSQL теперь слушает $src (перезапущен)"
    ok "$note$fw" ",\"host\":$(q "$src"),\"port\":$(q "$port")"
    ;;
  revoke)
    psqlq -d postgres -c "DO \$\$BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$LOGIN') THEN ALTER ROLE $LOGIN NOLOGIN; END IF; END\$\$;" >/dev/null 2>&1 \
      || err "вход логином $LOGIN не закрыт"
    # открытые сеансы 1С — завершить
    psqlq -d postgres -c "select pg_terminate_backend(pid) from pg_stat_activity where usename = '$LOGIN'" >/dev/null 2>&1
    hba_set "" || err "не удалось записать $hba"
    psqlq -d postgres -c 'select pg_reload_conf()' >/dev/null 2>&1
    if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
      while n=$(ufw status numbered 2>/dev/null | grep 'bj-onec' | head -1 | sed -n 's/^\[ *\([0-9]*\)\].*/\1/p') && [ -n "$n" ]; do
        ufw --force delete "$n" >/dev/null 2>&1 || break
      done
    fi
    ok "вход логином $LOGIN закрыт"
    ;;
  *) err "команда: access | revoke" ;;
esac
