Add as-console-user command

- Support MDM/Munki/Jamf runs where the parent process is root but
  Homebrew must execute as the logged-in macOS console user.
- Keep the nested operation constrained to `HOMEBREW_BREW_FILE` so this
  helper cannot become a general-purpose root command launcher.
- Share `macos_user.sh` with installer scripts so no-user and package
  plist fallback behaviour cannot drift between install paths.
- Stage `macos_user.sh` for `pkgbuild` because `preinstall` needs the
  resolver before the Homebrew payload has been installed.
This commit is contained in:
Mike McQuaid
2026-05-20 19:35:06 +01:00
parent 6c8be83af8
commit af3538d54d
8 changed files with 350 additions and 19 deletions
+13 -1
View File
@@ -7,6 +7,7 @@ on:
- "**"
paths:
- .github/workflows/release.yml
- Library/Homebrew/utils/macos_user.sh
- package/**/*
workflow_dispatch:
inputs:
@@ -143,13 +144,24 @@ jobs:
- name: Open macOS keychain
run: security list-keychain -d user -s "${RUNNER_TEMP}/${TEMPORARY_KEYCHAIN_FILE}"
- name: Prepare Homebrew installer scripts
run: |
installer_scripts="${RUNNER_TEMP}/homebrew-installer-scripts"
rm -rvf "${installer_scripts}"
mkdir -vp "${installer_scripts}"
cp -vp brew/package/scripts/preinstall \
brew/package/scripts/postinstall \
brew/Library/Homebrew/utils/macos_user.sh \
"${installer_scripts}/"
echo "HOMEBREW_INSTALLER_SCRIPTS=${installer_scripts}" >> "${GITHUB_ENV}"
- name: Build Homebrew installer component package
env:
HOMEBREW_VERSION: ${{ steps.homebrew-version.outputs.version }}
# Note: `Library/Homebrew/test/support/fixtures/` contains unsigned
# binaries so it needs to be excluded from notarization.
run: pkgbuild --root brew
--scripts brew/package/scripts
--scripts "${HOMEBREW_INSTALLER_SCRIPTS}"
--identifier sh.brew.homebrew
--version "${HOMEBREW_VERSION}"
--install-location /opt/homebrew
+4
View File
@@ -239,6 +239,10 @@ check-run-command-as-root() {
[[ -f /run/.containerenv ]] && return
[[ -f /proc/1/cgroup ]] && grep -E "azpl_job|actions_job|docker|garden|kubepods" -q /proc/1/cgroup && return
# `brew as-console-user` is run by root-owned MDM/Munki/Jamf workflows so it
# can immediately dispatch the requested Homebrew command as the console user.
[[ "${HOMEBREW_COMMAND}" == "as-console-user" ]] && return
# `brew services` may need `sudo` for system-wide daemons.
if [[ "${HOMEBREW_COMMAND}" == "services" ]]
then
+28
View File
@@ -0,0 +1,28 @@
# typed: strict
# frozen_string_literal: true
require "abstract_command"
require "shell_command"
module Homebrew
module Cmd
class AsConsoleUser < AbstractCommand
include ShellCommand
cmd_args do
usage_banner <<~EOS
`as-console-user` <command> [<args> ...]
Run a Homebrew command as the active macOS console user.
This is intended for MDM, Munki and Jamf workflows where `brew` is
invoked as root but Homebrew operations should run as the logged-in
console user. The nested command is always dispatched through
`HOMEBREW_BREW_FILE`.
EOS
named_args min: 1
end
end
end
end
+44
View File
@@ -0,0 +1,44 @@
# Documentation defined in Library/Homebrew/cmd/as-console-user.rb
# `HOMEBREW_*` variables are set by brew.sh before sourcing this command.
# shellcheck disable=SC2154
homebrew-as-console-user() {
case "${1:-}" in
--help | -h | --usage | "-?")
"${HOMEBREW_BREW_FILE}" help as-console-user
return
;;
*) ;;
esac
if [[ "$#" -eq 0 ]]
then
"${HOMEBREW_BREW_FILE}" help as-console-user
return 1
fi
[[ -n "${HOMEBREW_MACOS}" ]] || odie "\`brew as-console-user\` is only supported on macOS."
# `HOMEBREW_LIBRARY` is set by brew.sh, so ShellCheck cannot follow it.
# shellcheck disable=SC1091
source "${HOMEBREW_LIBRARY}/Homebrew/utils/macos_user.sh"
local console_user
console_user="$(homebrew-console-user)" || odie "No supported macOS console user is logged in."
local console_home
console_home="$(homebrew-user-home "${console_user}")" ||
odie "Could not determine home directory for console user: ${console_user}"
(
cd "${console_home}" &>/dev/null || odie "Failed to cd to ${console_home}!"
sudo -H -u "${console_user}" /usr/bin/env -i \
"HOME=${console_home}" \
"USER=${console_user}" \
"LOGNAME=${console_user}" \
"PWD=${console_home}" \
"PATH=/usr/bin:/bin:/usr/sbin:/sbin" \
"${HOMEBREW_BREW_FILE}" "$@"
)
}
@@ -0,0 +1,178 @@
# typed: false
# frozen_string_literal: true
require "open3"
require "cmd/shared_examples/args_parse"
require "cmd/as-console-user"
RSpec.describe Homebrew::Cmd::AsConsoleUser do
let(:as_console_user_script) { HOMEBREW_LIBRARY_PATH/"cmd/as-console-user.sh" }
let(:repository_root) { HOMEBREW_LIBRARY_PATH.parent.parent }
let(:test_root) { mktmpdir }
let(:macos_user_script) { repository_root/"Library/Homebrew/utils/macos_user.sh" }
let(:macos_env) do
{
"HOMEBREW_BREW_FILE" => "brew",
"HOMEBREW_LIBRARY" => (repository_root/"Library").to_s,
"HOMEBREW_MACOS" => "1",
}
end
it_behaves_like "parseable arguments"
def run_as_console_user_shell(script, env = {})
Bundler.with_unbundled_env do
Open3.capture3(env, "/bin/bash", "-c", script)
end
end
it "prints help and fails when no command is provided" do
stdout, stderr, status = run_as_console_user_shell(
<<~SH,
source "#{as_console_user_script}"
brew() { printf '%s\\n' "$*" >&2; }
homebrew-as-console-user
SH
"HOMEBREW_BREW_FILE" => "brew",
)
expect(status.exitstatus).to eq 1
expect(stdout).to be_empty
expect(stderr).to eq("help as-console-user\n")
end
it "rejects a root console user" do
stdout, stderr, status = run_as_console_user_shell(
<<~SH,
source "#{as_console_user_script}"
odie() { echo "Error: $*" >&2; exit 1; }
stat() { printf 'root\\n'; }
homebrew-as-console-user install wget
SH
macos_env,
)
expect(status.exitstatus).to eq 1
expect(stdout).to be_empty
expect(stderr).to eq("Error: No supported macOS console user is logged in.\n")
end
it "rejects a loginwindow console user" do
stdout, stderr, status = run_as_console_user_shell(
<<~SH,
source "#{as_console_user_script}"
odie() { echo "Error: $*" >&2; exit 1; }
stat() { printf 'loginwindow\\n'; }
homebrew-as-console-user install wget
SH
macos_env,
)
expect(status.exitstatus).to eq 1
expect(stdout).to be_empty
expect(stderr).to eq("Error: No supported macOS console user is logged in.\n")
end
it "rejects non-macOS systems" do
stdout, stderr, status = run_as_console_user_shell(
<<~SH,
source "#{as_console_user_script}"
odie() { echo "Error: $*" >&2; exit 1; }
homebrew-as-console-user install wget
SH
"HOMEBREW_BREW_FILE" => "brew",
)
expect(status.exitstatus).to eq 1
expect(stdout).to be_empty
expect(stderr).to eq("Error: `brew as-console-user` is only supported on macOS.\n")
end
it "uses the package user plist before the console user" do
homebrew_pkg_user_plist = test_root/".homebrew_pkg_user.plist"
homebrew_pkg_user_plist.write "plist"
stdout, stderr, status = run_as_console_user_shell(
<<~SH,
source "#{macos_user_script}"
defaults() { printf 'munki\\n'; }
stat() { printf 'root\\n'; }
homebrew-package-user
SH
"HOMEBREW_PKG_USER_PLIST" => homebrew_pkg_user_plist.to_s,
)
expect(status.success?).to be true
expect(stdout).to eq("munki\n")
expect(stderr).to be_empty
end
it "falls back to the console user without a package user plist" do
stdout, stderr, status = run_as_console_user_shell <<~SH
source "#{macos_user_script}"
stat() { printf 'mike\\n'; }
homebrew-package-user
SH
expect(status.success?).to be true
expect(stdout).to eq("mike\n")
expect(stderr).to be_empty
end
it "rejects package user lookup without a package user or console user" do
stdout, stderr, status = run_as_console_user_shell <<~SH
source "#{macos_user_script}"
stat() { printf 'root\\n'; }
homebrew-package-user
SH
expect(status.exitstatus).to eq 1
expect(stdout).to be_empty
expect(stderr).to be_empty
end
it "dispatches the nested brew command as the console user" do
args_file = test_root/"sudo-args.txt"
console_home = test_root/"console-home"
console_home.mkpath
stdout, stderr, status = run_as_console_user_shell(
<<~SH,
source "#{as_console_user_script}"
odie() { echo "Error: $*" >&2; exit 1; }
stat() { printf 'mike\\n'; }
id() { printf 'mike:*:501:20::0:0:Mike:#{console_home}:/bin/zsh\\n'; }
sudo() {
printf 'cwd=%s\\n' "$PWD" > "#{args_file}"
printf '%s\\n' "$@" >> "#{args_file}"
return 42
}
homebrew-as-console-user upgrade git --minimum-version=2.50.1
SH
macos_env.merge("HOMEBREW_BREW_FILE" => "/opt/homebrew/bin/brew"),
)
expect(status.exitstatus).to eq 42
expect(stdout).to be_empty
expect(stderr).to be_empty
expect(args_file.read).to eq <<~EOS
cwd=#{console_home}
-H
-u
mike
/usr/bin/env
-i
HOME=#{console_home}
USER=mike
LOGNAME=mike
PWD=#{console_home}
PATH=/usr/bin:/bin:/usr/sbin:/sbin
/opt/homebrew/bin/brew
upgrade
git
--minimum-version=2.50.1
EOS
end
end
+46
View File
@@ -0,0 +1,46 @@
# Used by `brew as-console-user` and macOS installer package scripts.
# Keep this standalone: package scripts source it before Homebrew is installed.
# Print the active macOS console user, or fail for login-window/system users.
homebrew-console-user() {
local console_user
console_user="$(stat -f "%Su" /dev/console 2>/dev/null)" || return 1
case "${console_user}" in
"" | root | loginwindow | _mbsetupuser)
return 1
;;
*) ;;
esac
echo "${console_user}"
}
# Print a user's home directory from the local account database.
homebrew-user-home() {
local user_record
user_record="$(id -P "$1" 2>/dev/null)" || return 1
user_record="${user_record%:*}"
user_record="${user_record##*:}"
[[ -n "${user_record}" ]] || return 1
echo "${user_record}"
}
# Print the package install user, preferring MDM's plist override.
homebrew-package-user() {
local homebrew_pkg_user_plist="${HOMEBREW_PKG_USER_PLIST:-/var/tmp/.homebrew_pkg_user.plist}"
if [[ -f "${homebrew_pkg_user_plist}" ]]
then
local homebrew_pkg_user
if homebrew_pkg_user="$(defaults read "${homebrew_pkg_user_plist}" HOMEBREW_PKG_USER 2>/dev/null)" &&
[[ -n "${homebrew_pkg_user}" ]]
then
echo "${homebrew_pkg_user}"
return
fi
fi
# Fall back to the active console user when MDM has not specified one.
homebrew-console-user
}
+22 -8
View File
@@ -5,6 +5,18 @@
# $4 System root directory (unused)
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)"
if [[ -f "${script_dir}/macos_user.sh" ]]
then
# `pkgbuild` stages this helper beside the package scripts.
# shellcheck disable=SC1091
source "${script_dir}/macos_user.sh"
else
# This fallback keeps direct local execution from the repository working.
# shellcheck disable=SC1091
source "${script_dir}/../../Library/Homebrew/utils/macos_user.sh"
fi
# disable analytics while installing
export HOMEBREW_NO_ANALYTICS_THIS_RUN=1
export HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT=1
@@ -72,14 +84,12 @@ fi
mkdir -vp Caskroom Cellar Frameworks etc include lib opt sbin share var/homebrew/linked
# optionally define an install user at /var/tmp/.homebrew_pkg_user.plist
homebrew_pkg_user_plist="/var/tmp/.homebrew_pkg_user.plist"
if [[ -f "${homebrew_pkg_user_plist}" ]] && [[ -n $(defaults read "${homebrew_pkg_user_plist}" HOMEBREW_PKG_USER) ]]
then
homebrew_pkg_user=$(defaults read /var/tmp/.homebrew_pkg_user HOMEBREW_PKG_USER)
# otherwise, get valid logged in user
else
homebrew_pkg_user=$(echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }')
fi
homebrew_pkg_user="$(homebrew-package-user)" ||
{
echo "No valid user for Homebrew installation. Log in before install or specify an install user."
exit 1
}
# set permissions
chmod ug=rwx Caskroom Cellar Frameworks bin etc include lib opt sbin share var var/homebrew var/homebrew/linked
@@ -93,7 +103,11 @@ else
fi
# move API cache to ~/Library/Caches/Homebrew
user_home_dir=$(dscl . read /Users/"${homebrew_pkg_user}" NFSHomeDirectory | awk '{ print $2 }')
user_home_dir="$(homebrew-user-home "${homebrew_pkg_user}")" ||
{
echo "No home directory for Homebrew installation user: ${homebrew_pkg_user}"
exit 1
}
user_cache_dir="${user_home_dir}/Library/Caches/Homebrew"
user_api_cache_dir="${user_cache_dir}/api"
mkdir -vp "${user_api_cache_dir}"
+15 -10
View File
@@ -1,17 +1,22 @@
#!/bin/bash
set -euo pipefail
homebrew_pkg_user_plist="/var/tmp/.homebrew_pkg_user.plist"
if [[ -f "${homebrew_pkg_user_plist}" ]] && [[ -n $(defaults read "${homebrew_pkg_user_plist}" HOMEBREW_PKG_USER) ]]
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)"
if [[ -f "${script_dir}/macos_user.sh" ]]
then
# `pkgbuild` stages this helper beside the package scripts.
# shellcheck disable=SC1091
source "${script_dir}/macos_user.sh"
else
# This fallback keeps direct local execution from the repository working.
# shellcheck disable=SC1091
source "${script_dir}/../../Library/Homebrew/utils/macos_user.sh"
fi
if homebrew-package-user &>/dev/null
then
exit 0
fi
homebrew_pkg_user=$(echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }')
if [[ "${homebrew_pkg_user}" =~ _mbsetupuser|loginwindow|root ]] || [[ -z "${homebrew_pkg_user}" ]]
then
echo "No valid user for Homebrew installation. Log in before install or specify an install user."
exit 1
else
exit 0
fi
echo "No valid user for Homebrew installation. Log in before install or specify an install user."
exit 1