ruby-changes:71645
From: Ashley <ko1@a...>
Date: Wed, 6 Apr 2022 08:55:16 +0900 (JST)
Subject: [ruby-changes:71645] b3f1b3ccef (master): [rubygems/rubygems] Enable mfa on specific keys during gem signin
https://git.ruby-lang.org/ruby.git/commit/?id=b3f1b3ccef From b3f1b3ccef6f61b95685690e5a8faaa3f009c25f Mon Sep 17 00:00:00 2001 From: Ashley Ellis Pierce <anellis12@g...> Date: Mon, 24 Jan 2022 15:25:28 -0500 Subject: [rubygems/rubygems] Enable mfa on specific keys during gem signin https://github.com/rubygems/rubygems/commit/e787f7f655 --- lib/rubygems/gemcutter_utilities.rb | 25 +++++++++- test/rubygems/test_gem_commands_push_command.rb | 5 ++ test/rubygems/test_gem_commands_signin_command.rb | 61 +++++++++++++++++++++-- test/rubygems/test_gem_gemcutter_utilities.rb | 2 + 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 0968e1a6f9..adf85d1b6c 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -1,6 +1,7 @@ https://github.com/ruby/ruby/blob/trunk/lib/rubygems/gemcutter_utilities.rb#L1 # frozen_string_literal: true require_relative 'remote_fetcher' require_relative 'text' +require 'json' ## # Utility methods for using the RubyGems API. @@ -163,12 +164,13 @@ module Gem::GemcutterUtilities https://github.com/ruby/ruby/blob/trunk/lib/rubygems/gemcutter_utilities.rb#L164 key_name = get_key_name(scope) scope_params = get_scope_params(scope) + mfa_params = get_mfa_params(email, password) response = rubygems_api_request(:post, "api/v1/api_key", sign_in_host, scope: scope) do |request| request.basic_auth email, password request["OTP"] = otp if otp - request.body = URI.encode_www_form({ name: key_name }.merge(scope_params)) + request.body = URI.encode_www_form({ name: key_name }.merge(scope_params, mfa_params)) end with_response response do |resp| @@ -267,6 +269,27 @@ module Gem::GemcutterUtilities https://github.com/ruby/ruby/blob/trunk/lib/rubygems/gemcutter_utilities.rb#L269 scope_params end + def get_mfa_params(email, password) + mfa_level = get_user_mfa_level(email, password) + params = {} + if mfa_level == "ui_only" || mfa_level == "ui_and_gem_sign" + selected = ask "Would you like to enable MFA for this key? [y/N]" + params["mfa"] = true if selected =~ /^[yY](es)?$/ + elsif mfa_level == "ui_and_api" + params["mfa"] = true + end + params + end + + def get_user_mfa_level(email, password) + response = rubygems_api_request(:get, "api/v1/profile") do |request| + request.basic_auth email, password + end + with_response response do |resp| + JSON.parse(resp.body)["mfa"] + end + end + def get_key_name(scope) hostname = Socket.gethostname || "unknown-host" user = ENV["USER"] || ENV["USERNAME"] || "unknown-user" diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index fa3968ffce..e219ae170e 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -435,6 +435,7 @@ class TestGemCommandsPushCommand < Gem::TestCase https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_commands_push_command.rb#L435 response_mfa_enabled = "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)' + response_profile = {"mfa" => "disabled"}.to_json @fetcher.data["#{@host}/api/v1/gems"] = [ [response_success, 200, "OK"], @@ -445,6 +446,10 @@ class TestGemCommandsPushCommand < Gem::TestCase https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_commands_push_command.rb#L446 ["", 200, "OK"], ] + @fetcher.data["#{@host}/api/v1/profile"] = [ + [response_profile, 200, "OK"], + ] + @cmd.instance_variable_set :@scope, :push_rubygem @cmd.options[:args] = [@path] @cmd.options[:host] = @host diff --git a/test/rubygems/test_gem_commands_signin_command.rb b/test/rubygems/test_gem_commands_signin_command.rb index 0f856a53ba..b1493fb137 100644 --- a/test/rubygems/test_gem_commands_signin_command.rb +++ b/test/rubygems/test_gem_commands_signin_command.rb @@ -105,18 +105,71 @@ class TestGemCommandsSigninCommand < Gem::TestCase https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_commands_signin_command.rb#L105 assert_equal api_key, credentials[:rubygems_api_key] end + def test_execute_with_key_name_scope_and_mfa + email = 'you@e...' + password = 'secret' + api_key = '1234' + fetcher = Gem::RemoteFetcher.fetcher + + key_name_ui = Gem::MockGemUi.new "#{email}\n#{password}\ntest-key\n\ny\n\n\n\n\n\ny" + util_capture_with_mfa_enabled(key_name_ui, nil, api_key, fetcher) { @cmd.execute } + + user = ENV["USER"] || ENV["USERNAME"] + + assert_match "API Key name [#{Socket.gethostname}-#{user}", key_name_ui.output + assert_match "index_rubygems [y/N]", key_name_ui.output + assert_match "push_rubygem [y/N]", key_name_ui.output + assert_match "yank_rubygem [y/N]", key_name_ui.output + assert_match "add_owner [y/N]", key_name_ui.output + assert_match "remove_owner [y/N]", key_name_ui.output + assert_match "access_webhooks [y/N]", key_name_ui.output + assert_match "show_dashboard [y/N]", key_name_ui.output + assert_match "Would you like to enable MFA for this key? [y/N]", key_name_ui.output + assert_equal "name=test-key&push_rubygem=true&mfa=true", fetcher.last_request.body + + credentials = load_yaml_file Gem.configuration.credentials_path + assert_equal api_key, credentials[:rubygems_api_key] + end + # Utility method to capture IO/UI within the block passed def util_capture(ui_stub = nil, host = nil, api_key = nil, fetcher = Gem::FakeFetcher.new) - api_key ||= 'a5fdbb6ba150cbb83aad2bb2fede64cf040453903' - response = [api_key, 200, 'OK'] - email = 'you@e...' - password = 'secret' + api_key ||= 'a5fdbb6ba150cbb83aad2bb2fede64cf040453903' + response = [api_key, 200, 'OK'] + profile_response =[{"mfa" => "disabled"}.to_json, 200, 'OK'] + email = 'you@e...' + password = 'secret' + + # Set the expected response for the Web-API supplied + ENV['RUBYGEMS_HOST'] = host || Gem::DEFAULT_HOST + data_key = "#{ENV['RUBYGEMS_HOST']}/api/v1/api_key" + fetcher.data[data_key] = response + profile = "#{ENV['RUBYGEMS_HOST']}/api/v1/profile" + fetcher.data[profile] = profile_response + Gem::RemoteFetcher.fetcher = fetcher + + sign_in_ui = ui_stub || Gem::MockGemUi.new("#{email}\n#{password}\n\n\n\n\n\n\n\n\n") + + use_ui sign_in_ui do + yield + end + + sign_in_ui + end + + def util_capture_with_mfa_enabled(ui_stub = nil, host = nil, api_key = nil, fetcher = Gem::FakeFetcher.new) + api_key ||= 'a5fdbb6ba150cbb83aad2bb2fede64cf040453903' + response = [api_key, 200, 'OK'] + profile_response =[{"mfa" => "ui_only"}.to_json, 200, 'OK'] + email = 'you@e...' + password = 'secret' # Set the expected response for the Web-API supplied ENV['RUBYGEMS_HOST'] = host || Gem::DEFAULT_HOST data_key = "#{ENV['RUBYGEMS_HOST']}/api/v1/api_key" fetcher.data[data_key] = response + profile = "#{ENV['RUBYGEMS_HOST']}/api/v1/profile" + fetcher.data[profile] = profile_response Gem::RemoteFetcher.fetcher = fetcher sign_in_ui = ui_stub || Gem::MockGemUi.new("#{email}\n#{password}\n\n\n\n\n\n\n\n\n") diff --git a/test/rubygems/test_gem_gemcutter_utilities.rb b/test/rubygems/test_gem_gemcutter_utilities.rb index 0bcd1504e9..6eebea6ca7 100644 --- a/test/rubygems/test_gem_gemcutter_utilities.rb +++ b/test/rubygems/test_gem_gemcutter_utilities.rb @@ -229,6 +229,7 @@ class TestGemGemcutterUtilities < Gem::TestCase https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_gemcutter_utilities.rb#L229 def util_sign_in(response, host = nil, args = [], extra_input = '') email = 'you@e...' password = 'secret' + profile_response =[{"mfa" => "disabled"}.to_json, 200, 'OK'] if host ENV['RUBYGEMS_HOST'] = host @@ -238,6 +239,7 @@ class TestGemGemcutterUtilities < Gem::TestCase https://github.com/ruby/ruby/blob/trunk/test/rubygems/test_gem_gemcutter_utilities.rb#L239 @fetcher = Gem::FakeFetcher.new @fetcher.data["#{host}/api/v1/api_key"] = response + @fetcher.data["#{host}/api/v1/profile"] = profile_response Gem::RemoteFetcher.fetcher = @fetcher @sign_in_ui = Gem::MockGemUi.new("#{email}\n#{password}\n\n\n\n\n\n\n\n\n" + extra_input) -- cgit v1.2.1 -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/