#!/bin/bash -p

# omarchy:summary=Toggle passwordless sudo for the current user.
# omarchy:args=[MINUTES]
# omarchy:requires-sudo=true

if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
  echo "Refusing an unsafe Bash startup for passwordless sudo." >&2
  exit 126
fi

security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
  omarchy_security_require_privileged_bash_startup || {
    echo "Refusing an unsafe Bash startup for passwordless sudo." >&2
    exit 126
  }
  omarchy_security_sanitize_bash_environment "$0" "$@" || exit 126
fi

set -euo pipefail

readonly DEFAULT_MINUTES=15
readonly MAX_MINUTES=1440
readonly STATE_DIR=/var/lib/omarchy/sudo-passwordless
readonly RUNTIME_DIR=/run/omarchy/sudo-passwordless
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
readonly PACKAGE_HOOK=/usr/share/libalpm/hooks/05-omarchy-passwordless-revoke.hook
readonly REMOVAL_BLOCKER=/run/omarchy-sudo-passwordless-package-removing
readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
readonly STATUS_INACTIVE=3

usage() {
  echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
  echo "MINUTES must be between 1 and $MAX_MINUTES." >&2
  exit 1
}

valid_minutes() {
  [[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES))
}

valid_uid() {
  [[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294))
}

valid_account_name() {
  [[ $1 =~ ^[a-z_][a-z0-9_-]{0,31}\$?$ ]] && (( ${#1} <= 32 ))
}

resolve_account() {
  local uid="$1" entry
  valid_uid "$uid" || return 1
  entry=$(/usr/bin/getent passwd "$((10#$uid))") || return 1
  IFS=: read -r ACCOUNT_NAME _ ACCOUNT_UID _ _ _ _ <<<"$entry"
  [[ $ACCOUNT_UID == "$((10#$uid))" ]] || return 1
  # Sudoers names and the legacy filename both have metacharacters. Omarchy
  # accounts use this portable subset; refusing anything else is safer than
  # attempting to quote privileged policy syntax.
  valid_account_name "$ACCOUNT_NAME" || return 1
  ACCOUNT_UID=$((10#$uid))
}

verify_sudo_caller() {
  local requested_uid="$1"
  ((EUID == 0)) || return 1
  valid_uid "$requested_uid" || return 1
  [[ ${SUDO_UID:-} =~ ^[0-9]+$ ]] || return 1
  ((10#$SUDO_UID == 10#$requested_uid)) || return 1
  resolve_account "$requested_uid"
}

prepare_root_state() {
  omarchy_security_assert_root_directory /var 755 || return 1
  [[ -d /var/lib && ! -L /var/lib ]] || return 1
  [[ $(/usr/bin/stat -Lc '%u' /var/lib) == 0 ]] || return 1
  ! ((8#$(/usr/bin/stat -Lc '%a' /var/lib) & 022)) || return 1

  if [[ ! -e /var/lib/omarchy && ! -L /var/lib/omarchy ]]; then
    /usr/bin/install -d -o root -g root -m 0755 /var/lib/omarchy || return 1
  fi
  omarchy_security_assert_root_directory /var/lib/omarchy 755 || return 1
  omarchy_security_prepare_private_root_directory "$STATE_DIR" /var/lib/omarchy || return 1

  omarchy_security_assert_root_directory /run 755 || return 1
  if [[ ! -e /run/omarchy && ! -L /run/omarchy ]]; then
    /usr/bin/install -d -o root -g root -m 0755 /run/omarchy || return 1
  fi
  omarchy_security_assert_root_directory /run/omarchy 755 || return 1
  omarchy_security_prepare_private_root_directory "$RUNTIME_DIR" /run/omarchy
}

with_root_lock() {
  local fd rc=0
  # The boot cleanup cannot depend on STATE_DIR or RUNTIME_DIR being healthy:
  # those are exactly the kinds of partial-install state it must fail closed
  # through. /run/lock is established by the OS before sysinit services run.
  omarchy_security_assert_root_directory /run 755 || return 1
  [[ -d /run/lock && ! -L /run/lock ]] || return 1
  [[ $(/usr/bin/stat -Lc '%u' /run/lock) == 0 ]] || return 1
  ! ((8#$(/usr/bin/stat -Lc '%a' /run/lock) & 022)) || return 1
  exec {fd}>"$LOCK_FILE" || return 1
  /usr/bin/chown root:root "$LOCK_FILE" || return 1
  /usr/bin/chmod 0600 "$LOCK_FILE" || return 1
  /usr/bin/flock -x "$fd" || return 1
  "$@" || rc=$?
  /usr/bin/flock -u "$fd" || rc=1
  exec {fd}>&-
  return "$rc"
}

rule_file() {
  printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$1"
}

state_file() {
  printf '%s/%s.state' "$STATE_DIR" "$1"
}

read_state_record() {
  local uid="$1" file state_uid name expires timer canonical_uid
  local -a lines=()
  valid_uid "$uid" || return 1
  canonical_uid=$((10#$uid))
  file=$(state_file "$uid")
  [[ -f $file && ! -L $file ]] || return 1
  mapfile -t lines <"$file" || return 1
  (( ${#lines[@]} == 4 )) || return 1
  [[ ${lines[0]} == UID=* && ${lines[1]} == USER=* &&
    ${lines[2]} == EXPIRES=* && ${lines[3]} == TIMER=* ]] || return 1
  state_uid=${lines[0]#UID=}
  name=${lines[1]#USER=}
  expires=${lines[2]#EXPIRES=}
  timer=${lines[3]#TIMER=}
  [[ $state_uid == "$canonical_uid" ]] || return 1
  valid_account_name "$name" || return 1
  [[ $expires =~ ^[1-9][0-9]{0,10}$ ]] || return 1
  [[ $timer =~ ^omarchy-nopasswd-expire-${canonical_uid}-[0-9a-f]{32}$ ]] || return 1
  printf '%s\t%s\t%s' "$name" "$expires" "$timer"
}

read_state_timer() {
  local record
  record=$(read_state_record "$1") || return 1
  printf '%s' "${record##*$'\t'}"
}

current_epoch() {
  local now
  now=$(/usr/bin/date +%s) || return 1
  [[ $now =~ ^[1-9][0-9]{0,10}$ ]] || return 1
  printf '%s' "$now"
}

valid_expiry() {
  [[ $1 =~ ^[1-9][0-9]{0,10}$ ]]
}

valid_timer_for_uid() {
  local uid="$1" timer="$2"
  valid_uid "$uid" || return 1
  uid=$((10#$uid))
  [[ $timer =~ ^omarchy-nopasswd-expire-${uid}-[0-9a-f]{32}$ ]]
}

stop_timer() {
  local timer="$1"
  [[ $timer =~ ^omarchy-nopasswd-expire-[0-9]+-[0-9a-f]{32}$ ]] || return 0
  /usr/bin/systemctl stop "${timer}.timer" "${timer}.service" >/dev/null 2>&1 || true
  /usr/bin/systemctl reset-failed "${timer}.timer" "${timer}.service" >/dev/null 2>&1 || true
}

classify_generated_rule() {
  local file=$1 suffix contents name

  GENERATED_RULE_LEGACY_TIMER=""
  [[ -f $file && ! -L $file ]] || return 1
  contents=$(/usr/bin/cat -- "$file") || return 2
  suffix=${file##*/99-omarchy-nopasswd-}

  if [[ $suffix =~ ^[0-9]+$ ]]; then
    name=${contents%' ALL=(ALL) NOPASSWD: ALL'}
    if valid_account_name "$name" && [[ $contents == "$name ALL=(ALL) NOPASSWD: ALL" ]]; then
      return 0
    fi
    name=${contents%%' ALL=(ALL) NOTAFTER='*}
    valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
  elif valid_account_name "$suffix" && [[ $contents == "$suffix ALL=(ALL) NOPASSWD: ALL" ]]; then
    GENERATED_RULE_LEGACY_TIMER="omarchy-nopasswd-expire-${suffix}"
  else
    return 1
  fi
}

remove_known_legacy_rules() {
  local file classification failed=0
  shopt -s nullglob
  for file in /etc/sudoers.d/99-omarchy-nopasswd-*; do
    if classify_generated_rule "$file"; then
      # A crash after publishing the numeric rule but before its state rename
      # must not survive the next boot. Do not require the account to still
      # exist: a deleted account could otherwise make the rule immortal and a
      # later username reuse could activate it again.
      if /usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]; then
        [[ -z $GENERATED_RULE_LEGACY_TIMER ]] ||
          /usr/bin/systemctl stop "${GENERATED_RULE_LEGACY_TIMER}.timer" \
            "${GENERATED_RULE_LEGACY_TIMER}.service" >/dev/null 2>&1 || true
      else
        failed=1
      fi
    else
      classification=$?
      # An unreadable candidate cannot be proven inert. A symlink, non-file,
      # or administrator-authored body is unrelated and remains untouched.
      (( classification == 1 )) || failed=1
    fi
  done
  shopt -u nullglob
  return "$failed"
}

cleanup_uid_locked() {
  local uid="$1" timer=""
  valid_uid "$uid" || return 1
  timer=$(read_state_timer "$uid" 2>/dev/null || true)
  # Remove policy first. A failed timer stop can only leave an inert cleanup
  # job behind, never extend passwordless access.
  /usr/bin/rm -f -- "$(rule_file "$uid")" || return 1
  [[ ! -e $(rule_file "$uid") && ! -L $(rule_file "$uid") ]] || return 1
  /usr/bin/rm -f -- "$(state_file "$uid")" || return 1
  [[ -z $timer ]] || stop_timer "$timer"
}

cleanup_all_locked() {
  local state uid failed=0 file classification
  shopt -s nullglob
  for state in "$STATE_DIR"/*.state; do
    uid=${state##*/}
    uid=${uid%.state}
    if valid_uid "$uid" && ! cleanup_uid_locked "$uid"; then failed=1; fi
  done
  shopt -u nullglob
  remove_known_legacy_rules || failed=1

  # Never report a successful boot cleanup while an exact rule emitted by any
  # Omarchy implementation is still active. Administrator-extended files do
  # not match these complete bodies and remain untouched.
  shopt -s nullglob
  for file in /etc/sudoers.d/99-omarchy-nopasswd-*; do
    if classify_generated_rule "$file"; then
      failed=1
    else
      classification=$?
      (( classification == 1 )) || failed=1
    fi
  done
  shopt -u nullglob
  return "$failed"
}

verify_root_policy_file() {
  local file=$1 owner mode canonical current
  [[ -f $file && ! -L $file ]] || return 1
  canonical=$(/usr/bin/realpath -e -- "$file") || return 1
  [[ $canonical == "$file" ]] || return 1
  owner=$(/usr/bin/stat -Lc '%u' -- "$file") || return 1
  mode=$(/usr/bin/stat -Lc '%a' -- "$file") || return 1
  [[ $owner == 0 && $mode =~ ^[0-7]+$ ]] && ! ((8#$mode & 022)) || return 1

  current=${file%/*}
  while :; do
    [[ -d $current && ! -L $current ]] || return 1
    canonical=$(/usr/bin/realpath -e -- "$current") || return 1
    [[ $canonical == "$current" ]] || return 1
    read -r owner mode < <(/usr/bin/stat -Lc '%u %a' -- "$current") || return 1
    [[ $owner == 0 && $mode =~ ^[0-7]+$ ]] && ! ((8#$mode & 022)) || return 1
    [[ $current == / ]] && break
    current=${current%/*}
    [[ -n $current ]] || current=/
  done
}

verify_boot_cleanup() {
  local active_rules hook
  [[ ! -e $REMOVAL_BLOCKER && ! -L $REMOVAL_BLOCKER ]] || return 1
  verify_root_policy_file "$BOOT_CLEANUP_FILE" || return 1
  active_rules=$(/usr/bin/awk '!/^[[:space:]]*(#|$)/ { print }' "$BOOT_CLEANUP_FILE") || return 1
  [[ $active_rules == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]] || return 1
  verify_root_policy_file "$PACKAGE_HOOK" || return 1
  hook=$(/usr/bin/cat -- "$PACKAGE_HOOK") || return 1
  [[ $hook == '[Trigger]
Operation = Upgrade
Operation = Remove
Type = Package
Target = omarchy-settings
Target = omarchy-settings-dev

[Action]
Description = Revoking temporary Omarchy sudo grants before settings changes...
When = PreTransaction
Exec = /usr/bin/omarchy-sudo-passwordless __package-removing
AbortOnFail' ]]
}

package_removing_locked() {
  # ALPM must abort before removing the helper or boot cleanup if revocation
  # fails. The marker also blocks publication after this lock is released.
  (umask 077; : >"$REMOVAL_BLOCKER") || return 1
  /usr/bin/rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-* || return 1
  cleanup_all_locked
}

prepare_state_file() {
  local uid="$1" name="$2" expires="$3" timer="$4" tmp
  tmp=$(/usr/bin/mktemp "$STATE_DIR/.state.XXXXXX") || return 1
  if ! /usr/bin/printf 'UID=%s\nUSER=%s\nEXPIRES=%s\nTIMER=%s\n' \
    "$uid" "$name" "$expires" "$timer" >"$tmp" ||
    ! /usr/bin/chown root:root "$tmp" || ! /usr/bin/chmod 0600 "$tmp"; then
    /usr/bin/rm -f -- "$tmp"
    return 1
  fi
  printf '%s' "$tmp"
}

start_expiry_timer() {
  local uid="$1" expires="$2" timer="$3"
  valid_uid "$uid" && valid_expiry "$expires" && valid_timer_for_uid "$uid" "$timer" || return 1
  # Calendar timers use CLOCK_REALTIME and catch up immediately after resume;
  # a monotonic OnActiveSec timer pauses while the machine is suspended.
  /usr/bin/systemd-run --quiet --collect --on-calendar="@${expires}" \
    --timer-property=AccuracySec=1s --unit="$timer" \
    -- "$INSTALLED_SELF" __expire "$uid" "$timer" || return 1
  /usr/bin/systemctl is-active --quiet "${timer}.timer"
}

publish_rule() {
  local uid="$1" name="$2" expires="$3" destination tmp deadline
  valid_expiry "$expires" || return 1
  deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
  [[ $deadline =~ ^[0-9]{14}Z$ ]] || return 1
  destination=$(rule_file "$uid")
  tmp=$(/usr/bin/mktemp "$STATE_DIR/.sudoers.XXXXXX") || return 1
  if ! /usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$name" "$deadline" >"$tmp" ||
    ! /usr/bin/chown root:root "$tmp" || ! /usr/bin/chmod 0440 "$tmp" ||
    ! /usr/sbin/visudo -cf "$tmp" >/dev/null ||
    ! /usr/bin/install -o root -g root -m 0440 -- "$tmp" "$destination"; then
    /usr/bin/rm -f -- "$tmp"
    return 1
  fi
  /usr/bin/rm -f -- "$tmp"
}

abort_enable_locked() {
  local uid=$1 timer=$2 old_timer=$3 pending_state=$4
  # Publication can install policy and then fail while cleaning its temporary
  # file. Never disarm either expiry job until policy revocation is confirmed.
  if cleanup_uid_locked "$uid"; then
    stop_timer "$timer"
    [[ -z $old_timer ]] || stop_timer "$old_timer"
  else
    echo "Could not revoke passwordless sudo after a failed grant; expiry jobs remain armed. Administrator cleanup is required." >&2
  fi
  /usr/bin/rm -f -- "$pending_state" || true
  return 1
}

enable_locked() {
  local uid="$1" minutes="$2" old_timer="" timer token expires pending_state now
  resolve_account "$uid" || return 1
  valid_minutes "$minutes" || return 1
  prepare_root_state || return 1
  verify_boot_cleanup || {
    echo "omarchy-sudo-passwordless: package-owned boot cleanup or transaction hook is missing or unsafe" >&2
    return 1
  }

  old_timer=$(read_state_timer "$uid" 2>/dev/null || true)
  token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid)
  [[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
  timer="omarchy-nopasswd-expire-${uid}-${token}"
  now=$(current_epoch) || return 1
  expires=$((10#$now + 10#$minutes * 60))

  # State and a verified timer exist before the policy becomes reachable. If
  # publication fails, cleanup removes both. During an update the old timer is
  # deliberately kept until the replacement is active, so failure shortens the
  # grant rather than extending it.
  pending_state=$(prepare_state_file "$uid" "$ACCOUNT_NAME" "$expires" "$timer") || return 1
  if ! start_expiry_timer "$uid" "$expires" "$timer"; then
    abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
    return 1
  fi
  if ! /usr/bin/mv -fT -- "$pending_state" "$(state_file "$uid")"; then
    abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
    return 1
  fi
  if ! verify_boot_cleanup || ! publish_rule "$uid" "$ACCOUNT_NAME" "$expires"; then
    abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
    return 1
  fi
  now=$(current_epoch) || {
    abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
    return 1
  }
  if ((10#$now >= 10#$expires)) || ! /usr/bin/systemctl is-active --quiet "${timer}.timer" || ! verify_boot_cleanup; then
    # The timer may have expired or failed between its initial verification and
    # rule publication. Revoke synchronously so a suspended or heavily loaded
    # machine cannot turn a short grant into a reboot-long one.
    abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
    return 1
  fi
  [[ -z $old_timer || $old_timer == "$timer" ]] || stop_timer "$old_timer"
}

status_locked() {
  local uid="$1" record state_name expires timer now remainder
  resolve_account "$uid" || return 2
  if [[ ! -e $(rule_file "$uid") && ! -L $(rule_file "$uid") ]]; then
    return "$STATUS_INACTIVE"
  fi
  record=$(read_state_record "$uid") || {
    revoke_inactive_grant "$uid"
    return $?
  }
  state_name=${record%%$'\t'*}
  remainder=${record#*$'\t'}
  expires=${remainder%%$'\t'*}
  timer=${record##*$'\t'}
  [[ $state_name == "$ACCOUNT_NAME" ]] || {
    revoke_inactive_grant "$uid"
    return $?
  }
  now=$(current_epoch) || {
    revoke_inactive_grant "$uid"
    return $?
  }
  ((10#$now < 10#$expires)) || {
    revoke_inactive_grant "$uid"
    return $?
  }
  /usr/bin/systemctl is-active --quiet "${timer}.timer" || {
    revoke_inactive_grant "$uid"
    return $?
  }
}

revoke_inactive_grant() {
  if cleanup_uid_locked "$1"; then
    return "$STATUS_INACTIVE"
  else
    echo "Could not revoke invalid or expired passwordless sudo. Administrator cleanup is required." >&2
    return 2
  fi
}

expire_locked() {
  local uid=$1 timer=${2:-} current_timer status
  if [[ -n $timer ]]; then
    current_timer=$(read_state_timer "$uid" 2>/dev/null || true)
    # A delayed predecessor must not revoke a newer, independently timed grant.
    [[ -z $current_timer || $current_timer == "$timer" ]] || return 0
    cleanup_uid_locked "$uid"
  elif status_locked "$uid"; then
    # Compatibility with already scheduled UID-only jobs: enforce the current
    # grant's expiry instead of letting an old timer shorten its replacement.
    return 0
  else
    status=$?
    (( status == STATUS_INACTIVE ))
  fi
}

root_dispatch() {
  local action="$1"
  shift
  case "$action" in
    __status)
      (($# == 1)) && verify_sudo_caller "$1" || return 2
      with_root_lock status_locked "$1"
      ;;
    __enable)
      (($# == 2)) && verify_sudo_caller "$1" && valid_minutes "$2" || return 1
      with_root_lock enable_locked "$1" "$2"
      ;;
    __disable)
      (($# == 1)) && verify_sudo_caller "$1" || return 1
      with_root_lock cleanup_uid_locked "$1"
      ;;
    __expire)
      (($# == 1 || $# == 2)) && ((EUID == 0)) && valid_uid "$1" || return 1
      [[ -z ${2:-} ]] || valid_timer_for_uid "$1" "$2" || return 1
      with_root_lock expire_locked "$@"
      ;;
    __cleanup-all)
      (($# == 0)) && ((EUID == 0)) || return 1
      with_root_lock cleanup_all_locked
      ;;
    __package-removing)
      (($# == 0)) && ((EUID == 0)) || return 1
      with_root_lock package_removing_locked
      ;;
    *) return 1 ;;
  esac
}

case "${1:-}" in
  __status|__enable|__disable|__expire|__cleanup-all|__package-removing)
    action=$1
    shift
    root_dispatch "$action" "$@"
    exit
    ;;
esac

(($# <= 1)) || usage
minutes=${1:-$DEFAULT_MINUTES}
valid_minutes "$minutes" || usage
uid=$(/usr/bin/id -u)
valid_uid "$uid" || {
  echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2
  exit 1
}

omarchy_security_sudo_supports_no_update || {
  echo "This sudo does not support --no-update; refusing the passwordless-sudo workflow." >&2
  exit 1
}

omarchy_security_install_sudo_cleanup_traps
/usr/bin/sudo -k >/dev/null 2>&1 || {
  echo "Could not start from a cold sudo credential state." >&2
  exit 1
}

echo "Toggle passwordless sudo..."
if /usr/bin/sudo -N -- "$INSTALLED_SELF" __status "$uid"; then
  if (($# == 0)); then
    /usr/bin/sudo -N -- "$INSTALLED_SELF" __disable "$uid"
    echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
  else
    /usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
    echo "Passwordless sudo timer updated. It will automatically disable in ${minutes} minutes."
  fi
else
  status=$?
  if (( status != STATUS_INACTIVE )); then
    echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2
    exit 1
  fi
  echo ""
  echo "⚠️ WARNING: This will allow ANY process running as your user to"
  echo "execute ANY command as root WITHOUT a password for ${minutes} minutes."
  echo ""
  echo "This is useful for AI agents that need to run sudo commands,"
  echo "but it significantly weakens the security of your system."
  echo "Anyone or anything with access to your user account gets full root."
  echo ""
  echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
  echo "including if the machine reboots before the timer fires."
  echo "Run this command again to disable it early."
  echo ""

  if /usr/bin/gum confirm "Enable passwordless sudo for ${minutes} minutes? This is a significant security risk!"; then
    /usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
    echo ""
    echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
  else
    echo "Aborted. No changes made."
  fi
fi
