#!/bin/sh
# ---------------------------------------------------------------------------
# pam_surepassid-configure-sshd — Configure sshd for SurePassID MFA.
#
# This script ensures:
#   1. KbdInteractiveAuthentication (or ChallengeResponseAuthentication on
#      older OpenSSH) is set to "yes".
#   2. If AuthenticationMethods is explicitly configured, keyboard-interactive
#      is appended to any method that does not already include it.
#
# It is called automatically by the DEB/RPM post-install scripts.
# Administrators may also invoke it manually — for example after installing
# openssh-server on a host where the PAM module was already present.
#
# Usage:
#   sudo pam_surepassid-configure-sshd
#
# Exit codes:
#   0  Success (or sshd not installed — nothing to do).
#   1  Fatal error.
# ---------------------------------------------------------------------------
set -e

SSHD_CONFIG_PATH="/etc/ssh/sshd_config"

# If sshd is not installed, print a notice and exit cleanly.
if [ ! -f "${SSHD_CONFIG_PATH}" ]; then
  echo "-------------------------------------------------------------------"
  echo "NOTICE: sshd is not installed — skipping SSH configuration."
  echo "If you plan to use SurePassID MFA with SSH, install openssh-server"
  echo "and re-run this script:"
  echo "  sudo pam_surepassid-configure-sshd"
  echo "-------------------------------------------------------------------"
  exit 0
fi

# ---------------------------------------------------------------------------
# Detect the correct directive name.
# OpenSSH 8.7+ renamed ChallengeResponseAuthentication to
# KbdInteractiveAuthentication.
# ---------------------------------------------------------------------------
SSHD_DIRECTIVE="KbdInteractiveAuthentication"

detect_openssh_version() {
  local ver=""
  # Debian/Ubuntu: use dpkg
  if command -v dpkg-query >/dev/null 2>&1; then
    ver=$(dpkg-query -W -f='${Version}' openssh-server 2>/dev/null || echo "")
    if [ -n "${ver}" ]; then
      local major_minor
      major_minor=$(echo "${ver}" | sed 's/^[0-9]*://; s/p.*//')
      if dpkg --compare-versions "${major_minor}" lt "8.7" 2>/dev/null; then
        SSHD_DIRECTIVE="ChallengeResponseAuthentication"
      fi
      return
    fi
  fi
  # RHEL/Fedora: use rpm
  if command -v rpm >/dev/null 2>&1; then
    local rhel_major
    rhel_major=$(rpm -E '%{rhel}' 2>/dev/null || echo "")
    if [ -n "${rhel_major}" ] && [ "${rhel_major}" != '%{rhel}' ]; then
      if [ "${rhel_major}" -lt 9 ] 2>/dev/null; then
        SSHD_DIRECTIVE="ChallengeResponseAuthentication"
      fi
    fi
  fi
}

detect_openssh_version

SSHD_DIRECTIVE_LC=$(echo "${SSHD_DIRECTIVE}" | tr '[:upper:]' '[:lower:]')
ORIG_TIMESTAMP="ORIG_$(date '+%Y-%m-%d_%H%M')"

# ---------------------------------------------------------------------------
# Helper: query sshd for the effective (running) value of SSHD_DIRECTIVE.
# ---------------------------------------------------------------------------
sshd_effective_value() {
  sshd -T 2>/dev/null \
    | awk -v key="${SSHD_DIRECTIVE_LC}" 'tolower($1)==key {print $2; exit}'
}

# ---------------------------------------------------------------------------
# Helper: validate config and restart sshd.
# ---------------------------------------------------------------------------
sshd_validate_and_restart() {
  if sshd -t 2>/dev/null; then
    if command -v systemctl >/dev/null 2>&1; then
      # Debian uses "ssh", RHEL uses "sshd".
      if systemctl list-unit-files ssh.service >/dev/null 2>&1; then
        systemctl restart ssh
      else
        systemctl restart sshd
      fi
    fi
  else
    echo "ERROR: sshd config validation failed. sshd NOT restarted."
    echo "Please check your sshd configuration manually."
  fi
}

