#!/usr/bin/env bash
# Copyright 2023 - Marco Trevisan
# Released under the GPLv3 terms
#
# A simple tool to simulate PAM authentication using GDM smartcard settings.
#
# This can also be used in interactive mode to test GDM login from the UI, by
# setting the environment variable `WAIT`, so that the script will restart the
# gdm service and will allow the user to login it using its name and a provided
# `PIN` (123456 by default).
#
# To be used with https://gist.github.com/3v1n0/287d02ca8e03936f1c7bba992173d47a
set -xe

required_tools=(
    gdm3           # debian package: gdm3
    pamtester      # debian package: pamtester
    softhsm2-util  # debian package: softhsm2
    sssd           # debian package: sssd
)

if [[ ! -v OFFLINE_MODE ]]; then
  required_tools+=(
    wget  # debian package: wget
  )
fi

if [ "$(id -u)" != 0 ] || [ -z "$SUDO_USER" ]; then
  echo "This tool requires sudo!"
  exit 2
fi

for cmd in "${required_tools[@]}"; do
  if ! command -v "$cmd" > /dev/null; then
    echo "Tool $cmd missing"
    exit 1
  fi
done

if [[ -v WAIT ]]; then
  PIN=${PIN:-123456}
fi

PIN=${PIN:-$RANDOM}
GDM_USER=${GDM_USER:-gdm}
tmpdir=${TEST_TMPDIR:-$(mktemp -d -t "sssd-softhsm2-gdm-certs-XXXXXX")}
backupsdir=

alternative_pam_configs=(
  /etc/pam.d/gdm-smartcard-sssd-exclusive
  /etc/pam.d/gdm-smartcard-sssd-or-password
)

declare -a restore_paths
declare -a delete_paths

function restore_changes() {
  for path in "${restore_paths[@]}"; do
    local original_path
    original_path="/$(realpath --strip --relative-base="$backupsdir" "$path")"
    rm "$original_path" && mv "$path" "$original_path" || true
  done

  for path in "${delete_paths[@]}"; do
    rm -f "$path"
  done

  update-alternatives --auto gdm-smartcard

  if [ -e /etc/sssd/sssd.conf ]; then
    systemctl restart sssd || true
  else
    systemctl stop sssd || true
  fi

  rm -rf "$tmpdir"
}

function backup_file() {
  if [ -z "$backupsdir" ]; then
    backupsdir=$(mktemp -d -t "sssd-softhsm2-gdm-backups-XXXXXX")
  fi

  if [ -e "$1" ]; then
    local back_dir="$backupsdir/$(dirname "$1")"
    local back_path="$back_dir/$(basename "$1")"
    [ ! -e "$back_path" ] || return 1

    mkdir -p "$back_dir" || return 1
    cp -a "$1" "$back_path" || return 1

    restore_paths+=("$back_path")
  else
    delete_paths+=("$1")
  fi
}

function handle_exit() {
  exit_code=$?

  restore_changes || return 1

  if [ $exit_code = 0 ]; then
    rm -rf "$backupsdir"
    set +x
    echo "Script completed successfully!"
  else
    set +x
    echo "Script failed, check the log!"
    echo "  Backup preserved at $backupsdir"
    echo "  PAM Log: /var/log/auth.log"
    echo "  SSSD PAM Log: /var/log/sssd/sssd_pam.log"
    echo "  SSSD p11_child Log: /var/log/sssd/p11_child.log"
  fi
}

trap 'handle_exit' EXIT

tester="$(dirname "$0")"/sssd-softhism2-certificates-tests.sh
if [ ! -e "$tester" ] && [[ ! -v OFFLINE_MODE ]]; then
  echo "Required $tester missing, we're downloading it..."
  tester="$tmpdir/sssd-softhism2-certificates-tests.sh"
  wget -q -c https://gist.github.com/3v1n0/287d02ca8e03936f1c7bba992173d47a/raw/sssd-softhism2-certificates-tests.sh \
    -O "$tester"
  [ -e "$tester" ] || exit 1
elif [ ! -e "$tester" ] && [[ -v OFFLINE_MODE ]]; then
  echo "Required $tester missing"
  exit 1
fi

export PIN TEST_TMPDIR="$tmpdir" GENERATE_SMART_CARDS=1 KEEP_TEMPORARY_FILES=1 NO_SSSD_TESTS=1
chown sssd:sssd "$tmpdir"
runuser -u sssd bash "$tester"

find "$tmpdir" -type d -exec chmod 777 {} \;
find "$tmpdir" -type f -exec chmod 666 {} \;
 
# Ensure that the test user has a non-trivial password. If it had a blank
# password, then /etc/pam.d/gdm-smartcard-sssd-or-password would always
# authenticate successfully
if pamtester -v gdm-password "$SUDO_USER" authenticate </dev/null; then
    backup_file /etc/passwd
    backup_file /etc/shadow
    new_pass=$(cat /proc/sys/kernel/random/uuid)

    echo "Temporarily $SUDO_USER password to '$new_pass'"
    (echo -n "$SUDO_USER:"; echo -n "$new_pass") | chpasswd
