diff options
-rw-r--r-- | lib/rubygems/gemcutter_utilities.rb | 28 | ||||
-rw-r--r-- | test/rubygems/test_gem_commands_owner_command.rb | 26 | ||||
-rw-r--r-- | test/rubygems/test_gem_commands_push_command.rb | 30 | ||||
-rw-r--r-- | test/rubygems/test_gem_commands_yank_command.rb | 33 | ||||
-rw-r--r-- | test/rubygems/test_gem_gemcutter_utilities.rb | 32 |
5 files changed, 140 insertions, 9 deletions
diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 4c510423cd..01c189a8e3 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -81,7 +81,7 @@ module Gem::GemcutterUtilities # # If +allowed_push_host+ metadata is present, then it will only allow that host. - def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, &block) + def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block) require "net/http" self.host = host if host @@ -104,7 +104,7 @@ module Gem::GemcutterUtilities response = request_with_otp(method, uri, &block) if mfa_unauthorized?(response) - ask_otp + ask_otp(credentials) response = request_with_otp(method, uri, &block) end @@ -166,11 +166,12 @@ module Gem::GemcutterUtilities mfa_params = get_mfa_params(profile) all_params = scope_params.merge(mfa_params) warning = profile["warning"] + credentials = { email: email, password: password } say "#{warning}\n" if warning response = rubygems_api_request(:post, "api/v1/api_key", - sign_in_host, scope: scope) do |request| + sign_in_host, credentials: credentials, scope: scope) do |request| request.basic_auth email, password request["OTP"] = otp if otp request.body = URI.encode_www_form({ name: key_name }.merge(all_params)) @@ -249,11 +250,28 @@ module Gem::GemcutterUtilities end end - def ask_otp - say "You have enabled multi-factor authentication. Please enter OTP code." + def ask_otp(credentials) + webauthn_url = webauthn_verification_url(credentials) + unless webauthn_url + say "You have enabled multi-factor authentication. Please enter OTP code." + else + say "You have enabled multi-factor authentication. Please enter OTP code from your security device by visiting #{webauthn_url} or your authenticator app." + end + options[:otp] = ask "Code: " end + def webauthn_verification_url(credentials) + response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request| + if credentials + request.basic_auth credentials[:email], credentials[:password] + else + request.add_field "Authorization", api_key + end + end + response.is_a?(Net::HTTPSuccess) ? response.body : nil + end + def pretty_host(host) if default_host? "RubyGems.org" diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index 7aaeb3a672..8774862070 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -331,6 +331,8 @@ EOF HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), ] + @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @otp_ui = Gem::MockGemUi.new "111111\n" use_ui @otp_ui do @@ -346,6 +348,8 @@ EOF 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"] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized") + @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @otp_ui = Gem::MockGemUi.new "111111\n" use_ui @otp_ui do @@ -358,6 +362,28 @@ EOF assert_equal "111111", @stub_fetcher.last_request["OTP"] end + def test_webauthn_otp_verified_success + webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" + 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/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") + @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [ + HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), + HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), + ] + + @otp_ui = Gem::MockGemUi.new "111111\n" + use_ui @otp_ui do + @cmd.add_owners("freewill", ["user-new1@example.com"]) + end + + assert_match "You have enabled multi-factor authentication. Please enter OTP code from your security device by visiting #{webauthn_verification_url}", @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_remove_owners_unathorized_api_key response_forbidden = "The API key doesn't have access" response_success = "Owner removed successfully." diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index b5458bc7ce..2a45853513 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -392,6 +392,8 @@ class TestGemCommandsPushCommand < Gem::TestCase HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), ] + @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @otp_ui = Gem::MockGemUi.new "111111\n" use_ui @otp_ui do @@ -407,6 +409,8 @@ class TestGemCommandsPushCommand < Gem::TestCase 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"] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized") + @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @otp_ui = Gem::MockGemUi.new "111111\n" assert_raise Gem::MockGemUi::TermError do @@ -421,6 +425,28 @@ class TestGemCommandsPushCommand < Gem::TestCase assert_equal "111111", @fetcher.last_request["OTP"] end + def test_webauthn_otp_verified_success + webauthn_verification_url = "#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY" + 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"] = [ + HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), + HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), + ] + @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") + + @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 from your security device by visiting #{webauthn_verification_url}", @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_sending_gem_unathorized_api_key_with_mfa_enabled response_mfa_enabled = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_forbidden = "The API key doesn't have access" @@ -431,6 +457,8 @@ class TestGemCommandsPushCommand < Gem::TestCase HTTPResponseFactory.create(body: response_forbidden, code: 403, msg: "Forbidden"), HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), ] + @fetcher.data["#{@host}/api/v1/webauthn_verification"] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @fetcher.data["#{@host}/api/v1/api_key"] = HTTPResponseFactory.create(body: "", code: 200, msg: "OK") @cmd.instance_variable_set :@host, @host @@ -471,6 +499,8 @@ class TestGemCommandsPushCommand < Gem::TestCase @fetcher.data["#{@host}/api/v1/profile/me.yaml"] = [ HTTPResponseFactory.create(body: response_profile, code: 200, msg: "OK"), ] + @fetcher.data["#{@host}/api/v1/webauthn_verification"] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @cmd.instance_variable_set :@scope, :push_rubygem @cmd.options[:args] = [@path] diff --git a/test/rubygems/test_gem_commands_yank_command.rb b/test/rubygems/test_gem_commands_yank_command.rb index 0a20617dff..1ded1146b1 100644 --- a/test/rubygems/test_gem_commands_yank_command.rb +++ b/test/rubygems/test_gem_commands_yank_command.rb @@ -73,6 +73,9 @@ class TestGemCommandsYankCommand < Gem::TestCase HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"), ] + webauthn_uri = "http://example/api/v1/webauthn_verification" + @fetcher.data[webauthn_uri] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -94,6 +97,9 @@ class TestGemCommandsYankCommand < Gem::TestCase response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." yank_uri = "http://example/api/v1/gems/yank" @fetcher.data[yank_uri] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized") + webauthn_uri = "http://example/api/v1/webauthn_verification" + @fetcher.data[webauthn_uri] = + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -110,6 +116,33 @@ class TestGemCommandsYankCommand < Gem::TestCase assert_equal "111111", @fetcher.last_request["OTP"] end + def test_execute_with_webauthn_otp_success + webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY" + response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." + yank_uri = "http://example/api/v1/gems/yank" + webauthn_uri = "http://example/api/v1/webauthn_verification" + @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") + @fetcher.data[yank_uri] = [ + HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), + HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"), + ] + + @cmd.options[:args] = %w[a] + @cmd.options[:added_platform] = true + @cmd.options[:version] = req("= 1.0") + + @otp_ui = Gem::MockGemUi.new "111111\n" + use_ui @otp_ui do + @cmd.execute + end + + assert_match "You have enabled multi-factor authentication. Please enter OTP code from your security device by visiting #{webauthn_verification_url}", @otp_ui.output + assert_match "Code: ", @otp_ui.output + assert_match %r{Yanking gem from http://example}, @otp_ui.output + assert_match %r{Successfully yanked}, @otp_ui.output + assert_equal "111111", @fetcher.last_request["OTP"] + end + def test_execute_key yank_uri = "http://example/api/v1/gems/yank" @fetcher.data[yank_uri] = HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK") diff --git a/test/rubygems/test_gem_gemcutter_utilities.rb b/test/rubygems/test_gem_gemcutter_utilities.rb index 3c021fed11..1291a39fc3 100644 --- a/test/rubygems/test_gem_gemcutter_utilities.rb +++ b/test/rubygems/test_gem_gemcutter_utilities.rb @@ -231,10 +231,33 @@ class TestGemGemcutterUtilities < Gem::TestCase assert_equal "111111", @fetcher.last_request["OTP"] end - def util_sign_in(response, host = nil, args = [], extra_input = "") - email = "you@example.com" - password = "secret" - profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK") + def test_sign_in_with_webauthn_otp + webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" + api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903" + response_fail = "You have enabled multifactor authentication" + + util_sign_in(proc do + @call_count ||= 0 + if (@call_count += 1).odd? + HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized") + else + HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK") + end + end, nil, [], "111111\n", webauthn_verification_url) + + assert_match "You have enabled multi-factor authentication. Please enter OTP code from your security device by visiting #{webauthn_verification_url}", @sign_in_ui.output + end + + def util_sign_in(response, host = nil, args = [], extra_input = "", webauthn_url = nil) + email = "you@example.com" + password = "secret" + profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK") + webauthn_response = + if webauthn_url + HTTPResponseFactory.create(body: webauthn_url, code: 200, msg: "OK") + else + HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") + end if host ENV["RUBYGEMS_HOST"] = host @@ -245,6 +268,7 @@ class TestGemGemcutterUtilities < Gem::TestCase @fetcher = Gem::FakeFetcher.new @fetcher.data["#{host}/api/v1/api_key"] = response @fetcher.data["#{host}/api/v1/profile/me.yaml"] = profile_response + @fetcher.data["#{host}/api/v1/webauthn_verification"] = webauthn_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) |