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

a448a4
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb
a448a4
index 8e56d4a9bc..c37946b46c 100644
a448a4
--- a/lib/bundler/definition.rb
a448a4
+++ b/lib/bundler/definition.rb
a448a4
@@ -901,6 +901,8 @@ def source_requirements
a448a4
       # Load all specs from remote sources
a448a4
       index
a448a4
 
a448a4
+      validate_dependency_confusion! unless disable_dependency_confusion_check?
a448a4
+
a448a4
       # Record the specs available in each gem's source, so that those
a448a4
       # specs will be available later when the resolver knows where to
a448a4
       # look for that gemspec (or its dependencies)
a448a4
@@ -980,5 +982,112 @@ def equivalent_rubygems_remotes?(source)
a448a4
 
a448a4
       Bundler.settings[:allow_deployment_source_credential_changes] && source.equivalent_remotes?(sources.rubygems_remotes)
a448a4
     end
a448a4
+
a448a4
+    def validate_dependency_confusion!
a448a4
+      # Continue if there is a scoped repository in the remote case.
a448a4
+      return unless @remote && sources.non_global_rubygems_sources.size > 0
a448a4
+
a448a4
+      # Raise an error unless all the scope repositories implement the dependency API.
a448a4
+      # When there is a non-dependency API scoped repository, we cannot get
a448a4
+      # indirect dependencies used in a `Gemfile`.
a448a4
+      unless sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
a448a4
+        non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?)
a448a4
+        non_api_source_names_str = non_api_sources.map {|d| "  * #{d}" }.join("\n")
a448a4
+
a448a4
+        msg = String.new
a448a4
+        msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n"
a448a4
+        msg << non_api_source_names_str
a448a4
+        msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \
a448a4
+          "To resolve this warning, make sure you use gem servers that implement dependency APIs, " \
a448a4
+          "such as gemstash or geminabox gem servers."
a448a4
+        raise_error_or_warn_dependency_confusion(msg)
a448a4
+        return
a448a4
+      end
a448a4
+
a448a4
+      indirect_dep_names = indirect_dependency_names_in_non_global_rubygems_soruces
a448a4
+      # Get all the gem names from the index made from the default source.
a448a4
+      # default_source_dep_names = @index.sources.select(&:default_source_used?).map(&:spec_names).flatten
a448a4
+      # Get all the gem names from each source.
a448a4
+      all_spec_names_list = @index.sources.map(&:spec_names)
a448a4
+
a448a4
+      # Only include the indirect dependency gems on the scoped sources that
a448a4
+      # also exist on another source. The gems are included in more than 2
a448a4
+      # sources (the own source + another source). If the gems don't exist on
a448a4
+      # the another source, the dependency confusion doesn't happen.
a448a4
+      indirect_dep_names.select! do |name|
a448a4
+        source_num = all_spec_names_list.select {|all_names| all_names.include?(name) }
a448a4
+        source_num.size >= 2
a448a4
+      end
a448a4
+
a448a4
+      # Raise an error if there is an indirect dependency.
a448a4
+      if indirect_dep_names.size > 0
a448a4
+        dep_names_str = indirect_dep_names.join(", ")
a448a4
+        source_names_str = sources.non_global_rubygems_sources.map {|d| "  * #{d}" }.join("\n")
a448a4
+
a448a4
+        msg = String.new
a448a4
+        msg << "Your Gemfile contains implicit dependency gems #{dep_names_str} on the scoped sources, namely:\n\n"
a448a4
+        msg << source_names_str
a448a4
+        msg << "\n\nUsing implicit dependency gems on the above sources may result in installing unexpected gems. "
a448a4
+        msg << "To suppress this message, make sure you set the gems explicitly in the Gemfile."
a448a4
+        raise_error_or_warn_dependency_confusion(msg)
a448a4
+        return
a448a4
+      end
a448a4
+    end
a448a4
+
a448a4
+    def raise_error_or_warn_dependency_confusion(msg)
a448a4
+      if warn_on_dependnecy_confusion?
a448a4
+        Bundler.ui.warn msg
a448a4
+      else
a448a4
+        msg = "#{msg} Or set the environment variable BUNDLE_WARN_ON_DEPENDENCY_CONFUSION."
a448a4
+        raise SecurityError, msg
a448a4
+      end
a448a4
+    end
a448a4
+
a448a4
+    def indirect_dependency_names_in_non_global_rubygems_soruces
a448a4
+      # Indirect dependency gem names
a448a4
+      indirect_dep_names = []
a448a4
+      # Direct dependency gem names
a448a4
+      direct_dep_names = @dependencies.map(&:name)
a448a4
+
a448a4
+      sources.non_global_rubygems_sources.each do |s|
a448a4
+        # If the non dependency API source is used, the `dependency_names`
a448a4
+        # returns gems not only used in the `Gemfile`, but also returns ones
a448a4
+        # existing in the scoped source too. This method shouldn't be used with
a448a4
+        # the non dependency API sources.
a448a4
+        s.specs.dependency_names.each do |dep_name|
a448a4
+          # Exclude direct dependency gems.
a448a4
+          next if direct_dep_names.include?(dep_name)
a448a4
+
a448a4
+          s.specs.local_search(dep_name).each do |spec|
a448a4
+            # Debug gems with unexpected `spec.class`.
a448a4
+            Bundler.ui.debug "Found dependency gem #{dep_name} (#{spec.class}) in scoped sources."
a448a4
+            # StubSpecification extending RemoteSpecification: the gems by
a448a4
+            #   `gem list`. Exclude the gems.
a448a4
+            # EndpointSpecification: gems returned by dependency API such as
a448a4
+            #   geminabox
a448a4
+            # RemoteSpecification: gems returned by non dependency API such as
a448a4
+            #   gem server. This method cannot be executed with the non
a448a4
+            #   dependency API sources.
a448a4
+            indirect_dep_names << dep_name if spec.class == EndpointSpecification
a448a4
+          end
a448a4
+        end
a448a4
+      end
a448a4
+
a448a4
+      indirect_dep_names.sort.uniq
a448a4
+    end
a448a4
+
a448a4
+    # Print a warning instead of raising an error when this option is enabled.
a448a4
+    # Don't use Bundler.settings to minimize the difference to backport easily
a448a4
+    # and avoid additional tests.
a448a4
+    def warn_on_dependnecy_confusion?
a448a4
+      @warn_on_dependnecy_confusion ||= ENV["BUNDLE_WARN_ON_DEPENDENCY_CONFUSION"]
a448a4
+    end
a448a4
+
a448a4
+    # Disable the dependency confusion check when this option is enabled.
a448a4
+    # The option can be used as a workaround if the check logic is problematic
a448a4
+    # in a case such as a performance issue.
a448a4
+    def disable_dependency_confusion_check?
a448a4
+      @disable_dependnecy_confusion_check ||= ENV["BUNDLE_DISABLE_DEPENDENCY_CONFUSION_CHECK"]
a448a4
+    end
a448a4
   end
a448a4
 end
a448a4
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
a448a4
index 485b388a32..48a2ab736b 100644
a448a4
--- a/lib/bundler/source/rubygems.rb
a448a4
+++ b/lib/bundler/source/rubygems.rb
a448a4
@@ -287,6 +287,10 @@ def dependency_names_to_double_check
a448a4
         names
a448a4
       end
a448a4
 
a448a4
+      def dependency_api_available?
a448a4
+        api_fetchers.any?
a448a4
+      end
a448a4
+
a448a4
     protected
a448a4
 
a448a4
       def credless_remotes
a448a4
diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb
a448a4
index ac2adacb3d..37869878ce 100644
a448a4
--- a/lib/bundler/source_list.rb
a448a4
+++ b/lib/bundler/source_list.rb
a448a4
@@ -64,6 +64,10 @@ def rubygems_sources
a448a4
       @rubygems_sources + [default_source]
a448a4
     end
a448a4
 
a448a4
+    def non_global_rubygems_sources
a448a4
+      @rubygems_sources
a448a4
+    end
a448a4
+
a448a4
     def rubygems_remotes
a448a4
       rubygems_sources.map(&:remotes).flatten.uniq
a448a4
     end