fi

backup_file /etc/sssd/sssd.conf
rm -f /etc/sssd/sssd.conf

user_home="$(runuser -u "$SUDO_USER" -- sh -c 'echo ~')"
mkdir -p "$user_home"
chown "$SUDO_USER:$SUDO_USER" "$user_home"

gdm_home="$(runuser -u "$GDM_USER" -- sh -c 'echo ~')"
mkdir -p "$gdm_home"
chown "$GDM_USER:$GDM_USER" "$gdm_home"

user_config="$(runuser -u "$SUDO_USER" -- sh -c 'echo ${XDG_CONFIG_HOME:-~/.config}')"
gdm_config="$(runuser -u "$GDM_USER" -- sh -c 'echo ${XDG_CONFIG_HOME:-~/.config}')"
system_config="/etc"

softhsm2_conf_paths=(
  "$SUDO_USER:$user_config/softhsm2/softhsm2.conf"
  "$GDM_USER:$gdm_config/softhsm2/softhsm2.conf"
  "sssd:$system_config/softhsm/softhsm2.conf"
)

chgrp sssd "$system_config/softhsm"
chmod g+rwx "$system_config/softhsm"
for path_pair in "${softhsm2_conf_paths[@]}"; do
  IFS=":" read -r -a path <<< "${path_pair}"
  path="${path[1]}"
  backup_file "$path"
  rm -f "$path"
done

function test_authentication() {
  certificate_config="$1"
  ca_db="$2"
  verification_options="$3"

  echo "Using CA DB '$ca_db' with verification options: '$verification_options'"

  cat <<EOF > /etc/sssd/sssd.conf || return 2
[sssd]
services = pam
domains = local

[certmap/local/${SUDO_USER}]
matchrule = <SUBJECT>.*Test Organization.*

[domain/local]
id_provider = proxy
proxy_lib_name = files
local_auth_policy = only

[pam]
pam_cert_db_path = $ca_db
pam_cert_verification = $verification_options
pam_cert_auth = True
pam_verbosity = 10
debug_level = 10
EOF

  chown root:sssd /etc/sssd/sssd.conf || return 2
  chmod 640 /etc/sssd/sssd.conf || return 2

  for path_pair in "${softhsm2_conf_paths[@]}"; do
    IFS=":" read -r -a path <<< "${path_pair}"
    user="${path[0]}"
    path="${path[1]}"

    runuser -u "$user" -- mkdir -p "$(dirname "$path")" || return 2
    runuser -u "$user" -- ln -sf "$certificate_config" "$path" || return 2
    runuser -u "$user" -- softhsm2-util --show-slots | grep "Test Organization" \
      || return 2
  done

  systemctl restart sssd || return 2

  for alternative in "${alternative_pam_configs[@]}"; do
    update-alternatives --set gdm-smartcard "$alternative" || return 2

    echo -n -e "$PIN" | runuser -u "$SUDO_USER" -- \
      pamtester -v gdm-smartcard "$SUDO_USER" authenticate || return 2
    echo -n -e "$PIN" | runuser -u "$SUDO_USER" -- \
      pamtester -v gdm-smartcard "" authenticate  || return 2

    if echo -n -e "wrong${PIN}" | runuser -u "$SUDO_USER" -- \
        pamtester -v gdm-smartcard "$SUDO_USER" authenticate; then
      echo "Unexpected pass!"
      return 2
    fi

    if echo -n -e "wrong${PIN}" | runuser -u "$SUDO_USER" -- \
        pamtester -v gdm-smartcard "" authenticate; then
      echo "Unexpected pass!"
      return 2
    fi

    if echo -n -e "$PIN" | pamtester -v gdm-smartcard root authenticate; then
      echo "Unexpected pass!"
      return 2
    fi

    if [[ -v WAIT ]]; then
      echo "Press any key and enter to continue"
      systemctl restart gdm3
      read
    fi
  done
}

test_authentication \
  "$tmpdir/softhsm2-test-root-CA-trusted-certificate-0001.conf" \
  "$tmpdir/test-full-chain-CA.pem"

if [[ -v WAIT ]] && ! [[ -v WAIT_ALL_TESTS ]]; then
  exit $?
fi

test_authentication \
  "$tmpdir/softhsm2-test-sub-intermediate-CA-trusted-certificate-0001.conf" \
  "$tmpdir/test-full-chain-CA.pem"

test_authentication \
  "$tmpdir/softhsm2-test-sub-intermediate-CA-trusted-certificate-0001.conf" \
  "$tmpdir/test-sub-intermediate-CA.pem" \
  "partial_chain"

! test_authentication \
  "$tmpdir/softhsm2-test-sub-intermediate-CA-trusted-certificate-0001.conf" \
  "$tmpdir/test-sub-intermediate-CA.pem"
