#!/usr/bin/env bash
# ============================================================================
# nodo01-ssh-dev-access.sh
#
# Provision y revocacion de accesos SSH RESTRINGIDOS en NODO-01 (DEV) para
# desarrolladores backend que necesitan el flujo de prueba local con tunel SSH.
#
# La cuenta creada SOLO puede reenviar los 4 puertos de plataforma del nodo
# (eureka 8761, config 8890, oauth2 8787, gateway 8090). No obtiene shell,
# no ejecuta comandos, no tiene sudo ni docker, y no alcanza ningun otro
# puerto del nodo (Oracle, Postgres, Mongo, Kafka, Redis quedan fuera).
#
# Se ejecuta DESDE el PC del arquitecto contra el alias SSH del nodo.
# Requiere: VPN activa + acceso sudo en nodo-01.
#
# Uso:
#   ./nodo01-ssh-dev-access.sh preflight
#   ./nodo01-ssh-dev-access.sh bootstrap                 # crea las 4 cuentas backend SIN llave
#   ./nodo01-ssh-dev-access.sh add <usuario> <ruta-llave-publica> ["Nombre Real"]
#   ./nodo01-ssh-dev-access.sh list
#   ./nodo01-ssh-dev-access.sh show <usuario>
#   ./nodo01-ssh-dev-access.sh revoke <usuario>
#   ./nodo01-ssh-dev-access.sh remove <usuario>
#
# Una cuenta creada con 'bootstrap' existe pero NO tiene acceso: queda sin
# authorized_keys hasta que se instale su llave publica con 'add'.
#
# Requiere terminal interactiva (sudo pide contrasena en el nodo).
#
# Variables de entorno opcionales:
#   NODE=nodo-01   alias SSH del nodo destino (default: nodo-01)
#
# Documentacion: ../../03-simappe-stack/flujos/FLUJO_PRUEBA_LOCAL_TUNEL_SSH.md
# ============================================================================
set -euo pipefail

NODE="${NODE:-nodo-01}"
GROUP="devtunnel"

# Puertos de plataforma que la cuenta puede reenviar. NADA MAS.
PORTS=(8761 8890 8787 8090)

# Cuentas backend autorizadas (alineadas con los logins del ambiente DEV).
# Provisionar una cuenta fuera de esta lista exige --force de forma explicita.
BACKEND_USERS=(liderbackend1 seniorbackend1 juniorbackend1 juniorbackend2)

# Titulares de cada cuenta (para el campo GECOS y la auditoria).
BACKEND_NAMES=(
  "liderbackend1|Vanessa Luna"
  "seniorbackend1|Jhoan Sebastian Saldarriaga Villada"
  "juniorbackend1|Kevin"
  "juniorbackend2|Brahian"
)

# ---------------------------------------------------------------------------
# Utilidades
# ---------------------------------------------------------------------------
die()  { echo "ERROR: $*" >&2; exit 1; }
info() { echo "[i] $*"; }
ok()   { echo "[OK] $*"; }
warn() { echo "[!] $*" >&2; }

usage() {
  sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//'
  exit 1
}

# Ejecuta en el nodo, con sudo y TTY (permite sudo con contrasena).
# El cuerpo del script remoto llega por stdin; los argumentos van en "$@".
#
# OJO: el heredoc que alimenta esta funcion deja stdin en EOF, asi que el ssh
# que ejecuta sudo debe leer del terminal real (/dev/tty) o no se asigna TTY
# y sudo falla con "a terminal is required to read the password".
remote_sudo() {
  local tmp="/tmp/.nodo-ssh-access.$$.$RANDOM.sh"
  ssh "$NODE" "cat > '$tmp' && chmod 700 '$tmp'"
  local qargs=""
  if [ "$#" -gt 0 ]; then qargs=$(printf '%q ' "$@"); fi
  if { : < /dev/tty; } 2>/dev/null; then
    ssh -tt "$NODE" "sudo bash '$tmp' $qargs; rc=\$?; rm -f '$tmp'; exit \$rc" < /dev/tty
  else
    # Sin terminal: 'sudo -n' falla de inmediato en vez de quedarse esperando
    # una contrasena que nadie puede teclear.
    warn "Sin terminal interactiva. Se intenta sudo sin contrasena (-n)."
    warn "Si sudo la exige, ejecuta este script desde una terminal real."
    ssh "$NODE" "sudo -n bash '$tmp' $qargs; rc=\$?; rm -f '$tmp'; exit \$rc" < /dev/null
  fi
}

