diff --git a/plugins/commands/cloud/client/client.rb b/plugins/commands/cloud/client/client.rb index 30de19043..1c75c7901 100644 --- a/plugins/commands/cloud/client/client.rb +++ b/plugins/commands/cloud/client/client.rb @@ -1,4 +1,3 @@ -require "rest_client" require "vagrant_cloud" require "vagrant/util/downloader" require "vagrant/util/presence" diff --git a/plugins/commands/login/client.rb b/plugins/commands/login/client.rb deleted file mode 100644 index ebfe717b3..000000000 --- a/plugins/commands/login/client.rb +++ /dev/null @@ -1,253 +0,0 @@ -require "rest_client" -require "vagrant/util/downloader" -require "vagrant/util/presence" - -module VagrantPlugins - module LoginCommand - class Client - APP = "app".freeze - - include Vagrant::Util::Presence - - 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::login::client") - @env = env - 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? - token = self.token - return false if !token - - with_error_handling do - url = "#{Vagrant.server_url}/api/v1/authenticate" + - "?access_token=#{token}" - RestClient.get(url, content_type: :json) - 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}'") - - response = post( - "/api/v1/authenticate", { - user: { - login: username_or_email, - password: password - }, - token: { - description: description - }, - two_factor: { - code: code - } - } - ) - - response["token"] - end - - # Requests a 2FA code - # @param [String] delivery_method - def request_code(delivery_method) - @env.ui.warn("Requesting 2FA code via #{delivery_method.upcase}...") - - response = post( - "/api/v1/two-factor/request-code", { - user: { - login: username_or_email, - password: password - }, - two_factor: { - delivery_method: delivery_method.downcase - } - } - ) - - two_factor = response['two_factor'] - obfuscated_destination = two_factor['obfuscated_destination'] - - @env.ui.success("2FA code sent to #{obfuscated_destination}.") - end - - # Issues a post to a Vagrant Cloud path with the given payload. - # @param [String] path - # @param [Hash] payload - # @return [Hash] response data - def post(path, payload) - with_error_handling do - url = File.join(Vagrant.server_url, path) - - proxy = nil - proxy ||= ENV["HTTPS_PROXY"] || ENV["https_proxy"] - proxy ||= ENV["HTTP_PROXY"] || ENV["http_proxy"] - RestClient.proxy = proxy - - response = RestClient::Request.execute( - method: :post, - url: url, - payload: JSON.dump(payload), - proxy: proxy, - headers: { - accept: :json, - content_type: :json, - user_agent: Vagrant::Util::Downloader::USER_AGENT, - }, - ) - - JSON.load(response.to_s) - end - end - - # Stores the given token locally, removing any previous tokens. - # - # @param [String] token - def store_token(token) - @logger.info("Storing token in #{token_path}") - - token_path.open("w") do |f| - f.write(token) - end - - 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") - return ENV["VAGRANT_CLOUD_TOKEN"] - end - - if token_path.exist? - @logger.debug("Using authentication token from disk at #{token_path}") - return token_path.read.strip - end - - if present?(ENV["ATLAS_TOKEN"]) - @logger.warn("ATLAS_TOKEN detected within environment. Using ATLAS_TOKEN in place of VAGRANT_CLOUD_TOKEN.") - return ENV["ATLAS_TOKEN"] - end - - @logger.debug("No authentication token in environment or #{token_path}") - - nil - end - - protected - - def with_error_handling(&block) - yield - rescue RestClient::Unauthorized - @logger.debug("Unauthorized!") - raise Errors::Unauthorized - rescue RestClient::BadRequest => e - @logger.debug("Bad request:") - @logger.debug(e.message) - @logger.debug(e.backtrace.join("\n")) - parsed_response = JSON.parse(e.response) - errors = parsed_response["errors"].join("\n") - raise Errors::ServerError, errors: errors - rescue RestClient::NotAcceptable => e - @logger.debug("Got unacceptable response:") - @logger.debug(e.message) - @logger.debug(e.backtrace.join("\n")) - - parsed_response = JSON.parse(e.response) - - 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 - - raise "An unexpected error occurred: #{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 diff --git a/plugins/commands/login/command.rb b/plugins/commands/login/command.rb deleted file mode 100644 index 10a8ef13f..000000000 --- a/plugins/commands/login/command.rb +++ /dev/null @@ -1,137 +0,0 @@ -require 'socket' - -module VagrantPlugins - module LoginCommand - class Command < Vagrant.plugin("2", "command") - def self.synopsis - "log in to HashiCorp's Vagrant Cloud" - end - - def execute - options = {} - - opts = OptionParser.new do |o| - o.banner = "Usage: vagrant login" - o.separator "" - o.on("-c", "--check", "Only checks if you're logged in") do |c| - options[:check] = c - end - - o.on("-d", "--description DESCRIPTION", String, "Description for the Vagrant Cloud token") do |t| - options[:description] = t - end - - o.on("-k", "--logout", "Logs you out if you're logged in") do |k| - options[:logout] = k - end - - o.on("-t", "--token TOKEN", String, "Set the Vagrant Cloud token") do |t| - options[:token] = t - end - - o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| - options[:login] = t - end - end - - # Parse the options - argv = parse_options(opts) - return if !argv - - @client = Client.new(@env) - @client.username_or_email = options[:login] - - # Determine what task we're actually taking based on flags - if options[:check] - return execute_check - elsif options[:logout] - return execute_logout - elsif options[:token] - return execute_token(options[:token]) - end - - # Let the user know what is going on. - @env.ui.output(I18n.t("login_command.command_header") + "\n") - - # If it is a private cloud installation, show that - if Vagrant.server_url != Vagrant::DEFAULT_SERVER_URL - @env.ui.output("Vagrant Cloud URL: #{Vagrant.server_url}") - end - - # Ask for the username - if @client.username_or_email - @env.ui.output("Vagrant Cloud username or email: #{@client.username_or_email}") - end - until @client.username_or_email - @client.username_or_email = @env.ui.ask("Vagrant Cloud username or email: ") - end - - until @client.password - @client.password = @env.ui.ask("Password (will be hidden): ", echo: false) - end - - description = options[:description] - if description - @env.ui.output("Token description: #{description}") - else - description_default = "Vagrant login from #{Socket.gethostname}" - until description - description = - @env.ui.ask("Token description (Defaults to #{description_default.inspect}): ") - end - description = description_default if description.empty? - end - - code = nil - - begin - token = @client.login(description: description, code: code) - rescue Errors::TwoFactorRequired - until code - code = @env.ui.ask("2FA code: ") - - if @client.two_factor_delivery_methods.include?(code.downcase) - delivery_method, code = code, nil - @client.request_code delivery_method - end - end - - retry - end - - @client.store_token(token) - @env.ui.success(I18n.t("login_command.logged_in")) - 0 - end - - def execute_check - if @client.logged_in? - @env.ui.success(I18n.t("login_command.check_logged_in")) - return 0 - else - @env.ui.error(I18n.t("login_command.check_not_logged_in")) - return 1 - end - end - - def execute_logout - @client.clear_token - @env.ui.success(I18n.t("login_command.logged_out")) - return 0 - end - - def execute_token(token) - @client.store_token(token) - @env.ui.success(I18n.t("login_command.token_saved")) - - if @client.logged_in? - @env.ui.success(I18n.t("login_command.check_logged_in")) - return 0 - else - @env.ui.error(I18n.t("login_command.invalid_token")) - return 1 - end - end - end - end -end diff --git a/plugins/commands/login/errors.rb b/plugins/commands/login/errors.rb deleted file mode 100644 index 4d56612bd..000000000 --- a/plugins/commands/login/errors.rb +++ /dev/null @@ -1,24 +0,0 @@ -module VagrantPlugins - module LoginCommand - module Errors - class Error < Vagrant::Errors::VagrantError - error_namespace("login_command.errors") - end - - class ServerError < Error - error_key(:server_error) - end - - class ServerUnreachable < Error - error_key(:server_unreachable) - end - - class Unauthorized < Error - error_key(:unauthorized) - end - - class TwoFactorRequired < Error - end - end - end -end diff --git a/plugins/commands/login/locales/en.yml b/plugins/commands/login/locales/en.yml deleted file mode 100644 index 3f41a1067..000000000 --- a/plugins/commands/login/locales/en.yml +++ /dev/null @@ -1,49 +0,0 @@ -en: - login_command: - middleware: - authentication: - different_target: |- - Vagrant has detected a custom Vagrant server in use for downloading - box files. An authentication token is currently set which will be - added to the box request. If the custom Vagrant server should not - be receiving the authentication token, please unset it. - - Known Vagrant server: %{known_host} - Custom Vagrant server: %{custom_host} - - Press ctrl-c to cancel... - errors: - server_error: |- - The Vagrant Cloud server responded with a not-OK response: - - %{errors} - server_unreachable: |- - The Vagrant Cloud server is not currently accepting connections. Please check - your network connection and try again later. - - unauthorized: |- - Invalid username or password. Please try again. - - check_logged_in: |- - You are already logged in. - check_not_logged_in: |- - You are not currently logged in. Please run `vagrant login` and provide - your login information to authenticate. - command_header: |- - In a moment we will ask for your username and password to HashiCorp's - Vagrant Cloud. After authenticating, we will store an access token locally on - disk. Your login details will be transmitted over a secure connection, and - are never stored on disk locally. - - If you do not have an Vagrant Cloud account, sign up at - https://www.vagrantcloud.com - invalid_login: |- - Invalid username or password. Please try again. - invalid_token: |- - Invalid token. Please try again. - logged_in: |- - You are now logged in. - logged_out: |- - You are logged out. - token_saved: |- - The token was successfully saved. diff --git a/plugins/commands/login/plugin.rb b/plugins/commands/login/plugin.rb index 9151c55a1..7cfbdb013 100644 --- a/plugins/commands/login/plugin.rb +++ b/plugins/commands/login/plugin.rb @@ -2,9 +2,6 @@ require "vagrant" module VagrantPlugins module LoginCommand - autoload :Client, File.expand_path("../client", __FILE__) - autoload :Errors, File.expand_path("../errors", __FILE__) - class Plugin < Vagrant.plugin("2") name "vagrant-login" description <<-DESC @@ -13,18 +10,8 @@ module VagrantPlugins command(:login) do require File.expand_path("../../cloud/auth/login", __FILE__) - init! VagrantPlugins::CloudCommand::AuthCommand::Command::Login end - - protected - - def self.init! - return if defined?(@_init) - I18n.load_path << File.expand_path("../../cloud/locales/en.yml", __FILE__) - I18n.reload! - @_init = true - end end end end diff --git a/test/unit/plugins/commands/login/client_test.rb b/test/unit/plugins/commands/login/client_test.rb deleted file mode 100644 index 06a110706..000000000 --- a/test/unit/plugins/commands/login/client_test.rb +++ /dev/null @@ -1,261 +0,0 @@ -require File.expand_path("../../../../base", __FILE__) - -require Vagrant.source_root.join("plugins/commands/login/command") - -describe VagrantPlugins::LoginCommand::Client do - include_context "unit" - - let(:env) { isolated_environment.create_vagrant_env } - - subject(:client) { described_class.new(env) } - - before(:all) do - I18n.load_path << Vagrant.source_root.join("plugins/commands/login/locales/en.yml") - I18n.reload! - end - - before do - stub_env("ATLAS_TOKEN" => nil) - subject.clear_token - end - - describe "#logged_in?" do - let(:url) { "#{Vagrant.server_url}/api/v1/authenticate?access_token=#{token}" } - let(:headers) { { "Content-Type" => "application/json" } } - - before { allow(subject).to receive(:token).and_return(token) } - - context "when there is no token" do - let(:token) { nil } - - it "returns false" do - expect(subject.logged_in?).to be(false) - end - end - - context "when there is a token" do - let(:token) { "ABCD1234" } - - it "returns true if the endpoint returns a 200" do - stub_request(:get, url) - .with(headers: headers) - .to_return(body: JSON.pretty_generate("token" => token)) - expect(subject.logged_in?).to be(true) - end - - it "raises an error if the endpoint returns a non-200" do - stub_request(:get, url) - .with(headers: headers) - .to_return(body: JSON.pretty_generate("bad" => true), status: 401) - expect(subject.logged_in?).to be(false) - end - - it "raises an exception if the server cannot be found" do - stub_request(:get, url) - .to_raise(SocketError) - expect { subject.logged_in? } - .to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) - end - end - end - - describe "#login" do - let(:request) { - { - user: { - login: login, - password: password, - }, - token: { - description: description, - }, - two_factor: { - code: nil - } - } - } - - let(:login) { "foo" } - let(:password) { "bar" } - let(:description) { "Token description" } - - let(:headers) { - { - "Accept" => "application/json", - "Content-Type" => "application/json", - } - } - let(:response) { - { - token: "baz" - } - } - - it "returns the access token after successful login" do - stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). - with(body: JSON.dump(request), headers: headers). - to_return(status: 200, body: JSON.dump(response)) - - client.username_or_email = login - client.password = password - - expect(client.login(description: "Token description")).to eq("baz") - end - - context "when 2fa is required" do - let(:response) { - { - two_factor: { - default_delivery_method: default_delivery_method, - delivery_methods: delivery_methods - } - } - } - let(:default_delivery_method) { "app" } - let(:delivery_methods) { ["app"] } - - before do - stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). - to_return(status: 406, body: JSON.dump(response)) - end - - it "raises a two-factor required error" do - expect { - client.login - }.to raise_error(VagrantPlugins::LoginCommand::Errors::TwoFactorRequired) - end - - context "when the default delivery method is not app" do - let(:default_delivery_method) { "sms" } - let(:delivery_methods) { ["app", "sms"] } - - it "requests a code and then raises a two-factor required error" do - expect(client) - .to receive(:request_code) - .with(default_delivery_method) - - expect { - client.login - }.to raise_error(VagrantPlugins::LoginCommand::Errors::TwoFactorRequired) - end - end - end - - context "on bad login" do - before do - stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). - to_return(status: 401, body: "") - end - - it "raises an error" do - expect { - client.login - }.to raise_error(VagrantPlugins::LoginCommand::Errors::Unauthorized) - end - end - - context "if it can't reach the server" do - before do - stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). - to_raise(SocketError) - end - - it "raises an exception" do - expect { - subject.login - }.to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) - end - end - end - - describe "#request_code" do - let(:request) { - { - user: { - login: login, - password: password, - }, - two_factor: { - delivery_method: delivery_method - } - } - } - - let(:login) { "foo" } - let(:password) { "bar" } - let(:delivery_method) { "sms" } - - let(:headers) { - { - "Accept" => "application/json", - "Content-Type" => "application/json" - } - } - - let(:response) { - { - two_factor: { - obfuscated_destination: "SMS number ending in 1234" - } - } - } - - it "displays that the code was sent" do - expect(env.ui) - .to receive(:success) - .with("2FA code sent to SMS number ending in 1234.") - - stub_request(:post, "#{Vagrant.server_url}/api/v1/two-factor/request-code"). - with(body: JSON.dump(request), headers: headers). - to_return(status: 201, body: JSON.dump(response)) - - client.username_or_email = login - client.password = password - - client.request_code delivery_method - end - end - - describe "#token" do - it "reads ATLAS_TOKEN" do - stub_env("ATLAS_TOKEN" => "ABCD1234") - expect(subject.token).to eq("ABCD1234") - end - - it "reads the stored file" do - subject.store_token("EFGH5678") - expect(subject.token).to eq("EFGH5678") - end - - it "prefers the environment variable" do - stub_env("VAGRANT_CLOUD_TOKEN" => "ABCD1234") - subject.store_token("EFGH5678") - expect(subject.token).to eq("ABCD1234") - end - - it "prints a warning if the envvar and stored file are both present" do - stub_env("VAGRANT_CLOUD_TOKEN" => "ABCD1234") - subject.store_token("EFGH5678") - expect(env.ui).to receive(:warn).with(/detected both/) - subject.token - end - - it "returns nil if there's no token set" do - expect(subject.token).to be(nil) - end - end - - describe "#store_token, #clear_token" do - it "stores the token and can re-access it" do - subject.store_token("foo") - expect(subject.token).to eq("foo") - expect(described_class.new(env).token).to eq("foo") - end - - it "deletes the token" do - subject.store_token("foo") - subject.clear_token - expect(subject.token).to be_nil - end - end -end diff --git a/test/unit/plugins/commands/login/command_test.rb b/test/unit/plugins/commands/login/command_test.rb deleted file mode 100644 index c92c51174..000000000 --- a/test/unit/plugins/commands/login/command_test.rb +++ /dev/null @@ -1,96 +0,0 @@ -require File.expand_path("../../../../base", __FILE__) - -require Vagrant.source_root.join("plugins/commands/login/command") - -describe VagrantPlugins::LoginCommand::Command do - include_context "unit" - - let(:env) { isolated_environment.create_vagrant_env } - - let(:token_path) { env.data_dir.join("vagrant_login_token") } - - let(:stdout) { StringIO.new } - let(:stderr) { StringIO.new } - - subject { described_class.new(argv, env) } - - before do - stub_env("ATLAS_TOKEN" => "") - end - - describe "#execute" do - context "with no args" do - let(:argv) { [] } - end - - context "with --check" do - let(:argv) { ["--check"] } - - context "when there is a token" do - before do - stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) - .to_return(status: 200) - end - - before do - File.open(token_path, "w+") { |f| f.write("abcd1234") } - end - - it "returns 0" do - expect(subject.execute).to eq(0) - end - end - - context "when there is no token" do - it "returns 1" do - expect(subject.execute).to eq(1) - end - end - end - - context "with --logout" do - let(:argv) { ["--logout"] } - - it "returns 0" do - expect(subject.execute).to eq(0) - end - - it "clears the token" do - subject.execute - expect(File.exist?(token_path)).to be(false) - end - end - - context "with --token" do - let(:argv) { ["--token", "efgh5678"] } - - context "when the token is valid" do - before do - stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) - .to_return(status: 200) - end - - it "sets the token" do - subject.execute - token = File.read(token_path).strip - expect(token).to eq("efgh5678") - end - - it "returns 0" do - expect(subject.execute).to eq(0) - end - end - - context "when the token is invalid" do - before do - stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) - .to_return(status: 401) - end - - it "returns 1" do - expect(subject.execute).to eq(1) - end - end - end - end -end