rubygems-update 3.4.11 → 3.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Manifest.txt +4 -0
- data/bundler/CHANGELOG.md +6 -0
- data/bundler/lib/bundler/build_metadata.rb +2 -2
- data/bundler/lib/bundler/templates/newgem/bin/console.tt +0 -4
- data/bundler/lib/bundler/version.rb +1 -1
- data/lib/rubygems/commands/owner_command.rb +4 -2
- data/lib/rubygems/exceptions.rb +10 -0
- data/lib/rubygems/gemcutter_utilities.rb +48 -6
- data/lib/rubygems/webauthn_listener/response.rb +161 -0
- data/lib/rubygems/webauthn_listener.rb +92 -0
- data/lib/rubygems.rb +1 -1
- data/rubygems-update.gemspec +1 -1
- data/test/rubygems/test_gem_commands_owner_command.rb +67 -0
- data/test/rubygems/test_gem_commands_push_command.rb +73 -0
- data/test/rubygems/test_gem_commands_yank_command.rb +84 -0
- data/test/rubygems/test_gem_gemcutter_utilities.rb +72 -4
- data/test/rubygems/test_webauthn_listener.rb +120 -0
- data/test/rubygems/test_webauthn_listener_response.rb +93 -0
- data/test/rubygems/utilities.rb +35 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 630124afddc18f8f7cb265b5de44eca7014d434e5ea8cd6371aefe67c70ea548
|
|
4
|
+
data.tar.gz: 5d6c95dc48dd7a700c98d000f02fc4fd24a6110ad5b4b584e80eb1d2f6a66d32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b48042d92e123bdd1923b74d3b3453efed7ff69096995605267f0e17d23110c3c33a7e70e288554f5088ab8ea60ffd2264007acffabc19c56b8f1a55304e31a
|
|
7
|
+
data.tar.gz: d5d31837a2e3e200876195b3eb4ce91000ae809a867017fe8264301d045995321adc5f88a8ce30a2a1271fcb26a28d200b51b92359f841a0ed1ad309d506f18c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# 3.4.12 / 2023-04-11
|
|
2
|
+
|
|
3
|
+
## Enhancements:
|
|
4
|
+
|
|
5
|
+
* [Experimental] Add WebAuthn Support to the CLI. Pull request
|
|
6
|
+
[#6560](https://github.com/rubygems/rubygems/pull/6560) by jenshenny
|
|
7
|
+
* Installs bundler 2.4.12 as a default gem.
|
|
8
|
+
|
|
1
9
|
# 3.4.11 / 2023-04-10
|
|
2
10
|
|
|
3
11
|
## Enhancements:
|
data/Manifest.txt
CHANGED
|
@@ -540,6 +540,8 @@ lib/rubygems/util/list.rb
|
|
|
540
540
|
lib/rubygems/validator.rb
|
|
541
541
|
lib/rubygems/version.rb
|
|
542
542
|
lib/rubygems/version_option.rb
|
|
543
|
+
lib/rubygems/webauthn_listener.rb
|
|
544
|
+
lib/rubygems/webauthn_listener/response.rb
|
|
543
545
|
rubygems-update.gemspec
|
|
544
546
|
setup.rb
|
|
545
547
|
test/rubygems/alternate_cert.pem
|
|
@@ -753,6 +755,8 @@ test/rubygems/test_project_sanity.rb
|
|
|
753
755
|
test/rubygems/test_remote_fetch_error.rb
|
|
754
756
|
test/rubygems/test_require.rb
|
|
755
757
|
test/rubygems/test_rubygems.rb
|
|
758
|
+
test/rubygems/test_webauthn_listener.rb
|
|
759
|
+
test/rubygems/test_webauthn_listener_response.rb
|
|
756
760
|
test/rubygems/utilities.rb
|
|
757
761
|
test/rubygems/wrong_key_cert.pem
|
|
758
762
|
test/rubygems/wrong_key_cert_32.pem
|
data/bundler/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,8 @@ module Bundler
|
|
|
4
4
|
# Represents metadata from when the Bundler gem was built.
|
|
5
5
|
module BuildMetadata
|
|
6
6
|
# begin ivars
|
|
7
|
-
@built_at = "2023-04-
|
|
8
|
-
@git_commit_sha = "
|
|
7
|
+
@built_at = "2023-04-11".freeze
|
|
8
|
+
@git_commit_sha = "e2cf278db1".freeze
|
|
9
9
|
@release = true
|
|
10
10
|
# end ivars
|
|
11
11
|
|
|
@@ -7,9 +7,5 @@ require "<%= config[:namespaced_path] %>"
|
|
|
7
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
|
9
9
|
|
|
10
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
-
# require "pry"
|
|
12
|
-
# Pry.start
|
|
13
|
-
|
|
14
10
|
require "irb"
|
|
15
11
|
IRB.start(__FILE__)
|
|
@@ -98,8 +98,10 @@ permission to.
|
|
|
98
98
|
action = method == :delete ? "Removing" : "Adding"
|
|
99
99
|
|
|
100
100
|
with_response response, "#{action} #{owner}"
|
|
101
|
-
rescue
|
|
102
|
-
|
|
101
|
+
rescue Gem::WebauthnVerificationError => e
|
|
102
|
+
raise e
|
|
103
|
+
rescue StandardError
|
|
104
|
+
# ignore early exits to allow for completing the iteration of all owners
|
|
103
105
|
end
|
|
104
106
|
end
|
|
105
107
|
end
|
data/lib/rubygems/exceptions.rb
CHANGED
|
@@ -213,6 +213,16 @@ class Gem::RubyVersionMismatch < Gem::Exception; end
|
|
|
213
213
|
|
|
214
214
|
class Gem::VerificationError < Gem::Exception; end
|
|
215
215
|
|
|
216
|
+
##
|
|
217
|
+
# Raised by Gem::WebauthnListener when an error occurs during security
|
|
218
|
+
# device verification.
|
|
219
|
+
|
|
220
|
+
class Gem::WebauthnVerificationError < Gem::Exception
|
|
221
|
+
def initialize(message)
|
|
222
|
+
super "Security device verification failed: #{message}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
216
226
|
##
|
|
217
227
|
# Raised to indicate that a system exit should occur with the specified
|
|
218
228
|
# exit_code
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require_relative "remote_fetcher"
|
|
3
3
|
require_relative "text"
|
|
4
|
+
require_relative "webauthn_listener"
|
|
4
5
|
|
|
5
6
|
##
|
|
6
7
|
# Utility methods for using the RubyGems API.
|
|
@@ -82,7 +83,7 @@ module Gem::GemcutterUtilities
|
|
|
82
83
|
#
|
|
83
84
|
# If +allowed_push_host+ metadata is present, then it will only allow that host.
|
|
84
85
|
|
|
85
|
-
def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, &block)
|
|
86
|
+
def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block)
|
|
86
87
|
require "net/http"
|
|
87
88
|
|
|
88
89
|
self.host = host if host
|
|
@@ -105,7 +106,7 @@ module Gem::GemcutterUtilities
|
|
|
105
106
|
response = request_with_otp(method, uri, &block)
|
|
106
107
|
|
|
107
108
|
if mfa_unauthorized?(response)
|
|
108
|
-
|
|
109
|
+
fetch_otp(credentials)
|
|
109
110
|
response = request_with_otp(method, uri, &block)
|
|
110
111
|
end
|
|
111
112
|
|
|
@@ -167,11 +168,12 @@ module Gem::GemcutterUtilities
|
|
|
167
168
|
mfa_params = get_mfa_params(profile)
|
|
168
169
|
all_params = scope_params.merge(mfa_params)
|
|
169
170
|
warning = profile["warning"]
|
|
171
|
+
credentials = { email: email, password: password }
|
|
170
172
|
|
|
171
173
|
say "#{warning}\n" if warning
|
|
172
174
|
|
|
173
175
|
response = rubygems_api_request(:post, "api/v1/api_key",
|
|
174
|
-
sign_in_host, scope: scope) do |request|
|
|
176
|
+
sign_in_host, credentials: credentials, scope: scope) do |request|
|
|
175
177
|
request.basic_auth email, password
|
|
176
178
|
request["OTP"] = otp if otp
|
|
177
179
|
request.body = URI.encode_www_form({ name: key_name }.merge(all_params))
|
|
@@ -250,9 +252,49 @@ module Gem::GemcutterUtilities
|
|
|
250
252
|
end
|
|
251
253
|
end
|
|
252
254
|
|
|
253
|
-
def
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
def fetch_otp(credentials)
|
|
256
|
+
options[:otp] = if webauthn_url = webauthn_verification_url(credentials)
|
|
257
|
+
wait_for_otp(webauthn_url)
|
|
258
|
+
else
|
|
259
|
+
say "You have enabled multi-factor authentication. Please enter OTP code."
|
|
260
|
+
ask "Code: "
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def wait_for_otp(webauthn_url)
|
|
265
|
+
server = TCPServer.new 0
|
|
266
|
+
port = server.addr[1].to_s
|
|
267
|
+
|
|
268
|
+
thread = Thread.new do
|
|
269
|
+
Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(host, server)
|
|
270
|
+
rescue Gem::WebauthnVerificationError => e
|
|
271
|
+
Thread.current[:error] = e
|
|
272
|
+
end
|
|
273
|
+
thread.abort_on_exception = true
|
|
274
|
+
thread.report_on_exception = false
|
|
275
|
+
|
|
276
|
+
url_with_port = "#{webauthn_url}?port=#{port}"
|
|
277
|
+
say "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option."
|
|
278
|
+
|
|
279
|
+
thread.join
|
|
280
|
+
if error = thread[:error]
|
|
281
|
+
alert_error error.message
|
|
282
|
+
terminate_interaction(1)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
say "You are verified with a security device. You may close the browser window."
|
|
286
|
+
thread[:otp]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def webauthn_verification_url(credentials)
|
|
290
|
+
response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request|
|
|
291
|
+
if credentials.empty?
|
|
292
|
+
request.add_field "Authorization", api_key
|
|
293
|
+
else
|
|
294
|
+
request.basic_auth credentials[:email], credentials[:password]
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
response.is_a?(Net::HTTPSuccess) ? response.body : nil
|
|
256
298
|
end
|
|
257
299
|
|
|
258
300
|
def pretty_host(host)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# The WebauthnListener Response class is used by the WebauthnListener to create
|
|
5
|
+
# responses to be sent to the Gem host. It creates a Net::HTTPResponse instance
|
|
6
|
+
# when initialized and can be converted to the appropriate format to be sent by a socket using `to_s`.
|
|
7
|
+
# Net::HTTPResponse instances cannot be directly sent over a socket.
|
|
8
|
+
#
|
|
9
|
+
# Types of response classes:
|
|
10
|
+
# - OkResponse
|
|
11
|
+
# - NoContentResponse
|
|
12
|
+
# - BadRequestResponse
|
|
13
|
+
# - NotFoundResponse
|
|
14
|
+
# - MethodNotAllowedResponse
|
|
15
|
+
#
|
|
16
|
+
# Example usage:
|
|
17
|
+
#
|
|
18
|
+
# server = TCPServer.new(0)
|
|
19
|
+
# socket = server.accept
|
|
20
|
+
#
|
|
21
|
+
# response = OkResponse.for("https://rubygems.example")
|
|
22
|
+
# socket.print response.to_s
|
|
23
|
+
# socket.close
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
class Gem::WebauthnListener
|
|
27
|
+
class Response
|
|
28
|
+
attr_reader :http_response
|
|
29
|
+
|
|
30
|
+
def self.for(host)
|
|
31
|
+
new(host)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(host)
|
|
35
|
+
@host = host
|
|
36
|
+
|
|
37
|
+
build_http_response
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
status_line = "HTTP/#{@http_response.http_version} #{@http_response.code} #{@http_response.message}\r\n"
|
|
42
|
+
headers = @http_response.to_hash.map {|header, value| "#{header}: #{value.join(", ")}\r\n" }.join + "\r\n"
|
|
43
|
+
body = @http_response.body ? "#{@http_response.body}\n" : ""
|
|
44
|
+
|
|
45
|
+
status_line + headers + body
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Must be implemented in subclasses
|
|
51
|
+
def code
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reason_phrase
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def body; end
|
|
60
|
+
|
|
61
|
+
def build_http_response
|
|
62
|
+
response_class = Net::HTTPResponse::CODE_TO_OBJ[code.to_s]
|
|
63
|
+
@http_response = response_class.new("1.1", code, reason_phrase)
|
|
64
|
+
@http_response.instance_variable_set(:@read, true)
|
|
65
|
+
|
|
66
|
+
add_connection_header
|
|
67
|
+
add_access_control_headers
|
|
68
|
+
add_body
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_connection_header
|
|
72
|
+
@http_response["connection"] = "close"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_access_control_headers
|
|
76
|
+
@http_response["access-control-allow-origin"] = @host
|
|
77
|
+
@http_response["access-control-allow-methods"] = "POST"
|
|
78
|
+
@http_response["access-control-allow-headers"] = %w[Content-Type Authorization x-csrf-token]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_body
|
|
82
|
+
return unless body
|
|
83
|
+
@http_response["content-type"] = "text/plain"
|
|
84
|
+
@http_response["content-length"] = body.bytesize
|
|
85
|
+
@http_response.instance_variable_set(:@body, body)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class OkResponse < Response
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def code
|
|
93
|
+
200
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def reason_phrase
|
|
97
|
+
"OK"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def body
|
|
101
|
+
"success"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class NoContentResponse < Response
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def code
|
|
109
|
+
204
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def reason_phrase
|
|
113
|
+
"No Content"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class BadRequestResponse < Response
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def code
|
|
121
|
+
400
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def reason_phrase
|
|
125
|
+
"Bad Request"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def body
|
|
129
|
+
"missing code parameter"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class NotFoundResponse < Response
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def code
|
|
137
|
+
404
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def reason_phrase
|
|
141
|
+
"Not Found"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class MethodNotAllowedResponse < Response
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def code
|
|
149
|
+
405
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def reason_phrase
|
|
153
|
+
"Method Not Allowed"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def add_access_control_headers
|
|
157
|
+
super
|
|
158
|
+
@http_response["allow"] = %w[GET OPTIONS]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "webauthn_listener/response"
|
|
4
|
+
|
|
5
|
+
##
|
|
6
|
+
# The WebauthnListener class retrieves an OTP after a user successfully WebAuthns with the Gem host.
|
|
7
|
+
# An instance opens a socket using the TCPServer instance given and listens for a request from the Gem host.
|
|
8
|
+
# The request should be a GET request to the root path and contains the OTP code in the form
|
|
9
|
+
# of a query parameter `code`. The listener will return the code which will be used as the OTP for
|
|
10
|
+
# API requests.
|
|
11
|
+
#
|
|
12
|
+
# Types of responses sent by the listener after receiving a request:
|
|
13
|
+
# - 200 OK: OTP code was successfully retrieved
|
|
14
|
+
# - 204 No Content: If the request was an OPTIONS request
|
|
15
|
+
# - 400 Bad Request: If the request did not contain a query parameter `code`
|
|
16
|
+
# - 404 Not Found: The request was not to the root path
|
|
17
|
+
# - 405 Method Not Allowed: OTP code was not retrieved because the request was not a GET/OPTIONS request
|
|
18
|
+
#
|
|
19
|
+
# Example usage:
|
|
20
|
+
#
|
|
21
|
+
# server = TCPServer.new(0)
|
|
22
|
+
# otp = Gem::WebauthnListener.wait_for_otp_code("https://rubygems.example", server)
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
class Gem::WebauthnListener
|
|
26
|
+
attr_reader :host
|
|
27
|
+
|
|
28
|
+
def initialize(host)
|
|
29
|
+
@host = host
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.wait_for_otp_code(host, server)
|
|
33
|
+
new(host).fetch_otp_from_connection(server)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_otp_from_connection(server)
|
|
37
|
+
loop do
|
|
38
|
+
socket = server.accept
|
|
39
|
+
request_line = socket.gets
|
|
40
|
+
|
|
41
|
+
method, req_uri, _protocol = request_line.split(" ")
|
|
42
|
+
req_uri = URI.parse(req_uri)
|
|
43
|
+
|
|
44
|
+
responder = SocketResponder.new(socket)
|
|
45
|
+
|
|
46
|
+
unless root_path?(req_uri)
|
|
47
|
+
responder.send(NotFoundResponse.for(host))
|
|
48
|
+
raise Gem::WebauthnVerificationError, "Page at #{req_uri.path} not found."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
case method.upcase
|
|
52
|
+
when "OPTIONS"
|
|
53
|
+
responder.send(NoContentResponse.for(host))
|
|
54
|
+
next # will be GET
|
|
55
|
+
when "GET"
|
|
56
|
+
if otp = parse_otp_from_uri(req_uri)
|
|
57
|
+
responder.send(OkResponse.for(host))
|
|
58
|
+
return otp
|
|
59
|
+
end
|
|
60
|
+
responder.send(BadRequestResponse.for(host))
|
|
61
|
+
raise Gem::WebauthnVerificationError, "Did not receive OTP from #{host}."
|
|
62
|
+
else
|
|
63
|
+
responder.send(MethodNotAllowedResponse.for(host))
|
|
64
|
+
raise Gem::WebauthnVerificationError, "Invalid HTTP method #{method.upcase} received."
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def root_path?(uri)
|
|
72
|
+
uri.path == "/"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_otp_from_uri(uri)
|
|
76
|
+
require "cgi"
|
|
77
|
+
|
|
78
|
+
return if uri.query.nil?
|
|
79
|
+
CGI.parse(uri.query).dig("code", 0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class SocketResponder
|
|
83
|
+
def initialize(socket)
|
|
84
|
+
@socket = socket
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def send(response)
|
|
88
|
+
@socket.print response.to_s
|
|
89
|
+
@socket.close
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/rubygems.rb
CHANGED
data/rubygems-update.gemspec
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |s|
|
|
4
4
|
s.name = "rubygems-update"
|
|
5
|
-
s.version = "3.4.
|
|
5
|
+
s.version = "3.4.12"
|
|
6
6
|
s.authors = ["Jim Weirich", "Chad Fowler", "Eric Hodel", "Luis Lavena", "Aaron Patterson", "Samuel Giddins", "André Arko", "Evan Phoenix", "Hiroshi SHIBATA"]
|
|
7
7
|
s.email = ["", "", "drbrain@segment7.net", "luislavena@gmail.com", "aaron@tenderlovemaking.com", "segiddins@segiddins.me", "andre@arko.net", "evan@phx.io", "hsbt@ruby-lang.org"]
|
|
8
8
|
|
|
@@ -330,6 +330,8 @@ EOF
|
|
|
330
330
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
331
331
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
|
332
332
|
]
|
|
333
|
+
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
|
|
334
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
333
335
|
|
|
334
336
|
@otp_ui = Gem::MockGemUi.new "111111\n"
|
|
335
337
|
use_ui @otp_ui do
|
|
@@ -345,6 +347,8 @@ EOF
|
|
|
345
347
|
def test_otp_verified_failure
|
|
346
348
|
response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
347
349
|
@stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized")
|
|
350
|
+
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
|
|
351
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
348
352
|
|
|
349
353
|
@otp_ui = Gem::MockGemUi.new "111111\n"
|
|
350
354
|
use_ui @otp_ui do
|
|
@@ -357,6 +361,69 @@ EOF
|
|
|
357
361
|
assert_equal "111111", @stub_fetcher.last_request["OTP"]
|
|
358
362
|
end
|
|
359
363
|
|
|
364
|
+
def test_with_webauthn_enabled_success
|
|
365
|
+
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
366
|
+
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
367
|
+
response_success = "Owner added successfully."
|
|
368
|
+
port = 5678
|
|
369
|
+
server = TCPServer.new(port)
|
|
370
|
+
|
|
371
|
+
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
|
372
|
+
@stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [
|
|
373
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
374
|
+
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
TCPServer.stub(:new, server) do
|
|
378
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
|
379
|
+
use_ui @stub_ui do
|
|
380
|
+
@cmd.add_owners("freewill", ["user-new1@example.com"])
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
ensure
|
|
384
|
+
server.close
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
388
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output
|
|
389
|
+
assert_match "You are verified with a security device. You may close the browser window.", @stub_ui.output
|
|
390
|
+
assert_equal "Uvh6T57tkWuUnWYo", @stub_fetcher.last_request["OTP"]
|
|
391
|
+
assert_match response_success, @stub_ui.output
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def test_with_webauthn_enabled_failure
|
|
395
|
+
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
396
|
+
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
397
|
+
response_success = "Owner added successfully."
|
|
398
|
+
port = 5678
|
|
399
|
+
server = TCPServer.new(port)
|
|
400
|
+
raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
|
|
401
|
+
|
|
402
|
+
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
|
403
|
+
@stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [
|
|
404
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
405
|
+
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
TCPServer.stub(:new, server) do
|
|
409
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
|
|
410
|
+
use_ui @stub_ui do
|
|
411
|
+
@cmd.add_owners("freewill", ["user-new1@example.com"])
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
ensure
|
|
415
|
+
server.close
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
419
|
+
|
|
420
|
+
assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
|
|
421
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output
|
|
422
|
+
assert_match "ERROR: Security device verification failed: Something went wrong", @stub_ui.error
|
|
423
|
+
refute_match "You are verified with a security device. You may close the browser window.", @stub_ui.output
|
|
424
|
+
refute_match response_success, @stub_ui.output
|
|
425
|
+
end
|
|
426
|
+
|
|
360
427
|
def test_remove_owners_unathorized_api_key
|
|
361
428
|
response_forbidden = "The API key doesn't have access"
|
|
362
429
|
response_success = "Owner removed successfully."
|
|
@@ -391,6 +391,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|
|
391
391
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
392
392
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
|
393
393
|
]
|
|
394
|
+
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
|
|
395
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
394
396
|
|
|
395
397
|
@otp_ui = Gem::MockGemUi.new "111111\n"
|
|
396
398
|
use_ui @otp_ui do
|
|
@@ -406,6 +408,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|
|
406
408
|
def test_otp_verified_failure
|
|
407
409
|
response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
408
410
|
@fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized")
|
|
411
|
+
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] =
|
|
412
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
409
413
|
|
|
410
414
|
@otp_ui = Gem::MockGemUi.new "111111\n"
|
|
411
415
|
assert_raise Gem::MockGemUi::TermError do
|
|
@@ -420,6 +424,71 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|
|
420
424
|
assert_equal "111111", @fetcher.last_request["OTP"]
|
|
421
425
|
end
|
|
422
426
|
|
|
427
|
+
def test_with_webauthn_enabled_success
|
|
428
|
+
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
429
|
+
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
430
|
+
response_success = "Successfully registered gem: freewill (1.0.0)"
|
|
431
|
+
port = 5678
|
|
432
|
+
server = TCPServer.new(port)
|
|
433
|
+
|
|
434
|
+
@fetcher.data["#{Gem.host}/api/v1/gems"] = [
|
|
435
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
436
|
+
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
|
437
|
+
]
|
|
438
|
+
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
|
439
|
+
|
|
440
|
+
TCPServer.stub(:new, server) do
|
|
441
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
|
442
|
+
use_ui @ui do
|
|
443
|
+
@cmd.send_gem(@path)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
ensure
|
|
447
|
+
server.close
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
451
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output
|
|
452
|
+
assert_match "You are verified with a security device. You may close the browser window.", @ui.output
|
|
453
|
+
assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
|
|
454
|
+
assert_match response_success, @ui.output
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def test_with_webauthn_enabled_failure
|
|
458
|
+
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
459
|
+
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
460
|
+
response_success = "Successfully registered gem: freewill (1.0.0)"
|
|
461
|
+
port = 5678
|
|
462
|
+
server = TCPServer.new(port)
|
|
463
|
+
raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
|
|
464
|
+
|
|
465
|
+
@fetcher.data["#{Gem.host}/api/v1/gems"] = [
|
|
466
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
467
|
+
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
|
468
|
+
]
|
|
469
|
+
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
|
470
|
+
|
|
471
|
+
error = assert_raise Gem::MockGemUi::TermError do
|
|
472
|
+
TCPServer.stub(:new, server) do
|
|
473
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
|
|
474
|
+
use_ui @ui do
|
|
475
|
+
@cmd.send_gem(@path)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
ensure
|
|
479
|
+
server.close
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
assert_equal 1, error.exit_code
|
|
483
|
+
|
|
484
|
+
assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
|
|
485
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
486
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output
|
|
487
|
+
assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error
|
|
488
|
+
refute_match "You are verified with a security device. You may close the browser window.", @ui.output
|
|
489
|
+
refute_match response_success, @ui.output
|
|
490
|
+
end
|
|
491
|
+
|
|
423
492
|
def test_sending_gem_unathorized_api_key_with_mfa_enabled
|
|
424
493
|
response_mfa_enabled = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
425
494
|
response_forbidden = "The API key doesn't have access"
|
|
@@ -430,6 +499,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|
|
430
499
|
HTTPResponseFactory.create(body: response_forbidden, code: 403, msg: "Forbidden"),
|
|
431
500
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
|
432
501
|
]
|
|
502
|
+
@fetcher.data["#{@host}/api/v1/webauthn_verification"] =
|
|
503
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
433
504
|
|
|
434
505
|
@fetcher.data["#{@host}/api/v1/api_key"] = HTTPResponseFactory.create(body: "", code: 200, msg: "OK")
|
|
435
506
|
@cmd.instance_variable_set :@host, @host
|
|
@@ -470,6 +541,8 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|
|
470
541
|
@fetcher.data["#{@host}/api/v1/profile/me.yaml"] = [
|
|
471
542
|
HTTPResponseFactory.create(body: response_profile, code: 200, msg: "OK"),
|
|
472
543
|
]
|
|
544
|
+
@fetcher.data["#{@host}/api/v1/webauthn_verification"] =
|
|
545
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
473
546
|
|
|
474
547
|
@cmd.instance_variable_set :@scope, :push_rubygem
|
|
475
548
|
@cmd.options[:args] = [@path]
|
|
@@ -72,6 +72,9 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|
|
72
72
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
73
73
|
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
|
74
74
|
]
|
|
75
|
+
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
|
76
|
+
@fetcher.data[webauthn_uri] =
|
|
77
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
75
78
|
|
|
76
79
|
@cmd.options[:args] = %w[a]
|
|
77
80
|
@cmd.options[:added_platform] = true
|
|
@@ -93,6 +96,9 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|
|
93
96
|
response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
94
97
|
yank_uri = "http://example/api/v1/gems/yank"
|
|
95
98
|
@fetcher.data[yank_uri] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized")
|
|
99
|
+
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
|
100
|
+
@fetcher.data[webauthn_uri] =
|
|
101
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
96
102
|
|
|
97
103
|
@cmd.options[:args] = %w[a]
|
|
98
104
|
@cmd.options[:added_platform] = true
|
|
@@ -109,6 +115,84 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|
|
109
115
|
assert_equal "111111", @fetcher.last_request["OTP"]
|
|
110
116
|
end
|
|
111
117
|
|
|
118
|
+
def test_with_webauthn_enabled_success
|
|
119
|
+
webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
120
|
+
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
121
|
+
yank_uri = "http://example/api/v1/gems/yank"
|
|
122
|
+
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
|
123
|
+
port = 5678
|
|
124
|
+
server = TCPServer.new(port)
|
|
125
|
+
|
|
126
|
+
@fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
|
127
|
+
@fetcher.data[yank_uri] = [
|
|
128
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
129
|
+
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
@cmd.options[:args] = %w[a]
|
|
133
|
+
@cmd.options[:added_platform] = true
|
|
134
|
+
@cmd.options[:version] = req("= 1.0")
|
|
135
|
+
|
|
136
|
+
TCPServer.stub(:new, server) do
|
|
137
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
|
138
|
+
use_ui @ui do
|
|
139
|
+
@cmd.execute
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
ensure
|
|
143
|
+
server.close
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
147
|
+
assert_match %r{Yanking gem from http://example}, @ui.output
|
|
148
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output
|
|
149
|
+
assert_match "You are verified with a security device. You may close the browser window.", @ui.output
|
|
150
|
+
assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
|
|
151
|
+
assert_match "Successfully yanked", @ui.output
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_with_webauthn_enabled_failure
|
|
155
|
+
webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
156
|
+
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
|
157
|
+
yank_uri = "http://example/api/v1/gems/yank"
|
|
158
|
+
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
|
159
|
+
port = 5678
|
|
160
|
+
server = TCPServer.new(port)
|
|
161
|
+
raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
|
|
162
|
+
|
|
163
|
+
@fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
|
164
|
+
@fetcher.data[yank_uri] = [
|
|
165
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
|
166
|
+
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
@cmd.options[:args] = %w[a]
|
|
170
|
+
@cmd.options[:added_platform] = true
|
|
171
|
+
@cmd.options[:version] = req("= 1.0")
|
|
172
|
+
|
|
173
|
+
error = assert_raise Gem::MockGemUi::TermError do
|
|
174
|
+
TCPServer.stub(:new, server) do
|
|
175
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
|
|
176
|
+
use_ui @ui do
|
|
177
|
+
@cmd.execute
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
ensure
|
|
181
|
+
server.close
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
assert_equal 1, error.exit_code
|
|
185
|
+
|
|
186
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
187
|
+
|
|
188
|
+
assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
|
|
189
|
+
assert_match %r{Yanking gem from http://example}, @ui.output
|
|
190
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output
|
|
191
|
+
assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error
|
|
192
|
+
refute_match "You are verified with a security device. You may close the browser window.", @ui.output
|
|
193
|
+
refute_match "Successfully yanked", @ui.output
|
|
194
|
+
end
|
|
195
|
+
|
|
112
196
|
def test_execute_key
|
|
113
197
|
yank_uri = "http://example/api/v1/gems/yank"
|
|
114
198
|
@fetcher.data[yank_uri] = HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK")
|
|
@@ -230,10 +230,77 @@ class TestGemGemcutterUtilities < Gem::TestCase
|
|
|
230
230
|
assert_equal "111111", @fetcher.last_request["OTP"]
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
def
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
def test_sign_in_with_webauthn_enabled
|
|
234
|
+
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
235
|
+
response_fail = "You have enabled multifactor authentication"
|
|
236
|
+
api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
|
|
237
|
+
port = 5678
|
|
238
|
+
server = TCPServer.new(port)
|
|
239
|
+
|
|
240
|
+
TCPServer.stub(:new, server) do
|
|
241
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
|
242
|
+
util_sign_in(proc do
|
|
243
|
+
@call_count ||= 0
|
|
244
|
+
if (@call_count += 1).odd?
|
|
245
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
|
|
246
|
+
else
|
|
247
|
+
HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
|
|
248
|
+
end
|
|
249
|
+
end, nil, [], "", webauthn_verification_url)
|
|
250
|
+
end
|
|
251
|
+
ensure
|
|
252
|
+
server.close
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
256
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output
|
|
257
|
+
assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
|
|
258
|
+
assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def test_sign_in_with_webauthn_enabled_with_error
|
|
262
|
+
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
|
263
|
+
response_fail = "You have enabled multifactor authentication"
|
|
264
|
+
api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
|
|
265
|
+
port = 5678
|
|
266
|
+
server = TCPServer.new(port)
|
|
267
|
+
raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
|
|
268
|
+
|
|
269
|
+
error = assert_raise Gem::MockGemUi::TermError do
|
|
270
|
+
TCPServer.stub(:new, server) do
|
|
271
|
+
Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
|
|
272
|
+
util_sign_in(proc do
|
|
273
|
+
@call_count ||= 0
|
|
274
|
+
if (@call_count += 1).odd?
|
|
275
|
+
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
|
|
276
|
+
else
|
|
277
|
+
HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
|
|
278
|
+
end
|
|
279
|
+
end, nil, [], "", webauthn_verification_url)
|
|
280
|
+
end
|
|
281
|
+
ensure
|
|
282
|
+
server.close
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
assert_equal 1, error.exit_code
|
|
286
|
+
|
|
287
|
+
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
|
288
|
+
assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output
|
|
289
|
+
assert_match "ERROR: Security device verification failed: Something went wrong", @sign_in_ui.error
|
|
290
|
+
refute_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
|
|
291
|
+
refute_match "Signed in with API key:", @sign_in_ui.output
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def util_sign_in(response, host = nil, args = [], extra_input = "", webauthn_url = nil)
|
|
295
|
+
email = "you@example.com"
|
|
296
|
+
password = "secret"
|
|
297
|
+
profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
|
|
298
|
+
webauthn_response =
|
|
299
|
+
if webauthn_url
|
|
300
|
+
HTTPResponseFactory.create(body: webauthn_url, code: 200, msg: "OK")
|
|
301
|
+
else
|
|
302
|
+
HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
|
|
303
|
+
end
|
|
237
304
|
|
|
238
305
|
if host
|
|
239
306
|
ENV["RUBYGEMS_HOST"] = host
|
|
@@ -244,6 +311,7 @@ class TestGemGemcutterUtilities < Gem::TestCase
|
|
|
244
311
|
@fetcher = Gem::FakeFetcher.new
|
|
245
312
|
@fetcher.data["#{host}/api/v1/api_key"] = response
|
|
246
313
|
@fetcher.data["#{host}/api/v1/profile/me.yaml"] = profile_response
|
|
314
|
+
@fetcher.data["#{host}/api/v1/webauthn_verification"] = webauthn_response
|
|
247
315
|
Gem::RemoteFetcher.fetcher = @fetcher
|
|
248
316
|
|
|
249
317
|
@sign_in_ui = Gem::MockGemUi.new("#{email}\n#{password}\n\n\n\n\n\n\n\n\n" + extra_input)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "helper"
|
|
4
|
+
require "rubygems/webauthn_listener"
|
|
5
|
+
|
|
6
|
+
class WebauthnListenerTest < Gem::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
super
|
|
9
|
+
@server = TCPServer.new 0
|
|
10
|
+
@port = @server.addr[1].to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_wait_for_otp_code_get_follows_options
|
|
14
|
+
wait_for_otp_code
|
|
15
|
+
assert Gem::MockBrowser.options(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPNoContent
|
|
16
|
+
assert Gem::MockBrowser.get(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPOK
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_wait_for_otp_code_options_request
|
|
20
|
+
wait_for_otp_code
|
|
21
|
+
response = Gem::MockBrowser.options URI("http://localhost:#{@port}?code=xyz")
|
|
22
|
+
|
|
23
|
+
assert response.is_a? Net::HTTPNoContent
|
|
24
|
+
assert_equal Gem.host, response["access-control-allow-origin"]
|
|
25
|
+
assert_equal "POST", response["access-control-allow-methods"]
|
|
26
|
+
assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
|
|
27
|
+
assert_equal "close", response["Connection"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_wait_for_otp_code_get_request
|
|
31
|
+
wait_for_otp_code
|
|
32
|
+
response = Gem::MockBrowser.get URI("http://localhost:#{@port}?code=xyz")
|
|
33
|
+
|
|
34
|
+
assert response.is_a? Net::HTTPOK
|
|
35
|
+
assert_equal "text/plain", response["Content-Type"]
|
|
36
|
+
assert_equal "7", response["Content-Length"]
|
|
37
|
+
assert_equal Gem.host, response["access-control-allow-origin"]
|
|
38
|
+
assert_equal "POST", response["access-control-allow-methods"]
|
|
39
|
+
assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
|
|
40
|
+
assert_equal "close", response["Connection"]
|
|
41
|
+
assert_equal "success", response.body
|
|
42
|
+
|
|
43
|
+
@thread.join
|
|
44
|
+
assert_equal "xyz", @thread[:otp]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_wait_for_otp_code_invalid_post_req_method
|
|
48
|
+
wait_for_otp_code_expect_error_with_message("Security device verification failed: Invalid HTTP method POST received.")
|
|
49
|
+
response = Gem::MockBrowser.post URI("http://localhost:#{@port}?code=xyz")
|
|
50
|
+
|
|
51
|
+
assert response
|
|
52
|
+
assert response.is_a? Net::HTTPMethodNotAllowed
|
|
53
|
+
assert_equal "GET, OPTIONS", response["allow"]
|
|
54
|
+
assert_equal "close", response["Connection"]
|
|
55
|
+
|
|
56
|
+
@thread.join
|
|
57
|
+
assert_nil @thread[:otp]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_wait_for_otp_code_incorrect_path
|
|
61
|
+
wait_for_otp_code_expect_error_with_message("Security device verification failed: Page at /path not found.")
|
|
62
|
+
response = Gem::MockBrowser.post URI("http://localhost:#{@port}/path?code=xyz")
|
|
63
|
+
|
|
64
|
+
assert response.is_a? Net::HTTPNotFound
|
|
65
|
+
assert_equal "close", response["Connection"]
|
|
66
|
+
|
|
67
|
+
@thread.join
|
|
68
|
+
assert_nil @thread[:otp]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_wait_for_otp_code_no_params_response
|
|
72
|
+
wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
|
|
73
|
+
response = Gem::MockBrowser.get URI("http://localhost:#{@port}")
|
|
74
|
+
|
|
75
|
+
assert response.is_a? Net::HTTPBadRequest
|
|
76
|
+
assert_equal "text/plain", response["Content-Type"]
|
|
77
|
+
assert_equal "22", response["Content-Length"]
|
|
78
|
+
assert_equal "close", response["Connection"]
|
|
79
|
+
assert_equal "missing code parameter", response.body
|
|
80
|
+
|
|
81
|
+
@thread.join
|
|
82
|
+
assert_nil @thread[:otp]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_wait_for_otp_code_incorrect_params
|
|
86
|
+
wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
|
|
87
|
+
response = Gem::MockBrowser.get URI("http://localhost:#{@port}?param=xyz")
|
|
88
|
+
|
|
89
|
+
assert response.is_a? Net::HTTPBadRequest
|
|
90
|
+
assert_equal "text/plain", response["Content-Type"]
|
|
91
|
+
assert_equal "22", response["Content-Length"]
|
|
92
|
+
assert_equal "close", response["Connection"]
|
|
93
|
+
assert_equal "missing code parameter", response.body
|
|
94
|
+
|
|
95
|
+
@thread.join
|
|
96
|
+
assert_nil @thread[:otp]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def wait_for_otp_code
|
|
102
|
+
@thread = Thread.new do
|
|
103
|
+
Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
|
|
104
|
+
end
|
|
105
|
+
@thread.abort_on_exception = true
|
|
106
|
+
@thread.report_on_exception = false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def wait_for_otp_code_expect_error_with_message(message)
|
|
110
|
+
@thread = Thread.new do
|
|
111
|
+
error = assert_raise Gem::WebauthnVerificationError do
|
|
112
|
+
Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
assert_equal message, error.message
|
|
116
|
+
end
|
|
117
|
+
@thread.abort_on_exception = true
|
|
118
|
+
@thread.report_on_exception = false
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "helper"
|
|
4
|
+
require "rubygems/webauthn_listener/response"
|
|
5
|
+
|
|
6
|
+
class WebauthnListenerResponseTest < Gem::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
super
|
|
9
|
+
@host = "rubygems.example"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_ok_response_to_s
|
|
13
|
+
to_s = Gem::WebauthnListener::OkResponse.new(@host).to_s
|
|
14
|
+
|
|
15
|
+
expected_to_s = <<~RESPONSE
|
|
16
|
+
HTTP/1.1 200 OK\r
|
|
17
|
+
connection: close\r
|
|
18
|
+
access-control-allow-origin: rubygems.example\r
|
|
19
|
+
access-control-allow-methods: POST\r
|
|
20
|
+
access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
|
|
21
|
+
content-type: text/plain\r
|
|
22
|
+
content-length: 7\r
|
|
23
|
+
\r
|
|
24
|
+
success
|
|
25
|
+
RESPONSE
|
|
26
|
+
|
|
27
|
+
assert_equal expected_to_s, to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_no_to_s_response_to_s
|
|
31
|
+
to_s = Gem::WebauthnListener::NoContentResponse.new(@host).to_s
|
|
32
|
+
|
|
33
|
+
expected_to_s = <<~RESPONSE
|
|
34
|
+
HTTP/1.1 204 No Content\r
|
|
35
|
+
connection: close\r
|
|
36
|
+
access-control-allow-origin: rubygems.example\r
|
|
37
|
+
access-control-allow-methods: POST\r
|
|
38
|
+
access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
|
|
39
|
+
\r
|
|
40
|
+
RESPONSE
|
|
41
|
+
|
|
42
|
+
assert_equal expected_to_s, to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_method_not_allowed_response_to_s
|
|
46
|
+
to_s = Gem::WebauthnListener::MethodNotAllowedResponse.new(@host).to_s
|
|
47
|
+
|
|
48
|
+
expected_to_s = <<~RESPONSE
|
|
49
|
+
HTTP/1.1 405 Method Not Allowed\r
|
|
50
|
+
connection: close\r
|
|
51
|
+
access-control-allow-origin: rubygems.example\r
|
|
52
|
+
access-control-allow-methods: POST\r
|
|
53
|
+
access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
|
|
54
|
+
allow: GET, OPTIONS\r
|
|
55
|
+
\r
|
|
56
|
+
RESPONSE
|
|
57
|
+
|
|
58
|
+
assert_equal expected_to_s, to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_method_not_found_response_to_s
|
|
62
|
+
to_s = Gem::WebauthnListener::NotFoundResponse.new(@host).to_s
|
|
63
|
+
|
|
64
|
+
expected_to_s = <<~RESPONSE
|
|
65
|
+
HTTP/1.1 404 Not Found\r
|
|
66
|
+
connection: close\r
|
|
67
|
+
access-control-allow-origin: rubygems.example\r
|
|
68
|
+
access-control-allow-methods: POST\r
|
|
69
|
+
access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
|
|
70
|
+
\r
|
|
71
|
+
RESPONSE
|
|
72
|
+
|
|
73
|
+
assert_equal expected_to_s, to_s
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_bad_request_response_to_s
|
|
77
|
+
to_s = Gem::WebauthnListener::BadRequestResponse.new(@host).to_s
|
|
78
|
+
|
|
79
|
+
expected_to_s = <<~RESPONSE
|
|
80
|
+
HTTP/1.1 400 Bad Request\r
|
|
81
|
+
connection: close\r
|
|
82
|
+
access-control-allow-origin: rubygems.example\r
|
|
83
|
+
access-control-allow-methods: POST\r
|
|
84
|
+
access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
|
|
85
|
+
content-type: text/plain\r
|
|
86
|
+
content-length: 22\r
|
|
87
|
+
\r
|
|
88
|
+
missing code parameter
|
|
89
|
+
RESPONSE
|
|
90
|
+
|
|
91
|
+
assert_equal expected_to_s, to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
data/test/rubygems/utilities.rb
CHANGED
|
@@ -186,6 +186,41 @@ class Gem::HTTPResponseFactory
|
|
|
186
186
|
end
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
+
##
|
|
190
|
+
# A Gem::MockBrowser is used in tests to mock a browser in that it can
|
|
191
|
+
# send HTTP requests to the defined URI.
|
|
192
|
+
#
|
|
193
|
+
# Example:
|
|
194
|
+
#
|
|
195
|
+
# # Sends a get request to http://localhost:5678
|
|
196
|
+
# Gem::MockBrowser.get URI("http://localhost:5678")
|
|
197
|
+
#
|
|
198
|
+
# See RubyGems' tests for more examples of MockBrowser.
|
|
199
|
+
#
|
|
200
|
+
|
|
201
|
+
class Gem::MockBrowser
|
|
202
|
+
def self.options(uri)
|
|
203
|
+
options = Net::HTTP::Options.new(uri)
|
|
204
|
+
Net::HTTP.start(uri.hostname, uri.port) do |http|
|
|
205
|
+
http.request(options)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def self.get(uri)
|
|
210
|
+
get = Net::HTTP::Get.new(uri)
|
|
211
|
+
Net::HTTP.start(uri.hostname, uri.port) do |http|
|
|
212
|
+
http.request(get)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def self.post(uri)
|
|
217
|
+
post = Net::HTTP::Post.new(uri)
|
|
218
|
+
Net::HTTP.start(uri.hostname, uri.port) do |http|
|
|
219
|
+
http.request(post)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
189
224
|
# :stopdoc:
|
|
190
225
|
class Gem::RemoteFetcher
|
|
191
226
|
def self.fetcher=(fetcher)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubygems-update
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.4.
|
|
4
|
+
version: 3.4.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jim Weirich
|
|
@@ -16,7 +16,7 @@ authors:
|
|
|
16
16
|
autorequire:
|
|
17
17
|
bindir: bin
|
|
18
18
|
cert_chain: []
|
|
19
|
-
date: 2023-04-
|
|
19
|
+
date: 2023-04-11 00:00:00.000000000 Z
|
|
20
20
|
dependencies: []
|
|
21
21
|
description: |-
|
|
22
22
|
A package (also known as a library) contains a set of functionality
|
|
@@ -596,6 +596,8 @@ files:
|
|
|
596
596
|
- lib/rubygems/validator.rb
|
|
597
597
|
- lib/rubygems/version.rb
|
|
598
598
|
- lib/rubygems/version_option.rb
|
|
599
|
+
- lib/rubygems/webauthn_listener.rb
|
|
600
|
+
- lib/rubygems/webauthn_listener/response.rb
|
|
599
601
|
- rubygems-update.gemspec
|
|
600
602
|
- setup.rb
|
|
601
603
|
- test/rubygems/alternate_cert.pem
|
|
@@ -809,6 +811,8 @@ files:
|
|
|
809
811
|
- test/rubygems/test_remote_fetch_error.rb
|
|
810
812
|
- test/rubygems/test_require.rb
|
|
811
813
|
- test/rubygems/test_rubygems.rb
|
|
814
|
+
- test/rubygems/test_webauthn_listener.rb
|
|
815
|
+
- test/rubygems/test_webauthn_listener_response.rb
|
|
812
816
|
- test/rubygems/utilities.rb
|
|
813
817
|
- test/rubygems/wrong_key_cert.pem
|
|
814
818
|
- test/rubygems/wrong_key_cert_32.pem
|
|
@@ -836,7 +840,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
836
840
|
- !ruby/object:Gem::Version
|
|
837
841
|
version: '0'
|
|
838
842
|
requirements: []
|
|
839
|
-
rubygems_version: 3.4.
|
|
843
|
+
rubygems_version: 3.4.12
|
|
840
844
|
signing_key:
|
|
841
845
|
specification_version: 4
|
|
842
846
|
summary: RubyGems is a package management framework for Ruby.
|