Module: Gem::GemcutterUtilities
- Includes:
- Text
- Included in:
- Commands::OwnerCommand, Commands::PushCommand, Commands::SigninCommand, Commands::YankCommand, WebauthnPoller
- Defined in:
- lib/rubygems/gemcutter_utilities.rb,
lib/rubygems/gemcutter_utilities/webauthn_poller.rb,
lib/rubygems/gemcutter_utilities/webauthn_listener.rb,
lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb
Overview
The WebauthnListener Response class is used by the WebauthnListener to create responses to be sent to the Gem host. It creates a Gem::Net::HTTPResponse instance when initialized and can be converted to the appropriate format to be sent by a socket using ‘to_s`. Gem::Net::HTTPResponse instances cannot be directly sent over a socket.
Types of response classes:
- OkResponse
- NoContentResponse
- BadRequestResponse
- NotFoundResponse
- MethodNotAllowedResponse
Example usage:
server = TCPServer.new(0)
socket = server.accept
response = OkResponse.for("https://rubygems.example")
socket.print response.to_s
socket.close
Defined Under Namespace
Classes: WebauthnListener, WebauthnPoller
Constant Summary collapse
- ERROR_CODE =
1
- API_SCOPES =
[:index_rubygems, :push_rubygem, :yank_rubygem, :add_owner, :remove_owner, :access_webhooks].freeze
- EXCLUSIVELY_API_SCOPES =
[:show_dashboard].freeze
Instance Attribute Summary collapse
-
#host ⇒ Object
The host to connect to either from the RUBYGEMS_HOST environment variable or from the user’s configuration.
-
#scope ⇒ Object
writeonly
Sets the attribute scope.
Instance Method Summary collapse
-
#add_key_option ⇒ Object
Add the –key option.
-
#add_otp_option ⇒ Object
Add the –otp option.
-
#api_key ⇒ Object
The API key from the command options or from the user’s configuration.
- #mfa_unauthorized?(response) ⇒ Boolean
-
#otp ⇒ Object
The OTP code from the command options or from the user’s configuration.
-
#rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block) ⇒ Object
Creates an RubyGems API to
host
andpath
with the given HTTPmethod
. -
#set_api_key(host, key) ⇒ Object
Returns true when the user has enabled multifactor authentication from
response
text and no otp provided by options. -
#sign_in(sign_in_host = nil, scope: nil) ⇒ Object
Signs in with the RubyGems API at
sign_in_host
and sets the rubygems API key. - #update_scope(scope) ⇒ Object
-
#verify_api_key(key) ⇒ Object
Retrieves the pre-configured API key
key
or terminates interaction with an error. - #webauthn_enabled? ⇒ Boolean
-
#with_response(response, error_prefix = nil) ⇒ Object
If
response
is an HTTP Success (2XX) response, yields the response if a block was given or shows the response body to the user.
Methods included from Text
#clean_text, #format_text, #levenshtein_distance, #min3, #truncate_text
Instance Attribute Details
#host ⇒ Object
The host to connect to either from the RUBYGEMS_HOST environment variable or from the user’s configuration
73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 73 def host configured_host = Gem.host unless Gem.configuration.disable_default_gem_server @host ||= begin env_rubygems_host = ENV["RUBYGEMS_HOST"] env_rubygems_host = nil if env_rubygems_host&.empty? env_rubygems_host || configured_host end end |
#scope=(value) ⇒ Object (writeonly)
Sets the attribute scope
19 20 21 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 19 def scope=(value) @scope = value end |
Instance Method Details
#add_key_option ⇒ Object
Add the –key option
24 25 26 27 28 29 30 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 24 def add_key_option add_option("-k", "--key KEYNAME", Symbol, "Use the given API key", "from #{Gem.configuration.credentials_path}") do |value,| [:key] = value end end |
#add_otp_option ⇒ Object
Add the –otp option
35 36 37 38 39 40 41 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 35 def add_otp_option add_option("--otp CODE", "Digit code for multifactor authentication", "You can also use the environment variable GEM_HOST_OTP_CODE") do |value, | [:otp] = value end end |
#api_key ⇒ Object
The API key from the command options or from the user’s configuration.
46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 46 def api_key if ENV["GEM_HOST_API_KEY"] ENV["GEM_HOST_API_KEY"] elsif [:key] verify_api_key [:key] elsif Gem.configuration.api_keys.key?(host) Gem.configuration.api_keys[host] else Gem.configuration.rubygems_api_key end end |
#mfa_unauthorized?(response) ⇒ Boolean
126 127 128 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 126 def (response) response.is_a?(Gem::Net::HTTPUnauthorized) && response.body.start_with?("You have enabled multifactor authentication") end |
#otp ⇒ Object
The OTP code from the command options or from the user’s configuration.
61 62 63 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 61 def otp [:otp] || ENV["GEM_HOST_OTP_CODE"] end |
#rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block) ⇒ Object
Creates an RubyGems API to host
and path
with the given HTTP method
.
If allowed_push_host
metadata is present, then it will only allow that host.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 91 def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block) require_relative "vendored_net_http" self.host = host if host unless self.host alert_error "You must specify a gem server" terminate_interaction(ERROR_CODE) end if allowed_push_host allowed_host_uri = Gem::URI.parse(allowed_push_host) host_uri = Gem::URI.parse(self.host) unless (host_uri.scheme == allowed_host_uri.scheme) && (host_uri.host == allowed_host_uri.host) alert_error "#{self.host.inspect} is not allowed by the gemspec, which only allows #{allowed_push_host.inspect}" terminate_interaction(ERROR_CODE) end end uri = Gem::URI.parse "#{self.host}/#{path}" response = request_with_otp(method, uri, &block) if (response) fetch_otp(credentials) response = request_with_otp(method, uri, &block) end if api_key_forbidden?(response) update_scope(scope) request_with_otp(method, uri, &block) else response end end |
#set_api_key(host, key) ⇒ Object
Returns true when the user has enabled multifactor authentication from response
text and no otp provided by options.
239 240 241 242 243 244 245 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 239 def set_api_key(host, key) if default_host? Gem.configuration.rubygems_api_key = key else Gem.configuration.set_api_key host, key end end |
#sign_in(sign_in_host = nil, scope: nil) ⇒ Object
Signs in with the RubyGems API at sign_in_host
and sets the rubygems API key.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 155 def sign_in(sign_in_host = nil, scope: nil) sign_in_host ||= host return if api_key pretty_host = pretty_host(sign_in_host) say "Enter your #{pretty_host} credentials." say "Don't have an account yet? " \ "Create one at #{sign_in_host}/sign_up" identifier = ask "Username/email: " password = ask_for_password " Password: " say "\n" key_name = get_key_name(scope) scope_params = get_scope_params(scope) profile = get_user_profile(identifier, password) mfa_params = get_mfa_params(profile) all_params = scope_params.merge(mfa_params) warning = profile["warning"] credentials = { identifier: identifier, password: password } say "#{warning}\n" if warning response = rubygems_api_request(:post, "api/v1/api_key", sign_in_host, credentials: credentials, scope: scope) do |request| request.basic_auth identifier, password request.body = Gem::URI.encode_www_form({ name: key_name }.merge(all_params)) end with_response response do |resp| say "Signed in with API key: #{key_name}." set_api_key host, resp.body end end |
#update_scope(scope) ⇒ Object
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 130 def update_scope(scope) sign_in_host = host pretty_host = pretty_host(sign_in_host) update_scope_params = { scope => true } say "The existing key doesn't have access of #{scope} on #{pretty_host}. Please sign in to update access." identifier = ask "Username/email: " password = ask_for_password " Password: " response = rubygems_api_request(:put, "api/v1/api_key", sign_in_host, scope: scope) do |request| request.basic_auth identifier, password request.body = Gem::URI.encode_www_form({ api_key: api_key }.merge(update_scope_params)) end with_response response do |_resp| say "Added #{scope} scope to the existing API key" end end |
#verify_api_key(key) ⇒ Object
Retrieves the pre-configured API key key
or terminates interaction with an error.
195 196 197 198 199 200 201 202 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 195 def verify_api_key(key) if Gem.configuration.api_keys.key? key Gem.configuration.api_keys[key] else alert_error "No such API key. Please add it to your configuration (done automatically on initial `gem push`)." terminate_interaction(ERROR_CODE) end end |
#webauthn_enabled? ⇒ Boolean
65 66 67 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 65 def webauthn_enabled? [:webauthn] end |
#with_response(response, error_prefix = nil) ⇒ Object
If response
is an HTTP Success (2XX) response, yields the response if a block was given or shows the response body to the user.
If the response was not successful, shows an error to the user including the error_prefix
and the response body. If the response was a permanent redirect, shows an error to the user including the redirect location.
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/rubygems/gemcutter_utilities.rb', line 212 def with_response(response, error_prefix = nil) case response when Gem::Net::HTTPSuccess then if block_given? yield response else say clean_text(response.body) end when Gem::Net::HTTPPermanentRedirect, Gem::Net::HTTPRedirection then = "The request has redirected permanently to #{response["location"]}. Please check your defined push host URL." = "#{error_prefix}: #{}" if error_prefix say clean_text() terminate_interaction(ERROR_CODE) else = response.body = "#{error_prefix}: #{}" if error_prefix say clean_text() terminate_interaction(ERROR_CODE) end end |