diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index b7d4309ca9..123796b456 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -574,15 +574,7 @@ on_request: true) sig { void } def forbidden_tap_check - forbidden_taps = Homebrew::EnvConfig.forbidden_taps - return if forbidden_taps.blank? - - forbidden_taps_set = Set.new(forbidden_taps.split.filter_map do |tap| - Tap.fetch(tap) - rescue Tap::InvalidNameError - opoo "Invalid tap name in `HOMEBREW_FORBIDDEN_TAPS`: #{tap}" - nil - end) + return if Tap.allowed_taps.blank? && Tap.forbidden_taps.blank? owner = Homebrew::EnvConfig.forbidden_owner owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence) @@ -592,27 +584,31 @@ on_request: true) unless skip_cask_deps? cask_and_formula_dependencies.each do |cask_or_formula| dep_tap = cask_or_formula.tap - next if dep_tap.blank? - next unless forbidden_taps_set.include?(dep_tap) + next if dep_tap.blank? || (dep_tap.allowed_by_env? && !dep_tap.forbidden_by_env?) dep_full_name = cask_or_formula.full_name - raise CaskCannotBeInstalledError.new(@cask, <<~EOS - The installation of #{@cask} has a dependency #{dep_full_name} - but the #{dep_tap} tap was forbidden by #{owner} in `HOMEBREW_FORBIDDEN_TAPS`.#{owner_contact} - EOS - ) + error_message = +"The installation of #{@cask} has a dependency #{dep_full_name}\n" \ + "from the #{dep_tap} tap but #{owner} " + error_message << "has not allowed this tap in `HOMEBREW_ALLOWED_TAPS`" unless dep_tap.allowed_by_env? + error_message << " and\n" if !dep_tap.allowed_by_env? && dep_tap.forbidden_by_env? + error_message << "has forbidden this tap in `HOMEBREW_FORBIDDEN_TAPS`" if dep_tap.forbidden_by_env? + error_message << ".#{owner_contact}" + + raise CaskCannotBeInstalledError.new(@cask, error_message) end end cask_tap = @cask.tap - return if cask_tap.blank? - return unless forbidden_taps_set.include?(cask_tap) + return if cask_tap.blank? || (cask_tap.allowed_by_env? && !cask_tap.forbidden_by_env?) - raise CaskCannotBeInstalledError.new(@cask, <<~EOS - The installation of #{@cask.full_name} has the tap #{cask_tap} - which was forbidden by #{owner} in `HOMEBREW_FORBIDDEN_TAPS`.#{owner_contact} - EOS - ) + error_message = +"The installation of #{@cask.full_name} has the tap #{cask_tap}\n" \ + "but #{owner} " + error_message << "has not allowed this tap in `HOMEBREW_ALLOWED_TAPS`" unless cask_tap.allowed_by_env? + error_message << " and\n" if !cask_tap.allowed_by_env? && cask_tap.forbidden_by_env? + error_message << "has forbidden this tap in `HOMEBREW_FORBIDDEN_TAPS`" if cask_tap.forbidden_by_env? + error_message << ".#{owner_contact}" + + raise CaskCannotBeInstalledError.new(@cask, error_message) end sig { void } diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb index ef9b72ea21..0424212119 100644 --- a/Library/Homebrew/env_config.rb +++ b/Library/Homebrew/env_config.rb @@ -11,6 +11,11 @@ module Homebrew module_function ENVS = { + HOMEBREW_ALLOWED_TAPS: { + description: "A space-separated list of taps. Homebrew will refuse to install a " \ + "formula unless it and all of its dependencies are in an official tap " \ + "or in a tap on this list.", + }, HOMEBREW_API_AUTO_UPDATE_SECS: { description: "Check Homebrew's API for new formulae or cask data every " \ "`HOMEBREW_API_AUTO_UPDATE_SECS` seconds. Alternatively, disable API auto-update " \ diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index d41673003e..21df0a776c 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -1391,15 +1391,7 @@ on_request: installed_on_request?, options:) sig { void } def forbidden_tap_check - forbidden_taps = Homebrew::EnvConfig.forbidden_taps - return if forbidden_taps.blank? - - forbidden_taps_set = Set.new(forbidden_taps.split.filter_map do |tap| - Tap.fetch(tap) - rescue Tap::InvalidNameError - opoo "Invalid tap name in `HOMEBREW_FORBIDDEN_TAPS`: #{tap}" - nil - end) + return if Tap.allowed_taps.blank? && Tap.forbidden_taps.blank? owner = Homebrew::EnvConfig.forbidden_owner owner_contact = if (contact = Homebrew::EnvConfig.forbidden_owner_contact.presence) @@ -1409,26 +1401,32 @@ on_request: installed_on_request?, options:) unless ignore_deps? compute_dependencies.each do |(dep, _options)| dep_tap = dep.tap - next if dep_tap.blank? - next unless forbidden_taps_set.include?(dep_tap) + next if dep_tap.blank? || (dep_tap.allowed_by_env? && !dep_tap.forbidden_by_env?) - raise CannotInstallFormulaError, <<~EOS - The installation of #{formula.name} has a dependency #{dep.name} - but the #{dep_tap} tap was forbidden by #{owner} in `HOMEBREW_FORBIDDEN_TAPS`.#{owner_contact} - EOS + error_message = +"The installation of #{formula.name} has a dependency #{dep.name}\n" \ + "from the #{dep_tap} tap but #{owner} " + error_message << "has not allowed this tap in `HOMEBREW_ALLOWED_TAPS`" unless dep_tap.allowed_by_env? + error_message << " and\n" if !dep_tap.allowed_by_env? && dep_tap.forbidden_by_env? + error_message << "has forbidden this tap in `HOMEBREW_FORBIDDEN_TAPS`" if dep_tap.forbidden_by_env? + error_message << ".#{owner_contact}" + + raise CannotInstallFormulaError, error_message end end return if only_deps? formula_tap = formula.tap - return if formula_tap.blank? - return unless forbidden_taps_set.include?(formula_tap) + return if formula_tap.blank? || (formula_tap.allowed_by_env? && !formula_tap.forbidden_by_env?) - raise CannotInstallFormulaError, <<~EOS - The installation of #{formula.full_name} has the tap #{formula_tap} - which was forbidden by #{owner} in `HOMEBREW_FORBIDDEN_TAPS`.#{owner_contact} - EOS + error_message = +"The installation of #{formula.full_name} has the tap #{formula_tap}\n" \ + "but #{owner} " + error_message << "has not allowed this tap in `HOMEBREW_ALLOWED_TAPS`" unless formula_tap.allowed_by_env? + error_message << " and\n" if !formula_tap.allowed_by_env? && formula_tap.forbidden_by_env? + error_message << "has forbidden this tap in `HOMEBREW_FORBIDDEN_TAPS`" if formula_tap.forbidden_by_env? + error_message << ".#{owner_contact}" + + raise CannotInstallFormulaError, error_message end sig { void } diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi index 938cd076f0..e6a1945bbe 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi @@ -9,6 +9,9 @@ module Homebrew::EnvConfig sig { returns(T.nilable(::String)) } def all_proxy; end + sig { returns(T.nilable(::String)) } + def allowed_taps; end + sig { returns(Integer) } def api_auto_update_secs; end diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 183bc7ffe4..1577624988 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -132,6 +132,36 @@ class Tap false end + sig { returns(T::Set[Tap]) } + def self.allowed_taps + cache_key = :"allowed_taps_#{Homebrew::EnvConfig.allowed_taps.to_s.tr(" ", "_")}" + cache[cache_key] ||= begin + allowed_tap_list = Homebrew::EnvConfig.allowed_taps.to_s.split + + Set.new(allowed_tap_list.filter_map do |tap| + Tap.fetch(tap) + rescue Tap::InvalidNameError + opoo "Invalid tap name in `HOMEBREW_ALLOWED_TAPS`: #{tap}" + nil + end).freeze + end + end + + sig { returns(T::Set[Tap]) } + def self.forbidden_taps + cache_key = :"forbidden_taps_#{Homebrew::EnvConfig.forbidden_taps.to_s.tr(" ", "_")}" + cache[cache_key] ||= begin + forbidden_tap_list = Homebrew::EnvConfig.forbidden_taps.to_s.split + + Set.new(forbidden_tap_list.filter_map do |tap| + Tap.fetch(tap) + rescue Tap::InvalidNameError + opoo "Invalid tap name in `HOMEBREW_FORBIDDEN_TAPS`: #{tap}" + nil + end).freeze + end + end + # @api public extend Enumerable @@ -1056,6 +1086,20 @@ class Tap end end + sig { returns(T::Boolean) } + def allowed_by_env? + @allowed_by_env ||= begin + allowed_taps = self.class.allowed_taps + + official? || allowed_taps.blank? || allowed_taps.include?(self) + end + end + + sig { returns(T::Boolean) } + def forbidden_by_env? + @forbidden_by_env ||= self.class.forbidden_taps.include?(self) + end + private sig { params(file: Pathname).returns(T.any(T::Array[String], Hash)) } diff --git a/Library/Homebrew/test/cask/installer_spec.rb b/Library/Homebrew/test/cask/installer_spec.rb index 031b9d4a99..568d848909 100644 --- a/Library/Homebrew/test/cask/installer_spec.rb +++ b/Library/Homebrew/test/cask/installer_spec.rb @@ -326,22 +326,48 @@ RSpec.describe Cask::Installer, :cask do end describe "#forbidden_tap_check" do - it "raises on forbidden tap on cask" do - ENV["HOMEBREW_FORBIDDEN_TAPS"] = tap = "homebrew/forbidden" + before do + allow(Tap).to receive_messages(allowed_taps: allowed_taps_set, forbidden_taps: forbidden_taps_set) + end - cask = Cask::Cask.new("homebrew-forbidden-tap", tap: Tap.fetch(tap)) do + let(:homebrew_forbidden) { Tap.fetch("homebrew/forbidden") } + let(:allowed_third_party) { Tap.fetch("nothomebrew/allowed") } + let(:disallowed_third_party) { Tap.fetch("nothomebrew/notallowed") } + let(:allowed_taps_set) { Set.new([allowed_third_party]) } + let(:forbidden_taps_set) { Set.new([homebrew_forbidden]) } + + it "raises on forbidden tap on cask" do + cask = Cask::Cask.new("homebrew-forbidden-tap", tap: homebrew_forbidden) do url "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz" end expect do described_class.new(cask).forbidden_tap_check - end.to raise_error(Cask::CaskCannotBeInstalledError, /has the tap #{tap}/) + end.to raise_error(Cask::CaskCannotBeInstalledError, /has the tap #{homebrew_forbidden}/) + end + + it "raises on not allowed third-party tap on cask" do + cask = Cask::Cask.new("homebrew-not-allowed-tap", tap: disallowed_third_party) do + url "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz" + end + + expect do + described_class.new(cask).forbidden_tap_check + end.to raise_error(Cask::CaskCannotBeInstalledError, /has the tap #{disallowed_third_party}/) + end + + it "does not raise on allowed tap on cask" do + cask = Cask::Cask.new("third-party-allowed-tap", tap: allowed_third_party) do + url "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz" + end + + expect { described_class.new(cask).forbidden_tap_check }.not_to raise_error end it "raises on forbidden tap on dependency" do - ENV["HOMEBREW_FORBIDDEN_TAPS"] = dep_tap = "homebrew/forbidden" + dep_tap = homebrew_forbidden dep_name = "homebrew-forbidden-dependency-tap" - dep_path = Tap.fetch(dep_tap).new_formula_path(dep_name) + dep_path = dep_tap.new_formula_path(dep_name) dep_path.parent.mkpath dep_path.write <<~RUBY class #{Formulary.class_s(dep_name)} < Formula @@ -358,7 +384,7 @@ RSpec.describe Cask::Installer, :cask do expect do described_class.new(cask).forbidden_tap_check - end.to raise_error(Cask::CaskCannotBeInstalledError, /but the #{dep_tap} tap was forbidden/) + end.to raise_error(Cask::CaskCannotBeInstalledError, /from the #{dep_tap} tap but/) ensure dep_path.parent.parent.rmtree end diff --git a/Library/Homebrew/test/formula_installer_spec.rb b/Library/Homebrew/test/formula_installer_spec.rb index e8be7f7cbb..e2655cd0c5 100644 --- a/Library/Homebrew/test/formula_installer_spec.rb +++ b/Library/Homebrew/test/formula_installer_spec.rb @@ -258,10 +258,20 @@ RSpec.describe FormulaInstaller do end describe "#forbidden_tap_check" do + before do + allow(Tap).to receive_messages(allowed_taps: allowed_taps_set, forbidden_taps: forbidden_taps_set) + end + + let(:homebrew_forbidden) { Tap.fetch("homebrew/forbidden") } + let(:allowed_third_party) { Tap.fetch("nothomebrew/allowed") } + let(:disallowed_third_party) { Tap.fetch("nothomebrew/notallowed") } + let(:allowed_taps_set) { Set.new([allowed_third_party]) } + let(:forbidden_taps_set) { Set.new([homebrew_forbidden]) } + it "raises on forbidden tap on formula" do - ENV["HOMEBREW_FORBIDDEN_TAPS"] = f_tap = "homebrew/forbidden" + f_tap = homebrew_forbidden f_name = "homebrew-forbidden-tap" - f_path = Tap.fetch(f_tap).new_formula_path(f_name) + f_path = homebrew_forbidden.new_formula_path(f_name) f_path.parent.mkpath f_path.write <<~RUBY class #{Formulary.class_s(f_name)} < Formula @@ -281,10 +291,54 @@ RSpec.describe FormulaInstaller do f_path.parent.parent.rmtree end + it "raises on not allowed third-party tap on formula" do + f_tap = disallowed_third_party + f_name = "homebrew-not-allowed-tap" + f_path = disallowed_third_party.new_formula_path(f_name) + f_path.parent.mkpath + f_path.write <<~RUBY + class #{Formulary.class_s(f_name)} < Formula + url "foo" + version "0.1" + end + RUBY + Formulary.cache.delete(f_path) + + f = Formulary.factory("#{f_tap}/#{f_name}") + fi = described_class.new(f) + + expect do + fi.forbidden_tap_check + end.to raise_error(CannotInstallFormulaError, /has the tap #{f_tap}/) + ensure + f_path.parent.parent.parent.rmtree + end + + it "does not raise on allowed tap on formula" do + f_tap = allowed_third_party + f_name = "homebrew-allowed-tap" + f_path = allowed_third_party.new_formula_path(f_name) + f_path.parent.mkpath + f_path.write <<~RUBY + class #{Formulary.class_s(f_name)} < Formula + url "foo" + version "0.1" + end + RUBY + Formulary.cache.delete(f_path) + + f = Formulary.factory("#{f_tap}/#{f_name}") + fi = described_class.new(f) + + expect { fi.forbidden_tap_check }.not_to raise_error + ensure + f_path.parent.parent.parent.rmtree + end + it "raises on forbidden tap on dependency" do - ENV["HOMEBREW_FORBIDDEN_TAPS"] = dep_tap = "homebrew/forbidden" + dep_tap = homebrew_forbidden dep_name = "homebrew-forbidden-dependency-tap" - dep_path = Tap.fetch(dep_tap).new_formula_path(dep_name) + dep_path = homebrew_forbidden.new_formula_path(dep_name) dep_path.parent.mkpath dep_path.write <<~RUBY class #{Formulary.class_s(dep_name)} < Formula @@ -310,7 +364,7 @@ RSpec.describe FormulaInstaller do expect do fi.forbidden_tap_check - end.to raise_error(CannotInstallFormulaError, /but the #{dep_tap} tap was forbidden/) + end.to raise_error(CannotInstallFormulaError, /from the #{dep_tap} tap but/) ensure dep_path.parent.parent.rmtree end diff --git a/Library/Homebrew/test/tap_spec.rb b/Library/Homebrew/test/tap_spec.rb index a8cafb6799..940ac944f9 100644 --- a/Library/Homebrew/test/tap_spec.rb +++ b/Library/Homebrew/test/tap_spec.rb @@ -142,6 +142,24 @@ RSpec.describe Tap do end end + describe "::allowed_taps" do + before { allow(Homebrew::EnvConfig).to receive(:allowed_taps).and_return("homebrew/allowed") } + + it "returns a set of allowed taps according to the environment" do + expect(described_class.allowed_taps) + .to contain_exactly(described_class.fetch("homebrew/allowed")) + end + end + + describe "::forbidden_taps" do + before { allow(Homebrew::EnvConfig).to receive(:forbidden_taps).and_return("homebrew/forbidden") } + + it "returns a set of forbidden taps according to the environment" do + expect(described_class.forbidden_taps) + .to contain_exactly(described_class.fetch("homebrew/forbidden")) + end + end + specify "::names" do expect(described_class.names.sort).to eq(["homebrew/core", "homebrew/foo"]) end