userdel rewrites /etc/subuid and /etc/subgid, and like every shadow-utils database write it leaves the previous contents behind in a dash-suffixed backup. The scrub removed four of the six backups those tools produce, so the retained @factory baseline still named the previous owner in /etc/subuid- and /etc/subgid- along with their subordinate ID range. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
5.3 KiB
Bash
149 lines
5.3 KiB
Bash
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
if (( EUID != 0 )); then
|
|
if unshare --user --map-root-user true 2>/dev/null; then
|
|
exec unshare --user --map-root-user bash "$0"
|
|
fi
|
|
pass "no unprivileged user namespace; skipping factory account cleanup"
|
|
exit 0
|
|
fi
|
|
|
|
test_tmp=$(mktemp -d)
|
|
trap 'rm -rf "$test_tmp"' EXIT
|
|
|
|
# Load the production functions without self-elevation or the reset entrypoint.
|
|
awk '
|
|
/^[a-z_]+\(\) \{/ { copying = 1 }
|
|
copying { print }
|
|
/^}/ { copying = 0 }
|
|
' "$ROOT/bin/omarchy-system-factory-reset" >"$test_tmp/functions"
|
|
|
|
cat >"$test_tmp/reset" <<'SH'
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
source "$1/functions"
|
|
TOP_MNT="$2"
|
|
NEXT_NAME=@omarchy-reset-next
|
|
PROVISIONING_DIR=/var/lib/omarchy/provisioning
|
|
LOG_FILE="$TOP_MNT/reset.log"
|
|
|
|
log() { printf '%s\n' "$1" >>"$LOG_FILE"; }
|
|
fail() { log "$1"; exit 1; }
|
|
|
|
# Account tools are real. Only snapshots, boot rebuilding, and system services
|
|
# are replaced: all writes stay inside this test's disposable directory.
|
|
btrfs() {
|
|
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then
|
|
mkdir -p "$4"
|
|
cp -a "$3/." "$4/"
|
|
elif [[ $1 == "property" ]]; then
|
|
printf '%s\n' "$6" >"$4/read-only"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
systemd-id128() { printf '%032d\n' 1; }
|
|
install_provisioning_units() { :; }
|
|
encrypted_install() { return 1; }
|
|
rebuild_next_boot() { touch "$TOP_MNT/rebuilt"; }
|
|
sync() { :; }
|
|
|
|
userdel() {
|
|
[[ ${FAIL_COMMAND:-} == "userdel" && $2 == "$FAIL_ROOT" ]] && return 42
|
|
command userdel "$@"
|
|
}
|
|
usermod() {
|
|
[[ ${FAIL_COMMAND:-} == "usermod" && $2 == "$FAIL_ROOT" ]] && return 42
|
|
command usermod "$@"
|
|
}
|
|
rm() {
|
|
[[ ${FAIL_COMMAND:-} == "rm" && $* == *"$FAIL_ROOT/etc/shadow-"* ]] && return 42
|
|
command rm "$@"
|
|
}
|
|
|
|
stage_full_reset
|
|
SH
|
|
|
|
make_fixture() {
|
|
local top="$1" root_hash="${2:-original-root-hash}"
|
|
local factory="$top/@factory"
|
|
mkdir -p "$top/@" "$factory/etc" "$factory/home/seller" \
|
|
"$factory/usr/bin" "$factory/usr/share/omarchy/install/provisioning" \
|
|
"$factory/var/lib/omarchy/provisioning/packages"
|
|
touch "$top/@/old-system" "$factory/home/seller/private-file" \
|
|
"$factory/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service" \
|
|
"$factory/var/lib/omarchy/provisioning/packages/node-v0.tar.gz"
|
|
printf '#!/bin/bash\n' >"$factory/usr/bin/omarchy-provision-owner"
|
|
chmod +x "$factory/usr/bin/omarchy-provision-owner"
|
|
printf 'true\n' >"$factory/read-only"
|
|
cat >"$factory/etc/passwd" <<'EOF'
|
|
root:x:0:0:root:/root:/bin/bash
|
|
daemon:x:1:1:daemon:/:/usr/bin/nologin
|
|
seller:x:1000:1000:Seller:/home/seller:/bin/bash
|
|
EOF
|
|
printf 'root:%s:20000:0:99999:7:::\ndaemon:*:20000:0:99999:7:::\nseller:original-user-hash:20000:0:99999:7:::\n' \
|
|
"$root_hash" >"$factory/etc/shadow"
|
|
printf 'root:x:0:\ndaemon:x:1:\nseller:x:1000:\nwheel:x:998:seller\n' >"$factory/etc/group"
|
|
printf 'root:!::\ndaemon:!::\nseller:!::\nwheel:!::seller\n' >"$factory/etc/gshadow"
|
|
printf 'USERGROUPS_ENAB yes\n' >"$factory/etc/login.defs"
|
|
printf 'seller:100000:65536\n' >"$factory/etc/subuid"
|
|
printf 'seller:100000:65536\n' >"$factory/etc/subgid"
|
|
chmod 600 "$factory/etc/"{shadow,gshadow}
|
|
for file in passwd shadow group gshadow subuid subgid; do
|
|
cp "$factory/etc/$file" "$factory/etc/$file-"
|
|
done
|
|
}
|
|
|
|
assert_scrubbed() {
|
|
local root="$1" file
|
|
[[ $(awk -F: '$1 == "root" { print $2 }' "$root/etc/shadow") == "!" ]] ||
|
|
fail "reset erases the root hash while keeping the account locked"
|
|
! grep -q 'original-.*-hash\|seller' "$root/etc/"{passwd,shadow,group,gshadow} ||
|
|
fail "reset removes seller account credentials and group membership"
|
|
[[ ! -e $root/home/seller ]] || fail "reset removes the seller's baseline home"
|
|
grep -q '^daemon:\*:' "$root/etc/shadow" || fail "reset preserves service accounts"
|
|
[[ $(stat -c '%a' "$root/etc/shadow") == "600" ]] || fail "shadow stays private"
|
|
for file in passwd shadow group gshadow subuid subgid; do
|
|
[[ ! -e $root/etc/$file- ]] || fail "reset removes the $file backup"
|
|
done
|
|
}
|
|
|
|
for scenario in normal locked; do
|
|
top="$test_tmp/$scenario"
|
|
if [[ $scenario == "locked" ]]; then
|
|
make_fixture "$top" '!'
|
|
else
|
|
make_fixture "$top"
|
|
fi
|
|
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset stages successfully"
|
|
assert_scrubbed "$top/@factory"
|
|
assert_scrubbed "$top/@"
|
|
[[ $(cat "$top/@factory/read-only") == "true" ]] || fail "baseline returns to read-only"
|
|
[[ -f $top/@/var/lib/omarchy/provisioning/pending && -f $top/rebuilt ]] ||
|
|
fail "reset reaches provisioning after cleanup"
|
|
|
|
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset can be repeated"
|
|
assert_scrubbed "$top/@factory"
|
|
assert_scrubbed "$top/@"
|
|
pass "$scenario reset scrubs both roots, preserves service accounts, and can be repeated"
|
|
done
|
|
|
|
for target in @omarchy-reset-next @factory; do
|
|
for command in userdel usermod rm; do
|
|
top="$test_tmp/fail-$target-$command"
|
|
make_fixture "$top"
|
|
if FAIL_COMMAND="$command" FAIL_ROOT="$top/$target" bash "$test_tmp/reset" "$test_tmp" "$top"; then
|
|
fail "reset accepted failed $command in $target"
|
|
fi
|
|
[[ -f $top/@/old-system && ! -e $top/rebuilt ]] ||
|
|
fail "failed cleanup must not activate or rebuild the reset system"
|
|
[[ $(cat "$top/@factory/read-only") == "true" ]] ||
|
|
fail "failed cleanup must leave the baseline read-only"
|
|
pass "failed $command in $target aborts reset before activation"
|
|
done
|
|
done
|