summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/rubygems/gemcutter_utilities.rb28
-rw-r--r--test/rubygems/test_gem_commands_owner_command.rb26
-rw-r--r--test/rubygems/test_gem_commands_push_command.rb30
-rw-r--r--test/rubygems/test_gem_commands_yank_command.rb33
-rw-r--r--test/rubygems/test_gem_gemcutter_utilities.rb32
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)