diff --git a/lib/vagrant/util/downloader.rb b/lib/vagrant/util/downloader.rb index b79a8734e..a8a0968c4 100644 --- a/lib/vagrant/util/downloader.rb +++ b/lib/vagrant/util/downloader.rb @@ -29,7 +29,7 @@ module Vagrant "vagrantup.com".freeze ].freeze - attr_reader :source + attr_accessor :source attr_reader :destination attr_accessor :headers diff --git a/plugins/commands/cloud/auth/middleware/add_downloader_authentication.rb b/plugins/commands/cloud/auth/middleware/add_downloader_authentication.rb index b99a01916..2c7237c36 100644 --- a/plugins/commands/cloud/auth/middleware/add_downloader_authentication.rb +++ b/plugins/commands/cloud/auth/middleware/add_downloader_authentication.rb @@ -6,6 +6,9 @@ require_relative "./add_authentication" require Vagrant.source_root.join("plugins/commands/cloud/client/client") +# Similar to AddAuthentication this middleware will add authentication for interacting +# with Vagrant cloud. It does this by adding Authentication headers to a +# Vagrant::Util::Downloader object. module VagrantPlugins module CloudCommand class AddDownloaderAuthentication < AddAuthentication @@ -13,16 +16,16 @@ module VagrantPlugins def call(env) client = Client.new(env[:env]) token = client.token - target_url = URI.parse(env[:downloader].source) Vagrant::Util::CredentialScrubber.sensitive(token) - if target_url.host != TARGET_HOST && REPLACEMENT_HOSTS.include?(target_url.host) - begin - target_url.host = TARGET_HOST - target_url = target_url.to_s - rescue URI::Error - # if there is an error, use current target_url + begin + target_url = URI.parse(env[:downloader].source) + if target_url.host != TARGET_HOST && REPLACEMENT_HOSTS.include?(target_url.host) + target_url.host = TARGET_HOST + env[:downloader].source = target_url.to_s end + rescue URI::Error + # if there is an error, use current target_url end server_uri = URI.parse(Vagrant.server_url.to_s) @@ -35,7 +38,8 @@ module VagrantPlugins self.class.custom_host_notified! end - if env[:downloader].headers && !env[:downloader].headers.any? { |h| h.include?("Authorization") } + if !Array(env[:downloader].headers).any? { |h| h.include?("Authorization") } + env[:downloader].headers ||= [] env[:downloader].headers << "Authorization: Bearer #{token}" end end diff --git a/test/unit/plugins/commands/cloud/auth/middleware/add_downloader_authentication_test.rb b/test/unit/plugins/commands/cloud/auth/middleware/add_downloader_authentication_test.rb new file mode 100644 index 000000000..9def49ff1 --- /dev/null +++ b/test/unit/plugins/commands/cloud/auth/middleware/add_downloader_authentication_test.rb @@ -0,0 +1,155 @@ +require File.expand_path("../../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cloud/auth/middleware/add_downloader_authentication") +require "vagrant/util/downloader" + +describe VagrantPlugins::CloudCommand::AddDownloaderAuthentication do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:ui) { double("ui") } + let(:env) { { + env: iso_env, + ui: ui + } } + + let(:iso_env) { isolated_environment.create_vagrant_env } + let(:server_url) { "http://vagrantcloud.com/box.box" } + let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {}) } + + subject { described_class.new(app, env) } + + before do + allow(Vagrant).to receive(:server_url).and_return(server_url) + allow(ui).to receive(:warn) + stub_env("ATLAS_TOKEN" => nil) + end + + describe "#call" do + context "non full paths" do + let(:server_url) { "http://vagrantcloud.com" } + let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {}) } + + it "does nothing if we have no server set" do + allow(Vagrant).to receive(:server_url).and_return(nil) + VagrantPlugins::CloudCommand::Client.new(iso_env).store_token("fooboohoo") + + env[:downloader] = dwnloader + subject.call(env) + expect(env[:downloader].headers.nil?).to eq(true) + end + + it "does nothing if we aren't logged in" do + env[:downloader] = dwnloader + subject.call(env) + expect(env[:downloader].headers.nil?).to eq(true) + end + end + + context "custom server" do + let(:server_url) { "http://surprise.com/box.box" } + let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {}) } + + it "warns when adding token to custom server" do + server_url = "https://surprise.com" + allow(Vagrant).to receive(:server_url).and_return(server_url) + + token = "foobarbaz" + VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) + + expect(subject).to receive(:sleep).once + expect(ui).to receive(:warn).once + + env[:downloader] = dwnloader + subject.call(env) + + expect(env[:downloader].headers).to eq(["Authorization: Bearer #{token}"]) + end + end + + context "replacement hosts" do + let(:dwnloader) { Vagrant::Util::Downloader.new("https://app.vagrantup.com", "/some/path", {}) } + + it "modifies host URL to target if authorized host" do + token = "foobarbaz" + VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) + env[:downloader] = dwnloader + subject.call(env) + expect(env[:downloader].headers).to eq(["Authorization: Bearer #{token}"]) + expect(URI.parse(env[:downloader].source).host).to eq(VagrantPlugins::CloudCommand::AddDownloaderAuthentication::TARGET_HOST) + end + end + + context "malformed url" do + let(:bad_url) { "this is not a valid url" } + let(:dwnloader) { Vagrant::Util::Downloader.new(bad_url, "/some/path", {}) } + + it "ignores urls that it cannot parse" do + # Ensure the bad URL does cause an exception + expect{ URI.parse(bad_url) }.to raise_error URI::Error + env[:downloader] = dwnloader + subject.call(env) + expect(env[:downloader].source).to eq(bad_url) + end + end + + context "with an headers already added" do + let(:auth_header) { "Authorization Bearer: token" } + let(:other_header) {"some: thing"} + let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {headers: [other_header]}) } + + it "appends the auth header" do + token = "foobarbaz" + VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) + + env[:downloader] = dwnloader + subject.call(env) + + expect(env[:downloader].headers).to eq([other_header, "Authorization: Bearer #{token}"]) + end + + context "with local file path" do + let(:file_path) { "file:////path/to/box.box" } + let(:dwnloader) { Vagrant::Util::Downloader.new(file_path, "/some/path", {}) } + + it "returns original urls when not modified" do + env[:downloader] = dwnloader + subject.call(env) + + expect(env[:downloader].source).to eq(file_path) + expect(env[:downloader].headers).to eq(nil) + end + end + + it "does not append multiple access_tokens" do + dwnloader.headers << auth_header + token = "foobarbaz" + VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) + + env[:downloader] = dwnloader + subject.call(env) + + expect(env[:downloader].headers).to eq([other_header, auth_header]) + end + end + + it "adds a token to the headers" do + token = "foobarbaz" + VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) + env[:downloader] = dwnloader + subject.call(env) + expect(env[:downloader].headers).to eq(["Authorization: Bearer #{token}"]) + end + + it "does not append the access token to vagrantcloud.com URLs if Atlas" do + server_url = "https://atlas.hashicorp.com" + allow(Vagrant).to receive(:server_url).and_return(server_url) + allow(subject).to receive(:sleep) + token = "foobarbaz" + VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) + env[:downloader] = dwnloader + subject.call(env) + expect(env[:downloader].headers).to eq(nil) + end + end +end diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb index 48406c2cf..e7bdb8b39 100644 --- a/test/unit/vagrant/action/builtin/box_add_test.rb +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -595,8 +595,10 @@ describe Vagrant::Action::Builtin::BoxAdd, :skip_windows, :bsdtar do env[:box_url] = "foo" env[:hook] = double("hook") - allow(env[:hook]).to receive(:call) do |name, opts| - expect(name).to eq(:authenticate_box_url) + + expect(env[:hook]).to receive(:call).with(:authenticate_box_downloader, any_args).at_least(:once) + + allow(env[:hook]).to receive(:call).with(:authenticate_box_url, any_args).at_least(:once) do |name, opts| if opts[:box_urls] == ["foo"] next { box_urls: [real_url] } elsif opts[:box_urls] == ["bar"] diff --git a/test/unit/vagrant/box_test.rb b/test/unit/vagrant/box_test.rb index fcb45780a..d045005cc 100644 --- a/test/unit/vagrant/box_test.rb +++ b/test/unit/vagrant/box_test.rb @@ -285,6 +285,24 @@ describe Vagrant::Box, :skip_windows do expect { subject.load_metadata }. to raise_error(Vagrant::Errors::BoxMetadataDownloadError) end + + context "box has a hook for adding authentication" do + + let(:hook){ double("hook") } + + subject do + described_class.new( + name, provider, version, directory, + metadata_url: metadata_url.path, hook: hook) + end + + it "add authentication headers to the url" do + expect(hook).to receive(:call).with(:authenticate_box_downloader, any_args) + result = subject.load_metadata + expect(result.name).to eq("foo") + expect(result.description).to eq("bar") + end + end end describe "destroying" do