Keep channel transitions inside command-scoped sudo
This commit is contained in:
1 parent
5bc41b2585
commit
c8697407cb
6 files changed
+188
-22
No files matched your search
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||
copy_boundary_file bin/omarchy-channel-set
|
||||
copy_boundary_file bin/omarchy-refresh-pacman
|
||||
copy_boundary_file bin/omarchy-update
|
||||
export OMARCHY_UPDATE_LOGGED=1
|
||||
|
||||
# Relocate the package root into the fixture, including the explicit handoff
|
||||
# from the development checkout. All privileged operations remain stand-ins.
|
||||
python3 - "$SUDO_TEST_ROOT/bin/omarchy-channel-set" "$SUDO_TEST_ROOT" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
p = Path(sys.argv[1])
|
||||
p.write_text(p.read_text().replace('/usr/share/omarchy', sys.argv[2]))
|
||||
PY
|
||||
|
||||
for command in omarchy-dev-link omarchy-dev-unlink omarchy-state gum git; do
|
||||
cat >"$SUDO_TEST_ROOT/bin/$command" <<'STUB'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
step=${0##*/}
|
||||
printf 'step:%s %s\n' "$step" "$*" >>"$SUDO_TEST_LOG"
|
||||
case "$step" in
|
||||
omarchy-dev-link|omarchy-dev-unlink) sudo /usr/bin/true ;;
|
||||
git)
|
||||
[[ $1 == "clone" ]] || exit 90
|
||||
/usr/bin/cp -a "$SUDO_TEST_ROOT" "${@: -1}"
|
||||
mkdir -p "${@: -1}/.git" "${@: -1}/shell"
|
||||
;;
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$SUDO_TEST_ROOT/bin/$command"
|
||||
done
|
||||
|
||||
assert_scoped_channel() {
|
||||
local label=$1
|
||||
assert_boundary_cold "$label"
|
||||
python3 - "$SUDO_TEST_LOG" <<'PY'
|
||||
import sys
|
||||
events = open(sys.argv[1]).read().splitlines()
|
||||
assert events[0] == 'sudo -k', events
|
||||
sudo = [event for event in events if event.startswith('sudo ')]
|
||||
assert all(event in ('sudo -h', 'sudo -k') or event.startswith('sudo -N ') for event in sudo), events
|
||||
hooks = [i for i, event in enumerate(events) if event.startswith('step:omarchy-hook ')]
|
||||
assert len(hooks) == 2, events
|
||||
assert events[hooks[-1]] == 'step:omarchy-hook pre-refresh-pacman', events
|
||||
assert not any(event.startswith('sudo -N ') for event in events[hooks[0]:]), events
|
||||
PY
|
||||
}
|
||||
|
||||
run_channel() {
|
||||
"$OMARCHY_PATH/bin/omarchy-channel-set" "$@" >"$boundary_tmp/output" 2>&1
|
||||
}
|
||||
for channel in stable rc edge dev; do
|
||||
reset_boundary
|
||||
run_channel "$channel" || fail "$channel failed" "$(<"$boundary_tmp/output")"
|
||||
assert_scoped_channel "$channel"
|
||||
pass "$channel starts cold, authorizes only individual commands, defers hooks and exits cold"
|
||||
done
|
||||
|
||||
reset_boundary
|
||||
OMARCHY_PATH="$SUDO_TEST_HOME/omarchy" run_channel stable || fail "leaving dev failed" "$(<"$boundary_tmp/output")"
|
||||
assert_scoped_channel "dev to stable"
|
||||
pass "leaving dev preserves no-update sudo through unlink and the packaged update"
|
||||
|
||||
mkdir "$boundary_tmp/user tools"
|
||||
cat >"$boundary_tmp/user tools/channel-user-tool" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'user-tool:%s\n' "$*" >>"$SUDO_TEST_LOG"
|
||||
STUB
|
||||
chmod +x "$boundary_tmp/user tools/channel-user-tool"
|
||||
for command in omarchy-hook omarchy-update-mise; do
|
||||
rm "$SUDO_TEST_ROOT/bin/$command"
|
||||
cat >"$SUDO_TEST_ROOT/bin/$command" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
|
||||
[[ $(command -v sudo) == "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" ]] || exit 92
|
||||
channel-user-tool "${0##*/}" "$@"
|
||||
STUB
|
||||
chmod +x "$SUDO_TEST_ROOT/bin/$command"
|
||||
done
|
||||
reset_boundary
|
||||
PATH="$boundary_tmp/user tools:$PATH" run_channel stable || fail "channel hooks lost the user's PATH" "$(<"$boundary_tmp/output")"
|
||||
for event in 'omarchy-hook post-update' 'omarchy-update-mise' 'omarchy-hook pre-refresh-pacman'; do
|
||||
grep -Fxq "user-tool:$event" "$SUDO_TEST_LOG" || fail "user PATH was not preserved for $event"
|
||||
done
|
||||
assert_boundary_cold "channel user PATH"
|
||||
for command in omarchy-hook omarchy-update-mise; do
|
||||
ln -sfn test-step "$SUDO_TEST_ROOT/bin/$command"
|
||||
done
|
||||
pass "channel switching preserves user tools behind the wrapper for both hooks and mise"
|
||||
|
||||
for step in pacman omarchy-update-system-pkgs omarchy-hook; do
|
||||
reset_boundary
|
||||
if SUDO_TEST_FAIL_STEP="$step" run_channel stable; then fail "$step failure was ignored"; fi
|
||||
assert_boundary_cold "$step failure"
|
||||
if grep -q '^step:omarchy-hook pre-refresh-pacman$' "$SUDO_TEST_LOG"; then fail "$step failure reached the deferred hook"; fi
|
||||
pass "$step failure exits cold without the deferred hook"
|
||||
done
|
||||
|
||||
for signal in HUP INT TERM; do
|
||||
reset_boundary
|
||||
cat >"$SUDO_TEST_ROOT/bin/omarchy-dev-unlink" <<'STUB'
|
||||
#!/bin/bash
|
||||
sudo /usr/bin/true || exit 1
|
||||
kill -s "$SUDO_TEST_CHANNEL_SIGNAL" "$PPID"
|
||||
STUB
|
||||
if SUDO_TEST_CHANNEL_SIGNAL="$signal" run_channel stable; then fail "$signal was ignored"; fi
|
||||
assert_boundary_cold "$signal"
|
||||
if grep -q '^step:omarchy-hook ' "$SUDO_TEST_LOG"; then fail "$signal reached an update hook"; fi
|
||||
pass "$signal stops the channel transition and revokes authorization"
|
||||
done
|
||||
|
||||
for refusal in unsupported-sudo failed-revocation ordinary-bash; do
|
||||
reset_boundary
|
||||
case "$refusal" in
|
||||
unsupported-sudo) export SUDO_TEST_UNSUPPORTED=1 ;;
|
||||
failed-revocation) export SUDO_TEST_REVOKE_FAIL=1 ;;
|
||||
esac
|
||||
if [[ $refusal == "ordinary-bash" ]]; then
|
||||
if /usr/bin/bash "$SUDO_TEST_ROOT/bin/omarchy-channel-set" -p >"$boundary_tmp/output" 2>&1; then fail "$refusal was accepted"; fi
|
||||
elif run_channel stable; then
|
||||
fail "$refusal was accepted"
|
||||
fi
|
||||
if grep -q '^step:' "$SUDO_TEST_LOG"; then fail "$refusal reached channel work"; fi
|
||||
pass "$refusal is rejected before channel work"
|
||||
done
|
||||
@@ -4,10 +4,18 @@ set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||
test_tmp="$boundary_tmp"
|
||||
package_root="$SUDO_TEST_ROOT"
|
||||
copy_boundary_file bin/omarchy-channel-set
|
||||
python3 - "$SUDO_TEST_ROOT/bin/omarchy-channel-set" "$package_root" <<'PYTHON'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
p = Path(sys.argv[1])
|
||||
p.write_text(p.read_text().replace("/usr/share/omarchy", sys.argv[2]))
|
||||
PYTHON
|
||||
|
||||
stub_bin="$test_tmp/bin"
|
||||
stub_bin="$SUDO_TEST_ROOT/bin"
|
||||
log_file="$test_tmp/channel.log"
|
||||
mkdir -p "$stub_bin" "$test_tmp/home"
|
||||
|
||||
@@ -15,6 +23,7 @@ write_stub() {
|
||||
local name="$1"
|
||||
local body="$2"
|
||||
|
||||
rm -f "$stub_bin/$name"
|
||||
cat >"$stub_bin/$name" <<<"$body"
|
||||
chmod +x "$stub_bin/$name"
|
||||
}
|
||||
@@ -26,11 +35,17 @@ printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
'
|
||||
|
||||
write_stub sudo '#!/bin/bash
|
||||
case "${1:-}" in
|
||||
-h) echo "usage: sudo [-ABbEHkNnPS] command"; exit 0 ;;
|
||||
-k|-K) exit 0 ;;
|
||||
esac
|
||||
printf "sudo" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
'
|
||||
|
||||
cp "$stub_bin/sudo" "$SUDO_TEST_ROOT/mock/sudo"
|
||||
|
||||
write_stub omarchy-dev-unlink '#!/bin/bash
|
||||
printf "unlink" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
@@ -63,7 +78,8 @@ for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
if [[ $1 == "clone" ]]; then
|
||||
dest="${@: -1}"
|
||||
mkdir -p "$dest/.git" "$dest/bin" "$dest/default" "$dest/shell"
|
||||
/usr/bin/cp -a "$SUDO_TEST_ROOT" "$dest"
|
||||
mkdir -p "$dest/.git" "$dest/shell"
|
||||
fi
|
||||
'
|
||||
|
||||
@@ -90,10 +106,10 @@ esac
|
||||
run_channel() {
|
||||
: >"$log_file"
|
||||
OMARCHY_CHANNEL_TEST_LOG="$log_file" \
|
||||
OMARCHY_PATH="${OMARCHY_TEST_PATH:-/usr/share/omarchy}" \
|
||||
OMARCHY_PATH="${OMARCHY_TEST_PATH:-$package_root}" \
|
||||
HOME="$test_tmp/home" \
|
||||
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-channel-set" "$@"
|
||||
"${OMARCHY_TEST_PATH:-$package_root}/bin/omarchy-channel-set" "$@"
|
||||
}
|
||||
|
||||
assert_log_line() {
|
||||
@@ -106,9 +122,9 @@ assert_log_line() {
|
||||
|
||||
run_channel stable
|
||||
assert_log_line $'refresh\tstable\tdefer-hook' "stable refreshes the stable pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
|
||||
assert_log_line $'unlink\t--no-reboot' "stable restores the package-backed Omarchy path without an early reboot prompt"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "stable runs the normal update pipeline from the package-backed path"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "stable runs the normal update pipeline from the package-backed path"
|
||||
if grep -q $'^state\tset\treboot-required$' "$log_file"; then
|
||||
fail "stable does not require reboot when already package-backed" "$(cat "$log_file")"
|
||||
fi
|
||||
@@ -116,17 +132,19 @@ pass "stable does not require reboot when already package-backed"
|
||||
|
||||
run_channel rc
|
||||
assert_log_line $'refresh\trc\tdefer-hook' "rc refreshes the rc pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
|
||||
assert_log_line $'unlink\t--no-reboot' "rc restores the package-backed Omarchy path without an early reboot prompt"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "rc runs the normal update pipeline from the package-backed path"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "rc runs the normal update pipeline from the package-backed path"
|
||||
|
||||
OMARCHY_TEST_PATH="$ROOT" run_channel edge
|
||||
active_checkout="$test_tmp/active-checkout"
|
||||
cp -a "$package_root" "$active_checkout"
|
||||
OMARCHY_TEST_PATH="$active_checkout" run_channel edge
|
||||
assert_log_line $'refresh\tedge\tdefer-hook' "edge refreshes the edge pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
|
||||
assert_log_line $'unlink\t--no-reboot' "edge unlinks dev without an early reboot prompt"
|
||||
assert_log_line $'state\tset\treboot-required' "edge marks reboot required when leaving dev"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "edge runs the normal update pipeline from the package-backed path"
|
||||
[[ $(grep -E '^(unlink|state|update)' "$log_file") == $'unlink\t--no-reboot\nstate\tset\treboot-required\nupdate\t-y\tOMARCHY_PATH=/usr/share/omarchy' ]] ||
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$package_root" "edge runs the normal update pipeline from the package-backed path"
|
||||
[[ $(grep -E '^(unlink|state|update)' "$log_file") == $'unlink\t--no-reboot\nstate\tset\treboot-required\nupdate\t-y\tOMARCHY_PATH='"$package_root" ]] ||
|
||||
fail "edge defers the reboot prompt until the update restart stage" "$(cat "$log_file")"
|
||||
pass "edge defers the reboot prompt until the update restart stage"
|
||||
|
||||
@@ -146,12 +164,12 @@ rmdir "$checkout"
|
||||
run_channel dev
|
||||
assert_log_line $'gum\tconfirm\t--default=false\tSwitch to dev channel?' "dev asks for confirmation"
|
||||
assert_log_line $'refresh\tedge\tdefer-hook' "dev refreshes the edge pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
||||
assert_log_line $'sudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
||||
assert_log_line $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
|
||||
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "dev links ~/omarchy without an early reboot prompt"
|
||||
assert_log_line $'state\tset\treboot-required' "dev defers the reboot prompt to the update pipeline"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$checkout" "dev runs the normal update pipeline from the source checkout"
|
||||
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nsudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
|
||||
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nsudo\t-N\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
|
||||
fail "dev activates the checkout before changing or updating packages" "$(cat "$log_file")"
|
||||
pass "dev activates the checkout before changing or updating packages"
|
||||
[[ $(tail -1 "$log_file") == $'refresh\tedge\trun-deferred' ]] || fail "channel refresh hook must run after the complete update"
|
||||
|
||||
@@ -11,7 +11,7 @@ export OMARCHY_UPDATE_LOGGED=1
|
||||
# source the library beside the resolved command instead of this file.
|
||||
mkdir "$boundary_tmp/links"
|
||||
printf '%s\n' 'touch "$SUDO_TEST_HOME/wrong-library"' >"$boundary_tmp/links/omarchy-security-functions"
|
||||
for command in omarchy-update omarchy-refresh-pacman omarchy-update-stay-awake; do
|
||||
for command in omarchy-update omarchy-refresh-pacman omarchy-update-stay-awake omarchy-channel-set; do
|
||||
rm -f "$SUDO_TEST_ROOT/bin/$command"
|
||||
copy_boundary_file "bin/$command"
|
||||
ln -s "$SUDO_TEST_ROOT/bin/$command" "$boundary_tmp/links/$command"
|
||||
|
||||
@@ -51,7 +51,7 @@ pass "a package link must resolve to its named command"
|
||||
# Run the protected entrypoints themselves with a mismatched root. These must
|
||||
# stop before any sudo or operational fixture command, not merely validate in
|
||||
# an isolated library test.
|
||||
for command in omarchy-update omarchy-refresh-pacman omarchy-update-stay-awake; do
|
||||
for command in omarchy-update omarchy-refresh-pacman omarchy-update-stay-awake omarchy-channel-set; do
|
||||
rm -f "$SUDO_TEST_ROOT/bin/$command"
|
||||
copy_boundary_file "bin/$command"
|
||||
for root in "$boundary_tmp/other-root" .; do
|
||||
|
||||
Reference in new issue
Block a user