# Construye la linea restringida de authorized_keys a partir de la llave publica.
build_key_line() {
  local pubkey="$1" opts="restrict,port-forwarding"
  local p
  for p in "${PORTS[@]}"; do
    opts+=",permitopen=\"127.0.0.1:${p}\""
  done
  printf '%s %s\n' "$opts" "$pubkey"
}

is_backend_user() {
  local u="$1" b
  for b in "${BACKEND_USERS[@]}"; do [ "$b" = "$u" ] && return 0; done
  return 1
}

# ---------------------------------------------------------------------------
# preflight — verifica que el nodo admite este modelo de acceso
# ---------------------------------------------------------------------------
cmd_preflight() {
  info "Verificando conectividad con $NODE ..."
  ssh -o ConnectTimeout=10 -o BatchMode=yes "$NODE" true 2>/dev/null \
    || die "No hay SSH a '$NODE'. Verifica la VPN y tu ~/.ssh/config."
  ok "SSH a $NODE responde."

  remote_sudo <<'REMOTE'
#!/usr/bin/env bash
set -uo pipefail
echo "--- Version de OpenSSH (se requiere >= 7.2 para 'restrict') ---"
sshd -V 2>&1 | head -1 || ssh -V 2>&1 | head -1

echo
echo "--- Configuracion efectiva de sshd ---"
CONF=$(sshd -T 2>/dev/null) || { echo "No se pudo leer 'sshd -T'."; exit 1; }
echo "$CONF" | grep -Ei '^(port|pubkeyauthentication|allowtcpforwarding|permitopen|passwordauthentication) ' || true

FWD=$(echo "$CONF" | awk '/^allowtcpforwarding /{print $2}')
case "$FWD" in
  yes|local) echo "[OK] AllowTcpForwarding=$FWD -> el reenvio local (-L) esta permitido." ;;
  *)         echo "[BLOQUEANTE] AllowTcpForwarding=$FWD -> el tunel NO funcionara. Corregir sshd_config." ;;
esac

PUBKEY=$(echo "$CONF" | awk '/^pubkeyauthentication /{print $2}')
[ "$PUBKEY" = "yes" ] || echo "[BLOQUEANTE] PubkeyAuthentication=$PUBKEY -> las llaves no serviran."

echo
echo "--- Listas de control de acceso en sshd (si existen, hay que incluir las cuentas) ---"
ACL=$(echo "$CONF" | grep -Ei '^(allowusers|allowgroups|denyusers|denygroups) ' || true)
if [ -n "$ACL" ]; then
  echo "$ACL"
  echo "[ATENCION] sshd filtra por usuario/grupo. Las cuentas nuevas deben agregarse a esa lista"
  echo "           (o al grupo permitido) o no podran autenticarse."
else
  echo "Sin AllowUsers/AllowGroups/DenyUsers/DenyGroups. No hay filtro adicional."
fi

echo
echo "--- Shell restringida disponible ---"
if [ -x /usr/sbin/nologin ]; then echo "[OK] /usr/sbin/nologin existe."
elif [ -x /sbin/nologin ]; then echo "[OK] /sbin/nologin existe (ajustar SHELL en el script)."
else echo "[BLOQUEANTE] No hay nologin disponible."; fi

echo
echo "--- Grupo de aislamiento ---"
getent group devtunnel >/dev/null && echo "[OK] El grupo 'devtunnel' ya existe." \
                                  || echo "[i] El grupo 'devtunnel' se creara en el primer 'add'."

