1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

Merge rubygems-3.0.0.beta3.

* [GSoC] Multi-factor feature for RubyGems https://github.com/rubygems/rubygems/pull/2369

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@66118 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
hsbt 2018-12-01 11:01:00 +00:00
parent 5cae104e51
commit 7a46a3b941
9 changed files with 169 additions and 12 deletions

View file

@ -9,7 +9,7 @@
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

View file

@ -30,6 +30,7 @@ permission to.
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.
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.
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

View file

@ -33,6 +33,7 @@ command. For further discussion see the help for the yank command.
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 latest release version with:
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 latest release version with:
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

View file

@ -12,6 +12,8 @@ class Gem::Commands::SigninCommand < Gem::Command
add_option('--host HOST', 'Push to another gemcutter-compatible host') do |value, options|
options[:host] = value
end
add_otp_option
end
def description # :nodoc:

View file

@ -24,6 +24,16 @@ module Gem::GemcutterUtilities
end
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.
@ -113,6 +123,13 @@ module Gem::GemcutterUtilities
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
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

View file

@ -87,7 +87,7 @@ class Gem::FakeFetcher
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?

View file

@ -235,4 +235,39 @@ EOF
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@example.com"])
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@example.com"])
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

View file

@ -161,6 +161,7 @@ class TestGemCommandsPushCommand < Gem::TestCase
@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::TestCase
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::TestCase
@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

View file

@ -187,7 +187,35 @@ class TestGemGemcutterUtilities < Gem::TestCase
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@example.com'
password = 'secret'
@ -201,7 +229,7 @@ class TestGemGemcutterUtilities < Gem::TestCase
@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