# ---------------------------------------------------------------------------
# Helper: fix "${SSHD_DIRECTIVE} no" → "yes" in a given file.
# ---------------------------------------------------------------------------
sshd_fix_no_to_yes() {
  local fileArg="$1"
  if grep -qE "^[[:space:]]*${SSHD_DIRECTIVE}[[:space:]]+no[[:space:]]*$" "${fileArg}" 2>/dev/null; then
    cp -p "${fileArg}" "${fileArg}.${ORIG_TIMESTAMP}"
    sed -i -E "s/^([[:space:]]*)${SSHD_DIRECTIVE}[[:space:]]+no[[:space:]]*\$/\1${SSHD_DIRECTIVE} yes/" "${fileArg}"
    echo "Updated '${SSHD_DIRECTIVE}' from 'no' to 'yes' in ${fileArg}"
    echo "  (backup: ${fileArg}.${ORIG_TIMESTAMP})"
  fi
}

# ---------------------------------------------------------------------------
# Collect Include globs from sshd_config.
# ---------------------------------------------------------------------------
INCLUDE_GLOBS=$(grep -E "^[[:space:]]*Include[[:space:]]+" "${SSHD_CONFIG_PATH}" 2>/dev/null \
  | sed -E 's/^[[:space:]]*Include[[:space:]]+//; s/[[:space:]]+$//')

INITIAL_VALUE=$(sshd_effective_value)

# ---------------------------------------------------------------------------
# Step 1: Fix explicit "no" in sshd_config itself.
# ---------------------------------------------------------------------------
sshd_fix_no_to_yes "${SSHD_CONFIG_PATH}"

# ---------------------------------------------------------------------------
# Step 2: Fix explicit "no" in files referenced by Include globs.
# ---------------------------------------------------------------------------
if [ -n "${INCLUDE_GLOBS}" ]; then
  OLD_IFS="${IFS}"
  IFS='
'
  for includeGlob in ${INCLUDE_GLOBS}; do
    IFS="${OLD_IFS}"
    # Intentionally unquoted to resolve the glob.
    for includedFile in ${includeGlob}; do
      if [ -f "${includedFile}" ]; then
        sshd_fix_no_to_yes "${includedFile}"
      fi
    done
    IFS='
'
  done
  IFS="${OLD_IFS}"
fi

# ---------------------------------------------------------------------------
# Step 3: If the effective value is still not "yes", set it explicitly.
# ---------------------------------------------------------------------------
FILE_IDX=""
CURRENT_VALUE=$(sshd_effective_value)
if [ "${CURRENT_VALUE}" != "yes" ]; then
  if [ -n "${INCLUDE_GLOBS}" ] \
     && echo "${INCLUDE_GLOBS}" | grep -qxF '/etc/ssh/sshd_config.d/*.conf'; then
    # Create a drop-in that sorts before existing configs (first-match-wins).
    LOWEST_IDX=$(cd /etc/ssh/sshd_config.d 2>/dev/null \
      && ls -1 *.conf 2>/dev/null \
      | sed 's/^\([0-9]*\).*$/\1/' \
      | sort -n \
      | head -1)
    if [ -z "${LOWEST_IDX}" ]; then
      FILE_IDX="00"
    else
      COMPUTED_IDX=$((LOWEST_IDX - 1))
      if [ "${COMPUTED_IDX}" -lt 0 ]; then
        COMPUTED_IDX=0
      fi
      FILE_IDX=$(printf "%02d" "${COMPUTED_IDX}")
    fi

    DROPIN_PATH="/etc/ssh/sshd_config.d/${FILE_IDX}-pam_surepassid.conf"
    echo "${SSHD_DIRECTIVE} yes" > "${DROPIN_PATH}"
    chown root:root "${DROPIN_PATH}"
    chmod 600 "${DROPIN_PATH}"
    echo "Created ${DROPIN_PATH} with '${SSHD_DIRECTIVE} yes'."
  else
    # No usable Include — append directly to sshd_config.
    cp -p "${SSHD_CONFIG_PATH}" "${SSHD_CONFIG_PATH}.${ORIG_TIMESTAMP}"
    printf '\n%s yes\n' "${SSHD_DIRECTIVE}" >> "${SSHD_CONFIG_PATH}"
    echo "Appended '${SSHD_DIRECTIVE} yes' to ${SSHD_CONFIG_PATH}"
    echo "  (backup: ${SSHD_CONFIG_PATH}.${ORIG_TIMESTAMP})."
  fi
