#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# pam_surepassid-configure — day-2 configuration CLI for SurePassID PAM.
#
# Provides idempotent, scriptable commands for placing the
# pam_surepassid.conf file, wiring/unwiring PAM services, querying
# status, and testing connectivity.
#
# Usage:
#   pam_surepassid-configure set-conf <path>
#   pam_surepassid-configure add-service <service> [service...]
#   pam_surepassid-configure remove-service <service> [service...]
#   pam_surepassid-configure status
#   pam_surepassid-configure test
#
# Requires root (sudo) for all mutating operations.
# The script acquires sudo credentials internally via ensure_sudo
# and prefixes privileged operations with sudo.
#
# Exit codes:
#   0  Success
#   1  Fatal error / invalid arguments
#   2  Test failed (connectivity unreachable)
# ---------------------------------------------------------------------------
set -e

PROG="pam_surepassid-configure"
VERSION_FILE="/usr/share/doc/pam_surepassid/VERSION"

# Paths — overridable via env vars for testing.
ETC_PAM_D="${PAM_SUREPASSID_ETC_PAM_D:-/etc/pam.d}"
ETC_SUREPASSID_D="${PAM_SUREPASSID_ETC_SUREPASSID_D:-/etc/surepassid.d}"
PAM_SUREPASSID_CONF="${ETC_SUREPASSID_D}/pam_surepassid.conf"

# ---------------------------------------------------------------------------
# OS detection — minimal, self-contained.
# ---------------------------------------------------------------------------
detect_os() {
  if [ ! -f /etc/os-release ]; then
    echo "ERROR: /etc/os-release not found. Cannot detect OS." >&2
    exit 1
  fi
  # shellcheck disable=SC1091
  . /etc/os-release
  OS_ID="${ID}"
}

# ---------------------------------------------------------------------------
# PAM config patterns — distro-aware.
# ---------------------------------------------------------------------------
set_pam_patterns() {
  case "${OS_ID}" in
    rhel|centos|rocky|fedora|ol)
      PAM_CONFIG_LINE="auth       required     pam_surepassid.so"
      PAM_CONFIG_LINE_REGX="^auth[[:space:]]*required[[:space:]]*pam_surepassid.so"
      PAM_CONFIG_INSTALL_AFTER_REGX="^auth[[:space:]]"
      ;;
    debian|ubuntu)
      PAM_CONFIG_LINE="@include surepassid"
      PAM_CONFIG_LINE_REGX="^@include[[:space:]]*surepassid$"
      PAM_CONFIG_INSTALL_AFTER_REGX="^@include[[:space:]]*common-auth$"
      ;;
    *)
      echo "ERROR: Unsupported OS: ${OS_ID}" >&2
      exit 1
      ;;
  esac
}

# ---------------------------------------------------------------------------
# .so path — distro-aware.
# ---------------------------------------------------------------------------
get_so_path() {
  case "${OS_ID}" in
    rhel|centos|rocky|fedora|ol)
      echo "/lib64/security/pam_surepassid.so"
      ;;
    debian|ubuntu)
      local multiarch=""
      if command -v dpkg-architecture >/dev/null 2>&1; then
        multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null || true)"
      fi
      if [ -z "${multiarch}" ] && command -v dpkg >/dev/null 2>&1; then
        case "$(dpkg --print-architecture)" in
          amd64) multiarch="x86_64-linux-gnu" ;;
          arm64) multiarch="aarch64-linux-gnu" ;;
          armhf) multiarch="arm-linux-gnueabihf" ;;
          i386)  multiarch="i386-linux-gnu" ;;
          *)     multiarch="unknown" ;;
        esac
      fi
      echo "/lib/${multiarch}/security/pam_surepassid.so"
      ;;
    *)
      echo "/lib/security/pam_surepassid.so"
      ;;
  esac
}

