From f9bcf5903a967bd37a4f6b8cbe52a5c14711a3b0 Mon Sep 17 00:00:00 2001 From: Brendon Smith Date: Sun, 5 Nov 2023 19:19:44 -0500 Subject: [PATCH] WIP: Enable non-admin and non-sudo `bootstrap.sh` usage --- .github/workflows/ci.yml | 13 +++ README.md | 11 ++- bootstrap.sh | 179 +++++++++++++++++++++++---------------- 3 files changed, 126 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e233f3..b25a7e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,12 +78,25 @@ jobs: run: | sudo rm -rf /usr/local/Caskroom /usr/local/Homebrew /usr/local/bin/brew \ /usr/local/.??* /Applications/Xcode.app /Library/Developer/CommandLineTools + - name: Create a non-admin user account + run: | + if ${{ runner.os == 'Linux' }}; then + sudo useradd -m non-admin-user + elif ${{ runner.os == 'macOS' }}; then + sudo sysadminctl -addUser non-admin-user + fi + - name: Run bootstrap.sh with a non-admin user without Homebrew installed + run: + bootstrap_script_url="https://raw.githubusercontent.com/$STRAP_GITHUB_USER/dotfiles/$STRAP_DOTFILES_BRANCH/bootstrap.sh" + sudo --preserve-env --user=non-admin-user /usr/bin/env bash -c "$(curl -fsSL $bootstrap_script_url)" - name: Run bootstrap.sh run: | bootstrap_script_url="https://raw.githubusercontent.com/$STRAP_GITHUB_USER/dotfiles/$STRAP_DOTFILES_BRANCH/bootstrap.sh" /usr/bin/env bash -c "$(curl -fsSL $bootstrap_script_url)" - name: Rerun bootstrap.sh to test idempotence run: bash "$HOME/.dotfiles/bootstrap.sh" + - name: Rerun bootstrap.sh with a non-admin user + run: sudo --preserve-env --user=non-admin-user /usr/bin/env bash "$HOME/.dotfiles/bootstrap.sh" - name: Check Homebrew configuration run: brew config - name: Check for potential problems with brew doctor diff --git a/README.md b/README.md index b1cd88a..2d5fd2b 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,16 @@ The following environment variables can be used to configure _bootstrap.sh_, and - `STRAP_DOTFILES_URL`: URL from which the dotfiles repo will be cloned. Defaults to `https://github.com/$STRAP_GITHUB_USER/dotfiles`, but any [Git-compatible URL](https://www.git-scm.com/docs/git-clone#_git_urls) can be used, so long as it is accessible at the time the script runs. - `STRAP_DOTFILES_BRANCH`: Git branch to check out after cloning dotfiles repo. Defaults to `main`. -_bootstrap.sh_ will set up macOS and Homebrew, run scripts in the _scripts/_ directory, and install Homebrew packages and casks from the _[Brewfile](Brewfile)_. +There are some additional variables for advanced usage. Consult the _[bootstrap.sh](bootstrap.sh)_ script to see all supported variables. -A Brewfile is a list of [Homebrew](https://brew.sh/) packages and casks (applications) that can be installed in a batch by [Homebrew Bundle](https://github.com/Homebrew/homebrew-bundle). The Brewfile can even be used to install Mac App Store apps with the `mas` CLI. Note that you must sign in to the App Store ahead of time for `mas` to work. +_bootstrap.sh_ will set up macOS and Homebrew, run scripts in the _scripts/_ directory, and install Homebrew packages and casks from the _[Brewfile](Brewfile)_. A Brewfile is a list of [Homebrew](https://brew.sh/) packages and casks (applications) that can be installed in a batch by [Homebrew Bundle](https://github.com/Homebrew/homebrew-bundle). The Brewfile can even be used to install Mac App Store apps with the `mas` CLI. Note that you must sign in to the App Store ahead of time for `mas` to work. + +The following list is a brief summary of permissions related to _bootstrap.sh_. + +- Initial setup of Homebrew itself does not require an admin user account, but does require `sudo`. See the [Homebrew installation docs](https://docs.brew.sh/Installation), [Homebrew/install#312](https://github.com/Homebrew/install/issues/312), and [Homebrew/install#315](https://github.com/Homebrew/install/pull/315/files). +- [After Homebrew setup, use of `sudo` with `brew` commands is discouraged](https://docs.brew.sh/FAQ#why-does-homebrew-say-sudo-is-bad). +- After Homebrew setup, commands such as `brew bundle install --global` should be run from the same user account used for setup. Attempts to run `brew` commands from another user account will result in errors, because directories that need to be updated are owned by the setup account. If access to the setup account is not routinely available, an alternative approach could be to change ownership of Homebrew directories to a group that includes the user account used for Homebrew setup as well as other users that need to run Homebrew commands. +- _bootstrap.sh_ can run with limited functionality on non-admin and non-`sudo` user accounts. A plausible use case could exist in which an admin runs `bootstrap.sh` to configure the system initially, then a non-admin runs `bootstrap.sh` to configure their own account. In this use case, the non-admin user should not need admin or `sudo` privileges, because all the pertinent setup (FileVault disk encryption, XCode developer tools, Homebrew, etc) is already complete. Users with more complex needs for multi-environment dotfiles management might consider a tool like [`chezmoi`](https://www.chezmoi.io/). diff --git a/bootstrap.sh b/bootstrap.sh index 659ad37..aba44be 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -27,6 +27,8 @@ Linux) esac [[ -z $HOMEBREW_PREFIX ]] && HOMEBREW_PREFIX="$DEFAULT_HOMEBREW_PREFIX" +STRAP_ADMIN=${STRAP_ADMIN:-0} +if groups | grep -qE "\b(admin)\b"; then STRAP_ADMIN=1; else STRAP_ADMIN=0; fi STRAP_CI=${STRAP_CI:=0} STRAP_DEBUG=${STRAP_DEBUG:-0} [[ $1 = "--debug" || -o xtrace ]] && STRAP_DEBUG=1 @@ -51,8 +53,10 @@ sudo_askpass() { cleanup() { set +e - sudo_askpass rm -rf "$CLT_PLACEHOLDER" "$SUDO_ASKPASS" "$SUDO_ASKPASS_DIR" - sudo --reset-timestamp + if [ -n "$SUDO_ASKPASS" ]; then + sudo_askpass rm -rf "$CLT_PLACEHOLDER" "$SUDO_ASKPASS" "$SUDO_ASKPASS_DIR" + sudo --reset-timestamp + fi if [ -z "$STRAP_SUCCESS" ]; then if [ -n "$STRAP_STEP" ]; then echo "!!! $STRAP_STEP FAILED" >&2 @@ -73,18 +77,41 @@ else Q="$STRAP_QUIET_FLAG" fi -# Prompt for sudo password and initialize (or reinitialize) sudo -sudo --reset-timestamp - clear_debug() { set +x } + reset_debug() { if [ "$STRAP_DEBUG" -gt 0 ]; then set -x fi } +abort() { + STRAP_STEP="" + echo "!!! $*" >&2 + exit 1 +} + +escape() { + printf '%s' "${1//\'/\'}" +} + +log_no_sudo() { + STRAP_STEP="$*" + echo "--> $*" +} + +logk() { + STRAP_STEP="" + echo "OK" +} + +logn_no_sudo() { + STRAP_STEP="$*" + printf -- "--> %s " "$*" +} + logskip() { STRAP_STEP="" echo "SKIPPED" @@ -97,7 +124,7 @@ sudo_init() { # Don't care about non-alphanumeric filenames when doing a specific match # shellcheck disable=SC2010,SC2086 if ls /usr/lib/pam | grep $Q "pam_tid.so"; then - logn "Configuring sudo authentication using TouchID:" + logn_no_sudo "Configuring sudo authentication using TouchID:" if [[ -f /etc/pam.d/sudo_local || -f /etc/pam.d/sudo_local.template ]]; then # New in macOS Sonoma, survives updates. PAM_FILE="/etc/pam.d/sudo_local" @@ -161,28 +188,17 @@ sudo_refresh() { reset_debug } -abort() { - STRAP_STEP="" - echo "!!! $*" >&2 - exit 1 -} log() { STRAP_STEP="$*" sudo_refresh echo "--> $*" } + logn() { STRAP_STEP="$*" sudo_refresh printf -- "--> %s " "$*" } -logk() { - STRAP_STEP="" - echo "OK" -} -escape() { - printf '%s' "${1//\'/\'}" -} # Given a list of scripts in the dotfiles repo, run the first one that exists run_dotfile_scripts() { @@ -191,7 +207,7 @@ run_dotfile_scripts() { cd ~/.dotfiles for i in "$@"; do if [ -f "$i" ] && [ -x "$i" ]; then - log "Running dotfiles $i:" + log_no_sudo "Running dotfiles $i:" if [ "$STRAP_DEBUG" -eq 0 ]; then "$i" 2>/dev/null else @@ -207,7 +223,7 @@ run_dotfile_scripts() { [ "$USER" = "root" ] && abort "Run bootstrap.sh as yourself, not root." # shellcheck disable=SC2086 -if [ "$MACOS" -gt 0 ]; then +if [ "$MACOS" -gt 0 ] && [ "$STRAP_ADMIN" -gt 0 ]; then [ "$STRAP_CI" -eq 0 ] && caffeinate -s -w $$ & groups | grep $Q -E "\b(admin)\b" || abort "Add $USER to admin." logn "Configuring security settings:" @@ -234,23 +250,21 @@ if [ "$MACOS" -gt 0 ]; then fi # Check for and enable full-disk encryption -logn "Checking full-disk encryption status:" -VAULT_MSG="FileVault is (On|Off, but will be enabled after the next restart)." -# shellcheck disable=SC2086 -if fdesetup status | grep $Q -E "$VAULT_MSG"; then - logk -elif [ "$MACOS" -eq 0 ] || [ "$STRAP_CI" -gt 0 ]; then - echo - logn "Skipping full-disk encryption." -elif [ "$STRAP_INTERACTIVE" -gt 0 ]; then - echo - log "Enabling full-disk encryption on next reboot:" - sudo_askpass fdesetup enable -user "$USER" | - tee ~/Desktop/"FileVault Recovery Key.txt" - logk -else - echo - abort "Run 'sudo fdesetup enable -user \"$USER\"' for full-disk encryption." +if [ "$MACOS" -eq 0 ] || [ "$STRAP_ADMIN" -eq 0 ] || [ "$STRAP_CI" -gt 0 ]; then + logskip "Skipping full-disk encryption." +elif [ "$MACOS" -gt 0 ] && [ "$STRAP_ADMIN" -gt 0 ]; then + logn "Checking full-disk encryption status:" + VAULT_MSG="FileVault is (On|Off, but will be enabled after the next restart)." + # shellcheck disable=SC2086 + if fdesetup status | grep $Q -E "$VAULT_MSG"; then + logk + elif sudo_askpass fdesetup enable -user "$USER" | + tee ~/Desktop/"FileVault Recovery Key.txt"; then + log "Full-disk encryption will be enabled after next reboot:" + logk + else + abort "Run 'sudo fdesetup enable -user \"$USER\"' for full-disk encryption." + fi fi # Set up Xcode Command Line Tools @@ -295,15 +309,34 @@ check_xcode_license() { fi } -if [ "$MACOS" -gt 0 ]; then +check_software_updates() { + log "Checking for software updates:" + # shellcheck disable=SC2086 + if softwareupdate -l 2>&1 | grep $Q "No new software available."; then + logk + else + if [ "$MACOS" -gt 0 ] && [ "$STRAP_CI" -eq 0 ]; then + echo + log "Installing software updates:" + sudo_askpass softwareupdate --install --all + check_xcode_license + else + logskip "Skipping software updates." + fi + logk + fi +} + +if [ "$MACOS" -gt 0 ] && [ "$STRAP_ADMIN" -gt 0 ]; then install_xcode_clt check_xcode_license + check_software_updates else - log "Not macOS. Xcode CLT install and license check skipped." + logskip "Xcode Command-Line Tools install and license check skipped." fi configure_git() { - logn "Configuring Git:" + log_no_sudo "Configuring Git:" if [ "$STRAP_CI" -gt 0 ]; then git config --global commit.gpgsign false git config --global gpg.format openpgp @@ -342,34 +375,17 @@ configure_git() { # The first call to `configure_git` is needed for cloning the dotfiles repo. configure_git -# Check for and install any remaining software updates -logn "Checking for software updates:" -# shellcheck disable=SC2086 -if softwareupdate -l 2>&1 | grep $Q "No new software available."; then - logk -else - if [ "$MACOS" -gt 0 ] && [ "$STRAP_CI" -eq 0 ]; then - echo - log "Installing software updates:" - sudo_askpass softwareupdate --install --all - check_xcode_license - else - log "Skipping software updates." - fi - logk -fi - # Set up dotfiles # shellcheck disable=SC2086 if [ ! -d "$HOME/.dotfiles" ]; then if [ -z "$STRAP_DOTFILES_URL" ] || [ -z "$STRAP_DOTFILES_BRANCH" ]; then abort "Please set STRAP_DOTFILES_URL and STRAP_DOTFILES_BRANCH." fi - log "Cloning $STRAP_DOTFILES_URL to ~/.dotfiles." + log_no_sudo "Cloning $STRAP_DOTFILES_URL to ~/.dotfiles." git clone $Q "$STRAP_DOTFILES_URL" ~/.dotfiles fi strap_dotfiles_branch_name="${STRAP_DOTFILES_BRANCH##*/}" -log "Checking out $strap_dotfiles_branch_name in ~/.dotfiles." +log_no_sudo "Checking out $strap_dotfiles_branch_name in ~/.dotfiles." # shellcheck disable=SC2086 ( cd ~/.dotfiles @@ -423,7 +439,7 @@ install_homebrew() { set_up_brew_skips() { local brewfile_path casks ci_skips mas_ids mas_prefix - log "Setting up Homebrew Bundle formula installs to skip." + log_no_sudo "Setting up Homebrew Bundle formula installs to skip." ci_skips="awscli black jupyterlab mkvtoolnix zsh-completions" [ "$STRAP_CI" -gt 0 ] && HOMEBREW_BUNDLE_BREW_SKIP="$ci_skips" if [ -f "$HOME/.Brewfile" ]; then @@ -433,16 +449,16 @@ set_up_brew_skips() { else abort "No Brewfile found" fi - log "Setting up Homebrew Bundle cask installs to skip." + log_no_sudo "Setting up Homebrew Bundle cask installs to skip." if [ "$MACOS" -gt 0 ] && [ "$brewfile_path" == "$HOME/.Brewfile" ]; then casks="$(brew bundle list --global --cask --quiet | tr '\n' ' ')" elif [ "$MACOS" -gt 0 ] && [ "$brewfile_path" == "Brewfile" ]; then casks="$(brew bundle list --cask --quiet | tr '\n' ' ')" else - log "Cask commands are only supported on macOS." + log_no_sudo "Cask commands are only supported on macOS." fi HOMEBREW_BUNDLE_CASK_SKIP="${casks%% }" - log "Setting up Homebrew Bundle Mac App Store (mas) installs to skip." + log_no_sudo "Setting up Homebrew Bundle Mac App Store (mas) installs to skip." mas_ids="" mas_prefix='*mas*, id: ' while read -r brewfile_line; do @@ -450,9 +466,9 @@ set_up_brew_skips() { [[ $brewfile_line == *$mas_prefix* ]] && mas_ids+="${brewfile_line##$mas_prefix} " done <"$brewfile_path" HOMEBREW_BUNDLE_MAS_SKIP="${mas_ids%% }" - log "HOMEBREW_BUNDLE_BREW_SKIP='$HOMEBREW_BUNDLE_BREW_SKIP'" - log "HOMEBREW_BUNDLE_CASK_SKIP='$HOMEBREW_BUNDLE_CASK_SKIP'" - log "HOMEBREW_BUNDLE_MAS_SKIP='$HOMEBREW_BUNDLE_MAS_SKIP'" + log_no_sudo "HOMEBREW_BUNDLE_BREW_SKIP='$HOMEBREW_BUNDLE_BREW_SKIP'" + log_no_sudo "HOMEBREW_BUNDLE_CASK_SKIP='$HOMEBREW_BUNDLE_CASK_SKIP'" + log_no_sudo "HOMEBREW_BUNDLE_MAS_SKIP='$HOMEBREW_BUNDLE_MAS_SKIP'" export HOMEBREW_BUNDLE_BREW_SKIP="$HOMEBREW_BUNDLE_BREW_SKIP" export HOMEBREW_BUNDLE_CASK_SKIP="$HOMEBREW_BUNDLE_CASK_SKIP" export HOMEBREW_BUNDLE_MAS_SKIP="$HOMEBREW_BUNDLE_MAS_SKIP" @@ -505,15 +521,28 @@ run_brew_installs() { fi } -# Install Homebrew: https://docs.brew.sh/Installation -script_url="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" -NONINTERACTIVE=$STRAP_CI \ - /usr/bin/env bash -c "$(curl -fsSL $script_url)" || install_homebrew - -# Set up Homebrew on Linux: https://docs.brew.sh/Homebrew-on-Linux -[ "$LINUX" -gt 0 ] && run_dotfile_scripts scripts/linuxbrew.sh - -run_brew_installs || abort "Homebrew installs were not successful." +# Install Homebrew +# https://docs.brew.sh/Installation +# https://docs.brew.sh/Homebrew-on-Linux +# Homebrew installs require `sudo`, but not necessarily admin +# https://docs.brew.sh/FAQ#why-does-homebrew-say-sudo-is-bad +# https://github.com/Homebrew/install/issues/312 +# https://github.com/Homebrew/install/pull/315/files +if [ -z "$SUDO_ASKPASS" ]; then + log_no_sudo "No sudo access. Homebrew installation requires sudo." +else + if [ "$MACOS" -gt 0 ]; then + script_url="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + NONINTERACTIVE=$STRAP_CI \ + /usr/bin/env bash -c "$(curl -fsSL $script_url)" || install_homebrew + logk + elif [ "$LINUX" -gt 0 ]; then + # Set up Homebrew on Linux: https://docs.brew.sh/Homebrew-on-Linux + run_dotfile_scripts scripts/linuxbrew.sh + logk + fi + run_brew_installs || abort "Homebrew installs were not successful." +fi run_dotfile_scripts scripts/strap-after-setup.sh