Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare train for using credential sets #394

Merged
merged 5 commits into from
Jan 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 61 additions & 46 deletions lib/train.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,53 +61,61 @@ def self.load_transport(transport_name)
raise ex
end

# Resolve target configuration in URI-scheme into
# all respective fields and merge with existing configuration.
# e.g. ssh://bob@remote => backend: ssh, user: bob, host: remote
def self.target_config(config = nil) # rubocop:disable Metrics/AbcSize
conf = config.nil? ? {} : config.dup
conf = symbolize_keys(conf)

group_keys_and_keyfiles(conf)
# Legacy code to unpack a series of items from an incoming Hash
# Inspec::Config.unpack_train_credentials now handles this in most cases that InSpec needs
# If you need to unpack a URI, use unpack_target_from_uri
# TODO: deprecate; can't issue a warning because train doesn't have a logger until the connection is setup (See base_connection.rb)
def self.target_config(config = nil)
clintoncwolfe marked this conversation as resolved.
Show resolved Hide resolved
conf = config.dup
# Symbolize keys
conf.keys.each do |key|
clintoncwolfe marked this conversation as resolved.
Show resolved Hide resolved
unless key.is_a? Symbol
conf[key.to_sym] = conf.delete(key)
end
end

group_keys_and_keyfiles(conf) # TODO: move logic into SSH plugin
return conf if conf[:target].to_s.empty?
unpack_target_from_uri(conf[:target], conf).merge(conf)
end

# Given a string that looks like a URI, unpack connection credentials.
# The name of the desired transport is always taken from the 'scheme' slot of the URI;
# the remaining portion of the URI is parsed as if it were an HTTP URL, and then
# the URL components are stored in the credentials hash. It is up to the transport
# to interpret the fields in a sensible way for that transport.
# New transport authors are encouraged to use transport://credset format (see
# inspec/inspec/issues/3661) rather than inventing a new field mapping.
def self.unpack_target_from_uri(uri_string, opts = {}) # rubocop: disable Metrics/AbcSize
clintoncwolfe marked this conversation as resolved.
Show resolved Hide resolved
creds = {}
return creds if uri_string.empty?

# split up the target's host/scheme configuration
uri = parse_uri(conf[:target].to_s)
uri = parse_uri(uri_string)
unless uri.host.nil? and uri.scheme.nil?
conf[:backend] ||= uri.scheme
conf[:host] ||= uri.hostname
conf[:port] ||= uri.port
conf[:user] ||= uri.user
conf[:path] ||= uri.path
conf[:password] ||=
if conf[:www_form_encoded_password] && !uri.password.nil?
creds[:backend] ||= uri.scheme
creds[:host] ||= uri.hostname
creds[:port] ||= uri.port
creds[:user] ||= uri.user
creds[:path] ||= uri.path
creds[:password] ||=
if opts[:www_form_encoded_password] && !uri.password.nil?
URI.decode_www_form_component(uri.password)
else
uri.password
end
end

# ensure path is nil, if its empty; e.g. required to reset defaults for winrm
conf[:path] = nil if !conf[:path].nil? && conf[:path].to_s.empty?
# ensure path is nil, if its empty; e.g. required to reset defaults for winrm # TODO: move logic into winrm plugin
creds[:path] = nil if !creds[:path].nil? && creds[:path].to_s.empty?

# return the updated config
conf
end
# compact! is available in ruby 2.4+
# TODO: rewrite next line using compact! once we drop support for ruby 2.3
creds = creds.delete_if { |_, value| value.nil? }

# Takes a map of key-value pairs and turns all keys into symbols. For this
# to work, only keys are supported that can be turned into symbols.
# Example: { 'a' => 123 } ==> { a: 123 }
#
# @param map [Hash]
# @return [Hash] new map with all keys being symbols
def self.symbolize_keys(map)
map.each_with_object({}) do |(k, v), acc|
acc[k.to_sym] = v
acc
end
# return the updated config
creds
end
private_class_method :symbolize_keys

# Parse a URI. Supports empty URI's with paths, e.g. `mock://`
#
Expand All @@ -127,34 +135,41 @@ def self.parse_uri(string)
raise Train::UserError, e
end