# ---------------------------------------------------------------------------
# Service name validation.
# Rejects empty names, names with / or ., names starting with common-
# (Debian shared snippets), and services without a pam.d file.
# ---------------------------------------------------------------------------
validate_service() {
  local service="${1:-}"

  if [ -z "${service}" ]; then
    echo "ERROR: Service name must not be empty." >&2
    return 1
  fi

  case "${service}" in
    */*)
      echo "ERROR: Service name must not contain '/'." >&2
      return 1
      ;;
    *.*)
      echo "ERROR: Service name must not contain '.'." >&2
      return 1
      ;;
    common-*)
      echo "ERROR: Cannot modify Debian shared snippet '${service}'." >&2
      return 1
      ;;
  esac

  if [ ! -f "${ETC_PAM_D}/${service}" ]; then
    echo "ERROR: ${ETC_PAM_D}/${service} does not exist." >&2
    return 1
  fi

  return 0
}

# ---------------------------------------------------------------------------
# ensure_sudo — acquire and cache sudo credentials for privileged ops.
#
# When PAM_SUREPASSID_SKIP_ROOT_CHECK=1 (testing), this is a no-op and
# SUDO_CMD is set to "" so privileged commands run unprivileged against
# user-writable test directories.
# ---------------------------------------------------------------------------
SUDO_CMD="sudo"

ensure_sudo() {
  if [ "${PAM_SUREPASSID_SKIP_ROOT_CHECK:-0}" = "1" ]; then
    SUDO_CMD=""
    return 0
  fi
  # Already root — no sudo needed; avoids PAM issues in containers.
  if [ "$(id -u)" -eq 0 ]; then
    SUDO_CMD=""
    return 0
  fi
  if ! command -v sudo >/dev/null 2>&1; then
    echo "ERROR: sudo is not installed. Please install sudo or run as root." >&2
    exit 1
  fi
  echo "Requesting sudo privileges (you may be prompted for your password) ..."
  if ! sudo -v; then
    echo "ERROR: sudo authentication failed." >&2
    exit 1
  fi
  # Keep cached credentials alive until the script exits.
  ( while true; do
      sudo -n true 2>/dev/null || exit 0
      sleep 60
      kill -0 "$$" 2>/dev/null || exit 0
    done ) &
}

# ---------------------------------------------------------------------------
# cmd_set_conf — install a pam_surepassid.conf file.
# ---------------------------------------------------------------------------
cmd_set_conf() {
  local conf_path="${1:-}"

  if [ -z "${conf_path}" ]; then
    echo "Usage: ${PROG} set-conf <path/to/pam_surepassid.conf>" >&2
    exit 1
  fi

  if [ ! -f "${conf_path}" ]; then
    echo "ERROR: File not found: ${conf_path}" >&2
    exit 1
  fi

  if [ ! -r "${conf_path}" ]; then
    echo "ERROR: File not readable: ${conf_path}" >&2
    exit 1
  fi

  ensure_sudo

  # Ensure target directory exists with correct permissions.
  if ! ${SUDO_CMD} test -d "${ETC_SUREPASSID_D}"; then
    ${SUDO_CMD} mkdir -p "${ETC_SUREPASSID_D}"
  fi
  if [ "${PAM_SUREPASSID_SKIP_ROOT_CHECK:-0}" != "1" ]; then
    ${SUDO_CMD} chown root:root "${ETC_SUREPASSID_D}"
    ${SUDO_CMD} chmod 0750 "${ETC_SUREPASSID_D}"
  fi

  # Backup existing conf if present.
  if ${SUDO_CMD} test -f "${PAM_SUREPASSID_CONF}"; then
    local backup_path
    backup_path="${PAM_SUREPASSID_CONF}.bak.$(date '+%Y%m%d_%H%M%S')"
    ${SUDO_CMD} cp -p "${PAM_SUREPASSID_CONF}" "${backup_path}"
    echo "Backed up existing config to ${backup_path}"
  fi

  ${SUDO_CMD} cp -f "${conf_path}" "${PAM_SUREPASSID_CONF}"
  if [ "${PAM_SUREPASSID_SKIP_ROOT_CHECK:-0}" != "1" ]; then
    ${SUDO_CMD} chown root:root "${PAM_SUREPASSID_CONF}"
    ${SUDO_CMD} chmod 0640 "${PAM_SUREPASSID_CONF}"
  fi

  echo "Installed ${conf_path} -> ${PAM_SUREPASSID_CONF}"
}

# ---------------------------------------------------------------------------
# preflight_check — verify .so and .conf exist before wiring a service.
# Skipped when FORCE=1 (--force).
# ---------------------------------------------------------------------------
preflight_check() {
  if [ "${FORCE}" -eq 1 ]; then
    return 0
  fi

  local so_path
  so_path="$(get_so_path)"
  local missing=0

  if [ ! -f "${so_path}" ]; then
    echo "" >&2
    echo "ERROR: pam_surepassid.so is not installed at ${so_path}." >&2
    case "${OS_ID}" in
      rhel|centos|rocky|fedora|ol)
        echo "       Install the package first:  sudo dnf install pam_surepassid" >&2
        ;;
      debian|ubuntu)
        echo "       Install the package first:  sudo apt install libpam-surepassid" >&2
        ;;
    esac
    missing=$((missing + 1))
  fi

  if ! ${SUDO_CMD} test -f "${PAM_SUREPASSID_CONF}"; then
    echo "" >&2
    echo "ERROR: ${PAM_SUREPASSID_CONF} is not present." >&2
    echo "       Place the config first:  ${PROG} set-conf <path>" >&2
    missing=$((missing + 1))
  fi

  if [ "${missing}" -gt 0 ]; then
    echo "" >&2
    echo "Wiring a PAM service without these prerequisites will break" >&2
    echo "authentication. Use --force to override if you know what you" >&2
    echo "are doing." >&2
    return 1
  fi

  return 0
}

# ---------------------------------------------------------------------------
# cmd_add_service — wire one or more PAM services for SurePassID MFA.
# ---------------------------------------------------------------------------
_add_one_service() {
  local service="${1}"
  local pam_file="${ETC_PAM_D}/${service}"

  # Idempotent: already wired.
  if grep -qE "${PAM_CONFIG_LINE_REGX}" "${pam_file}" 2>/dev/null; then
    echo "${pam_file} is already configured for SurePassID MFA."
    return 0
  fi

  # Find the insertion point (after the last line matching the anchor regex).
  local insert_after
  insert_after=$(grep -nE "${PAM_CONFIG_INSTALL_AFTER_REGX}" "${pam_file}" \
    | tail -1 | cut -d: -f1)

  if [ -z "${insert_after}" ]; then
    echo "ERROR: No line matching '${PAM_CONFIG_INSTALL_AFTER_REGX}' in ${pam_file}." >&2
    echo "       Cannot determine where to insert pam_surepassid." >&2
    return 1
  fi

  # Backup the original file.
  ${SUDO_CMD} cp -p "${pam_file}" "${pam_file}.bak.$(date '+%Y%m%d_%H%M%S')"

  # Insert PAM config line after the anchor.
  ${SUDO_CMD} sed -i "${insert_after}a\\${PAM_CONFIG_LINE}" "${pam_file}"

  echo "Added SurePassID MFA to ${pam_file} (after line ${insert_after})."

  # RHEL sudo fixup: replace "auth include system-auth" with
  # "auth substack system-auth" to prevent short-circuiting.
  if [ "${service}" = "sudo" ]; then
    case "${OS_ID}" in
      rhel|centos|rocky|fedora|ol)
        if grep -q "^auth[[:space:]]*include[[:space:]]*system-auth" "${pam_file}"; then
          ${SUDO_CMD} sed -i 's/^auth[[:space:]]*include[[:space:]]*system-auth/auth       substack     system-auth/' "${pam_file}"
          echo "Fixed sudo: changed 'auth include system-auth' to 'auth substack system-auth'."
        fi
        ;;
    esac
  fi
}

cmd_add_service() {
  if [ $# -eq 0 ]; then
    echo "Usage: ${PROG} add-service [--force] <service> [service...]" >&2
    exit 1
  fi

  ensure_sudo

  # Pre-flight check (unless --force).
  if ! preflight_check; then
    exit 1
  fi

  local failures=0
  local service
  for service in "$@"; do
    if ! validate_service "${service}"; then
      failures=$((failures + 1))
      continue
    fi
    if ! _add_one_service "${service}"; then
      failures=$((failures + 1))
    fi
  done

  if [ "${failures}" -gt 0 ]; then
    exit 1
  fi
}

# ---------------------------------------------------------------------------
# cmd_remove_service — unwire one or more PAM services from SurePassID MFA.
# ---------------------------------------------------------------------------
_remove_one_service() {
  local service="${1}"
  local pam_file="${ETC_PAM_D}/${service}"

  # Idempotent: not wired.
  if ! grep -qE "${PAM_CONFIG_LINE_REGX}" "${pam_file}" 2>/dev/null; then
    echo "${pam_file} does not have SurePassID MFA configured."
    return 0
  fi

  # Backup the original file.
  ${SUDO_CMD} cp -p "${pam_file}" "${pam_file}.bak.$(date '+%Y%m%d_%H%M%S')"

  # Remove the pam_surepassid line(s).
  ${SUDO_CMD} sed -i "/${PAM_CONFIG_LINE_REGX}/d" "${pam_file}"

  echo "Removed SurePassID MFA from ${pam_file}."
}

cmd_remove_service() {
  if [ $# -eq 0 ]; then
    echo "Usage: ${PROG} remove-service <service> [service...]" >&2
    exit 1
  fi

  ensure_sudo

  local failures=0
  local service
  for service in "$@"; do
    if ! validate_service "${service}"; then
      failures=$((failures + 1))
      continue
    fi
    if ! _remove_one_service "${service}"; then
      failures=$((failures + 1))
    fi
  done

  if [ "${failures}" -gt 0 ]; then
    exit 1
  fi
}

# ---------------------------------------------------------------------------
# cmd_status — display current SurePassID PAM configuration.
# ---------------------------------------------------------------------------
cmd_status() {
  ensure_sudo

  echo "=== SurePassID PAM Module Status ==="
  echo ""

  # Module version.
  local version="unknown"
  if [ -f "${VERSION_FILE}" ]; then
    version="$(cat "${VERSION_FILE}")"
  else
    # Try package manager.
    case "${OS_ID}" in
      rhel|centos|rocky|fedora|ol)
        if command -v rpm >/dev/null 2>&1; then
          version="$(rpm -q --qf '%{VERSION}-%{RELEASE}' pam_surepassid 2>/dev/null || echo "not installed")"
        fi
        ;;
      debian|ubuntu)
        if command -v dpkg-query >/dev/null 2>&1; then
          version="$(dpkg-query -W -f='${Version}' libpam-surepassid 2>/dev/null || echo "not installed")"
        fi
        ;;
    esac
  fi
  echo "Version     : ${version}"

  # .so path and existence.
  local so_path
  so_path="$(get_so_path)"
  local so_installed=0
  if [ -f "${so_path}" ]; then
    echo "Module      : ${so_path} (installed)"
    so_installed=1
  else
    echo "Module      : ${so_path} (NOT FOUND)"
  fi

  # Configuration file.
  local conf_present=0
  if ${SUDO_CMD} test -f "${PAM_SUREPASSID_CONF}"; then
    echo "Config      : ${PAM_SUREPASSID_CONF} (present)"
    conf_present=1
  else
    echo "Config      : ${PAM_SUREPASSID_CONF} (not found)"
  fi

  # Configured services.
  echo ""
  echo "Configured PAM services:"
  local found_any=0
  local wired_services=""
  if [ -d "${ETC_PAM_D}" ]; then
    for pam_file in "${ETC_PAM_D}"/*; do
      # Skip backup files created by add-service / remove-service.
      case "$(basename "${pam_file}")" in
        *.bak.*) continue ;;
      esac
      if [ -f "${pam_file}" ] && grep -qE "${PAM_CONFIG_LINE_REGX}" "${pam_file}" 2>/dev/null; then
        local svc_name
        svc_name="$(basename "${pam_file}")"
        echo "  - ${svc_name}"
        wired_services="${wired_services} ${svc_name}"
        found_any=$((found_any + 1))
      fi
    done
  fi
  if [ "${found_any}" -eq 0 ]; then
    echo "  (none)"
  fi

  # SurePassID host from conf.
  echo ""
  if ${SUDO_CMD} test -f "${PAM_SUREPASSID_CONF}"; then
    local host
    host=$(${SUDO_CMD} grep -E '^[[:space:]]*host[[:space:]]*=' "${PAM_SUREPASSID_CONF}" \
      | head -1 \
      | sed -E 's/^[[:space:]]*host[[:space:]]*=[[:space:]]*//' \
      | tr -d '[:space:]')
    if [ -n "${host}" ]; then
      echo "SurePassID host: ${host}"
    else
      echo "SurePassID host: (not configured in ${PAM_SUREPASSID_CONF})"
    fi
  fi

  # Health warnings — only when services are wired.
  if [ "${found_any}" -gt 0 ]; then
    if [ "${so_installed}" -eq 0 ]; then
      echo ""
      echo "WARNING: ${found_any} service(s) wired for SurePassID MFA but"
      echo "pam_surepassid.so is NOT installed at ${so_path}."
      echo "Authentication WILL FAIL for:${wired_services}"
      echo "Either install the module or run:"
      echo "  ${PROG} remove-service${wired_services}"
    fi
    if [ "${conf_present}" -eq 0 ]; then
      echo ""
      echo "WARNING: ${found_any} service(s) wired for SurePassID MFA but"
      echo "${PAM_SUREPASSID_CONF} is not present."
      echo "The module cannot reach the SurePassID server without it."
      echo "Either place the config file or run:"
      echo "  ${PROG} remove-service${wired_services}"
    fi
  fi
}

