cups-browsed is the daemon that watches the network and creates print queues by itself. Hardening it took a root daemon with a predictable cache down to a confined service account, but a daemon that turns anything advertising itself on the network into a print queue is a lot of exposure for a convenience, so it comes out of the default install while that is reworked. Only the discovery half: CUPS itself stays and printing keeps working, with each printer added by hand in Print Settings. The migration disables the unit before removing the package because that is the only order that works: pacman deletes the unit file but not the enable symlink, and once the unit is gone systemd can no longer resolve it by name to clean that up. It then removes the queues discovery generated. cups-browsed keeps those when it stops, since KeepGeneratedQueuesOnShutdown defaults to Yes, and they route through its own implicitclass backend, which goes with the package, so they cannot print again. Idle ones go. A queue with jobs on it is left alone and named: implicitclass only needs cups-browsed to choose a destination, so a job already past that point finishes on its own, and deleting the queue would abort it. One printer's job does not hold up the removal. A printer added by hand has an ipp:// or usb:// device and is left where it is. A queue whose jobs cannot be asked about is left alone rather than assumed idle, including one named so that lpstat would misread it -- "all" is its word for every destination, and a leading dash or a comma reads as another option or a list. Where CUPS does not answer at all, or a queue will not delete, discovery is still stopped but the package stays and no marker is written. omarchy-migrate records a migration for the user as soon as it exits zero, so that is where the machine stays until someone removes the package by hand, and the message says so rather than implying a retry. The queue list is read under LC_ALL=C because lpstat translates "device for", and captured rather than piped, so a cupsd it cannot reach is reported instead of reading like a machine with nothing to clean up. It removes with plain pacman -R rather than omarchy-pkg-drop, which passes -n and would discard /etc/cups/cups-browsed.conf instead of keeping it as a .pacsave. A removal meant to be temporary should not delete the machine's copy of its own configuration. Without -s either, so it only ever removes the package it names: sweeping newly unneeded dependencies is nothing today, but it is not a promise a rolling dependency graph can keep. Queue names come off the network, since cups-browsed names its queues after what the printer advertised. CUPS allows every printable character but space, tab, / and #, and lpstat and lpadmin take a destination as an option value, so a name with a leading dash or a comma is reported rather than passed to them and guessed at. Migration state is per user, so a machine-wide marker records the one removal. Without it, an account whose first migration run came after someone deliberately reinstalled discovery would quietly take it back out again. The install-time override for cups-browsed.conf now waits for cups-browsed rather than for CUPS. Guarding it on a file CUPS still ships would write a configuration file for a package nothing installed, and pacman would later land the package's own copy beside it as a .pacnew. The hardened configuration stays in the tree. omarchy-settings still ships the cups-browsed.conf override, the sysusers account and the service drop-in, so they are what discovery returns onto. Co-Authored-By: Codex XHigh <noreply@openai.com>
249 lines
10 KiB
Bash
249 lines
10 KiB
Bash
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
packages="$ROOT/install/omarchy-base.packages"
|
|
cups_browsed_conf="$ROOT/etc/cups/cups-browsed.conf"
|
|
cups_files_conf="$ROOT/etc/cups/cups-files.conf"
|
|
sysusers_conf="$ROOT/etc/sysusers.d/omarchy-cups-browsed.conf"
|
|
service_dropin="$ROOT/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf"
|
|
|
|
# Only discovery goes. Everything else printing needs stays, or this stops
|
|
# being a removal of one daemon and becomes a removal of printing.
|
|
grep -qxF cups "$packages" || fail "CUPS itself remains in the base package set"
|
|
grep -qxF cups-filters "$packages" || fail "the CUPS filters remain in the base package set"
|
|
grep -qxF system-config-printer "$packages" || fail "Print Settings remains in the base package set"
|
|
grep -qxF cups-pk-helper "$packages" || fail "Polkit printer administration is installed"
|
|
! grep -qxF cups-pdf "$packages" || fail "the root CUPS-PDF backend is removed"
|
|
|
|
# Automatic discovery is temporarily out of the default install while it is
|
|
# reworked. The hardened configuration below still ships: it is what a
|
|
# hand-installed cups-browsed gets, and what discovery comes back onto.
|
|
! grep -qxF cups-browsed "$packages" || fail "automatic printer discovery is out of the base package set"
|
|
! grep -q 'cups-browsed' "$ROOT/install/config/enable-services.sh" ||
|
|
fail "a fresh install does not enable a discovery service it no longer installs"
|
|
! grep -q 'enable_system_service cups-browsed' "$ROOT/bin/omarchy-upgrade-to-quattro" ||
|
|
fail "the Quattro upgrade does not enable a discovery service it no longer installs"
|
|
|
|
pass "the base install keeps CUPS and Polkit administration, without automatic discovery"
|
|
|
|
# The install-time override for a file has to wait for the package that owns
|
|
# it. CUPS still ships /etc/cups/cups-files.conf, so a guard on that file no
|
|
# longer says anything about cups-browsed: writing its override on a machine
|
|
# without the package leaves a configuration file for a package nothing
|
|
# installed, and pacman lands its own copy beside it as a .pacnew later.
|
|
post_install_pacman="$ROOT/install/post-install/pacman.sh"
|
|
|
|
grep -q 'cups-cups-browsed.conf && -f /etc/cups/cups-browsed.conf' "$post_install_pacman" ||
|
|
fail "the discovery override waits for the package that owns the file it replaces"
|
|
grep -q 'cups-cups-files.conf && -f /etc/cups/cups-files.conf' "$post_install_pacman" ||
|
|
fail "the CUPS authorization override waits for the file it replaces"
|
|
|
|
pass "install-time overrides wait for the packages that own their files"
|
|
|
|
grep -qxF 'CacheDir /var/cache/cups-browsed' "$cups_browsed_conf" ||
|
|
fail "cups-browsed keeps state outside the print-filter cache"
|
|
grep -qxF 'CreateIPPPrinterQueues Driverless' "$cups_browsed_conf" ||
|
|
fail "automatic queues are limited to driverless IPP printers"
|
|
grep -qxF 'CreateRemoteCUPSPrinterQueues No' "$cups_browsed_conf" ||
|
|
fail "remote CUPS queues are not created automatically"
|
|
! grep -q 'CreateRemotePrinters' "$cups_browsed_conf" ||
|
|
fail "the unsupported CreateRemotePrinters directive is gone"
|
|
|
|
pass "cups-browsed uses explicit supported discovery policy and an isolated cache"
|
|
|
|
grep -qxF 'SystemGroup cups-browsed sys root' "$cups_files_conf" ||
|
|
fail "only the printer discovery account receives passwordless CUPS administration"
|
|
grep -qxF 'PeerCred on' "$cups_files_conf" ||
|
|
fail "the packaged CUPS policy enables peer credentials"
|
|
[[ $(grep -ciE '^[[:space:]]*SystemGroup[[:space:]]' "$cups_files_conf") == 1 ]] ||
|
|
fail "the packaged CUPS policy has one SystemGroup directive"
|
|
[[ $(grep -ciE '^[[:space:]]*PeerCred[[:space:]]' "$cups_files_conf") == 1 ]] ||
|
|
fail "the packaged CUPS policy has one PeerCred directive"
|
|
[[ ! -e $ROOT/install/config/printing.sh ]] ||
|
|
fail "printing policy is not rewritten by an install script"
|
|
! grep -q 'config/printing.sh' "$ROOT/install/config/all.sh" "$ROOT/migrations/1787815267.sh" ||
|
|
fail "neither install nor update invokes a printing rewrite script"
|
|
|
|
pass "CUPS authorization ships as a canonical package override"
|
|
|
|
grep -qxF 'u cups-browsed - "CUPS printer discovery" / -' "$sysusers_conf" ||
|
|
fail "a locked cups-browsed system account is declared"
|
|
|
|
for setting in \
|
|
'User=cups-browsed' \
|
|
'Group=cups-browsed' \
|
|
'CacheDirectory=cups-browsed' \
|
|
'CacheDirectoryMode=0750' \
|
|
'UMask=0027' \
|
|
'NoNewPrivileges=yes' \
|
|
'ProtectSystem=strict' \
|
|
'ProtectHome=yes' \
|
|
'PrivateTmp=yes' \
|
|
'RestrictSUIDSGID=yes'; do
|
|
grep -qxF "$setting" "$service_dropin" ||
|
|
fail "cups-browsed service hardening includes $setting"
|
|
done
|
|
|
|
! grep -q '^\(Ambient\|CapabilityBoundingSet\).*CAP_NET_BIND_SERVICE' "$service_dropin" ||
|
|
fail "cups-browsed is not granted an unverified network capability"
|
|
|
|
pass "cups-browsed runs as its confined service account without added capabilities"
|
|
|
|
test_tmp=$(mktemp -d)
|
|
trap 'rm -rf "$test_tmp"' EXIT
|
|
|
|
mock_bin="$test_tmp/bin"
|
|
mkdir -p "$mock_bin" "$test_tmp/var/lib/omarchy/migrations"
|
|
|
|
passwd_db="$test_tmp/passwd"
|
|
group_db="$test_tmp/group"
|
|
touch "$passwd_db" "$group_db"
|
|
|
|
cat >"$mock_bin/getent" <<'SH'
|
|
#!/bin/bash
|
|
case "$1" in
|
|
passwd) database="$OMARCHY_CUPS_TEST_PASSWD" ;;
|
|
group) database="$OMARCHY_CUPS_TEST_GROUP" ;;
|
|
*) exit 2 ;;
|
|
esac
|
|
|
|
if (($# == 1)); then
|
|
cat "$database"
|
|
else
|
|
awk -F: -v name="$2" '$1 == name { print; found = 1 } END { exit !found }' "$database"
|
|
fi
|
|
SH
|
|
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
|
|
#!/bin/bash
|
|
[[ $1 == "cups" || $1 == "cups-browsed" ]]
|
|
SH
|
|
for command in omarchy-pkg-add omarchy-pkg-drop; do
|
|
cat >"$mock_bin/$command" <<'SH'
|
|
#!/bin/bash
|
|
printf '%s\t%s\n' "${0##*/}" "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
|
SH
|
|
done
|
|
cat >"$mock_bin/systemctl" <<'SH'
|
|
#!/bin/bash
|
|
printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
|
exit 0
|
|
SH
|
|
cat >"$mock_bin/sudo" <<'SH'
|
|
#!/bin/bash
|
|
printf 'sudo\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
|
exec "$@"
|
|
SH
|
|
chmod +x "$mock_bin"/*
|
|
|
|
log="$test_tmp/actions.log"
|
|
touch "$log"
|
|
export OMARCHY_CUPS_TEST_LOG="$log"
|
|
export OMARCHY_CUPS_TEST_PASSWD="$passwd_db"
|
|
export OMARCHY_CUPS_TEST_GROUP="$group_db"
|
|
|
|
printf 'cups-browsed:x:1000:1000:Desktop user:/home/cups-browsed:/usr/bin/bash\n' >"$passwd_db"
|
|
printf 'cups-browsed:x:1000:\n' >"$group_db"
|
|
if PATH="$mock_bin:$PATH" \
|
|
OMARCHY_PATH="$ROOT" \
|
|
OMARCHY_CUPS_MIGRATION_MARKER="$test_tmp/desktop-collision-marker" \
|
|
bash -euo pipefail "$ROOT/migrations/1787815267.sh" 2>/dev/null; then
|
|
fail "the migration accepts an existing desktop user named cups-browsed"
|
|
fi
|
|
[[ ! -s $log ]] || fail "an account collision stops the migration before changing the system"
|
|
|
|
printf 'alice:x:1000:947:Desktop user:/home/alice:/usr/bin/bash\n' >"$passwd_db"
|
|
printf 'cups-browsed:x:947:alice\n' >"$group_db"
|
|
if PATH="$mock_bin:$PATH" \
|
|
OMARCHY_PATH="$ROOT" \
|
|
OMARCHY_CUPS_MIGRATION_MARKER="$test_tmp/group-collision-marker" \
|
|
bash -euo pipefail "$ROOT/migrations/1787815267.sh" 2>/dev/null; then
|
|
fail "the migration accepts an existing cups-browsed group with members"
|
|
fi
|
|
[[ ! -s $log ]] || fail "a group collision stops the migration before changing the system"
|
|
|
|
printf 'cups-browsed:x:947:947:CUPS printer discovery:/:/usr/bin/nologin\n' >"$passwd_db"
|
|
printf 'cups-browsed:x:947:\n' >"$group_db"
|
|
|
|
pass "the migration rejects account and group collisions before changing printing"
|
|
|
|
marker="$test_tmp/var/lib/omarchy/migrations/1787815267"
|
|
PATH="$mock_bin:$PATH" \
|
|
OMARCHY_PATH="$ROOT" \
|
|
OMARCHY_CUPS_MIGRATION_MARKER="$marker" \
|
|
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
|
|
|
grep -qxF $'omarchy-pkg-drop\tcups-pdf' "$log" ||
|
|
fail "the migration removes CUPS-PDF"
|
|
grep -qxF $'omarchy-pkg-add\tcups-pk-helper' "$log" ||
|
|
fail "the migration installs authenticated printer administration"
|
|
grep -qxF $'systemctl\tstop cups-browsed.service' "$log" ||
|
|
fail "the migration stops the root cups-browsed process before reconfiguration"
|
|
grep -qxF $'systemctl\tdaemon-reload' "$log" ||
|
|
fail "the migration reloads the hardened service"
|
|
grep -qxF $'systemctl\ttry-reload-or-restart cups.service' "$log" ||
|
|
fail "the migration reloads the packaged CUPS authorization"
|
|
grep -qxF $'systemctl\trestart cups-browsed.service' "$log" ||
|
|
fail "the migration resumes an active cups-browsed service"
|
|
[[ -f $marker ]] || fail "the migration records machine-wide completion"
|
|
|
|
actions_after_first_run=$(wc -l <"$log")
|
|
PATH="$mock_bin:$PATH" \
|
|
OMARCHY_PATH="$ROOT" \
|
|
OMARCHY_CUPS_MIGRATION_MARKER="$marker" \
|
|
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
|
[[ $(wc -l <"$log") == "$actions_after_first_run" ]] ||
|
|
fail "the machine-wide migration repeats privileged work"
|
|
|
|
pass "the migration safely converts an active existing installation once"
|
|
|
|
# An interrupted earlier run leaves cups-browsed stopped. A retry still needs
|
|
# to resume an enabled service before recording completion.
|
|
cat >"$mock_bin/systemctl" <<'SH'
|
|
#!/bin/bash
|
|
printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
|
[[ $1 == "is-active" ]] && exit 1
|
|
exit 0
|
|
SH
|
|
chmod +x "$mock_bin/systemctl"
|
|
|
|
retry_log="$test_tmp/retry.log"
|
|
retry_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-retry"
|
|
|
|
OMARCHY_CUPS_TEST_LOG="$retry_log" \
|
|
PATH="$mock_bin:$PATH" \
|
|
OMARCHY_PATH="$ROOT" \
|
|
OMARCHY_CUPS_MIGRATION_MARKER="$retry_marker" \
|
|
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
|
|
|
grep -qxF $'systemctl\trestart cups-browsed.service' "$retry_log" ||
|
|
fail "the retry resumes cups-browsed after an interrupted earlier run"
|
|
|
|
pass "a run following an interrupted one still resumes printer discovery"
|
|
|
|
# A masked or disabled unit is deliberately left alone.
|
|
cat >"$mock_bin/systemctl" <<'SH'
|
|
#!/bin/bash
|
|
printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
|
[[ $1 == "is-active" || $1 == "is-enabled" ]] && exit 1
|
|
exit 0
|
|
SH
|
|
chmod +x "$mock_bin/systemctl"
|
|
|
|
masked_log="$test_tmp/masked.log"
|
|
masked_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-masked"
|
|
|
|
OMARCHY_CUPS_TEST_LOG="$masked_log" \
|
|
PATH="$mock_bin:$PATH" \
|
|
OMARCHY_PATH="$ROOT" \
|
|
OMARCHY_CUPS_MIGRATION_MARKER="$masked_marker" \
|
|
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
|
|
|
! grep -qxF $'systemctl\trestart cups-browsed.service' "$masked_log" ||
|
|
fail "the migration leaves a masked or disabled cups-browsed alone"
|
|
[[ -f $masked_marker ]] || fail "the migration completes with cups-browsed masked"
|
|
|
|
pass "a masked or disabled cups-browsed is left alone and does not fail the migration"
|