Files
brew/Library/Homebrew/cask/artifact/install_steps.rb
T
Mike McQuaid 8b3a119e78 Allow cask runs to access the network
Structured cask install steps run in a sandbox that denies network
access. This breaks casks whose upstream setup commands must retrieve
installation-time dependencies, including `gcloud-cli` virtualenv
creation.

- Add a serialised `network_access:` option to `run` steps.
- Omit the option from API data when it retains the default denial.
- Enable network access only when an explicit run requests it.
- Preserve network denial for existing steps and generated completions.
- Document the opt-in and cover both sandbox policies in tests.

This avoids disabling network restrictions for all cask operations while
giving affected casks an explicit, auditable escape hatch.

See #23495.
2026-08-11 12:02:16 +01:00

116 lines
4.0 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "cask/artifact/abstract_artifact"
require "install_steps"
require "keg"
module Cask
module Artifact
# Abstract superclass for install steps artifacts.
class AbstractInstallSteps < AbstractArtifact
abstract!
sig { params(cask: Cask, steps: Homebrew::InstallSteps::Steps).void }
def initialize(cask, steps)
super
@steps = T.let(Homebrew::InstallSteps::DSL.normalise_steps(steps), Homebrew::InstallSteps::Steps)
end
sig { returns(Homebrew::InstallSteps::Steps) }
attr_reader :steps
sig { override.returns(T::Array[T.anything]) }
def to_args = [{ steps: }]
sig { override.returns(String) }
def summarize
::Utils.pluralize("install step", steps.length, include_count: true)
end
private
sig { params(command: T.class_of(SystemCommand), phase: Symbol).void }
def run_steps(command, phase: :install)
runner = Homebrew::InstallSteps::Runner.new(context: cask, command:)
sandbox = cask_sandbox(network_access_allowed: steps.any? do |step|
step["type"] == "run" && step["network_access"] == true
end)
unless sandbox
runner.run(steps, phase:)
return
end
sandbox.allow_write_path cask.caskroom_path
sandbox.allow_write_path cask.config.appdir
sandbox.allow_process_exec "/usr/bin/sudo", no_sandbox: true if runner.sudo_required?(steps)
Keg.keg_link_directories.each { |directory| sandbox.allow_write_path HOMEBREW_PREFIX/directory }
original_home = Pathname(Dir.home).expand_path
runner.sandbox_write_paths(steps, phase:).each do |path|
sandbox.allow_write_path path
sandbox.allow_read(path:, type: :subpath) if path.expand_path.ascend.include?(original_home)
end
run_cask_sandbox(
sandbox,
{
"action" => "install_steps",
"context" => {
"name" => cask.name,
"token" => cask.token,
"version" => cask.version.to_s,
"staged_path" => cask.staged_path.to_s,
"caskroom_path" => cask.caskroom_path.to_s,
"home" => Dir.home,
"config" => cask.config.to_json,
},
"phase" => phase.to_s,
"steps" => steps,
},
)
end
end
# Artifact corresponding to the `preflight_steps` stanza.
class PreflightSteps < AbstractInstallSteps
sig { params(command: T.class_of(SystemCommand), _options: T.anything).void }
def install_phase(command: SystemCommand, **_options)
run_steps(command)
end
sig { params(command: T.class_of(SystemCommand), _options: T.anything).void }
def uninstall_phase(command: SystemCommand, **_options)
run_steps(command, phase: :uninstall)
end
end
# Artifact corresponding to the `postflight_steps` stanza.
class PostflightSteps < AbstractInstallSteps
sig { params(command: T.class_of(SystemCommand), _options: T.anything).void }
def install_phase(command: SystemCommand, **_options)
run_steps(command)
end
sig { params(command: T.class_of(SystemCommand), _options: T.anything).void }
def uninstall_phase(command: SystemCommand, **_options)
run_steps(command, phase: :uninstall)
end
end
# Artifact corresponding to the `uninstall_preflight_steps` stanza.
class UninstallPreflightSteps < AbstractInstallSteps
sig { params(command: T.class_of(SystemCommand), _options: T.anything).void }
def uninstall_phase(command: SystemCommand, **_options)
run_steps(command)
end
end
# Artifact corresponding to the `uninstall_postflight_steps` stanza.
class UninstallPostflightSteps < AbstractInstallSteps
sig { params(command: T.class_of(SystemCommand), _options: T.anything).void }
def uninstall_phase(command: SystemCommand, **_options)
run_steps(command)
end
end
end
end