# ---------------------------------------------------------------------------
# cmd_test — test connectivity to the configured SurePassID server.
# ---------------------------------------------------------------------------
cmd_test() {
  ensure_sudo

  if ! ${SUDO_CMD} test -f "${PAM_SUREPASSID_CONF}"; then
    echo "ERROR: ${PAM_SUREPASSID_CONF} not found." >&2
    echo "Run '${PROG} set-conf <path>' first." >&2
    exit 1
  fi

  local host
  host=$(${SUDO_CMD} grep -E '^[[:space:]]*host[[:space:]]*=' "${PAM_SUREPASSID_CONF}" \
    | head -1 \
    | sed -E 's/^[[:space:]]*host[[:space:]]*=[[:space:]]*//' \
    | tr -d '[:space:]')

  if [ -z "${host}" ]; then
    echo "ERROR: No 'host=' directive found in ${PAM_SUREPASSID_CONF}." >&2
    exit 1
  fi

  echo "Testing connectivity to SurePassID server: ${host}"
  echo ""

  # DNS resolution.
  echo -n "  DNS resolution ... "
  if command -v getent >/dev/null 2>&1; then
    if getent hosts "${host}" >/dev/null 2>&1; then
      local resolved
      resolved="$(getent hosts "${host}" | head -1 | awk '{print $1}')"
      echo "OK (${resolved})"
    else
      echo "FAILED"
      echo ""
      echo "Cannot resolve '${host}'. Check DNS configuration." >&2
      exit 2
    fi
  elif command -v host >/dev/null 2>&1; then
    if host "${host}" >/dev/null 2>&1; then
      echo "OK"
    else
      echo "FAILED"
      exit 2
    fi
  else
    echo "SKIPPED (no getent or host command)"
  fi

  # HTTPS connectivity.
  echo -n "  HTTPS connectivity ... "
  if command -v curl >/dev/null 2>&1; then
    if curl -fsS --connect-timeout 5 --max-time 10 \
         --head "https://${host}/" -o /dev/null 2>/dev/null; then
      echo "OK"
    else
      echo "FAILED"
      echo ""
      echo "Cannot reach https://${host}/. Check firewall/proxy settings." >&2
      exit 2
    fi
  elif command -v wget >/dev/null 2>&1; then
    if wget --quiet --spider --tries=1 --timeout=10 "https://${host}/" 2>/dev/null; then
      echo "OK"
    else
      echo "FAILED"
      echo ""
      echo "Cannot reach https://${host}/. Check firewall/proxy settings." >&2
      exit 2
    fi
  else
    echo "SKIPPED (no curl or wget)"
  fi

  # TLS certificate check.
  echo -n "  TLS certificate ... "
  if command -v openssl >/dev/null 2>&1; then
    local tls_output
    tls_output="$(echo "" | openssl s_client -connect "${host}:443" -servername "${host}" 2>/dev/null)"
    if echo "${tls_output}" | grep -q "Verify return code: 0"; then
      echo "OK (verified)"
    elif echo "${tls_output}" | grep -q "BEGIN CERTIFICATE"; then
      local verify_code
      verify_code="$(echo "${tls_output}" | grep "Verify return code:" | sed 's/.*Verify return code: //')"
      echo "WARNING (${verify_code})"
    else
      echo "FAILED"
    fi
  else
    echo "SKIPPED (no openssl)"
  fi

  echo ""
  echo "Connectivity test complete."
}

