modul Gem::GemcutterUtilities
Hilfsmethoden für die Verwendung der RubyGems API.
Die Klasse WebauthnListener ruft ein OTP ab, nachdem ein Benutzer erfolgreich eine WebAuthn-Authentifizierung mit dem Gem-Host durchgeführt hat. Eine Instanz öffnet einen Socket unter Verwendung der übergebenen TCPServer-Instanz und wartet auf eine Anfrage vom Gem-Host. Die Anfrage sollte eine GET-Anfrage an den Stamm-Pfad sein und den OTP-Code als Query-Parameter „code" enthalten. Der Listener gibt den Code zurück, der als OTP für API-Anfragen verwendet wird.
Arten von Antworten, die vom Listener nach Erhalt einer Anfrage gesendet werden
- 200 OK: OTP code was successfully retrieved - 204 No Content: If the request was an OPTIONS request - 400 Bad Request: If the request did not contain a query parameter `code` - 404 Not Found: The request was not to the root path - 405 Method Not Allowed: OTP code was not retrieved because the request was not a GET/OPTIONS request
Beispielverwendung
thread = Gem::WebauthnListener.listener_thread("https://rubygems.example", server) thread.join otp = thread[:otp] error = thread[:error]
Die WebauthnListener Response-Klasse wird von WebauthnListener verwendet, um Antworten zu erstellen, die an den Gem-Host gesendet werden. Sie erstellt eine Gem::Net::HTTPResponse-Instanz bei der Initialisierung und kann in das entsprechende Format für die Socket-Übertragung mittels 'to_s' konvertiert werden. Gem::Net::HTTPResponse-Instanzen können nicht direkt über einen Socket gesendet werden.
Arten von Antwortklassen
- OkResponse - NoContentResponse - BadRequestResponse - NotFoundResponse - MethodNotAllowedResponse
Beispielverwendung
server = TCPServer.new(0) socket = server.accept response = OkResponse.for("https://rubygems.example") socket.print response.to_s socket.close
Die Klasse WebauthnPoller ruft ein OTP ab, nachdem ein Benutzer erfolgreich eine WebAuthn-Authentifizierung durchgeführt hat. Eine Instanz fragt den Gem-Host nach dem OTP-Code ab. Die Abfrageanfrage (api/v1/webauthn_verification/<webauthn_token>/status.json) wird alle 5 Sekunden an den Gem-Host gesendet und hat ein Timeout von 5 Minuten. Wenn das Feld „status" in der JSON-Antwort „success" lautet, enthält das Feld „code" den OTP-Code.
Beispielverwendung
thread = Gem::WebauthnPoller.poll_thread( {}, "RubyGems.org", "https://rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY", { email: "email@example.com", password: "password" } ) thread.join otp = thread[:otp] error = thread[:error]
Constants
- API_SCOPES
- ERROR_CODE
- EXCLUSIVELY_API_SCOPES
Attribute
Öffentliche Instanzmethoden
Source
# 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,options| options[:key] = value end end
Fügt die Option –key hinzu
Source
# 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, options| options[:otp] = value end end
Fügt die Option –otp hinzu
Source
# File lib/rubygems/gemcutter_utilities.rb, line 46 def api_key if ENV["GEM_HOST_API_KEY"] ENV["GEM_HOST_API_KEY"] elsif options[:key] verify_api_key options[:key] elsif Gem.configuration.api_keys.key?(host) Gem.configuration.api_keys[host] else Gem.configuration.rubygems_api_key end end
Der API-Schlüssel aus den Befehlsoptionen oder aus der Konfiguration des Benutzers.
Source
# 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
Der Host, zu dem eine Verbindung hergestellt werden soll, entweder aus der Umgebungsvariable RUBYGEMS_HOST oder aus der Konfiguration des Benutzers
Source
# File lib/rubygems/gemcutter_utilities.rb, line 61 def otp options[:otp] || ENV["GEM_HOST_OTP_CODE"] end
Der OTP-Code aus den Befehlsoptionen oder aus der Konfiguration des Benutzers.
Source
# 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 mfa_unauthorized?(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
Erstellt eine RubyGems API-Anfrage an host und path mit der gegebenen HTTP-method.
Wenn Metadaten für allowed_push_host vorhanden sind, wird nur dieser Host zugelassen.
Source
# 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
Gibt true zurück, wenn der Benutzer die Multifaktor-Authentifizierung aus response aktiviert hat und kein OTP über Optionen bereitgestellt wurde.
Source
# 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
Meldet sich bei der RubyGems API unter sign_in_host an und setzt den RubyGems API-Schlüssel.
Source
# 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
Source
# 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
Ruft den voreingestellten API-Schlüssel key ab oder beendet die Interaktion mit einer Fehlermeldung.
Source
# File lib/rubygems/gemcutter_utilities.rb, line 65 def webauthn_enabled? options[:webauthn] end
Source
# 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 message = "The request has redirected permanently to #{response["location"]}. Please check your defined push host URL." message = "#{error_prefix}: #{message}" if error_prefix say clean_text(message) terminate_interaction(ERROR_CODE) else message = response.body message = "#{error_prefix}: #{message}" if error_prefix say clean_text(message) terminate_interaction(ERROR_CODE) end end
Wenn response eine erfolgreiche HTTP-Antwort (2XX) ist, wird die Antwort übergeben, wenn ein Block gegeben wurde, oder der Antwortkörper wird dem Benutzer angezeigt.
Wenn die Antwort nicht erfolgreich war, wird dem Benutzer ein Fehler angezeigt, der den error_prefix und den Antwortkörper enthält. Wenn die Antwort eine permanente Weiterleitung war, wird dem Benutzer ein Fehler angezeigt, der den Weiterleitungsort enthält.
Private Instanzmethoden
Source
# File lib/rubygems/gemcutter_utilities.rb, line 394 def api_key_forbidden?(response) response.is_a?(Gem::Net::HTTPForbidden) && response.body.start_with?("The API key doesn't have access") end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 354 def default_host? host == Gem::DEFAULT_HOST end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 260 def fetch_otp(credentials) options[:otp] = if webauthn_url = webauthn_verification_url(credentials) server = TCPServer.new 0 port = server.addr[1].to_s url_with_port = "#{webauthn_url}?port=#{port}" say "You have enabled multi-factor authentication. Please visit the following URL 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." say "" say url_with_port say "" threads = [WebauthnListener.listener_thread(host, server), WebauthnPoller.poll_thread(options, host, webauthn_url, credentials)] otp_thread = wait_for_otp_thread(*threads) threads.each(&:join) if error = otp_thread[:error] alert_error error.message terminate_interaction(1) end options[:webauthn] = true say "You are verified with a security device. You may close the browser window." otp_thread[:otp] else say "You have enabled multi-factor authentication. Please enter OTP code." ask "Code: " end end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 380 def get_key_name(scope) hostname = Socket.gethostname || "unknown-host" user = ENV["USER"] || ENV["USERNAME"] || "unknown-user" ts = Time.now.strftime("%Y%m%d%H%M%S") default_key_name = "#{hostname}-#{user}-#{ts}" key_name = ask "API Key name [#{default_key_name}]: " unless scope if key_name.nil? || key_name.empty? default_key_name else key_name end end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 370 def get_mfa_params(profile) mfa_level = profile["mfa"] params = {} if ["ui_only", "ui_and_gem_signin"].include?(mfa_level) selected = ask_yes_no("Would you like to enable MFA for this key? (strongly recommended)") params["mfa"] = true if selected end params end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 321 def get_scope_params(scope) scope_params = { index_rubygems: true, push_rubygem: true } if scope scope_params = { scope => true } else say "The default access scope is:" scope_params.each do |k, _v| say " #{k}: y" end say "\n" customise = ask_yes_no("Do you want to customise scopes?", false) if customise EXCLUSIVELY_API_SCOPES.each do |excl_scope| selected = ask_yes_no("#{excl_scope} (exclusive scope, answering yes will not prompt for other scopes)", false) next unless selected return { excl_scope => true } end scope_params = {} API_SCOPES.each do |s| selected = ask_yes_no(s.to_s, false) scope_params[s] = true if selected end end say "\n" end scope_params end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 358 def get_user_profile(identifier, password) return {} unless default_host? response = rubygems_api_request(:get, "api/v1/profile/me.yaml") do |request| request.basic_auth identifier, password end with_response response do |resp| Gem::ConfigFile.load_with_rubygems_config_hash(clean_text(resp.body)) end end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 313 def pretty_host(host) if default_host? "RubyGems.org" else host end end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 249 def request_with_otp(method, uri, &block) request_method = Gem::Net::HTTP.const_get method.to_s.capitalize Gem::RemoteFetcher.fetcher.request(uri, request_method) do |req| req["OTP"] = otp if otp block.call(req) end ensure options[:otp] = nil if webauthn_enabled? end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 291 def wait_for_otp_thread(*threads) loop do threads.each do |otp_thread| return otp_thread unless otp_thread.alive? end sleep 0.1 end ensure threads.each(&:exit) end
Source
# File lib/rubygems/gemcutter_utilities.rb, line 302 def webauthn_verification_url(credentials) response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request| if credentials.empty? request.add_field "Authorization", api_key else request.basic_auth credentials[:identifier], credentials[:password] end end response.is_a?(Gem::Net::HTTPSuccess) ? response.body : nil end