diff --git a/bin/omarchy-dns b/bin/omarchy-dns index fdb49183..a22e028c 100755 --- a/bin/omarchy-dns +++ b/bin/omarchy-dns @@ -33,45 +33,28 @@ provider_from_arg() { # 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 +# True when sudo would run this exact command without stopping for a password. +# `sudo -l` on its own reports whether a command is permitted, which the blanket +# %wheel rule answers yes to for everything; the long listing prints the matched +# entry's tags, so !authenticate is the grant in etc/sudoers.d/omarchy-dns and +# nothing else. Listing runs nothing and, under -n, prompts for nothing, so a +# machine whose omarchy-settings predates that file falls through to polkit +# instead of dying on a password prompt it has no terminal to show. +sudo_grants_passwordless() { + sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate' } require_root() { if (( EUID == 0 )); then return - fi - - # A terminal can carry sudo's own password prompt. Without one, sudo is still - # right when etc/sudoers.d/omarchy-dns covers this invocation: that grant is - # what keeps the panel's one-click provider switch from raising a polkit - # 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 + elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then + # A terminal can carry sudo's own password prompt. Without one, sudo is + # right only where the grant reaches; polkit can at least put a prompt on + # screen, and offer to authenticate as someone else. exec sudo "$PACKAGED_PATH" "$@" + else + exec pkexec "$PACKAGED_PATH" "$@" fi - - exec pkexec "$PACKAGED_PATH" "$@" } networkmanager_global_dns() { diff --git a/test/shell.d/dns-sudoers-test.sh b/test/shell.d/dns-sudoers-test.sh index 25cb2272..e8424e4a 100755 --- a/test/shell.d/dns-sudoers-test.sh +++ b/test/shell.d/dns-sudoers-test.sh @@ -22,12 +22,11 @@ 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" +# `sudo -l` on its own answers whether a command is permitted, not whether it +# is passwordless, and Omarchy ships a blanket %wheel rule that permits +# everything. Only the long listing prints the matched entry's tags. +grep -E 'sudo -n -l -l' "$dns" >/dev/null || + fail "omarchy-dns reads the grant from the long sudo listing" pass "dns sudoers rule is scoped to the stock providers" @@ -44,25 +43,32 @@ 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" <"$stub_bin/pkexec" <<'SH' #!/bin/bash -printf '$command %s\n' "\$*" >"\$ELEVATION_LOG" +printf 'pkexec %s\n' "$*" >"$ELEVATION_LOG" SH - chmod +x "$stub_bin/$command" -done +chmod +x "$stub_bin/pkexec" -# 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' +# The sudo stub plays both parts: it answers the passwordless probe from +# STUB_GRANTED, the providers etc/sudoers.d/omarchy-dns covers on this machine, +# and logs the elevation otherwise. STUB_GRANTED empty stands for an install +# whose omarchy-settings predates the file. +cat >"$stub_bin/sudo" <<'SH' #!/bin/bash -[[ ${1:-} == -nG ]] || exec /usr/bin/id "$@" -printf '%s\n' "${STUB_GROUPS-users wheel}" +if [[ $1 == -n && $2 == -l ]]; then + for granted in ${STUB_GRANTED-Cloudflare Google DHCP}; do + [[ ${!#} == "$granted" ]] || continue + echo " Options: !authenticate" + exit 0 + done + echo " Matched: ${!#}" + exit 0 +fi +printf 'sudo %s\n' "$*" >"$ELEVATION_LOG" SH -chmod +x "$stub_bin/id" +chmod +x "$stub_bin/sudo" elevation_for() { : >"$test_tmp/elevation" @@ -90,8 +96,12 @@ 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" +# The grant is what makes sudo passwordless, so its absence -- an install still +# on an older omarchy-settings, or a user outside %wheel the rule cannot match +# -- has to route to polkit. Betting on sudo here leaves the panel's one-click +# toggle execing into a password prompt it has no terminal to show. +ungranted=$(STUB_GRANTED="" elevation_for Cloudflare) +[[ $ungranted == "pkexec /usr/bin/omarchy-dns Cloudflare" ]] || + fail "omarchy-dns falls back to polkit where the sudoers grant is not installed" "got: $ungranted" pass "omarchy-dns falls back to polkit wherever the grant does not reach"