Refresh the passwordless sudo indicator on change instead of polling (#15026)
* Refresh the passwordless sudo indicator on change instead of polling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refresh the sudo indicator after failed changes and on a wall-clock deadline Addresses Codex review: a rolled-back renewal skipped the refresh, the monotonic user timer paused through suspend, and default timer accuracy allowed a minute's delay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the sudo indicator refreshes by running the grant flows Replace source-string checks with runs of timed, renewed, failed, permanent and disable flows against the fixture, asserting the recorded omarchy-shell refresh and wall-clock systemd-run --user timer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8bd1b7c045
commit
3c1f7b780f
5 files changed
+96
-14
No files matched your search
@@ -462,7 +462,34 @@ root_dispatch() {
|
||||
esac
|
||||
}
|
||||
|
||||
# The bar indicator only probes on demand, so every grant change pushes a
|
||||
# refresh. Root's expiry timer can't reach the user's shell, so a user timer
|
||||
# refreshes once the deadline has passed. Both are best effort: a stale red
|
||||
# indicator still revokes cleanly when clicked.
|
||||
refresh_indicator() {
|
||||
/usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# A failed enable can still change the grant (a renewal that rolls back removes
|
||||
# the previous policy), so refresh whatever the privileged action returned.
|
||||
change_grant() {
|
||||
local status=0
|
||||
/usr/bin/sudo "$@" || status=$?
|
||||
refresh_indicator
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# A wall-clock deadline, like the sudoers rule and root's timer, so expiry
|
||||
# during suspend still refreshes on resume.
|
||||
schedule_expiry_refresh() {
|
||||
[[ $minutes == "permanent" ]] && return 0
|
||||
/usr/bin/systemd-run --user --quiet --collect --on-calendar="@$((EPOCHSECONDS + 10#$minutes * 60 + 2))" \
|
||||
--timer-property=AccuracySec=1s \
|
||||
-- /usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
show_enabled() {
|
||||
schedule_expiry_refresh
|
||||
if [[ $minutes == "permanent" ]]; then
|
||||
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
|
||||
else
|
||||
@@ -542,12 +569,12 @@ else
|
||||
fi
|
||||
fi
|
||||
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
|
||||
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid"
|
||||
change_grant -n -N -- "$INSTALLED_SELF" __disable "$uid"
|
||||
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
|
||||
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
|
||||
echo "Passwordless sudo is already DISABLED."
|
||||
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then
|
||||
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||
change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||
show_enabled
|
||||
else
|
||||
if (( status != 0 && status != STATUS_INACTIVE )); then
|
||||
@@ -587,7 +614,7 @@ else
|
||||
echo ""
|
||||
|
||||
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then
|
||||
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||
change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||
echo ""
|
||||
show_enabled
|
||||
else
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
`omarchy-sudo-passwordless` publishes a timed or permanent grant for the numeric UID authenticated by sudo. Its user interface inspects sudo policy without prompting and authenticates only the enable action when access is off, without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`.
|
||||
|
||||
The menu bar's `PasswordlessSudo` indicator polls `--active` every five seconds and shows the passwordless sudo icon in red while access is active. The probe normally reads sudo's noninteractive long policy listing for the installed `__status` action and checks its `!authenticate` tag. If listing requires authentication, it falls back to a noninteractive `__status` call that ignores cached credentials. Neither path prompts or updates the credential cache. Interactive setup likewise falls back to noninteractive status and, when authentication is unavailable, leaves validation to the single confirmed enable action. Clicking the active indicator runs `--disable` noninteractively without opening a terminal, so a grant expiring between display and click cannot start the enable flow.
|
||||
The menu bar's `PasswordlessSudo` indicator runs `--active` when the shell starts and whenever `omarchy.indicators` is refreshed, and shows the passwordless sudo icon in red while access is active. The probe normally reads sudo's noninteractive long policy listing for the installed `__status` action and checks its `!authenticate` tag. If listing requires authentication, it falls back to a noninteractive `__status` call that ignores cached credentials. Neither path prompts or updates the credential cache. Interactive setup likewise falls back to noninteractive status and, when authentication is unavailable, leaves validation to the single confirmed enable action. Clicking the active indicator runs `--disable` noninteractively without opening a terminal, so a grant expiring between display and click cannot start the enable flow. The indicator never polls: every enable or disable attempt refreshes it directly, failed ones included since a rolled-back renewal can remove the previous grant, and enabling a timed grant also starts a transient user timer on the same wall-clock deadline that refreshes it two seconds later, so expiry during suspend shows on resume. The root expiry timer cannot reach the user's shell. If that refresh is missed, the indicator stays red past expiry, and clicking it reports access is already disabled and clears it.
|
||||
|
||||
## Grant lifecycle
|
||||
|
||||
|
||||
@@ -31,19 +31,15 @@ BarIndicator {
|
||||
onBarChanged: refresh()
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
// No polling: each probe is a hardened script plus sudo calls that can log.
|
||||
// omarchy-sudo-passwordless refreshes omarchy.indicators on enable, disable
|
||||
// and expiry instead.
|
||||
Connections {
|
||||
target: root.indicatorHost
|
||||
ignoreUnknownSignals: true
|
||||
function onRefreshRequested() { root.refresh() }
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
repeat: true
|
||||
running: !!root.bar
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: statusProc
|
||||
command: ["omarchy-sudo-passwordless", "--active"]
|
||||
|
||||
@@ -58,6 +58,8 @@ case "$name" in
|
||||
[[ ${TEST_POST_PUBLISH_FAIL:-0} != 1 ]] || : >"$TEST_GRANT_ROOT/run/omarchy-sudo-passwordless-package-removing"
|
||||
;;
|
||||
systemd-run)
|
||||
# The indicator's user refresh timer is not the root expiry timer.
|
||||
[[ ${1:-} != --user ]] || exit 0
|
||||
[[ ${TEST_TIMER_FAIL:-0} != 1 ]] || exit 1
|
||||
if [[ ${TEST_CANCEL_ENABLE:-0} == 1 ]]; then kill -TERM "$PPID"; fi
|
||||
;;
|
||||
@@ -67,6 +69,7 @@ case "$name" in
|
||||
date)
|
||||
if [[ ${TEST_EXPIRED:-0} == 1 && $* == '-u +%Y%m%d%H%M%SZ' ]]; then echo 99991231235959Z; else /usr/bin/date "$@"; fi
|
||||
;;
|
||||
omarchy-shell) ;;
|
||||
getent) printf '%s:x:1000:1000:Test:/nonexistent:/bin/bash\n' "${TEST_ACCOUNT:-audituser}" ;;
|
||||
sudo)
|
||||
if [[ ${1:-} == -h ]]; then echo 'usage: sudo [-N] command'; exit 0; fi
|
||||
@@ -86,6 +89,7 @@ case "$name" in
|
||||
TEST_EUID=0 /usr/bin/bash -p "$@"
|
||||
else
|
||||
[[ ${2:-} != __status ]] || exit "${TEST_STATUS:-3}"
|
||||
[[ ${2:-} != __enable || ${TEST_ENABLE_FAIL:-0} != 1 ]] || exit 1
|
||||
fi
|
||||
;;
|
||||
gum)
|
||||
@@ -100,7 +104,7 @@ case "$name" in
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$test_tmp/bin/mock"
|
||||
for name in stat chown install rm mv systemd-run systemctl date getent sudo gum; do
|
||||
for name in stat chown install rm mv systemd-run systemctl date getent sudo gum omarchy-shell; do
|
||||
ln -s mock "$test_tmp/bin/$name"
|
||||
done
|
||||
|
||||
@@ -114,7 +118,7 @@ for name in ('omarchy-sudo-passwordless', 'omarchy-security-functions'):
|
||||
target = str(temp/'hooks') if path == '/usr/share/libalpm/hooks' else str(temp) + path
|
||||
text = text.replace(path, target)
|
||||
text = text.replace('((EUID == 0))', '((${TEST_EUID:-1} == 0))')
|
||||
for command in ('stat', 'chown', 'install', 'rm', 'mv', 'systemd-run', 'systemctl', 'date', 'getent', 'sudo', 'gum'):
|
||||
for command in ('stat', 'chown', 'install', 'rm', 'mv', 'systemd-run', 'systemctl', 'date', 'getent', 'sudo', 'gum', 'omarchy-shell'):
|
||||
text = text.replace('/usr/bin/' + command, str(temp/'bin'/command))
|
||||
(temp/name).write_text(text)
|
||||
(temp/name).chmod(0o755)
|
||||
|
||||
@@ -17,7 +17,8 @@ assert(!widget.includes('sudoHorizontal') && !widget.includes('sudoVertical'), '
|
||||
assert(!indicator.includes('visible:'), 'sudo uses the shared indicator visibility and hover behavior')
|
||||
assert(indicator.includes('command: ["omarchy-sudo-passwordless", "--active"]'), 'indicator uses the noninteractive grant probe')
|
||||
assert(indicator.includes('root.granted = exitCode === 0 && exitStatus === 0'), 'failed or interrupted probes do not claim an active grant')
|
||||
assert(indicator.includes('interval: 5000') && indicator.includes('onTriggered: root.refresh()'), 'indicator refreshes after activation, revocation, and expiry')
|
||||
assert(!indicator.includes('Timer'), 'indicator does not poll the sudo probe')
|
||||
assert(indicator.includes('function onRefreshRequested() { root.refresh() }'), 'indicator probes when the indicator host asks for a refresh')
|
||||
assert(indicator.includes('command: ["omarchy-sudo-passwordless", "--disable"]'), 'active indicator revokes access through a background process')
|
||||
assert(indicator.includes('if (root.granted) disableProc.running = true'), 'active click bypasses the terminal launcher')
|
||||
assert(indicator.includes('root.indicatorHost.refresh()'), 'disabling access refreshes all indicator instances immediately')
|
||||
@@ -34,3 +35,57 @@ revoke.running = false
|
||||
press(button, revoke)
|
||||
assertDeepEqual(launched, ['omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless'], 'inactive click still opens interactive setup')
|
||||
JS
|
||||
|
||||
# Run the public grant flows against the fixture's stand-ins and check the
|
||||
# refreshes they push, without touching the host shell or user timers.
|
||||
source "$SHELL_TEST_DIR/fixtures/passwordless-sudo-test.sh"
|
||||
refresh="omarchy-shell -q omarchy.indicators refresh"
|
||||
run_grant_flow() {
|
||||
: >"$test_tmp/commands"
|
||||
env "$@" /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" "${grant_args[@]}" >"$test_tmp/public.log" 2>&1 || true
|
||||
}
|
||||
refresh_follows() {
|
||||
grep -A1 -- "$1" "$test_tmp/commands" | grep -qxF -- "$refresh"
|
||||
}
|
||||
user_timers() {
|
||||
grep -c '^systemd-run --user ' "$test_tmp/commands" || true
|
||||
}
|
||||
|
||||
grant_args=(15)
|
||||
before=$EPOCHSECONDS
|
||||
run_grant_flow TEST_STATUS=3 TEST_CONFIRM_STATUS=0
|
||||
after=$EPOCHSECONDS
|
||||
refresh_follows ' __enable [0-9]* 15$' || fail "timed enable must refresh the indicator after the grant changes"
|
||||
timer=$(grep '^systemd-run --user ' "$test_tmp/commands") || fail "timed enable must schedule an expiry refresh"
|
||||
timer_pattern="^systemd-run --user --quiet --collect --on-calendar=@([0-9]+) --timer-property=AccuracySec=1s -- $test_tmp/bin/omarchy-shell -q omarchy.indicators refresh$"
|
||||
[[ $timer =~ $timer_pattern ]] || fail "unexpected expiry refresh timer: $timer"
|
||||
deadline=${BASH_REMATCH[1]}
|
||||
(( deadline >= before + 15 * 60 + 2 && deadline <= after + 15 * 60 + 2 )) || fail "expiry refresh must fire just after the wall-clock deadline"
|
||||
pass "timed enable refreshes now and on a wall-clock timer just after expiry"
|
||||
|
||||
run_grant_flow TEST_STATUS=0
|
||||
refresh_follows ' __enable [0-9]* 15$' || fail "renewal must refresh the indicator"
|
||||
(( $(user_timers) == 1 )) || fail "renewal must schedule an expiry refresh"
|
||||
pass "renewing a timed grant refreshes and reschedules the expiry refresh"
|
||||
|
||||
run_grant_flow TEST_STATUS=0 TEST_ENABLE_FAIL=1
|
||||
refresh_follows ' __enable [0-9]* 15$' || fail "failed renewal must still refresh the indicator"
|
||||
(( $(user_timers) == 0 )) || fail "failed renewal must not schedule an expiry refresh"
|
||||
pass "a failed grant change still refreshes the indicator"
|
||||
|
||||
grant_args=(permanent)
|
||||
run_grant_flow TEST_STATUS=3 TEST_CONFIRM_STATUS=0
|
||||
refresh_follows ' __enable [0-9]* permanent$' || fail "permanent enable must refresh the indicator"
|
||||
(( $(user_timers) == 0 )) || fail "permanent enable must not schedule an expiry refresh"
|
||||
pass "permanent enable refreshes without scheduling an expiry refresh"
|
||||
|
||||
assert_disable_refreshes() {
|
||||
run_grant_flow TEST_STATUS=0
|
||||
refresh_follows ' __disable [0-9]*$' || fail "disable ${grant_args[*]} must refresh the indicator"
|
||||
(( $(user_timers) == 0 )) || fail "disable must not schedule an expiry refresh"
|
||||
}
|
||||
grant_args=()
|
||||
assert_disable_refreshes
|
||||
grant_args=(--disable)
|
||||
assert_disable_refreshes
|
||||
pass "disabling access refreshes the indicator"
|
||||
Reference in new issue
Block a user