From 7c11699d73f2d44de0e20b9c30c187ac7ccb38cd Mon Sep 17 00:00:00 2001 From: Caleb Xu Date: Sat, 13 Apr 2024 00:23:00 -0400 Subject: [PATCH] formula: add methods for allowing/denying network access --- Library/Homebrew/formula.rb | 69 +++++++++++++++++++++++++++ Library/Homebrew/test/formula_spec.rb | 37 ++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 5f83c4c7e1..e3eb453bc7 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -70,6 +70,11 @@ class Formula extend Attrable extend APIHashable + SUPPORTED_NETWORK_ACCESS_PHASES = [:build, :test, :postinstall].freeze + DEFAULT_NETWORK_ACCESS_ALLOWED = true + private_constant :SUPPORTED_NETWORK_ACCESS_PHASES + private_constant :DEFAULT_NETWORK_ACCESS_ALLOWED + # The name of this {Formula}. # e.g. `this-formula` sig { returns(String) } @@ -400,6 +405,7 @@ class Formula !!head && !stable end + # Stop RuboCop from erroneously indenting hash target delegate [ # rubocop:disable Layout/HashAlignment :bottle_defined?, :bottle_tag?, @@ -459,6 +465,13 @@ class Formula # @see .version delegate version: :active_spec + # Stop RuboCop from erroneously indenting hash target + delegate [ # rubocop:disable Layout/HashAlignment + :allow_network_access!, + :deny_network_access!, + :network_access_allowed?, + ] => :"self.class" + # Whether this formula was loaded using the formulae.brew.sh API # @!method loaded_from_api? # @private @@ -3028,6 +3041,9 @@ class Formula @skip_clean_paths = Set.new @link_overwrite_paths = Set.new @loaded_from_api = false + @network_access_allowed = SUPPORTED_NETWORK_ACCESS_PHASES.to_h do |phase| + [phase, DEFAULT_NETWORK_ACCESS_ALLOWED] + end end end @@ -3104,6 +3120,59 @@ class Formula end end + # @!attribute [w] allow_network_access! + # The phases for which network access is allowed. By default, network + # access is allowed for all phases. Valid phases are `:build`, `:test`, + # and `:postinstall`. When no argument is passed, network access will be + # allowed for all phases. + #
allow_network_access!
+ #
allow_network_access! :build
+ #
allow_network_access! [:build, :test]
+ sig { params(phases: T.any(Symbol, T::Array[Symbol])).void } + def allow_network_access!(phases = []) + phases_array = Array(phases) + if phases_array.empty? + @network_access_allowed.each_key { |phase| @network_access_allowed[phase] = true } + else + phases_array.each do |phase| + raise ArgumentError, "Unknown phase: #{phase}" unless SUPPORTED_NETWORK_ACCESS_PHASES.include?(phase) + + @network_access_allowed[phase] = true + end + end + end + + # @!attribute [w] deny_network_access! + # The phases for which network access is denied. By default, network + # access is allowed for all phases. Valid phases are `:build`, `:test`, + # and `:postinstall`. When no argument is passed, network access will be + # denied for all phases. + #
deny_network_access!
+ #
deny_network_access! :build
+ #
deny_network_access! [:build, :test]
+ sig { params(phases: T.any(Symbol, T::Array[Symbol])).void } + def deny_network_access!(phases = []) + phases_array = Array(phases) + if phases_array.empty? + @network_access_allowed.each_key { |phase| @network_access_allowed[phase] = false } + else + phases_array.each do |phase| + raise ArgumentError, "Unknown phase: #{phase}" unless SUPPORTED_NETWORK_ACCESS_PHASES.include?(phase) + + @network_access_allowed[phase] = false + end + end + end + + # Whether the specified phase should be forced offline. + sig { params(phase: Symbol).returns(T::Boolean) } + def network_access_allowed?(phase) + raise ArgumentError, "Unknown phase: #{phase}" unless SUPPORTED_NETWORK_ACCESS_PHASES.include?(phase) + + env_var = Homebrew::EnvConfig.send(:"formula_#{phase}_network") + env_var.nil? ? @network_access_allowed[phase] : env_var == "allow" + end + # @!attribute [w] homepage # The homepage for the software. Used by users to get more information # about the software and Homebrew maintainers as a point of contact for diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index 73d1584bf9..6fc08f61d1 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -42,6 +42,7 @@ RSpec.describe Formula do expect(f.alias_name).to be_nil expect(f.full_alias_name).to be_nil expect(f.specified_path).to eq(path) + [:build, :test, :postinstall].each { |phase| expect(f.network_access_allowed?(phase)).to be(true) } expect { klass.new }.to raise_error(ArgumentError) end @@ -55,6 +56,7 @@ RSpec.describe Formula do expect(f_alias.specified_path).to eq(Pathname(alias_path)) expect(f_alias.full_alias_name).to eq(alias_name) expect(f_alias.full_specified_name).to eq(alias_name) + [:build, :test, :postinstall].each { |phase| expect(f_alias.network_access_allowed?(phase)).to be(true) } expect { klass.new }.to raise_error(ArgumentError) end @@ -1895,4 +1897,39 @@ RSpec.describe Formula do expect(f.fish_completion/"testball.fish").to be_a_file end end + + describe "{allow,deny}_network_access" do + phases = [:build, :postinstall, :test].freeze + actions = %w[allow deny].freeze + phases.each do |phase| + actions.each do |action| + it "can #{action} network access for #{phase}" do + f = Class.new(Testball) do + send(:"#{action}_network_access!", phase) + end + + expect(f.network_access_allowed?(phase)).to be(action == "allow") + end + end + end + + actions.each do |action| + it "can #{action} network access for all phases" do + f = Class.new(Testball) do + send(:"#{action}_network_access!") + end + + phases.each do |phase| + expect(f.network_access_allowed?(phase)).to be(action == "allow") + end + end + end + end + + describe "#network_access_allowed?" do + it "throws an error when passed an invalid symbol" do + f = Testball.new + expect { f.network_access_allowed?(:foo) }.to raise_error(ArgumentError) + end + end end