echo
echo "--- Cuentas ya provisionadas ---"
# devtunnel es el grupo PRIMARIO de estas cuentas: no aparecen en el campo de
# miembros de 'getent group', hay que buscarlas por GID en passwd.
DTGID=$(getent group devtunnel | cut -d: -f3)
if [ -n "$DTGID" ]; then
  getent passwd | awk -F: -v g="$DTGID" '$4==g {print "  " $1 "  (" $5 ")"}'
else
  echo "  (ninguna)"
fi
REMOTE
}

# ---------------------------------------------------------------------------
# bootstrap — crea las cuentas backend SIN llave (quedan sin acceso hasta 'add')
# ---------------------------------------------------------------------------
cmd_bootstrap() {
  echo "Se crearan en $NODE las siguientes cuentas, sin llave y por lo tanto SIN acceso:"
  local e
  for e in "${BACKEND_NAMES[@]}"; do printf '  %-16s %s\n' "${e%%|*}" "${e#*|}"; done
  echo
  echo "Cada cuenta: grupo '$GROUP', shell nologin, sin sudo, sin docker, sin contrasena."
  echo "El acceso se habilita despues, cuando llegue la llave publica, con:"
  echo "  ./nodo01-ssh-dev-access.sh add <usuario> <llave.pub> \"Nombre\""
  echo
  read -r -p "Confirmas? (escribe SI): " answer
  [ "$answer" = "SI" ] || die "Cancelado por el operador."

  remote_sudo "$GROUP" "${BACKEND_NAMES[@]}" <<'REMOTE'
#!/usr/bin/env bash
set -euo pipefail
GROUP_NAME="$1"; shift

SHELL_BIN=/usr/sbin/nologin
[ -x "$SHELL_BIN" ] || SHELL_BIN=/sbin/nologin

getent group "$GROUP_NAME" >/dev/null || { groupadd "$GROUP_NAME"; echo "[+] Grupo $GROUP_NAME creado."; }

for entry in "$@"; do
  USER_NAME="${entry%%|*}"
  COMMENT="${entry#*|}"

  if id "$USER_NAME" >/dev/null 2>&1; then
    echo "[i] $USER_NAME ya existe; no se modifica su llave."
    usermod --shell "$SHELL_BIN" --comment "$COMMENT" "$USER_NAME"
  else
    useradd --create-home --shell "$SHELL_BIN" --gid "$GROUP_NAME" --comment "$COMMENT" "$USER_NAME"
    echo "[+] $USER_NAME creado ($COMMENT)."
  fi

  passwd -l "$USER_NAME" >/dev/null

  for g in sudo docker adm wheel root; do
    if id -nG "$USER_NAME" | tr ' ' '\n' | grep -qx "$g"; then
      gpasswd -d "$USER_NAME" "$g" >/dev/null 2>&1 || true
      echo "[!] $USER_NAME removido del grupo privilegiado '$g'."
    fi
  done

  HOME_DIR=$(getent passwd "$USER_NAME" | cut -d: -f6)
  install -d -m 700 -o "$USER_NAME" -g "$GROUP_NAME" "$HOME_DIR/.ssh"
  chmod 750 "$HOME_DIR"
  if [ -f "$HOME_DIR/.ssh/authorized_keys" ]; then
    echo "    (ya tiene llave instalada: conserva acceso)"
  else
    echo "    sin authorized_keys -> SIN acceso hasta instalar la llave"
  fi
done

echo
echo "--- Cuentas del grupo $GROUP_NAME ---"
# El grupo es PRIMARIO: hay que buscarlas por GID en passwd, no en el campo
# de miembros de 'getent group' (que solo lista miembros secundarios).
GID_G=$(getent group "$GROUP_NAME" | cut -d: -f3)
getent passwd | awk -F: -v g="$GID_G" '$4==g {print "  " $1 "  (" $5 ")"}'
REMOTE

  ok "Cuentas creadas en $NODE. Ninguna tiene acceso hasta instalar su llave publica."
}

