Simplify passwordless sudo grant lifecycle

This commit is contained in:
Afonso Oliveira committed 2026-09-10 22:01:09 +01:00
1 parent 5bfc8b5cc3
commit c46f321680
6 files changed
+525 -944

No files matched your search

+134 -286
View File
@@ -24,12 +24,11 @@ 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 MIGRATION_MARKER=/var/lib/omarchy/migrations/1788163635
readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
readonly STATUS_INACTIVE=3
@@ -73,26 +72,6 @@ verify_sudo_caller() {
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:
@@ -116,67 +95,23 @@ 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
# 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
}
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-}
@@ -189,81 +124,40 @@ classify_generated_rule() {
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}"
return 0
else
return 1
fi
}
remove_known_legacy_rules() {
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 ]]
}
cleanup_all_locked() {
local file classification failed=0
shopt -s nullglob
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
# 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
if ! /usr/bin/rm -f -- "$file" || [[ -e $file || -L $file ]]; then
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() {
verify_root_path() {
local file=$1 owner mode canonical current
[[ -f $file && ! -L $file ]] || return 1
[[ ( -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
@@ -286,10 +180,10 @@ verify_root_policy_file() {
verify_boot_cleanup() {
local active_rules hook
[[ ! -e $REMOVAL_BLOCKER && ! -L $REMOVAL_BLOCKER ]] || return 1
verify_root_policy_file "$BOOT_CLEANUP_FILE" || 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
verify_root_policy_file "$PACKAGE_HOOK" || return 1
verify_root_path "$PACKAGE_HOOK" || return 1
hook=$(/usr/bin/cat -- "$PACKAGE_HOOK") || return 1
[[ $hook == '[Trigger]
Operation = Upgrade
@@ -313,166 +207,113 @@ package_removing_locked() {
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
migration_complete() {
[[ -f $MIGRATION_MARKER && ! -s $MIGRATION_MARKER ]] && verify_root_path "$MIGRATION_MARKER"
}
migrate_locked() {
local directory
if migration_complete; then
return 0
fi
printf '%s' "$tmp"
[[ ! -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"
}
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"
# 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
echo "Could not revoke passwordless sudo after a failed grant; expiry jobs remain armed. Administrator cleanup is required." >&2
status=$?
if (( status == STATUS_INACTIVE )); then
return 0
else
cleanup_uid_locked "$1"
fi
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
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"
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 ))
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
@@ -491,9 +332,16 @@ root_dispatch() {
;;
__expire)
(($# == 1 || $# == 2)) && ((EUID == 0)) && valid_uid "$1" || return 1
[[ -z ${2:-} ]] || valid_timer_for_uid "$1" "$2" || 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
@@ -507,7 +355,7 @@ root_dispatch() {
}
case "${1:-}" in
__status|__enable|__disable|__expire|__cleanup-all|__package-removing)
__status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete)
action=$1
shift
root_dispatch "$action" "$@"
@@ -542,7 +390,7 @@ if /usr/bin/sudo -N -- "$INSTALLED_SELF" __status "$uid"; then
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."
echo "Passwordless sudo expiry updated. It will automatically disable in ${minutes} minutes."
fi
else
status=$?
@@ -559,7 +407,7 @@ else
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 "including if the machine reboots before the deadline."
echo "Run this command again to disable it early."
echo ""
+6 -4
View File
@@ -4,11 +4,13 @@
## Grant lifecycle
Root state records the resolved account name, absolute expiry epoch and unique timer name. A calendar timer is armed and verified before the generated policy becomes active. The sudoers rule also embeds the same UTC deadline with `NOTAFTER`, so sudo independently rejects it after expiry even if timer cleanup is delayed. Publication rechecks the package-owned boot cleanup before and after installing policy. Policy revocation must succeed before expiry jobs are stopped; a deletion error leaves those jobs armed and reports that administrator cleanup is required.
The sudoers rule is the only grant record: it contains the resolved account name and a UTC `NOTAFTER` deadline enforced by sudo itself, including after suspend. Publication validates a dot-prefixed temporary file with `visudo`, arms a calendar cleanup timer, then atomically renames the complete rule into place. There is no separate per-user state file to publish, parse, or reconcile. Failure after renewal starts removes the old grant; failed revocation remains an error and leaves the cleanup timer armed.
An internal status result is `0` for an active, validated grant and `3` for confirmed inactive access. All other results are errors, including failed authentication and failed revocation. The user interface only offers a new grant after result `3`. It must not turn an inspection failure into a claim that no grant exists.
Each new expiry callback carries its timer identity. A delayed predecessor cannot revoke a newer grant. Already scheduled UID-only callbacks remain compatible by checking the current grant's expiry. Boot-time tmpfiles cleanup removes the reserved generated filename namespace before users log in; it does not run during routine non-boot tmpfiles maintenance.
Calendar timers clean up expired files; their liveness does not define authorization. Callbacks read the current rule and remove it only when expired. Earlier callbacks cannot shorten a renewed grant, so no timer identity needs to be persisted. Old UID-only and token-bearing callbacks remain accepted. Pending callbacks after renewal or manual disable are harmless and expire within the maximum 24-hour grant window. Boot-time tmpfiles cleanup removes the reserved generated filename namespace before users log in; routine non-boot tmpfiles maintenance leaves live grants alone.
Legacy cleanup uses a root-owned machine marker under `/var/lib/omarchy/migrations/`, written only after successful cleanup under the grant lock. Later accounts can finish their migration queues without sudo and without revoking grants created after the repair. Old grant state files are no longer consulted; generated legacy policy is removed conservatively and administrator-modified policy is preserved by the migration.
## Package ownership
@@ -20,6 +22,6 @@ The runtime marker need not survive reboot: pre-removal revokes the old grants b
## Validation
`test/shell.d/nopasswd-sudo-expiry-test.sh` covers the public interface, cold authentication, timer setup, boot cleanup, package transitions and lock contention. `test/shell.d/passwordless-grant-lifecycle-test.sh` covers publication/cleanup failures, error status, supported account syntax, predecessor callbacks and the shared package-removal lock. Supply `OMARCHY_PKGS_PATH` as either a repository root or its `pkgbuilds` directory.
The two passwordless-sudo test suites share a private filesystem and command fixture. They cover caller validation, the public prompt boundary, atomic publication, renewal failures, expiry, old callbacks, machine migration, and the source/package lock. Supply `OMARCHY_PKGS_PATH` as either a repository root or its `pkgbuilds` directory. An optional `OMARCHY_TEST_SUDOERS` path to sudo's upstream `testsudoers` executable evaluates the generated policy before and after its deadline without root or changing host policy.
These tests use private filesystem fixtures and mapped privileged commands. Package archive ownership, actual install/upgrade/removal, real calendar expiry, suspend/resume and boot cleanup must also be validated in a disposable VM before claiming release readiness. Changes to the common library require integration checks on the downstream update, migration, installer, package-picker and diagnostic PRs.
These local tests do not establish release readiness. The simplified candidate needs fresh installed-package, suspend/resume, boot-cleanup, and package-removal validation in a disposable VM. The shared security library and its interface are unchanged for downstream PRs.
+4 -4
View File
@@ -1,6 +1,6 @@
echo "Remove legacy temporary passwordless sudo grants"
# This removes current numeric grants, exact legacy username grants, corrupt or
# orphaned state, and their known timers. Administrator-authored sudoers files
# whose contents do not exactly match Omarchy's generated grammar are preserved.
sudo /usr/bin/omarchy-sudo-passwordless __cleanup-all
# Migration queues are per-user; the privileged repair is once per machine.
if ! /usr/bin/omarchy-sudo-passwordless __migration-complete; then
sudo /usr/bin/omarchy-sudo-passwordless __migrate
fi
@@ -0,0 +1,125 @@
#!/bin/bash
# Exercise complete production functions with private paths and harmless
# command stand-ins. Never install sudo policy or start a host timer.
test_tmp=$(mktemp -d)
children=()
cleanup_grant_fixture() {
local status=$?
trap - EXIT
if (( ${#children[@]} )); then
kill "${children[@]}" 2>/dev/null || true
wait "${children[@]}" 2>/dev/null || true
fi
rm -rf "$test_tmp"
exit "$status"
}
trap cleanup_grant_fixture EXIT
export TEST_GRANT_ROOT=$test_tmp
mkdir -p "$test_tmp/bin" "$test_tmp/etc/sudoers.d" "$test_tmp/etc/tmpfiles.d" "$test_tmp/run/lock" "$test_tmp/var/lib" "$test_tmp/hooks"
cat >"$test_tmp/bin/mock" <<'STUB'
#!/bin/bash
set -euo pipefail
name=${0##*/}
printf '%s %s\n' "$name" "$*" >>"$TEST_GRANT_ROOT/commands"
case "$name" in
stat)
path=${@: -1}
owner=0
mode=$(/usr/bin/stat -Lc '%a' -- "$path")
[[ $path != /tmp ]] || mode=755
[[ $path != "${TEST_BAD_PATH:-}" ]] || owner=1000
case $2 in
'%u') echo "$owner" ;;
'%a') echo "$mode" ;;
'%u %a') echo "$owner $mode" ;;
*) exec /usr/bin/stat "$@" ;;
esac
;;
chown) exit 0 ;;
install)
args=()
while (($#)); do
case $1 in -o|-g) shift 2 ;; *) args+=("$1"); shift ;; esac
done
exec /usr/bin/install "${args[@]}"
;;
rm)
for path in "$@"; do
if [[ ${TEST_DELETE_FAIL:-0} == 1 && $path == "$TEST_GRANT_ROOT/etc/sudoers.d/99-omarchy-nopasswd-1000" ]]; then exit 1; fi
done
exec /usr/bin/rm "$@"
;;
mv)
[[ ${TEST_PUBLISH_FAIL:-0} != 1 ]] || exit 1
/usr/bin/mv "$@"
[[ ${TEST_POST_PUBLISH_FAIL:-0} != 1 ]] || : >"$TEST_GRANT_ROOT/run/omarchy-sudo-passwordless-package-removing"
;;
systemd-run)
[[ ${TEST_TIMER_FAIL:-0} != 1 ]] || exit 1
if [[ ${TEST_CANCEL_ENABLE:-0} == 1 ]]; then kill -TERM "$PPID"; fi
;;
systemctl)
[[ $1 != "is-active" || ${TEST_INACTIVE_TIMER:-0} != 1 ]]
;;
date)
if [[ ${TEST_EXPIRED:-0} == 1 && $* == '-u +%Y%m%d%H%M%SZ' ]]; then echo 99991231235959Z; else /usr/bin/date "$@"; fi
;;
getent) printf '%s:x:1000:1000:Test:/nonexistent:/bin/bash\n' "${TEST_ACCOUNT:-audituser}" ;;
sudo)
if [[ ${1:-} == -h ]]; then echo 'usage: sudo [-N] command'; exit 0; fi
if [[ ${1:-} == -k ]]; then exit 0; fi
if [[ ${1:-} == -N ]]; then shift; fi
if [[ ${1:-} == -- ]]; then shift; fi
if [[ ${TEST_MIGRATION:-0} == 1 ]]; then
[[ ${TEST_NO_SUDO:-0} != 1 ]] || exit 1
TEST_EUID=0 /usr/bin/bash -p "$@"
else
[[ ${2:-} != __status ]] || exit "${TEST_STATUS:-3}"
fi
;;
gum) exit 1 ;;
*) exit 99 ;;
esac
STUB
chmod +x "$test_tmp/bin/mock"
for name in stat chown install rm mv systemd-run systemctl date getent sudo gum; do
ln -s mock "$test_tmp/bin/$name"
done
python3 - "$ROOT" "$test_tmp" <<'PY'
from pathlib import Path
import sys
root, temp = map(Path, sys.argv[1:])
for name in ('omarchy-sudo-passwordless', 'omarchy-security-functions'):
text = (root/'bin'/name).read_text()
for path in ('/etc/', '/var/lib', '/run/', '/usr/share/libalpm/hooks'):
target = str(temp/'hooks') if path == '/usr/share/libalpm/hooks' else str(temp) + path
text = text.replace(path, target)
text = text.replace('((EUID == 0))', '((${TEST_EUID:-1} == 0))')
for command in ('stat', 'chown', 'install', 'rm', 'mv', 'systemd-run', 'systemctl', 'date', 'getent', 'sudo', 'gum'):
text = text.replace('/usr/bin/' + command, str(temp/'bin'/command))
(temp/name).write_text(text)
(temp/name).chmod(0o755)
PY
library="$test_tmp/functions.sh"
{
printf 'source %q\n' "$test_tmp/omarchy-security-functions"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$test_tmp/omarchy-sudo-passwordless"
} >"$library"
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
sed "s|/etc/|$test_tmp/etc/|g" "$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf" >"$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
: >"$test_tmp/commands"
# New subshell per case prevents one test's overrides and readonly constants
# from affecting the next. External commands log enough to verify ordering.
assert_status() {
local expected=$1 actual=0
shift
"$@" || actual=$?
(( actual == expected )) || fail "expected status $expected, got $actual from $*"
}
reset_grant() {
rm -f "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
: >"$test_tmp/commands"
}
+142 -437
View File
@@ -1,458 +1,163 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/passwordless-sudo-test.sh"
command_path="$ROOT/bin/omarchy-sudo-passwordless"
security_library_path="$ROOT/bin/omarchy-security-functions"
tmpfiles_path="$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
migration_path="$ROOT/migrations/1788163635.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
function_prefix() {
printf 'source %q\n' "$security_library_path"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$command_path"
}
# Exercise the validation code itself. Leading zeroes remain numeric, but zero,
# negatives, oversized grants, and shell syntax are rejected.
(
source <(function_prefix)
for minutes in 1 15 1440 00015; do
valid_minutes "$minutes" || fail "passwordless sudo accepts bounded duration $minutes"
done
for minutes in 0 1441 -1 1m '1;id' '' 18446744073709551617; do
! valid_minutes "$minutes" || fail "passwordless sudo rejects invalid duration '$minutes'"
done
source "$library"
for minutes in 1 15 1440 00015; do valid_minutes "$minutes" || exit 1; done
for minutes in 0 1441 -1 1m '' 18446744073709551617; do ! valid_minutes "$minutes" || exit 1; done
for name in audituser 'buildbot$'; do valid_account_name "$name" || exit 1; done
for name in 'a$b' '$' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do ! valid_account_name "$name" || exit 1; done
! valid_uid 18446744073709551617
)
pass "passwordless sudo validates a bounded positive duration"
pass "duration and account validation retains bounded inputs and trailing-dollar usernames"
# The public entry point uses the kernel-backed numeric identity; $USER is
# never interpolated into a privileged filename or sudoers rule.
grep -F 'uid=$(/usr/bin/id -u)' "$command_path" >/dev/null ||
fail "passwordless sudo derives the caller from id -u"
! grep -Eq '\$\{?USER\}?' "$command_path" ||
fail "passwordless sudo does not trust USER for privileged policy"
grep -F '[[ ${SUDO_UID:-} =~ ^[0-9]+$ ]]' "$command_path" >/dev/null ||
fail "passwordless sudo validates sudo provenance"
pass "passwordless sudo derives and validates trusted account identity"
(
source "$library"
assert_status 2 root_dispatch __status 1000
assert_status 2 env TEST_EUID=0 SUDO_UID=1001 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __status 1000
assert_status 3 env TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __status 1000
)
pass "internal actions reject missing root and mismatched sudo identity"
# Status inspection and the confirmation UI are mixed-trust: a normal sudo
# status call would publish a timestamp that a hostile prompt helper could use
# even when the user declines the grant. Exercise the public flow with a sudo
# model that publishes a token only when -N is missing.
grep -Fxq '#!/bin/bash -p' "$command_path" ||
fail "passwordless sudo no longer suppresses Bash startup injection"
public_sudo_stub="$test_tmp/public-sudo"
public_gum_stub="$test_tmp/public-gum"
public_token="$test_tmp/public-token"
public_exploit="$test_tmp/public-exploit"
cat >"$public_sudo_stub" <<'STUB'
#!/bin/bash
if [[ ${1:-} == -h ]]; then
echo 'usage: sudo [-ABbEHkNnPS] command'
exit 0
fi
if [[ ${1:-} == -k ]]; then
rm -f -- "$TEST_PUBLIC_TOKEN"
exit 0
fi
no_update=0
if [[ ${1:-} == -N ]]; then no_update=1; shift; fi
[[ ${1:-} != -- ]] || shift
((no_update)) || : >"$TEST_PUBLIC_TOKEN"
case "${2:-}" in
__status) exit "${TEST_PUBLIC_STATUS:-3}" ;;
__enable|__disable) exit 0 ;;
*) exit 2 ;;
esac
STUB
cat >"$public_gum_stub" <<'STUB'
#!/bin/bash
[[ -z ${TEST_PUBLIC_GUM_LOG:-} ]] || : >"$TEST_PUBLIC_GUM_LOG"
[[ ! -e $TEST_PUBLIC_TOKEN ]] || : >"$TEST_PUBLIC_EXPLOIT"
exit 1
STUB
chmod 0755 "$public_sudo_stub" "$public_gum_stub"
public_flow="$test_tmp/passwordless-public-flow"
/usr/bin/sed "s#/usr/bin/sudo#$public_sudo_stub#g" "$security_library_path" >"$test_tmp/omarchy-security-functions"
/usr/bin/sed \
-e "s#/usr/bin/sudo#$public_sudo_stub#g" \
-e "s#/usr/bin/gum#$public_gum_stub#g" \
"$command_path" >"$public_flow"
chmod 0755 "$public_flow"
TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
/usr/bin/bash -p "$public_flow" 15 >/dev/null
[[ ! -e $public_token && ! -e $public_exploit ]] ||
fail "passwordless confirmation inherited a reusable status credential"
for status in 1 2; do
if TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
TEST_PUBLIC_STATUS="$status" TEST_PUBLIC_GUM_LOG="$test_tmp/unsafe-status-confirmation" \
/usr/bin/bash -p "$public_flow" 15 >"$test_tmp/status-error.output" 2>&1; then
fail "passwordless sudo treats status/authorization failure $status as inactive"
for status in 1 2 3; do
: >"$test_tmp/commands"
result=0
TEST_STATUS=$status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1 || result=$?
if (( status == 3 )); then
(( result == 0 )) && grep -q '^gum confirm ' "$test_tmp/commands" || fail "inactive status must allow confirmation"
else
(( result != 0 )) && ! grep -q '^gum ' "$test_tmp/commands" || fail "inspection errors must not offer enablement"
fi
[[ ! -e $test_tmp/unsafe-status-confirmation ]] || fail "failed status inspection opens the enable prompt"
grep -q 'Could not safely inspect passwordless sudo' "$test_tmp/status-error.output" ||
fail "failed status inspection lacks recovery guidance"
grep -q '^sudo -N -- .* __status ' "$test_tmp/commands" || fail "status must not publish reusable authorization"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]] || fail "public exit must revoke its authorization"
done
pass "public status distinguishes inactive from errors and revokes authorization on exit"
startup_env="$test_tmp/passwordless-bash-env"
startup_marker="$test_tmp/passwordless-bash-env-ran"
cat >"$startup_env" <<'STUB'
: >"$TEST_STARTUP_MARKER"
set -o privileged
unset BASH_ENV
STUB
if BASH_ENV="$startup_env" TEST_STARTUP_MARKER="$startup_marker" \
/usr/bin/bash "$public_flow" -p >/dev/null 2>&1; then
fail "passwordless sudo accepted an unsafe interpreter with a decoy -p"
printf ': >"$TEST_STARTUP_MARKER"\nset -o privileged\nunset BASH_ENV\n' >"$test_tmp/startup"
: >"$test_tmp/commands"
if TEST_STARTUP_MARKER="$test_tmp/startup-ran" BASH_ENV="$test_tmp/startup" bash "$test_tmp/omarchy-sudo-passwordless" -p >/dev/null 2>&1; then
fail "ordinary Bash with a decoy -p was accepted"
fi
[[ -e $startup_marker && ! -e $public_token && ! -e $public_exploit ]] ||
fail "unsafe passwordless startup reached its sudo workflow"
pass "passwordless confirmation uses a cold command-scoped credential boundary"
[[ -f $test_tmp/startup-ran && ! -s $test_tmp/commands ]] || fail "startup rejection must precede sudo"
pass "startup validation rejects ordinary Bash before authorization"
# Source a path-rewritten copy so the real cleanup implementation can be
# exercised without touching /etc. Exact generated numeric rules are removed
# even after account deletion or a crash before state publication. Anything an
# administrator changed, and every symlink, is preserved.
fake_sudoers="$test_tmp/sudoers.d"
mkdir "$fake_sudoers"
rewritten="$test_tmp/passwordless-lib.sh"
function_prefix | sed "s#/etc/sudoers.d#$fake_sudoers#g" >"$rewritten"
(
source "$rewritten"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$fake_sudoers/99-omarchy-nopasswd-424242"
printf 'admin ALL=(ALL) NOPASSWD: /usr/bin/pacman\n' >"$fake_sudoers/99-omarchy-nopasswd-424243"
ln -s "$fake_sudoers/99-omarchy-nopasswd-424243" "$fake_sudoers/99-omarchy-nopasswd-424244"
remove_known_legacy_rules
)
[[ ! -e $fake_sudoers/99-omarchy-nopasswd-424242 ]] ||
fail "boot cleanup removes a state-less numeric orphan"
[[ -f $fake_sudoers/99-omarchy-nopasswd-424243 ]] ||
fail "boot cleanup preserves administrator-authored policy"
[[ -L $fake_sudoers/99-omarchy-nopasswd-424244 ]] ||
fail "boot cleanup refuses sudoers symlinks"
pass "boot cleanup removes crash/deleted-account orphans conservatively"
# A boot gate must not report success when deletion itself fails. Exercise the
# real cleanup and post-cleanup verification with a deterministic failing rm.
rm_failure_dir="$test_tmp/rm-failure-sudoers"
mkdir "$rm_failure_dir"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$rm_failure_dir/99-omarchy-nopasswd-424245"
failing_rm="$test_tmp/failing-rm"
cat >"$failing_rm" <<'FAILING_RM'
#!/bin/bash
exit 1
FAILING_RM
chmod +x "$failing_rm"
rm_failure_lib="$test_tmp/rm-failure-lib.sh"
function_prefix |
sed -e "s#/etc/sudoers.d#$rm_failure_dir#g" \
-e "s#/var/lib/omarchy/sudo-passwordless#$test_tmp/empty-state#g" \
-e "s#/usr/bin/rm#$failing_rm#g" >"$rm_failure_lib"
mkdir "$test_tmp/empty-state"
(
source "$rm_failure_lib"
! cleanup_all_locked
) || fail "boot cleanup fails when an Omarchy rule cannot be removed"
[[ -f $rm_failure_dir/99-omarchy-nopasswd-424245 ]] ||
fail "rm-failure fixture remains available for verification"
pass "boot cleanup fails closed when policy deletion fails"
# Reproduce the migration's real sudo provenance: sudo sets SUDO_UID. Rewrite
# only the read-only EUID probe so this unprivileged test can exercise the root
# dispatcher, then assert that cleanup (which can only revoke privilege) runs.
dispatch_lib="$test_tmp/dispatch-lib.sh"
function_prefix | sed 's/((EUID == 0))/((TEST_EUID == 0))/g' >"$dispatch_lib"
(
source "$dispatch_lib"
called=""
cleanup_all_locked() { called=cleanup; }
with_root_lock() { "$@"; }
TEST_EUID=0 SUDO_UID=1000 root_dispatch __cleanup-all
[[ $called == cleanup ]]
) || fail "migration cleanup dispatch accepts authenticated sudo provenance"
pass "migration can invoke fail-closed cleanup through sudo"
# A grant cannot be published until the static unit is verified/enabled, and a
# timer setup failure removes its pending state without calling publish_rule.
transaction_dir="$test_tmp/transaction"
mkdir "$transaction_dir"
transaction_lib="$test_tmp/transaction-lib.sh"
function_prefix | sed "s#/var/lib/omarchy/sudo-passwordless#$transaction_dir#g" >"$transaction_lib"
(
source "$transaction_lib"
ACCOUNT_NAME=audituser
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 1; }
publish_rule() { return 99; }
! enable_locked 1000 15
)
(
source "$transaction_lib"
ACCOUNT_NAME=audituser
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
read_state_timer() { return 1; }
prepare_state_file() { local pending="$transaction_dir/pending"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 1; }
publish_rule() { printf published >"$transaction_dir/published"; }
cleanup_uid_locked() { : >"$transaction_dir/failed-timer-cleanup"; }
! enable_locked 1000 15
[[ ! -e $transaction_dir/pending && ! -e $transaction_dir/published &&
-e $transaction_dir/failed-timer-cleanup ]]
) || fail "passwordless sudo fails closed on prerequisite/timer failure"
pass "passwordless sudo publishes no rule after partial setup failure"
# Erik's predecessor fix revoked an already-active grant when an extension
# could not arm its replacement timer. Keep that fail-closed property while
# the new transaction deliberately leaves the old timer armed until the new
# one is verified.
replacement_state="$transaction_dir/1000.state"
replacement_rule="$transaction_dir/1000.rule"
replacement_stopped="$transaction_dir/old-timer-stopped"
old_timer=omarchy-nopasswd-expire-1000-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
printf 'UID=1000\nUSER=audituser\nEXPIRES=2000000000\nTIMER=%s\n' "$old_timer" >"$replacement_state"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$replacement_rule"
(
source "$transaction_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
state_file() { printf '%s' "$replacement_state"; }
rule_file() { printf '%s' "$replacement_rule"; }
prepare_state_file() { local pending="$transaction_dir/replacement-pending"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 1; }
stop_timer() { [[ $1 == "$old_timer" ]] && : >"$replacement_stopped"; }
! enable_locked 1000 30
[[ ! -e $replacement_state && ! -e $replacement_rule && -e $replacement_stopped ]]
) || fail "passwordless sudo leaves an existing grant live after replacement timer failure"
pass "replacement timer failure revokes the existing grant"
# Expiry is a wall-clock promise, so the transient timer must carry the exact
# absolute epoch recorded in root state. A monotonic-only --on-active timer
# pauses during suspend and can otherwise extend a short grant by hours.
timer_args="$test_tmp/timer-args"
calendar_systemd_run="$test_tmp/calendar-systemd-run"
calendar_systemctl="$test_tmp/calendar-systemctl"
cat >"$calendar_systemd_run" <<'STUB'
#!/bin/bash
printf '%s\n' "$@" >"$TEST_TIMER_ARGS"
STUB
cat >"$calendar_systemctl" <<'STUB'
#!/bin/bash
exit 0
STUB
chmod 0755 "$calendar_systemd_run" "$calendar_systemctl"
calendar_lib="$test_tmp/calendar-lib.sh"
function_prefix |
sed -e "s#/usr/bin/systemd-run#$calendar_systemd_run#g" \
-e "s#/usr/bin/systemctl#$calendar_systemctl#g" >"$calendar_lib"
(
source "$calendar_lib"
TEST_TIMER_ARGS="$timer_args" start_expiry_timer 1000 2000000000 \
omarchy-nopasswd-expire-1000-0123456789abcdef0123456789abcdef
) || fail "passwordless sudo cannot arm its absolute expiry timer"
grep -Fx -- '--on-calendar=@2000000000' "$timer_args" >/dev/null ||
fail "passwordless sudo timer does not advance across suspend"
pass "passwordless sudo arms the recorded absolute wall-clock expiry"
# A resumed machine can briefly observe the timer as active before systemd
# dispatches its overdue service. Status must independently enforce EXPIRES and
# synchronously remove policy instead of trusting timer activity alone.
expired_state="$test_tmp/expired-state"
expired_sudoers="$test_tmp/expired-sudoers"
mkdir "$expired_state" "$expired_sudoers"
expired_timer=omarchy-nopasswd-expire-1000-0123456789abcdef0123456789abcdef
printf 'UID=1000\nUSER=audituser\nEXPIRES=1\nTIMER=%s\n' "$expired_timer" >"$expired_state/1000.state"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$expired_sudoers/99-omarchy-nopasswd-1000"
expired_lib="$test_tmp/expired-lib.sh"
function_prefix |
sed -e "s#/var/lib/omarchy/sudo-passwordless#$expired_state#g" \
-e "s#/etc/sudoers.d#$expired_sudoers#g" \
-e "s#/usr/bin/systemctl#$calendar_systemctl#g" >"$expired_lib"
(
source "$expired_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
! status_locked 1000
) || fail "passwordless sudo accepts expired root state while its timer is active"
[[ ! -e $expired_state/1000.state && ! -e $expired_sudoers/99-omarchy-nopasswd-1000 ]] ||
fail "passwordless sudo does not synchronously revoke expired state"
pass "passwordless sudo enforces wall-clock expiry independently of timer dispatch"
# If the transient timer fires between its first active check and publication,
# the just-created rule must be synchronously revoked instead of surviving to
# reboot. Model that narrow transition with the real enable transaction.
inactive_systemctl="$test_tmp/inactive-systemctl"
cat >"$inactive_systemctl" <<'STUB'
#!/bin/bash
exit 1
STUB
chmod 0755 "$inactive_systemctl"
post_publish_lib="$test_tmp/post-publish-lib.sh"
sed "s#/usr/bin/systemctl#$inactive_systemctl#g" "$transaction_lib" >"$post_publish_lib"
(
source "$post_publish_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
read_state_timer() { return 1; }
prepare_state_file() { local pending="$transaction_dir/pending-after-arm"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 0; }
publish_rule() { : >"$transaction_dir/published-after-arm"; }
cleanup_uid_locked() { rm -f "$transaction_dir/published-after-arm"; : >"$transaction_dir/revoked-after-arm"; }
! enable_locked 1000 15
[[ ! -e $transaction_dir/published-after-arm && -e $transaction_dir/revoked-after-arm ]]
) || fail "passwordless sudo leaves a grant when its armed timer expires before publication completes"
pass "timer expiry during publication revokes the grant synchronously"
# Follow the maintainer's package-owned tmpfiles design: one boot-only rule
# owns this filename namespace. A routine --remove leaves live grants alone;
# early boot removes them before a user can log in. The migration only revokes
# legacy runtime state and never writes static policy into /usr.
mapfile -t tmpfiles_rules < <(/usr/bin/grep -vE '^[[:space:]]*(#|$)' "$tmpfiles_path")
(( ${#tmpfiles_rules[@]} == 1 )) || fail "passwordless sudo ships one boot cleanup rule"
[[ ${tmpfiles_rules[0]} == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]] ||
fail "passwordless sudo boot cleanup does not own the exact generated namespace"
fake_root="$test_tmp/tmpfiles-root"
sudoers_dir="$fake_root/etc/sudoers.d"
mkdir -p "$sudoers_dir"
for name in alice buildbot-2 424242; do
: >"$sudoers_dir/99-omarchy-nopasswd-$name"
done
: >"$sudoers_dir/omarchy-dns"
/usr/bin/systemd-tmpfiles --root="$fake_root" --remove --inline "${tmpfiles_rules[0]}"
[[ -e $sudoers_dir/99-omarchy-nopasswd-alice ]] || fail "non-boot tmpfiles run shortened a live grant"
/usr/bin/systemd-tmpfiles --root="$fake_root" --remove --boot --inline "${tmpfiles_rules[0]}"
! find "$sudoers_dir" -name '99-omarchy-nopasswd-*' -print -quit | /usr/bin/grep -q . ||
fail "boot cleanup left a generated passwordless grant"
[[ -e $sudoers_dir/omarchy-dns ]] || fail "boot cleanup removed an unrelated sudoers rule"
/usr/bin/grep -Fx 'sudo /usr/bin/omarchy-sudo-passwordless __cleanup-all' "$migration_path" >/dev/null
! /usr/bin/grep -q 'omarchy-sudo-passwordless-cleanup.service' "$migration_path" ||
fail "migration retained a custom boot service instead of package-owned tmpfiles"
pass "package-owned boot cleanup is narrow, boot-only, and migration-safe"
# Removing the settings package also removes the tmpfiles rule. Its package
# lifecycle must therefore revoke the same owned namespace synchronously, while
# preserving every unrelated sudoers file.
pkgs_candidates=(
"${OMARCHY_PKGS_PATH:-}"
"$ROOT/../omarchy-pkgs"
"$ROOT/../../omarchy-pkgs"
"$HOME/Work/omarchy/omarchy-pkgs"
"$HOME/Work/omacom/omarchy-pkgs"
)
pkgs_root=""
for candidate in "${pkgs_candidates[@]}"; do
if [[ -n $candidate && -d $candidate/pkgbuilds/omarchy-settings ]]; then
pkgs_root=$candidate/pkgbuilds
break
elif [[ -n $candidate && -d $candidate/omarchy-settings ]]; then
pkgs_root=$candidate
break
source "$library"
enable_locked 1000 15
read_grant 1000
[[ $GRANT_NAME == audituser && $(stat -c '%a' "$(rule_file 1000)") == 440 ]]
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
expiry=$(sed -n 's/^systemd-run .*--on-calendar=@\([0-9]*\).*$/\1/p' "$test_tmp/commands" | tail -1)
[[ $GRANT_DEADLINE == "$(/usr/bin/date -u -d "@$expiry" +%Y%m%d%H%M%SZ)" ]]
if [[ -n ${OMARCHY_TEST_SUDOERS:-} ]]; then
[[ -x $OMARCHY_TEST_SUDOERS ]] || fail "OMARCHY_TEST_SUDOERS must name an executable"
printf 'root:x:0:0:root:/root:/bin/bash\naudituser:x:1000:1000:Test:/nonexistent:/bin/bash\n' >"$test_tmp/passwd"
printf 'root:x:0:\naudituser:x:1000:\n' >"$test_tmp/group"
{ printf 'audituser ALL=(ALL) ALL\n'; cat "$(rule_file 1000)"; } >"$test_tmp/policy"
for offset in -1 1; do
when=$(/usr/bin/date -u -d "@$((expiry + offset))" +%Y%m%d%H%M%SZ)
"$OMARCHY_TEST_SUDOERS" -p "$test_tmp/passwd" -P "$test_tmp/group" -T "$when" audituser /usr/bin/true <"$test_tmp/policy" >"$test_tmp/policy-result"
if (( offset < 0 )); then
! grep -q 'Password required' "$test_tmp/policy-result" || fail "native policy requires a password before expiry"
else
grep -q 'Password required' "$test_tmp/policy-result" || fail "native policy remains passwordless after expiry"
fi
done
pass "native sudoers evaluation requires authentication after the generated deadline"
fi
done
[[ -n $pkgs_root ]] || fail "omarchy-pkgs checkout found for passwordless package-removal coverage"
assert_status 0 status_locked 1000
TEST_INACTIVE_TIMER=1 assert_status 0 status_locked 1000
[[ ! -e $test_tmp/var/lib/omarchy/sudo-passwordless ]]
! compgen -G "$test_tmp/etc/sudoers.d/.omarchy-nopasswd.*"
)
pass "one complete mode-0440 sudoers rule holds the deadline with no separate grant state"
for package_name in omarchy-settings omarchy-settings-dev; do
install_script="$pkgs_root/$package_name/$package_name.install"
transformed_install="$test_tmp/$package_name.install"
removal_root="$test_tmp/$package_name-remove"
removal_sudoers="$removal_root/etc/sudoers.d"
mkdir -p "$removal_sudoers" "$removal_root/run/lock" "$removal_root/etc/tmpfiles.d"
: >"$removal_sudoers/99-omarchy-nopasswd-1000"
: >"$removal_sudoers/99-omarchy-nopasswd-legacy-user"
: >"$removal_sudoers/omarchy-dns"
ln -s ../administrator/os-release "$removal_root/etc/os-release"
package_stat="$test_tmp/package-stat"
cat >"$package_stat" <<'STUB'
#!/bin/bash
if [[ $2 == '%u' ]]; then printf '0\n'; else /usr/bin/stat "$@"; fi
STUB
chmod +x "$package_stat"
sed -e "s#/etc/#$removal_root/etc/#g" \
-e "s#/run#$removal_root/run#g" \
-e "s#/usr/bin/stat#$package_stat#g" "$install_script" >"$transformed_install"
(
source "$library"
before=$(cat "$(rule_file 1000)")
expire_locked 1000 omarchy-nopasswd-expire-1000-ffffffffffffffffffffffffffffffff
[[ $(cat "$(rule_file 1000)") == "$before" ]]
enable_locked 1000 30
renewed=$(cat "$(rule_file 1000)")
[[ $renewed != "$before" ]]
expire_locked 1000
[[ $(cat "$(rule_file 1000)") == "$renewed" ]]
TEST_EXPIRED=1 expire_locked 1000
[[ ! -e $(rule_file 1000) ]]
)
pass "legacy and current callbacks preserve renewed grants and remove expired ones"
(
source "$library"
enable_locked 1000 1
assert_status 2 env TEST_EXPIRED=1 TEST_DELETE_FAIL=1 TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __status 1000
[[ -e $(rule_file 1000) ]]
TEST_EXPIRED=1 assert_status 3 status_locked 1000
[[ ! -e $(rule_file 1000) ]]
)
pass "expired status reports cleanup failure separately from confirmed inactivity"
for failure in TEST_TIMER_FAIL TEST_INACTIVE_TIMER TEST_PUBLISH_FAIL TEST_POST_PUBLISH_FAIL TEST_CANCEL_ENABLE; do
reset_grant
expected=1
if [[ $failure == "TEST_CANCEL_ENABLE" ]]; then expected=143; fi
(
source "$transformed_install"
pre_remove
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
post_remove
) || fail "$package_name removal revokes active passwordless grants"
! find "$removal_sudoers" -name '99-omarchy-nopasswd-*' -print -quit | grep -q . ||
fail "$package_name removal leaves a passwordless grant behind"
[[ -e $removal_sudoers/omarchy-dns ]] ||
fail "$package_name removal deletes an unrelated sudoers policy"
[[ $(readlink "$removal_root/etc/os-release") == ../administrator/os-release ]] ||
fail "$package_name removal changes unrelated OS metadata"
: >"$removal_sudoers/99-omarchy-nopasswd-1001"
(
source "$transformed_install"
post_remove
) || fail "$package_name removal handles administrator OS selector state"
[[ $(readlink "$removal_root/etc/os-release") == ../administrator/os-release ]] ||
fail "$package_name removal overwrites an administrator OS selector"
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1001 ]] ||
fail "$package_name removal grant cleanup depends on OS selector state"
(
source "$transformed_install"
_etc_overrides_apply() { :; }
if post_install; then exit 1; fi
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_root/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
post_install
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_sudoers/99-omarchy-nopasswd-1002"
pre_upgrade
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1002 ]]
post_upgrade
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
) || fail "$package_name restores grant availability only after boot cleanup is installed"
source "$library"
enable_locked 1000 15
assert_status "$expected" env "$failure=1" TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __enable 1000 30
[[ ! -e $(rule_file 1000) ]]
)
done
pass "settings package transitions revoke grants and preserve unrelated configuration"
pass "timer, publication, post-publication and cancellation failures revoke renewed access"
# Exercise the production flock wrapper under contention. mkdir is an atomic
# overlap detector; all workers must enter and leave the protected region.
lock_dir="$test_tmp/lock-runtime"
mkdir "$lock_dir"
lock_lib="$test_tmp/lock-lib.sh"
function_prefix |
sed -e "s#/run/omarchy/sudo-passwordless#$lock_dir#g" \
-e "s#/run/lock/omarchy-sudo-passwordless.lock#$test_tmp/passwordless.lock#g" \
-e 's#/usr/bin/chown root:root "$LOCK_FILE"#/usr/bin/true#' >"$lock_lib"
worker="$test_tmp/worker.sh"
cat >"$worker" <<'WORKER'
#!/bin/bash
set -euo pipefail
source "$LOCK_LIB"
prepare_root_state() { :; }
critical() {
mkdir "$LOCK_SENTINEL"
sleep 0.03
rmdir "$LOCK_SENTINEL"
printf x >>"$LOCK_RESULTS"
}
with_root_lock critical
WORKER
chmod +x "$worker"
for _ in {1..8}; do
LOCK_LIB="$lock_lib" LOCK_SENTINEL="$test_tmp/held" LOCK_RESULTS="$test_tmp/results" bash "$worker" &
done
wait
[[ $(wc -c <"$test_tmp/results") == 8 ]] || fail "concurrent passwordless operations serialize"
pass "passwordless sudo serializes concurrent operations"
reset_grant
(
source "$library"
TEST_POST_PUBLISH_FAIL=1 TEST_DELETE_FAIL=1 assert_status 1 enable_locked 1000 15
[[ -e $(rule_file 1000) ]]
! grep -q '^systemctl stop ' "$test_tmp/commands"
)
pass "failed policy deletion retains the timer and reports failure"
# Same-boot expiry calls the fixed installed cleanup command, and cleanup
# removes policy before touching a timer so timer failures cannot extend it.
grep -F '"$INSTALLED_SELF" __expire "$uid" "$timer"' "$command_path" >/dev/null
cleanup_body=$(awk '/^cleanup_uid_locked\(\) \{/ { in_body=1 } in_body { print } in_body && /^}/ { exit }' "$command_path")
rm_line=$(grep -n '/usr/bin/rm -f' <<<"$cleanup_body" | head -1 | cut -d: -f1)
stop_line=$(grep -n 'stop_timer' <<<"$cleanup_body" | tail -1 | cut -d: -f1)
((rm_line < stop_line)) || fail "expiry removes sudo policy before timer cleanup"
pass "same-boot expiration is fixed-target and fail closed"
reset_grant
(
source "$library"
printf 'audituser ALL=(ALL) NOPASSWD: /usr/bin/true\n' >"$(rule_file 1000)"
cp "$(rule_file 1000)" "$test_tmp/admin-rule"
assert_status 2 status_locked 1000
assert_status 1 enable_locked 1000 15
assert_status 1 cleanup_uid_locked 1000
cmp "$(rule_file 1000)" "$test_tmp/admin-rule"
rm "$(rule_file 1000)"
ln -s "$test_tmp/admin-rule" "$(rule_file 1000)"
assert_status 2 status_locked 1000
assert_status 1 cleanup_uid_locked 1000
[[ -L $(rule_file 1000) ]]
)
pass "grant operations preserve administrator policies and reject symlinks"
reset_grant
(
source "$library"
TEST_BAD_PATH="$test_tmp/etc/sudoers.d" assert_status 1 enable_locked 1000 15
[[ ! -e $(rule_file 1000) ]]
rm "$PACKAGE_HOOK"
assert_status 1 enable_locked 1000 15
)
pass "publication requires trusted paths and the packaged cleanup hook"
reset_grant
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
(
source "$library"
TEST_ACCOUNT='buildbot$' enable_locked 1000 15
read_grant 1000
[[ $GRANT_NAME == 'buildbot$' ]]
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
cleanup_uid_locked 1000
[[ ! -e $(rule_file 1000) ]]
)
pass "trailing-dollar accounts publish and revoke valid native policy"
+114 -213
View File
@@ -2,234 +2,135 @@
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
children=()
cleanup() {
local status=$?
trap - EXIT
if (( ${#children[@]} )); then
kill "${children[@]}" 2>/dev/null || true
wait "${children[@]}" 2>/dev/null || true
fi
rm -rf "$test_tmp"
exit "$status"
}
trap cleanup EXIT
# All policy, state, locks and command mutations stay in this private fixture.
# Native visudo validates inert fragments; no test installs host sudo policy.
mkdir -p "$test_tmp/bin" "$test_tmp/state" "$test_tmp/etc/sudoers.d" "$test_tmp/etc/tmpfiles.d" "$test_tmp/run/lock" "$test_tmp/hooks"
export TEST_GRANT_ROOT="$test_tmp"
cat >"$test_tmp/bin/stat" <<'STUB'
#!/bin/bash
case $2 in
'%u') printf '0\n' ;;
'%a') if [[ -d ${@: -1} ]]; then printf '755\n'; else printf '644\n'; fi ;;
'%u %a') if [[ -d ${@: -1} ]]; then printf '0 755\n'; else printf '0 644\n'; fi ;;
*) exec /usr/bin/stat "$@" ;;
esac
STUB
cat >"$test_tmp/bin/install" <<'STUB'
#!/bin/bash
args=()
while (($#)); do
case $1 in -o|-g) shift 2 ;; *) args+=("$1"); shift ;; esac
done
exec /usr/bin/install "${args[@]}"
STUB
cat >"$test_tmp/bin/rm" <<'STUB'
#!/bin/bash
for path in "$@"; do
if [[ ${TEST_FAIL_TEMP_CLEANUP:-0} == 1 && $path == "$TEST_GRANT_ROOT/state/".sudoers.* ]]; then exit 1; fi
if [[ ${TEST_FAIL_RULE_DELETE:-0} == 1 && $path == "$TEST_GRANT_ROOT/etc/sudoers.d/"* ]]; then exit 1; fi
done
exec /usr/bin/rm "$@"
STUB
cat >"$test_tmp/bin/systemctl" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_GRANT_ROOT/systemctl.log"
exit 0
STUB
chmod +x "$test_tmp/bin/"*
library="$test_tmp/grant-functions.sh"
{
printf 'source %q\n' "$ROOT/bin/omarchy-security-functions"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$ROOT/bin/omarchy-sudo-passwordless"
} | sed \
-e "s|/var/lib/omarchy/sudo-passwordless|$test_tmp/state|g" \
-e "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|g" \
-e "s|/etc/tmpfiles.d|$test_tmp/etc/tmpfiles.d|g" \
-e "s|/usr/share/libalpm/hooks|$test_tmp/hooks|g" \
-e "s|/run/lock/omarchy-sudo-passwordless.lock|$test_tmp/run/lock/omarchy-sudo-passwordless.lock|g" \
-e "s|/run/omarchy-sudo-passwordless-package-removing|$test_tmp/run/omarchy-sudo-passwordless-package-removing|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" \
-e "s|/usr/bin/install|$test_tmp/bin/install|g" \
-e "s|/usr/bin/rm|$test_tmp/bin/rm|g" \
-e "s|/usr/bin/systemctl|$test_tmp/bin/systemctl|g" \
-e 's|/usr/bin/chown|/usr/bin/true|g' >"$library"
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
printf 'r! /etc/sudoers.d/99-omarchy-nopasswd-*\n' >"$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
# The expected policy text is mapped along with its filename in this fixture.
sed -i "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|" "$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
source "$SHELL_TEST_DIR/fixtures/passwordless-sudo-test.sh"
(
source "$library"
for name in 'buildbot$' audituser aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
valid_account_name "$name" || fail "supported account name rejected: $name"
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$name" >"$test_tmp/name-policy"
/usr/sbin/visudo -cf "$test_tmp/name-policy" >/dev/null
done
for name in 'a$b' '$' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
! valid_account_name "$name" || fail "invalid account name accepted"
done
! valid_uid 18446744073709551617 || fail "overflowed UID accepted"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file 1000)"
printf 'buildbot$ ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot$"
remove_known_legacy_rules
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot\$ ]]
) || fail "supported account names and legacy cleanup disagree"
pass "provisioning-compatible names validate as sudoers and clean up correctly"
printf 'admin ALL=(ALL) NOPASSWD: /usr/bin/true\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-custom"
TEST_DELETE_FAIL=1 assert_status 1 cleanup_all_locked
[[ -e $(rule_file 1000) ]]
cleanup_all_locked
[[ ! -e $(rule_file 1000) && -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-custom ]]
! compgen -G "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot*"
)
pass "legacy cleanup removes generated orphan rules and preserves custom policy"
transaction_setup() {
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
start_expiry_timer() { printf '%s\n' "$3" >>"$test_tmp/armed"; }
stop_timer() { printf '%s\n' "$1" >>"$test_tmp/stopped"; }
# Run the actual migration queue for separate temporary homes. Sudo only calls
# the mapped helper and can be refused without requesting host authorization.
mkdir -p "$test_tmp/source/migrations"
sed "s|/usr/bin/omarchy-sudo-passwordless|$test_tmp/omarchy-sudo-passwordless|g" \
"$ROOT/migrations/1788163635.sh" >"$test_tmp/source/migrations/1788163635.sh"
printf 'echo "later migration ran"\n' >"$test_tmp/source/migrations/1788163636.sh"
run_migrations() {
TEST_MIGRATION=1 OMARCHY_PATH="$test_tmp/source" OMARCHY_MIGRATION_STATE="$test_tmp/$1" \
PATH="$test_tmp/bin:$PATH" /usr/bin/bash "$ROOT/bin/omarchy-migrate" >"$test_tmp/migrations.log" 2>&1
}
marker="$test_tmp/var/lib/omarchy/migrations/1788163635"
(
source "$library"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file 1000)"
TEST_DELETE_FAIL=1 assert_status 1 run_migrations first
[[ ! -e $marker && ! -e $test_tmp/first/1788163636.sh ]]
run_migrations first
[[ -f $marker && -f $test_tmp/first/1788163636.sh ]]
enable_locked 1000 15
cp "$(rule_file 1000)" "$test_tmp/renewed"
: >"$test_tmp/commands"
TEST_NO_SUDO=1 run_migrations second
[[ -f $test_tmp/second/1788163636.sh ]]
! grep -q '^sudo ' "$test_tmp/commands"
cmp "$(rule_file 1000)" "$test_tmp/renewed"
)
pass "migration completion is machine-wide, retryable, and needs no sudo for later users"
(
source "$library"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) && ! -e $(state_file 1000) && -s $test_tmp/stopped ]]
) || fail "post-publication cleanup failure did not revoke before timer cleanup"
pass "failed temporary cleanup after publication revokes the live policy"
rm -f "$test_tmp/stopped"
(
source "$library"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 TEST_FAIL_RULE_DELETE=1 enable_locked 1000 15 && exit 1
[[ -f $(rule_file 1000) && -f $(state_file 1000) && ! -e $test_tmp/stopped ]]
if TEST_FAIL_RULE_DELETE=1 revoke_inactive_grant 1000; then exit 1; else status=$?; fi
(( status == 2 ))
) || fail "failed policy revocation disarmed expiry or claimed inactive status"
pass "failed revocation preserves expiry jobs and returns a distinct error"
(
source "$library"
transaction_setup
current_timer=$(read_state_timer 1000)
expire_locked 1000 omarchy-nopasswd-expire-1000-ffffffffffffffffffffffffffffffff
[[ -f $(rule_file 1000) ]]
expire_locked 1000
[[ -f $(rule_file 1000) ]]
expire_locked 1000 "$current_timer"
[[ ! -e $(rule_file 1000) ]]
) || fail "a predecessor timer invalidates its replacement"
pass "old and legacy timer callbacks preserve a newer valid grant"
(
source "$library"
transaction_setup
start_expiry_timer() {
: >"$REMOVAL_BLOCKER"
return 0
}
enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) ]]
) || fail "publication ignores a lost package prerequisite"
rm "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
pass "grant publication rechecks package availability after timer setup"
TEST_BAD_PATH="$marker" assert_status 1 migration_complete
rm "$marker"
ln -s "$test_tmp/renewed" "$marker"
assert_status 1 migration_complete
assert_status 1 migrate_locked
[[ -L $marker ]]
rm "$marker"
)
pass "migration checks marker ownership and rejects symlinks"
# Keep real package scripts in the contract: source and packaging share the
# same lock and blocker, including the legacy scriptlet fallback.
pkgs_path=${OMARCHY_PKGS_PATH:-$ROOT/../omarchy-pkgs}
[[ ! -d $pkgs_path/pkgbuilds ]] || pkgs_path=$pkgs_path/pkgbuilds
package_script="$pkgs_path/omarchy-settings/omarchy-settings.install"
[[ -f $package_script ]] || fail "package checkout is required for shared lifecycle coverage"
sed -e "s|/etc/|$test_tmp/etc/|g" \
-e "s|/run|$test_tmp/run|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" \
-e "s|/usr/bin/rm|$test_tmp/bin/rm|g" "$package_script" >"$test_tmp/package.install"
for name in omarchy-settings omarchy-settings-dev; do
script="$pkgs_path/$name/$name.install"
[[ -f $script ]] || fail "set OMARCHY_PKGS_PATH to the companion package checkout"
sed -e "s|/etc/|$test_tmp/etc/|g" -e "s|/run|$test_tmp/run|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" -e "s|/usr/bin/rm|$test_tmp/bin/rm|g" \
"$script" >"$test_tmp/$name.install"
reset_grant
(
source "$library"
source "$test_tmp/$name.install"
_etc_overrides_apply() { :; }
enable_locked 1000 15
TEST_DELETE_FAIL=1 assert_status 1 pre_remove
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
assert_status 1 enable_locked 1000 15
pre_remove && post_remove
[[ ! -e $(rule_file 1000) ]]
post_install
[[ ! -e $REMOVAL_BLOCKER ]]
enable_locked 1000 15
pre_upgrade && post_upgrade
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
)
done
pass "both settings packages revoke grants, block publication, and recover on installation"
worker="$test_tmp/publisher.sh"
{
printf '#!/bin/bash\nset -euo pipefail\nsource %q\n' "$library"
declare -f transaction_setup
printf 'test_tmp=%q\ntransaction_setup\n' "$test_tmp"
cat <<'WORKER'
publish_rule() {
: >"$test_tmp/publisher.entered"
while [[ ! -e $test_tmp/publisher.release ]]; do sleep 0.02; done
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file "$1")"
reset_grant
# Hold the source lock, then start package removal. A native flock on the
# mapped file must serialize both implementations.
cat >"$test_tmp/worker" <<'WORKER'
#!/bin/bash
set -euo pipefail
source "$TEST_LIBRARY"
critical() {
touch "$TEST_GRANT_ROOT/entered"
for (( attempt=0; attempt<500; attempt++ )); do
[[ ! -e $TEST_GRANT_ROOT/release ]] || break
sleep 0.01
done
[[ -e $TEST_GRANT_ROOT/release ]] || return 1
enable_locked 1000 15
}
with_root_lock enable_locked 1000 15
with_root_lock critical
WORKER
} >"$worker"
bash "$worker" >"$test_tmp/publisher.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/publisher.entered ]] || break
sleep 0.02
TEST_LIBRARY="$library" /usr/bin/bash "$test_tmp/worker" >"$test_tmp/publisher.log" 2>&1 &
publisher=$!
children+=("$publisher")
for (( attempt=0; attempt<200; attempt++ )); do
[[ ! -e $test_tmp/entered ]] || break
sleep 0.01
done
[[ -e $test_tmp/publisher.entered ]] || fail "grant publisher did not enter the shared lock"
bash -euo pipefail -c 'source "$1"; : >"$2"; pre_remove; post_remove' bash \
"$test_tmp/package.install" "$test_tmp/removal.started" >"$test_tmp/removal.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/removal.started ]] || break
sleep 0.02
done
[[ -e $test_tmp/removal.started ]] || fail "package removal did not start"
touch "$test_tmp/publisher.release"
for child in "${children[@]}"; do wait "$child" || fail "shared lifecycle worker failed"; done
[[ -f $test_tmp/entered ]] || fail "publisher failed to acquire the lock"
/usr/bin/bash -euo pipefail -c 'source "$1"; pre_remove; post_remove' bash "$test_tmp/omarchy-settings.install" >"$test_tmp/removal.log" 2>&1 &
removal=$!
children+=("$removal")
touch "$test_tmp/release"
wait "$publisher" || fail "publisher failed" "$(cat "$test_tmp/publisher.log")"
wait "$removal" || fail "removal failed" "$(cat "$test_tmp/removal.log")"
children=()
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]] || fail "removal left a concurrently published grant"
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || fail "removal did not block later publication"
(
source "$library"
transaction_setup
! with_root_lock enable_locked 1000 15
) || fail "a publisher can create a grant after package removal begins"
pass "package removal shares the grant lock and blocks later publication"
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]]
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]]
pass "native lock serializes grant publication with package removal"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000"
if TEST_FAIL_RULE_DELETE=1 bash -euo pipefail -c 'source "$1"; post_remove' bash "$test_tmp/package.install" >"$test_tmp/removal-failure.output" 2>&1; then
fail "package removal hid a failed policy deletion"
fi
grep -q 'Administrator cleanup is required' "$test_tmp/removal-failure.output" || fail "package deletion failure lacks recovery guidance"
pass "package removal reports cleanup failures instead of successful revocation"
(
source "$library"
transaction_setup
rm -f "$REMOVAL_BLOCKER"
enable_locked 1000 5
record=$(read_state_record 1000)
expiry=${record#*$'\t'}
expiry=${expiry%%$'\t'*}
deadline=$(/usr/bin/date -u -d "@$expiry" +%Y%m%d%H%M%SZ)
[[ $(cat "$(rule_file 1000)") == "audituser ALL=(ALL) NOTAFTER=$deadline NOPASSWD: ALL" ]]
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
classify_generated_rule "$(rule_file 1000)"
rm -f "$(state_file 1000)"
remove_known_legacy_rules
[[ ! -e $(rule_file 1000) ]]
) || fail "native sudo deadline or state-independent bounded rule cleanup is incorrect"
pass "sudo policy contains the same deadline and bounded orphan rules are recognized"
(
source "$library"
transaction_setup
rm -f "$REMOVAL_BLOCKER"
enable_locked 1000 5
if TEST_FAIL_RULE_DELETE=1 package_removing_locked; then exit 1; fi
[[ -f $REMOVAL_BLOCKER && -f $(rule_file 1000) ]]
! enable_locked 1000 5
package_removing_locked
[[ ! -e $(rule_file 1000) ]]
rm -f "$REMOVAL_BLOCKER" "$PACKAGE_HOOK"
! enable_locked 1000 5
) || fail "pre-transaction revocation error or missing hook does not prevent new grants"
pass "package hook fails closed and grants require its installed policy"
# systemd-tmpfiles operates on an explicit disposable root, never the host.
reset_grant
: >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000"
: >"$test_tmp/etc/sudoers.d/unrelated"
rule='r! /etc/sudoers.d/99-omarchy-nopasswd-*'
/usr/bin/systemd-tmpfiles --root="$test_tmp" --remove --inline "$rule"
[[ -f $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]] || fail "routine tmpfiles shortened a live grant"
/usr/bin/systemd-tmpfiles --root="$test_tmp" --remove --boot --inline "$rule"
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 && -f $test_tmp/etc/sudoers.d/unrelated ]] || fail "boot cleanup boundary"
pass "native boot cleanup removes grants while routine tmpfiles preserves them"