Switch DNS providers without a password prompt (#7472)
* Switch DNS providers without a password prompt The network panel and the menu run omarchy-dns from a process with no terminal, so require_root reached for pkexec and put a polkit password prompt in front of what is meant to be a one-click toggle. Grant %wheel passwordless sudo for the three stock providers and take that path whenever the grant covers the invocation. Custom stays out of the grant: it points the machine at servers the caller supplies, and it already runs in a terminal that can ask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Pick the elevation path without asking sudo The `sudo -n -l` probe answered the wrong question. It reports whether a command is permitted, not whether it is passwordless, and the %wheel rule every Omarchy install ships permits everything -- `sudo -n -l /usr/bin/rm -rf /tmp/x` exits 0. So the probe passed for Custom too, and the exec below it ran `sudo -n`, which fails outright with no terminal and no way back to pkexec. Decide from what the sudoers rule actually says instead: sudo when there is a terminal to type into, or when the resolved path and the provider are both ones the rule names. Everything else keeps going through polkit. Pin a root-owned PATH once elevated, too. `omarchy dev link` puts a user-writable checkout ahead of sudo's secure_path for every command, so a passwordless grant on a script that resolves nmcli, tee, and install through PATH would otherwise hand root to whoever can write there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep users outside %wheel on the polkit path The rule grants %wheel, so path and provider alone do not mean sudo will take it. A user outside the group was sent to sudo anyway, and with no terminal to answer the prompt that is a dead end -- polkit at least offers to authenticate as somebody else. Two holes in the test alongside it: it accepted any file containing the expected rule, so a second, argument-free line would have widened the grant unnoticed, and run as root it would have sailed past the stubs and rewritten the host's own DNS config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Elevate the system install, whatever copy was invoked The rule names /usr/bin/omarchy-dns, so a dev-linked checkout handed sudo a path nothing could match and fell back to a polkit prompt. Re-exec the packaged path instead: the privileged half is the system install everywhere, the grant matches everywhere, and the path comparison and the PATH pinning that existed to work around the checkout both go away. Dev-linked checkouts run their own unprivileged half and the installed one as root, which is the trade for not carrying a second code path. --------- Co-authored-by: Omabot <david@hey.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Omabot
Claude Opus 5
parent
a4219f8f4a
commit
3765e8010b
+36
-6
@@ -28,8 +28,28 @@ provider_from_arg() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
self_path() {
|
# The path etc/sudoers.d/omarchy-dns names. The privileged half always runs from
|
||||||
readlink -f "$OMARCHY_PATH/bin/${BASH_SOURCE[0]##*/}"
|
# there rather than from whichever copy was invoked, so the rule matches even
|
||||||
|
# where $OMARCHY_PATH points at a checkout.
|
||||||
|
PACKAGED_PATH=/usr/bin/omarchy-dns
|
||||||
|
|
||||||
|
# True when etc/sudoers.d/omarchy-dns covers this invocation. Both halves of the
|
||||||
|
# rule have to hold -- one of the three providers, and %wheel -- or sudo asks
|
||||||
|
# for a password like any other command.
|
||||||
|
grant_covers() {
|
||||||
|
local provider="${1:-}"
|
||||||
|
local group
|
||||||
|
|
||||||
|
case "$provider" in
|
||||||
|
Cloudflare | Google | DHCP) ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for group in $(id -nG 2>/dev/null); do
|
||||||
|
[[ $group == wheel ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
require_root() {
|
require_root() {
|
||||||
@@ -37,11 +57,21 @@ require_root() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -t 0 ]]; then
|
# A terminal can carry sudo's own password prompt. Without one, sudo is still
|
||||||
exec sudo "$(self_path)" "$@"
|
# right when etc/sudoers.d/omarchy-dns covers this invocation: that grant is
|
||||||
else
|
# what keeps the panel's one-click provider switch from raising a polkit
|
||||||
exec pkexec "$(self_path)" "$@"
|
# prompt. Custom and a caller outside %wheel still go through polkit, which
|
||||||
|
# can at least offer to authenticate as someone else.
|
||||||
|
#
|
||||||
|
# Do not swap this for a `sudo -n -l` probe. That reports whether a command is
|
||||||
|
# permitted, not whether it is passwordless, so the blanket %wheel rule
|
||||||
|
# answers yes for every argument and Custom dies on `sudo -n` instead of
|
||||||
|
# falling through to pkexec.
|
||||||
|
if [[ -t 0 ]] || grant_covers "${1:-}"; then
|
||||||
|
exec sudo "$PACKAGED_PATH" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
exec pkexec "$PACKAGED_PATH" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
networkmanager_global_dns() {
|
networkmanager_global_dns() {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Switching between the stock providers is a one-click toggle in the network
|
||||||
|
# panel, so it must not stop for a password. Custom is deliberately absent: it
|
||||||
|
# points the machine at servers the caller supplies, and it already runs in a
|
||||||
|
# terminal that can ask.
|
||||||
|
%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-dns Cloudflare, /usr/bin/omarchy-dns Google, /usr/bin/omarchy-dns DHCP
|
||||||
Executable
+97
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(dirname "$0")/base-test.sh"
|
||||||
|
|
||||||
|
dns="$ROOT/bin/omarchy-dns"
|
||||||
|
sudoers_file="$ROOT/etc/sudoers.d/omarchy-dns"
|
||||||
|
rule='%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-dns Cloudflare, /usr/bin/omarchy-dns Google, /usr/bin/omarchy-dns DHCP'
|
||||||
|
|
||||||
|
# Exactly one rule, matched whole. A second line -- or the same command with its
|
||||||
|
# arguments dropped, which sudoers reads as "any arguments" -- would widen the
|
||||||
|
# grant while leaving this line in place.
|
||||||
|
rules=$(grep -vE '^[[:space:]]*(#|$)' "$sudoers_file")
|
||||||
|
[[ $rules == "$rule" ]] ||
|
||||||
|
fail "dns sudoers file carries exactly the stock-provider rule and nothing else" "got: $rules"
|
||||||
|
|
||||||
|
if command -v visudo >/dev/null; then
|
||||||
|
visudo -cf "$sudoers_file" >/dev/null || fail "dns sudoers rule parses"
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -Fx 'PACKAGED_PATH=/usr/bin/omarchy-dns' "$dns" >/dev/null ||
|
||||||
|
fail "omarchy-dns elevates the path the sudoers rule names"
|
||||||
|
|
||||||
|
# sudo -l answers whether a command is permitted, not whether it is
|
||||||
|
# passwordless, and Omarchy ships a blanket %wheel rule that permits
|
||||||
|
# everything. A probe built on it sends Custom into `sudo -n`, which fails
|
||||||
|
# outright instead of falling through to pkexec.
|
||||||
|
! grep -E '^[[:space:]]*[^#[:space:]].*sudo -n -l' "$dns" >/dev/null ||
|
||||||
|
fail "omarchy-dns does not decide elevation with a sudo -l probe"
|
||||||
|
|
||||||
|
pass "dns sudoers rule is scoped to the stock providers"
|
||||||
|
|
||||||
|
# require_root returns immediately for root, so the stubs below would not stand
|
||||||
|
# between the script and the host's real NetworkManager and resolved config.
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
pass "running as root; skipping the elevation checks, which would rewrite this machine's DNS"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
test_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
stub_bin="$test_tmp/bin"
|
||||||
|
mkdir -p "$stub_bin"
|
||||||
|
|
||||||
|
# Both stubs stand in for the exec at the end of require_root, so the DNS writes
|
||||||
|
# below it never run, and neither real sudo nor real pkexec is reached.
|
||||||
|
for command in sudo pkexec; do
|
||||||
|
cat >"$stub_bin/$command" <<SH
|
||||||
|
#!/bin/bash
|
||||||
|
printf '$command %s\n' "\$*" >"\$ELEVATION_LOG"
|
||||||
|
SH
|
||||||
|
chmod +x "$stub_bin/$command"
|
||||||
|
done
|
||||||
|
|
||||||
|
# The rule is %wheel-scoped, so group membership decides the route as much as
|
||||||
|
# the provider does. Stub id rather than reading the real groups: the suite has
|
||||||
|
# to give the same answer on a build user outside wheel.
|
||||||
|
cat >"$stub_bin/id" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
[[ ${1:-} == -nG ]] || exec /usr/bin/id "$@"
|
||||||
|
printf '%s\n' "${STUB_GROUPS-users wheel}"
|
||||||
|
SH
|
||||||
|
chmod +x "$stub_bin/id"
|
||||||
|
|
||||||
|
elevation_for() {
|
||||||
|
: >"$test_tmp/elevation"
|
||||||
|
ELEVATION_LOG="$test_tmp/elevation" \
|
||||||
|
PATH="$stub_bin:$PATH" \
|
||||||
|
bash "$dns" "$1" </dev/null >/dev/null
|
||||||
|
cat "$test_tmp/elevation"
|
||||||
|
}
|
||||||
|
|
||||||
|
for provider in Cloudflare Google DHCP; do
|
||||||
|
elevation=$(elevation_for "$provider")
|
||||||
|
[[ $elevation == "sudo /usr/bin/omarchy-dns $provider" ]] ||
|
||||||
|
fail "omarchy-dns takes the passwordless sudo grant for $provider without a terminal" "got: $elevation"
|
||||||
|
done
|
||||||
|
|
||||||
|
pass "omarchy-dns elevates the stock providers through sudo, not polkit"
|
||||||
|
|
||||||
|
# A dev-linked checkout elevates the packaged path like everyone else, rather
|
||||||
|
# than handing sudo a path no rule can name and losing the grant.
|
||||||
|
dev_linked=$(OMARCHY_PATH="$test_tmp/checkout" elevation_for Cloudflare)
|
||||||
|
[[ $dev_linked == "sudo /usr/bin/omarchy-dns Cloudflare" ]] ||
|
||||||
|
fail "omarchy-dns elevates the system install wherever OMARCHY_PATH points" "got: $dev_linked"
|
||||||
|
|
||||||
|
custom=$(elevation_for Custom)
|
||||||
|
[[ $custom == "pkexec /usr/bin/omarchy-dns Custom" ]] ||
|
||||||
|
fail "omarchy-dns leaves Custom on the polkit path, since no sudoers rule covers it" "got: $custom"
|
||||||
|
|
||||||
|
non_wheel=$(STUB_GROUPS="users" elevation_for Cloudflare)
|
||||||
|
[[ $non_wheel == "pkexec /usr/bin/omarchy-dns Cloudflare" ]] ||
|
||||||
|
fail "omarchy-dns leaves a user outside %wheel on the polkit path, since the rule cannot match them" "got: $non_wheel"
|
||||||
|
|
||||||
|
pass "omarchy-dns falls back to polkit wherever the grant does not reach"
|
||||||
Reference in New Issue
Block a user