rubygems-update 3.4.11 → 3.4.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b282bb00b57945e0f0754620e2748128cd209c4babfd48a99b65cd4b3a49453
4
- data.tar.gz: c0eff9cc6ff4ab163302bd9157a18ba0da4ff1bbf3a3c739f5e2cae74b5d82c5
3
+ metadata.gz: 71052fdbd665d26b95bc8c6421c6d1b70434b4b9feea468c6d9ced57bca5e39c
4
+ data.tar.gz: 597a3e2df64060ed8f118cec9538463d372cb69b5da29e105202ade63635d32b
5
5
  SHA512:
6
- metadata.gz: a8d6983ca3a5bc09424c7182ad4d6467bd7f3d4c464c2145e2948b5a3a8bdc2d9beebfc5a1dccbc29d214fe957fee57493361609f93828f6c90ffa6a49ff6e5a
7
- data.tar.gz: f625b38ddc0dfa4403f2e643a12b300f94fc373c2ba108f21c5be468229aef1bec8f6e31a49061284cd102d22cd15e53b26a71f9ca8af983496317659a4b696a
6
+ metadata.gz: f9de905f5b7850a006d8f1812fed59c13fef02018e58d0a247c83de76df2fbd530c547a1733586cfd96a28740d7793e1f179f9f245ea7bf8073e43bdfb1c2393
7
+ data.tar.gz: cd73b0615a21d38d59988a6619d5196fcc2e232e0bd24861162bf7db090e353baae7ea23dde77ffc8a86f4f81071c296578956b8447e0528383d6073919522e3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # 3.4.13 / 2023-05-09
2
+
3
+ ## Enhancements:
4
+
5
+ * Installs bundler 2.4.13 as a default gem.
6
+
7
+ # 3.4.12 / 2023-04-11
8
+
9
+ ## Enhancements:
10
+
11
+ * [Experimental] Add WebAuthn Support to the CLI. Pull request
12
+ [#6560](https://github.com/rubygems/rubygems/pull/6560) by jenshenny
13
+ * Installs bundler 2.4.12 as a default gem.
14
+
1
15
  # 3.4.11 / 2023-04-10
2
16
 
3
17
  ## Enhancements:
data/Manifest.txt CHANGED
@@ -180,6 +180,7 @@ bundler/lib/bundler/rubygems_ext.rb
180
180
  bundler/lib/bundler/rubygems_gem_installer.rb
181
181
  bundler/lib/bundler/rubygems_integration.rb
182
182
  bundler/lib/bundler/runtime.rb
183
+ bundler/lib/bundler/safe_marshal.rb
183
184
  bundler/lib/bundler/self_manager.rb
184
185
  bundler/lib/bundler/settings.rb
185
186
  bundler/lib/bundler/settings/validator.rb
@@ -540,6 +541,8 @@ lib/rubygems/util/list.rb
540
541
  lib/rubygems/validator.rb
541
542
  lib/rubygems/version.rb
542
543
  lib/rubygems/version_option.rb
544
+ lib/rubygems/webauthn_listener.rb
545
+ lib/rubygems/webauthn_listener/response.rb
543
546
  rubygems-update.gemspec
544
547
  setup.rb
545
548
  test/rubygems/alternate_cert.pem
@@ -753,6 +756,8 @@ test/rubygems/test_project_sanity.rb
753
756
  test/rubygems/test_remote_fetch_error.rb
754
757
  test/rubygems/test_require.rb
755
758
  test/rubygems/test_rubygems.rb
759
+ test/rubygems/test_webauthn_listener.rb
760
+ test/rubygems/test_webauthn_listener_response.rb
756
761
  test/rubygems/utilities.rb
757
762
  test/rubygems/wrong_key_cert.pem
758
763
  test/rubygems/wrong_key_cert_32.pem
data/bundler/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # 2.4.13 (May 9, 2023)
2
+
3
+ ## Bug fixes:
4
+
5
+ - Fix unexpected fallbacks to full index by adding FalseClass and Time to the SafeMarshal list [#6655](https://github.com/rubygems/rubygems/pull/6655)
6
+
7
+ ## Documentation:
8
+
9
+ - Fix broken hyperlinks in bundle cache documentation [#6606](https://github.com/rubygems/rubygems/pull/6606)
10
+
11
+ # 2.4.12 (April 11, 2023)
12
+
13
+ ## Enhancements:
14
+
15
+ - Remove reference to `pry` gem from generated `bin/console` file [#6515](https://github.com/rubygems/rubygems/pull/6515)
16
+
1
17
  # 2.4.11 (April 10, 2023)
2
18
 
3
19
  ## Security:
@@ -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-10".freeze
8
- @git_commit_sha = "be1d1b4623".freeze
7
+ @built_at = "2023-05-10".freeze
8
+ @git_commit_sha = "26eb456c6c".freeze
9
9
  @release = true
10
10
  # end ivars
11
11
 
@@ -13,7 +13,7 @@
13
13
  alias: \fBpackage\fR, \fBpack\fR
14
14
  .
15
15
  .SH "DESCRIPTION"
16
- Copy all of the \fB\.gem\fR files needed to run the application into the \fBvendor/cache\fR directory\. In the future, when running [bundle install(1)][bundle\-install], use the gems in the cache in preference to the ones on \fBrubygems\.org\fR\.
16
+ Copy all of the \fB\.gem\fR files needed to run the application into the \fBvendor/cache\fR directory\. In the future, when running \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR, use the gems in the cache in preference to the ones on \fBrubygems\.org\fR\.
17
17
  .
18
18
  .SH "GIT AND PATH GEMS"
19
19
  The \fBbundle cache\fR command can also package \fB:git\fR and \fB:path\fR dependencies besides \.gem files\. This needs to be explicitly enabled via the \fB\-\-all\fR option\. Once used, the \fB\-\-all\fR option will be remembered\.
@@ -22,7 +22,7 @@ The \fBbundle cache\fR command can also package \fB:git\fR and \fB:path\fR depen
22
22
  When using gems that have different packages for different platforms, Bundler supports caching of gems for other platforms where the Gemfile has been resolved (i\.e\. present in the lockfile) in \fBvendor/cache\fR\. This needs to be enabled via the \fB\-\-all\-platforms\fR option\. This setting will be remembered in your local bundler configuration\.
23
23
  .
24
24
  .SH "REMOTE FETCHING"
25
- By default, if you run \fBbundle install(1)\fR](bundle\-install\.1\.html) after running bundle cache(1) \fIbundle\-cache\.1\.html\fR, bundler will still connect to \fBrubygems\.org\fR to check whether a platform\-specific gem exists for any of the gems in \fBvendor/cache\fR\.
25
+ By default, if you run \fBbundle install(1)\fR \fIbundle\-install\.1\.html\fR after running bundle cache(1) \fIbundle\-cache\.1\.html\fR, bundler will still connect to \fBrubygems\.org\fR to check whether a platform\-specific gem exists for any of the gems in \fBvendor/cache\fR\.
26
26
  .
27
27
  .P
28
28
  For instance, consider this Gemfile(5):
@@ -10,7 +10,7 @@ alias: `package`, `pack`
10
10
  ## DESCRIPTION
11
11
 
12
12
  Copy all of the `.gem` files needed to run the application into the
13
- `vendor/cache` directory. In the future, when running [bundle install(1)][bundle-install],
13
+ `vendor/cache` directory. In the future, when running [`bundle install(1)`](bundle-install.1.html),
14
14
  use the gems in the cache in preference to the ones on `rubygems.org`.
15
15
 
16
16
  ## GIT AND PATH GEMS
@@ -29,7 +29,7 @@ bundler configuration.
29
29
 
30
30
  ## REMOTE FETCHING
31
31
 
32
- By default, if you run `bundle install(1)`](bundle-install.1.html) after running
32
+ By default, if you run [`bundle install(1)`](bundle-install.1.html) after running
33
33
  [bundle cache(1)](bundle-cache.1.html), bundler will still connect to `rubygems.org`
34
34
  to check whether a platform-specific gem exists for any of the gems
35
35
  in `vendor/cache`.
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module SafeMarshal
5
+ ALLOWED_CLASSES = [
6
+ Array,
7
+ FalseClass,
8
+ Gem::Specification,
9
+ Gem::Version,
10
+ Hash,
11
+ String,
12
+ Symbol,
13
+ Time,
14
+ TrueClass,
15
+ ].freeze
16
+
17
+ ERROR = "Unexpected class %s present in marshaled data. Only %s are allowed."
18
+
19
+ PROC = proc do |object|
20
+ object.tap do
21
+ unless ALLOWED_CLASSES.include?(object.class)
22
+ raise TypeError, format(ERROR, object.class, ALLOWED_CLASSES.join(", "))
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.proc
28
+ PROC
29
+ end
30
+ end
31
+ end
@@ -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__)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  module Bundler
4
- VERSION = "2.4.11".freeze
4
+ VERSION = "2.4.13".freeze
5
5
 
6
6
  def self.bundler_major_version
7
7
  @bundler_major_version ||= VERSION.split(".").first.to_i
@@ -39,16 +39,6 @@ module Bundler
39
39
  environment_preserver.replace_with_backup
40
40
  SUDO_MUTEX = Thread::Mutex.new
41
41
 
42
- SAFE_MARSHAL_CLASSES = [Symbol, TrueClass, String, Array, Hash, Gem::Version, Gem::Specification].freeze
43
- SAFE_MARSHAL_ERROR = "Unexpected class %s present in marshaled data. Only %s are allowed."
44
- SAFE_MARSHAL_PROC = proc do |object|
45
- object.tap do
46
- unless SAFE_MARSHAL_CLASSES.include?(object.class)
47
- raise TypeError, format(SAFE_MARSHAL_ERROR, object.class, SAFE_MARSHAL_CLASSES.join(", "))
48
- end
49
- end
50
- end
51
-
52
42
  autoload :Definition, File.expand_path("bundler/definition", __dir__)
53
43
  autoload :Dependency, File.expand_path("bundler/dependency", __dir__)
54
44
  autoload :Deprecate, File.expand_path("bundler/deprecate", __dir__)
@@ -86,6 +76,7 @@ module Bundler
86
76
  autoload :UI, File.expand_path("bundler/ui", __dir__)
87
77
  autoload :URICredentialsFilter, File.expand_path("bundler/uri_credentials_filter", __dir__)
88
78
  autoload :URINormalizer, File.expand_path("bundler/uri_normalizer", __dir__)
79
+ autoload :SafeMarshal, File.expand_path("bundler/safe_marshal", __dir__)
89
80
 
90
81
  class << self
91
82
  def configure
@@ -523,7 +514,7 @@ EOF
523
514
  end
524
515
 
525
516
  def safe_load_marshal(data)
526
- load_marshal(data, :marshal_proc => SAFE_MARSHAL_PROC)
517
+ load_marshal(data, :marshal_proc => SafeMarshal.proc)
527
518
  end
528
519
 
529
520
  def load_gemspec(file, validate = false)
@@ -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
- # ignore
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
@@ -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
- ask_otp
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 ask_otp
254
- say "You have enabled multi-factor authentication. Please enter OTP code."
255
- options[:otp] = ask "Code: "
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
@@ -8,7 +8,7 @@
8
8
  require "rbconfig"
9
9
 
10
10
  module Gem
11
- VERSION = "3.4.11"
11
+ VERSION = "3.4.13"
12
12
  end
13
13
 
14
14
  # Must be first since it unloads the prelude from 1.9.2
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "rubygems-update"
5
- s.version = "3.4.11"
5
+ s.version = "3.4.13"
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 util_sign_in(response, host = nil, args = [], extra_input = "")
234
- email = "you@example.com"
235
- password = "secret"
236
- profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
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
@@ -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.11
4
+ version: 3.4.13
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-10 00:00:00.000000000 Z
19
+ date: 2023-05-10 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
@@ -236,6 +236,7 @@ files:
236
236
  - bundler/lib/bundler/rubygems_gem_installer.rb
237
237
  - bundler/lib/bundler/rubygems_integration.rb
238
238
  - bundler/lib/bundler/runtime.rb
239
+ - bundler/lib/bundler/safe_marshal.rb
239
240
  - bundler/lib/bundler/self_manager.rb
240
241
  - bundler/lib/bundler/settings.rb
241
242
  - bundler/lib/bundler/settings/validator.rb
@@ -596,6 +597,8 @@ files:
596
597
  - lib/rubygems/validator.rb
597
598
  - lib/rubygems/version.rb
598
599
  - lib/rubygems/version_option.rb
600
+ - lib/rubygems/webauthn_listener.rb
601
+ - lib/rubygems/webauthn_listener/response.rb
599
602
  - rubygems-update.gemspec
600
603
  - setup.rb
601
604
  - test/rubygems/alternate_cert.pem
@@ -809,6 +812,8 @@ files:
809
812
  - test/rubygems/test_remote_fetch_error.rb
810
813
  - test/rubygems/test_require.rb
811
814
  - test/rubygems/test_rubygems.rb
815
+ - test/rubygems/test_webauthn_listener.rb
816
+ - test/rubygems/test_webauthn_listener_response.rb
812
817
  - test/rubygems/utilities.rb
813
818
  - test/rubygems/wrong_key_cert.pem
814
819
  - test/rubygems/wrong_key_cert_32.pem
@@ -836,7 +841,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
836
841
  - !ruby/object:Gem::Version
837
842
  version: '0'
838
843
  requirements: []
839
- rubygems_version: 3.4.11
844
+ rubygems_version: 3.4.13
840
845
  signing_key:
841
846
  specification_version: 4
842
847
  summary: RubyGems is a package management framework for Ruby.