From e9cb2afd7ed40e80770c3c79d4fcdf219140ef02 Mon Sep 17 00:00:00 2001 From: atpons Date: Tue, 18 Feb 2025 18:10:29 +0900 Subject: [PATCH 1/9] Add credential helper support for private gem registries https://github.com/rubygems/rfcs/pull/59 --- bundler/lib/bundler/settings.rb | 16 +++++++++++++++- bundler/spec/bundler/settings_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index cde01e0181ed..1d383a5d2b57 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -89,6 +89,7 @@ class Settings system_bindir trust-policy version + credential-helper ].freeze DEFAULT_CONFIG = { @@ -197,7 +198,7 @@ def mirror_for(uri) end def credentials_for(uri) - self[uri.to_s] || self[uri.host] + credentials_from_helper(uri) || self[uri.to_s] || self[uri.host] end def gem_mirrors @@ -595,5 +596,18 @@ def self.key_to_s(key) end end end + + def credentials_from_helper(uri) + helper_key = "credential-helper.#{uri.host}" + helper_path = self[helper_key] + return unless helper_path + + begin + output = `#{helper_path}`.strip + output unless output.empty? + rescue StandardError => e + Bundler.ui.warn "Credential helper failed: #{e.message}" + end + end end end diff --git a/bundler/spec/bundler/settings_spec.rb b/bundler/spec/bundler/settings_spec.rb index 592db81e9b90..da8930b6cef2 100644 --- a/bundler/spec/bundler/settings_spec.rb +++ b/bundler/spec/bundler/settings_spec.rb @@ -263,6 +263,31 @@ expect(settings.credentials_for(uri)).to eq(credentials) end end + + context "with credential helper configured" do + let(:helper_path) { "/path/to/helper" } + + before do + settings.set_local "credential-helper.gemserver.example.org", helper_path + end + + it "uses the credential helper when configured" do + expect(settings).to receive(:`).with(helper_path).and_return("username:password\n") + expect(settings.credentials_for(uri)).to eq("username:password") + end + + it "fallback to config when helper fails" do + expect(settings).to receive(:`).with(helper_path).and_raise(StandardError, "Helper failed") + expect(Bundler.ui).to receive(:warn).with("Credential helper failed: Helper failed") + settings.set_local "gemserver.example.org", "fallback:password" + expect(settings.credentials_for(uri)).to eq("fallback:password") + end + + it "returns nil when helper fails and no fallback config exists" do + expect(settings).to receive(:`).with(helper_path).and_return("") + expect(settings.credentials_for(uri)).to be_nil + end + end end describe "URI normalization" do From 0e48def6422616be29f664d935b40d093784be55 Mon Sep 17 00:00:00 2001 From: atpons Date: Tue, 18 Feb 2025 18:22:35 +0900 Subject: [PATCH 2/9] Add man pages for credential helpers --- bundler/lib/bundler/man/bundle-config.1 | 2 ++ bundler/lib/bundler/man/bundle-config.1.ronn | 3 +++ 2 files changed, 5 insertions(+) diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index fb45e631c5fc..2f2156e9c164 100644 --- a/bundler/lib/bundler/man/bundle-config.1 +++ b/bundler/lib/bundler/man/bundle-config.1 @@ -113,6 +113,8 @@ The following is a list of all configuration keys and their purpose\. You can le .IP "\(bu" 4 \fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. .IP "\(bu" 4 +\fBcredential\-helper\fR (\fBBUNDLE_CREDENTIAL_HELPER\fR): The path to a credential helper to use for fetching credentials from a remote gem server\. +.IP "\(bu" 4 \fBdefault_install_uses_path\fR (\fBBUNDLE_DEFAULT_INSTALL_USES_PATH\fR): Whether a \fBbundle install\fR without an explicit \fB\-\-path\fR argument defaults to installing gems in \fB\.bundle\fR\. .IP "\(bu" 4 \fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR): Disallow changes to the \fBGemfile\fR\. When the \fBGemfile\fR is changed and the lockfile has not been updated, running Bundler commands will be blocked\. diff --git a/bundler/lib/bundler/man/bundle-config.1.ronn b/bundler/lib/bundler/man/bundle-config.1.ronn index 00e208195904..faba50995583 100644 --- a/bundler/lib/bundler/man/bundle-config.1.ronn +++ b/bundler/lib/bundler/man/bundle-config.1.ronn @@ -161,6 +161,9 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). `bundle install`. * `console` (`BUNDLE_CONSOLE`): The console that `bundle console` starts. Defaults to `irb`. +* `credential-helper` (`BUNDLE_CREDENTIAL_HELPER`): + The path to a credential helper to use for fetching credentials from a + remote gem server. * `default_install_uses_path` (`BUNDLE_DEFAULT_INSTALL_USES_PATH`): Whether a `bundle install` without an explicit `--path` argument defaults to installing gems in `.bundle`. From 1ffae052535b47618ffe6aff861c9e662f959fc3 Mon Sep 17 00:00:00 2001 From: atpons Date: Mon, 10 Mar 2025 13:05:04 +0900 Subject: [PATCH 3/9] Apply changes from reviews - Git-flavored configurations - IO.popen with exectution --- bundler/lib/bundler/man/bundle-config.1.ronn | 2 +- bundler/lib/bundler/settings.rb | 6 +++--- bundler/spec/bundler/settings_spec.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bundler/lib/bundler/man/bundle-config.1.ronn b/bundler/lib/bundler/man/bundle-config.1.ronn index faba50995583..1d90e9ec197e 100644 --- a/bundler/lib/bundler/man/bundle-config.1.ronn +++ b/bundler/lib/bundler/man/bundle-config.1.ronn @@ -161,7 +161,7 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). `bundle install`. * `console` (`BUNDLE_CONSOLE`): The console that `bundle console` starts. Defaults to `irb`. -* `credential-helper` (`BUNDLE_CREDENTIAL_HELPER`): +* `credential.helper` (`BUNDLE_CREDENTIAL_HELPER`): The path to a credential helper to use for fetching credentials from a remote gem server. * `default_install_uses_path` (`BUNDLE_DEFAULT_INSTALL_USES_PATH`): diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 1d383a5d2b57..9d2760a0d03c 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -89,7 +89,7 @@ class Settings system_bindir trust-policy version - credential-helper + credential.helper ].freeze DEFAULT_CONFIG = { @@ -598,12 +598,12 @@ def self.key_to_s(key) end def credentials_from_helper(uri) - helper_key = "credential-helper.#{uri.host}" + helper_key = "credential.helper.#{uri.host}" helper_path = self[helper_key] return unless helper_path begin - output = `#{helper_path}`.strip + output = IO.popen(helper_path, &:read).strip output unless output.empty? rescue StandardError => e Bundler.ui.warn "Credential helper failed: #{e.message}" diff --git a/bundler/spec/bundler/settings_spec.rb b/bundler/spec/bundler/settings_spec.rb index da8930b6cef2..faa9eb0f01ea 100644 --- a/bundler/spec/bundler/settings_spec.rb +++ b/bundler/spec/bundler/settings_spec.rb @@ -268,7 +268,7 @@ let(:helper_path) { "/path/to/helper" } before do - settings.set_local "credential-helper.gemserver.example.org", helper_path + settings.set_local "credential.helper.gemserver.example.org", helper_path end it "uses the credential helper when configured" do From 972132a7172f3a32f5038938a3843c8735345c41 Mon Sep 17 00:00:00 2001 From: atpons Date: Mon, 10 Mar 2025 16:45:54 +0900 Subject: [PATCH 4/9] Use shellwords and adoption for git-credential-helper protocol --- bundler/lib/bundler/settings.rb | 19 +++++++++++++++++-- bundler/spec/bundler/settings_spec.rb | 16 +++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 9d2760a0d03c..b168330cff64 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -603,10 +603,25 @@ def credentials_from_helper(uri) return unless helper_path begin - output = IO.popen(helper_path, &:read).strip - output unless output.empty? + require "shellwords" + command = Shellwords.shellsplit(helper_path) + command[0] = if command[0].start_with?("/", "~") + command[0] + else + "bundler-credential-#{command[0]}" + end + + output = SharedHelpers.filesystem_access(command[0], :read) do + Bundler.clean_system(*command, out: :err) + end + + output&.strip unless output.to_s.empty? + rescue Errno::ENOENT, ArgumentError => e + Bundler.ui.warn "Credential helper #{helper_path} not available: #{e.message}" + nil rescue StandardError => e Bundler.ui.warn "Credential helper failed: #{e.message}" + nil end end end diff --git a/bundler/spec/bundler/settings_spec.rb b/bundler/spec/bundler/settings_spec.rb index faa9eb0f01ea..fee714e614a7 100644 --- a/bundler/spec/bundler/settings_spec.rb +++ b/bundler/spec/bundler/settings_spec.rb @@ -266,27 +266,37 @@ context "with credential helper configured" do let(:helper_path) { "/path/to/helper" } + let(:uri) { Gem::URI("https://gemserver.example.org") } before do settings.set_local "credential.helper.gemserver.example.org", helper_path end it "uses the credential helper when configured" do - expect(settings).to receive(:`).with(helper_path).and_return("username:password\n") + expect(Bundler).to receive(:clean_system).with(helper_path, out: :err).and_return("username:password\n") expect(settings.credentials_for(uri)).to eq("username:password") end it "fallback to config when helper fails" do - expect(settings).to receive(:`).with(helper_path).and_raise(StandardError, "Helper failed") + expect(Bundler).to receive(:clean_system).with(helper_path, out: :err).and_raise(StandardError, "Helper failed") expect(Bundler.ui).to receive(:warn).with("Credential helper failed: Helper failed") settings.set_local "gemserver.example.org", "fallback:password" expect(settings.credentials_for(uri)).to eq("fallback:password") end it "returns nil when helper fails and no fallback config exists" do - expect(settings).to receive(:`).with(helper_path).and_return("") + expect(Bundler).to receive(:clean_system).with(helper_path, out: :err).and_return("") expect(settings.credentials_for(uri)).to be_nil end + + context "with relative helper path and options" do + let(:helper_path) { "custom-helper --foo=bar" } + + it "prepends bundler-credential- to the helper name" do + expect(Bundler).to receive(:clean_system).with("bundler-credential-custom-helper", "--foo=bar", out: :err).and_return("username:password\n") + expect(settings.credentials_for(uri)).to eq("username:password") + end + end end end From 5830288d8916bb6438c011b4195b2c1047c9fe8c Mon Sep 17 00:00:00 2001 From: atpons Date: Mon, 12 May 2025 06:48:04 +0900 Subject: [PATCH 5/9] Fix shared helpers usage --- bundler/lib/bundler/settings.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index b168330cff64..5e07094fbe02 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -611,10 +611,7 @@ def credentials_from_helper(uri) "bundler-credential-#{command[0]}" end - output = SharedHelpers.filesystem_access(command[0], :read) do - Bundler.clean_system(*command, out: :err) - end - + output = Bundler.with_unbundled_env { IO.popen(command, &:read) } output&.strip unless output.to_s.empty? rescue Errno::ENOENT, ArgumentError => e Bundler.ui.warn "Credential helper #{helper_path} not available: #{e.message}" From a408b428c543ecedeb25c42228bb5ca052b6172c Mon Sep 17 00:00:00 2001 From: atpons Date: Mon, 12 May 2025 06:50:44 +0900 Subject: [PATCH 6/9] Fix helps --- bundler/lib/bundler/man/bundle-config.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index 2f2156e9c164..b13af16b227c 100644 --- a/bundler/lib/bundler/man/bundle-config.1 +++ b/bundler/lib/bundler/man/bundle-config.1 @@ -113,7 +113,7 @@ The following is a list of all configuration keys and their purpose\. You can le .IP "\(bu" 4 \fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. .IP "\(bu" 4 -\fBcredential\-helper\fR (\fBBUNDLE_CREDENTIAL_HELPER\fR): The path to a credential helper to use for fetching credentials from a remote gem server\. +\fBcredential\.helper\fR (\fBBUNDLE_CREDENTIAL_HELPER\fR): The path to a credential helper to use for fetching credentials from a remote gem server\. .IP "\(bu" 4 \fBdefault_install_uses_path\fR (\fBBUNDLE_DEFAULT_INSTALL_USES_PATH\fR): Whether a \fBbundle install\fR without an explicit \fB\-\-path\fR argument defaults to installing gems in \fB\.bundle\fR\. .IP "\(bu" 4 From c7431fa09862bc73f507ca27c25a8c7714bc1ae7 Mon Sep 17 00:00:00 2001 From: atpons Date: Sat, 17 May 2025 10:26:41 +0900 Subject: [PATCH 7/9] Fix empty line check Co-authored-by: Martin Emde --- bundler/lib/bundler/settings.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 5e07094fbe02..08b356831fb0 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -612,7 +612,8 @@ def credentials_from_helper(uri) end output = Bundler.with_unbundled_env { IO.popen(command, &:read) } - output&.strip unless output.to_s.empty? + output = output.to_s.strip + output.empty? ? nil : output rescue Errno::ENOENT, ArgumentError => e Bundler.ui.warn "Credential helper #{helper_path} not available: #{e.message}" nil From 259610f1276a62b5ce40e2c4302855a86b042bda Mon Sep 17 00:00:00 2001 From: atpons Date: Sat, 17 May 2025 10:31:25 +0900 Subject: [PATCH 8/9] Fix spec for using IO.popen --- bundler/spec/bundler/settings_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundler/spec/bundler/settings_spec.rb b/bundler/spec/bundler/settings_spec.rb index fee714e614a7..bbac691004d1 100644 --- a/bundler/spec/bundler/settings_spec.rb +++ b/bundler/spec/bundler/settings_spec.rb @@ -273,19 +273,19 @@ end it "uses the credential helper when configured" do - expect(Bundler).to receive(:clean_system).with(helper_path, out: :err).and_return("username:password\n") + expect(IO).to receive(:popen).with([helper_path]).and_yield(StringIO.new("username:password\n")) expect(settings.credentials_for(uri)).to eq("username:password") end it "fallback to config when helper fails" do - expect(Bundler).to receive(:clean_system).with(helper_path, out: :err).and_raise(StandardError, "Helper failed") + expect(IO).to receive(:popen).with([helper_path]).and_raise(StandardError, "Helper failed") expect(Bundler.ui).to receive(:warn).with("Credential helper failed: Helper failed") settings.set_local "gemserver.example.org", "fallback:password" expect(settings.credentials_for(uri)).to eq("fallback:password") end it "returns nil when helper fails and no fallback config exists" do - expect(Bundler).to receive(:clean_system).with(helper_path, out: :err).and_return("") + expect(IO).to receive(:popen).with([helper_path]).and_yield(StringIO.new("")) expect(settings.credentials_for(uri)).to be_nil end @@ -293,7 +293,7 @@ let(:helper_path) { "custom-helper --foo=bar" } it "prepends bundler-credential- to the helper name" do - expect(Bundler).to receive(:clean_system).with("bundler-credential-custom-helper", "--foo=bar", out: :err).and_return("username:password\n") + expect(IO).to receive(:popen).with(["bundler-credential-custom-helper", "--foo=bar"]).and_yield(StringIO.new("username:password\n")) expect(settings.credentials_for(uri)).to eq("username:password") end end From db1e5abb1cff38aec8e840c2a705071c74293d01 Mon Sep 17 00:00:00 2001 From: atpons Date: Sun, 18 May 2025 12:43:22 +0900 Subject: [PATCH 9/9] Check Process.last_status for fail execution command --- bundler/lib/bundler/settings.rb | 4 ++++ bundler/spec/bundler/settings_spec.rb | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 08b356831fb0..042d87aac1c5 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -612,6 +612,10 @@ def credentials_from_helper(uri) end output = Bundler.with_unbundled_env { IO.popen(command, &:read) } + unless Process.last_status.success? + Bundler.ui.warn "Credential helper failed with exit status #{$?.exitstatus}" + return nil + end output = output.to_s.strip output.empty? ? nil : output rescue Errno::ENOENT, ArgumentError => e diff --git a/bundler/spec/bundler/settings_spec.rb b/bundler/spec/bundler/settings_spec.rb index bbac691004d1..f053a285b0dd 100644 --- a/bundler/spec/bundler/settings_spec.rb +++ b/bundler/spec/bundler/settings_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "stringio" require "bundler/settings" RSpec.describe Bundler::Settings do @@ -270,6 +271,7 @@ before do settings.set_local "credential.helper.gemserver.example.org", helper_path + allow(Process).to receive(:last_status).and_return(double(success?: true, exitstatus: 0)) end it "uses the credential helper when configured" do @@ -292,6 +294,11 @@ context "with relative helper path and options" do let(:helper_path) { "custom-helper --foo=bar" } + before do + settings.set_local "credential.helper.gemserver.example.org", helper_path + allow(Process).to receive(:last_status).and_return(double(success?: true, exitstatus: 0)) + end + it "prepends bundler-credential- to the helper name" do expect(IO).to receive(:popen).with(["bundler-credential-custom-helper", "--foo=bar"]).and_yield(StringIO.new("username:password\n")) expect(settings.credentials_for(uri)).to eq("username:password")