Integrate the shared sudo lifecycle foundation

This commit is contained in:
Afonso Oliveira committed 2026-09-07 23:10:29 +01:00
commit 75e58f034f
26 files changed
+1596 -164

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ Three documentation trees, split by genre and audience:
- Prefer `(( ))` over numeric operators inside `[[ ]]` (e.g., `(( count < 50 ))`, not `[[ $count -lt 50 ]]`)
- Prefer a full `if`/`else` conditional for simple two-path control flow; don't rely on `exec` or `exit` in one branch to make following statements unreachable
- For strings/paths with spaces, quote them instead of escaping spaces with `\ ` (e.g., `"$APP_DIR/Disk Usage.desktop"`, not `$APP_DIR/Disk\ Usage.desktop`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`). A security entrypoint may use `#!/bin/bash -p` when it must suppress inherited startup code before its first command; document the boundary and test rejection of ordinary Bash with a decoy `-p` argument.
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`). A security-sensitive entrypoint may use the exact `#!/bin/bash -p` form only when it must suppress `BASH_ENV` and exported-function startup injection before its first command; that exception must be explained at the boundary and covered by a regression that rejects an ordinary Bash launch with a decoy `-p` argument.
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs
# Command Naming
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# omarchy:summary=Install T3 Code and point it at the Omarchy palette
# omarchy:requires-sudo=true
set -e
T3CODE_HOME="${T3CODE_HOME:-$HOME/.t3}"
T3CODE_STATE_DIR="$T3CODE_HOME/userdata"
echo "Installing T3 Code..."
omarchy-pkg-add t3code-bin
echo "Matching T3 Code to the current theme..."
mkdir -p "$T3CODE_STATE_DIR"
# An updated user's current theme may have been staged before this template existed.
if [[ ! -f $HOME/.local/state/omarchy/current/theme/t3code.json ]]; then
omarchy-theme-refresh
fi
omarchy-theme-set-t3code
# The packaged CLI selects the published palette before the app's first launch.
t3 theme set omarchy --base-dir "$T3CODE_HOME"
echo "Opening T3 Code..."
setsid uwsm-app -- gtk-launch t3code >/dev/null 2>&1 &
echo ""
echo "T3 Code has been installed."
+4 -1
View File
@@ -4,8 +4,11 @@
set -e
# 1Password reads the display scale itself, the way Electron apps do, and comes
# up oversized next to every other window on a scaled monitor. The packaged
# .desktop pins it too; this covers the hotkey, which runs the binary directly.
if omarchy-cmd-present 1password; then
exec setsid uwsm-app -- 1password
exec setsid uwsm-app -- 1password --force-device-scale-factor=1
else
exec omarchy-launch-floating-terminal-with-presentation omarchy-install-service-1password
fi
Regular → Executable
+34 -2
View File
@@ -70,9 +70,11 @@ omarchy_security_revoke_sudo_timestamp() {
omarchy_security_exit_with_revoked_sudo() {
local status=$1
local message=${2:-Could not invalidate cached sudo authorization.}
trap - EXIT HUP INT TERM
if ! omarchy_security_revoke_sudo_timestamp; then
echo "Could not invalidate cached sudo authorization." >&2
echo "$message" >&2
(( status != 0 )) || status=1
fi
exit "$status"
@@ -85,7 +87,8 @@ omarchy_security_install_signal_exit_traps() {
}
omarchy_security_install_sudo_cleanup_traps() {
trap 'omarchy_security_exit_with_revoked_sudo "$?"' EXIT
OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE=${1:-Could not invalidate cached sudo authorization.}
trap omarchy_security_run_sudo_cleanup_trap EXIT
omarchy_security_install_signal_exit_traps
}
@@ -103,3 +106,32 @@ omarchy_security_enable_no_update_sudo() {
OMARCHY_SUDO_NO_UPDATE=1
export PATH OMARCHY_SUDO_NO_UPDATE
}
omarchy_security_run_sudo_cleanup_trap() {
local status=$?
omarchy_security_exit_with_revoked_sudo "$status" \
"${OMARCHY_SECURITY_SUDO_CLEANUP_MESSAGE:-Could not invalidate cached sudo authorization.}"
}
omarchy_security_assert_root_directory() {
local path=$1 expected_mode=$2 canonical owner actual_mode
[[ $path == /* && -d $path && ! -L $path ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$path") || return 1
[[ $canonical == "$path" ]] || return 1
read -r owner actual_mode < <(/usr/bin/stat -Lc '%u %a' -- "$path") || return 1
[[ $owner == "0" && $actual_mode == "$expected_mode" ]]
}
omarchy_security_prepare_private_root_directory() {
local path=$1 parent=$2
omarchy_security_assert_root_directory "$parent" 755 || return 1
if [[ -e $path || -L $path ]]; then
omarchy_security_assert_root_directory "$path" 700
else
/usr/bin/install -d -o root -g root -m 0700 -- "$path" || return 1
omarchy_security_assert_root_directory "$path" 700
fi
}
+540 -38
View File
@@ -1,70 +1,572 @@
#!/bin/bash
#!/bin/bash -p
# omarchy:summary=Toggle passwordless sudo for the current user.
# omarchy:args=[MINUTES]
# omarchy:requires-sudo=true
NOPASSWD_FILE="/etc/sudoers.d/99-omarchy-nopasswd-${USER}"
TIMER_NAME="omarchy-nopasswd-expire-${USER}"
MINUTES=${1:-15}
if [[ $1 && ! $1 =~ ^[0-9]+$ ]]; then
echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
exit 1
if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
echo "Refusing an unsafe Bash startup for passwordless sudo." >&2
exit 126
fi
arm_expiry() {
if sudo systemd-run --on-active=${MINUTES}m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \
rm -f -- "$NOPASSWD_FILE"; then
return 0
fi
security_entrypoint=$(/usr/bin/readlink -e -- "${BASH_SOURCE[0]}") || exit 126
source "${security_entrypoint%/*}/omarchy-security-functions" || exit 126
echo "Failed to schedule passwordless sudo expiry. Revoking access now." >&2
if ! sudo rm -f -- "$NOPASSWD_FILE"; then
echo "CRITICAL: Could not remove $NOPASSWD_FILE. Remove it as root immediately." >&2
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
omarchy_security_require_privileged_bash_startup || {
echo "Refusing an unsafe Bash startup for passwordless sudo." >&2
exit 126
}
omarchy_security_sanitize_bash_environment "$0" "$@" || exit 126
fi
set -euo pipefail
readonly DEFAULT_MINUTES=15
readonly MAX_MINUTES=1440
readonly STATE_DIR=/var/lib/omarchy/sudo-passwordless
readonly RUNTIME_DIR=/run/omarchy/sudo-passwordless
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
readonly PACKAGE_HOOK=/usr/share/libalpm/hooks/05-omarchy-passwordless-revoke.hook
readonly REMOVAL_BLOCKER=/run/omarchy-sudo-passwordless-package-removing
readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
readonly STATUS_INACTIVE=3
usage() {
echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
echo "MINUTES must be between 1 and $MAX_MINUTES." >&2
exit 1
}
valid_minutes() {
[[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES))
}
valid_uid() {
[[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294))
}
valid_account_name() {
[[ $1 =~ ^[a-z_][a-z0-9_-]{0,31}\$?$ ]] && (( ${#1} <= 32 ))
}
resolve_account() {
local uid="$1" entry
valid_uid "$uid" || return 1
entry=$(/usr/bin/getent passwd "$((10#$uid))") || return 1
IFS=: read -r ACCOUNT_NAME _ ACCOUNT_UID _ _ _ _ <<<"$entry"
[[ $ACCOUNT_UID == "$((10#$uid))" ]] || return 1
# Sudoers names and the legacy filename both have metacharacters. Omarchy
# accounts use this portable subset; refusing anything else is safer than
# attempting to quote privileged policy syntax.
valid_account_name "$ACCOUNT_NAME" || return 1
ACCOUNT_UID=$((10#$uid))
}
verify_sudo_caller() {
local requested_uid="$1"
((EUID == 0)) || return 1
valid_uid "$requested_uid" || return 1
[[ ${SUDO_UID:-} =~ ^[0-9]+$ ]] || return 1
((10#$SUDO_UID == 10#$requested_uid)) || return 1
resolve_account "$requested_uid"
}
prepare_root_state() {
omarchy_security_assert_root_directory /var 755 || return 1
[[ -d /var/lib && ! -L /var/lib ]] || return 1
[[ $(/usr/bin/stat -Lc '%u' /var/lib) == 0 ]] || return 1
! ((8#$(/usr/bin/stat -Lc '%a' /var/lib) & 022)) || return 1
if [[ ! -e /var/lib/omarchy && ! -L /var/lib/omarchy ]]; then
/usr/bin/install -d -o root -g root -m 0755 /var/lib/omarchy || return 1
fi
omarchy_security_assert_root_directory /var/lib/omarchy 755 || return 1
omarchy_security_prepare_private_root_directory "$STATE_DIR" /var/lib/omarchy || return 1
omarchy_security_assert_root_directory /run 755 || return 1
if [[ ! -e /run/omarchy && ! -L /run/omarchy ]]; then
/usr/bin/install -d -o root -g root -m 0755 /run/omarchy || return 1
fi
omarchy_security_assert_root_directory /run/omarchy 755 || return 1
omarchy_security_prepare_private_root_directory "$RUNTIME_DIR" /run/omarchy
}
with_root_lock() {
local fd rc=0
# The boot cleanup cannot depend on STATE_DIR or RUNTIME_DIR being healthy:
# those are exactly the kinds of partial-install state it must fail closed
# through. /run/lock is established by the OS before sysinit services run.
omarchy_security_assert_root_directory /run 755 || return 1
[[ -d /run/lock && ! -L /run/lock ]] || return 1
[[ $(/usr/bin/stat -Lc '%u' /run/lock) == 0 ]] || return 1
! ((8#$(/usr/bin/stat -Lc '%a' /run/lock) & 022)) || return 1
exec {fd}>"$LOCK_FILE" || return 1
/usr/bin/chown root:root "$LOCK_FILE" || return 1
/usr/bin/chmod 0600 "$LOCK_FILE" || return 1
/usr/bin/flock -x "$fd" || return 1
"$@" || rc=$?
/usr/bin/flock -u "$fd" || rc=1
exec {fd}>&-
return "$rc"
}
rule_file() {
printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$1"
}
state_file() {
printf '%s/%s.state' "$STATE_DIR" "$1"
}
read_state_record() {
local uid="$1" file state_uid name expires timer canonical_uid
local -a lines=()
valid_uid "$uid" || return 1
canonical_uid=$((10#$uid))
file=$(state_file "$uid")
[[ -f $file && ! -L $file ]] || return 1
mapfile -t lines <"$file" || return 1
(( ${#lines[@]} == 4 )) || return 1
[[ ${lines[0]} == UID=* && ${lines[1]} == USER=* &&
${lines[2]} == EXPIRES=* && ${lines[3]} == TIMER=* ]] || return 1
state_uid=${lines[0]#UID=}
name=${lines[1]#USER=}
expires=${lines[2]#EXPIRES=}
timer=${lines[3]#TIMER=}
[[ $state_uid == "$canonical_uid" ]] || return 1
valid_account_name "$name" || return 1
[[ $expires =~ ^[1-9][0-9]{0,10}$ ]] || return 1
[[ $timer =~ ^omarchy-nopasswd-expire-${canonical_uid}-[0-9a-f]{32}$ ]] || return 1
printf '%s\t%s\t%s' "$name" "$expires" "$timer"
}
read_state_timer() {
local record
record=$(read_state_record "$1") || return 1
printf '%s' "${record##*$'\t'}"
}
current_epoch() {
local now
now=$(/usr/bin/date +%s) || return 1
[[ $now =~ ^[1-9][0-9]{0,10}$ ]] || return 1
printf '%s' "$now"
}
valid_expiry() {
[[ $1 =~ ^[1-9][0-9]{0,10}$ ]]
}
valid_timer_for_uid() {
local uid="$1" timer="$2"
valid_uid "$uid" || return 1
uid=$((10#$uid))
[[ $timer =~ ^omarchy-nopasswd-expire-${uid}-[0-9a-f]{32}$ ]]
}
stop_timer() {
local timer="$1"
[[ $timer =~ ^omarchy-nopasswd-expire-[0-9]+-[0-9a-f]{32}$ ]] || return 0
/usr/bin/systemctl stop "${timer}.timer" "${timer}.service" >/dev/null 2>&1 || true
/usr/bin/systemctl reset-failed "${timer}.timer" "${timer}.service" >/dev/null 2>&1 || true
}
classify_generated_rule() {
local file=$1 suffix contents name
GENERATED_RULE_LEGACY_TIMER=""
[[ -f $file && ! -L $file ]] || return 1
contents=$(/usr/bin/cat -- "$file") || return 2
suffix=${file##*/99-omarchy-nopasswd-}
if [[ $suffix =~ ^[0-9]+$ ]]; then
name=${contents%' ALL=(ALL) NOPASSWD: ALL'}
if valid_account_name "$name" && [[ $contents == "$name ALL=(ALL) NOPASSWD: ALL" ]]; then
return 0
fi
name=${contents%%' ALL=(ALL) NOTAFTER='*}
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
elif valid_account_name "$suffix" && [[ $contents == "$suffix ALL=(ALL) NOPASSWD: ALL" ]]; then
GENERATED_RULE_LEGACY_TIMER="omarchy-nopasswd-expire-${suffix}"
else
return 1
fi
}
remove_known_legacy_rules() {
local file classification failed=0
shopt -s nullglob
for file in /etc/sudoers.d/99-omarchy-nopasswd-*; do
if classify_generated_rule "$file"; then
# A crash after publishing the numeric rule but before its state rename
# must not survive the next boot. Do not require the account to still
# exist: a deleted account could otherwise make the rule immortal and a
# later username reuse could activate it again.
if /usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]; then
[[ -z $GENERATED_RULE_LEGACY_TIMER ]] ||
/usr/bin/systemctl stop "${GENERATED_RULE_LEGACY_TIMER}.timer" \
"${GENERATED_RULE_LEGACY_TIMER}.service" >/dev/null 2>&1 || true
else
failed=1
fi
else
classification=$?
# An unreadable candidate cannot be proven inert. A symlink, non-file,
# or administrator-authored body is unrelated and remains untouched.
(( classification == 1 )) || failed=1
fi
done
shopt -u nullglob
return "$failed"
}
cleanup_uid_locked() {
local uid="$1" timer=""
valid_uid "$uid" || return 1
timer=$(read_state_timer "$uid" 2>/dev/null || true)
# Remove policy first. A failed timer stop can only leave an inert cleanup
# job behind, never extend passwordless access.
/usr/bin/rm -f -- "$(rule_file "$uid")" || return 1
[[ ! -e $(rule_file "$uid") && ! -L $(rule_file "$uid") ]] || return 1
/usr/bin/rm -f -- "$(state_file "$uid")" || return 1
[[ -z $timer ]] || stop_timer "$timer"
}
cleanup_all_locked() {
local state uid failed=0 file classification
shopt -s nullglob
for state in "$STATE_DIR"/*.state; do
uid=${state##*/}
uid=${uid%.state}
if valid_uid "$uid" && ! cleanup_uid_locked "$uid"; then failed=1; fi
done
shopt -u nullglob
remove_known_legacy_rules || failed=1
# Never report a successful boot cleanup while an exact rule emitted by any
# Omarchy implementation is still active. Administrator-extended files do
# not match these complete bodies and remain untouched.
shopt -s nullglob
for file in /etc/sudoers.d/99-omarchy-nopasswd-*; do
if classify_generated_rule "$file"; then
failed=1
else
classification=$?
(( classification == 1 )) || failed=1
fi
done
shopt -u nullglob
return "$failed"
}
verify_root_policy_file() {
local file=$1 owner mode canonical current
[[ -f $file && ! -L $file ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$file") || return 1
[[ $canonical == "$file" ]] || return 1
owner=$(/usr/bin/stat -Lc '%u' -- "$file") || return 1
mode=$(/usr/bin/stat -Lc '%a' -- "$file") || return 1
[[ $owner == 0 && $mode =~ ^[0-7]+$ ]] && ! ((8#$mode & 022)) || return 1
current=${file%/*}
while :; do
[[ -d $current && ! -L $current ]] || return 1
canonical=$(/usr/bin/realpath -e -- "$current") || return 1
[[ $canonical == "$current" ]] || return 1
read -r owner mode < <(/usr/bin/stat -Lc '%u %a' -- "$current") || return 1
[[ $owner == 0 && $mode =~ ^[0-7]+$ ]] && ! ((8#$mode & 022)) || return 1
[[ $current == / ]] && break
current=${current%/*}
[[ -n $current ]] || current=/
done
}
verify_boot_cleanup() {
local active_rules hook
[[ ! -e $REMOVAL_BLOCKER && ! -L $REMOVAL_BLOCKER ]] || return 1
verify_root_policy_file "$BOOT_CLEANUP_FILE" || return 1
active_rules=$(/usr/bin/awk '!/^[[:space:]]*(#|$)/ { print }' "$BOOT_CLEANUP_FILE") || return 1
[[ $active_rules == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]] || return 1
verify_root_policy_file "$PACKAGE_HOOK" || return 1
hook=$(/usr/bin/cat -- "$PACKAGE_HOOK") || return 1
[[ $hook == '[Trigger]
Operation = Upgrade
Operation = Remove
Type = Package
Target = omarchy-settings
Target = omarchy-settings-dev
[Action]
Description = Revoking temporary Omarchy sudo grants before settings changes...
When = PreTransaction
Exec = /usr/bin/omarchy-sudo-passwordless __package-removing
AbortOnFail' ]]
}
package_removing_locked() {
# ALPM must abort before removing the helper or boot cleanup if revocation
# fails. The marker also blocks publication after this lock is released.
(umask 077; : >"$REMOVAL_BLOCKER") || return 1
/usr/bin/rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-* || return 1
cleanup_all_locked
}
prepare_state_file() {
local uid="$1" name="$2" expires="$3" timer="$4" tmp
tmp=$(/usr/bin/mktemp "$STATE_DIR/.state.XXXXXX") || return 1
if ! /usr/bin/printf 'UID=%s\nUSER=%s\nEXPIRES=%s\nTIMER=%s\n' \
"$uid" "$name" "$expires" "$timer" >"$tmp" ||
! /usr/bin/chown root:root "$tmp" || ! /usr/bin/chmod 0600 "$tmp"; then
/usr/bin/rm -f -- "$tmp"
return 1
fi
printf '%s' "$tmp"
}
start_expiry_timer() {
local uid="$1" expires="$2" timer="$3"
valid_uid "$uid" && valid_expiry "$expires" && valid_timer_for_uid "$uid" "$timer" || return 1
# Calendar timers use CLOCK_REALTIME and catch up immediately after resume;
# a monotonic OnActiveSec timer pauses while the machine is suspended.
/usr/bin/systemd-run --quiet --collect --on-calendar="@${expires}" \
--timer-property=AccuracySec=1s --unit="$timer" \
-- "$INSTALLED_SELF" __expire "$uid" "$timer" || return 1
/usr/bin/systemctl is-active --quiet "${timer}.timer"
}
publish_rule() {
local uid="$1" name="$2" expires="$3" destination tmp deadline
valid_expiry "$expires" || return 1
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
[[ $deadline =~ ^[0-9]{14}Z$ ]] || return 1
destination=$(rule_file "$uid")
tmp=$(/usr/bin/mktemp "$STATE_DIR/.sudoers.XXXXXX") || return 1
if ! /usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$name" "$deadline" >"$tmp" ||
! /usr/bin/chown root:root "$tmp" || ! /usr/bin/chmod 0440 "$tmp" ||
! /usr/sbin/visudo -cf "$tmp" >/dev/null ||
! /usr/bin/install -o root -g root -m 0440 -- "$tmp" "$destination"; then
/usr/bin/rm -f -- "$tmp"
return 1
fi
/usr/bin/rm -f -- "$tmp"
}
abort_enable_locked() {
local uid=$1 timer=$2 old_timer=$3 pending_state=$4
# Publication can install policy and then fail while cleaning its temporary
# file. Never disarm either expiry job until policy revocation is confirmed.
if cleanup_uid_locked "$uid"; then
stop_timer "$timer"
[[ -z $old_timer ]] || stop_timer "$old_timer"
else
echo "Could not revoke passwordless sudo after a failed grant; expiry jobs remain armed. Administrator cleanup is required." >&2
fi
/usr/bin/rm -f -- "$pending_state" || true
return 1
}
echo "Toggle passwordless sudo..."
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
}
# Safety: if the file exists but the timer doesn't (e.g. after reboot), clean up
if sudo test -f "$NOPASSWD_FILE" && ! systemctl is-active "${TIMER_NAME}.timer" &>/dev/null; then
sudo rm "$NOPASSWD_FILE"
fi
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))
# Check for the file directly — sudo -n can stay cached or be granted by other rules
if sudo test -f "$NOPASSWD_FILE"; then
if [[ $1 ]]; then
sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null
arm_expiry || exit 1
echo "Passwordless sudo timer updated. It will now automatically disable in ${MINUTES} minutes."
# State and a verified timer exist before the policy becomes reachable. If
# publication fails, cleanup removes both. During an update the old timer is
# deliberately kept until the replacement is active, so failure shortens the
# grant rather than extending it.
pending_state=$(prepare_state_file "$uid" "$ACCOUNT_NAME" "$expires" "$timer") || return 1
if ! start_expiry_timer "$uid" "$expires" "$timer"; then
abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
return 1
fi
if ! /usr/bin/mv -fT -- "$pending_state" "$(state_file "$uid")"; then
abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
return 1
fi
if ! verify_boot_cleanup || ! publish_rule "$uid" "$ACCOUNT_NAME" "$expires"; then
abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
return 1
fi
now=$(current_epoch) || {
abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
return 1
}
if ((10#$now >= 10#$expires)) || ! /usr/bin/systemctl is-active --quiet "${timer}.timer" || ! verify_boot_cleanup; then
# The timer may have expired or failed between its initial verification and
# rule publication. Revoke synchronously so a suspended or heavily loaded
# machine cannot turn a short grant into a reboot-long one.
abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
return 1
fi
[[ -z $old_timer || $old_timer == "$timer" ]] || stop_timer "$old_timer"
}
status_locked() {
local uid="$1" record state_name expires timer now remainder
resolve_account "$uid" || return 2
if [[ ! -e $(rule_file "$uid") && ! -L $(rule_file "$uid") ]]; then
return "$STATUS_INACTIVE"
fi
record=$(read_state_record "$uid") || {
revoke_inactive_grant "$uid"
return $?
}
state_name=${record%%$'\t'*}
remainder=${record#*$'\t'}
expires=${remainder%%$'\t'*}
timer=${record##*$'\t'}
[[ $state_name == "$ACCOUNT_NAME" ]] || {
revoke_inactive_grant "$uid"
return $?
}
now=$(current_epoch) || {
revoke_inactive_grant "$uid"
return $?
}
((10#$now < 10#$expires)) || {
revoke_inactive_grant "$uid"
return $?
}
/usr/bin/systemctl is-active --quiet "${timer}.timer" || {
revoke_inactive_grant "$uid"
return $?
}
}
revoke_inactive_grant() {
if cleanup_uid_locked "$1"; then
return "$STATUS_INACTIVE"
else
sudo rm "$NOPASSWD_FILE"
sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null
echo "Could not revoke invalid or expired passwordless sudo. Administrator cleanup is required." >&2
return 2
fi
}
expire_locked() {
local uid=$1 timer=${2:-} current_timer status
if [[ -n $timer ]]; then
current_timer=$(read_state_timer "$uid" 2>/dev/null || true)
# A delayed predecessor must not revoke a newer, independently timed grant.
[[ -z $current_timer || $current_timer == "$timer" ]] || return 0
cleanup_uid_locked "$uid"
elif status_locked "$uid"; then
# Compatibility with already scheduled UID-only jobs: enforce the current
# grant's expiry instead of letting an old timer shorten its replacement.
return 0
else
status=$?
(( status == STATUS_INACTIVE ))
fi
}
root_dispatch() {
local action="$1"
shift
case "$action" in
__status)
(($# == 1)) && verify_sudo_caller "$1" || return 2
with_root_lock status_locked "$1"
;;
__enable)
(($# == 2)) && verify_sudo_caller "$1" && valid_minutes "$2" || return 1
with_root_lock enable_locked "$1" "$2"
;;
__disable)
(($# == 1)) && verify_sudo_caller "$1" || return 1
with_root_lock cleanup_uid_locked "$1"
;;
__expire)
(($# == 1 || $# == 2)) && ((EUID == 0)) && valid_uid "$1" || return 1
[[ -z ${2:-} ]] || valid_timer_for_uid "$1" "$2" || return 1
with_root_lock expire_locked "$@"
;;
__cleanup-all)
(($# == 0)) && ((EUID == 0)) || return 1
with_root_lock cleanup_all_locked
;;
__package-removing)
(($# == 0)) && ((EUID == 0)) || return 1
with_root_lock package_removing_locked
;;
*) return 1 ;;
esac
}
case "${1:-}" in
__status|__enable|__disable|__expire|__cleanup-all|__package-removing)
action=$1
shift
root_dispatch "$action" "$@"
exit
;;
esac
(($# <= 1)) || usage
minutes=${1:-$DEFAULT_MINUTES}
valid_minutes "$minutes" || usage
uid=$(/usr/bin/id -u)
valid_uid "$uid" || {
echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2
exit 1
}
omarchy_security_sudo_supports_no_update || {
echo "This sudo does not support --no-update; refusing the passwordless-sudo workflow." >&2
exit 1
}
omarchy_security_install_sudo_cleanup_traps
/usr/bin/sudo -k >/dev/null 2>&1 || {
echo "Could not start from a cold sudo credential state." >&2
exit 1
}
echo "Toggle passwordless sudo..."
if /usr/bin/sudo -N -- "$INSTALLED_SELF" __status "$uid"; then
if (($# == 0)); then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __disable "$uid"
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
else
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo "Passwordless sudo timer updated. It will automatically disable in ${minutes} minutes."
fi
else
status=$?
if (( status != STATUS_INACTIVE )); then
echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2
exit 1
fi
echo ""
echo "⚠️ WARNING: This will allow ANY process running as your user to"
echo "execute ANY command as root WITHOUT a password for ${MINUTES} minutes."
echo "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 "Passwordless sudo will automatically disable after ${minutes} minutes,"
echo "including if the machine reboots before the timer fires."
echo "Run this command again to disable it early."
echo ""
if gum confirm "Enable passwordless sudo for ${MINUTES} minutes? This is a significant security risk!"; then
echo "${USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee "$NOPASSWD_FILE" > /dev/null
sudo chmod 440 "$NOPASSWD_FILE"
arm_expiry || exit 1
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."
echo "A restart removes the passwordless sudo rule as well."
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
else
echo "Aborted. No changes made."
fi
+1
View File
@@ -372,6 +372,7 @@ post_theme_commands=(
omarchy-theme-set-pi
omarchy-theme-set-claude
omarchy-theme-set-hermes
omarchy-theme-set-t3code
omarchy-theme-set-browser
omarchy-theme-set-vscode
omarchy-theme-set-obsidian
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# omarchy:summary=Sync the generated Omarchy theme to T3 Code
# omarchy:hidden=true
set -euo pipefail
T3CODE_SOURCE_PATH="$HOME/.local/state/omarchy/current/theme/t3code.json"
T3CODE_HOME="${T3CODE_HOME:-$HOME/.t3}"
T3CODE_STATE_DIR="$T3CODE_HOME/userdata"
# The filename is the theme id T3 Code shows and `t3 theme set` accepts, and
# it stays stable while the colors change underneath on every theme switch.
T3CODE_THEME_PATH="$T3CODE_STATE_DIR/themes/omarchy.json"
[[ -f $T3CODE_SOURCE_PATH ]] || exit 0
# A theme without an accent or a background leaves its placeholder unresolved.
# Publishing that would only make T3 Code reject the file, so skip instead and
# leave the previous palette in place.
if grep -q '{{' "$T3CODE_SOURCE_PATH"; then
echo "Skipping T3 Code theme: $(basename "$T3CODE_SOURCE_PATH") has unresolved colors." >&2
exit 0
fi
# Only follow an install that already exists. Creating the directory here would
# leave a stray T3 Code state dir on machines that do not run it.
[[ -d $T3CODE_STATE_DIR ]] || exit 0
# T3 Code watches this file and retints every connected client on change, so
# the write has to be atomic: a half-written file would read as invalid.
mkdir -p "$(dirname "$T3CODE_THEME_PATH")"
tmp=$(mktemp "$T3CODE_THEME_PATH.XXXXXX")
cp "$T3CODE_SOURCE_PATH" "$tmp"
mv "$tmp" "$T3CODE_THEME_PATH"
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# omarchy:summary=Toggle a full screen desktop: hide the top bar and remove the window gaps together
# omarchy:args=[toggle|on|off]
# omarchy:examples=omarchy toggle fullscreen desktop | omarchy toggle fullscreen desktop off | omarchy toggle fullscreen desktop on
ACTION="${1:-toggle}"
case $ACTION in
toggle|"")
# Only leave full screen when both halves are already in it, so a lone bar
# or a lone gap toggle is pulled into line rather than flipped away.
if omarchy-toggle-enabled bar-off && omarchy-hyprland-toggle-enabled window-no-gaps; then
ACTION="off"
else
ACTION="on"
fi
;;
on|off) ;;
*)
echo "Usage: omarchy-toggle-fullscreen-desktop [toggle|on|off]" >&2
exit 1
;;
esac
omarchy-toggle-bar "$ACTION"
omarchy-hyprland-toggle window-no-gaps "$ACTION"
+1
View File
@@ -19,6 +19,7 @@ o.bind("SUPER + SHIFT + CTRL + SPACE", "Theme menu", "omarchy-menu toggle theme"
o.bind("SUPER + BACKSPACE", "Toggle window transparency", "omarchy-hyprland-window-transparency-toggle")
o.bind("SUPER + SHIFT + BACKSPACE", "Toggle window gaps", "omarchy-hyprland-window-gaps-toggle")
o.bind("SUPER + CTRL + BACKSPACE", "Toggle single-window square aspect", "omarchy-hyprland-window-single-square-aspect-toggle")
o.bind_toggle("SUPER + CTRL + ALT + F", "Toggle full screen desktop", "fullscreen-desktop")
-- xkbcommon names the comma keysym "comma"; the upper-case "COMMA" does not match.
o.bind("SUPER + comma", "Dismiss last notification", "omarchy-shell notifications dismissOne")
@@ -0,0 +1,12 @@
[Trigger]
Operation = Upgrade
Operation = Remove
Type = Package
Target = omarchy-settings
Target = omarchy-settings-dev
[Action]
Description = Revoking temporary Omarchy sudo grants before settings changes...
When = PreTransaction
Exec = /usr/bin/omarchy-sudo-passwordless __package-removing
AbortOnFail
+1 -1
View File
@@ -248,7 +248,7 @@
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
"install.ai.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","disabled":"omarchy-pkg-present openclaw","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-openclaw"},
"install.ai.perplexity": {"icon":"","iconFont":"omarchy","label":"Perplexity","disabled":"omarchy-pkg-present perplexity","action":"omarchy-install-and-launch Perplexity perplexity perplexity"},
"install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-install-and-launch 'T3 Code' t3code-bin t3code"},
"install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-t3-code"},
"install.gaming.steam": {"icon":"","label":"Steam","disabled":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
"install.gaming.retroarch": {"icon":"󰯉","label":"RetroArch","disabled":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
"install.gaming.minecraft": {"icon":"󰍳","label":"Minecraft","disabled":"omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"},
+36
View File
@@ -0,0 +1,36 @@
{
"name": "Omarchy",
"appearance": "{{ mode }}",
"canvas": "{{ background }}",
"accent": "{{ accent }}",
"colors": {
"chrome": "{{ background }}",
"toolbar": "{{ background }}",
"toolbarForeground": "{{ foreground }}",
"toolbarBorder": "{{ muted }}",
"text": "{{ foreground }}",
"border": "{{ muted }}",
"focus": "{{ accent }}",
"sidebar": "{{ dark_background }}",
"sidebarForeground": "{{ foreground }}",
"sidebarBorder": "{{ muted }}",
"sidebarRowHover": "{{ mix dark_background foreground 6% }}",
"sidebarRowActive": "{{ mix dark_background accent 18% }}",
"sidebarRowSelected": "{{ selection }}",
"error": "{{ mix red foreground 35% }}",
"warning": "{{ mix yellow foreground 35% }}",
"codeBackground": "{{ dark_background }}",
"codeForeground": "{{ foreground }}",
"terminalBackground": "{{ background }}",
"terminalForeground": "{{ foreground }}",
"terminalCursor": "{{ bright_foreground }}",
"terminalSelection": "{{ selection }}",
"terminalScrollbar": "{{ muted }}",
"terminalScrollbarHover": "{{ dark_foreground }}"
}
}
+25
View File
@@ -0,0 +1,25 @@
# Temporary passwordless sudo
`omarchy-sudo-passwordless` publishes a bounded grant for the numeric UID authenticated by sudo. Its user interface runs without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`.
## 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.
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.
## Package ownership
The packaging companion must put the publication/expiry command, `omarchy-security-functions` `omarchy-nopasswd-sudo.conf` and the pre-transaction revocation hook in the settings package together. Removing the desktop runtime alone must leave a working expiry command behind. Stable and development package pairs must transfer ownership in one transaction without duplicate files.
Before settings removal or upgrade, the installed ALPM `PreTransaction` hook invokes the fixed `__package-removing` action, acquires the same grant lock, sets `/run/omarchy-sudo-passwordless-package-removing` and revokes existing policy. The marker prevents a waiting publisher from creating a new grant while package files change. A successful installation clears the marker only after boot cleanup exists. The hook uses `AbortOnFail` because a scriptlet failure alone does not abort pacman. The scriptlets repeat cleanup as a fallback for upgrades from older packages that have no installed hook. New grants require both the boot rule and hook before publication. Failed or interrupted transactions leave the marker set; retry the package transaction successfully before requesting another grant.
The runtime marker need not survive reboot: pre-removal revokes the old grants before package files disappear, and a new invocation independently verifies boot cleanup. Both root operations use fixed machine paths. The marker is not a user-controlled mode switch.
## 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.
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.
+4 -4
View File
@@ -1,5 +1,5 @@
# omarchy-sudo-passwordless writes /etc/sudoers.d/99-omarchy-nopasswd-<user> and
# arms a transient systemd-run timer to remove it again. Transient units do not
# survive a reboot, so remove any remaining grant during early boot. Boot-only
# (r!) ensures a later systemd-tmpfiles --remove cannot cut a live grant short.
# omarchy-sudo-passwordless creates grants in this owned filename namespace.
# Transient expiry timers do not survive reboot, so early boot removes every
# remaining grant. The boot-only modifier prevents later tmpfiles runs from
# shortening a live, explicitly requested window.
r! /etc/sudoers.d/99-omarchy-nopasswd-*
+4
View File
@@ -1,3 +1,7 @@
# Upgrades must not delete the version a running process is executing from:
# mise up would prune the old install dir out from under a live session.
mise settings set upgrade.auto_prune false
omarchy-mise-install codex
omarchy-mise-install claude
omarchy-mise-install crush
+1
View File
@@ -192,6 +192,7 @@ All style options are also accessible under _Style_ in the Omarchy menu (`Super
| `Shift + Mute` | Switch to next audio output |
| `Shift + Play` | Switch to next media source |
| `Super + Shift + Backspace` | Toggle window gaps |
| `Super + Ctrl + Alt + F` | Toggle full screen desktop (top bar + window gaps) |
## Reminders
+3 -1
View File
@@ -20,7 +20,9 @@ It works by restoring the baseline snapshot the installer takes, so it's only av
## Passwordless sudo
Sometimes you want `sudo` to stop asking, most often when an AI agent is doing a long stretch of system work for you. _Setup > Security > Passwordless Sudo_ turns that off for 15 minutes and then puts it back automatically. Run it again before the timer runs out to end it early, and pass your own number of minutes with `omarchy-sudo-passwordless 30` if 15 isn't enough. A restart removes the passwordless sudo rule as well.
Sometimes you want `sudo` to stop asking, most often when an AI agent is doing a long stretch of system work for you. _Setup > Security > Passwordless Sudo_ turns that off for 15 wall-clock minutes and then puts it back automatically, including immediately after resuming from a suspend that crossed the deadline. A package-owned boot-time cleanup rule removes the grant before logins if the computer restarts first. Run the command again before the timer runs out to end it early, and pass your own number of minutes (from 1 to 1440) with `omarchy-sudo-passwordless 30` if 15 isn't enough.
Updating or removing Omarchy's settings package ends any temporary grant before its expiry support changes. If the command reports an authorization or cleanup error, resolve it before trying to enable another grant; an error does not mean passwordless access is inactive.
Be clear-eyed about this one: while it's on, anything running as your user can do anything as root without being asked. That's the whole point, and it's also the whole risk.
+3
View File
@@ -0,0 +1,3 @@
echo "Stop mise upgrades from pruning versions still in use"
mise settings set upgrade.auto_prune false
+6
View File
@@ -0,0 +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
+37 -10
View File
@@ -27,6 +27,11 @@ BarWidget {
// the widget ships on the bar and stays out of the way until there are two.
// An older Hyprland that doesn't report the list keeps showing the label.
property bool multipleLayouts: true
// Where the reading sits in the layout list, how long that list is, and every
// keyboard sharing it. A switch moves that set together, so it needs all three.
property int layoutIndex: 0
property int layoutCount: 0
property var syncNames: []
// Short language code per layout description ("English (US)": "en"), read from
// xkb's own table rather than maintained by hand.
property var layoutBriefs: ({})
@@ -63,17 +68,31 @@ BarWidget {
}
// switchxkblayout is a hyprctl command rather than a dispatcher, so it has to
// be run rather than sent over the dispatch socket. It switches the keyboard
// the last reading spoke for, so a click always advances the device the label
// is describing. Switching the seat together would reach the typed keyboard
// without having to name it, but it would also carry the buttons along, and
// the whole read depends on those staying where they started: once a button
// has been advanced too, a toggle that wraps the keyboard back to the first
// layout leaves the button reading as the furthest along, and the label
// follows the button.
// be run rather than sent over the dispatch socket.
//
// Move every keyboard holding the same layout list, rather than the single one
// the last reading spoke for. Naming one device puts the whole switch behind
// UNTYPED_KEYBOARDS recognising every non-keyboard by name, and that list
// cannot keep up with what a seat carries: vendor hotkey blocks
// (intel-hid-events, dell-wmi-hotkeys), HID consumer controls, and Bluetooth
// AVRCP endpoints from a pair of headphones all arrive holding the seat's
// layout list, and they sort ahead of the keyboard being typed on. The click
// then advances a device nobody types on; that device is now the furthest
// along, so it wins the next reading too, and the label describes it while the
// real keyboard never moved.
//
// An absolute index rather than "next", because "next" advances each device
// from wherever it already sits: a seat that has drifted apart stays drifted
// and merely inverts. One index converges them in a single click, and a seat
// in lockstep is what leaves the reading nothing to disagree about afterwards.
//
// Keyboards given their own kb_layout hold a different list and are left out:
// an index into this list would not mean the same layout to them.
function cycleLayout() {
if (!root.keyboardName || !root.bar) return
root.bar.run("hyprctl switchxkblayout " + Util.shellQuote(root.keyboardName) + " next")
if (!root.bar || root.layoutCount < 2 || root.syncNames.length === 0) return
const next = (root.layoutIndex + 1) % root.layoutCount
root.bar.run(root.syncNames.map(name =>
"hyprctl switchxkblayout " + Util.shellQuote(name) + " " + next).join("; "))
refreshTimer.restart()
}
@@ -149,6 +168,14 @@ BarWidget {
root.keyboardCount = typed.length
root.keyboardName = String(kb.name || "")
root.multipleLayouts = kb.layout === undefined || String(kb.layout).indexOf(",") !== -1
root.layoutIndex = kb.active_layout_index || 0
root.layoutCount = kb.layout === undefined ? 0 : String(kb.layout).split(",").length
// Buttons and virtual keyboards are included on purpose: they hold the
// same list, and leaving them behind is what lets a reading drift onto
// one of them later.
root.syncNames = listed.filter(k => String(k.layout) === String(kb.layout))
.map(k => String(k.name || ""))
.filter(name => name !== "")
root.layoutFull = kb.active_keymap
}
}
+3 -2
View File
@@ -31,8 +31,9 @@ chmod +x "$mock_bin"/*
launch_log="$test_tmp/launch-log"
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=true OMARCHY_TEST_LOG="$launch_log" \
bash "$ROOT/bin/omarchy-launch-1password"
grep -Fxq 'launch:-- 1password' "$launch_log" || fail "1Password launcher starts the installed app"
pass "1Password launcher starts the installed app"
grep -Fxq 'launch:-- 1password --force-device-scale-factor=1' "$launch_log" ||
fail "1Password launcher starts the installed app at a fixed scale factor"
pass "1Password launcher starts the installed app at a fixed scale factor"
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=false OMARCHY_TEST_LOG="$launch_log" \
bash "$ROOT/bin/omarchy-launch-1password"
+438 -103
View File
@@ -2,122 +2,457 @@
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
script="$ROOT/bin/omarchy-sudo-passwordless"
tmpfiles_file="$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
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
mock_bin="$test_tmp/bin"
grant="$test_tmp/grant"
calls="$test_tmp/calls"
mkdir -p "$mock_bin"
cat >"$mock_bin/gum" <<'SH'
#!/bin/bash
exit 0
SH
cat >"$mock_bin/systemctl" <<'SH'
#!/bin/bash
printf 'systemctl %s\n' "$*" >>"$TEST_CALLS"
[[ ${1:-} == "is-active" && ${TEST_TIMER_ACTIVE:-false} == "true" ]]
SH
cat >"$mock_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$TEST_CALLS"
case ${1:-} in
test)
[[ ${2:-} == "-f" && -f $TEST_GRANT ]]
;;
tee)
/usr/bin/tee "$TEST_GRANT"
;;
chmod)
/usr/bin/chmod "$2" "$TEST_GRANT"
;;
systemd-run)
[[ ${TEST_FAIL_SYSTEMD_RUN:-false} != "true" ]]
;;
rm)
/usr/bin/rm -f -- "$TEST_GRANT"
;;
systemctl)
exit 0
;;
*)
echo "unexpected sudo command: $*" >&2
exit 90
;;
esac
SH
chmod +x "$mock_bin/gum" "$mock_bin/sudo" "$mock_bin/systemctl"
run_command() {
TEST_CALLS="$calls" TEST_GRANT="$grant" PATH="$mock_bin:$PATH" USER=alice \
"$script" "$@"
function_prefix() {
printf 'source %q\n' "$security_library_path"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$command_path"
}
: >"$calls"
enable_output=$(run_command 15)
[[ -f $grant ]] || fail "successful timer setup leaves the passwordless sudo grant enabled"
[[ $(cat "$grant") == "alice ALL=(ALL) NOPASSWD: ALL" ]] ||
fail "the enabled grant belongs to the current user" "$(cat "$grant")"
grep -q '^sudo systemd-run --on-active=15m .* rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-alice$' "$calls" ||
fail "enabling arms the expiry timer" "$(cat "$calls")"
[[ $enable_output == *"automatically disable in 15 minutes"* ]] ||
fail "success is reported after the timer is armed" "$enable_output"
pass "enabling arms expiry before reporting success"
# 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
)
pass "passwordless sudo validates a bounded positive duration"
: >"$calls"
rm -f "$grant"
if failure_output=$(TEST_FAIL_SYSTEMD_RUN=true run_command 15 2>&1); then
fail "enabling fails when the expiry timer cannot be armed"
# 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"
# 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
[[ ! -e $grant ]] || fail "timer setup failure revokes the new passwordless sudo grant"
[[ $failure_output == *"Revoking access now"* ]] ||
fail "timer setup failure explains the fail-closed revocation" "$failure_output"
[[ $failure_output != *"Passwordless sudo has been ENABLED"* ]] ||
fail "timer setup failure does not report that passwordless sudo was enabled" "$failure_output"
pass "timer setup failure revokes a new grant"
: >"$calls"
printf 'alice ALL=(ALL) NOPASSWD: ALL\n' >"$grant"
if update_output=$(TEST_TIMER_ACTIVE=true TEST_FAIL_SYSTEMD_RUN=true run_command 30 2>&1); then
fail "updating fails when the replacement expiry timer cannot be armed"
if [[ ${1:-} == -k ]]; then
rm -f -- "$TEST_PUBLIC_TOKEN"
exit 0
fi
[[ ! -e $grant ]] || fail "timer update failure revokes the existing passwordless sudo grant"
[[ $update_output != *"timer updated"* ]] ||
fail "timer update failure does not report success" "$update_output"
pass "timer update failure revokes the existing grant"
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"
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"
done
mapfile -t tmpfiles_rules < <(grep -vE '^[[:space:]]*(#|$)' "$tmpfiles_file")
(( ${#tmpfiles_rules[@]} == 1 )) ||
fail "passwordless sudo ships one tmpfiles rule" "${tmpfiles_rules[*]}"
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"
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"
fake_root="$test_tmp/root"
# 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"
grant_names=(alice buildbot-2 user.123 'service$')
for grant_name in "${grant_names[@]}"; do
touch "$sudoers_dir/99-omarchy-nopasswd-$grant_name"
for name in alice buildbot-2 424242; do
: >"$sudoers_dir/99-omarchy-nopasswd-$name"
done
touch "$sudoers_dir/omarchy-dns"
: >"$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"
systemd-tmpfiles --root="$fake_root" --remove --inline "${tmpfiles_rules[@]}"
[[ -f $sudoers_dir/99-omarchy-nopasswd-alice ]] ||
fail "boot-only cleanup leaves a live grant alone outside boot"
systemd-tmpfiles --root="$fake_root" --remove --boot --inline "${tmpfiles_rules[@]}"
for grant_name in "${grant_names[@]}"; do
stale_grant="$sudoers_dir/99-omarchy-nopasswd-$grant_name"
[[ ! -e $stale_grant ]] || fail "boot cleanup removes every generated grant" "$stale_grant"
# 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
fi
done
[[ -f $sudoers_dir/omarchy-dns ]] || fail "boot cleanup preserves unrelated sudoers rules"
pass "systemd-tmpfiles removes generated grants only during boot"
[[ -n $pkgs_root ]] || fail "omarchy-pkgs checkout found for passwordless package-removal coverage"
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 "$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"
done
pass "settings package transitions revoke grants and preserve unrelated configuration"
# 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"
# 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"
@@ -0,0 +1,235 @@
#!/bin/bash
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 "$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 '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"
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"; }
}
(
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"
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"
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")"
}
with_root_lock enable_locked 1000 15
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
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
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"
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"
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
test_home="$test_tmp/home"
mock_bin="$test_tmp/bin"
mkdir -p "$mock_bin"
# Run the real theme refresh, renderer and publisher in a throwaway home.
# Only package installation, the T3 CLI and desktop launch are stubbed.
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
[[ $1 == "t3code-bin" ]]
SH
cat >"$mock_bin/t3" <<'SH'
#!/bin/bash
[[ $1 == "theme" && $2 == "set" && $3 == "omarchy" && $4 == "--base-dir" && $5 == "$T3CODE_HOME" ]] || exit 1
palette="$T3CODE_HOME/userdata/themes/omarchy.json"
if [[ ! -f $palette ]] || ! jq -e '.name == "Omarchy" and (.accent | test("^#[0-9a-fA-F]{6}$"))' "$palette" >/dev/null; then
echo "No published Omarchy theme"
exit 1
fi
if [[ ${OMARCHY_TEST_T3_FAIL:-0} == "1" ]]; then
echo "T3 theme selection failed"
exit 1
fi
echo "T3 selected Omarchy"
SH
cat >"$mock_bin/setsid" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$mock_bin"/*
run_installer() {
HOME="$test_home" T3CODE_HOME="$test_tmp/custom t3 home" \
OMARCHY_PATH="$ROOT" PATH="$mock_bin:$ROOT/bin:$PATH" \
OMARCHY_THEME_HEADLESS=1 XDG_RUNTIME_DIR="$test_tmp" \
"$ROOT/bin/omarchy-install-ai-t3-code" >"$test_tmp/output" 2>&1
}
state="$test_home/.local/state/omarchy/current"
palette="$test_tmp/custom t3 home/userdata/themes/omarchy.json"
settings="$test_tmp/custom t3 home/userdata/settings.json"
mkdir -p "$state/theme"
echo 'tokyo-night' >"$state/theme.name"
cp "$ROOT/themes/tokyo-night/colors.toml" "$state/theme/colors.toml"
run_installer || fail "installing after an update succeeds" "$(cat "$test_tmp/output")"
[[ -f $state/theme/t3code.json && -f $palette ]] || fail "the missing palette is rendered and published"
cmp -s "$state/theme/t3code.json" "$palette" || fail "T3 receives the current theme's palette"
grep -q 'T3 selected Omarchy' "$test_tmp/output" || fail "the published palette is selected"
grep -q 'Opening T3 Code' "$test_tmp/output" || fail "the themed app opens"
[[ ! -e $test_home/.t3 ]] || fail "the custom T3 home is respected"
pass "an update with no staged T3 palette renders, publishes and selects it before launch"
# Preserve an already rendered palette, including a user-supplied color.
jq '.accent = "#abcdef"' "$state/theme/t3code.json" >"$test_tmp/palette.json"
cp "$test_tmp/palette.json" "$state/theme/t3code.json"
run_installer || fail "installing with an existing palette succeeds" "$(cat "$test_tmp/output")"
cmp -s "$test_tmp/palette.json" "$palette" || fail "an existing palette is published without re-staging"
pass "an existing palette is used without refreshing the current theme"
printf '{"defaultTheme":"dark","keep":"existing settings"}\n' >"$settings"
cp "$settings" "$test_tmp/settings-before.json"
if OMARCHY_TEST_T3_FAIL=1 run_installer; then
fail "a failed T3 theme selection fails the installer"
fi
grep -q 'T3 theme selection failed' "$test_tmp/output" || fail "the CLI failure is visible"
if grep -Eq 'Opening T3 Code|T3 Code has been installed' "$test_tmp/output"; then
fail "a failed selection does not report successful setup"
fi
cmp -s "$test_tmp/settings-before.json" "$settings" || fail "a failed selection does not rewrite settings"
pass "a CLI failure remains visible and leaves the existing settings intact"
+1 -1
View File
@@ -207,7 +207,7 @@ pass "a theme name cannot climb out of the theme directories"
# generates. Every generated theme file is either denied to an installed theme or
# recorded here as carrying colour, so a new template fails until it is placed.
denied=(alacritty.toml foot.ini ghostty.conf kitty.conf gum_env.lua hyprland.lua neovim.lua vscode.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml t3code.json vscode-theme.json)
for tpl in "$ROOT"/default/themed/*.tpl; do
generated=$(basename "$tpl" .tpl)
+33
View File
@@ -51,3 +51,36 @@ pass "bar on is idempotent"
HOME="$test_home" omarchy-toggle-bar off
[[ ! -f $bar_flag ]] || fail "bar off disables bar-off toggle"
pass "bar off disables bar-off toggle"
# The gaps half of full screen copies a flag file in and reloads Hyprland, so
# give it this checkout to copy from and a hyprctl that answers without a
# compositor.
export OMARCHY_PATH="$ROOT"
stub_bin="$TMPDIR/bin"
mkdir -p "$stub_bin"
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/hyprctl"
chmod +x "$stub_bin/hyprctl"
export PATH="$stub_bin:$PATH"
gaps_flag="$test_home/.local/state/omarchy/toggles/hypr/window-no-gaps.lua"
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle hides the bar and the gaps together"
pass "fullscreen toggle hides the bar and the gaps together"
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen toggle restores the bar and the gaps together"
pass "fullscreen toggle restores the bar and the gaps together"
HOME="$test_home" omarchy-toggle-bar on
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle pulls a half-hidden desktop into full screen"
pass "fullscreen toggle pulls a half-hidden desktop into full screen"
HOME="$test_home" omarchy-toggle-fullscreen-desktop off
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen off leaves full screen"
pass "fullscreen off leaves full screen"
HOME="$test_home" omarchy-toggle-fullscreen-desktop on
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen on enters full screen"
pass "fullscreen on enters full screen"