Blame SOURCES/ruby-bundler-raise-error-in-dependency-confusion.patch

0bc38e
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb
0bc38e
index 8e56d4a9bc..c37946b46c 100644
0bc38e
--- a/lib/bundler/definition.rb
0bc38e
+++ b/lib/bundler/definition.rb
0bc38e
@@ -910,6 +910,8 @@ def source_requirements
0bc38e
       # Load all specs from remote sources
0bc38e
       index
0bc38e
 
0bc38e
+      validate_dependency_confusion! unless disable_dependency_confusion_check?
0bc38e
+
0bc38e
       # Record the specs available in each gem's source, so that those
0bc38e
       # specs will be available later when the resolver knows where to
0bc38e
       # look for that gemspec (or its dependencies)
0bc38e
@@ -989,5 +991,112 @@ def equivalent_rubygems_remotes?(source)
0bc38e
 
0bc38e
       Bundler.settings[:allow_deployment_source_credential_changes] && source.equivalent_remotes?(sources.rubygems_remotes)
0bc38e
     end
0bc38e
+
0bc38e
+    def validate_dependency_confusion!
0bc38e
+      # Continue if there is a scoped repository in the remote case.
0bc38e
+      return unless @remote && sources.non_global_rubygems_sources.size > 0
0bc38e
+
0bc38e
+      # Raise an error unless all the scope repositories implement the dependency API.
0bc38e
+      # When there is a non-dependency API scoped repository, we cannot get
0bc38e
+      # indirect dependencies used in a `Gemfile`.
0bc38e
+      unless sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
0bc38e
+        non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?)
0bc38e
+        non_api_source_names_str = non_api_sources.map {|d| "  * #{d}" }.join("\n")
0bc38e
+
0bc38e
+        msg = String.new
0bc38e
+        msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n"
0bc38e
+        msg << non_api_source_names_str
0bc38e
+        msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \
0bc38e
+          "To resolve this warning, make sure you use gem servers that implement dependency APIs, " \
0bc38e
+          "such as gemstash or geminabox gem servers."
0bc38e
+        raise_error_or_warn_dependency_confusion(msg)
0bc38e
+        return
0bc38e
+      end
0bc38e
+
0bc38e
+      indirect_dep_names = indirect_dependency_names_in_non_global_rubygems_soruces
0bc38e
+      # Get all the gem names from the index made from the default source.
0bc38e
+      # default_source_dep_names = @index.sources.select(&:default_source_used?).map(&:spec_names).flatten
0bc38e
+      # Get all the gem names from each source.
0bc38e
+      all_spec_names_list = @index.sources.map(&:spec_names)
0bc38e
+
0bc38e
+      # Only include the indirect dependency gems on the scoped sources that
0bc38e
+      # also exist on another source. The gems are included in more than 2
0bc38e
+      # sources (the own source + another source). If the gems don't exist on
0bc38e
+      # the another source, the dependency confusion doesn't happen.
0bc38e
+      indirect_dep_names.select! do |name|
0bc38e
+        source_num = all_spec_names_list.select {|all_names| all_names.include?(name) }
0bc38e
+        source_num.size >= 2
0bc38e
+      end
0bc38e
+
0bc38e
+      # Raise an error if there is an indirect dependency.
0bc38e
+      if indirect_dep_names.size > 0
0bc38e
+        dep_names_str = indirect_dep_names.join(", ")
0bc38e
+        source_names_str = sources.non_global_rubygems_sources.map {|d| "  * #{d}" }.join("\n")
0bc38e
+
0bc38e
+        msg = String.new
0bc38e
+        msg << "Your Gemfile contains implicit dependency gems #{dep_names_str} on the scoped sources, namely:\n\n"
0bc38e
+        msg << source_names_str
0bc38e
+        msg << "\n\nUsing implicit dependency gems on the above sources may result in installing unexpected gems. "
0bc38e
+        msg << "To suppress this message, make sure you set the gems explicitly in the Gemfile."
0bc38e
+        raise_error_or_warn_dependency_confusion(msg)
0bc38e
+        return
0bc38e
+      end
0bc38e
+    end
0bc38e
+
0bc38e
+    def raise_error_or_warn_dependency_confusion(msg)
0bc38e
+      if warn_on_dependnecy_confusion?
0bc38e
+        Bundler.ui.warn msg
0bc38e
+      else
0bc38e
+        msg = "#{msg} Or set the environment variable BUNDLE_WARN_ON_DEPENDENCY_CONFUSION."
0bc38e
+        raise SecurityError, msg
0bc38e
+      end
0bc38e
+    end
0bc38e
+
0bc38e
+    def indirect_dependency_names_in_non_global_rubygems_soruces
0bc38e
+      # Indirect dependency gem names
0bc38e
+      indirect_dep_names = []
0bc38e
+      # Direct dependency gem names
0bc38e
+      direct_dep_names = @dependencies.map(&:name)
0bc38e
+
0bc38e
+      sources.non_global_rubygems_sources.each do |s|
0bc38e
+        # If the non dependency API source is used, the `dependency_names`
0bc38e
+        # returns gems not only used in the `Gemfile`, but also returns ones
0bc38e
+        # existing in the scoped source too. This method shouldn't be used with
0bc38e
+        # the non dependency API sources.
0bc38e
+        s.specs.dependency_names.each do |dep_name|
0bc38e
+          # Exclude direct dependency gems.
0bc38e
+          next if direct_dep_names.include?(dep_name)
0bc38e
+
0bc38e
+          s.specs.local_search(dep_name).each do |spec|
0bc38e
+            # Debug gems with unexpected `spec.class`.
0bc38e
+            Bundler.ui.debug "Found dependency gem #{dep_name} (#{spec.class}) in scoped sources."
0bc38e
+            # StubSpecification extending RemoteSpecification: the gems by
0bc38e
+            #   `gem list`. Exclude the gems.
0bc38e
+            # EndpointSpecification: gems returned by dependency API such as
0bc38e
+            #   geminabox
0bc38e
+            # RemoteSpecification: gems returned by non dependency API such as
0bc38e
+            #   gem server. This method cannot be executed with the non
0bc38e
+            #   dependency API sources.
0bc38e
+            indirect_dep_names << dep_name if spec.class == EndpointSpecification
0bc38e
+          end
0bc38e
+        end
0bc38e
+      end
0bc38e
+
0bc38e
+      indirect_dep_names.sort.uniq
0bc38e
+    end
0bc38e
+
0bc38e
+    # Print a warning instead of raising an error when this option is enabled.
0bc38e
+    # Don't use Bundler.settings to minimize the difference to backport easily
0bc38e
+    # and avoid additional tests.
0bc38e
+    def warn_on_dependnecy_confusion?
0bc38e
+      @warn_on_dependnecy_confusion ||= ENV["BUNDLE_WARN_ON_DEPENDENCY_CONFUSION"]
0bc38e
+    end
0bc38e
+
0bc38e
+    # Disable the dependency confusion check when this option is enabled.
0bc38e
+    # The option can be used as a workaround if the check logic is problematic
0bc38e
+    # in a case such as a performance issue.
0bc38e
+    def disable_dependency_confusion_check?
0bc38e
+      @disable_dependnecy_confusion_check ||= ENV["BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK"]
0bc38e
+    end
0bc38e
   end
