#!/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 LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
readonly REMOVAL_BLOCKER=/run/omarchy-sudo-passwordless-package-removing
readonly MIGRATION_MARKER=/var/lib/omarchy/migrations/1788163635
readonly QUARANTINE_DIR=/var/lib/omarchy/sudoers-quarantine
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 has metacharacters, and it reads an upper-case word such as ALICE
  # as an alias reference rather than a user. Accounts use this portable
  # lower-case 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"
}

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
  # Grant operations are short. A stalled holder must not hang a caller
  # indefinitely, least of all a settings package RPM scriptlet.
  /usr/bin/flock -x -w 60 "$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"
}

# The sudoers rule is the only grant record. A missing file is distinct from
# an unreadable, unsafe, or administrator-modified file.
read_grant() {
  local file contents
  file=$(rule_file "$1")
  [[ -e $file || -L $file ]] || return "$STATUS_INACTIVE"
  verify_root_path "$file" && [[ -f $file ]] || return 2
  contents=$(/usr/bin/cat -- "$file") || return 2
  [[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOTAFTER=([0-9]{14}Z)\ NOPASSWD:\ ALL$ ]] || return 2
  GRANT_NAME=${BASH_REMATCH[1]}
  GRANT_DEADLINE=${BASH_REMATCH[2]}
  valid_account_name "$GRANT_NAME" || return 2
}

# Returns 0 for a rule this command generated, 1 for anything else under the
# owned prefix (preserved as administrator policy), and 2 when unreadable.
classify_generated_rule() {
  local file=$1 suffix contents name

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

  # The legacy command wrote the caller's unvalidated name into both the
  # filename and the rule. That exact relationship is its fingerprint, so an
  # account the current policy would reject still has its old grant removed.
  if [[ $contents == "$suffix ALL=(ALL) NOPASSWD: ALL" ]]; then
    return 0
  fi

  [[ $suffix =~ ^[0-9]+$ ]] || return 1
  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$ ]]
}

cleanup_uid_locked() {
  local file
  file=$(rule_file "$1")
  [[ -e $file || -L $file ]] || return 0
  verify_root_path "$file" && classify_generated_rule "$file" || return 1
  /usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]
}

# The generated prefix is reserved: boot cleanup and the package scriptlets
# already remove everything in it, and the legacy writer could produce a rule
# whose body differs from its filename. Nothing unrecognized may stay live
# there, but its content is kept for the administrator instead of being
# deleted.
quarantine_foreign_rule() {
  local file=$1 target
  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_prepare_private_root_directory "$QUARANTINE_DIR" /var/lib/omarchy || return 1
  # A legacy filename can already be close to NAME_MAX, so the destination
  # name is fixed and the original name travels beside it.
  target=$(/usr/bin/mktemp -d "$QUARANTINE_DIR/XXXXXXXXXX") || return 1
  /usr/bin/printf '%s\n' "${file##*/}" >"$target/name" || return 1
  /usr/bin/mv -fT -- "$file" "$target/policy" && [[ ! -e $file && ! -L $file ]] || return 1
  echo "Moved unrecognized sudoers policy $file to $target/policy" >&2
}

cleanup_all_locked() {
  local file classification failed=0
  verify_root_path /etc/sudoers.d || return 1
  for file in /etc/sudoers.d/99-omarchy-nopasswd-*; do
    [[ -e $file || -L $file ]] || continue
    if classify_generated_rule "$file"; then
      if ! /usr/bin/rm -f -- "$file" || [[ -e $file || -L $file ]]; then
        failed=1
      fi
    else
      classification=$?
      if (( classification != 1 )) || ! quarantine_foreign_rule "$file"; then
        failed=1
      fi
    fi
  done
  return "$failed"
}