# ---------------------------------------------------------------------------
# add — crea/actualiza la cuenta restringida
# ---------------------------------------------------------------------------
cmd_add() {
  local force="no"
  if [ "${1:-}" = "--force" ]; then force="yes"; shift; fi

  local user="${1:-}" keyfile="${2:-}" comment="${3:-}"
  [ -n "$user" ]    || die "Falta el usuario. Uso: add <usuario> <ruta-llave-publica> [\"Nombre Real\"]"
  [ -n "$keyfile" ] || die "Falta la ruta de la llave publica."
  [ -f "$keyfile" ] || die "No existe el archivo de llave: $keyfile"

  # Guardarraíl 1: jamas aceptar una llave PRIVADA por error.
  if grep -qi 'PRIVATE KEY' "$keyfile"; then
    die "'$keyfile' es una llave PRIVADA. Se debe entregar unicamente la publica (.pub). ABORTADO."
  fi

  # Guardarraíl 2: la llave debe ser valida y de una sola linea.
  local nlines
  nlines=$(grep -cv '^[[:space:]]*$' "$keyfile")
  [ "$nlines" -eq 1 ] || die "'$keyfile' contiene $nlines llaves. Se espera exactamente una."
  ssh-keygen -l -f "$keyfile" >/dev/null 2>&1 || die "'$keyfile' no es una llave publica valida."

  local fp type
  fp=$(ssh-keygen -l -f "$keyfile" | awk '{print $2}')
  type=$(ssh-keygen -l -f "$keyfile" | awk '{print $NF}' | tr -d '()')
  if [ "$type" != "ED25519" ]; then
    warn "La llave es de tipo $type. El estandar del proyecto es ED25519."
    [ "$force" = "yes" ] || die "Re-ejecuta con --force si aceptas esta llave de todos modos."
  fi

  # Guardarraíl 3: solo cuentas backend autorizadas.
  if ! is_backend_user "$user"; then
    warn "'$user' no esta en la lista de backend autorizados: ${BACKEND_USERS[*]}"
    [ "$force" = "yes" ] || die "Re-ejecuta con --force si la provision es intencional."
  fi

  local pubkey key_line
  pubkey=$(grep -v '^[[:space:]]*$' "$keyfile" | head -1)
  key_line=$(build_key_line "$pubkey")

  echo
  echo "  Nodo        : $NODE"
  echo "  Cuenta      : $user"
  echo "  Titular     : ${comment:-<sin nombre>}"
  echo "  Huella      : $fp ($type)"
  echo "  Reenvios    : ${PORTS[*]}"
  echo "  Shell       : /usr/sbin/nologin (sin sesion interactiva)"
  echo
  read -r -p "Confirmas la provision? (escribe SI): " answer
  [ "$answer" = "SI" ] || die "Cancelado por el operador."

  remote_sudo "$user" "$GROUP" "$key_line" "${comment:-Acceso tunel DEV}" <<'REMOTE'
#!/usr/bin/env bash
set -euo pipefail
USER_NAME="$1"; GROUP_NAME="$2"; KEY_LINE="$3"; COMMENT="$4"

SHELL_BIN=/usr/sbin/nologin
[ -x "$SHELL_BIN" ] || SHELL_BIN=/sbin/nologin

getent group "$GROUP_NAME" >/dev/null || { groupadd "$GROUP_NAME"; echo "[+] Grupo $GROUP_NAME creado."; }

if id "$USER_NAME" >/dev/null 2>&1; then
  echo "[i] La cuenta $USER_NAME ya existe: se reemplaza su llave autorizada."
  usermod --shell "$SHELL_BIN" "$USER_NAME"
else
  useradd --create-home --shell "$SHELL_BIN" --gid "$GROUP_NAME" --comment "$COMMENT" "$USER_NAME"
  echo "[+] Cuenta $USER_NAME creada (grupo $GROUP_NAME, shell $SHELL_BIN)."
fi

# Sin contrasena: el unico acceso posible es por llave.
passwd -l "$USER_NAME" >/dev/null
echo "[+] Autenticacion por contrasena bloqueada."

# Defensa en profundidad: la cuenta no puede pertenecer a grupos privilegiados.
for g in sudo docker adm wheel root; do
  if id -nG "$USER_NAME" | tr ' ' '\n' | grep -qx "$g"; then
    gpasswd -d "$USER_NAME" "$g" >/dev/null 2>&1 || true
    echo "[!] $USER_NAME removido del grupo privilegiado '$g'."
  fi
done

HOME_DIR=$(getent passwd "$USER_NAME" | cut -d: -f6)
install -d -m 700 -o "$USER_NAME" -g "$GROUP_NAME" "$HOME_DIR/.ssh"
printf '%s\n' "$KEY_LINE" > "$HOME_DIR/.ssh/authorized_keys"
chown "$USER_NAME:$GROUP_NAME" "$HOME_DIR/.ssh/authorized_keys"
chmod 600 "$HOME_DIR/.ssh/authorized_keys"
chmod 750 "$HOME_DIR"
echo "[+] Llave restringida instalada en $HOME_DIR/.ssh/authorized_keys"

echo
echo "--- Verificacion ---"
getent passwd "$USER_NAME"
echo "grupos: $(id -nG "$USER_NAME")"
echo "sudo  : $( (id -nG "$USER_NAME" | grep -qw sudo)  && echo 'SI (REVISAR)' || echo 'no')"
echo "docker: $( (id -nG "$USER_NAME" | grep -qw docker) && echo 'SI (REVISAR)' || echo 'no')"
echo "opciones de la llave:"
sed 's/ ssh-.*//' "$HOME_DIR/.ssh/authorized_keys"
REMOTE

  ok "Cuenta '$user' provisionada en $NODE."
  echo
  echo "Entrega al desarrollador el bloque de ~/.ssh/config (solo alcanzable con VPN activa):"
  echo
  echo "    Host nodo-01"
  echo "        HostName 10.120.0.2"
  echo "        Port 2202"
  echo "        User $user"
  echo "        IdentityFile ~/.ssh/id_ed25519_nebula"
  echo "        IdentitiesOnly yes"
  echo
  echo "Recuerdale que 'ssh nodo-01' NO debe abrir sesion: eso es lo esperado."
}