0bc38e
 end
0bc38e
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
0bc38e
index 485b388a32..48a2ab736b 100644
0bc38e
--- a/lib/bundler/source/rubygems.rb
0bc38e
+++ b/lib/bundler/source/rubygems.rb
0bc38e
@@ -289,6 +289,10 @@ def dependency_names_to_double_check
0bc38e
         names
0bc38e
       end
0bc38e
 
0bc38e
+      def dependency_api_available?
0bc38e
+        api_fetchers.any?
0bc38e
+      end
0bc38e
+
0bc38e
     protected
0bc38e
 
0bc38e
       def credless_remotes
0bc38e
diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb
0bc38e
index ac2adacb3d..37869878ce 100644
0bc38e
--- a/lib/bundler/source_list.rb
0bc38e
+++ b/lib/bundler/source_list.rb
0bc38e
@@ -64,6 +64,10 @@ def rubygems_sources
0bc38e
       @rubygems_sources + [default_source]
0bc38e
     end
0bc38e
 
0bc38e
+    def non_global_rubygems_sources
0bc38e
+      @rubygems_sources
0bc38e
+    end
0bc38e
+
0bc38e
     def rubygems_remotes
0bc38e
       rubygems_sources.map(&:remotes).flatten.uniq
0bc38e
     end
