ruby-changes:53899
From: hsbt <ko1@a...>
Date: Sat, 1 Dec 2018 20:01:05 +0900 (JST)
Subject: [ruby-changes:53899] hsbt:r66118 (trunk): Merge rubygems-3.0.0.beta3.
hsbt 2018-12-01 20:01:00 +0900 (Sat, 01 Dec 2018) New Revision: 66118 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=66118 Log: Merge rubygems-3.0.0.beta3. * [GSoC] Multi-factor feature for RubyGems https://github.com/rubygems/rubygems/pull/2369 Modified files: trunk/lib/rubygems/commands/owner_command.rb trunk/lib/rubygems/commands/push_command.rb trunk/lib/rubygems/commands/signin_command.rb trunk/lib/rubygems/gemcutter_utilities.rb trunk/lib/rubygems/test_utilities.rb trunk/lib/rubygems.rb trunk/test/rubygems/test_gem_commands_owner_command.rb trunk/test/rubygems/test_gem_commands_push_command.rb trunk/test/rubygems/test_gem_gemcutter_utilities.rb Index: lib/rubygems.rb =================================================================== --- lib/rubygems.rb (revision 66117) +++ lib/rubygems.rb (revision 66118) @@ -9,7 +9,7 @@ https://github.com/ruby/ruby/blob/trunk/lib/rubygems.rb#L9 require 'rbconfig' module Gem - VERSION = "3.0.0.beta2".freeze + VERSION = "3.0.0.beta3".freeze end # Must be first since it unloads the prelude from 1.9.2 Index: lib/rubygems/test_utilities.rb =================================================================== --- lib/rubygems/test_utilities.rb (revision 66117) +++ lib/rubygems/test_utilities.rb (revision 66118) @@ -87,7 +87,7 @@ class Gem::FakeFetcher https://github.com/ruby/ruby/blob/trunk/lib/rubygems/test_utilities.rb#L87 def request(uri, request_class, last_modified = nil) data = find_data(uri) - body, code, msg = data + body, code, msg = (data.respond_to?(:call) ? data.call : data) @last_request = request_class.new uri.request_uri yield @last_request if block_given? Index: lib/rubygems/gemcutter_utilities.rb =================================================================== --- lib/rubygems/gemcutter_utilities.rb (revision 66117) +++ lib/rubygems/gemcutter_utilities.rb (revision 66118) @@ -25,6 +25,16 @@ module Gem::GemcutterUtilities https://github.com/ruby/ruby/blob/trunk/lib/rubygems/gemcutter_utilities.rb#L25 end ## + # Add the --otp option + + def add_otp_option + add_option('--otp CODE', + 'Digit code for multifactor authentication') do |value, options| + options[:otp] = value + end + end + + ## # The API key from the command options or from the user's configuration. def api_key @@ -113,6 +123,13 @@ module Gem::GemcutterUtilities https://github.com/ruby/ruby/blob/trunk/lib/rubygems/gemcutter_utilities.rb#L123 request.basic_auth email, password end + if need_otp? response + response = rubygems_api_request(:get, "api/v1/api_key", sign_in_host) do |request| + request.basic_auth email, password + request.add_field "OTP", options[:otp] + end + end + with_response response do |resp| say "Signed in." set_api_key host, resp.body @@ -156,6 +173,20 @@ module Gem::GemcutterUtilities https://github.com/ruby/ruby/blob/trunk/lib/rubygems/gemcutter_utilities.rb#L173 end end + ## + # Returns true when the user has enabled multifactor authentication from + # +response+ text. + + def need_otp?(response) + return unless response.kind_of?(Net::HTTPUnauthorized) && + response.body.start_with?('You have enabled multifactor authentication') + return true if options[:otp] + + say 'You have enabled multi-factor authentication. Please enter OTP code.' + options[:otp] = ask 'Code: ' + true + end + def set_api_key(host, key) if host == Gem::DEFAULT_HOST Gem.configuration.rubygems_api_key = key Index: lib/rubygems/commands/push_command.rb =================================================================== --- lib/rubygems/commands/push_command.rb (revision 66117) +++ lib/rubygems/commands/push_command.rb (revision 66118) @@ -33,6 +33,7 @@ command. For further discussion see the https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/push_command.rb#L33 add_proxy_option add_key_option + add_otp_option add_option('--host HOST', 'Push to another gemcutter-compatible host', @@ -113,11 +114,10 @@ You can upgrade or downgrade to the late https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/push_command.rb#L114 say "Pushing gem to #{@host || Gem.host}..." - response = rubygems_api_request(*args) do |request| - request.body = Gem.read_binary name - request.add_field "Content-Length", request.body.size - request.add_field "Content-Type", "application/octet-stream" - request.add_field "Authorization", api_key + response = send_push_request(name, args) + + if need_otp? response + response = send_push_request(name, args, true) end with_response response @@ -125,6 +125,16 @@ You can upgrade or downgrade to the late https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/push_command.rb#L125 private + def send_push_request(name, args, use_otp = false) + rubygems_api_request(*args) do |request| + request.body = Gem.read_binary name + request.add_field "Content-Length", request.body.size + request.add_field "Content-Type", "application/octet-stream" + request.add_field "Authorization", api_key + request.add_field "OTP", options[:otp] if use_otp + end + end + def get_hosts_for(name) gem_metadata = Gem::Package.new(name).spec.metadata Index: lib/rubygems/commands/signin_command.rb =================================================================== --- lib/rubygems/commands/signin_command.rb (revision 66117) +++ lib/rubygems/commands/signin_command.rb (revision 66118) @@ -12,6 +12,8 @@ class Gem::Commands::SigninCommand < Gem https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/signin_command.rb#L12 add_option('--host HOST', 'Push to another gemcutter-compatible host') do |value, options| options[:host] = value end + + add_otp_option end def description # :nodoc: Index: lib/rubygems/commands/owner_command.rb =================================================================== --- lib/rubygems/commands/owner_command.rb (revision 66117) +++ lib/rubygems/commands/owner_command.rb (revision 66118) @@ -30,6 +30,7 @@ permission to. https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/owner_command.rb#L30 super 'owner', 'Manage gem owners of a gem on the push server' add_proxy_option add_key_option + add_otp_option defaults.merge! :add => [], :remove => [] add_option '-a', '--add EMAIL', 'Add an owner' do |value, options| @@ -84,9 +85,10 @@ permission to. https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/owner_command.rb#L85 def manage_owners(method, name, owners) owners.each do |owner| begin - response = rubygems_api_request method, "api/v1/gems/#{name}/owners" do |request| - request.set_form_data 'email' => owner - request.add_field "Authorization", api_key + response = send_owner_request(method, name, owner) + + if need_otp? response + response = send_owner_request(method, name, owner, true) end action = method == :delete ? "Removing" : "Adding" @@ -98,4 +100,14 @@ permission to. https://github.com/ruby/ruby/blob/trunk/lib/rubygems/commands/owner_command.rb#L100 end end + private + + def send_owner_request(method, name, owner, use_otp = false) + rubygems_api_request method, "api/v1/gems/#{name}/owners" do |request| + request.set_form_data 'email' => owner + request.add_field "Authorization", api_key + request.add_field "OTP", options[:otp] if use_otp + end + end + end Index: test/rubygems/test_gem_commands_push_command.rb =================================================================== --- test/rubygems/test_gem_commands_push_command.rb (revision 66117) +++ test/rubygems/test_gem_commands_push_command.rb (revision 66118) @@ -161,6 +161,7 @@ class TestGemCommandsPushCommand < Gem:: https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_commands_push_command.rb#L161 @response = "Successfully registered gem: freebird (1.0.1)" @fetcher.data["#{@host}/api/v1/gems"] = [@response, 200, 'OK'] + send_battery end @@ -230,6 +231,7 @@ class TestGemCommandsPushCommand < Gem:: https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_commands_push_command.rb#L231 spec.metadata['allowed_push_host'] = "https://privategemserver.example" end + response = %{ERROR: "#{@host}" is not allowed by the gemspec, which only allows "https://privategemserver.example"} assert_raises Gem::MockGemUi::TermError do @@ -347,4 +349,41 @@ class TestGemCommandsPushCommand < Gem:: https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_commands_push_command.rb#L349 @fetcher.last_request["Authorization"] end + def test_otp_verified_success + response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." + response_success = 'Successfully registered gem: freewill (1.0.0)' + + @fetcher.data["#{Gem.host}/api/v1/gems"] = proc do + @call_count ||= 0 + (@call_count += 1).odd? ? [response_fail, 401, 'Unauthorized'] : [response_success, 200, 'OK'] + end + + @otp_ui = Gem::MockGemUi.new "111111\n" + use_ui @otp_ui do + @cmd.send_gem(@path) + end + + assert_match 'You have enabled multi-factor authentication. Please enter OTP code.', @otp_ui.output + assert_match 'Code: ', @otp_ui.output + assert_match response_success, @otp_ui.output + assert_equal '111111', @fetcher.last_request['OTP'] + end + + def test_otp_verified_failure + response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." + @fetcher.data["#{Gem.host}/api/v1/gems"] = [response, 401, 'Unauthorized'] + + @otp_ui = Gem::MockGemUi.new "111111\n" + assert_raises Gem::MockGemUi::TermError do + use_ui @otp_ui do + @cmd.send_gem(@path) + end + end + + assert_match response, @otp_ui.output + assert_match 'You have enabled multi-factor authentication. Please enter OTP code.', @otp_ui.output + assert_match 'Code: ', @otp_ui.output + assert_equal '111111', @fetcher.last_request['OTP'] + end + end Index: test/rubygems/test_gem_gemcutter_utilities.rb =================================================================== --- test/rubygems/test_gem_gemcutter_utilities.rb (revision 66117) +++ test/rubygems/test_gem_gemcutter_utilities.rb (revision 66118) @@ -187,7 +187,35 @@ class TestGemGemcutterUtilities < Gem::T https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_gemcutter_utilities.rb#L187 assert_match %r{Access Denied.}, @sign_in_ui.output end - def util_sign_in(response, host = nil, args = []) + def test_sign_in_with_correct_otp_code + api_key = 'a5fdbb6ba150cbb83aad2bb2fede64cf040453903' + response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." + + util_sign_in(proc do + @call_count ||= 0 + (@call_count += 1).odd? ? [response_fail, 401, 'Unauthorized'] : [api_key, 200, 'OK'] + end, nil, [], "111111\n") + + assert_match 'You have enabled multi-factor authentication. Please enter OTP code.', @sign_in_ui.output + assert_match 'Code: ', @sign_in_ui.output + assert_match 'Signed in.', @sign_in_ui.output + assert_equal '111111', @fetcher.last_request['OTP'] + end + + def test_sign_in_with_incorrect_otp_code + response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." + + assert_raises Gem::MockGemUi::TermError do + util_sign_in [response, 401, 'Unauthorized'], nil, [], "111111\n" + end + + assert_match 'You have enabled multi-factor authentication. Please enter OTP code.', @sign_in_ui.output + assert_match 'Code: ', @sign_in_ui.output + assert_match response, @sign_in_ui.output + assert_equal '111111', @fetcher.last_request['OTP'] + end + + def util_sign_in(response, host = nil, args = [], extra_input = '') email = 'you@e...' password = 'secret' @@ -201,7 +229,7 @@ class TestGemGemcutterUtilities < Gem::T https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_gemcutter_utilities.rb#L229 @fetcher.data["#{host}/api/v1/api_key"] = response Gem::RemoteFetcher.fetcher = @fetcher - @sign_in_ui = Gem::MockGemUi.new "#{email}\n#{password}\n" + @sign_in_ui = Gem::MockGemUi.new("#{email}\n#{password}\n" + extra_input) use_ui @sign_in_ui do if args.length > 0 Index: test/rubygems/test_gem_commands_owner_command.rb =================================================================== --- test/rubygems/test_gem_commands_owner_command.rb (revision 66117) +++ test/rubygems/test_gem_commands_owner_command.rb (revision 66118) @@ -235,4 +235,39 @@ EOF https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_commands_owner_command.rb#L235 assert_equal "Removing missing@example: #{response}\n", @stub_ui.output end + def test_otp_verified_success + response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." + response_success = "Owner added successfully." + + @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = proc do + @call_count ||= 0 + (@call_count += 1).odd? ? [response_fail, 401, 'Unauthorized'] : [response_success, 200, 'OK'] + end + + @otp_ui = Gem::MockGemUi.new "111111\n" + use_ui @otp_ui do + @cmd.add_owners("freewill", ["user-new1@e..."]) + end + + assert_match 'You have enabled multi-factor authentication. Please enter OTP code.', @otp_ui.output + assert_match 'Code: ', @otp_ui.output + assert_match response_success, @otp_ui.output + assert_equal '111111', @stub_fetcher.last_request['OTP'] + end + + def test_otp_verified_failure + response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." + @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [response, 401, 'Unauthorized'] + + @otp_ui = Gem::MockGemUi.new "111111\n" + use_ui @otp_ui do + @cmd.add_owners("freewill", ["user-new1@e..."]) + end + + assert_match response, @otp_ui.output + assert_match 'You have enabled multi-factor authentication. Please enter OTP code.', @otp_ui.output + assert_match 'Code: ', @otp_ui.output + assert_equal '111111', @stub_fetcher.last_request['OTP'] + end + end -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/