# ---------------------------------------------------------------------------
# usage
# ---------------------------------------------------------------------------
usage() {
  cat <<EOF
Usage: ${PROG} <command> [arguments]

Commands:
  set-conf <path>              Install a pam_surepassid.conf file
  add-service [--force] <svc...>
                               Wire one or more PAM services for SurePassID MFA
  remove-service <svc...>      Unwire one or more PAM services from SurePassID MFA
  status                       Show current configuration and health warnings
  test                         Test connectivity to SurePassID server

Options:
  --force   (add-service only) Skip the pre-flight check that verifies
            pam_surepassid.so and pam_surepassid.conf are present.
            Use when the caller guarantees ordering (e.g. the install
            script calls set-conf before add-service in the same run).

Examples:
  ${PROG} set-conf /path/to/pam_surepassid.conf
  ${PROG} add-service sshd sudo
  ${PROG} add-service --force sshd
  ${PROG} remove-service sshd sudo
  ${PROG} status
  ${PROG} test
EOF
}

# ---------------------------------------------------------------------------
# Main dispatch.
# ---------------------------------------------------------------------------
detect_os
set_pam_patterns

FORCE=0
COMMAND="${1:-}"

case "${COMMAND}" in
  set-conf)
    shift
    cmd_set_conf "$@"
    ;;
  add-service)
    shift
    # Parse --force before passing service names.
    if [ "${1:-}" = "--force" ]; then
      FORCE=1
      shift
    fi
    cmd_add_service "$@"
    ;;
  remove-service)
    shift
    cmd_remove_service "$@"
    ;;
  status)
    cmd_status
    ;;
  test)
    cmd_test
    ;;
  --help|-h|help)
    usage
    ;;
  "")
    usage
    exit 1
    ;;
  *)
    echo "ERROR: Unknown command '${COMMAND}'." >&2
    echo ""
    usage
    exit 1
    ;;
esac