# ---------------------------------------------------------------------------
# list / show
# ---------------------------------------------------------------------------
cmd_list() {
  remote_sudo "$GROUP" <<'REMOTE'
#!/usr/bin/env bash
set -uo pipefail
GROUP_NAME="$1"
printf '%-18s %-28s %-8s %-8s %s\n' "CUENTA" "TITULAR" "SUDO" "DOCKER" "HUELLA DE LA LLAVE"
getent passwd | awk -F: -v g="$(getent group "$GROUP_NAME" | cut -d: -f3)" '$4==g {print $1"|"$5"|"$6}' \
| while IFS='|' read -r u c h; do
    fp="(sin llave)"
    [ -f "$h/.ssh/authorized_keys" ] && fp=$(ssh-keygen -lf "$h/.ssh/authorized_keys" 2>/dev/null | awk '{print $2}' | head -1)
    s=$( (id -nG "$u" | grep -qw sudo)   && echo SI || echo no)
    d=$( (id -nG "$u" | grep -qw docker) && echo SI || echo no)
    printf '%-18s %-28s %-8s %-8s %s\n' "$u" "${c%%,*}" "$s" "$d" "$fp"
  done
REMOTE
}

cmd_show() {
  local user="${1:-}"; [ -n "$user" ] || die "Falta el usuario. Uso: show <usuario>"
  remote_sudo "$user" <<'REMOTE'
#!/usr/bin/env bash
set -uo pipefail
USER_NAME="$1"
id "$USER_NAME" >/dev/null 2>&1 || { echo "La cuenta $USER_NAME no existe."; exit 1; }
getent passwd "$USER_NAME"
echo "grupos : $(id -nG "$USER_NAME")"
echo "estado : $(passwd -S "$USER_NAME" 2>/dev/null)"
HOME_DIR=$(getent passwd "$USER_NAME" | cut -d: -f6)
echo "llave  :"
if [ -f "$HOME_DIR/.ssh/authorized_keys" ]; then
  ssh-keygen -lf "$HOME_DIR/.ssh/authorized_keys" 2>/dev/null
  echo "opciones:"; sed 's/ ssh-.*//' "$HOME_DIR/.ssh/authorized_keys"
else
  echo "  (sin authorized_keys — acceso revocado o nunca provisionado)"
fi
echo "sesiones activas:"
who | grep -w "$USER_NAME" || echo "  ninguna"
REMOTE
}