0bc38e
diff --git a/spec/bundler/bundler/definition_dep_confusion_spec.rb b/spec/bundler/bundler/definition_dep_confusion_spec.rb
0bc38e
new file mode 100644
0bc38e
index 0000000000..9fee464960
0bc38e
--- /dev/null
0bc38e
+++ b/spec/bundler/bundler/definition_dep_confusion_spec.rb
0bc38e
@@ -0,0 +1,257 @@
0bc38e
+# frozen_string_literal: true
0bc38e
+
0bc38e
+require "bundler/definition"
0bc38e
+
0bc38e
+RSpec.describe Bundler::Definition do
0bc38e
+  before do
0bc38e
+    allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") }
0bc38e
+  end
0bc38e
+
0bc38e
+  let(:sources) { Bundler::SourceList.new }
0bc38e
+  subject { Bundler::Definition.new(nil, [], sources, []) }
0bc38e
+
0bc38e
+  describe "#validate_dependency_confusion!" do
0bc38e
+    before do
0bc38e
+      subject.instance_variable_set(:@remote, remote)
0bc38e
+    end
0bc38e
+
0bc38e
+    context "when it's not remote" do
0bc38e
+      let(:remote) { false }
0bc38e
+
0bc38e
+      it "should neither raise an error nor warn" do
0bc38e
+        expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
0bc38e
+        subject.send(:validate_dependency_confusion!)
0bc38e
+      end
0bc38e
+    end
0bc38e
+
0bc38e
+    context "when it's remote" do
0bc38e
+      before do
0bc38e
+        allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources)
0bc38e
+      end
0bc38e
+
0bc38e
+      let(:remote) { true }
0bc38e
+
0bc38e
+      context "when the number of non-global source is zero" do
0bc38e
+        let(:non_global_rubygems_sources) { [] }
0bc38e
+
0bc38e
+        it "should neither raise an error nor warn" do
0bc38e
+          expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
0bc38e
+          subject.send(:validate_dependency_confusion!)
0bc38e
+        end
0bc38e
+      end
0bc38e
+
0bc38e
+      context "when there are any non dependency API non global sources" do
0bc38e
+        let(:non_global_rubygems_sources) do
0bc38e
+          [
0bc38e
+            double("non-global-source-0", :dependency_api_available? => true, :to_s => "a"),
0bc38e
+            double("non-global-source-1", :dependency_api_available? => false, :to_s => "b"),
0bc38e
+            double("non-global-source-2", :dependency_api_available? => false, :to_s => "c"),
0bc38e
+          ]
0bc38e
+        end
0bc38e
+
0bc38e
+        it "should raise an error or warn" do
0bc38e
+          expect(subject).to receive(:raise_error_or_warn_dependency_confusion).with(<<-M.strip)
0bc38e
+Your Gemfile contains scoped sources that don't implement a dependency API, namely:
0bc38e
+
0bc38e
+  * b
0bc38e
+  * c
0bc38e
+
0bc38e
+Using the above gem servers may result in installing unexpected gems. To resolve this warning, make sure you use gem servers that implement dependency APIs, such as gemstash or geminabox gem servers.
0bc38e
+          M
0bc38e
+          subject.send(:validate_dependency_confusion!)
0bc38e
+        end
0bc38e
+      end
0bc38e
+
0bc38e
+      context "when all the non global sources implement dependency API" do
0bc38e
+        before do
0bc38e
+          allow(subject).to receive(:indirect_dependency_names_in_non_global_rubygems_soruces).and_return(indirect_dependency_names)
0bc38e
+          subject.instance_variable_set(:@index, index)
0bc38e
+        end
0bc38e
+
0bc38e
+        let(:non_global_rubygems_sources) do
0bc38e
+          [
0bc38e
+            double("non-global-source-0", :dependency_api_available? => true, :to_s => "a"),
0bc38e
+            double("non-global-source-1", :dependency_api_available? => true, :to_s => "b"),
0bc38e
+          ]
0bc38e
+        end
0bc38e
+
0bc38e
+        let(:index) { double("index", :sources => index_sources) }
0bc38e
+        let(:index_sources) do
0bc38e
+          [
0bc38e
+            double("index-source-1", :spec_names => ["a1", "a2"]),
0bc38e
+            double("index-source-2", :spec_names => ["a2", "b1", "b2"]),
0bc38e
+            double("index-source-3", :spec_names => ["b2"])
0bc38e
+          ]
0bc38e
+        end
0bc38e
+
0bc38e
+        context "when there is not an indirect dependency in the non global sources" do
0bc38e
+          let(:indirect_dependency_names) {[]}
0bc38e
+
0bc38e
+          it "should neither raise an error nor warn" do
0bc38e
+            expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
0bc38e
+            subject.send(:validate_dependency_confusion!)
0bc38e
+          end
0bc38e
+        end
0bc38e
+
0bc38e
+        context "when there is an indirect dependency in the non global sources" do
0bc38e
+
0bc38e
+          context "when the indirect dependency doesn't exist in another source" do
0bc38e
+            let(:indirect_dependency_names) {["a1", "b1"]}
0bc38e
+
0bc38e
+            it "should neither raise an error nor warn" do
0bc38e
+              expect(subject).not_to receive(:raise_error_or_warn_dependency_confusion)
0bc38e
+              subject.send(:validate_dependency_confusion!)
0bc38e
+            end
0bc38e
+          end
0bc38e
+
0bc38e
+          context "when the indirect dependency also exists in anotehr source" do
0bc38e
+            let(:indirect_dependency_names) {["a1", "a2", "b2"]}
0bc38e
+
0bc38e
+            it "should raise an error or warn" do
0bc38e
+              expect(subject).to receive(:raise_error_or_warn_dependency_confusion).with(<<-M.strip)
0bc38e
+Your Gemfile contains implicit dependency gems a2, b2 on the scoped sources, namely:
0bc38e
+
0bc38e
+  * a
0bc38e
+  * b
0bc38e
+
0bc38e
+Using implicit dependency gems on the above sources may result in installing unexpected gems. To suppress this message, make sure you set the gems explicitly in the Gemfile.
0bc38e
+              M
0bc38e
+              subject.send(:validate_dependency_confusion!)
0bc38e
+            end
0bc38e
+          end
0bc38e
+        end
0bc38e
+      end
0bc38e
+    end
0bc38e
+  end
0bc38e
+
0bc38e
+  describe "#indirect_dependency_names_in_non_global_rubygems_soruces" do
0bc38e
+    before do
0bc38e
+      subject.instance_variable_set(:@dependencies, dependencies)
0bc38e
+      allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources)
0bc38e
+    end
0bc38e
+
0bc38e
+    # Direct dependencies
0bc38e
+    let(:dependencies) do
0bc38e
+      [
0bc38e
+        double("dependency-0", :name => "g0"),
0bc38e
+        double("dependency-1", :name => "g3")
0bc38e
+      ]
0bc38e
+    end
0bc38e
+    let(:non_global_rubygems_sources) do
0bc38e
+      [
0bc38e
+        double("non-global-source-0", :specs => index_0, :to_s => "s0"),
0bc38e
+        double("non-global-source-1", :specs => index_1, :to_s => "s1"),
0bc38e
+      ]
0bc38e
+    end
0bc38e
+    let(:index_0) do
0bc38e
+      # All the dependencies in the source-0.
0bc38e
+      index = double("index-0", :dependency_names => ["g0", "g1", "g2", "g5"])
0bc38e
+      allow(index).to receive(:local_search) do |query|
0bc38e
+        return_map = {
0bc38e
+          "g1" => [double("spec", :class => Bundler::StubSpecification, :to_s => "g1")],
0bc38e
+          "g2" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g2")],
0bc38e
+          "g5" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g5")]
0bc38e
+        }
0bc38e
+        return_map[query]
0bc38e
+      end
0bc38e
+      index
0bc38e
+    end
0bc38e
+    let(:index_1) do
0bc38e
+      # All the dependencies in the source-1.
0bc38e
+      index = double("index-1", :dependency_names => ["g3", "g4", "g5"])
0bc38e
+      allow(index).to receive(:local_search) do |query|
0bc38e
+        return_map = {
0bc38e
+          "g4" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g4")],
0bc38e
+          "g5" => [double("spec", :class => Bundler::EndpointSpecification, :to_s => "g5")]
0bc38e
+        }
0bc38e
+        return_map[query]
0bc38e
+      end
0bc38e
+      index
0bc38e
+    end
0bc38e
+
0bc38e
+    it "should return only indirect dependencies of endpoint specification" do
0bc38e
+      expect(subject.send(:indirect_dependency_names_in_non_global_rubygems_soruces)).to eq(["g2", "g4", "g5"])
0bc38e
+    end
0bc38e
+  end
0bc38e
+
0bc38e
+  describe "#raise_error_or_warn_dependency_confusion" do
0bc38e
+    before do
0bc38e
+      allow(subject).to receive(:warn_on_dependnecy_confusion?).and_return(warn_on_dependnecy_confusion)
0bc38e
+    end
0bc38e
+
0bc38e
+    context "when #warn_on_dependnecy_confusion? returns false" do
0bc38e
+      let(:warn_on_dependnecy_confusion) { false }
0bc38e
+
0bc38e
+      it "should raise an error" do
0bc38e
+        expect(Bundler.ui).not_to receive(:warn)
0bc38e
+        expect do
0bc38e
+          subject.send(:raise_error_or_warn_dependency_confusion, "This is a message.")
0bc38e
+        end.to raise_error(Bundler::SecurityError, "This is a message. " \
0bc38e
+          "Or set the environment variable BUNDLE_WARN_ON_DEPENDENCY_CONFUSION.")
0bc38e
+      end
0bc38e
+    end
0bc38e
+
0bc38e
+    context "when #warn_on_dependnecy_confusion? returns true" do
0bc38e
+      let(:warn_on_dependnecy_confusion) { true }
0bc38e
+
0bc38e
+      it "should warn" do
0bc38e
+        expect(Bundler.ui).to receive(:warn).with(<<-W.strip)
0bc38e
+This is a message.
0bc38e
+W
0bc38e
+        subject.send(:raise_error_or_warn_dependency_confusion, "This is a message.")
0bc38e
+      end
0bc38e
+    end
0bc38e
+  end
0bc38e
+
0bc38e
+  describe "#warn_on_dependnecy_confusion?" do
0bc38e
+    context "when BUNDLE_WARN_ON_DEPENDENCY_CONFUSION is set" do
0bc38e
+      it "should return true" do
0bc38e
+        with_env({"BUNDLE_WARN_ON_DEPENDENCY_CONFUSION" => "1"}) do
0bc38e
+          expect(subject.send(:warn_on_dependnecy_confusion?)).to be_truthy
0bc38e
+        end
0bc38e
+      end
0bc38e
+    end
0bc38e
+
0bc38e
+    context "when BUNDLE_WARN_ON_DEPENDENCY_CONFUSION is not set" do
0bc38e
+      it "should return false" do
0bc38e
+        with_env({"BUNDLE_WARN_ON_DEPENDENCY_CONFUSION" => nil}) do
0bc38e
+          expect(subject.send(:warn_on_dependnecy_confusion?)).to be_falsy
0bc38e
+        end
0bc38e
+      end
0bc38e
+    end
0bc38e
+  end
0bc38e
+
0bc38e
+  describe "#disable_dependency_confusion_check?" do
0bc38e
+    context "when BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK is set" do
0bc38e
+      it "should return true" do
0bc38e
+        with_env({"BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK" => "1"}) do
0bc38e
+          expect(subject.send(:disable_dependency_confusion_check?)).to be_truthy
0bc38e
+        end
0bc38e
+      end
0bc38e
+    end
0bc38e
+
0bc38e
+    context "when BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK is not set" do
0bc38e
+      it "should return false" do
0bc38e
+        with_env({"BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK" => nil}) do
0bc38e
+          expect(subject.send(:disable_dependency_confusion_check?)).to be_falsy
0bc38e
+        end
0bc38e
+      end
0bc38e
+    end
0bc38e
+  end
0bc38e
+
0bc38e
+  def with_env(env={})
0bc38e
+    begin
0bc38e
+      tmp_env = {}
0bc38e
+      env.each do |key, value|
0bc38e
+        tmp_env[key] = ENV.delete key
0bc38e
+        ENV[key] = value
0bc38e
+      end
0bc38e
+
0bc38e
+      yield
0bc38e
+    ensure
0bc38e
+      tmp_env.each do |key, value|
0bc38e
+        ENV[key] = value
0bc38e
+      end
0bc38e
+    end
0bc38e
+  end
0bc38e
+end
0bc38e
-- 
0bc38e
2.31.1
0bc38e