From 05bb82b34efe066e82349d1bc41e8e87ec6501f6 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Tue, 11 Aug 2026 11:04:48 -0400 Subject: [PATCH] Add checkout bin to sudoers path --- bin/omarchy-dev-link | 43 +++++++++++ bin/omarchy-dev-status | 22 ++++++ bin/omarchy-dev-unlink | 10 +++ docs/file-layout.md | 13 ++++ test/shell.d/dev-link-test.sh | 124 ++++++++++++++++++++++++++++++++ test/shell.d/dev-unlink-test.sh | 6 ++ 6 files changed, 218 insertions(+) create mode 100644 test/shell.d/dev-link-test.sh diff --git a/bin/omarchy-dev-link b/bin/omarchy-dev-link index 00fba9d8..ccaa404f 100755 --- a/bin/omarchy-dev-link +++ b/bin/omarchy-dev-link @@ -14,6 +14,15 @@ fi prompt_reboot=1 +# sudo resolves a bare command name against secure_path, never the caller's +# PATH, so a dev-linked checkout is invisible to `sudo omarchy-*`: a command the +# package does not ship yet fails outright, and one it does ship silently runs +# the packaged copy while every unprivileged call runs the checkout. Prepending +# the checkout's bin keeps root on the code being edited — the same trust the +# link already extends to every system script Omarchy runs out of $OMARCHY_PATH. +sudoers_file="/etc/sudoers.d/omarchy-dev-path" +system_secure_path="/usr/local/sbin:/usr/local/bin:/usr/bin" + if (( $# < 1 || $# > 2 )) || [[ $1 == "-h" || $1 == "--help" ]]; then cat < [--no-reboot] @@ -28,6 +37,10 @@ themes/, applications/, config/. Files installed at fixed system paths creation, /usr/share/plymouth) are NOT covered — for those, use omarchy-dev-pkg-test to build and install the package from the checkout. +Also writes $sudoers_file so sudo resolves omarchy-* +from the checkout instead of the packaged copies. That part takes effect +immediately, no reboot needed. + Use --no-reboot when another command will handle the reboot prompt. USAGE exit 0 @@ -51,6 +64,16 @@ omarchy_conf_quote() { printf '"%s"' "$value" } +# A double-quoted sudoers string takes a backslash escape for a literal +# backslash or quote, and nothing else — a checkout path with a space in it is +# already covered by the quotes. +sudoers_quote() { + local value="$1" + value=${value//\\/\\\\} + value=${value//\"/\\\"} + printf '"%s"' "$value" +} + target=$(realpath -e "$1" 2>/dev/null) || { echo "Error: path does not exist: $1" >&2 exit 1 @@ -62,13 +85,33 @@ for required in bin default shell; do fi done +# Staged and parsed before anything is installed: a sudoers file sudo refuses to +# read takes every rule after it down with it, including the %wheel grant, and +# the password prompt needed to undo that is on the other side of the breakage. +staged_sudoers=$(mktemp) +trap 'rm -f "$staged_sudoers"' EXIT + +{ + printf 'Defaults secure_path=' + sudoers_quote "$target/bin:$system_secure_path" + printf '\n' +} >"$staged_sudoers" + +if ! visudo -cf "$staged_sudoers" >/dev/null; then + echo "Error: refusing to install an invalid $sudoers_file for $target" >&2 + exit 1 +fi + { printf 'export OMARCHY_PATH=' omarchy_conf_quote "$target" printf '\n' } | sudo tee /etc/omarchy.conf >/dev/null +sudo install -Dm440 -o root -g root "$staged_sudoers" "$sudoers_file" + echo "Pointed Omarchy at $target" +echo "sudo now resolves omarchy-* from $target/bin" echo if (( prompt_reboot )) && gum confirm "Reboot now to activate?"; then diff --git a/bin/omarchy-dev-status b/bin/omarchy-dev-status index 07c66093..19817a1b 100755 --- a/bin/omarchy-dev-status +++ b/bin/omarchy-dev-status @@ -6,6 +6,7 @@ set -euo pipefail default_target="/usr/share/omarchy" +sudoers_file="/etc/sudoers.d/omarchy-dev-path" configured="$default_target" conf_present=0 linked=0 @@ -23,9 +24,30 @@ if [[ -f /etc/omarchy.conf ]]; then fi fi +# /etc/sudoers.d is root-only, so report what sudo resolves rather than reading +# the drop-in — and say so plainly instead of guessing when there is no cached +# credential to ask with. A missing entry here is what makes `sudo omarchy-*` +# run the packaged copy of a command the checkout has changed. +sudo_bin_dir() { + local resolved + + if ! sudo -n true 2>/dev/null; then + echo "unknown (needs sudo)" + return + fi + + resolved=$(sudo -n bash -c 'type -P omarchy-dev-status' 2>/dev/null) || { + echo "not on sudo's PATH" + return + } + + dirname "$resolved" +} + if (( linked )); then echo "dev-link: configured" echo " /etc/omarchy.conf -> OMARCHY_PATH=$configured" + echo " sudo resolves omarchy-* from: $(sudo_bin_dir)" echo " status: reboot required before all session layers use this checkout" else echo "dev-link: inactive" diff --git a/bin/omarchy-dev-unlink b/bin/omarchy-dev-unlink index 020774f6..b1892639 100755 --- a/bin/omarchy-dev-unlink +++ b/bin/omarchy-dev-unlink @@ -13,6 +13,8 @@ fi prompt_reboot=1 +sudoers_file="/etc/sudoers.d/omarchy-dev-path" + if (( $# > 1 )); then echo "Usage: omarchy dev unlink [--no-reboot]" >&2 exit 1 @@ -32,6 +34,9 @@ Writes /etc/omarchy.conf so OMARCHY_PATH resolves to /usr/share/omarchy after reboot. This intentionally does not rewrite the running Hyprland, systemd, shell, or app-launcher environment; reboot to make every layer agree. +Removes $sudoers_file, the drop-in that pointed sudo +at the checkout, so sudo goes back to the packaged omarchy-* immediately. + Use --no-reboot when another command will handle the reboot prompt. USAGE exit 0 @@ -46,6 +51,11 @@ default_target="/usr/share/omarchy" printf 'export OMARCHY_PATH="%s"\n' "$default_target" | sudo tee /etc/omarchy.conf >/dev/null +# omarchy-dev-link prepended the checkout to sudo's secure_path. Drop it in the +# same step that drops the checkout, or sudo keeps running a tree nothing else +# points at — and keeps trusting a user-writable directory for root's commands. +sudo rm -f "$sudoers_file" + echo "Pointed Omarchy at $default_target" echo diff --git a/docs/file-layout.md b/docs/file-layout.md index 8b577380..90b9b200 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -158,6 +158,19 @@ Sourced by every entry point that needs the env set: Idempotent — safe to source more than once in the same shell. +`PATH` covers everything the user runs, but not `sudo`, which resolves command +names against `secure_path` from `/etc/sudoers`. So `omarchy-dev-link` also +writes `/etc/sudoers.d/omarchy-dev-path`: + +``` +Defaults secure_path="/bin:/usr/local/sbin:/usr/local/bin:/usr/bin" +``` + +Without it, `sudo omarchy-*` fails for a command the package has not shipped +yet and silently runs the packaged copy of one it has. The drop-in is validated +with `visudo -c` before install and removed by `omarchy-dev-unlink`; unlike +`/etc/omarchy.conf`, it takes effect without a reboot. + ## Runtime finalization (`omarchy-finalize-user`) Runs once per user. It does **not** copy `~/.config/**`, `~/.bashrc`, diff --git a/test/shell.d/dev-link-test.sh b/test/shell.d/dev-link-test.sh new file mode 100644 index 00000000..748b3780 --- /dev/null +++ b/test/shell.d/dev-link-test.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +log_file="$test_tmp/dev-link.log" +conf_file="$test_tmp/omarchy.conf" +sudoers_file="$test_tmp/omarchy-dev-path" +mkdir -p "$stub_bin" "$test_tmp/home" + +cat >"$stub_bin/sudo" <<'SH' +#!/bin/bash + +printf 'sudo' >>"$OMARCHY_DEV_LINK_TEST_LOG" +for arg in "$@"; do + printf '\t%s' "$arg" >>"$OMARCHY_DEV_LINK_TEST_LOG" +done +printf '\n' >>"$OMARCHY_DEV_LINK_TEST_LOG" + +case "$1" in + tee) + cat >"$OMARCHY_DEV_LINK_TEST_CONF" + ;; + install) + # The staged file is the second-to-last argument. + cp "${@: -2:1}" "$OMARCHY_DEV_LINK_TEST_SUDOERS" + ;; +esac +SH +chmod +x "$stub_bin/sudo" + +cat >"$stub_bin/gum" <<'SH' +#!/bin/bash + +printf 'gum' >>"$OMARCHY_DEV_LINK_TEST_LOG" +for arg in "$@"; do + printf '\t%s' "$arg" >>"$OMARCHY_DEV_LINK_TEST_LOG" +done +printf '\n' >>"$OMARCHY_DEV_LINK_TEST_LOG" +SH +chmod +x "$stub_bin/gum" + +cat >"$stub_bin/omarchy-system-reboot" <<'SH' +#!/bin/bash + +printf 'reboot\n' >>"$OMARCHY_DEV_LINK_TEST_LOG" +SH +chmod +x "$stub_bin/omarchy-system-reboot" + +run_link() { + HOME="$test_tmp/home" \ + OMARCHY_DEV_LINK_TEST_LOG="$log_file" \ + OMARCHY_DEV_LINK_TEST_CONF="$conf_file" \ + OMARCHY_DEV_LINK_TEST_SUDOERS="$sudoers_file" \ + PATH="$stub_bin:$PATH" \ + "$ROOT/bin/omarchy-dev-link" "$@" +} + +make_checkout() { + local checkout="$test_tmp/$1" + + mkdir -p "$checkout/bin" "$checkout/default" "$checkout/shell" + printf '%s' "$checkout" +} + +checkout=$(make_checkout checkout) + +: >"$log_file" +: >"$sudoers_file" +run_link "$checkout" --no-reboot >"$test_tmp/link.out" + +[[ $(<"$conf_file") == "export OMARCHY_PATH=\"$checkout\"" ]] || + fail "dev link points OMARCHY_PATH at the checkout" "$(<"$conf_file")" +pass "dev link points OMARCHY_PATH at the checkout" + +# sudo reads secure_path, not the caller's PATH, so the checkout has to come +# first there too or `sudo omarchy-*` runs the packaged copy. +[[ $(<"$sudoers_file") == "Defaults secure_path=\"$checkout/bin:/usr/local/sbin:/usr/local/bin:/usr/bin\"" ]] || + fail "dev link prepends the checkout to sudo's secure_path" "$(<"$sudoers_file")" +pass "dev link prepends the checkout to sudo's secure_path" + +grep -Eq $'^sudo\tinstall\t-Dm440\t-o\troot\t-g\troot\t[^\t]+\t/etc/sudoers\\.d/omarchy-dev-path$' "$log_file" || + fail "dev link installs the drop-in root-owned and read-only" "$(cat "$log_file")" +pass "dev link installs the drop-in root-owned and read-only" + +visudo -cf "$sudoers_file" >/dev/null || + fail "dev link writes a sudoers drop-in sudo can parse" "$(<"$sudoers_file")" +pass "dev link writes a sudoers drop-in sudo can parse" + +grep -F "sudo now resolves omarchy-* from $checkout/bin" "$test_tmp/link.out" >/dev/null || + fail "dev link reports the sudo change" "$(cat "$test_tmp/link.out")" +pass "dev link reports the sudo change" + +if grep -Eq '^(gum|reboot)' "$log_file"; then + fail "dev link --no-reboot skips the reboot prompt" "$(cat "$log_file")" +fi +pass "dev link --no-reboot skips the reboot prompt" + +# A path sudoers would have to escape, not one the shell alone handles. +quoted_checkout=$(make_checkout 'check "out"') + +: >"$log_file" +: >"$sudoers_file" +run_link "$quoted_checkout" --no-reboot >/dev/null + +visudo -cf "$sudoers_file" >/dev/null || + fail "dev link escapes a checkout path for sudoers" "$(<"$sudoers_file")" +pass "dev link escapes a checkout path for sudoers" + +: >"$log_file" +if run_link "$test_tmp/missing" --no-reboot >/dev/null 2>"$test_tmp/missing.err"; then + fail "dev link rejects a path that does not exist" +fi +grep -F "Error: path does not exist: $test_tmp/missing" "$test_tmp/missing.err" >/dev/null || + fail "dev link explains a path that does not exist" "$(cat "$test_tmp/missing.err")" +if grep -q 'sudo' "$log_file"; then + fail "dev link touches nothing when the path does not exist" "$(cat "$log_file")" +fi +pass "dev link rejects a path that does not exist" diff --git a/test/shell.d/dev-unlink-test.sh b/test/shell.d/dev-unlink-test.sh index fb952fca..1fdecf93 100644 --- a/test/shell.d/dev-unlink-test.sh +++ b/test/shell.d/dev-unlink-test.sh @@ -60,6 +60,12 @@ grep -Fx $'sudo\ttee\t/etc/omarchy.conf' "$log_file" >/dev/null || fail "dev unlink writes the package path without rebooting" "$(cat "$log_file")" [[ $(<"$conf_file") == 'export OMARCHY_PATH="/usr/share/omarchy"' ]] || fail "dev unlink writes the package path guard" "$(<"$conf_file")" + +# Left behind, it keeps sudo running a checkout nothing else points at. +grep -Fx $'sudo\trm\t-f\t/etc/sudoers.d/omarchy-dev-path' "$log_file" >/dev/null || + fail "dev unlink drops the sudo secure_path drop-in" "$(cat "$log_file")" +pass "dev unlink drops the sudo secure_path drop-in" + if grep -Eq '^(gum|reboot)' "$log_file"; then fail "dev unlink --no-reboot skips the reboot prompt" "$(cat "$log_file")" fi