vaguerent/test/unit/plugins/communicators/ssh/communicator_test.rb
Chris Roberts 5a7fd6b302 Inspect guest for supported key types
Updates the SSH communicator to inspect the guest sshd configuration for
supported key types when creating a new key to replace the default
insecure public key. If the guest cannot be inspected, the connection
will be inspected. If the connection cannot be inspected, it will simply
fall back to the original behavior of using an rsa type key.
2024-01-18 11:53:00 -08:00

1211 lines
39 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/communicators/ssh/communicator")
describe VagrantPlugins::CommunicatorSSH::Communicator do
include_context "unit"
let(:export_command_template){ 'export %ENV_KEY%="%ENV_VALUE%"' }
# SSH configuration information mock
let(:ssh) do
double("ssh",
key_type: :auto,
timeout: 1,
host: nil,
port: 5986,
guest_port: 5986,
pty: false,
keep_alive: false,
insert_key: insert_ssh_key,
export_command_template: export_command_template,
shell: 'bash -l'
)
end
# Do not insert public key by default
let(:insert_ssh_key){ false }
# Configuration mock
let(:config) { double("config", ssh: ssh) }
# Provider mock
let(:provider) { double("provider") }
let(:ui) { Vagrant::UI::Silent.new }
# Machine mock built with previously defined
let(:machine) do
double("machine",
config: config,
provider: provider,
ui: ui,
env: env
)
end
let(:env){ double("env", host: host) }
let(:host){ double("host") }
# SSH information of the machine
let(:machine_ssh_info){ {host: '10.1.2.3', port: 22} }
# Subject instance to test
let(:communicator){ @communicator ||= described_class.new(machine) }
# Underlying net-ssh connection mock
let(:connection) { double("connection") }
# Base net-ssh connection channel mock
let(:channel) { double("channel") }
# net-ssh connection channel mock for running commands
let(:command_channel) { double("command_channel") }
# Default exit data for commands run
let(:exit_data) { double("exit_data", read_long: 0) }
# Core shell command used when starting command connection
let(:core_shell_cmd) { "bash -l" }
# Marker used for flagging start of output
let(:command_garbage_marker) { communicator.class.const_get(:CMD_GARBAGE_MARKER) }
# Start marker output when PTY is enabled
let(:pty_delim_start) { communicator.class.const_get(:PTY_DELIM_START) }
# End marker output when PTY is enabled
let(:pty_delim_end) { communicator.class.const_get(:PTY_DELIM_END) }
# Command output returned on stdout
let(:command_stdout_data) { '' }
# Command output returned on stderr
let(:command_stderr_data) { '' }
# Mock for net-ssh scp
let(:scp) { double("scp") }
# Value returned from remote ssh supported key check
let(:sudo_supported_key_list) { "pubkeyacceptedalgorithms ssh-rsa" }
# Setup for commands using the net-ssh connection. This can be reused where needed
# by providing to `before`
connection_setup = proc do
allow(connection).to receive(:closed?).and_return false
allow(connection).to receive(:open_channel).
and_yield(channel).and_return(channel)
allow(connection).to receive(:close)
allow(channel).to receive(:wait).and_return true
allow(channel).to receive(:close)
allow(command_channel).to receive(:send_data)
allow(command_channel).to receive(:eof!)
allow(command_channel).to receive(:on_data).
and_yield(nil, command_stdout_data)
allow(command_channel).to receive(:on_extended_data).
and_yield(nil, nil, command_stderr_data)
allow(machine).to receive(:ssh_info).and_return(machine_ssh_info)
allow(channel).to receive(:exec).with(core_shell_cmd).
and_yield(command_channel, '').and_return channel
allow(command_channel).to receive(:on_request).with('exit-status').
and_yield(nil, exit_data)
# Return mocked net-ssh connection during setup
allow(communicator).to receive(:retryable).and_return(connection)
# Stub in a response for supported key types check
allow(communicator).to receive(:sudo).with("sshd -T | grep key", any_args).
and_yield(:stdout, sudo_supported_key_list).and_return(0)
end
before do
allow(host).to receive(:capability?).and_return(false)
end
describe "#wait_for_ready" do
before(&connection_setup)
context "with no static config (default scenario)" do
context "when ssh_info requires a multiple tries before it is ready" do
before do
expect(machine).to receive(:ssh_info).
and_return(nil).ordered
expect(machine).to receive(:ssh_info).
and_return(host: '10.1.2.3', port: 22).ordered
end
it "retries ssh_info until ready" do
# retries are every 0.5 so buffer the timeout just a hair over
expect(communicator.wait_for_ready(0.6)).to eq(true)
end
end
context "when printing message to the user" do
before do
allow(machine).to receive(:ssh_info).
and_return(host: '10.1.2.3', port: 22)
allow(communicator).to receive(:connect)
allow(communicator).to receive(:ready?).and_return(true)
allow(ui).to receive(:detail).and_call_original
end
it "should print message" do
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout)
expect(ui).to receive(:detail).with(/timeout/).and_call_original
communicator.wait_for_ready(0.5)
end
it "should not print the same message twice" do
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout)
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout)
expect(ui).to receive(:detail).with(/timeout/).and_call_original
expect(ui).not_to receive(:detail).with(/timeout/)
communicator.wait_for_ready(0.5)
end
it "should print different messages" do
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout)
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHDisconnected)
expect(ui).to receive(:detail).with(/timeout/).and_call_original
expect(ui).to receive(:detail).with(/disconnect/).and_call_original
communicator.wait_for_ready(0.5)
end
it "should not print different messages twice" do
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout)
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHDisconnected)
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout)
expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHDisconnected)
expect(ui).to receive(:detail).with(/timeout/).and_call_original
expect(ui).to receive(:detail).with(/disconnect/).and_call_original
expect(ui).not_to receive(:detail).with(/timeout/)
expect(ui).not_to receive(:detail).with(/disconnect/)
communicator.wait_for_ready(0.5)
end
end
end
end
describe "#reset!" do
let(:connection) { double("connection") }
before do
allow(communicator).to receive(:wait_for_ready)
allow(connection).to receive(:close)
communicator.send(:instance_variable_set, :@connection, connection)
end
it "should close existing connection" do
expect(connection).to receive(:close)
communicator.reset!
end
it "should call wait_for_ready to re-enable the connection" do
expect(communicator).to receive(:wait_for_ready)
communicator.reset!
end
end
describe "#ready?" do
before(&connection_setup)
it "returns true if shell test is successful" do
expect(communicator.ready?).to be(true)
end
context "with an invalid shell test" do
before do
expect(exit_data).to receive(:read_long).and_return 1
end
it "returns raises SSHInvalidShell error" do
expect{ communicator.ready? }.to raise_error Vagrant::Errors::SSHInvalidShell
end
end
context "with insert key enabled" do
before do
allow(machine).to receive(:guest).and_return(guest)
allow(guest).to receive(:capability?).with(:insert_public_key).
and_return(has_insert_cap)
allow(guest).to receive(:capability?).with(:remove_public_key).
and_return(has_remove_cap)
allow(communicator).to receive(:insecure_key?).with("KEY_PATH").and_return(true)
end
let(:insert_ssh_key){ true }
let(:has_insert_cap){ false }
let(:has_remove_cap){ false }
let(:machine_ssh_info){
{host: '10.1.2.3', port: 22, private_key_path: ["KEY_PATH"]}
}
let(:guest){ double("guest") }
context "without guest insert_ssh_key or remove_ssh_key capabilities" do
it "should raise an error" do
expect{ communicator.ready? }.to raise_error(Vagrant::Errors::SSHInsertKeyUnsupported)
end
end
context "without guest insert_ssh_key capability" do
let(:has_remove_cap){ true }
it "should raise an error" do
expect{ communicator.ready? }.to raise_error(Vagrant::Errors::SSHInsertKeyUnsupported)
end
end
context "without guest remove_ssh_key capability" do
let(:has_insert_cap){ true }
it "should raise an error" do
expect{ communicator.ready? }.to raise_error(Vagrant::Errors::SSHInsertKeyUnsupported)
end
end
context "with guest insert_ssh_key capability and remove_ssh_key capability" do
let(:has_insert_cap){ true }
let(:has_remove_cap){ true }
let(:new_public_key){ :new_public_key }
let(:new_private_key){ :new_private_key }
let(:openssh){ :openssh }
let(:private_key_file){ double("private_key_file") }
let(:path_joiner){ double("path_joiner") }
let(:algorithms) { double(:algorithms) }
let(:transport) { double(:transport, algorithms: algorithms) }
before do
allow(Vagrant::Util::Keypair).to receive(:create).
and_return([new_public_key, new_private_key, openssh])
allow(private_key_file).to receive(:open).and_yield(private_key_file)
allow(private_key_file).to receive(:write)
allow(private_key_file).to receive(:chmod)
allow(guest).to receive(:capability)
allow(File).to receive(:chmod)
allow(machine).to receive(:data_dir).and_return(path_joiner)
allow(path_joiner).to receive(:join).and_return(private_key_file)
allow(guest).to receive(:capability).with(:insert_public_key)
allow(guest).to receive(:capability).with(:remove_public_key)
allow(connection).to receive(:transport).and_return(transport)
allow(communicator).to receive(:supported_key_types).and_raise(described_class.const_get(:ServerDataError))
end
it "should create a new key pair" do
expect(Vagrant::Util::Keypair).to receive(:create).
and_return([new_public_key, new_private_key, openssh])
communicator.ready?
end
it "should call the insert_public_key guest capability" do
expect(guest).to receive(:capability).with(:insert_public_key, openssh)
communicator.ready?
end
it "should write the new private key" do
expect(private_key_file).to receive(:write).with(new_private_key)
communicator.ready?
end
it "should call the set_ssh_key_permissions host capability" do
expect(host).to receive(:capability?).with(:set_ssh_key_permissions).and_return(true)
expect(host).to receive(:capability).with(:set_ssh_key_permissions, private_key_file)
communicator.ready?
end
it "should remove the default public key" do
expect(guest).to receive(:capability).with(:remove_public_key, any_args)
communicator.ready?
end
context "with server algorithm support data" do
before do
allow(communicator).to receive(:supported_key_types).and_return(valid_key_types)
end
context "when rsa is the only match" do
let(:valid_key_types) { ["ssh-ecdsa", "ssh-rsa"] }
it "should use rsa type" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :rsa).and_call_original
communicator.ready?
end
end
context "when ed25519 and rsa are both available" do
let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa"] }
it "should use ed25519 type" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ed25519).and_call_original
communicator.ready?
end
end
context "when ed25519 is the only match" do
let(:valid_key_types) { ["ssh-ecdsa", "ssh-ed25519"] }
it "should use ed25519 type" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ed25519).and_call_original
communicator.ready?
end
end
context "with key_type set as :auto in configuration" do
let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa"] }
before { allow(ssh).to receive(:key_type).and_return(:auto) }
it "should use the preferred ed25519 key type" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ed25519).and_call_original
communicator.ready?
end
context "when no supported key type is detected" do
let(:valid_key_types) { ["fake-type", "other-fake-type"] }
it "should raise an error" do
expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer)
end
end
end
context "with key_type set as :ecdsa521 in configuration" do
let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp256"] }
before { allow(ssh).to receive(:key_type).and_return(:ecdsa521) }
it "should use the requested key type" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ecdsa521).and_call_original
communicator.ready?
end
context "when requested key type is not supported" do
let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256"] }
it "should raise an error" do
expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer)
end
end
end
end
context "when an error is encountered getting server data" do
before do
expect(communicator).to receive(:supported_key_types).and_raise(StandardError)
end
it "should default to rsa key" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :rsa).and_call_original
communicator.ready?
end
end
end
end
end
describe "#execute" do
before(&connection_setup)
it "runs valid command and returns successful status code" do
expect(command_channel).to receive(:send_data).with(/ls \/\n/)
expect(communicator.execute("ls /")).to eq(0)
end
it "prepends UUID output to command for garbage removal" do
expect(command_channel).to receive(:send_data).
with("printf '#{command_garbage_marker}'\n(>&2 printf '#{command_garbage_marker}')\nls /\n")
expect(communicator.execute("ls /")).to eq(0)
end
context "with command returning an error" do
let(:exit_data) { double("exit_data", read_long: 1) }
it "raises error when exit-code is non-zero" do
expect(command_channel).to receive(:send_data).with(/ls \/\n/)
expect{ communicator.execute("ls /") }.to raise_error(Vagrant::Errors::VagrantError)
end
it "returns exit-code when exit-code is non-zero and error check is disabled" do
expect(command_channel).to receive(:send_data).with(/ls \/\n/)
expect(communicator.execute("ls /", error_check: false)).to eq(1)
end
end
context "with no exit code received" do
let(:exit_data) { double("exit_data", read_long: nil) }
it "raises error when exit code is nil" do
expect(command_channel).to receive(:send_data).with(/make\n/)
expect{ communicator.execute("make") }.to raise_error(Vagrant::Errors::SSHNoExitStatus)
end
end
context "with garbage content prepended to command output" do
let(:command_stdout_data) do
"Line of garbage\nMore garbage\n#{command_garbage_marker}bin\ntmp\n"
end
let(:command_stderr_data) { "some data" }
it "removes any garbage output prepended to command output" do
stdout = ''
expect(
communicator.execute("ls /") do |type, data|
if type == :stdout
stdout << data
end
end
).to eq(0)
expect(stdout).to eq("bin\ntmp\n")
end
it "should not receive any stderr data" do
stderr = ''
communicator.execute("ls /") do |type, data|
if type == :stderr
stderr << data
end
end
expect(stderr).to be_empty
end
end
context "with no command output" do
let(:command_stdout_data) do
"#{command_garbage_marker}"
end
let(:command_stderr_data) { "some data" }
it "does not send empty stdout data string" do
empty = true
expect(
communicator.execute("ls /") do |type, data|
if type == :stdout && data.empty?
empty = false
end
end
).to eq(0)
expect(empty).to be(true)
end
it "should not receive any stderr data" do
stderr = ''
communicator.execute("ls /") do |type, data|
if type == :stderr
stderr << data
end
end
expect(stderr).to be_empty
end
end
context "with garbage content prepended to command stderr output" do
let(:command_stderr_data) do
"Line of garbage\nMore garbage\n#{command_garbage_marker}bin\ntmp\n"
end
let(:command_stdout_data) { "some data" }
it "removes any garbage output prepended to command stderr output" do
stderr = ''
expect(
communicator.execute("ls /") do |type, data|
if type == :stderr
stderr << data
end
end
).to eq(0)
expect(stderr).to eq("bin\ntmp\n")
end
it "should not receive any stdout data" do
stdout = ''
communicator.execute("ls /") do |type, data|
if type == :stdout
stdout << data
end
end
expect(stdout).to be_empty
end
end
context "with no command output on stderr" do
let(:command_stderr_data) do
"#{command_garbage_marker}"
end
let(:command_std_data) { "some data" }
it "does not send empty stderr data string" do
empty = true
expect(
communicator.execute("ls /") do |type, data|
if type == :stderr && data.empty?
empty = false
end
end
).to eq(0)
expect(empty).to be(true)
end
it "should not receive any stdout data" do
stdout = ''
communicator.execute("ls /") do |type, data|
if type == :stdout
stdout << data
end
end
expect(stdout).to be_empty
end
end
context "with pty enabled" do
before do
expect(ssh).to receive(:pty).and_return true
expect(channel).to receive(:request_pty).and_yield(command_channel, true)
expect(command_channel).to receive(:send_data).
with(/#{Regexp.escape(pty_delim_start)}/)
end
let(:command_stdout_data) do
"#{pty_delim_start}bin\ntmp\n#{pty_delim_end}"
end
it "requests pty for connection" do
expect(communicator.execute("ls")).to eq(0)
end
context "with sudo enabled" do
let(:core_shell_cmd){ 'sudo bash -l' }
before do
expect(ssh).to receive(:sudo_command).and_return 'sudo %c'
end
it "wraps command in elevated shell when sudo is true" do
expect(communicator.execute("ls", sudo: true)).to eq(0)
end
end
end
context "with sudo enabled" do
let(:core_shell_cmd){ 'sudo bash -l' }
before do
expect(ssh).to receive(:sudo_command).and_return 'sudo %c'
end
it "wraps command in elevated shell when sudo is true" do
expect(communicator.execute("ls", sudo: true)).to eq(0)
end
end
end
describe "#test" do
before(&connection_setup)
context "with exit code as zero" do
it "returns true" do
expect(communicator.test("ls")).to be(true)
end
end
context "with exit code as non-zero" do
before do
expect(exit_data).to receive(:read_long).and_return 1
end
it "returns false" do
expect(communicator.test("/bin/false")).to be(false)
end
end
end
describe "#upload" do
before do
expect(communicator).to receive(:scp_connect).and_yield(scp)
allow(communicator).to receive(:create_remote_directory)
end
context "directory uploads" do
let(:test_dir) { @dir }
let(:test_file) { File.join(test_dir, "test-file") }
let(:dir_name) { File.basename(test_dir) }
let(:file_name) { File.basename(test_file) }
before do
@dir = Dir.mktmpdir("vagrant-test")
FileUtils.touch(test_file)
end
after { FileUtils.rm_rf(test_dir) }
it "uploads directory when directory path provided" do
expect(scp).to receive(:upload!).with(instance_of(File),
File.join("", "destination", dir_name, file_name))
communicator.upload(test_dir, "/destination")
end
it "uploads contents of directory when dot suffix provided on directory" do
expect(scp).to receive(:upload!).with(instance_of(File),
File.join("", "destination", file_name))
communicator.upload(File.join(test_dir, "."), "/destination")
end
it "creates directories before upload" do
expect(communicator).to receive(:create_remote_directory).with(
/#{Regexp.escape(File.join("", "destination", dir_name))}/)
allow(scp).to receive(:upload!)
communicator.upload(test_dir, "/destination")
end
end
it "uploads a file if local path is a file" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), '/destination/file')
communicator.upload(file.path, '/destination/file')
ensure
file.delete
end
end
it "uploads file to directory if destination ends with file separator" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), "/destination/dir/#{File.basename(file.path)}")
expect(communicator).to receive(:create_remote_directory).with("/destination/dir")
communicator.upload(file.path, "/destination/dir/")
ensure
file.delete
end
end
it "creates remote directory path to destination on upload" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), "/destination/dir/file.txt")
expect(communicator).to receive(:create_remote_directory).with("/destination/dir")
communicator.upload(file.path, "/destination/dir/file.txt")
ensure
file.delete
end
end
it "creates remote directory given an empty directory" do
file = Dir.mktmpdir
begin
expect(communicator).to receive(:create_remote_directory).with("/destination/dir/#{ File.basename(file)}/")
communicator.upload(file, "/destination/dir")
ensure
FileUtils.rm_rf(file)
end
end
it "raises custom error on permission errors" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), '/destination/file').
and_raise("Permission denied")
expect{ communicator.upload(file.path, '/destination/file') }.to(
raise_error(Vagrant::Errors::SCPPermissionDenied)
)
ensure
file.delete
end
end
it "does not raise custom error on non-permission errors" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), '/destination/file').
and_raise("Some other error")
expect{ communicator.upload(file.path, '/destination/file') }.to raise_error(RuntimeError)
ensure
file.delete
end
end
end
describe "#download" do
before do
expect(communicator).to receive(:scp_connect).and_yield(scp)
end
it "calls scp to download file" do
expect(scp).to receive(:download!).with('/path/from', '/path/to')
communicator.download('/path/from', '/path/to')
end
end
describe "#connect" do
it "cannot be called directly" do
expect{ communicator.connect }.to raise_error(NoMethodError)
end
context "with default configuration" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: nil,
port: nil,
private_key_path: nil,
username: nil,
password: nil,
keys_only: true,
verify_host_key: false
)
end
it "has keys_only enabled" do
expect(Net::SSH).to receive(:start).with(
nil, nil, hash_including(
keys_only: true
)
).and_return(true)
communicator.send(:connect)
end
it "has verify_host_key disabled" do
expect(Net::SSH).to receive(:start).with(
nil, nil, hash_including(
verify_host_key: false
)
).and_return(true)
communicator.send(:connect)
end
it "does not include any private key paths" do
expect(Net::SSH).to receive(:start).with(
nil, nil, hash_excluding(
keys: anything
)
).and_return(true)
communicator.send(:connect)
end
it "includes `none` and `hostbased` auth methods" do
expect(Net::SSH).to receive(:start).with(
nil, nil, hash_including(
auth_methods: ["none", "hostbased"]
)
).and_return(true)
communicator.send(:connect)
end
context "keep ssh connection alive" do
let(:ssh) do
double("ssh",
timeout: 1,
host: nil,
port: 5986,
guest_port: 5986,
pty: false,
keep_alive: true,
insert_key: insert_ssh_key,
export_command_template: export_command_template,
shell: 'bash -l'
)
end
it "sets keepalive settings" do
expect(Net::SSH).to receive(:start).with(
nil, nil, hash_including(
keepalive: true,
keepalive_interval: 5
)
).and_return(true)
communicator.send(:connect)
end
end
end
context "with keys_only disabled and verify_host_key enabled" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: nil,
port: nil,
private_key_path: nil,
username: nil,
password: nil,
keys_only: false,
verify_host_key: true
)
end
it "has keys_only enabled" do
expect(Net::SSH).to receive(:start).with(
nil, nil, hash_including(
keys_only: false
)
).and_return(true)
communicator.send(:connect)
end
it "has verify_host_key disabled" do
expect(Net::SSH).to receive(:start).with(
nil, nil, hash_including(
verify_host_key: true
)
).and_return(true)
communicator.send(:connect)
end
end
context "with host and port configured" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: '127.0.0.1',
port: 2222,
private_key_path: nil,
username: nil,
password: nil,
keys_only: true,
verify_host_key: false
)
end
it "specifies configured host" do
expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, anything)
communicator.send(:connect)
end
it "has port defined" do
expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, hash_including(port: 2222))
communicator.send(:connect)
end
end
context "with private_key_path configured" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: '127.0.0.1',
port: 2222,
private_key_path: ['/priv/key/path'],
username: nil,
password: nil,
keys_only: true,
verify_host_key: false
)
end
it "includes private key paths" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
keys: ["/priv/key/path"]
)
).and_return(true)
communicator.send(:connect)
end
it "includes `publickey` auth method" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
auth_methods: ["none", "hostbased", "publickey"]
)
).and_return(true)
communicator.send(:connect)
end
end
context "with username and password configured" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: '127.0.0.1',
port: 2222,
private_key_path: nil,
username: 'vagrant',
password: 'vagrant',
keys_only: true,
verify_host_key: false
)
end
it "has username defined" do
expect(Net::SSH).to receive(:start).with(anything, 'vagrant', anything).and_return(true)
communicator.send(:connect)
end
it "has password defined" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
password: 'vagrant'
)
).and_return(true)
communicator.send(:connect)
end
it "includes `password` auth method" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
auth_methods: ["none", "hostbased", "password"]
)
).and_return(true)
communicator.send(:connect)
end
end
context "with password and private_key_path configured" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: '127.0.0.1',
port: 2222,
private_key_path: ['/priv/key/path'],
username: 'vagrant',
password: 'vagrant',
keys_only: true,
verify_host_key: false
)
end
it "has password defined" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
password: 'vagrant'
)
).and_return(true)
communicator.send(:connect)
end
it "includes private key paths" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
keys: ["/priv/key/path"]
)
).and_return(true)
communicator.send(:connect)
end
it "includes `publickey` and `password` auth methods" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
auth_methods: ["none", "hostbased", "publickey", "password"]
)
).and_return(true)
communicator.send(:connect)
end
end
context "with config configured" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: '127.0.0.1',
port: 2222,
config: './ssh_config',
keys_only: true,
verify_host_key: false
)
end
it "has config defined" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
config: './ssh_config'
)
).and_return(true)
communicator.send(:connect)
end
end
context "with remote_user configured" do
let(:remote_user) { double("remote_user") }
before do
expect(machine).to receive(:ssh_info).and_return(
host: '127.0.0.1',
port: 2222,
remote_user: remote_user
)
end
it "has remote_user defined" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
remote_user: remote_user
)
).and_return(true)
communicator.send(:connect)
end
end
context "with connect_timeout configured" do
before do
expect(machine).to receive(:ssh_info).and_return(
host: '127.0.0.1',
port: 2222,
connect_timeout: 30
)
end
it "has connect_timeout defined" do
expect(Net::SSH).to receive(:start).with(
anything, anything, hash_including(
timeout: 30
)
).and_return(true)
communicator.send(:connect)
end
end
end
describe "#insecure_key?" do
let(:key_data) { "" }
let(:key_file) {
if !@key_file
f = Tempfile.new
f.write(key_data)
f.close
@key_file = f.path
end
@key_file
}
after { File.delete(key_file) }
context "when using rsa private key" do
let(:key_data) { File.read(Vagrant.source_root.join("keys", "vagrant.key.rsa")) }
it "should match as insecure key" do
expect(communicator.send(:insecure_key?, key_file)).to be_truthy
end
end
context "when using ed25519 private key" do
let(:key_data) { File.read(Vagrant.source_root.join("keys", "vagrant.key.ed25519")) }
it "should match as insecure key" do
expect(communicator.send(:insecure_key?, key_file)).to be_truthy
end
end
context "when using unknown private key" do
let(:key_data) { "invalid data" }
it "should not match as insecure key" do
expect(communicator.send(:insecure_key?, key_file)).to be_falsey
end
end
end
describe "#generate_environment_export" do
it "should generate bourne shell compatible export" do
expect(communicator.send(:generate_environment_export, "TEST", "value")).to eq("export TEST=\"value\"\n")
end
context "with custom template defined" do
let(:export_command_template){ "setenv %ENV_KEY% %ENV_VALUE%" }
it "should generate custom export based on template" do
expect(communicator.send(:generate_environment_export, "TEST", "value")).to eq("setenv TEST value\n")
end
end
end
describe "#supported_key_types" do
let(:sudo_result) { 0 }
let(:sudo_data) { "" }
let(:server_data_error) { VagrantPlugins::CommunicatorSSH::Communicator::ServerDataError }
let(:transport) { double("transport", algorithms: algorithms) }
let(:algorithms) { double("algorithms") }
before do
allow(communicator).to receive(:ready?).and_return(true)
expect(communicator).to receive(:sudo).
with("sshd -T | grep key", any_args).
and_yield(:stdout, sudo_data).
and_return(sudo_result)
# The @connection value is checked to determine if supported key types
# can be checked. To facilitate this, set it to a non-nil value
communicator.instance_variable_set(:@connection, connection)
allow(connection).to receive(:transport).and_return(transport)
end
it "should raise an error when no data is returned" do
expect { communicator.send(:supported_key_types) }.to raise_error(server_data_error)
end
context "when sudo command is unsuccessful" do
let(:sudo_result) { 1 }
it "should inspect the net-ssh connection" do
expect(algorithms).to receive(:instance_variable_get).
with(:@server_data).and_return({})
communicator.send(:supported_key_types)
end
end
context "when data includes pubkeyacceptedalgorithms" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
pubkeyacceptedalgorithms rsa-sha2-512,rsa-sha2-256,ssh-rsa
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end
it "should return expected values" do
expect(communicator.send(:supported_key_types)).to eq(["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"])
end
end
context "when data includes pubkeyacceptedkeytypes" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
pubkeyacceptedkeytypes rsa-sha2-512,rsa-sha2-256,ssh-rsa
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end
it "should return expected values" do
expect(communicator.send(:supported_key_types)).
to eq(["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"])
end
end
context "when data does not include pubkeyacceptedalgorithms or pubkeyacceptedkeytypes" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end
it "should use hostkeyalgorithms" do
expect(communicator.send(:supported_key_types)).
to eq(["ssh-ed25519", "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"])
end
end
context "when data does not include defined config options" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end
it "should raise error" do
expect { communicator.send(:supported_key_types) }.
to raise_error(server_data_error)
end
end
end
end