This PR is dependent on the 2.1.0 release of the vagrant_cloud library. It updates the `cloud` command to use the new interface for interacting with the Vagrant Cloud API. It also adds support for direct to backend storage uploads, and defaults to this method. Also included is a bit of cleanup refactoring, addition of method documentation, and fixing up some small issues around custom username usage within the internal client for authentication.
224 lines
7.4 KiB
Ruby
224 lines
7.4 KiB
Ruby
require "rest_client"
|
|
require "vagrant_cloud"
|
|
require "vagrant/util/downloader"
|
|
require "vagrant/util/presence"
|
|
require Vagrant.source_root.join("plugins/commands/cloud/errors")
|
|
|
|
module VagrantPlugins
|
|
module CloudCommand
|
|
class Client
|
|
######################################################################
|
|
# Class that deals with managing users 'local' token for Vagrant Cloud
|
|
######################################################################
|
|
APP = "app".freeze
|
|
|
|
include Vagrant::Util::Presence
|
|
|
|
attr_accessor :client
|
|
attr_accessor :username_or_email
|
|
attr_accessor :password
|
|
attr_reader :two_factor_default_delivery_method
|
|
attr_reader :two_factor_delivery_methods
|
|
|
|
# Initializes a login client with the given Vagrant::Environment.
|
|
#
|
|
# @param [Vagrant::Environment] env
|
|
def initialize(env)
|
|
@logger = Log4r::Logger.new("vagrant::cloud::client")
|
|
@env = env
|
|
@client = VagrantCloud::Client.new(access_token: token)
|
|
end
|
|
|
|
# Removes the token, effectively logging the user out.
|
|
def clear_token
|
|
@logger.info("Clearing token")
|
|
token_path.delete if token_path.file?
|
|
end
|
|
|
|
# Checks if the user is logged in by verifying their authentication
|
|
# token.
|
|
#
|
|
# @return [Boolean]
|
|
def logged_in?
|
|
return false if !client.access_token
|
|
Vagrant::Util::CredentialScrubber.sensitive(client.access_token)
|
|
|
|
with_error_handling do
|
|
client.authentication_token_validate
|
|
true
|
|
end
|
|
rescue Errors::Unauthorized
|
|
false
|
|
end
|
|
|
|
# Login logs a user in and returns the token for that user. The token
|
|
# is _not_ stored unless {#store_token} is called.
|
|
#
|
|
# @param [String] description
|
|
# @param [String] code
|
|
# @return [String] token The access token, or nil if auth failed.
|
|
def login(description: nil, code: nil)
|
|
@logger.info("Logging in '#{username_or_email}'")
|
|
|
|
Vagrant::Util::CredentialScrubber.sensitive(password)
|
|
with_error_handling do
|
|
r = client.authentication_token_create(username: username_or_email,
|
|
password: password, description: description, code: code)
|
|
|
|
Vagrant::Util::CredentialScrubber.sensitive(r[:token])
|
|
@client = VagrantCloud::Client.new(access_token: r[:token])
|
|
r[:token]
|
|
end
|
|
end
|
|
|
|
# Requests a 2FA code
|
|
# @param [String] delivery_method
|
|
def request_code(delivery_method)
|
|
@env.ui.warn("Requesting 2FA code via #{delivery_method.upcase}...")
|
|
|
|
Vagrant::Util::CredentialScrubber.sensitive(password)
|
|
with_error_handling do
|
|
r = client.authentication_request_2fa_code(
|
|
username: username_or_email, password: password, delivery_method: delivery_method)
|
|
|
|
two_factor = r[:two_factor]
|
|
obfuscated_destination = two_factor[:obfuscated_destination]
|
|
|
|
@env.ui.success("2FA code sent to #{obfuscated_destination}.")
|
|
end
|
|
end
|
|
|
|
# Stores the given token locally, removing any previous tokens.
|
|
#
|
|
# @param [String] token
|
|
def store_token(token)
|
|
Vagrant::Util::CredentialScrubber.sensitive(token)
|
|
@logger.info("Storing token in #{token_path}")
|
|
|
|
token_path.open("w") do |f|
|
|
f.write(token)
|
|
end
|
|
|
|
# Reset after we store the token since this is now _our_ token
|
|
@client = VagrantCloud::Client.new(access_token: token)
|
|
|
|
nil
|
|
end
|
|
|
|
# Reads the access token if there is one. This will first read the
|
|
# `VAGRANT_CLOUD_TOKEN` environment variable and then fallback to the stored
|
|
# access token on disk.
|
|
#
|
|
# @return [String]
|
|
def token
|
|
if present?(ENV["VAGRANT_CLOUD_TOKEN"]) && token_path.exist?
|
|
@env.ui.warn <<-EOH.strip
|
|
Vagrant detected both the VAGRANT_CLOUD_TOKEN environment variable and a Vagrant login
|
|
token are present on this system. The VAGRANT_CLOUD_TOKEN environment variable takes
|
|
precedence over the locally stored token. To remove this error, either unset
|
|
the VAGRANT_CLOUD_TOKEN environment variable or remove the login token stored on disk:
|
|
|
|
~/.vagrant.d/data/vagrant_login_token
|
|
|
|
EOH
|
|
end
|
|
|
|
if present?(ENV["VAGRANT_CLOUD_TOKEN"])
|
|
@logger.debug("Using authentication token from environment variable")
|
|
t = ENV["VAGRANT_CLOUD_TOKEN"]
|
|
elsif token_path.exist?
|
|
@logger.debug("Using authentication token from disk at #{token_path}")
|
|
t = token_path.read.strip
|
|
elsif present?(ENV["ATLAS_TOKEN"])
|
|
@logger.warn("ATLAS_TOKEN detected within environment. Using ATLAS_TOKEN in place of VAGRANT_CLOUD_TOKEN.")
|
|
t = ENV["ATLAS_TOKEN"]
|
|
end
|
|
|
|
if !t.nil?
|
|
Vagrant::Util::CredentialScrubber.sensitive(t)
|
|
return t
|
|
end
|
|
|
|
@logger.debug("No authentication token in environment or #{token_path}")
|
|
|
|
nil
|
|
end
|
|
|
|
protected
|
|
|
|
def with_error_handling(&block)
|
|
yield
|
|
rescue Excon::Error::Unauthorized
|
|
@logger.debug("Unauthorized!")
|
|
raise Errors::Unauthorized
|
|
rescue Excon::Error::BadRequest => e
|
|
@logger.debug("Bad request:")
|
|
@logger.debug(e.message)
|
|
@logger.debug(e.backtrace.join("\n"))
|
|
parsed_response = JSON.parse(e.response.body)
|
|
errors = parsed_response["errors"].join("\n")
|
|
raise Errors::ServerError, errors: errors
|
|
rescue Excon::Error::NotAcceptable => e
|
|
@logger.debug("Got unacceptable response:")
|
|
@logger.debug(e.message)
|
|
@logger.debug(e.backtrace.join("\n"))
|
|
|
|
parsed_response = JSON.parse(e.response.body)
|
|
|
|
if two_factor = parsed_response['two_factor']
|
|
store_two_factor_information two_factor
|
|
|
|
if two_factor_default_delivery_method != APP
|
|
request_code two_factor_default_delivery_method
|
|
end
|
|
|
|
raise Errors::TwoFactorRequired
|
|
end
|
|
|
|
begin
|
|
errors = parsed_response["errors"].join("\n")
|
|
raise Errors::ServerError, errors: errors
|
|
rescue JSON::ParserError; end
|
|
|
|
@logger.debug("Got an unexpected error:")
|
|
@logger.debug(e.inspect)
|
|
raise Errors::Unexpected, error: e.inspect
|
|
rescue SocketError
|
|
@logger.info("Socket error")
|
|
raise Errors::ServerUnreachable, url: Vagrant.server_url.to_s
|
|
end
|
|
|
|
def token_path
|
|
@env.data_dir.join("vagrant_login_token")
|
|
end
|
|
|
|
def store_two_factor_information(two_factor)
|
|
@two_factor_default_delivery_method =
|
|
two_factor['default_delivery_method']
|
|
|
|
@two_factor_delivery_methods =
|
|
two_factor['delivery_methods']
|
|
|
|
@env.ui.warn "2FA is enabled for your account."
|
|
if two_factor_default_delivery_method == APP
|
|
@env.ui.info "Enter the code from your authenticator."
|
|
else
|
|
@env.ui.info "Default method is " \
|
|
"'#{two_factor_default_delivery_method}'."
|
|
end
|
|
|
|
other_delivery_methods =
|
|
two_factor_delivery_methods - [APP]
|
|
|
|
if other_delivery_methods.any?
|
|
other_delivery_methods_sentence = other_delivery_methods
|
|
.map { |word| "'#{word}'" }
|
|
.join(' or ')
|
|
@env.ui.info "You can also type #{other_delivery_methods_sentence} " \
|
|
"to request a new code."
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|