Integrate the shared sudo lifecycle foundation
This commit is contained in:
commit
75e58f034f
26 files changed
+1596
-164
No files matched your search
Executable
+31
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+34
@@ -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"
|
||||
Executable
+27
@@ -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"
|
||||
Reference in new issue
Block a user