diff --git a/agents/skills/migrations.md b/agents/skills/migrations.md index 603a491d..2c97ebde 100644 --- a/agents/skills/migrations.md +++ b/agents/skills/migrations.md @@ -166,13 +166,14 @@ Omarchy 4.0 is upgraded through `bin/omarchy-upgrade-to-quattro`, not through th normal migration runner. Do not add compatibility migrations for old installer layouts; put pre-4 package-layout transition work in the upgrade command instead. -Clearing a pre-4 layout that is a security defect is the exception, and belongs in -a migration. The upgrade command only runs on a machine still making the 3 to 4 -crossing, so anything put there never reaches an install that crossed already — -and a file an old installer wrote with a vulnerability in it is still sitting on -those machines. The upgrade command finishes by running `omarchy-migrate` -(`run_post_upgrade_migrations`), so one migration reaches both populations; -a copy in the upgrade command would only be a second copy of the same predicate -to keep correct. Such a migration must name the defect it clears and match the -state the old installer actually produced, so a file the user wrote themselves is -left alone. +Clearing a privileged file that a retired installer left on disk is the exception, +and belongs in a migration whether or not that installer was part of a package +layout transition. The upgrade command only runs on a machine still making the 3 +to 4 crossing, so anything put there never reaches an install that crossed +already, and it never runs at all for an installer that was retired on its own — +while the file the installer wrote is still sitting on those machines. The upgrade +command finishes by running `omarchy-migrate` (`run_post_upgrade_migrations`), so +one migration reaches every population; a copy in the upgrade command would only +be a second copy of the same predicate to keep correct. Such a migration must name +the defect it clears and match what the old installer actually produced, so a file +an administrator wrote themselves is left alone. diff --git a/migrations/1788025225.sh b/migrations/1788025225.sh new file mode 100644 index 00000000..ce95b9bc --- /dev/null +++ b/migrations/1788025225.sh @@ -0,0 +1,256 @@ +echo "Remove privileged files left behind by retired Omarchy installers" + +sudoers_dir="${OMARCHY_SUDOERS_DIR:-/etc/sudoers.d}" +systemd_dir="${OMARCHY_SYSTEMD_SYSTEM_DIR:-/etc/systemd/system}" + +as_root() { + if (( EUID == 0 )); then + "$@" + else + sudo "$@" + fi +} + +# Three installers that no longer exist each left a root-owned file behind, and +# nothing in Omarchy has ever removed any of them. Each is judged against what +# the installer that wrote it actually produced, so a file of the same name that +# an administrator wrote themselves is left alone. +# +# Emit the lines a parser would act on: comments and blanks dropped, backslash +# continuations joined, and runs of whitespace collapsed so a reformatted copy +# still compares equal. Reads the body on stdin, because /etc/sudoers.d is 0750 +# root:root and the caller has to hand us an elevated read. +# +# Comments are tested before continuations are joined, which is the order every +# consumer here uses: udev's parse_file discards a '#' line without looking at a +# trailing backslash (`udevadm verify` on "# disabled \" plus a bogus key reports +# the error on line 2), sudo's toke.l comment rule consumes to the newline and +# clears its continuation flag, and systemd's config_parse tests the comment +# characters before appending to a continuation. Joining first would let a +# comment ending in a backslash swallow the live line beneath it. +# +# FORMAT is sudoers or systemd. systemd takes ';' as well as '#'. sudo does not +# treat every '#' as a comment: toke.l has INITIAL rules for ^#include and +# ^#includedir, and its comment pattern excludes '#' followed by a digit or +# -digit so those reach the ID token as a numeric uid user spec. Those lines are +# active directives, and a file carrying one must not read as though it held only +# generated lines. +active_lines() { + local format="$1" + local comments='#' + local line logical="" + + [[ $format == "systemd" ]] && comments='#;' + + while IFS= read -r line || [[ -n $line ]]; do + if [[ $line =~ ^[[:space:]]*[$comments] ]] && + ! { [[ $format == "sudoers" ]] && sudoers_hash_is_active "$line"; }; then + # The two consumers part company here. sudo ends the logical line at a + # comment and keeps what came before it, so `visudo -cf` reads a spec + # ending in a backslash, then a comment, then a second spec as two live + # specs; dropping the pending half would hide an administrator's grant and + # let this file read as though the installer had written all of it. systemd + # resumes the continuation instead: `systemd-analyze verify` on "ExecStop=\" + # + "; c" + a path resolves that path, so the pending half has to stay. + if [[ $format == "sudoers" ]]; then + emit_logical "$logical" + logical="" + fi + continue + fi + + if [[ $line == *\\ ]]; then + logical+="${line%\\} " + continue + fi + + emit_logical "$logical$line" + logical="" + done +} + +# One logical line, whitespace collapsed so a reformatted copy still compares +# equal, and nothing at all for a line that held only whitespace. +emit_logical() { + local -a parts + + read -ra parts <<<"$1" + if (( ${#parts[@]} )); then + printf '%s\n' "${parts[*]}" + fi +} + +sudoers_hash_is_active() { + local line="$1" + + [[ $line =~ ^[[:space:]]*#include[[:blank:]] ]] && return 0 + [[ $line =~ ^[[:space:]]*#includedir[[:blank:]] ]] && return 0 + [[ $line =~ ^[[:space:]]*#-?[0-9] ]] && return 0 + + return 1 +} + +# install/preflight/first-run-mode.sh (2025-08-25 to 2026-05-25) granted the +# installing account passwordless sudo for the rest of the first boot, including +# an unrestricted /usr/bin/systemctl from 2025-10-14 on -- enough to link and +# start a unit of the user's own, which is root. bin/omarchy-first-run was meant +# to delete the grant, but it clears its first-run.mode guard as the very first +# statement and only reaches the removal after eight set -e steps, two of which +# touch the network. Any failure in between leaves the grant on the machine with +# nothing left to retry it. +# +# The installer rewrote this file eight times, and only the last four carry both +# Cmnd_Alias lines, so keying on those would walk past the earlier ones. Instead +# require every active line to be one the installer itself emitted, plus at least +# one line that is unmistakably this grant: its own self-cleanup. One +# hand-written line anywhere in the file and it is not ours to delete. +first_run_sudoers_is_generated() { + local spec_pattern='^[^[:space:]]+ ALL=\(ALL\) NOPASSWD: (.+)$' + local marker_pattern='^/bin/rm -f /home/[^/]+/\.local/state/omarchy/first-run\.mode$' + local line command + local seen_any=0 seen_marker=0 + + while IFS= read -r line; do + seen_any=1 + + case "$line" in + "Cmnd_Alias SYMLINK_RESOLVED = /usr/bin/ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf") + continue + ;; + "Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run" | \ + "Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run, /bin/rm -f /etc/sudoers.d/99-omarchy-installer-reboot") + seen_marker=1 + continue + ;; + esac + + # Everything else the installer wrote is a user spec naming the installing + # account, whose name cannot be assumed here: it may since have been renamed + # or removed, and a second account runs this migration too. + if [[ ! $line =~ $spec_pattern ]]; then + return 1 + fi + command=${BASH_REMATCH[1]} + + case "$command" in + "/usr/bin/systemctl" | "/usr/bin/ufw" | "/usr/bin/ufw-docker" | \ + "/usr/bin/gtk-update-icon-cache" | "/usr/bin/udevadm" | \ + "/usr/bin/tee /etc/udev/rules.d/*" | "SYMLINK_RESOLVED") + continue + ;; + "FIRST_RUN_CLEANUP" | "/bin/rm -f /etc/sudoers.d/first-run") + seen_marker=1 + continue + ;; + esac + + if [[ $command =~ $marker_pattern ]]; then + seen_marker=1 + continue + fi + + return 1 + done < <(active_lines sudoers) + + (( seen_any && seen_marker )) +} + +# bin/omarchy-install-tailscale (2025-08-22 to 2026-02-02) ran +# "echo \"\$USER ALL=(ALL) NOPASSWD: \$(which tsui)\" | sudo tee +# /etc/sudoers.d/tsui" one line after installing tsui by piping a vendor script +# to bash with no sudo at all, so the path it resolved was usually the user's own +# ~/.local/bin. Overwrite that file, run sudo tsui, and you are root. The grant +# goes whatever the path turned out to be: the feature was dropped from Omarchy, +# and unrestricted NOPASSWD on a TUI that can shell out is an escalation from a +# root-owned path too. +tsui_sudoers_is_generated() { + local spec_pattern='^[^[:space:]]+ ALL=\(ALL\) NOPASSWD: ([^[:space:]]+)$' + local line command="" count=0 + + while IFS= read -r line; do + count=$(( count + 1 )) + if (( count > 1 )); then + return 1 + fi + if [[ ! $line =~ $spec_pattern ]]; then + return 1 + fi + command=${BASH_REMATCH[1]} + done < <(active_lines sudoers) + + if (( count == 1 )) && [[ ${command##*/} == "tsui" ]]; then + return 0 + fi + + return 1 +} + +# install/plymouth.sh wrote this unit for two days (2025-07-05 to 2025-07-07) +# with an unquoted heredoc, so ExecStop names the installing user's home. The +# unit is enabled WantedBy=multi-user.target, so systemd runs that path as uid 0 +# on every shutdown, with no hardware event needed to reach it. +plymouth_unit_runs_from_home() { + local binary="omarchy-plymouth-shutdown-sync" + local exec_stop_pattern='^ExecStop[[:space:]]*=[[:space:]]*(.*)$' + local home_pattern="^(/home/[^/]+|/root)/\\.local/share/omarchy/bin/$binary\$" + local line word + local -a words + + while IFS= read -r line; do + if [[ ! $line =~ $exec_stop_pattern ]]; then + continue + fi + + read -ra words <<<"${BASH_REMATCH[1]}" + if (( ! ${#words[@]} )); then + continue + fi + + # systemd reads -, @, +, ! and : ahead of the command as flags, not as part + # of the path it runs. + word=${words[0]} + while [[ $word == [-@+!:]* ]]; do + word=${word:1} + done + + if [[ $word =~ $home_pattern || $word == "$HOME/.local/share/omarchy/bin/$binary" ]]; then + return 0 + fi + done < <(active_lines systemd) + + return 1 +} + +# /etc/sudoers.d is 0750 root:root as shipped, and omarchy-migrate runs as the +# logged-in user, so an unelevated [[ -f ]] on a file in there is false whether or +# not the file exists and an unelevated read returns nothing. Both tests and both +# reads have to be elevated or this migration reports success having done nothing. +# One combined probe first, so the common case of neither file being present costs +# a single sudo call rather than one per file. +first_run_sudoers="$sudoers_dir/first-run" +tsui_sudoers="$sudoers_dir/tsui" + +if as_root test -e "$first_run_sudoers" -o -e "$tsui_sudoers"; then + if as_root test -f "$first_run_sudoers" && + as_root cat "$first_run_sudoers" | first_run_sudoers_is_generated; then + as_root rm -f "$first_run_sudoers" + fi + + if as_root test -f "$tsui_sudoers" && + as_root cat "$tsui_sudoers" | tsui_sudoers_is_generated; then + as_root rm -f "$tsui_sudoers" + fi +fi + +# /etc/systemd/system is 0755, so this one needs no elevation to look at. +plymouth_unit="$systemd_dir/omarchy-plymouth-shutdown.service" +if [[ -f $plymouth_unit ]] && plymouth_unit_runs_from_home <"$plymouth_unit"; then + # Disable, never stop. Stopping the unit is precisely what runs ExecStop, and + # ExecStop is the path this migration exists to keep root away from; disabling + # only drops the multi-user.target symlink. + as_root systemctl disable omarchy-plymouth-shutdown.service >/dev/null 2>&1 || true + as_root rm -f "$plymouth_unit" + # systemd keeps serving the copy it already loaded until it rereads the + # directory, so without this the unit is still there to run at shutdown. + as_root systemctl daemon-reload >/dev/null 2>&1 || true +fi diff --git a/test/shell.d/retired-installer-artifacts-migration-test.sh b/test/shell.d/retired-installer-artifacts-migration-test.sh new file mode 100755 index 00000000..d626fc33 --- /dev/null +++ b/test/shell.d/retired-installer-artifacts-migration-test.sh @@ -0,0 +1,456 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +migration="$ROOT/migrations/1788025225.sh" +[[ -f $migration ]] || fail "the retired installer artifact migration exists at $migration" + +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +mkdir -p "$test_dir/bin" + +# sudo runs the real command, so the removals act on the redirected directories +# below and the elevated calls land in the log beside it. +cat >"$test_dir/bin/sudo" <<'STUB' +#!/bin/bash + +printf 'sudo %s\n' "$*" >>"$CALLS" +exec "$@" +STUB + +cat >"$test_dir/bin/systemctl" <<'STUB' +#!/bin/bash + +printf 'systemctl %s\n' "$*" >>"$CALLS" +STUB + +chmod +x "$test_dir/bin/"* + +export CALLS="$test_dir/calls" + +sudoers_dir="$test_dir/sudoers.d" +systemd_dir="$test_dir/systemd" +home_dir="$test_dir/home" +first_run="$sudoers_dir/first-run" +tsui="$sudoers_dir/tsui" +plymouth_unit="$systemd_dir/omarchy-plymouth-shutdown.service" + +reset_machine() { + rm -rf "$sudoers_dir" "$systemd_dir" "$home_dir" + mkdir -p "$sudoers_dir" "$systemd_dir" "$home_dir" +} + +run_migration() { + : >"$CALLS" + + HOME="$home_dir" \ + OMARCHY_SUDOERS_DIR="$sudoers_dir" \ + OMARCHY_SYSTEMD_SYSTEM_DIR="$systemd_dir" \ + PATH="$test_dir/bin:$PATH" \ + bash -euo pipefail "$migration" >/dev/null +} + +# /etc/sudoers.d is 0750 root:root on a real machine, so the migration has to +# escalate merely to see whether either grant is there. An empty call log is +# therefore the wrong invariant: what must be absent unless a file really is +# Omarchy's is a removal, or a unit being disabled or reloaded. +assert_changed_nothing() { + local label="$1" + + ! grep -qE '^(sudo rm|systemctl disable|systemctl daemon-reload)' "$CALLS" || + fail "$label" "$(cat "$CALLS")" + pass "$label" +} + +# The reads themselves must be elevated too. A plain [[ -f ]] or cat under a +# root-only directory returns nothing as the logged-in user, which would make the +# migration report success having looked at nothing at all. +assert_read_elevated() { + local file="$1" label="$2" + + grep -qF "sudo test -f $file" "$CALLS" || + fail "$label" "$(cat "$CALLS")" + grep -qF "sudo cat $file" "$CALLS" || + fail "$label" "$(cat "$CALLS")" + pass "$label" +} + +write_plymouth_unit() { + cat >"$plymouth_unit" <"$first_run" + run_migration + + [[ ! -e $first_run ]] || + fail "migration removes first-run grant variant $variant" "$(cat "$first_run")" +done +pass "migration removes every first-run sudoers grant the installer ever wrote" + +grep -q '^sudo rm -f .*/sudoers\.d/first-run$' "$CALLS" || + fail "migration removes the first-run grant with elevated privileges" "$(cat "$CALLS")" +pass "migration removes the first-run grant with elevated privileges" + +# The grant is only recognisable as Omarchy's because every line in it is one the +# installer emitted. One line an administrator added and the file is theirs. +reset_machine +cat >"$first_run" <<'EOF' +Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run +Cmnd_Alias SYMLINK_RESOLVED = /usr/bin/ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf +installer ALL=(ALL) NOPASSWD: /usr/bin/systemctl +installer ALL=(ALL) NOPASSWD: /usr/local/bin/our-own-deploy-script +installer ALL=(ALL) NOPASSWD: FIRST_RUN_CLEANUP +EOF +before=$(cat "$first_run") +run_migration + +[[ -e $first_run ]] || fail "migration keeps a first-run file carrying a hand-written rule" +[[ $(cat "$first_run") == "$before" ]] || + fail "migration leaves a hand-written first-run file byte for byte" +assert_changed_nothing "migration changes nothing for a hand-written first-run file" +pass "migration keeps a first-run file carrying a hand-written rule" + +# Nothing in this file ties it to Omarchy's first run: no self-cleanup line. +reset_machine +cat >"$first_run" <<'EOF' +installer ALL=(ALL) NOPASSWD: /usr/bin/ufw +installer ALL=(ALL) NOPASSWD: /usr/bin/ufw-docker +EOF +run_migration + +[[ -e $first_run ]] || + fail "migration keeps a same-named file that never cleaned itself up" +pass "migration keeps a same-named file that never cleaned itself up" + +# A rule continued onto the next line is one logical line, and a comment that is +# continued stays a comment for the whole of it. +reset_machine +cat >"$first_run" <<'EOF' +# Retired, keeping the old grant here for reference: \ +installer ALL=(ALL) NOPASSWD: /usr/bin/systemctl +Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run +installer ALL=(ALL) NOPASSWD: \ + /usr/local/bin/our-own-deploy-script +EOF +run_migration + +[[ -e $first_run ]] || + fail "migration reads a continued line as one rule and a continued comment as comment" +pass "migration reads a continued line as one rule and a continued comment as comment" + +reset_machine +printf 'installer ALL=(ALL) NOPASSWD: %s/.local/bin/tsui\n' "$home_dir" >"$tsui" +run_migration + +[[ ! -e $tsui ]] || fail "migration removes the tsui grant pointing into the user's home" +grep -q '^sudo rm -f .*/sudoers\.d/tsui$' "$CALLS" || + fail "migration removes the tsui grant with elevated privileges" "$(cat "$CALLS")" +pass "migration removes the tsui grant pointing into the user's home" + +# The feature is gone from Omarchy either way, and unrestricted NOPASSWD on a TUI +# that can shell out escalates from a root-owned path too. +reset_machine +printf 'installer ALL=(ALL) NOPASSWD: /usr/bin/tsui\n' >"$tsui" +run_migration + +[[ ! -e $tsui ]] || fail "migration removes the tsui grant wherever the path points" +pass "migration removes the tsui grant wherever the path points" + +reset_machine +cat >"$tsui" <<'EOF' +# Kept after Omarchy dropped tsui, extended for our operators +installer ALL=(ALL) NOPASSWD: /usr/bin/tsui +operator ALL=(ALL) NOPASSWD: /usr/bin/tsui +EOF +before=$(cat "$tsui") +run_migration + +[[ -e $tsui ]] || fail "migration keeps a tsui file an administrator extended" +[[ $(cat "$tsui") == "$before" ]] || fail "migration leaves an extended tsui file byte for byte" +assert_changed_nothing "migration changes nothing for an extended tsui file" +pass "migration keeps a tsui file an administrator extended" + +reset_machine +printf 'installer ALL=(ALL) NOPASSWD: /usr/bin/tailscale\n' >"$tsui" +run_migration + +[[ -e $tsui ]] || fail "migration keeps a lone grant for some other command" +pass "migration keeps a lone grant for some other command" + +reset_machine +write_plymouth_unit "/home/installer/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync" +run_migration + +[[ ! -e $plymouth_unit ]] || + fail "migration removes the shutdown unit that runs out of a user home" +pass "migration removes the shutdown unit that runs out of a user home" + +# Stopping the unit is exactly what runs ExecStop, which is the path being taken +# away from root. Disabling only drops the multi-user.target symlink. +! grep -q '^systemctl stop' "$CALLS" || + fail "migration never stops the unit, which would run ExecStop as root" "$(cat "$CALLS")" +pass "migration never stops the unit, which would run ExecStop as root" + +disable_at=$(grep -n '^systemctl disable omarchy-plymouth-shutdown\.service$' "$CALLS" | cut -d: -f1) +remove_at=$(grep -n '^sudo rm -f .*omarchy-plymouth-shutdown\.service$' "$CALLS" | cut -d: -f1) +reload_at=$(grep -n '^systemctl daemon-reload$' "$CALLS" | cut -d: -f1) +[[ -n $disable_at && -n $remove_at && -n $reload_at ]] || + fail "migration disables, removes, then reloads the unit" "$(cat "$CALLS")" +(( disable_at < remove_at && remove_at < reload_at )) || + fail "migration disables before removing and reloads last" "$(cat "$CALLS")" +pass "migration disables the unit, removes it, then reloads systemd in that order" + +# Homes are not all under /home. +reset_machine +write_plymouth_unit "$home_dir/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync" +run_migration + +[[ ! -e $plymouth_unit ]] || + fail "migration removes a shutdown unit rooted in a home outside /home" +pass "migration removes a shutdown unit rooted in a home outside /home" + +reset_machine +write_plymouth_unit "/usr/bin/omarchy-plymouth-shutdown-sync" +run_migration + +[[ -e $plymouth_unit ]] || + fail "migration keeps a same-named unit that runs a packaged command" +assert_changed_nothing "migration changes nothing for a packaged shutdown unit" +pass "migration keeps a same-named unit that runs a packaged command" + +# systemd takes ';' as a comment too, and an ExecStop behind one runs nothing. +reset_machine +cat >"$plymouth_unit" <<'EOF' +[Service] +Type=oneshot +; ExecStop=/home/installer/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync +ExecStop=/usr/bin/true +EOF +run_migration + +[[ -e $plymouth_unit ]] || fail "migration keeps a unit whose home ExecStop is commented out" +pass "migration keeps a unit whose home ExecStop is commented out" + +reset_machine +run_migration + +assert_changed_nothing "migration changes nothing when no retired artifact is present" +pass "migration leaves a machine without any retired artifact alone" + +# All three at once, then the same run again: what a second account on the +# machine does, and what running omarchy-migrate twice does. +reset_machine +printf '%s\n' "${first_run_variants[-1]}" >"$first_run" +printf 'installer ALL=(ALL) NOPASSWD: /usr/bin/tsui\n' >"$tsui" +write_plymouth_unit "/home/installer/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync" +run_migration + +[[ ! -e $first_run && ! -e $tsui && ! -e $plymouth_unit ]] || + fail "migration clears all three retired artifacts in one pass" +pass "migration clears all three retired artifacts in one pass" + +run_migration + +assert_changed_nothing "migration changes nothing on a second run" +pass "migration is a no-op on a second run" + +# sudo does not treat every '#' as a comment. plugins/sudoers/toke.l matches +# ^#include and ^#includedir as directives in the INITIAL state, and its comment +# rule excludes '#' followed by a digit or -digit so those reach the ID token as a +# numeric uid user spec -- sudoers(5) says the same. Dropping such a line as a +# comment would let a file that still carries an active directive read as though +# it held only generated lines, and be deleted. +for directive in \ + '#include /etc/sudoers.local' \ + '#includedir /etc/sudoers.d.local' \ + '#1000 ALL=(ALL) NOPASSWD: ALL' \ + '#-1000 ALL=(ALL) NOPASSWD: ALL'; do + reset_machine + { + printf '%s\n' "$directive" + printf '%s\n' "${first_run_variants[-1]}" + } >"$first_run" + before=$(cat "$first_run") + run_migration + + [[ -e $first_run ]] || + fail "migration keeps a first-run file carrying the active directive $directive" + [[ $(cat "$first_run") == "$before" ]] || + fail "migration leaves a first-run file with $directive byte for byte" + pass "migration keeps a first-run file carrying the active directive $directive" + + reset_machine + { + printf '%s\n' "$directive" + printf 'installer ALL=(ALL) NOPASSWD: /usr/bin/tsui\n' + } >"$tsui" + before=$(cat "$tsui") + run_migration + + [[ -e $tsui ]] || + fail "migration keeps a tsui file carrying the active directive $directive" + [[ $(cat "$tsui") == "$before" ]] || + fail "migration leaves a tsui file with $directive byte for byte" + pass "migration keeps a tsui file carrying the active directive $directive" +done + +# A sudoers comment ending in a backslash does not swallow the line beneath it: +# toke.l's comment rule consumes to the newline and clears the continuation flag. +# Joining before testing for a comment would hide this administrator's grant. +reset_machine +cat >"$tsui" <<'EOF' +# retired, kept for reference \ +ops ALL=(ALL) NOPASSWD: /usr/bin/tsui +installer ALL=(ALL) NOPASSWD: /usr/bin/tsui +EOF +before=$(cat "$tsui") +run_migration + +[[ -e $tsui ]] || + fail "migration keeps a tsui file whose second grant survives a commented continuation" +[[ $(cat "$tsui") == "$before" ]] || + fail "migration leaves that tsui file byte for byte" +pass "migration keeps a tsui file whose second grant survives a commented continuation" + +# Both grants live under a root-only directory, so seeing them at all takes +# elevation. Pin that the migration reads them elevated rather than silently +# reading nothing. +reset_machine +printf '%s\n' "${first_run_variants[-1]}" >"$first_run" +run_migration + +assert_read_elevated "$first_run" "migration reads the first-run grant with elevated privileges" + +reset_machine +printf 'installer ALL=(ALL) NOPASSWD: /usr/bin/tsui\n' >"$tsui" +run_migration + +assert_read_elevated "$tsui" "migration reads the tsui grant with elevated privileges" + +# sudo ends a logical line at a comment and keeps what came before it: visudo -cf +# reads a spec ending in a backslash, then a comment, then a second spec as two +# live specs. Dropping the pending half would hide this administrator's grant and +# let the file read as though the installer had written all of it. +reset_machine +cat >"$first_run" <<'EOF' +Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run +operator ALL=(ALL) NOPASSWD: /usr/local/bin/deploy \ +# kept deliberately +installer ALL=(ALL) NOPASSWD: /usr/bin/systemctl +installer ALL=(ALL) NOPASSWD: FIRST_RUN_CLEANUP +EOF +before=$(cat "$first_run") +run_migration + +[[ -e $first_run ]] || + fail "migration keeps a first-run file whose hand-written spec precedes a comment" +[[ $(cat "$first_run") == "$before" ]] || + fail "migration leaves that first-run file byte for byte" +pass "migration keeps a first-run file whose hand-written spec precedes a comment" + +reset_machine +cat >"$tsui" <<'EOF' +operator ALL=(ALL) NOPASSWD: /usr/local/bin/deploy \ +# kept deliberately +installer ALL=(ALL) NOPASSWD: /usr/bin/tsui +EOF +before=$(cat "$tsui") +run_migration + +[[ -e $tsui ]] || + fail "migration keeps a tsui file whose hand-written spec precedes a comment" +[[ $(cat "$tsui") == "$before" ]] || + fail "migration leaves that tsui file byte for byte" +pass "migration keeps a tsui file whose hand-written spec precedes a comment" + +# systemd resumes a continuation across a comment: systemd-analyze verify on +# "ExecStop=\" + "; c" + a path resolves that path. The unit is live and has to go. +reset_machine +cat >"$plymouth_unit" <<'EOF' +[Service] +Type=oneshot +ExecStart=/usr/bin/true +ExecStop=\ +; still one directive +/home/installer/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync +EOF +run_migration + +[[ ! -e $plymouth_unit ]] || + fail "migration removes a unit whose ExecStop continues across a comment" "$(cat "$plymouth_unit")" +pass "migration removes a unit whose ExecStop continues across a comment"