mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 14:23:48 +04:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user