From 95b791af16dc7cdb1a5291c94c7301f3972c0926 Mon Sep 17 00:00:00 2001 From: acrogenesis Date: Mon, 24 Aug 2026 01:20:40 -0600 Subject: [PATCH 1/6] Stop world-writable browser policy directories Chromium managed policy is mandatory for every profile. World-writable dirs let any local uid plant policy, including force-installed extensions. Write goes through the omarchy-browser-policy group at 2775 so theme colour still works without other-write. --- bin/omarchy-install-browser | 30 ++- bin/omarchy-provision-owner | 9 + bin/omarchy-theme-set-browser | 18 +- bin/omarchy-upgrade-to-quattro | 22 +- install/config/all.sh | 1 + install/config/browser-policy.sh | 3 + install/config/theme-system.sh | 4 - install/helpers/as-root.sh | 7 + install/helpers/browser-policy.sh | 171 ++++++++++++++++ migrations/1787515927.sh | 24 +++ test/shell.d/browser-policy-dir-test.sh | 244 +++++++++++++++++++++++ test/shell.d/default-apps-test.sh | 47 ++++- test/shell.d/provisioning-groups-test.sh | 37 +++- test/shell.d/upgrade-to-quattro-test.sh | 17 ++ 14 files changed, 596 insertions(+), 38 deletions(-) create mode 100644 install/config/browser-policy.sh create mode 100644 install/helpers/as-root.sh create mode 100644 install/helpers/browser-policy.sh create mode 100644 migrations/1787515927.sh create mode 100755 test/shell.d/browser-policy-dir-test.sh diff --git a/bin/omarchy-install-browser b/bin/omarchy-install-browser index f71c7c98..d808c805 100755 --- a/bin/omarchy-install-browser +++ b/bin/omarchy-install-browser @@ -6,9 +6,12 @@ set -e -setup_policy_directory() { - sudo mkdir -p "$1" - sudo chmod a+rw "$1" +source "$OMARCHY_PATH/install/helpers/browser-policy.sh" + +setup_chromium_policy_directory() { + browser_policy_setup_group + browser_policy_grant_user "${USER:-$(id -un)}" + browser_policy_setup_dir "$1" } announce_browser_installed() { @@ -23,13 +26,6 @@ copy_chromium_flags() { omarchy-install-chromium-ytdlp } -setup_firefox_preferences() { - local distribution_dir="$1" - - setup_policy_directory "$distribution_dir" - sudo cp -f "$OMARCHY_PATH/default/firefox/policies.json" "$distribution_dir/policies.json" -} - setup_firefox_wayland() { mkdir -p ~/.config/environment.d echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf @@ -40,7 +36,7 @@ chromium) echo "Installing Chromium..." omarchy-pkg-add chromium - setup_policy_directory /etc/chromium/policies/managed + setup_chromium_policy_directory /etc/chromium/policies/managed copy_chromium_flags ~/.config/chromium-flags.conf omarchy-theme-set-browser announce_browser_installed "Chromium" @@ -49,7 +45,7 @@ chrome) echo "Installing Chrome..." omarchy-pkg-aur-add google-chrome || exit 1 - setup_policy_directory /etc/opt/chrome/policies/managed + setup_chromium_policy_directory /etc/opt/chrome/policies/managed copy_chromium_flags ~/.config/chrome-flags.conf omarchy-theme-set-browser announce_browser_installed "Chrome" @@ -58,7 +54,7 @@ edge) echo "Installing Edge..." omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1 - setup_policy_directory /etc/opt/edge/policies/managed + setup_chromium_policy_directory /etc/opt/edge/policies/managed copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf omarchy-theme-set-browser announce_browser_installed "Edge" @@ -67,7 +63,7 @@ brave) echo "Installing Brave..." omarchy-pkg-aur-add brave-bin || exit 1 - setup_policy_directory /etc/brave/policies/managed + setup_chromium_policy_directory /etc/brave/policies/managed copy_chromium_flags ~/.config/brave-flags.conf omarchy-theme-set-browser announce_browser_installed "Brave" @@ -76,7 +72,7 @@ brave-origin) echo "Installing Brave Origin..." omarchy-pkg-aur-add brave-origin-bin || exit 1 - setup_policy_directory /etc/brave/policies/managed + setup_chromium_policy_directory /etc/brave/policies/managed copy_chromium_flags ~/.config/brave-origin-flags.conf omarchy-theme-set-browser announce_browser_installed "Brave Origin" @@ -85,7 +81,7 @@ firefox) echo "Installing Firefox..." omarchy-pkg-add firefox || exit 1 - setup_firefox_preferences /usr/lib/firefox/distribution + browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution setup_firefox_wayland announce_browser_installed "Firefox" ;; @@ -93,7 +89,7 @@ zen) echo "Installing Zen..." omarchy-pkg-aur-add zen-browser-bin || exit 1 - setup_firefox_preferences /opt/zen-browser/distribution + browser_policy_setup_firefox_distribution /opt/zen-browser/distribution setup_firefox_wayland announce_browser_installed "Zen" ;; diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index c27de22c..7c208eb9 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -742,6 +742,15 @@ create_user() { # for specific commands), and a duplicate grant is harmless. echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel chmod 440 /etc/sudoers.d/00-omarchy-wheel + + source "$OMARCHY_PATH/install/helpers/browser-policy.sh" + OMARCHY_INSTALL_USER=$username + OMARCHY_PROVISIONING_DIR=$PROVISIONING_DIR + browser_policy_setup_group + for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do + [[ -d $dir ]] || continue + browser_policy_setup_dir "$dir" + done } install_authorized_keys() { diff --git a/bin/omarchy-theme-set-browser b/bin/omarchy-theme-set-browser index 4dd7592f..a22cb0d6 100755 --- a/bin/omarchy-theme-set-browser +++ b/bin/omarchy-theme-set-browser @@ -13,11 +13,10 @@ else THEME_HEX_COLOR="#1c2027" fi -set_browser_policy() { - local policy_dir="$1" +source "$OMARCHY_PATH/install/helpers/browser-policy.sh" - [[ -d $policy_dir ]] || return - echo "{\"BrowserThemeColor\": \"$THEME_HEX_COLOR\", \"BrowserColorScheme\": \"device\"}" | tee "$policy_dir/color.json" >/dev/null +set_browser_policy() { + browser_policy_write_color "$1" "$THEME_HEX_COLOR" } refresh_running_browser() { @@ -30,17 +29,20 @@ refresh_running_browser() { fi } -set_browser_policy /etc/chromium/policies/managed +failed=0 +set_browser_policy /etc/chromium/policies/managed || failed=1 refresh_running_browser chromium chromium -set_browser_policy /etc/opt/chrome/policies/managed +set_browser_policy /etc/opt/chrome/policies/managed || failed=1 refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome -set_browser_policy /etc/opt/edge/policies/managed +set_browser_policy /etc/opt/edge/policies/managed || failed=1 refresh_running_browser msedge microsoft-edge-stable -set_browser_policy /etc/brave/policies/managed +set_browser_policy /etc/brave/policies/managed || failed=1 refresh_running_browser brave brave # Match on the binary path: the running process is named plain "brave", and a # bare -f brave-origin pattern would also match the installer's own terminal. refresh_running_browser /opt/brave-origin-bin/ brave-origin -f + +exit "$failed" diff --git a/bin/omarchy-upgrade-to-quattro b/bin/omarchy-upgrade-to-quattro index e28da697..62ef21b9 100755 --- a/bin/omarchy-upgrade-to-quattro +++ b/bin/omarchy-upgrade-to-quattro @@ -1312,7 +1312,22 @@ apply_system_transition() { /usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true - as_root install -d -m 0777 /etc/chromium/policies/managed + local browser_policy_helper=/usr/share/omarchy/install/helpers/browser-policy.sh + if ! as_root test -f "$browser_policy_helper"; then + warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened." + else + as_root env OMARCHY_PATH=/usr/share/omarchy OMARCHY_INSTALL_USER="$target_user" \ + bash -euo pipefail -c ' + source "$OMARCHY_PATH/install/helpers/browser-policy.sh" + browser_policy_setup_group + browser_policy_setup_dir /etc/chromium/policies/managed + for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do + [[ $dir == "/etc/chromium/policies/managed" ]] && continue + [[ -d $dir ]] || continue + browser_policy_setup_dir "$dir" + done + ' + fi as_root install -d -m 0755 /usr/lib/chromium printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \ as_root tee /usr/lib/chromium/initial_preferences >/dev/null @@ -2306,6 +2321,11 @@ refresh_current_theme_after_upgrade() { # hooks because one of them runs `hyprctl reload`. Still poke terminal # emulators so the active upgrade terminal picks up generated theme files. run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true + + # apply_system_transition purged user-owned color.json. Headless theme-set + # skipped omarchy-theme-set-browser, so rewrite the colour here. + run_as_user_omarchy omarchy-theme-set-browser >/dev/null 2>&1 || + warn "Could not apply browser theme colour. Run 'omarchy theme set \"$theme_name\"' after reboot if Chromium's theme looks stale." } # Everything below mutates the system, so a non-zero exit from here on leaves a diff --git a/install/config/all.sh b/install/config/all.sh index 91256dc7..d8c7d9bb 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -1,4 +1,5 @@ run_logged "$OMARCHY_INSTALL/config/theme-system.sh" +run_logged "$OMARCHY_INSTALL/config/browser-policy.sh" run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh" run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh" run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh" diff --git a/install/config/browser-policy.sh b/install/config/browser-policy.sh new file mode 100644 index 00000000..a02de2f3 --- /dev/null +++ b/install/config/browser-policy.sh @@ -0,0 +1,3 @@ +source "$OMARCHY_PATH/install/helpers/browser-policy.sh" +browser_policy_setup_group +browser_policy_setup_dir /etc/chromium/policies/managed diff --git a/install/config/theme-system.sh b/install/config/theme-system.sh index 2902e1cd..83db0e15 100644 --- a/install/config/theme-system.sh +++ b/install/config/theme-system.sh @@ -6,10 +6,6 @@ ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-next-symbolic.svg \ /usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg gtk-update-icon-cache /usr/share/icons/Yaru &>/dev/null || true -# Chromium policy directory for theme -mkdir -p /etc/chromium/policies/managed -chmod a+rw /etc/chromium/policies/managed - # Default Chromium to follow system appearance ("device") instead of dark mkdir -p /usr/lib/chromium echo '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \ diff --git a/install/helpers/as-root.sh b/install/helpers/as-root.sh new file mode 100644 index 00000000..005ae351 --- /dev/null +++ b/install/helpers/as-root.sh @@ -0,0 +1,7 @@ +as_root() { + if (( EUID == 0 )); then + "$@" + else + sudo "$@" + fi +} diff --git a/install/helpers/browser-policy.sh b/install/helpers/browser-policy.sh new file mode 100644 index 00000000..803df2c6 --- /dev/null +++ b/install/helpers/browser-policy.sh @@ -0,0 +1,171 @@ +# Chromium-family machine policy is mandatory for every profile. A dedicated +# group at 2775 lets every Omarchy user write color.json and every other uid +# read; other-write stays off. Setgid so new files inherit the group. + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/as-root.sh" + +BROWSER_POLICY_GROUP=omarchy-browser-policy + +BROWSER_POLICY_MANAGED_DIRS=( + /etc/chromium/policies/managed + /etc/opt/chrome/policies/managed + /etc/opt/edge/policies/managed + /etc/brave/policies/managed +) + +BROWSER_POLICY_FIREFOX_DIRS=( + /usr/lib/firefox/distribution + /opt/zen-browser/distribution +) + +browser_policy_setup_group() { + local provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}" + + as_root groupadd --system --force "$BROWSER_POLICY_GROUP" + as_root mkdir -p "$provisioning_dir" + if ! grep -qxF "$BROWSER_POLICY_GROUP" "$provisioning_dir/groups" 2>/dev/null; then + printf '%s\n' "$BROWSER_POLICY_GROUP" | as_root tee -a "$provisioning_dir/groups" >/dev/null + fi + + if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then + as_root usermod -aG "$BROWSER_POLICY_GROUP" "$OMARCHY_INSTALL_USER" + fi +} + +browser_policy_grant_user() { + local user=${1:-} + + if [[ -z $user || $user == "root" ]]; then + user=${SUDO_USER:-} + fi + + [[ -n $user && $user != "root" ]] || return 0 + getent passwd "$user" >/dev/null || return 0 + as_root usermod -aG "$BROWSER_POLICY_GROUP" "$user" +} + +browser_policy_purge_dir() { + local dir=$1 + + as_root find "$dir" -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} + +} + +browser_policy_dir_hardened() { + local dir=$1 + + [[ -d $dir ]] || return 1 + [[ $(stat -c '%a' "$dir") == "2775" ]] || return 1 + [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 + [[ $(stat -c '%G' "$dir") == $BROWSER_POLICY_GROUP ]] || return 1 +} + +browser_policy_setup_dir() { + local dir=$1 + + as_root install -d -m 2775 -o root -g "$BROWSER_POLICY_GROUP" "$dir" + browser_policy_purge_dir "$dir" +} + +browser_policy_file_owner() { + local user + + if [[ -n ${OMARCHY_INSTALL_USER:-} && $OMARCHY_INSTALL_USER != "root" ]]; then + printf '%s\n' "$OMARCHY_INSTALL_USER" + return + fi + if [[ -n ${SUDO_USER:-} && $SUDO_USER != "root" ]]; then + printf '%s\n' "$SUDO_USER" + return + fi + if [[ -n ${PKEXEC_UID:-} ]]; then + user=$(getent passwd "$PKEXEC_UID" | cut -d: -f1) + if [[ -n $user && $user != "root" ]]; then + printf '%s\n' "$user" + return + fi + fi + user=${USER:-$(id -un)} + if [[ $user != "root" ]]; then + printf '%s\n' "$user" + fi +} + +# sudo when this process has a controlling terminal (fd 0 is /dev/null under +# `bash -lc cmd &`, but /dev/tty still works). pkexec when it does not. +browser_policy_elevate() { + if (( EUID == 0 )); then + "$@" + elif { exec 3/dev/null; then + exec 3<&- + sudo "$@" + else + pkexec "$@" + fi +} + +browser_policy_write_color() { + local policy_dir=$1 + local hex=$2 + local dest=$policy_dir/color.json + local payload + local tmp + local owner + + [[ -d $policy_dir ]] || return 0 + + payload=$(printf '{"BrowserThemeColor": "%s", "BrowserColorScheme": "device"}\n' "$hex") + tmp=$(mktemp) || return 1 + printf '%s' "$payload" >"$tmp" + + # A planted symlink or directory must not be written through or into. + if [[ -L $dest || -d $dest ]]; then + if ! rm -rf -- "$dest" 2>/dev/null; then + if ! browser_policy_elevate rm -rf -- "$dest"; then + rm -f "$tmp" + echo "omarchy-theme-set-browser: cannot replace $dest (need group $BROWSER_POLICY_GROUP)" >&2 + return 1 + fi + fi + fi + + if install -m 664 -T "$tmp" "$dest" 2>/dev/null; then + rm -f "$tmp" + return 0 + fi + + owner=$(browser_policy_file_owner) + [[ -n $owner ]] || owner=root + if browser_policy_elevate install -m 664 -o "$owner" -g "$BROWSER_POLICY_GROUP" -T "$tmp" "$dest"; then + rm -f "$tmp" + return 0 + fi + + rm -f "$tmp" + echo "omarchy-theme-set-browser: cannot write $dest (need group $BROWSER_POLICY_GROUP)" >&2 + return 1 +} + +browser_policy_firefox_hardened() { + local dir=$1 + + [[ -d $dir ]] || return 1 + [[ $(stat -c '%a' "$dir") == "755" ]] || return 1 + [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 + [[ -f $dir/policies.json && ! -L $dir/policies.json ]] || return 1 +} + +browser_policy_install_firefox_policies() { + local distribution_dir=$1 + local policies=${2:-$OMARCHY_PATH/default/firefox/policies.json} + + as_root install -m 644 -o root -g root -T "$policies" "$distribution_dir/policies.json" +} + +browser_policy_setup_firefox_distribution() { + local distribution_dir=$1 + local policies=${2:-$OMARCHY_PATH/default/firefox/policies.json} + + as_root install -d -m 0755 -o root -g root "$distribution_dir" + browser_policy_purge_dir "$distribution_dir" + browser_policy_install_firefox_policies "$distribution_dir" "$policies" +} diff --git a/migrations/1787515927.sh b/migrations/1787515927.sh new file mode 100644 index 00000000..f15298bd --- /dev/null +++ b/migrations/1787515927.sh @@ -0,0 +1,24 @@ +echo "Stop world-writable Chromium and Firefox policy directories" + +source "$OMARCHY_PATH/install/helpers/browser-policy.sh" + +browser_policy_setup_group +browser_policy_grant_user "${USER:-$(id -un)}" + +repaired=0 +for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do + [[ -d $dir ]] || continue + browser_policy_dir_hardened "$dir" && continue + browser_policy_setup_dir "$dir" + repaired=1 +done + +if (( repaired )); then + omarchy-theme-set-browser +fi + +for dir in "${BROWSER_POLICY_FIREFOX_DIRS[@]}"; do + [[ -d $dir ]] || continue + browser_policy_firefox_hardened "$dir" && continue + browser_policy_setup_firefox_distribution "$dir" +done diff --git a/test/shell.d/browser-policy-dir-test.sh b/test/shell.d/browser-policy-dir-test.sh new file mode 100755 index 00000000..34a0c3cd --- /dev/null +++ b/test/shell.d/browser-policy-dir-test.sh @@ -0,0 +1,244 @@ +#!/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 + +mock_bin=$test_tmp/bin +mkdir -p "$mock_bin" +elev_log=$test_tmp/elev.log +cat >"$mock_bin/sudo" <>"$elev_log" +[[ \${OMARCHY_TEST_SUDO_FAIL:-} == 1 ]] && exit 1 +exit 0 +SH +cat >"$mock_bin/pkexec" <>"$elev_log" +[[ \${OMARCHY_TEST_SUDO_FAIL:-} == 1 ]] && exit 1 +exit 0 +SH +chmod +x "$mock_bin/sudo" "$mock_bin/pkexec" +export PATH="$mock_bin:$PATH" +: >"$elev_log" +export OMARCHY_PATH="$ROOT" +export OMARCHY_PROVISIONING_DIR="$test_tmp/provisioning" + +source "$ROOT/install/helpers/browser-policy.sh" + +# Temp dirs are user-owned; drop -o/-g so install(1) can run unprivileged. +unprivileged_as_root() { + if [[ $1 == "install" ]]; then + shift + local args=() + local skip=0 + local arg + for arg in "$@"; do + if (( skip )); then + skip=0 + continue + fi + case $arg in + -o|-g) skip=1 ;; + *) args+=("$arg") ;; + esac + done + command install "${args[@]}" + else + "$@" + fi +} + +write_dir=$test_tmp/writable +mkdir -p "$write_dir" +browser_policy_write_color "$write_dir" "#aabbcc" || + fail "theme colour writes into a writable policy directory" +grep -F '"BrowserThemeColor": "#aabbcc"' "$write_dir/color.json" >/dev/null || + fail "theme colour writes BrowserThemeColor" +mode=$(stat -c '%a' "$write_dir/color.json") +[[ $mode == "664" ]] || fail "theme colour creates a group-writable policy file" "mode=$mode" +pass "theme colour writes a group-writable color.json" + +if (( EUID == 0 )); then + pass "running as root; skipping the mktemp-failure check" +else + chmod u+w "$write_dir" + export TMPDIR=$test_tmp/missing-tmp + if browser_policy_write_color "$write_dir" "#dead00" 2>/dev/null; then + fail "theme colour fails when mktemp cannot create a file" + fi + unset TMPDIR + grep -F '"BrowserThemeColor": "#aabbcc"' "$write_dir/color.json" >/dev/null || + fail "a failed mktemp leaves an existing color.json intact" + pass "a failed mktemp does not truncate color.json" +fi + +printf 'original\n' >"$test_tmp/pwn" +rm -f "$write_dir/color.json" +ln -s "$test_tmp/pwn" "$write_dir/color.json" +browser_policy_write_color "$write_dir" "#aabbcc" || + fail "theme colour replaces a planted color.json symlink" +[[ -f $write_dir/color.json && ! -L $write_dir/color.json ]] || + fail "theme colour unlinks a planted color.json symlink instead of writing through it" +grep -Fxq 'original' "$test_tmp/pwn" || fail "theme colour leaves the symlink target unchanged" +pass "theme colour does not follow a planted color.json symlink" + +plant_write=$test_tmp/plant-dir +mkdir -p "$plant_write/color.json/nested" +printf 'inside\n' >"$plant_write/color.json/nested/x" +browser_policy_write_color "$plant_write" "#aabbcc" || + fail "theme colour replaces a planted color.json directory" +[[ -f $plant_write/color.json && ! -d $plant_write/color.json ]] || + fail "theme colour does not write into a planted color.json directory" +pass "theme colour does not write into a planted color.json directory" + +missing_dir=$test_tmp/missing +browser_policy_write_color "$missing_dir" "#aabbcc" || + fail "theme colour skips a policy directory that does not exist" +[[ ! -e $missing_dir ]] || fail "theme colour does not create a missing policy directory" +pass "theme colour skips a missing policy directory" + +if (( EUID == 0 )); then + pass "running as root; skipping elevation checks" +else + denied_dir=$test_tmp/denied + mkdir -p "$denied_dir" + chmod a-w "$denied_dir" + owner=${USER:-$(id -un)} + : >"$elev_log" + browser_policy_write_color "$denied_dir" "#aabbcc" || + fail "elevated install reports success from pkexec" + grep -E "^PKEXEC install -m 664 -o $owner -g omarchy-browser-policy -T .+ $denied_dir/color.json$" "$elev_log" >/dev/null || + fail "without a controlling tty, colour write elevates through pkexec as the owner" "$(cat "$elev_log")" + if grep -E '^SUDO ' "$elev_log" >/dev/null; then + fail "without a controlling tty, colour write does not call sudo" "$(cat "$elev_log")" + fi + pass "without a controlling tty, colour write elevates through pkexec" + + : >"$elev_log" + export OMARCHY_TEST_SUDO_FAIL=1 + if browser_policy_write_color "$denied_dir" "#aabbcc" 2>"$test_tmp/write.err"; then + fail "theme colour fails when the policy directory is not writable" + fi + unset OMARCHY_TEST_SUDO_FAIL + grep -F 'omarchy-browser-policy' "$test_tmp/write.err" >/dev/null || + fail "theme colour names the group when the write is denied" + pass "theme colour reports a denied policy write" + + if command -v script >/dev/null; then + : >"$elev_log" + cat >"$test_tmp/tty-write.sh" </dev/null + grep -E "^SUDO install -m 664 -o $owner -g omarchy-browser-policy -T .+ $denied_dir/color.json$" "$elev_log" >/dev/null || + fail "with a controlling tty, colour write elevates through sudo" "$(cat "$elev_log")" + if grep -E '^PKEXEC ' "$elev_log" >/dev/null; then + fail "with a controlling tty, colour write does not call pkexec" "$(cat "$elev_log")" + fi + pass "with a controlling tty, colour write elevates through sudo" + else + pass "script(1) unavailable; skipping the controlling-tty elevation check" + fi +fi + +planted_dir=$test_tmp/planted +mkdir -p "$planted_dir/evil" +printf 'evil\n' >"$planted_dir/evil/f" +printf 'old\n' >"$planted_dir/color.json" +as_root() { unprivileged_as_root "$@"; } +browser_policy_setup_dir "$planted_dir" +[[ ! -e $planted_dir/evil ]] || fail "policy setup drops a non-empty non-root subdirectory" +[[ ! -e $planted_dir/color.json ]] || fail "policy setup drops a non-root color.json" +[[ -d $planted_dir ]] || fail "policy setup leaves the managed directory in place" +pass "policy setup drops non-root files and non-empty subdirectories" + +owned=$test_tmp/not-root +mkdir -p "$owned" +chmod 2775 "$owned" +BROWSER_POLICY_GROUP=$(id -gn) +if browser_policy_dir_hardened "$owned"; then + fail "a user-owned 2775 directory is not treated as hardened" +fi +BROWSER_POLICY_GROUP=omarchy-browser-policy +pass "a hardened directory must be root-owned" + +dist=$test_tmp/distribution +mkdir -p "$dist" +printf 'original\n' >"$test_tmp/firefox-pwn" +ln -s "$test_tmp/firefox-pwn" "$dist/policies.json" +as_root() { unprivileged_as_root "$@"; } +browser_policy_install_firefox_policies "$dist" || + fail "Firefox policy install replaces a planted policies.json symlink" +[[ -f $dist/policies.json && ! -L $dist/policies.json ]] || + fail "Firefox policy install unlinks a planted policies.json symlink instead of writing through it" +grep -Fxq 'original' "$test_tmp/firefox-pwn" || fail "Firefox policy install leaves the symlink target unchanged" +grep -q '"policies"' "$dist/policies.json" || fail "Firefox policy install writes the stock policies" +pass "Firefox policy install does not follow a planted policies.json symlink" + +dir_dist=$test_tmp/distribution-dir +mkdir -p "$dir_dist" +mkdir "$dir_dist/policies.json" +as_root() { unprivileged_as_root "$@"; } +if browser_policy_install_firefox_policies "$dir_dist" 2>/dev/null; then + fail "Firefox policy install refuses a planted policies.json directory" +fi +[[ -d $dir_dist/policies.json ]] || fail "Firefox policy install leaves a planted policies.json directory in place" +pass "Firefox policy install does not write into a planted policies.json directory" + +grant_log=$test_tmp/usermod.calls +as_root() { + if [[ $1 == "usermod" ]]; then + printf '%s\n' "$*" >>"$grant_log" + return 0 + fi + unprivileged_as_root "$@" +} +invoker=${USER:-$(id -un)} +: >"$grant_log" +SUDO_USER=$invoker +browser_policy_grant_user root +unset SUDO_USER +grep -qx -- "usermod -aG omarchy-browser-policy $invoker" "$grant_log" || + fail "granting as root uses SUDO_USER" "$(cat "$grant_log")" +: >"$grant_log" +OMARCHY_INSTALL_USER="" +browser_policy_grant_user "" +[[ ! -s $grant_log ]] || fail "an empty grant does not usermod" +pass "sudo install browser grants the invoking user, not root" + +grep -F 'exit "$failed"' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null || + fail "omarchy-theme-set-browser exits non-zero when a policy write fails" +pass "omarchy-theme-set-browser exits non-zero when a policy write fails" + +policy_files=( + "$ROOT/bin/omarchy-install-browser" + "$ROOT/bin/omarchy-provision-owner" + "$ROOT/bin/omarchy-theme-set-browser" + "$ROOT/bin/omarchy-upgrade-to-quattro" + "$ROOT/install/config/theme-system.sh" + "$ROOT/install/config/browser-policy.sh" + "$ROOT/install/helpers/browser-policy.sh" + "$ROOT/migrations/1787515927.sh" +) +if grep -nE 'chmod a\+rwx\b|chmod a\+rw\b|chmod a\+w\b|chmod o\+w|chmod ugo\+w|chmod 2777\b|chmod 0777\b|chmod 777\b|install -d -m 0?[27]?777' "${policy_files[@]}" >/dev/null; then + fail "browser policy setup is not world-writable" +fi +pass "browser policy setup is not world-writable" + +mapfile -t migrations < <(rg -l 'Stop world-writable Chromium and Firefox policy directories' "$ROOT/migrations") +(( ${#migrations[@]} == 1 )) || fail "exactly one migration locks existing policy directories" "${migrations[*]}" +grep -F 'browser_policy_dir_hardened' "${migrations[0]}" >/dev/null || + fail "the policy-directory migration no-ops a machine already repaired" +grep -F 'browser_policy_grant_user' "${migrations[0]}" >/dev/null || + fail "the policy-directory migration still grants the current user the group" +grep -F 'BROWSER_POLICY_FIREFOX_DIRS' "${migrations[0]}" >/dev/null || + fail "the policy-directory migration covers Firefox and Zen" +grep -F '/opt/zen-browser/distribution' "$ROOT/install/helpers/browser-policy.sh" >/dev/null || + fail "the shared helper names the Zen distribution directory" +pass "a migration locks existing policy directories" diff --git a/test/shell.d/default-apps-test.sh b/test/shell.d/default-apps-test.sh index aa07659d..00a9af55 100755 --- a/test/shell.d/default-apps-test.sh +++ b/test/shell.d/default-apps-test.sh @@ -61,11 +61,13 @@ if [[ $installer == "omarchy-install-browser" && ${OMARCHY_TEST_REAL_BROWSER_INS fi case $installer in -omarchy-pkg-add) +omarchy-pkg-add|omarchy-pkg-aur-add) package=$1 printf 'pkg:%s\n' "$package" >>"$OMARCHY_TEST_INSTALL_LOG" case $package in chromium) command=chromium ;; + firefox) command=firefox ;; + zen-browser-bin) command=zen-browser ;; cursor-bin) command=cursor ;; sublime-text-4) command=sublime_text ;; vim) command=vim ;; @@ -107,6 +109,7 @@ SH for installer in \ omarchy-pkg-add \ + omarchy-pkg-aur-add \ omarchy-install-browser \ omarchy-install-terminal \ omarchy-install-editor-vscode \ @@ -205,10 +208,14 @@ OMARCHY_TEST_REAL_BROWSER_INSTALL=true omarchy-default-browser --install chromiu [[ $(omarchy-default-browser) == "chromium" ]] || fail "Chromium becomes the default after its full installer succeeds" cmp -s "$ROOT/config/chromium-flags.conf" "$test_home/.config/chromium-flags.conf" || fail "Chromium browser installer copies the default flags" -grep -Fxq 'sudo:mkdir -p /etc/chromium/policies/managed' "$setup_log" || - fail "Chromium browser installer creates its policy directory" -grep -Fxq 'sudo:chmod a+rw /etc/chromium/policies/managed' "$setup_log" || - fail "Chromium browser installer makes its policy directory writable" +grep -Fxq 'sudo:groupadd --system --force omarchy-browser-policy' "$setup_log" || + fail "Chromium browser installer creates the browser-policy group" +grep -Fxq 'sudo:install -d -m 2775 -o root -g omarchy-browser-policy /etc/chromium/policies/managed' "$setup_log" || + fail "Chromium browser installer creates a group-writable managed policy directory" +grep -Fxq 'sudo:find /etc/chromium/policies/managed -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" || + fail "Chromium browser installer drops non-root files from its policy directory" +grep -Fxq "sudo:usermod -aG omarchy-browser-policy ${USER:-$(id -un)}" "$setup_log" || + fail "Chromium browser installer grants the installing user the browser-policy group" grep -Fxq 'omarchy-install-chromium-copy-url:' "$setup_log" || fail "Chromium browser installer registers the Copy URL host" grep -Fxq 'omarchy-install-chromium-ytdlp:' "$setup_log" || @@ -217,6 +224,36 @@ grep -Fxq 'omarchy-theme-set-browser:' "$setup_log" || fail "Chromium browser installer applies the current theme" pass "Chromium browser installer restores the complete Omarchy setup" +: >"$install_log" +: >"$setup_log" +rm -f "$installed_dir/firefox" +OMARCHY_TEST_REAL_BROWSER_INSTALL=true omarchy-default-browser --install firefox >/dev/null +[[ $(<"$install_log") == "pkg:firefox" ]] || fail "Firefox browser installer installs the package" +[[ $(omarchy-default-browser) == "firefox" ]] || fail "Firefox becomes the default after its full installer succeeds" +grep -Fxq 'sudo:install -d -m 0755 -o root -g root /usr/lib/firefox/distribution' "$setup_log" || + fail "Firefox browser installer creates its distribution directory" +grep -Fxq 'sudo:find /usr/lib/firefox/distribution -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" || + fail "Firefox browser installer drops non-root files from its distribution directory" +grep -Fxq "sudo:install -m 644 -o root -g root -T $ROOT/default/firefox/policies.json /usr/lib/firefox/distribution/policies.json" "$setup_log" || + fail "Firefox browser installer copies policies.json without following a destination symlink" +[[ -e $installed_dir/firefox ]] || fail "Firefox browser installer marks firefox installed" +pass "Firefox browser installer restores the complete Omarchy setup" + +: >"$install_log" +: >"$setup_log" +rm -f "$installed_dir/zen-browser" +OMARCHY_TEST_REAL_BROWSER_INSTALL=true omarchy-default-browser --install zen >/dev/null +[[ $(<"$install_log") == "pkg:zen-browser-bin" ]] || fail "Zen browser installer installs the package" +[[ $(omarchy-default-browser) == "zen" ]] || fail "Zen becomes the default after its full installer succeeds" +grep -Fxq 'sudo:install -d -m 0755 -o root -g root /opt/zen-browser/distribution' "$setup_log" || + fail "Zen browser installer creates its distribution directory" +grep -Fxq 'sudo:find /opt/zen-browser/distribution -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" || + fail "Zen browser installer drops non-root files from its distribution directory" +grep -Fxq "sudo:install -m 644 -o root -g root -T $ROOT/default/firefox/policies.json /opt/zen-browser/distribution/policies.json" "$setup_log" || + fail "Zen browser installer copies policies.json without following a destination symlink" +[[ -e $installed_dir/zen-browser ]] || fail "Zen browser installer marks zen-browser installed" +pass "Zen browser installer restores the complete Omarchy setup" + omarchy-default-browser zen rm -f "$installed_dir/chromium" if OMARCHY_TEST_REAL_BROWSER_INSTALL=true OMARCHY_TEST_INSTALL_FAIL=true \ diff --git a/test/shell.d/provisioning-groups-test.sh b/test/shell.d/provisioning-groups-test.sh index d0eac226..7b5c6964 100644 --- a/test/shell.d/provisioning-groups-test.sh +++ b/test/shell.d/provisioning-groups-test.sh @@ -27,16 +27,40 @@ cat >"$TMPDIR/bin/usermod" <>"$TMPDIR/usermod.calls" STUB -chmod +x "$TMPDIR/bin/getent" "$TMPDIR/bin/usermod" +cat >"$TMPDIR/bin/groupadd" <>"$TMPDIR/groupadd.calls" +STUB +cat >"$TMPDIR/bin/install" <>"$TMPDIR/install.calls" +STUB +cat >"$TMPDIR/bin/find" <>"$TMPDIR/find.calls" +STUB +cat >"$TMPDIR/bin/sudo" <>"$TMPDIR/sudo.calls" +exec "\$@" +STUB +chmod +x "$TMPDIR/bin"/{getent,usermod,groupadd,install,find,sudo} export PATH="$TMPDIR/bin:$PATH" +export OMARCHY_PATH="$ROOT" -# No install user (deferred-provisioning install): input recorded, usermod not called. +# No install user (deferred-provisioning install): groups recorded, usermod not called. OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/docker.sh" OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh" +OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh" [[ -f $OMARCHY_PROVISIONING_DIR/groups ]] || fail "groups file written without an install user" grep -qxF input "$OMARCHY_PROVISIONING_DIR/groups" || fail "input group recorded" +grep -qxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups" || fail "browser-policy group recorded" [[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called without an install user" +grep -F -- '--system --force omarchy-browser-policy' "$TMPDIR/groupadd.calls" >/dev/null || + fail "browser-policy group is created as a system group" +grep -F -- '-d -m 2775 -o root -g omarchy-browser-policy /etc/chromium/policies/managed' "$TMPDIR/install.calls" >/dev/null || + fail "browser-policy directory is created group-writable" pass "deferred provisioning records groups without calling usermod" # The docker group is root-equivalent and must never be granted automatically. @@ -45,17 +69,24 @@ pass "docker group is not recorded at install" # Missing user (defensive): no usermod either. OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/hardware/input-group.sh" +OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/config/browser-policy.sh" [[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called for a missing user" pass "missing install user defers group grants" # Re-running never duplicates entries. OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh" [[ $(grep -cxF input "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || fail "input group recorded once" +OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh" +[[ $(grep -cxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || + fail "browser-policy group recorded once" pass "group recording is idempotent" # Existing user: usermod applies the recorded groups, and docker is never among them. OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/docker.sh" OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/hardware/input-group.sh" +OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/browser-policy.sh" grep -qx -- "-aG input existing" "$TMPDIR/usermod.calls" || fail "usermod grants input to the install user" +grep -qx -- "-aG omarchy-browser-policy existing" "$TMPDIR/usermod.calls" || + fail "usermod grants browser-policy to the install user" ! grep -q -- "docker" "$TMPDIR/usermod.calls" || fail "usermod must not grant docker to the install user" -pass "existing install user gets input but never docker" +pass "existing install user gets input and browser-policy but never docker" diff --git a/test/shell.d/upgrade-to-quattro-test.sh b/test/shell.d/upgrade-to-quattro-test.sh index bf94605f..c38c17f7 100644 --- a/test/shell.d/upgrade-to-quattro-test.sh +++ b/test/shell.d/upgrade-to-quattro-test.sh @@ -67,6 +67,23 @@ grep -F 'OMARCHY_INSTALL_USER="$target_user"' "$upgrade_to_quattro" >/dev/null grep -F '"$apply_lock"' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade configures lock screen authentication for the target user" +grep -F 'install/helpers/browser-policy.sh' "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade uses the shared browser-policy helper" +grep -F 'as_root test -f "$browser_policy_helper"' "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade survives a packaged tree without the browser-policy helper" +grep -F 'browser_policy_setup_group' "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade creates the browser-policy group" +grep -F 'browser_policy_setup_dir /etc/chromium/policies/managed' "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade creates a group-writable Chromium policy directory" +grep -F 'BROWSER_POLICY_MANAGED_DIRS' "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade hardens every Chromium-family policy directory" +grep -F 'run_as_user_omarchy omarchy-theme-set-browser' "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade rewrites browser theme colour after a headless theme-set" +if grep -E 'install -d -m 0?[27]?777 /etc/.*/policies|chmod a\+rw' "$upgrade_to_quattro" >/dev/null; then + fail "Omarchy 4 upgrade does not create a world-writable Chromium policy directory" +fi +pass "Omarchy 4 upgrade locks the Chromium policy directory to the browser-policy group" + grep -F 'OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd.service' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd.socket' "$upgrade_to_quattro" >/dev/null From b0e6611c704e617a238d250ab62109047a8239ac Mon Sep 17 00:00:00 2001 From: acrogenesis Date: Mon, 24 Aug 2026 02:01:24 -0600 Subject: [PATCH 2/6] Require root-owned Firefox policies before skipping repair The hardened gate only looked at the distribution directory. A regular policies.json planted under the old 777 mode would then be left in place if the directory later looked 755/root. --- install/helpers/browser-policy.sh | 16 +++++++++++++++- test/shell.d/browser-policy-dir-test.sh | 12 ++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/install/helpers/browser-policy.sh b/install/helpers/browser-policy.sh index 803df2c6..b383e325 100644 --- a/install/helpers/browser-policy.sh +++ b/install/helpers/browser-policy.sh @@ -145,13 +145,27 @@ browser_policy_write_color() { return 1 } +browser_policy_firefox_policy_file_ok() { + local file=$1 + local mode + local group_write + local other_write + + [[ -f $file && ! -L $file ]] || return 1 + [[ $(stat -c '%U' "$file") == "root" ]] || return 1 + mode=$(stat -c '%a' "$file") + group_write=$((8#${mode: -2:1})) + other_write=$((8#${mode: -1})) + (( (group_write & 2) == 0 && (other_write & 2) == 0 )) +} + browser_policy_firefox_hardened() { local dir=$1 [[ -d $dir ]] || return 1 [[ $(stat -c '%a' "$dir") == "755" ]] || return 1 [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 - [[ -f $dir/policies.json && ! -L $dir/policies.json ]] || return 1 + browser_policy_firefox_policy_file_ok "$dir/policies.json" } browser_policy_install_firefox_policies() { diff --git a/test/shell.d/browser-policy-dir-test.sh b/test/shell.d/browser-policy-dir-test.sh index 34a0c3cd..c7b795fc 100755 --- a/test/shell.d/browser-policy-dir-test.sh +++ b/test/shell.d/browser-policy-dir-test.sh @@ -168,6 +168,18 @@ fi BROWSER_POLICY_GROUP=omarchy-browser-policy pass "a hardened directory must be root-owned" +fx_policy=$test_tmp/policies.json +printf '%s\n' '{"policies":{}}' >"$fx_policy" +chmod 644 "$fx_policy" +if browser_policy_firefox_policy_file_ok "$fx_policy"; then + fail "a user-owned policies.json is not treated as hardened" +fi +ln -sf "$fx_policy" "$test_tmp/policies-link.json" +if browser_policy_firefox_policy_file_ok "$test_tmp/policies-link.json"; then + fail "a policies.json symlink is not treated as hardened" +fi +pass "Firefox policy files must be root-owned regular files without group or other write" + dist=$test_tmp/distribution mkdir -p "$dist" printf 'original\n' >"$test_tmp/firefox-pwn" From 87dfa14c5645d37bc9df454450f91680ffe587dc Mon Sep 17 00:00:00 2001 From: acrogenesis Date: Mon, 24 Aug 2026 10:58:33 -0600 Subject: [PATCH 3/6] Keep a trusted Firefox policies.json when repairing the directory A world-writable distribution dir failed the hardened check even when policies.json was already root-owned, and setup then overwrote it. --- migrations/1787515927.sh | 6 +++++- test/shell.d/browser-policy-dir-test.sh | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/migrations/1787515927.sh b/migrations/1787515927.sh index f15298bd..e16efd20 100644 --- a/migrations/1787515927.sh +++ b/migrations/1787515927.sh @@ -20,5 +20,9 @@ fi for dir in "${BROWSER_POLICY_FIREFOX_DIRS[@]}"; do [[ -d $dir ]] || continue browser_policy_firefox_hardened "$dir" && continue - browser_policy_setup_firefox_distribution "$dir" + as_root install -d -m 0755 -o root -g root "$dir" + browser_policy_purge_dir "$dir" + if ! browser_policy_firefox_policy_file_ok "$dir/policies.json"; then + browser_policy_install_firefox_policies "$dir" + fi done diff --git a/test/shell.d/browser-policy-dir-test.sh b/test/shell.d/browser-policy-dir-test.sh index c7b795fc..63591de1 100755 --- a/test/shell.d/browser-policy-dir-test.sh +++ b/test/shell.d/browser-policy-dir-test.sh @@ -251,6 +251,8 @@ grep -F 'browser_policy_grant_user' "${migrations[0]}" >/dev/null || fail "the policy-directory migration still grants the current user the group" grep -F 'BROWSER_POLICY_FIREFOX_DIRS' "${migrations[0]}" >/dev/null || fail "the policy-directory migration covers Firefox and Zen" +grep -F 'browser_policy_firefox_policy_file_ok' "${migrations[0]}" >/dev/null || + fail "the policy-directory migration keeps a trusted Firefox policies.json" grep -F '/opt/zen-browser/distribution' "$ROOT/install/helpers/browser-policy.sh" >/dev/null || fail "the shared helper names the Zen distribution directory" pass "a migration locks existing policy directories" From bebe19bc70696c916d4fa1d0fd39d480bfff99a0 Mon Sep 17 00:00:00 2001 From: acrogenesis Date: Tue, 25 Aug 2026 12:10:08 -0600 Subject: [PATCH 4/6] Harden browser policy parent directories and validate theme RGB install -d follows a planted ancestor symlink, and a writable parent can rename the managed leaf aside. chromium.theme is user-installed, so only a 0-255 RGB triple becomes a colour. --- bin/omarchy-theme-set-browser | 11 ++-- install/helpers/browser-policy.sh | 67 ++++++++++++++++++++ migrations/1787515927.sh | 4 +- test/shell.d/browser-policy-dir-test.sh | 83 +++++++++++++++++++++++++ test/shell.d/default-apps-test.sh | 4 ++ 5 files changed, 161 insertions(+), 8 deletions(-) diff --git a/bin/omarchy-theme-set-browser b/bin/omarchy-theme-set-browser index a22cb0d6..60fa3211 100755 --- a/bin/omarchy-theme-set-browser +++ b/bin/omarchy-theme-set-browser @@ -3,18 +3,15 @@ # omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave # omarchy:hidden=true +source "$OMARCHY_PATH/install/helpers/browser-policy.sh" + CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme +THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR if [[ -f $CHROMIUM_THEME ]]; then - THEME_RGB_COLOR=$(<$CHROMIUM_THEME) - THEME_HEX_COLOR=$(printf '#%02x%02x%02x' ${THEME_RGB_COLOR//,/ }) -else - # Use a default, neutral grey if theme doesn't have a color - THEME_HEX_COLOR="#1c2027" + THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)") fi -source "$OMARCHY_PATH/install/helpers/browser-policy.sh" - set_browser_policy() { browser_policy_write_color "$1" "$THEME_HEX_COLOR" } diff --git a/install/helpers/browser-policy.sh b/install/helpers/browser-policy.sh index b383e325..8826243b 100644 --- a/install/helpers/browser-policy.sh +++ b/install/helpers/browser-policy.sh @@ -13,11 +13,26 @@ BROWSER_POLICY_MANAGED_DIRS=( /etc/brave/policies/managed ) +# Ancestors of the managed dirs, shortest first. A writable or attacker-owned +# parent can rename the leaf aside; install -d follows a planted symlink. +BROWSER_POLICY_PARENT_DIRS=( + /etc/chromium + /etc/chromium/policies + /etc/opt/chrome + /etc/opt/chrome/policies + /etc/opt/edge + /etc/opt/edge/policies + /etc/brave + /etc/brave/policies +) + BROWSER_POLICY_FIREFOX_DIRS=( /usr/lib/firefox/distribution /opt/zen-browser/distribution ) +BROWSER_POLICY_DEFAULT_COLOR="#1c2027" + browser_policy_setup_group() { local provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}" @@ -59,13 +74,65 @@ browser_policy_dir_hardened() { [[ $(stat -c '%G' "$dir") == $BROWSER_POLICY_GROUP ]] || return 1 } +browser_policy_parent_hardened() { + local dir=$1 + + [[ -d $dir && ! -L $dir ]] || return 1 + [[ $(stat -c '%a' "$dir") == "755" ]] || return 1 + [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 +} + +browser_policy_parents_hardened() { + local dir=$1 + local parent + + for parent in "${BROWSER_POLICY_PARENT_DIRS[@]}"; do + [[ $dir == "$parent"/* ]] || continue + [[ -e $parent || -L $parent ]] || continue + browser_policy_parent_hardened "$parent" || return 1 + done +} + +browser_policy_setup_parent() { + local dir=$1 + + if [[ -L $dir || ( -e $dir && ! -d $dir ) ]]; then + as_root rm -rf -- "$dir" + fi + as_root install -d -m 0755 -o root -g root "$dir" +} + +browser_policy_setup_parents_for() { + local dir=$1 + local parent + + for parent in "${BROWSER_POLICY_PARENT_DIRS[@]}"; do + [[ $dir == "$parent"/* ]] || continue + browser_policy_setup_parent "$parent" + done +} + browser_policy_setup_dir() { local dir=$1 + browser_policy_setup_parents_for "$dir" as_root install -d -m 2775 -o root -g "$BROWSER_POLICY_GROUP" "$dir" browser_policy_purge_dir "$dir" } +# Themes are user-installed. Accept only three 0-255 components. +browser_policy_theme_hex() { + local theme_rgb=$1 + + if [[ $theme_rgb =~ ^[[:space:]]*([0-9]{1,3})[[:space:]]*,[[:space:]]*([0-9]{1,3})[[:space:]]*,[[:space:]]*([0-9]{1,3})[[:space:]]*$ ]] && + (( 10#${BASH_REMATCH[1]} < 256 && 10#${BASH_REMATCH[2]} < 256 && 10#${BASH_REMATCH[3]} < 256 )); then + printf '#%02x%02x%02x' "$((10#${BASH_REMATCH[1]}))" "$((10#${BASH_REMATCH[2]}))" "$((10#${BASH_REMATCH[3]}))" + return + fi + + printf '%s' "$BROWSER_POLICY_DEFAULT_COLOR" +} + browser_policy_file_owner() { local user diff --git a/migrations/1787515927.sh b/migrations/1787515927.sh index e16efd20..787418e7 100644 --- a/migrations/1787515927.sh +++ b/migrations/1787515927.sh @@ -8,7 +8,9 @@ browser_policy_grant_user "${USER:-$(id -un)}" repaired=0 for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do [[ -d $dir ]] || continue - browser_policy_dir_hardened "$dir" && continue + if browser_policy_dir_hardened "$dir" && browser_policy_parents_hardened "$dir"; then + continue + fi browser_policy_setup_dir "$dir" repaired=1 done diff --git a/test/shell.d/browser-policy-dir-test.sh b/test/shell.d/browser-policy-dir-test.sh index 63591de1..7092a515 100755 --- a/test/shell.d/browser-policy-dir-test.sh +++ b/test/shell.d/browser-policy-dir-test.sh @@ -168,6 +168,87 @@ fi BROWSER_POLICY_GROUP=omarchy-browser-policy pass "a hardened directory must be root-owned" +saved_parent_dirs=("${BROWSER_POLICY_PARENT_DIRS[@]}") +parent_root=$test_tmp/parents +mkdir -p "$parent_root/etc/chromium/policies/managed/keep" +printf 'keep\n' >"$parent_root/etc/chromium/policies/managed/keep/x" +chmod 0777 "$parent_root/etc/chromium" "$parent_root/etc/chromium/policies" +chmod 2775 "$parent_root/etc/chromium/policies/managed" +BROWSER_POLICY_PARENT_DIRS=( + "$parent_root/etc/chromium" + "$parent_root/etc/chromium/policies" +) +as_root() { unprivileged_as_root "$@"; } +if browser_policy_parents_hardened "$parent_root/etc/chromium/policies/managed"; then + fail "a world-writable policy parent is not treated as hardened" +fi +browser_policy_setup_parents_for "$parent_root/etc/chromium/policies/managed" +mode=$(stat -c '%a' "$parent_root/etc/chromium") +[[ $mode == "755" ]] || fail "setup tightens /etc/chromium" "mode=$mode" +mode=$(stat -c '%a' "$parent_root/etc/chromium/policies") +[[ $mode == "755" ]] || fail "setup tightens /etc/chromium/policies" "mode=$mode" +[[ -d $parent_root/etc/chromium/policies/managed/keep ]] || + fail "parent repair does not purge the managed directory" +pass "policy parent directories are tightened to 0755 without purging the leaf" + +symlink_root=$test_tmp/symlink-parents +mkdir -p "$symlink_root/etc" "$symlink_root/attacker/policies/managed" +printf 'planted\n' >"$symlink_root/attacker/policies/managed/evil.json" +ln -s "$symlink_root/attacker" "$symlink_root/etc/chromium" +BROWSER_POLICY_PARENT_DIRS=( + "$symlink_root/etc/chromium" + "$symlink_root/etc/chromium/policies" +) +as_root() { unprivileged_as_root "$@"; } +browser_policy_setup_dir "$symlink_root/etc/chromium/policies/managed" +[[ ! -L $symlink_root/etc/chromium ]] || fail "setup replaces a planted /etc/chromium symlink" +[[ -d $symlink_root/etc/chromium && ! -L $symlink_root/etc/chromium ]] || + fail "setup recreates /etc/chromium as a real directory" +[[ -d $symlink_root/etc/chromium/policies && ! -L $symlink_root/etc/chromium/policies ]] || + fail "setup recreates /etc/chromium/policies as a real directory" +[[ ! -e $symlink_root/etc/chromium/policies/managed/evil.json ]] || + fail "setup does not keep policy that lived behind a planted parent symlink" +grep -Fxq 'planted' "$symlink_root/attacker/policies/managed/evil.json" || + fail "replacing a parent symlink does not delete the symlink target" +BROWSER_POLICY_PARENT_DIRS=("${saved_parent_dirs[@]}") +pass "policy setup does not follow a planted parent symlink" + +[[ $(browser_policy_theme_hex "242,240,229") == "#f2f0e5" ]] || + fail "theme colour converts an RGB triple to hex" +[[ $(browser_policy_theme_hex $'14,31,41\n') == "#0e1f29" ]] || + fail "theme colour accepts a trailing newline" +[[ $(browser_policy_theme_hex "0,0,0") == "#000000" ]] || + fail "theme colour pads single-digit components" +[[ $(browser_policy_theme_hex " 12 , 11 , 12 ") == "#0c0b0c" ]] || + fail "theme colour tolerates surrounding whitespace" +[[ $(browser_policy_theme_hex "08,09,10") == "#08090a" ]] || + fail "theme colour treats leading zeros as decimal" +for malformed in "" "not,a,color" "1,2" "1,2,3,4" "256,0,0" "999,999,999" "-1,0,0" \ + "1,2,3;id" '1,2,$(id)' "0x10,0,0" "1,2,3 4,5,6"; do + [[ $(browser_policy_theme_hex "$malformed") == "#1c2027" ]] || + fail "theme colour falls back to the neutral grey for '$malformed'" +done +pass "theme colour is six hex digits or the stock grey" + +for theme in "$ROOT"/themes/*/chromium.theme; do + [[ -f $theme ]] || continue + rgb=$(<$theme) + hex=$(browser_policy_theme_hex "$rgb") + [[ $hex =~ ^#[0-9a-f]{6}$ ]] || + fail "shipped $(basename "$(dirname "$theme")") chromium.theme parses as hex" "got: $hex from $(printf %q "$rgb")" + if [[ $hex == "#1c2027" && ! $rgb =~ ^[[:space:]]*28[[:space:]]*,[[:space:]]*32[[:space:]]*,[[:space:]]*39[[:space:]]*$ ]]; then + fail "shipped $(basename "$(dirname "$theme")") chromium.theme is a valid RGB triple" "got: $(printf %q "$rgb")" + fi +done +pass "shipped chromium.theme files parse as RGB triples" + +grep -F 'browser_policy_theme_hex' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null || + fail "omarchy-theme-set-browser parses chromium.theme through browser_policy_theme_hex" +if grep -E 'printf.*THEME_RGB_COLOR' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null; then + fail "omarchy-theme-set-browser does not hand unvetted theme words to printf" +fi +pass "omarchy-theme-set-browser validates the theme colour" + fx_policy=$test_tmp/policies.json printf '%s\n' '{"policies":{}}' >"$fx_policy" chmod 644 "$fx_policy" @@ -247,6 +328,8 @@ mapfile -t migrations < <(rg -l 'Stop world-writable Chromium and Firefox policy (( ${#migrations[@]} == 1 )) || fail "exactly one migration locks existing policy directories" "${migrations[*]}" grep -F 'browser_policy_dir_hardened' "${migrations[0]}" >/dev/null || fail "the policy-directory migration no-ops a machine already repaired" +grep -F 'browser_policy_parents_hardened' "${migrations[0]}" >/dev/null || + fail "the policy-directory migration repairs a world-writable parent of a hardened leaf" grep -F 'browser_policy_grant_user' "${migrations[0]}" >/dev/null || fail "the policy-directory migration still grants the current user the group" grep -F 'BROWSER_POLICY_FIREFOX_DIRS' "${migrations[0]}" >/dev/null || diff --git a/test/shell.d/default-apps-test.sh b/test/shell.d/default-apps-test.sh index 00a9af55..c88c2253 100755 --- a/test/shell.d/default-apps-test.sh +++ b/test/shell.d/default-apps-test.sh @@ -210,6 +210,10 @@ cmp -s "$ROOT/config/chromium-flags.conf" "$test_home/.config/chromium-flags.con fail "Chromium browser installer copies the default flags" grep -Fxq 'sudo:groupadd --system --force omarchy-browser-policy' "$setup_log" || fail "Chromium browser installer creates the browser-policy group" +grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium' "$setup_log" || + fail "Chromium browser installer creates a root-owned Chromium policy parent" +grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium/policies' "$setup_log" || + fail "Chromium browser installer creates a root-owned Chromium policies parent" grep -Fxq 'sudo:install -d -m 2775 -o root -g omarchy-browser-policy /etc/chromium/policies/managed' "$setup_log" || fail "Chromium browser installer creates a group-writable managed policy directory" grep -Fxq 'sudo:find /etc/chromium/policies/managed -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" || From 44a186afe48891f5304cd14fafa3462093cfede9 Mon Sep 17 00:00:00 2001 From: acrogenesis Date: Tue, 25 Aug 2026 12:14:52 -0600 Subject: [PATCH 5/6] Replace planted policy directory symlinks instead of following them install -d follows a managed or distribution symlink and would chmod the target. Unlink those paths first, and treat a dangling symlink as a directory the migration still has to repair. --- install/helpers/browser-policy.sh | 9 +++-- migrations/1787515927.sh | 6 ++-- test/shell.d/browser-policy-dir-test.sh | 44 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/install/helpers/browser-policy.sh b/install/helpers/browser-policy.sh index 8826243b..687edce2 100644 --- a/install/helpers/browser-policy.sh +++ b/install/helpers/browser-policy.sh @@ -68,7 +68,7 @@ browser_policy_purge_dir() { browser_policy_dir_hardened() { local dir=$1 - [[ -d $dir ]] || return 1 + [[ -d $dir && ! -L $dir ]] || return 1 [[ $(stat -c '%a' "$dir") == "2775" ]] || return 1 [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 [[ $(stat -c '%G' "$dir") == $BROWSER_POLICY_GROUP ]] || return 1 @@ -116,6 +116,9 @@ browser_policy_setup_dir() { local dir=$1 browser_policy_setup_parents_for "$dir" + if [[ -L $dir || ( -e $dir && ! -d $dir ) ]]; then + as_root rm -rf -- "$dir" + fi as_root install -d -m 2775 -o root -g "$BROWSER_POLICY_GROUP" "$dir" browser_policy_purge_dir "$dir" } @@ -229,7 +232,7 @@ browser_policy_firefox_policy_file_ok() { browser_policy_firefox_hardened() { local dir=$1 - [[ -d $dir ]] || return 1 + [[ -d $dir && ! -L $dir ]] || return 1 [[ $(stat -c '%a' "$dir") == "755" ]] || return 1 [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 browser_policy_firefox_policy_file_ok "$dir/policies.json" @@ -246,7 +249,7 @@ browser_policy_setup_firefox_distribution() { local distribution_dir=$1 local policies=${2:-$OMARCHY_PATH/default/firefox/policies.json} - as_root install -d -m 0755 -o root -g root "$distribution_dir" + browser_policy_setup_parent "$distribution_dir" browser_policy_purge_dir "$distribution_dir" browser_policy_install_firefox_policies "$distribution_dir" "$policies" } diff --git a/migrations/1787515927.sh b/migrations/1787515927.sh index 787418e7..113daf34 100644 --- a/migrations/1787515927.sh +++ b/migrations/1787515927.sh @@ -7,7 +7,7 @@ browser_policy_grant_user "${USER:-$(id -un)}" repaired=0 for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do - [[ -d $dir ]] || continue + [[ -d $dir || -L $dir ]] || continue if browser_policy_dir_hardened "$dir" && browser_policy_parents_hardened "$dir"; then continue fi @@ -20,9 +20,9 @@ if (( repaired )); then fi for dir in "${BROWSER_POLICY_FIREFOX_DIRS[@]}"; do - [[ -d $dir ]] || continue + [[ -d $dir || -L $dir ]] || continue browser_policy_firefox_hardened "$dir" && continue - as_root install -d -m 0755 -o root -g root "$dir" + browser_policy_setup_parent "$dir" browser_policy_purge_dir "$dir" if ! browser_policy_firefox_policy_file_ok "$dir/policies.json"; then browser_policy_install_firefox_policies "$dir" diff --git a/test/shell.d/browser-policy-dir-test.sh b/test/shell.d/browser-policy-dir-test.sh index 7092a515..b06f174f 100755 --- a/test/shell.d/browser-policy-dir-test.sh +++ b/test/shell.d/browser-policy-dir-test.sh @@ -213,6 +213,50 @@ grep -Fxq 'planted' "$symlink_root/attacker/policies/managed/evil.json" || BROWSER_POLICY_PARENT_DIRS=("${saved_parent_dirs[@]}") pass "policy setup does not follow a planted parent symlink" +leaf_link_root=$test_tmp/leaf-link +mkdir -p "$leaf_link_root/etc/chromium/policies" "$leaf_link_root/attacker" +printf 'planted\n' >"$leaf_link_root/attacker/evil.json" +chmod 755 "$leaf_link_root/etc/chromium" "$leaf_link_root/etc/chromium/policies" +ln -s "$leaf_link_root/attacker" "$leaf_link_root/etc/chromium/policies/managed" +BROWSER_POLICY_PARENT_DIRS=( + "$leaf_link_root/etc/chromium" + "$leaf_link_root/etc/chromium/policies" +) +as_root() { unprivileged_as_root "$@"; } +if browser_policy_dir_hardened "$leaf_link_root/etc/chromium/policies/managed"; then + fail "a planted managed symlink is not treated as hardened" +fi +browser_policy_setup_dir "$leaf_link_root/etc/chromium/policies/managed" +[[ ! -L $leaf_link_root/etc/chromium/policies/managed ]] || + fail "setup replaces a planted managed symlink" +[[ -d $leaf_link_root/etc/chromium/policies/managed && ! -L $leaf_link_root/etc/chromium/policies/managed ]] || + fail "setup recreates managed as a real directory" +[[ ! -e $leaf_link_root/etc/chromium/policies/managed/evil.json ]] || + fail "setup does not keep policy that lived behind a planted managed symlink" +grep -Fxq 'planted' "$leaf_link_root/attacker/evil.json" || + fail "replacing a managed symlink does not delete the symlink target" +BROWSER_POLICY_PARENT_DIRS=("${saved_parent_dirs[@]}") +pass "policy setup does not follow a planted managed symlink" + +fx_link_root=$test_tmp/fx-link +mkdir -p "$fx_link_root/attacker" "$fx_link_root/opt" +printf 'planted\n' >"$fx_link_root/attacker/policies.json" +ln -s "$fx_link_root/attacker" "$fx_link_root/opt/zen" +as_root() { unprivileged_as_root "$@"; } +if browser_policy_firefox_hardened "$fx_link_root/opt/zen"; then + fail "a planted Firefox distribution symlink is not treated as hardened" +fi +browser_policy_setup_firefox_distribution "$fx_link_root/opt/zen" || + fail "Firefox setup replaces a planted distribution symlink" +[[ ! -L $fx_link_root/opt/zen ]] || fail "Firefox setup unlinks a planted distribution symlink" +[[ -d $fx_link_root/opt/zen && ! -L $fx_link_root/opt/zen ]] || + fail "Firefox setup recreates the distribution directory" +[[ -f $fx_link_root/opt/zen/policies.json && ! -L $fx_link_root/opt/zen/policies.json ]] || + fail "Firefox setup writes policies.json into the recreated directory" +grep -Fxq 'planted' "$fx_link_root/attacker/policies.json" || + fail "replacing a Firefox distribution symlink does not delete the symlink target" +pass "Firefox setup does not follow a planted distribution symlink" + [[ $(browser_policy_theme_hex "242,240,229") == "#f2f0e5" ]] || fail "theme colour converts an RGB triple to hex" [[ $(browser_policy_theme_hex $'14,31,41\n') == "#0e1f29" ]] || From bafc9a1000b503856e6fd642b87791cc3ddae5cf Mon Sep 17 00:00:00 2001 From: acrogenesis Date: Tue, 25 Aug 2026 13:01:01 -0600 Subject: [PATCH 6/6] Write browser theme colour through a passwordless helper Managed policy dirs are enterprise trust roots, so they stay 0755 root:root. The menu path takes root for that one write through a sudoers glob of six hex digits, the same shape as omarchy-dns, and falls back to pkexec where the grant is not installed. Drop omarchy-browser-policy; a group member could plant any JSON, not just a colour. --- bin/omarchy-install-browser | 2 - bin/omarchy-provision-owner | 5 +- bin/omarchy-theme-set-browser | 13 +- bin/omarchy-theme-set-browser-policy | 118 +++++++++++++ bin/omarchy-upgrade-to-quattro | 5 +- etc/sudoers.d/omarchy-theme-browser | 8 + install/config/browser-policy.sh | 1 - install/helpers/browser-policy.sh | 117 ++----------- migrations/1787515927.sh | 11 +- test/shell.d/browser-policy-dir-test.sh | 132 +++----------- test/shell.d/browser-policy-sudoers-test.sh | 184 ++++++++++++++++++++ test/shell.d/default-apps-test.sh | 11 +- test/shell.d/provisioning-groups-test.sh | 19 +- test/shell.d/upgrade-to-quattro-test.sh | 11 +- 14 files changed, 383 insertions(+), 254 deletions(-) create mode 100755 bin/omarchy-theme-set-browser-policy create mode 100644 etc/sudoers.d/omarchy-theme-browser create mode 100755 test/shell.d/browser-policy-sudoers-test.sh diff --git a/bin/omarchy-install-browser b/bin/omarchy-install-browser index d808c805..4593bc72 100755 --- a/bin/omarchy-install-browser +++ b/bin/omarchy-install-browser @@ -9,8 +9,6 @@ set -e source "$OMARCHY_PATH/install/helpers/browser-policy.sh" setup_chromium_policy_directory() { - browser_policy_setup_group - browser_policy_grant_user "${USER:-$(id -un)}" browser_policy_setup_dir "$1" } diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index 7c208eb9..0db99f2e 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -744,11 +744,8 @@ create_user() { chmod 440 /etc/sudoers.d/00-omarchy-wheel source "$OMARCHY_PATH/install/helpers/browser-policy.sh" - OMARCHY_INSTALL_USER=$username - OMARCHY_PROVISIONING_DIR=$PROVISIONING_DIR - browser_policy_setup_group for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do - [[ -d $dir ]] || continue + [[ -d $dir || -L $dir ]] || continue browser_policy_setup_dir "$dir" done } diff --git a/bin/omarchy-theme-set-browser b/bin/omarchy-theme-set-browser index 60fa3211..612e586f 100755 --- a/bin/omarchy-theme-set-browser +++ b/bin/omarchy-theme-set-browser @@ -12,10 +12,6 @@ if [[ -f $CHROMIUM_THEME ]]; then THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)") fi -set_browser_policy() { - browser_policy_write_color "$1" "$THEME_HEX_COLOR" -} - refresh_running_browser() { local process="$1" local command="$2" @@ -27,16 +23,11 @@ refresh_running_browser() { } failed=0 -set_browser_policy /etc/chromium/policies/managed || failed=1 +omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1 + refresh_running_browser chromium chromium - -set_browser_policy /etc/opt/chrome/policies/managed || failed=1 refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome - -set_browser_policy /etc/opt/edge/policies/managed || failed=1 refresh_running_browser msedge microsoft-edge-stable - -set_browser_policy /etc/brave/policies/managed || failed=1 refresh_running_browser brave brave # Match on the binary path: the running process is named plain "brave", and a # bare -f brave-origin pattern would also match the installer's own terminal. diff --git a/bin/omarchy-theme-set-browser-policy b/bin/omarchy-theme-set-browser-policy new file mode 100755 index 00000000..6f628f9b --- /dev/null +++ b/bin/omarchy-theme-set-browser-policy @@ -0,0 +1,118 @@ +#!/bin/bash + +# omarchy:summary=Write the current theme color into the browser policy directories +# omarchy:args= +# omarchy:hidden=true + +set -euo pipefail + +# Whenever this runs as root — invoked directly through the passwordless +# sudoers rule, or re-execed by require_root below — sudo's secure_path decides +# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path) +# prepends a user-writable checkout bin/ to it. Every helper this script calls +# by bare name (printf's builtin aside: install, mktemp, rm) is a system tool, +# never an omarchy-* command, so pin PATH to trusted system directories and keep +# root from resolving one out of that checkout. The unprivileged wrapper phase +# keeps the caller's PATH so it can still find sudo/pkexec. +if (( EUID == 0 )); then + export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin +fi + +# Enterprise policy trust roots. The list is fixed here rather than taken from +# the caller: the caller chooses a color, never a path. +POLICY_DIRS=( + /etc/chromium/policies/managed + /etc/opt/chrome/policies/managed + /etc/opt/edge/policies/managed + /etc/brave/policies/managed +) + +# The path etc/sudoers.d/omarchy-theme-browser names. The privileged half always +# runs from there rather than from whichever copy was invoked, so the rule +# matches even where $OMARCHY_PATH points at a checkout. +PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy + +usage() { + echo "Usage: omarchy-theme-set-browser-policy " >&2 +} + +if (( $# != 1 )); then + usage + exit 1 +fi + +color="$1" + +# Six lowercase hex digits is the whole of what this accepts. The leading "#" +# is added when the JSON is written rather than passed in: "#" opens a comment +# in sudoers, and keeping it out of argv lets the sudoers rule spell the +# argument as a plain six-character glob. +if [[ ! $color =~ ^[0-9a-f]{6}$ ]]; then + echo "omarchy-theme-set-browser-policy: expected six lowercase hex digits, got '$color'" >&2 + exit 1 +fi + +# True when sudo would run this exact command without stopping for a password. +# `sudo -l` on its own reports whether a command is permitted, which the blanket +# %wheel rule answers yes to for everything; the long listing prints the matched +# entry's tags, so !authenticate is the grant in +# etc/sudoers.d/omarchy-theme-browser and nothing else. Listing runs nothing +# and, under -n, prompts for nothing. +sudo_grants_passwordless() { + sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate' +} + +require_root() { + if (( EUID == 0 )); then + return + elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then + exec sudo "$PACKAGED_PATH" "$@" + else + exec pkexec "$PACKAGED_PATH" "$@" + fi +} + +require_root "$color" + +failed=0 +staged="" +cleanup() { + [[ -n $staged ]] && rm -f "$staged" +} +trap cleanup EXIT + +for policy_dir in "${POLICY_DIRS[@]}"; do + # Only browsers Omarchy has installed have a policy directory. Creating one + # here would hand a browser a managed-policy root it does not otherwise have. + [[ -d $policy_dir && ! -L $policy_dir ]] || continue + + dest=$policy_dir/color.json + staged=$(mktemp) || { + failed=1 + continue + } + printf '{"BrowserThemeColor": "#%s", "BrowserColorScheme": "device"}\n' "$color" >"$staged" + + if [[ -L $dest || -d $dest ]]; then + if ! rm -rf -- "$dest"; then + rm -f "$staged" + staged="" + echo "omarchy-theme-set-browser-policy: cannot replace $dest" >&2 + failed=1 + continue + fi + fi + + if ! install -m 0644 -o root -g root -T "$staged" "$dest"; then + rm -f "$staged" + staged="" + echo "omarchy-theme-set-browser-policy: cannot write $dest" >&2 + failed=1 + continue + fi + + rm -f "$staged" + staged="" +done + +exit "$failed" diff --git a/bin/omarchy-upgrade-to-quattro b/bin/omarchy-upgrade-to-quattro index 62ef21b9..32ea6465 100755 --- a/bin/omarchy-upgrade-to-quattro +++ b/bin/omarchy-upgrade-to-quattro @@ -1316,14 +1316,13 @@ apply_system_transition() { if ! as_root test -f "$browser_policy_helper"; then warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened." else - as_root env OMARCHY_PATH=/usr/share/omarchy OMARCHY_INSTALL_USER="$target_user" \ + as_root env OMARCHY_PATH=/usr/share/omarchy \ bash -euo pipefail -c ' source "$OMARCHY_PATH/install/helpers/browser-policy.sh" - browser_policy_setup_group browser_policy_setup_dir /etc/chromium/policies/managed for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do [[ $dir == "/etc/chromium/policies/managed" ]] && continue - [[ -d $dir ]] || continue + [[ -d $dir || -L $dir ]] || continue browser_policy_setup_dir "$dir" done ' diff --git a/etc/sudoers.d/omarchy-theme-browser b/etc/sudoers.d/omarchy-theme-browser new file mode 100644 index 00000000..853d2412 --- /dev/null +++ b/etc/sudoers.d/omarchy-theme-browser @@ -0,0 +1,8 @@ +# Theme switching is a menu action with no terminal to carry a password prompt, +# and it repaints the browser accent on every switch, so this one write must not +# stop for a password. The argument is spelled out as six hex digits rather than +# a wildcard: the grant covers a color and nothing else, and sudoers matches a +# command's arguments exactly, so it cannot be stretched into extra ones. The +# helper revalidates the same shape, since the terminal path does not come +# through this rule. +%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-theme-set-browser-policy [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] diff --git a/install/config/browser-policy.sh b/install/config/browser-policy.sh index a02de2f3..fd802c66 100644 --- a/install/config/browser-policy.sh +++ b/install/config/browser-policy.sh @@ -1,3 +1,2 @@ source "$OMARCHY_PATH/install/helpers/browser-policy.sh" -browser_policy_setup_group browser_policy_setup_dir /etc/chromium/policies/managed diff --git a/install/helpers/browser-policy.sh b/install/helpers/browser-policy.sh index 687edce2..c2c93d8e 100644 --- a/install/helpers/browser-policy.sh +++ b/install/helpers/browser-policy.sh @@ -1,11 +1,9 @@ -# Chromium-family machine policy is mandatory for every profile. A dedicated -# group at 2775 lets every Omarchy user write color.json and every other uid -# read; other-write stays off. Setgid so new files inherit the group. +# Chromium-family machine policy is mandatory for every profile. Directories +# stay 0755 root:root; omarchy-theme-set-browser-policy is the privileged +# write for color.json. source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/as-root.sh" -BROWSER_POLICY_GROUP=omarchy-browser-policy - BROWSER_POLICY_MANAGED_DIRS=( /etc/chromium/policies/managed /etc/opt/chrome/policies/managed @@ -33,47 +31,12 @@ BROWSER_POLICY_FIREFOX_DIRS=( BROWSER_POLICY_DEFAULT_COLOR="#1c2027" -browser_policy_setup_group() { - local provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}" - - as_root groupadd --system --force "$BROWSER_POLICY_GROUP" - as_root mkdir -p "$provisioning_dir" - if ! grep -qxF "$BROWSER_POLICY_GROUP" "$provisioning_dir/groups" 2>/dev/null; then - printf '%s\n' "$BROWSER_POLICY_GROUP" | as_root tee -a "$provisioning_dir/groups" >/dev/null - fi - - if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then - as_root usermod -aG "$BROWSER_POLICY_GROUP" "$OMARCHY_INSTALL_USER" - fi -} - -browser_policy_grant_user() { - local user=${1:-} - - if [[ -z $user || $user == "root" ]]; then - user=${SUDO_USER:-} - fi - - [[ -n $user && $user != "root" ]] || return 0 - getent passwd "$user" >/dev/null || return 0 - as_root usermod -aG "$BROWSER_POLICY_GROUP" "$user" -} - browser_policy_purge_dir() { local dir=$1 as_root find "$dir" -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} + } -browser_policy_dir_hardened() { - local dir=$1 - - [[ -d $dir && ! -L $dir ]] || return 1 - [[ $(stat -c '%a' "$dir") == "2775" ]] || return 1 - [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 - [[ $(stat -c '%G' "$dir") == $BROWSER_POLICY_GROUP ]] || return 1 -} - browser_policy_parent_hardened() { local dir=$1 @@ -82,6 +45,10 @@ browser_policy_parent_hardened() { [[ $(stat -c '%U' "$dir") == "root" ]] || return 1 } +browser_policy_dir_hardened() { + browser_policy_parent_hardened "$1" +} + browser_policy_parents_hardened() { local dir=$1 local parent @@ -116,10 +83,7 @@ browser_policy_setup_dir() { local dir=$1 browser_policy_setup_parents_for "$dir" - if [[ -L $dir || ( -e $dir && ! -d $dir ) ]]; then - as_root rm -rf -- "$dir" - fi - as_root install -d -m 2775 -o root -g "$BROWSER_POLICY_GROUP" "$dir" + browser_policy_setup_parent "$dir" browser_policy_purge_dir "$dir" } @@ -136,82 +100,31 @@ browser_policy_theme_hex() { printf '%s' "$BROWSER_POLICY_DEFAULT_COLOR" } -browser_policy_file_owner() { - local user - - if [[ -n ${OMARCHY_INSTALL_USER:-} && $OMARCHY_INSTALL_USER != "root" ]]; then - printf '%s\n' "$OMARCHY_INSTALL_USER" - return - fi - if [[ -n ${SUDO_USER:-} && $SUDO_USER != "root" ]]; then - printf '%s\n' "$SUDO_USER" - return - fi - if [[ -n ${PKEXEC_UID:-} ]]; then - user=$(getent passwd "$PKEXEC_UID" | cut -d: -f1) - if [[ -n $user && $user != "root" ]]; then - printf '%s\n' "$user" - return - fi - fi - user=${USER:-$(id -un)} - if [[ $user != "root" ]]; then - printf '%s\n' "$user" - fi -} - -# sudo when this process has a controlling terminal (fd 0 is /dev/null under -# `bash -lc cmd &`, but /dev/tty still works). pkexec when it does not. -browser_policy_elevate() { - if (( EUID == 0 )); then - "$@" - elif { exec 3/dev/null; then - exec 3<&- - sudo "$@" - else - pkexec "$@" - fi -} - -browser_policy_write_color() { +browser_policy_install_color() { local policy_dir=$1 local hex=$2 local dest=$policy_dir/color.json - local payload local tmp - local owner - [[ -d $policy_dir ]] || return 0 + [[ -d $policy_dir && ! -L $policy_dir ]] || return 0 + [[ $hex =~ ^#[0-9a-f]{6}$ ]] || return 1 - payload=$(printf '{"BrowserThemeColor": "%s", "BrowserColorScheme": "device"}\n' "$hex") tmp=$(mktemp) || return 1 - printf '%s' "$payload" >"$tmp" + printf '{"BrowserThemeColor": "%s", "BrowserColorScheme": "device"}\n' "$hex" >"$tmp" - # A planted symlink or directory must not be written through or into. if [[ -L $dest || -d $dest ]]; then if ! rm -rf -- "$dest" 2>/dev/null; then - if ! browser_policy_elevate rm -rf -- "$dest"; then - rm -f "$tmp" - echo "omarchy-theme-set-browser: cannot replace $dest (need group $BROWSER_POLICY_GROUP)" >&2 - return 1 - fi + rm -f "$tmp" + return 1 fi fi - if install -m 664 -T "$tmp" "$dest" 2>/dev/null; then - rm -f "$tmp" - return 0 - fi - - owner=$(browser_policy_file_owner) - [[ -n $owner ]] || owner=root - if browser_policy_elevate install -m 664 -o "$owner" -g "$BROWSER_POLICY_GROUP" -T "$tmp" "$dest"; then + if install -m 0644 -T "$tmp" "$dest" 2>/dev/null; then rm -f "$tmp" return 0 fi rm -f "$tmp" - echo "omarchy-theme-set-browser: cannot write $dest (need group $BROWSER_POLICY_GROUP)" >&2 return 1 } diff --git a/migrations/1787515927.sh b/migrations/1787515927.sh index 113daf34..f1f33869 100644 --- a/migrations/1787515927.sh +++ b/migrations/1787515927.sh @@ -2,15 +2,9 @@ echo "Stop world-writable Chromium and Firefox policy directories" source "$OMARCHY_PATH/install/helpers/browser-policy.sh" -browser_policy_setup_group -browser_policy_grant_user "${USER:-$(id -un)}" - repaired=0 for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do [[ -d $dir || -L $dir ]] || continue - if browser_policy_dir_hardened "$dir" && browser_policy_parents_hardened "$dir"; then - continue - fi browser_policy_setup_dir "$dir" repaired=1 done @@ -21,7 +15,10 @@ fi for dir in "${BROWSER_POLICY_FIREFOX_DIRS[@]}"; do [[ -d $dir || -L $dir ]] || continue - browser_policy_firefox_hardened "$dir" && continue + if browser_policy_firefox_hardened "$dir"; then + browser_policy_purge_dir "$dir" + continue + fi browser_policy_setup_parent "$dir" browser_policy_purge_dir "$dir" if ! browser_policy_firefox_policy_file_ok "$dir/policies.json"; then diff --git a/test/shell.d/browser-policy-dir-test.sh b/test/shell.d/browser-policy-dir-test.sh index b06f174f..20b8067c 100755 --- a/test/shell.d/browser-policy-dir-test.sh +++ b/test/shell.d/browser-policy-dir-test.sh @@ -7,24 +7,6 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" test_tmp=$(mktemp -d) trap 'rm -rf "$test_tmp"' EXIT -mock_bin=$test_tmp/bin -mkdir -p "$mock_bin" -elev_log=$test_tmp/elev.log -cat >"$mock_bin/sudo" <>"$elev_log" -[[ \${OMARCHY_TEST_SUDO_FAIL:-} == 1 ]] && exit 1 -exit 0 -SH -cat >"$mock_bin/pkexec" <>"$elev_log" -[[ \${OMARCHY_TEST_SUDO_FAIL:-} == 1 ]] && exit 1 -exit 0 -SH -chmod +x "$mock_bin/sudo" "$mock_bin/pkexec" -export PATH="$mock_bin:$PATH" -: >"$elev_log" export OMARCHY_PATH="$ROOT" export OMARCHY_PROVISIONING_DIR="$test_tmp/provisioning" @@ -55,20 +37,20 @@ unprivileged_as_root() { write_dir=$test_tmp/writable mkdir -p "$write_dir" -browser_policy_write_color "$write_dir" "#aabbcc" || +browser_policy_install_color "$write_dir" "#aabbcc" || fail "theme colour writes into a writable policy directory" grep -F '"BrowserThemeColor": "#aabbcc"' "$write_dir/color.json" >/dev/null || fail "theme colour writes BrowserThemeColor" mode=$(stat -c '%a' "$write_dir/color.json") -[[ $mode == "664" ]] || fail "theme colour creates a group-writable policy file" "mode=$mode" -pass "theme colour writes a group-writable color.json" +[[ $mode == "644" ]] || fail "theme colour creates a root-mode policy file" "mode=$mode" +pass "theme colour writes a 0644 color.json" if (( EUID == 0 )); then pass "running as root; skipping the mktemp-failure check" else chmod u+w "$write_dir" export TMPDIR=$test_tmp/missing-tmp - if browser_policy_write_color "$write_dir" "#dead00" 2>/dev/null; then + if browser_policy_install_color "$write_dir" "#dead00" 2>/dev/null; then fail "theme colour fails when mktemp cannot create a file" fi unset TMPDIR @@ -80,7 +62,7 @@ fi printf 'original\n' >"$test_tmp/pwn" rm -f "$write_dir/color.json" ln -s "$test_tmp/pwn" "$write_dir/color.json" -browser_policy_write_color "$write_dir" "#aabbcc" || +browser_policy_install_color "$write_dir" "#aabbcc" || fail "theme colour replaces a planted color.json symlink" [[ -f $write_dir/color.json && ! -L $write_dir/color.json ]] || fail "theme colour unlinks a planted color.json symlink instead of writing through it" @@ -90,62 +72,25 @@ pass "theme colour does not follow a planted color.json symlink" plant_write=$test_tmp/plant-dir mkdir -p "$plant_write/color.json/nested" printf 'inside\n' >"$plant_write/color.json/nested/x" -browser_policy_write_color "$plant_write" "#aabbcc" || +browser_policy_install_color "$plant_write" "#aabbcc" || fail "theme colour replaces a planted color.json directory" [[ -f $plant_write/color.json && ! -d $plant_write/color.json ]] || fail "theme colour does not write into a planted color.json directory" pass "theme colour does not write into a planted color.json directory" missing_dir=$test_tmp/missing -browser_policy_write_color "$missing_dir" "#aabbcc" || +browser_policy_install_color "$missing_dir" "#aabbcc" || fail "theme colour skips a policy directory that does not exist" [[ ! -e $missing_dir ]] || fail "theme colour does not create a missing policy directory" pass "theme colour skips a missing policy directory" -if (( EUID == 0 )); then - pass "running as root; skipping elevation checks" -else - denied_dir=$test_tmp/denied - mkdir -p "$denied_dir" - chmod a-w "$denied_dir" - owner=${USER:-$(id -un)} - : >"$elev_log" - browser_policy_write_color "$denied_dir" "#aabbcc" || - fail "elevated install reports success from pkexec" - grep -E "^PKEXEC install -m 664 -o $owner -g omarchy-browser-policy -T .+ $denied_dir/color.json$" "$elev_log" >/dev/null || - fail "without a controlling tty, colour write elevates through pkexec as the owner" "$(cat "$elev_log")" - if grep -E '^SUDO ' "$elev_log" >/dev/null; then - fail "without a controlling tty, colour write does not call sudo" "$(cat "$elev_log")" - fi - pass "without a controlling tty, colour write elevates through pkexec" - - : >"$elev_log" - export OMARCHY_TEST_SUDO_FAIL=1 - if browser_policy_write_color "$denied_dir" "#aabbcc" 2>"$test_tmp/write.err"; then - fail "theme colour fails when the policy directory is not writable" - fi - unset OMARCHY_TEST_SUDO_FAIL - grep -F 'omarchy-browser-policy' "$test_tmp/write.err" >/dev/null || - fail "theme colour names the group when the write is denied" - pass "theme colour reports a denied policy write" - - if command -v script >/dev/null; then - : >"$elev_log" - cat >"$test_tmp/tty-write.sh" </dev/null - grep -E "^SUDO install -m 664 -o $owner -g omarchy-browser-policy -T .+ $denied_dir/color.json$" "$elev_log" >/dev/null || - fail "with a controlling tty, colour write elevates through sudo" "$(cat "$elev_log")" - if grep -E '^PKEXEC ' "$elev_log" >/dev/null; then - fail "with a controlling tty, colour write does not call pkexec" "$(cat "$elev_log")" - fi - pass "with a controlling tty, colour write elevates through sudo" - else - pass "script(1) unavailable; skipping the controlling-tty elevation check" - fi +if browser_policy_install_color "$write_dir" "aabbcc" 2>/dev/null; then + fail "theme colour rejects hex without a leading #" fi +if browser_policy_install_color "$write_dir" "#AABBCC" 2>/dev/null; then + fail "theme colour rejects uppercase hex" +fi +pass "theme colour accepts only # plus six lowercase hex digits" planted_dir=$test_tmp/planted mkdir -p "$planted_dir/evil" @@ -156,16 +101,16 @@ browser_policy_setup_dir "$planted_dir" [[ ! -e $planted_dir/evil ]] || fail "policy setup drops a non-empty non-root subdirectory" [[ ! -e $planted_dir/color.json ]] || fail "policy setup drops a non-root color.json" [[ -d $planted_dir ]] || fail "policy setup leaves the managed directory in place" +mode=$(stat -c '%a' "$planted_dir") +[[ $mode == "755" ]] || fail "policy setup leaves the managed directory 0755" "mode=$mode" pass "policy setup drops non-root files and non-empty subdirectories" owned=$test_tmp/not-root mkdir -p "$owned" -chmod 2775 "$owned" -BROWSER_POLICY_GROUP=$(id -gn) +chmod 755 "$owned" if browser_policy_dir_hardened "$owned"; then - fail "a user-owned 2775 directory is not treated as hardened" + fail "a user-owned 0755 directory is not treated as hardened" fi -BROWSER_POLICY_GROUP=omarchy-browser-policy pass "a hardened directory must be root-owned" saved_parent_dirs=("${BROWSER_POLICY_PARENT_DIRS[@]}") @@ -173,7 +118,7 @@ parent_root=$test_tmp/parents mkdir -p "$parent_root/etc/chromium/policies/managed/keep" printf 'keep\n' >"$parent_root/etc/chromium/policies/managed/keep/x" chmod 0777 "$parent_root/etc/chromium" "$parent_root/etc/chromium/policies" -chmod 2775 "$parent_root/etc/chromium/policies/managed" +chmod 755 "$parent_root/etc/chromium/policies/managed" BROWSER_POLICY_PARENT_DIRS=( "$parent_root/etc/chromium" "$parent_root/etc/chromium/policies" @@ -270,7 +215,7 @@ pass "Firefox setup does not follow a planted distribution symlink" for malformed in "" "not,a,color" "1,2" "1,2,3,4" "256,0,0" "999,999,999" "-1,0,0" \ "1,2,3;id" '1,2,$(id)' "0x10,0,0" "1,2,3 4,5,6"; do [[ $(browser_policy_theme_hex "$malformed") == "#1c2027" ]] || - fail "theme colour falls back to the neutral grey for '$malformed'" + fail "theme colour falls back to the stock grey for '$malformed'" done pass "theme colour is six hex digits or the stock grey" @@ -288,6 +233,8 @@ pass "shipped chromium.theme files parse as RGB triples" grep -F 'browser_policy_theme_hex' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null || fail "omarchy-theme-set-browser parses chromium.theme through browser_policy_theme_hex" +grep -F 'omarchy-theme-set-browser-policy' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null || + fail "omarchy-theme-set-browser writes colour through omarchy-theme-set-browser-policy" if grep -E 'printf.*THEME_RGB_COLOR' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null; then fail "omarchy-theme-set-browser does not hand unvetted theme words to printf" fi @@ -328,27 +275,6 @@ fi [[ -d $dir_dist/policies.json ]] || fail "Firefox policy install leaves a planted policies.json directory in place" pass "Firefox policy install does not write into a planted policies.json directory" -grant_log=$test_tmp/usermod.calls -as_root() { - if [[ $1 == "usermod" ]]; then - printf '%s\n' "$*" >>"$grant_log" - return 0 - fi - unprivileged_as_root "$@" -} -invoker=${USER:-$(id -un)} -: >"$grant_log" -SUDO_USER=$invoker -browser_policy_grant_user root -unset SUDO_USER -grep -qx -- "usermod -aG omarchy-browser-policy $invoker" "$grant_log" || - fail "granting as root uses SUDO_USER" "$(cat "$grant_log")" -: >"$grant_log" -OMARCHY_INSTALL_USER="" -browser_policy_grant_user "" -[[ ! -s $grant_log ]] || fail "an empty grant does not usermod" -pass "sudo install browser grants the invoking user, not root" - grep -F 'exit "$failed"' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null || fail "omarchy-theme-set-browser exits non-zero when a policy write fails" pass "omarchy-theme-set-browser exits non-zero when a policy write fails" @@ -357,25 +283,25 @@ policy_files=( "$ROOT/bin/omarchy-install-browser" "$ROOT/bin/omarchy-provision-owner" "$ROOT/bin/omarchy-theme-set-browser" + "$ROOT/bin/omarchy-theme-set-browser-policy" "$ROOT/bin/omarchy-upgrade-to-quattro" "$ROOT/install/config/theme-system.sh" "$ROOT/install/config/browser-policy.sh" "$ROOT/install/helpers/browser-policy.sh" "$ROOT/migrations/1787515927.sh" ) -if grep -nE 'chmod a\+rwx\b|chmod a\+rw\b|chmod a\+w\b|chmod o\+w|chmod ugo\+w|chmod 2777\b|chmod 0777\b|chmod 777\b|install -d -m 0?[27]?777' "${policy_files[@]}" >/dev/null; then - fail "browser policy setup is not world-writable" +if grep -nE 'chmod a\+rwx\b|chmod a\+rw\b|chmod a\+w\b|chmod o\+w|chmod ugo\+w|chmod 2775\b|chmod 2777\b|chmod 0777\b|chmod 777\b|install -d -m 0?[27]?777|omarchy-browser-policy' "${policy_files[@]}" >/dev/null; then + fail "browser policy setup is not world-writable and does not use omarchy-browser-policy" fi pass "browser policy setup is not world-writable" mapfile -t migrations < <(rg -l 'Stop world-writable Chromium and Firefox policy directories' "$ROOT/migrations") (( ${#migrations[@]} == 1 )) || fail "exactly one migration locks existing policy directories" "${migrations[*]}" -grep -F 'browser_policy_dir_hardened' "${migrations[0]}" >/dev/null || - fail "the policy-directory migration no-ops a machine already repaired" -grep -F 'browser_policy_parents_hardened' "${migrations[0]}" >/dev/null || - fail "the policy-directory migration repairs a world-writable parent of a hardened leaf" -grep -F 'browser_policy_grant_user' "${migrations[0]}" >/dev/null || - fail "the policy-directory migration still grants the current user the group" +grep -F 'browser_policy_setup_dir' "${migrations[0]}" >/dev/null || + fail "the policy-directory migration repairs managed directories" +if grep -F 'browser_policy_grant_user' "${migrations[0]}" >/dev/null; then + fail "the policy-directory migration does not grant a browser-policy group" +fi grep -F 'BROWSER_POLICY_FIREFOX_DIRS' "${migrations[0]}" >/dev/null || fail "the policy-directory migration covers Firefox and Zen" grep -F 'browser_policy_firefox_policy_file_ok' "${migrations[0]}" >/dev/null || diff --git a/test/shell.d/browser-policy-sudoers-test.sh b/test/shell.d/browser-policy-sudoers-test.sh new file mode 100755 index 00000000..2af8ea2b --- /dev/null +++ b/test/shell.d/browser-policy-sudoers-test.sh @@ -0,0 +1,184 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +helper="$ROOT/bin/omarchy-theme-set-browser-policy" +setter="$ROOT/bin/omarchy-theme-set-browser" +sudoers_file="$ROOT/etc/sudoers.d/omarchy-theme-browser" +rule='%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-theme-set-browser-policy [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' + +# Exactly one rule, matched whole. Dropping the argument -- which sudoers reads +# as "any arguments" -- or widening the glob to `*` would let the grant carry +# something other than a color while leaving this line looking right. +rules=$(grep -vE '^[[:space:]]*(#|$)' "$sudoers_file") +[[ $rules == "$rule" ]] || + fail "browser policy sudoers file carries exactly the six-hex-digit rule and nothing else" "got: $rules" + +if command -v visudo >/dev/null; then + visudo -cf "$sudoers_file" >/dev/null || fail "browser policy sudoers rule parses" +fi + +grep -Fx 'PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy' "$helper" >/dev/null || + fail "omarchy-theme-set-browser-policy elevates the path the sudoers rule names" + +grep -E 'sudo -n -l -l' "$helper" >/dev/null || + fail "omarchy-theme-set-browser-policy reads the grant from the long sudo listing" + +grep -Eq '^\s*export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin' "$helper" || + fail "omarchy-theme-set-browser-policy pins PATH to trusted system directories when it holds root" +gated=$(grep -A1 -E '^if \(\( EUID == 0 \)\); then$' "$helper" || true) +[[ $gated == *"export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin"* ]] || + fail "omarchy-theme-set-browser-policy gates the trusted-PATH pin on holding root" + +pass "browser policy sudoers rule is scoped to a single color argument" + +for dir in /etc/chromium/policies/managed /etc/opt/chrome/policies/managed \ + /etc/opt/edge/policies/managed /etc/brave/policies/managed; do + grep -Fx " $dir" "$helper" >/dev/null || + fail "omarchy-theme-set-browser-policy names $dir in its fixed policy directory list" +done + +policy_dir_count=$(sed -n '/^POLICY_DIRS=(/,/^)/p' "$helper" | grep -c '^ /') +((policy_dir_count == 4)) || + fail "omarchy-theme-set-browser-policy writes only the four known policy directories" \ + "got: $policy_dir_count" + +grep -F 'install -m 0644 -o root -g root -T' "$helper" >/dev/null || + fail "omarchy-theme-set-browser-policy installs color.json with install -T" +if grep -E 'mv -f' "$helper" >/dev/null; then + fail "omarchy-theme-set-browser-policy does not mv into a planted color.json directory" +fi + +pass "browser policy helper writes a fixed set of policy directories" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +mkdir -p "$stub_bin" + +cat >"$stub_bin/pkexec" <<'SH' +#!/bin/bash +printf 'pkexec %s\n' "$*" >"$ELEVATION_LOG" +SH +chmod +x "$stub_bin/pkexec" + +# STUB_GRANTED empty stands for an install whose omarchy-settings predates the +# sudoers file. The default is granted, matching a current Omarchy. +cat >"$stub_bin/sudo" <<'SH' +#!/bin/bash +if [[ $1 == -n && $2 == -l ]]; then + if [[ ${STUB_GRANTED-granted} == "granted" ]]; then + echo " Options: !authenticate" + else + echo " Matched: ${!#}" + fi + exit 0 +fi +printf 'sudo %s\n' "$*" >"$ELEVATION_LOG" +SH +chmod +x "$stub_bin/sudo" + +if ((EUID == 0)); then + pass "running as root; skipping the elevation checks, which would rewrite this machine's browser policy" +else + elevation_for() { + : >"$test_tmp/elevation" + ELEVATION_LOG="$test_tmp/elevation" \ + PATH="$stub_bin:$PATH" \ + bash "$helper" "$@" /dev/null 2>&1 || true + cat "$test_tmp/elevation" + } + + elevation=$(elevation_for 1c2027) + [[ $elevation == "sudo /usr/bin/omarchy-theme-set-browser-policy 1c2027" ]] || + fail "omarchy-theme-set-browser-policy takes the passwordless sudo grant without a terminal" \ + "got: $elevation" + + dev_linked=$(OMARCHY_PATH="$test_tmp/checkout" elevation_for 1c2027) + [[ $dev_linked == "sudo /usr/bin/omarchy-theme-set-browser-policy 1c2027" ]] || + fail "omarchy-theme-set-browser-policy elevates the system install wherever OMARCHY_PATH points" \ + "got: $dev_linked" + + pass "browser policy helper elevates a valid color through the sudo grant" + + ungranted=$(STUB_GRANTED="" elevation_for 1c2027) + [[ $ungranted == "pkexec /usr/bin/omarchy-theme-set-browser-policy 1c2027" ]] || + fail "omarchy-theme-set-browser-policy falls back to polkit where the grant does not reach" \ + "got: $ungranted" + + pass "browser policy helper falls back to polkit wherever the grant does not reach" + + for bad in "" "1C2027" "abc12" "abc1234" "1c202g" "../../etc/passwd" "1c2027 1c2027" \ + '$(id)' "1c2027;id" "#1c2027"; do + if PATH="$stub_bin:$PATH" ELEVATION_LOG="$test_tmp/elevation" \ + bash "$helper" "$bad" /dev/null 2>&1; then + fail "omarchy-theme-set-browser-policy rejects '$bad'" + fi + + rejected=$(elevation_for "$bad") + [[ -z $rejected ]] || + fail "omarchy-theme-set-browser-policy rejects '$bad' before elevating" "got: $rejected" + done + + if PATH="$stub_bin:$PATH" bash "$helper" 1c2027 ffffff /dev/null 2>&1; then + fail "omarchy-theme-set-browser-policy rejects more than one argument" + fi + + pass "browser policy helper accepts nothing but six lowercase hex digits" +fi + +setter_bin="$test_tmp/setter-bin" +mkdir -p "$setter_bin" + +cat >"$setter_bin/omarchy-theme-set-browser-policy" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >"$COLOR_LOG" +SH +chmod +x "$setter_bin/omarchy-theme-set-browser-policy" + +cat >"$setter_bin/omarchy-cmd-present" <<'SH' +#!/bin/bash +exit 1 +SH +chmod +x "$setter_bin/omarchy-cmd-present" + +setter_home="$test_tmp/home" +theme_dir="$setter_home/.local/state/omarchy/current/theme" +mkdir -p "$theme_dir" + +color_for_theme() { + : >"$test_tmp/color" + if [[ $# -gt 0 ]]; then + printf '%s' "$1" >"$theme_dir/chromium.theme" + else + rm -f "$theme_dir/chromium.theme" + fi + + HOME="$setter_home" COLOR_LOG="$test_tmp/color" PATH="$setter_bin:$stub_bin:$PATH" \ + OMARCHY_PATH="$ROOT" bash "$setter" /dev/null 2>&1 || true + cat "$test_tmp/color" +} + +[[ $(color_for_theme "242,240,229") == "f2f0e5" ]] || + fail "omarchy-theme-set-browser converts an RGB triple to six hex digits" +[[ $(color_for_theme $'14,31,41\n') == "0e1f29" ]] || + fail "omarchy-theme-set-browser accepts a trailing newline" +[[ $(color_for_theme "0,0,0") == "000000" ]] || + fail "omarchy-theme-set-browser pads single-digit components" +[[ $(color_for_theme " 12 , 11 , 12 ") == "0c0b0c" ]] || + fail "omarchy-theme-set-browser tolerates surrounding whitespace" + +for malformed in "" "not,a,color" "1,2" "1,2,3,4" "256,0,0" "999,999,999" "-1,0,0" \ + "1,2,3;id" '1,2,$(id)' "0x10,0,0" "1,2,3 4,5,6"; do + color=$(color_for_theme "$malformed") + [[ $color == "1c2027" ]] || + fail "omarchy-theme-set-browser falls back to the stock colour for '$malformed'" "got: $color" +done + +[[ $(color_for_theme) == "1c2027" ]] || + fail "omarchy-theme-set-browser falls back to the stock colour with no theme file" + +pass "browser theme color is derived as six hex digits or falls back to the stock grey" diff --git a/test/shell.d/default-apps-test.sh b/test/shell.d/default-apps-test.sh index c88c2253..8151c250 100755 --- a/test/shell.d/default-apps-test.sh +++ b/test/shell.d/default-apps-test.sh @@ -208,18 +208,17 @@ OMARCHY_TEST_REAL_BROWSER_INSTALL=true omarchy-default-browser --install chromiu [[ $(omarchy-default-browser) == "chromium" ]] || fail "Chromium becomes the default after its full installer succeeds" cmp -s "$ROOT/config/chromium-flags.conf" "$test_home/.config/chromium-flags.conf" || fail "Chromium browser installer copies the default flags" -grep -Fxq 'sudo:groupadd --system --force omarchy-browser-policy' "$setup_log" || - fail "Chromium browser installer creates the browser-policy group" grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium' "$setup_log" || fail "Chromium browser installer creates a root-owned Chromium policy parent" grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium/policies' "$setup_log" || fail "Chromium browser installer creates a root-owned Chromium policies parent" -grep -Fxq 'sudo:install -d -m 2775 -o root -g omarchy-browser-policy /etc/chromium/policies/managed' "$setup_log" || - fail "Chromium browser installer creates a group-writable managed policy directory" +grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium/policies/managed' "$setup_log" || + fail "Chromium browser installer creates a root-owned managed policy directory" grep -Fxq 'sudo:find /etc/chromium/policies/managed -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" || fail "Chromium browser installer drops non-root files from its policy directory" -grep -Fxq "sudo:usermod -aG omarchy-browser-policy ${USER:-$(id -un)}" "$setup_log" || - fail "Chromium browser installer grants the installing user the browser-policy group" +if grep -E 'groupadd|usermod|omarchy-browser-policy' "$setup_log" >/dev/null; then + fail "Chromium browser installer does not create a browser-policy group" "$(cat "$setup_log")" +fi grep -Fxq 'omarchy-install-chromium-copy-url:' "$setup_log" || fail "Chromium browser installer registers the Copy URL host" grep -Fxq 'omarchy-install-chromium-ytdlp:' "$setup_log" || diff --git a/test/shell.d/provisioning-groups-test.sh b/test/shell.d/provisioning-groups-test.sh index 7b5c6964..5a5fc516 100644 --- a/test/shell.d/provisioning-groups-test.sh +++ b/test/shell.d/provisioning-groups-test.sh @@ -55,12 +55,13 @@ OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh" [[ -f $OMARCHY_PROVISIONING_DIR/groups ]] || fail "groups file written without an install user" grep -qxF input "$OMARCHY_PROVISIONING_DIR/groups" || fail "input group recorded" -grep -qxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups" || fail "browser-policy group recorded" +! grep -qxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups" || + fail "browser-policy group must not be recorded" [[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called without an install user" -grep -F -- '--system --force omarchy-browser-policy' "$TMPDIR/groupadd.calls" >/dev/null || - fail "browser-policy group is created as a system group" -grep -F -- '-d -m 2775 -o root -g omarchy-browser-policy /etc/chromium/policies/managed' "$TMPDIR/install.calls" >/dev/null || - fail "browser-policy directory is created group-writable" +[[ ! -f $TMPDIR/groupadd.calls ]] || ! grep -F omarchy-browser-policy "$TMPDIR/groupadd.calls" >/dev/null || + fail "browser-policy group is not created" +grep -F -- '-d -m 0755 -o root -g root /etc/chromium/policies/managed' "$TMPDIR/install.calls" >/dev/null || + fail "browser-policy directory is created root-owned" pass "deferred provisioning records groups without calling usermod" # The docker group is root-equivalent and must never be granted automatically. @@ -77,8 +78,6 @@ pass "missing install user defers group grants" OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh" [[ $(grep -cxF input "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || fail "input group recorded once" OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh" -[[ $(grep -cxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || - fail "browser-policy group recorded once" pass "group recording is idempotent" # Existing user: usermod applies the recorded groups, and docker is never among them. @@ -86,7 +85,7 @@ OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/docker.sh" OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/hardware/input-group.sh" OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/browser-policy.sh" grep -qx -- "-aG input existing" "$TMPDIR/usermod.calls" || fail "usermod grants input to the install user" -grep -qx -- "-aG omarchy-browser-policy existing" "$TMPDIR/usermod.calls" || - fail "usermod grants browser-policy to the install user" +! grep -q -- "omarchy-browser-policy" "$TMPDIR/usermod.calls" || + fail "usermod must not grant browser-policy to the install user" ! grep -q -- "docker" "$TMPDIR/usermod.calls" || fail "usermod must not grant docker to the install user" -pass "existing install user gets input and browser-policy but never docker" +pass "existing install user gets input but never docker or browser-policy" diff --git a/test/shell.d/upgrade-to-quattro-test.sh b/test/shell.d/upgrade-to-quattro-test.sh index c38c17f7..60e8abff 100644 --- a/test/shell.d/upgrade-to-quattro-test.sh +++ b/test/shell.d/upgrade-to-quattro-test.sh @@ -71,18 +71,19 @@ grep -F 'install/helpers/browser-policy.sh' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade uses the shared browser-policy helper" grep -F 'as_root test -f "$browser_policy_helper"' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade survives a packaged tree without the browser-policy helper" -grep -F 'browser_policy_setup_group' "$upgrade_to_quattro" >/dev/null || - fail "Omarchy 4 upgrade creates the browser-policy group" +if grep -F 'browser_policy_setup_group' "$upgrade_to_quattro" >/dev/null; then + fail "Omarchy 4 upgrade does not create a browser-policy group" +fi grep -F 'browser_policy_setup_dir /etc/chromium/policies/managed' "$upgrade_to_quattro" >/dev/null || - fail "Omarchy 4 upgrade creates a group-writable Chromium policy directory" + fail "Omarchy 4 upgrade creates a root-owned Chromium policy directory" grep -F 'BROWSER_POLICY_MANAGED_DIRS' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade hardens every Chromium-family policy directory" grep -F 'run_as_user_omarchy omarchy-theme-set-browser' "$upgrade_to_quattro" >/dev/null || fail "Omarchy 4 upgrade rewrites browser theme colour after a headless theme-set" -if grep -E 'install -d -m 0?[27]?777 /etc/.*/policies|chmod a\+rw' "$upgrade_to_quattro" >/dev/null; then +if grep -E 'install -d -m 0?[27]?777 /etc/.*/policies|chmod a\+rw|2775' "$upgrade_to_quattro" >/dev/null; then fail "Omarchy 4 upgrade does not create a world-writable Chromium policy directory" fi -pass "Omarchy 4 upgrade locks the Chromium policy directory to the browser-policy group" +pass "Omarchy 4 upgrade locks the Chromium policy directory to root" grep -F 'OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd.service' "$upgrade_to_quattro" >/dev/null