# ---------------------------------------------------------------------------
# revoke — quita el acceso conservando la cuenta
# ---------------------------------------------------------------------------
cmd_revoke() {
  local user="${1:-}"; [ -n "$user" ] || die "Falta el usuario. Uso: revoke <usuario>"
  echo "Se revocara el acceso SSH de '$user' en $NODE (la cuenta se conserva, bloqueada)."
  read -r -p "Confirmas? (escribe SI): " answer
  [ "$answer" = "SI" ] || die "Cancelado por el operador."

  remote_sudo "$user" <<'REMOTE'
#!/usr/bin/env bash
set -euo pipefail
USER_NAME="$1"
id "$USER_NAME" >/dev/null 2>&1 || { echo "La cuenta $USER_NAME no existe."; exit 1; }
HOME_DIR=$(getent passwd "$USER_NAME" | cut -d: -f6)
STAMP=$(date +%Y%m%d-%H%M%S)

if [ -f "$HOME_DIR/.ssh/authorized_keys" ]; then
  mv "$HOME_DIR/.ssh/authorized_keys" "$HOME_DIR/.ssh/authorized_keys.revocada-$STAMP"
  chmod 600 "$HOME_DIR/.ssh/authorized_keys.revocada-$STAMP"
  echo "[+] Llave retirada (respaldo: authorized_keys.revocada-$STAMP)."
else
  echo "[i] La cuenta no tenia authorized_keys."
fi

usermod --lock --expiredate 1 "$USER_NAME"
echo "[+] Cuenta bloqueada y expirada."

# Cierra cualquier tunel que el usuario tenga abierto en este momento.
if pkill -u "$USER_NAME" 2>/dev/null; then echo "[+] Sesiones/tuneles activos terminados."; else echo "[i] No habia procesos activos."; fi

echo "[OK] Acceso de $USER_NAME revocado el $STAMP."
REMOTE
}

# ---------------------------------------------------------------------------
# remove — baja definitiva de la cuenta
# ---------------------------------------------------------------------------
cmd_remove() {
  local user="${1:-}"; [ -n "$user" ] || die "Falta el usuario. Uso: remove <usuario>"
  echo "Se ELIMINARA por completo la cuenta '$user' y su home en $NODE. Es irreversible."
  read -r -p "Confirmas? (escribe ELIMINAR): " answer
  [ "$answer" = "ELIMINAR" ] || die "Cancelado por el operador."

  remote_sudo "$user" <<'REMOTE'
#!/usr/bin/env bash
set -euo pipefail
USER_NAME="$1"
id "$USER_NAME" >/dev/null 2>&1 || { echo "La cuenta $USER_NAME no existe."; exit 0; }
pkill -u "$USER_NAME" 2>/dev/null || true
sleep 1
userdel --remove "$USER_NAME"
echo "[OK] Cuenta $USER_NAME eliminada junto con su home."
REMOTE
}

# ---------------------------------------------------------------------------
main() {
  case "${1:-}" in
    preflight) shift; cmd_preflight "$@" ;;
    bootstrap) shift; cmd_bootstrap "$@" ;;
    add)       shift; cmd_add "$@" ;;
    list)      shift; cmd_list "$@" ;;
    show)      shift; cmd_show "$@" ;;
    revoke)    shift; cmd_revoke "$@" ;;
    remove)    shift; cmd_remove "$@" ;;
    *) usage ;;
  esac
}

main "$@"
