Let users choose passwordless sudo duration (#14435)
* Let users choose passwordless sudo duration * Warn in the menu bar when passwordless sudo is active * Fix sudo indicator hover behavior and repeated authentication * Disable passwordless sudo from the bar without a terminal * Shorten passwordless sudo indicator tooltip * Recover interrupted passwordless sudo duration switches * Allow sudo grant changes when listings require authentication * Start passwordless sudo setup with the duration question
This commit is contained in:
1 parent
982290fa42
commit
d9b970dd62
9 files changed
+552
-59
No files matched your search
@@ -52,7 +52,9 @@ case "$name" in
|
||||
;;
|
||||
mv)
|
||||
[[ ${TEST_PUBLISH_FAIL:-0} != 1 ]] || exit 1
|
||||
[[ ${TEST_REQUIRE_EXISTING_TARGET:-0} != 1 || -f ${@: -1} ]] || exit 1
|
||||
/usr/bin/mv "$@"
|
||||
[[ ${TEST_KILL_AFTER_PUBLISH:-0} != 1 ]] || /usr/bin/kill -KILL "$PPID"
|
||||
[[ ${TEST_POST_PUBLISH_FAIL:-0} != 1 ]] || : >"$TEST_GRANT_ROOT/run/omarchy-sudo-passwordless-package-removing"
|
||||
;;
|
||||
systemd-run)
|
||||
@@ -69,7 +71,15 @@ case "$name" in
|
||||
sudo)
|
||||
if [[ ${1:-} == -h ]]; then echo 'usage: sudo [-N] command'; exit 0; fi
|
||||
if [[ ${1:-} == -k ]]; then exit 0; fi
|
||||
if [[ ${1:-} == -N ]]; then shift; fi
|
||||
if [[ ${1:-} == -n && ${3:-} == -l ]]; then
|
||||
[[ ${TEST_POLICY_FAILURE:-0} != 1 ]] || exit 1
|
||||
default_policy=' Options: authenticate'
|
||||
[[ ${TEST_STATUS:-3} == 3 ]] || default_policy=' Options: !authenticate'
|
||||
printf '%s\n' "${TEST_POLICY:-$default_policy}"
|
||||
exit 0
|
||||
fi
|
||||
if [[ ${1:-} == -n ]]; then shift; fi
|
||||
if [[ ${1:-} == -N || ${1:-} == -kn ]]; then shift; fi
|
||||
if [[ ${1:-} == -- ]]; then shift; fi
|
||||
if [[ ${TEST_MIGRATION:-0} == 1 ]]; then
|
||||
[[ ${TEST_NO_SUDO:-0} != 1 ]] || exit 1
|
||||
@@ -78,7 +88,14 @@ case "$name" in
|
||||
[[ ${2:-} != __status ]] || exit "${TEST_STATUS:-3}"
|
||||
fi
|
||||
;;
|
||||
gum) exit 1 ;;
|
||||
gum)
|
||||
if [[ $1 == choose ]]; then
|
||||
[[ ${TEST_CHOICE_CANCEL:-0} != 1 ]] || exit 130
|
||||
printf '%s\n' "${TEST_CHOICE:-15 minutes}"
|
||||
else
|
||||
exit "${TEST_CONFIRM_STATUS:-1}"
|
||||
fi
|
||||
;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
STUB
|
||||
@@ -120,6 +137,6 @@ assert_status() {
|
||||
(( actual == expected )) || fail "expected status $expected, got $actual from $*"
|
||||
}
|
||||
reset_grant() {
|
||||
rm -f "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
|
||||
rm -f "$test_tmp/etc/sudoers.d/99-omarchy-permanent-nopasswd-1000" "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
|
||||
: >"$test_tmp/commands"
|
||||
}
|
||||
@@ -32,7 +32,7 @@ for status in 1 2 3; do
|
||||
else
|
||||
(( result != 0 )) && ! grep -q '^gum ' "$test_tmp/commands" || fail "inspection errors must not offer enablement"
|
||||
fi
|
||||
grep -q '^sudo -N -- .* __status ' "$test_tmp/commands" || fail "status must not publish reusable authorization"
|
||||
grep -q '^sudo -n -N -l -l -- .* __status ' "$test_tmp/commands" || fail "status must inspect policy without authentication"
|
||||
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]] || fail "public exit must revoke its authorization"
|
||||
done
|
||||
pass "public status distinguishes inactive from errors and revokes authorization on exit"
|
||||
@@ -164,3 +164,214 @@ cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/
|
||||
[[ ! -e $(rule_file 1000) ]]
|
||||
)
|
||||
pass "trailing-dollar accounts publish valid native policy and alias-shaped names never publish"
|
||||
|
||||
reset_grant
|
||||
(
|
||||
source "$library"
|
||||
valid_duration permanent
|
||||
! valid_duration forever || fail "invalid duration was accepted"
|
||||
enable_locked 1000 permanent
|
||||
read_grant 1000
|
||||
[[ $GRANT_NAME == audituser && -z $GRANT_DEADLINE ]]
|
||||
[[ $(stat -c '%a' "$(rule_file 1000 permanent)") == 440 ]]
|
||||
/usr/sbin/visudo -cf "$(rule_file 1000 permanent)" >/dev/null
|
||||
! grep -q '^systemd-run ' "$test_tmp/commands" || fail "permanent grant started a timer"
|
||||
TEST_EXPIRED=1 status_locked 1000
|
||||
TEST_EXPIRED=1 expire_locked 1000
|
||||
cleanup_all_locked
|
||||
/usr/bin/systemd-tmpfiles --root="$test_tmp" --remove --boot --inline 'r! /etc/sudoers.d/99-omarchy-nopasswd-*'
|
||||
[[ -f $(rule_file 1000 permanent) ]]
|
||||
cleanup_uid_locked 1000
|
||||
assert_status 3 status_locked 1000
|
||||
)
|
||||
pass "permanent access has no timer, survives expiry and temporary cleanup, and can be disabled"
|
||||
|
||||
reset_grant
|
||||
(
|
||||
source "$library"
|
||||
enable_locked 1000 15
|
||||
enable_locked 1000 permanent
|
||||
[[ ! -e $(rule_file 1000) && -e $(rule_file 1000 permanent) ]]
|
||||
TEST_EXPIRED=1 expire_locked 1000
|
||||
[[ -e $(rule_file 1000 permanent) ]]
|
||||
enable_locked 1000 60
|
||||
[[ -e $(rule_file 1000) && ! -e $(rule_file 1000 permanent) ]]
|
||||
TEST_EXPIRED=1 expire_locked 1000
|
||||
assert_status 3 status_locked 1000
|
||||
TEST_PUBLISH_FAIL=1 assert_status 1 enable_locked 1000 permanent
|
||||
[[ ! -e $(rule_file 1000 permanent) ]]
|
||||
)
|
||||
pass "switching duration replaces the prior policy and old callbacks preserve permanent access"
|
||||
|
||||
reset_grant
|
||||
(
|
||||
source "$library"
|
||||
permanent=$(rule_file 1000 permanent)
|
||||
printf 'audituser ALL=(ALL) NOPASSWD: /usr/bin/true\n' >"$permanent"
|
||||
assert_status 2 status_locked 1000
|
||||
assert_status 1 enable_locked 1000 permanent
|
||||
assert_status 1 cleanup_uid_locked 1000
|
||||
[[ -e $permanent ]]
|
||||
rm "$permanent"
|
||||
enable_locked 1000 permanent
|
||||
TEST_BAD_PATH="$permanent" assert_status 2 status_locked 1000
|
||||
)
|
||||
pass "permanent policy rejects unsafe ownership and preserves administrator edits"
|
||||
|
||||
for choice in '15 minutes' '1 Hour' '1 Day' Permanently; do
|
||||
case "$choice" in
|
||||
'15 minutes') expected=15 ;;
|
||||
'1 Hour') expected=60 ;;
|
||||
'1 Day') expected=1440 ;;
|
||||
Permanently) expected=permanent ;;
|
||||
esac
|
||||
: >"$test_tmp/commands"
|
||||
TEST_CHOICE="$choice" TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
|
||||
grep -q '^gum choose .*15 minutes 1 Hour 1 Day Permanently$' "$test_tmp/commands"
|
||||
grep -q "^sudo -N -- .* __enable .* $expected$" "$test_tmp/commands"
|
||||
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
|
||||
if [[ $expected == permanent ]]; then
|
||||
grep -q 'remain enabled across reboots until you disable it' "$test_tmp/public.log"
|
||||
! grep -q 'automatically disable' "$test_tmp/public.log" || fail "permanent grant claims automatic expiry"
|
||||
fi
|
||||
done
|
||||
pass "each duration choice dispatches its exact grant duration with permanent-specific copy"
|
||||
|
||||
: >"$test_tmp/commands"
|
||||
assert_status 130 env TEST_CHOICE_CANCEL=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
|
||||
! grep -q '__enable\|^gum confirm ' "$test_tmp/commands" || fail "cancelled picker continued enablement"
|
||||
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
|
||||
: >"$test_tmp/commands"
|
||||
TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
|
||||
! grep -q '^gum ' "$test_tmp/commands" || fail "disabling access offered a picker"
|
||||
grep -q '__disable ' "$test_tmp/commands"
|
||||
pass "cancelling the picker grants nothing and active access still toggles off"
|
||||
|
||||
for confirm_status in 0 1; do
|
||||
: >"$test_tmp/commands"
|
||||
TEST_STATUS=0 TEST_CONFIRM_STATUS=$confirm_status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" permanent >"$test_tmp/public.log" 2>&1
|
||||
grep -q '^gum confirm Enable passwordless sudo permanently?' "$test_tmp/commands"
|
||||
if (( confirm_status == 0 )); then
|
||||
grep -q '__enable .* permanent$' "$test_tmp/commands"
|
||||
else
|
||||
! grep -q '__enable ' "$test_tmp/commands" || fail "declining permanent access still enabled it"
|
||||
fi
|
||||
done
|
||||
pass "changing active access to permanent always requires confirmation"
|
||||
|
||||
reset_grant
|
||||
(
|
||||
source "$library"
|
||||
enable_locked 1000 15
|
||||
TEST_REQUIRE_EXISTING_TARGET=1 enable_locked 1000 60
|
||||
cleanup_uid_locked 1000
|
||||
enable_locked 01000 permanent
|
||||
status_locked 1000
|
||||
[[ -e $(rule_file 1000 permanent) ]]
|
||||
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-permanent-nopasswd-01000 ]]
|
||||
cleanup_uid_locked 1000
|
||||
assert_status 3 status_locked 01000
|
||||
)
|
||||
pass "renewal replaces the existing rule atomically and zero-padded UIDs share one policy"
|
||||
|
||||
|
||||
for policy in ' Options: !authenticate' ' Options: authenticate'; do
|
||||
: >"$test_tmp/commands"
|
||||
expected=1
|
||||
[[ $policy != *'!authenticate'* ]] || expected=0
|
||||
assert_status "$expected" env TEST_POLICY="$policy" /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
|
||||
[[ $(wc -l <"$test_tmp/commands") == 1 ]]
|
||||
grep -q '^sudo -n -N -l -l -- .* __status ' "$test_tmp/commands"
|
||||
done
|
||||
assert_status 1 env TEST_POLICY_FAILURE=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
|
||||
pass "menu probe reads policy tags without executing a privileged action or changing the timestamp"
|
||||
|
||||
for status in 0 3; do
|
||||
: >"$test_tmp/commands"
|
||||
TEST_STATUS=$status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
|
||||
! grep -q '^gum\|__enable ' "$test_tmp/commands" || fail "explicit disable offered enablement"
|
||||
if (( status == 0 )); then
|
||||
grep -q '__disable ' "$test_tmp/commands"
|
||||
fi
|
||||
done
|
||||
pass "explicit disable never enables access even when a displayed grant has expired"
|
||||
|
||||
: >"$test_tmp/commands"
|
||||
TEST_STATUS=3 TEST_CHOICE='15 minutes' TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
|
||||
[[ $(grep -c '^sudo -N -- ' "$test_tmp/commands") == 1 ]] || fail "enable must authenticate exactly one sudo call"
|
||||
grep -q '^sudo -N -- .* __enable .* 15$' "$test_tmp/commands"
|
||||
! grep -q '^sudo -n -N -- .* __status ' "$test_tmp/commands" || fail "inactive flow attempted root status"
|
||||
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
|
||||
pass "enabling from inactive policy has exactly one password-capable sudo invocation"
|
||||
|
||||
: >"$test_tmp/commands"
|
||||
TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
|
||||
grep -q '^sudo -n -N -- .* __disable ' "$test_tmp/commands" || fail "disable must refuse interactive authentication"
|
||||
! grep -q '^sudo -N -- ' "$test_tmp/commands" || fail "disable attempted an interactive sudo command"
|
||||
pass "disabling active access is fully noninteractive"
|
||||
|
||||
for duration in permanent 15; do
|
||||
reset_grant
|
||||
(
|
||||
source "$library"
|
||||
if [[ $duration == permanent ]]; then
|
||||
enable_locked 1000 15
|
||||
else
|
||||
enable_locked 1000 permanent
|
||||
fi
|
||||
assert_status 137 env TEST_KILL_AFTER_PUBLISH=1 TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __enable 1000 "$duration"
|
||||
[[ -f $(rule_file 1000) && -f $(rule_file 1000 permanent) ]]
|
||||
status_locked 1000
|
||||
TEST_EXPIRED=1 expire_locked 1000
|
||||
[[ -f $(rule_file 1000 permanent) ]]
|
||||
cleanup_uid_locked 1000
|
||||
assert_status 3 status_locked 1000
|
||||
)
|
||||
done
|
||||
pass "SIGKILL during either duration switch leaves access inspectable and revocable without expiring permanent policy"
|
||||
|
||||
reset_grant
|
||||
(
|
||||
source "$library"
|
||||
enable_locked 1000 permanent
|
||||
timed=$(rule_file 1000)
|
||||
printf 'otheruser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
|
||||
assert_status 2 read_grant 1000
|
||||
assert_status 1 enable_locked 1000 15
|
||||
printf 'audituser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
|
||||
TEST_BAD_PATH="$timed" assert_status 2 read_grant 1000
|
||||
rm "$timed"
|
||||
ln -s "$(rule_file 1000 permanent)" "$timed"
|
||||
assert_status 2 read_grant 1000
|
||||
rm "$timed"
|
||||
printf 'audituser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
|
||||
enable_locked 1000 15
|
||||
[[ -f $timed && ! -e $(rule_file 1000 permanent) ]]
|
||||
)
|
||||
pass "paired policy recovery rejects mismatched or unsafe rules and allows a complete duration change"
|
||||
|
||||
: >"$test_tmp/commands"
|
||||
TEST_POLICY_FAILURE=1 TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
|
||||
grep -q '^sudo -n -N -- .* __disable ' "$test_tmp/commands"
|
||||
! grep -q '^gum\|^sudo -N -- ' "$test_tmp/commands" || fail "listpw=always disable prompted"
|
||||
TEST_POLICY_FAILURE=1 TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
|
||||
assert_status 1 env TEST_POLICY_FAILURE=1 TEST_STATUS=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
|
||||
: >"$test_tmp/commands"
|
||||
TEST_POLICY_FAILURE=1 TEST_STATUS=1 TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1
|
||||
[[ $(grep -c '^sudo -N -- ' "$test_tmp/commands") == 1 ]] || fail "listing failure must leave one enable authentication"
|
||||
grep -q '^sudo -N -- .* __enable .* 15$' "$test_tmp/commands"
|
||||
: >"$test_tmp/commands"
|
||||
assert_status 1 env TEST_POLICY_FAILURE=1 TEST_STATUS=2 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1
|
||||
! grep -q '^gum\|__enable ' "$test_tmp/commands" || fail "unsafe grant was offered enablement"
|
||||
pass "password-required listings do not block enabling, disabling, or active detection"
|
||||
|
||||
: >"$test_tmp/commands"
|
||||
TEST_POLICY_FAILURE=1 TEST_STATUS=2 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
|
||||
grep -q '^sudo -kn -- .* __status ' "$test_tmp/commands" || fail "fallback probe must ignore cached credentials"
|
||||
(
|
||||
source "$library"
|
||||
with_root_lock() { return 1; }
|
||||
verify_sudo_caller() { return 0; }
|
||||
assert_status 2 root_dispatch __status 1000
|
||||
)
|
||||
pass "status lock failures remain inspection errors and an authenticated unsafe grant keeps its warning"
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const read = name => fs.readFileSync(path.join(root, name), 'utf8')
|
||||
const indicator = read('shell/plugins/bar/indicators/PasswordlessSudo.qml')
|
||||
const widget = read('shell/plugins/bar/widgets/Indicators.qml')
|
||||
const manifest = JSON.parse(read('shell/plugins/bar/widgets/Indicators.manifest.json'))
|
||||
assert(widget.match(/defaultIndicatorEntries: \[ "PasswordlessSudo", "Dictation"/), 'passwordless sudo is included in the default indicator tray')
|
||||
assert(manifest.barWidget.schema.find(field => field.key === 'items').options.some(option => option.value === 'PasswordlessSudo'), 'passwordless sudo is configurable alongside Night Light')
|
||||
assert(indicator.includes('useActiveColor: true') && indicator.includes('activeColor: Color.urgent'), 'active passwordless sudo uses the theme danger color')
|
||||
assertEqual(indicator.match(/activeText: "([^"]+)"/)[1], indicator.match(/inactiveText: "([^"]+)"/)[1], 'sudo keeps the same icon in both states')
|
||||
assert(!widget.includes('sudoHorizontal') && !widget.includes('sudoVertical'), 'sudo participates in the normal indicator blocks')
|
||||
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('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')
|
||||
const press = new Function('root', 'disableProc', indicator.match(/onPressed: function\(\) \{([\s\S]*?)\n \}/)[1])
|
||||
const launched = []
|
||||
const button = { granted: true, bar: { run: command => launched.push(command) } }
|
||||
const revoke = { running: false }
|
||||
press(button, revoke)
|
||||
assert(revoke.running && launched.length === 0, 'clicking active sudo starts background revocation without a terminal')
|
||||
button.granted = false
|
||||
press(button, revoke)
|
||||
assert(launched.length === 0, 'repeat clicks during revocation cannot open the enable flow')
|
||||
revoke.running = false
|
||||
press(button, revoke)
|
||||
assertDeepEqual(launched, ['omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless'], 'inactive click still opens interactive setup')
|
||||
JS
|
||||
Reference in new issue
Block a user