u = URI.parse(string)
u.host = nil
u
uri = URI.parse(string)
uri.host = nil
uri
end
private_class_method :parse_uri

def self.validate_backend(conf, default = :local)
return default if conf.nil?
res = conf[:backend]
# Examine the given credential information, and if all is well,
# return the transport name.
# TODO: this actually does no validation of the credential options whatsoever
def self.validate_backend(credentials, default_transport_name = 'local')
return default_transport_name if credentials.nil?
transport_name = credentials[:backend]

if (res.nil? || res == 'localhost') && conf[:sudo]
# TODO: Determine if it is ever possible (or supported) for transport_name to be 'localhost'
# TODO: After inspec/inspec/pull/3750 is merged, should be able to remove nil from the list
if credentials[:sudo] && [nil, 'local', 'localhost'].include?(transport_name)
fail Train::UserError, 'Sudo is only valid when running against a remote host. '\
'To run this locally with elevated privileges, run the command with `sudo ...`.'
end

return res if !res.nil?
return transport_name if !transport_name.nil?

if !conf[:target].nil?
if !credentials[:target].nil?
# We should not get here, because if target_uri unpacking was successful,
# it would have set credentials[:backend]
fail Train::UserError, 'Cannot determine backend from target '\
"configuration #{conf[:target].inspect}. Valid example: ssh://192.168.0.1."
"configuration #{credentials[:target]}. Valid example: ssh://192.168.0.1"
end

if !conf[:host].nil?
if !credentials[:host].nil?
fail Train::UserError, 'Host configured, but no backend was provided. Please '\
'specify how you want to connect. Valid example: ssh://192.168.0.1.'
'specify how you want to connect. Valid example: ssh://192.168.0.1'
end

conf[:backend] = default
credentials[:backend] = default_transport_name
end

def self.group_keys_and_keyfiles(conf)
Expand Down
1 change: 1 addition & 0 deletions test/unit/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
require 'minitest/autorun'
require 'minitest/spec'
require 'mocha/setup'
require 'byebug'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! It's in the helper now 😄


require 'train'
24 changes: 12 additions & 12 deletions test/unit/train_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
end
end

describe '#target_config' do
describe '#target_config - URI parsing' do
it 'configures resolves target' do
org = {
target: 'ssh://user:pass@host.com:123/path',
Expand Down Expand Up @@ -124,7 +124,7 @@
res[:target].must_equal org[:target]
end

it 'always takes ruby sumbols as configuration fields' do
it 'always transforms config fields into ruby symbols' do
org = {
'target' => 'ssh://user:pass@host.com:123/path',
'backend' => rand,
Expand Down Expand Up @@ -192,15 +192,15 @@
it 'supports www-form encoded passwords when the option is set' do
raw_password = '+!@#$%^&*()_-\';:"\\|/?.>,<][}{=`~'
encoded_password = URI.encode_www_form_component(raw_password)
org = { target: "mock://username:#{encoded_password}@1.2.3.4:100",
www_form_encoded_password: true}
res = Train.target_config(org)
res[:backend].must_equal 'mock'
res[:host].must_equal '1.2.3.4'
res[:user].must_equal 'username'
res[:password].must_equal raw_password
res[:port].must_equal 100
res[:target].must_equal org[:target]
orig = { target: "mock://username:#{encoded_password}@1.2.3.4:100",
www_form_encoded_password: true}
result = Train.target_config(orig)
result[:backend].must_equal 'mock'
result[:host].must_equal '1.2.3.4'
result[:user].must_equal 'username'
result[:password].must_equal raw_password
result[:port].must_equal 100
result[:target].must_equal orig[:target]
end

it 'ignores www-form-encoded password value when there is no password' do
Expand Down Expand Up @@ -228,7 +228,7 @@
end

it 'returns the local backend if nothing was provided' do
Train.validate_backend({}).must_equal :local
Train.validate_backend({}).must_equal 'local'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all backend driver names are strings, how did this ever pass as symbols?

end

it 'returns the default backend if nothing was provided' do
Expand Down