fi

# ---------------------------------------------------------------------------
# Step 4: Validate and restart only if the effective value actually changed.
# ---------------------------------------------------------------------------
FINAL_VALUE=$(sshd_effective_value)
if [ "${FINAL_VALUE}" = "yes" ] && [ "${INITIAL_VALUE}" != "yes" ]; then
  sshd_validate_and_restart
elif [ "${FINAL_VALUE}" != "yes" ]; then
  echo "WARNING: Unable to set '${SSHD_DIRECTIVE}' to 'yes' automatically."
  echo "Please set '${SSHD_DIRECTIVE} yes' manually and restart sshd."
fi

# ---------------------------------------------------------------------------
# Step 5: If AuthenticationMethods is explicitly configured and missing
#          keyboard-interactive, append it to each method.
# ---------------------------------------------------------------------------
AUTH_METHODS_LINE=$(sshd -T 2>/dev/null \
  | awk '/^authenticationmethods / {$1=""; print; exit}' | xargs)

sshd_write_auth_methods() {
  local new_value="$1"
  if [ -n "${INCLUDE_GLOBS}" ] \
     && echo "${INCLUDE_GLOBS}" | grep -qxF '/etc/ssh/sshd_config.d/*.conf'; then
    DROPIN_PATH_AM="/etc/ssh/sshd_config.d/${FILE_IDX:-00}-pam_surepassid.conf"
    if [ -f "${DROPIN_PATH_AM}" ]; then
      # Remove any existing AuthenticationMethods line before rewriting.
      sed -i '/^AuthenticationMethods/d' "${DROPIN_PATH_AM}"
    else
      : > "${DROPIN_PATH_AM}"
      chown root:root "${DROPIN_PATH_AM}"
      chmod 600 "${DROPIN_PATH_AM}"
    fi
    printf 'AuthenticationMethods %s\n' "${new_value}" >> "${DROPIN_PATH_AM}"
  else
    if grep -qE "^[[:space:]]*AuthenticationMethods" "${SSHD_CONFIG_PATH}" 2>/dev/null; then
      cp -p "${SSHD_CONFIG_PATH}" "${SSHD_CONFIG_PATH}.${ORIG_TIMESTAMP}_authmethods"
      sed -i -E "s|^([[:space:]]*)AuthenticationMethods.*|\1AuthenticationMethods ${new_value}|" "${SSHD_CONFIG_PATH}"
    else
      cp -p "${SSHD_CONFIG_PATH}" "${SSHD_CONFIG_PATH}.${ORIG_TIMESTAMP}_authmethods"
      printf '\nAuthenticationMethods %s\n' "${new_value}" >> "${SSHD_CONFIG_PATH}"
    fi
  fi
}

if [ "${AUTH_METHODS_LINE}" = "any" ] || [ -z "${AUTH_METHODS_LINE}" ]; then
  # Default (not explicitly set) — leave it alone.
  # KbdInteractiveAuthentication=yes + PAM config is sufficient.
  :
elif echo "${AUTH_METHODS_LINE}" | grep -qv "keyboard-interactive"; then
  # Explicitly set but missing keyboard-interactive — append to each method.
  NEW_METHODS=""
  for method in ${AUTH_METHODS_LINE}; do
    if echo "${method}" | grep -q "keyboard-interactive"; then
      NEW_METHODS="${NEW_METHODS} ${method}"
    else
      NEW_METHODS="${NEW_METHODS} ${method},keyboard-interactive"
    fi
  done
  NEW_METHODS=$(echo "${NEW_METHODS}" | xargs)
  sshd_write_auth_methods "${NEW_METHODS}"
  echo "Updated 'AuthenticationMethods' to '${NEW_METHODS}' for MFA enforcement."
  sshd_validate_and_restart
fi

echo "pam_surepassid-configure-sshd: done."
