mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
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.
200 lines
7.1 KiB
Ruby
200 lines
7.1 KiB
Ruby
# typed: false
|
|
# frozen_string_literal: true
|
|
|
|
RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do
|
|
before do
|
|
allow(Sandbox).to receive(:available?).and_return(false)
|
|
end
|
|
|
|
let(:cask) do
|
|
Cask::Cask.new("with-install-steps") do
|
|
version "1.2.3"
|
|
sha256 :no_check
|
|
url "file://#{TEST_FIXTURE_DIR}/cask/container.zip"
|
|
|
|
preflight_steps do
|
|
mkdir_p "Prepared"
|
|
set_permissions "Prepared", "0755"
|
|
touch "Prepared/touched"
|
|
end
|
|
|
|
postflight_steps do
|
|
move "move-source", "Prepared/moved"
|
|
symlink "Prepared/moved", "PreparedLink", source_base: :relative, remove_on_uninstall: true
|
|
run "/usr/bin/true"
|
|
end
|
|
|
|
uninstall_preflight_steps do
|
|
mkdir_p "UninstallPrepared"
|
|
touch "UninstallPrepared/touched"
|
|
end
|
|
|
|
uninstall_postflight_steps do
|
|
move_contents "UninstallPrepared", "UninstallMoved"
|
|
end
|
|
end
|
|
end
|
|
|
|
it "runs structured steps through installer artifact phases" do
|
|
cask.staged_path.mkpath
|
|
cask.config_path.dirname.mkpath
|
|
(cask.staged_path/"move-source").write "moved"
|
|
|
|
installer = Cask::Installer.new(cask, command: NeverSudoSystemCommand)
|
|
previous_umask = File.umask(077)
|
|
begin
|
|
installer.install_artifacts
|
|
ensure
|
|
File.umask(previous_umask)
|
|
end
|
|
|
|
expect(cask.staged_path/"Prepared").to be_a_directory
|
|
expect((cask.staged_path/"Prepared").stat.mode & 0777).to eq(0755)
|
|
expect(cask.staged_path/"Prepared/touched").to exist
|
|
expect(cask.staged_path/"Prepared/moved").to exist
|
|
expect(cask.staged_path/"PreparedLink").to be_a_symlink
|
|
|
|
installer.uninstall_artifacts
|
|
|
|
expect(cask.staged_path/"PreparedLink").not_to exist
|
|
expect(cask.staged_path/"UninstallMoved/touched").to exist
|
|
end
|
|
|
|
it "omits cask command output defaults" do
|
|
artifact = cask.artifacts.find { |candidate| candidate.is_a?(Cask::Artifact::PostflightSteps) }
|
|
run_step = artifact.steps.find { |step| step["type"] == "run" }
|
|
|
|
expect(run_step).not_to include("print_stdout", "suppress_stderr", "writable_paths", "network_access")
|
|
end
|
|
|
|
it "sandboxes complete step blocks, including system commands" do
|
|
original_home = mktmpdir
|
|
ENV["HOME"] = original_home.to_s
|
|
cask = Cask::Cask.new("with-sandboxed-install-steps") do
|
|
version "1.2.3"
|
|
sha256 :no_check
|
|
url "file://#{TEST_FIXTURE_DIR}/cask/container.zip"
|
|
|
|
postflight_steps do
|
|
touch "Library/Application Support/cask-home-state", base: :home
|
|
run "helper", args: ["/"], base: :staged_path,
|
|
writable_paths: ["/Library/Example"]
|
|
run "/usr/bin/true"
|
|
end
|
|
end
|
|
sandbox = instance_double(Sandbox).as_null_object
|
|
cask.staged_path.mkpath
|
|
cask.config_path.dirname.mkpath
|
|
(cask.staged_path/"helper").write <<~SH
|
|
#!/bin/sh
|
|
touch "#{cask.staged_path}/sandbox-ran"
|
|
SH
|
|
(cask.staged_path/"helper").chmod 0755
|
|
|
|
allow(Sandbox).to receive_messages(available?: true, new: sandbox)
|
|
allow(sandbox).to receive(:allow_write_path)
|
|
expect(Sandbox).to receive(:with_preserved_brew_file).and_yield
|
|
expect(sandbox).to receive(:add_install_hook_rules).with(network_access_allowed: false)
|
|
expect(sandbox).to receive(:allow_write_path).with(cask.caskroom_path)
|
|
expect(sandbox).to receive(:allow_write_path).with(Pathname("/Library/Example"))
|
|
expect(sandbox).not_to receive(:allow_write_path).with(Pathname("/"))
|
|
expect(sandbox).to receive(:allow_write_path).with(original_home/"Library/Application Support")
|
|
expect(sandbox).to receive(:allow_read)
|
|
.with(path: original_home/"Library/Application Support", type: :subpath)
|
|
expect(sandbox).to receive(:run).once do |*args|
|
|
expect(args).to include(HOMEBREW_LIBRARY_PATH/"cask_artifact.rb")
|
|
|
|
payload = JSON.parse(Pathname(args.last).read)
|
|
expect(payload.fetch("action")).to eq("install_steps")
|
|
expect(payload.fetch("steps").filter_map do |step|
|
|
step.dig("command", "path") if step["type"] == "run"
|
|
end)
|
|
.to eq(%w[helper /usr/bin/true])
|
|
Utils.safe_fork { exec(*args.map(&:to_s)) }
|
|
end
|
|
|
|
Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts
|
|
|
|
expect(cask.staged_path/"sandbox-ran").to exist
|
|
expect(original_home/"Library/Application Support/cask-home-state").to exist
|
|
end
|
|
|
|
it "allows network access for runs that request it" do
|
|
cask = Cask::Cask.new("with-networked-install-step") do
|
|
version "1.2.3"
|
|
sha256 :no_check
|
|
url "file://#{TEST_FIXTURE_DIR}/cask/container.zip"
|
|
|
|
postflight_steps do
|
|
run "/usr/bin/true", network_access: true
|
|
end
|
|
end
|
|
sandbox = instance_double(Sandbox).as_null_object
|
|
cask.staged_path.mkpath
|
|
cask.config_path.dirname.mkpath
|
|
|
|
allow(Sandbox).to receive_messages(available?: true, new: sandbox)
|
|
allow(Sandbox).to receive(:with_preserved_brew_file).and_yield
|
|
expect(sandbox).to receive(:add_install_hook_rules).with(network_access_allowed: true)
|
|
expect(sandbox).to receive(:run)
|
|
|
|
Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts
|
|
end
|
|
|
|
context "when install steps may require sudo" do
|
|
{
|
|
"an explicitly privileged command" => proc { run "/usr/bin/true", sudo: true },
|
|
"a conditionally privileged step" => proc { remove "/usr/local/example", sudo: :if_needed },
|
|
"an ownership step" => proc { set_ownership "/usr/local/example" },
|
|
"a keychain certificate step" => proc { delete_keychain_certificates "Example" },
|
|
}.each do |description, step|
|
|
it "allows #{description} to run sudo outside the sandbox" do
|
|
cask = Cask::Cask.new("with-sandboxed-sudo-install-steps") do
|
|
version "1.2.3"
|
|
sha256 :no_check
|
|
url "file://#{TEST_FIXTURE_DIR}/cask/container.zip"
|
|
|
|
postflight_steps(&step)
|
|
end
|
|
sandbox = instance_double(Sandbox).as_null_object
|
|
cask.staged_path.mkpath
|
|
cask.config_path.dirname.mkpath
|
|
|
|
allow(Sandbox).to receive_messages(available?: true, new: sandbox)
|
|
allow(sandbox).to receive(:allow_write_path)
|
|
allow(Sandbox).to receive(:with_preserved_brew_file).and_yield
|
|
expect(sandbox).to receive(:allow_process_exec).with("/usr/bin/sudo", no_sandbox: true)
|
|
expect(sandbox).to receive(:run)
|
|
|
|
Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts
|
|
end
|
|
end
|
|
end
|
|
|
|
it "runs a flight block after matching steps during migration" do
|
|
cask = Cask::Cask.new("with-install-steps-bridge") do
|
|
version "1.2.3"
|
|
sha256 :no_check
|
|
url "file://#{TEST_FIXTURE_DIR}/cask/container.zip"
|
|
|
|
preflight_steps do
|
|
touch "steps-ran"
|
|
end
|
|
|
|
preflight do
|
|
raise "preflight steps did not run first" unless (staged_path/"steps-ran").exist?
|
|
|
|
FileUtils.touch staged_path/"ruby-block-ran"
|
|
end
|
|
end
|
|
|
|
cask.staged_path.mkpath
|
|
cask.config_path.dirname.mkpath
|
|
|
|
Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts
|
|
|
|
expect(cask.staged_path/"ruby-block-ran").to exist
|
|
expect(cask.staged_path/"steps-ran").to exist
|
|
end
|
|
end
|