diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index 190177eb37e2..b7eec4410814 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 44c31cd10d84..ab33433e75c2 100644 --- a/bundler/lib/bundler/man/bundle-config.1.ronn +++ b/bundler/lib/bundler/man/bundle-config.1.ronn @@ -166,6 +166,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`. diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index cde01e0181ed..042d87aac1c5 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,35 @@ 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 + require "shellwords" + command = Shellwords.shellsplit(helper_path) + command[0] = if command[0].start_with?("/", "~") + command[0] + else + "bundler-credential-#{command[0]}" + 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 + 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 end diff --git a/bundler/spec/bundler/settings_spec.rb b/bundler/spec/bundler/settings_spec.rb index 592db81e9b90..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 @@ -263,6 +264,47 @@ expect(settings.credentials_for(uri)).to eq(credentials) end end + + 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 + allow(Process).to receive(:last_status).and_return(double(success?: true, exitstatus: 0)) + end + + it "uses the credential helper when configured" do + 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(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(IO).to receive(:popen).with([helper_path]).and_yield(StringIO.new("")) + expect(settings.credentials_for(uri)).to be_nil + end + + 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") + end + end + end end describe "URI normalization" do