verify_root_path() {
  local file=$1 owner mode canonical current
  [[ ( -f $file || -d $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 owner tag scriptlet
  [[ ! -e $REMOVAL_BLOCKER && ! -L $REMOVAL_BLOCKER ]] || return 1
  verify_root_path "$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
  # RPM runs a package's %pre before installing or replacing its files and its
  # %preun before erasing them, and a failing scriptlet stops that package's
  # step. The settings package must own this helper and revoke grants from
  # both scriptlets, so no package change can strand a grant without expiry.
  owner=$(/usr/bin/rpm -qf --queryformat '%{NAME}\n' -- "$INSTALLED_SELF") || return 1
  [[ $owner == "omarchy-settings" || $owner == "omarchy-settings-dev" ]] || return 1
  for tag in PREIN PREUN; do
    scriptlet=$(/usr/bin/rpm -q --queryformat "%{$tag}" -- "$owner") || return 1
    [[ $scriptlet == *"$INSTALLED_SELF __package-removing"* ]] || return 1
  done
  # %posttrans lifts the blocker once the transaction has finished; without it
  # the first package change would refuse publication for good.
  scriptlet=$(/usr/bin/rpm -q --queryformat '%{POSTTRANS}' -- "$owner") || return 1
  [[ $scriptlet == *"$INSTALLED_SELF __package-installed"* ]] || return 1
}

package_removing_locked() {
  # A failing %pre or %preun makes RPM skip installing or erasing the settings
  # package, so a failed revocation leaves the helper and boot cleanup in
  # place. 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
}

# RPM runs %posttrans after the whole transaction. A failed or interrupted
# %pre/%preun, or a stranded rule, may have left grants and the blocker behind,
# so completion sweeps the namespace while still blocked and lifts the blocker
# only once nothing is left and boot cleanup is in place.
package_installed_locked() {
  cleanup_all_locked || return 1
  verify_root_path "$BOOT_CLEANUP_FILE" || return 1
  /usr/bin/rm -f -- "$REMOVAL_BLOCKER"
}

migration_complete() {
  [[ -f $MIGRATION_MARKER && ! -s $MIGRATION_MARKER ]] && verify_root_path "$MIGRATION_MARKER"
}

migrate_locked() {
  local directory
  if migration_complete; then
    return 0
  fi
  [[ ! -e $MIGRATION_MARKER && ! -L $MIGRATION_MARKER ]] || return 1
  verify_root_path /var/lib || return 1
  for directory in /var/lib/omarchy /var/lib/omarchy/migrations; do
    if [[ ! -e $directory && ! -L $directory ]]; then
      /usr/bin/install -d -o root -g root -m 0755 -- "$directory" || return 1
    fi
    verify_root_path "$directory" || return 1
  done
  cleanup_all_locked || return 1
  # The empty marker is written only after cleanup succeeds, under the same
  # machine lock. Later accounts need no sudo and cannot revoke newer grants.
  /usr/bin/install -o root -g root -m 0644 /dev/null "$MIGRATION_MARKER"
}

# Old callbacks only remove an expired current rule. Renewing a grant never
# needs a second state file or a stored timer generation to identify it.
expire_locked() {
  local status now
  if read_grant "$1"; then
    now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
    [[ $now < $GRANT_DEADLINE ]] && return 0
    cleanup_uid_locked "$1"
  else
    status=$?
    if (( status == STATUS_INACTIVE )); then
      return 0
    else
      cleanup_uid_locked "$1"
    fi
  fi
}

status_locked() {
  local status now
  resolve_account "$1" || return 2
  if read_grant "$1"; then
    [[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2
    now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
    if [[ $now < $GRANT_DEADLINE ]]; then
      return 0
    fi
    cleanup_uid_locked "$1" || return 2
    return "$STATUS_INACTIVE"
  else
    status=$?
    return "$status"
  fi
}

finish_enable() {
  local status=$?
  trap - EXIT HUP INT TERM
  if (( status != 0 )); then
    if cleanup_uid_locked "$uid"; then
      [[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true
    else
      echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
    fi
  fi
  [[ -z $pending ]] || /usr/bin/rm -f -- "$pending"
  exit "$status"
}

enable_locked() (
  local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status
  resolve_account "$uid" && valid_minutes "$minutes" || return 1
  verify_boot_cleanup && verify_root_path /etc/sudoers.d || return 1
  file=$(rule_file "$uid")
  if read_grant "$uid"; then
    [[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1
  else
    status=$?
    (( status == STATUS_INACTIVE )) || return 1
  fi
  trap finish_enable EXIT
  omarchy_security_install_signal_exit_traps
  now=$(/usr/bin/date +%s) || return 1
  expires=$((now + 10#$minutes * 60))
  deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
  pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1
  /usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
  /usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1
  /usr/sbin/visudo -cf "$pending" >/dev/null || return 1
  token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1
  [[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
  timer="omarchy-nopasswd-expire-$uid-$token"
  /usr/bin/systemd-run --quiet --collect --on-calendar="@$expires" \
    --timer-property=AccuracySec=1s --unit="$timer" \
    -- "$INSTALLED_SELF" __expire "$uid" || return 1
  /usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
  # The temporary filename contains a dot, so sudo ignores it. Rename within
  # sudoers.d publishes the complete validated policy in one operation.
  /usr/bin/mv -fT -- "$pending" "$file" || return 1
  pending=""
  now=$(/usr/bin/date +%s) || return 1
  (( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
)

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:-} || $2 =~ ^omarchy-nopasswd-expire-${1}-[0-9a-f]{32}$ ]] || return 1
      with_root_lock expire_locked "$@"
      ;;
    __migration-complete)
      (($# == 0)) && migration_complete
      ;;
    __migrate)
      (($# == 0)) && ((EUID == 0)) || return 1
      with_root_lock migrate_locked
      ;;
    __cleanup-all)
      (($# == 0)) && ((EUID == 0)) || return 1
      with_root_lock cleanup_all_locked
      ;;
    __package-removing)
      (($# == 0)) && ((EUID == 0)) || return 1
      # Announce before waiting for the lock, so a publisher that holds it is
      # refused instead of publishing a rule this removal would delete.
      (umask 077; : >"$REMOVAL_BLOCKER") || return 1
      with_root_lock package_removing_locked
      ;;
    __package-installed)
      (($# == 0)) && ((EUID == 0)) || return 1
      # Hold the blocker while sweeping: a fresh install has no %pre of its own
      # to have set it, and a failure before the lock must still refuse.
      (umask 077; : >"$REMOVAL_BLOCKER") || return 1
      with_root_lock package_installed_locked
      ;;
    *) return 1 ;;
  esac
}

case "${1:-}" in
  __status|__enable|__disable|__expire|__cleanup-all|__package-removing|__package-installed|__migrate|__migration-complete)
    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
}

# Root actions, including the unattended expiry timer, only ever run the
# packaged copy: root must not execute user-writable checkout code. An install
# that runs only from a checkout has no such copy and cannot hold a grant.
[[ -f $INSTALLED_SELF && -x $INSTALLED_SELF ]] || {
  echo "Passwordless sudo needs Omadora's packaged, root-owned helper at $INSTALLED_SELF, which checkout-only installs do not have." >&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 expiry 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 deadline."
  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
