#!/bin/bash

## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## Bootstrap dependency probe uses 'command -v' (no helper-scripts dep).
## style-ok: no-has

set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

exit_with_error() { printf '%s\n' "ERROR: $*" >&2; exit 1; }

print_usage() {
  printf '%s\n' "\
help-steps/run-as-user

Idempotent \"root-to-user drop\" wrapper. Designed to be called as root
with a target sudoers user and an arbitrary command; on first call it
sets up the user and a passwordless-sudo drop-in, on subsequent calls
it is a no-op (other than the exec), and when the caller is already
the target user it just execs the command without touching any
privileged state.

Use cases:
- automated_builder smoke-test harness (test on a non-root user
  without bespoke per-test plumbing).
- Operator on a fresh VPS who wants:
    sudo help-steps/run-as-user builder -- ./derivative-maker ...
  without manually running 'useradd' / 'visudo'.
- Any script under 'build-steps.d/', 'help-steps/', or 'derivative-*'
  that someone wants to drop-and-run as a non-root user.

Usage:
   help-steps/run-as-user [--chown <dir>] [--shell <shell>] -- <user> <cmd> [args...]

Options:
   --chown <dir>    'chown -R' <dir> to <user>:<user> after creating
                    the user (opt-in; recursive ownership change is
                    destructive). Useful when the source tree is
                    owned by root and the target user must be able
                    to write to it (git, build artifacts).
   --shell <shell>  login shell for a newly-created user
                    (default: /bin/bash). Ignored if user already
                    exists.

Security:
   This script writes '/etc/sudoers.d/run-as-user-<user>' granting
   passwordless sudo to <user>. That is a real privilege grant; only
   use this on machines where you have already decided to do so
   (build VPS, dedicated test machine, sandboxed CI runner).

Idempotence:
   - 'useradd' is skipped if <user> already exists.
   - The sudoers drop-in is rewritten via 'visudo -f' with 'sponge'
     as the editor every call, so its contents are guaranteed to
     match this script's intent. visudo validates syntax and writes
     atomically.
   - When invoked while already running as <user>, no privileged
     state is touched at all; the command is exec'd directly."
}

chown_dir=""
target_shell="/bin/bash"
target_user=""

while [ "$#" -gt 0 ]; do
   case "$1" in
      --chown)
         [ -n "${2:-}" ] || exit_with_error "--chown requires a directory argument"
         chown_dir="$2"
         shift 2
         ;;
      --shell)
         [ -n "${2:-}" ] || exit_with_error "--shell requires a shell path argument"
         target_shell="$2"
         shift 2
         ;;
      --)
         shift
         break
         ;;
      -h|--help)
         print_usage
         exit 0
         ;;
      *)
         exit_with_error "unknown option: '$1' (use '--' to separate options from <user> <cmd>)"
         ;;
   esac
done

if [ "$#" -eq 0 ]; then
  exit_with_error "missing <user> and <cmd> arguments; see --help"
elif [ "$#" -eq 1 ]; then
  exit_with_error "missing <cmd>; see --help"
fi

target_user="$1"
shift

case "$target_user" in
   *[!a-zA-Z0-9_.-]*|"")
      exit_with_error "refusing to operate on suspicious username: '$target_user'"
      ;;
esac

caller_uid="$(id -u)"

## Re-entrance: already running as <user>? Just exec; no privileged
## work to do.
if id -u -- "$target_user" >/dev/null 2>&1; then
   target_uid="$(id -u -- "$target_user")"
   if [ "$caller_uid" = "$target_uid" ]; then
      exec "$@"
   fi
fi

[ "$caller_uid" = "0" ] || \
   exit_with_error "caller is uid '$caller_uid', not root, and not '$target_user'; refusing to escalate."

## Prerequisite: 'sponge' (moreutils) for atomic visudo write. The
## build itself installs 'moreutils' in
## 'build-steps.d/1200_prepare-build-machine' (per
## 'buildconfig.d/30_dependencies.conf'), but this wrapper runs before
## any build step, so we need it on the host already.
command -v sponge >/dev/null \
   || exit_with_error "'sponge' not found on PATH; 'apt-get install moreutils' and retry"

## Idempotent user creation.
if ! id -- "$target_user" >/dev/null 2>&1; then
   useradd --create-home --shell "$target_shell" -- "$target_user"
fi

## Idempotent sudoers drop-in. 'visudo -f' with 'sponge' as the editor
## validates syntax and writes atomically (a partially-written drop-in
## would otherwise leave sudo unusable).
sudoers_file="/etc/sudoers.d/run-as-user-$target_user"

SUDO_EDITOR="" VISUAL="" EDITOR=sponge visudo -f "$sudoers_file" >/dev/null <<EOF
## Auto-generated by help-steps/run-as-user. Safe to delete after use.

$target_user ALL=(ALL:ALL) NOPASSWD:ALL
Defaults:%$target_user runcwd=*
EOF

## Optional source-tree chown. Recursive ownership changes are
## destructive, so this is opt-in.
if [ -n "$chown_dir" ]; then
   [ -d "$chown_dir" ] || exit_with_error "--chown target '$chown_dir' is not a directory"
   chown -R -- "$target_user:$target_user" "$chown_dir"
fi

## Hand off. 'sudo --preserve-env=PATH' is the conservative default;
## callers that want more env vars passed through can prepend
## 'env VAR=val ...' inside the command they pass after '--'.
##
## 'user_name=$target_user' is set as an inline sudo env assignment
## (sudo's 'VAR=val cmd' syntax) so the target process sees the
## correct invoking-user identity. Without this, help-steps/variables
## would fall back to '$SUDO_USER' (= 'root' here, because we are
## root invoking sudo to drop privileges) and compute
## HOMEVAR=/home/root, which is nonsensical and breaks downstream
## 'mkdir -p "$HOMEVAR/..."' calls. variables already documents the
## precedence "honor a caller-provided user_name first"; this is
## that caller doing so. Inline assignment (not --preserve-env) so
## sudo's env-strip cannot drop it.
sudo --non-interactive -u "$target_user" --preserve-env=PATH \
   user_name="$target_user" -- "$@"
