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

Sudo #12

Merged
merged 12 commits into from
Oct 21, 2015
9 changes: 5 additions & 4 deletions lib/train/extras.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
# Author:: Dominik Richter (<dominik.richter@gmail.com>)

module Train::Extras
autoload :FileCommon, 'train/extras/file_common'
autoload :LinuxFile, 'train/extras/linux_file'
autoload :OSCommon, 'train/extras/os_common'
autoload :Stat, 'train/extras/stat'
autoload :CommandWrapper, 'train/extras/command_wrapper'
autoload :FileCommon, 'train/extras/file_common'
autoload :LinuxFile, 'train/extras/linux_file'
autoload :OSCommon, 'train/extras/os_common'
autoload :Stat, 'train/extras/stat'

CommandResult = Struct.new(:stdout, :stderr, :exit_status)
LoginCommand = Struct.new(:command, :arguments)
Expand Down
63 changes: 63 additions & 0 deletions lib/train/extras/command_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann

require 'base64'

module Train::Extras
class LinuxCommand
Train::Options.attach(self)

option :sudo, default: false
option :sudo_options, default: nil
option :sudo_password, default: nil
option :user

def initialize(backend, options)
@backend = backend
validate_options(options)

@sudo = options[:sudo]
@sudo_options = options[:sudo_options]
@sudo_password = options[:sudo_password]
@user = options[:user]
@prefix = build_prefix
end

def run(command)
@prefix + command
end

def self.active?(options)
options.is_a?(Hash) && options[:sudo]
end

private

def build_prefix
return '' unless @sudo
return '' if @user == 'root'

res = 'sudo '

unless @sudo_password.nil?
b64pw = Base64.strict_encode64(@sudo_password + "\n")
res = "echo #{b64pw} | base64 -d | sudo -S "
end

res << @sudo_options.to_s + ' ' unless @sudo_options.nil?

res
end
end

class CommandWrapper
include_options LinuxCommand

def self.load(transport, options)
return nil unless LinuxCommand.active?(options)
return nil unless transport.os.unix?
LinuxCommand.new(transport, options)
end
end
end
4 changes: 4 additions & 0 deletions lib/train/transports/docker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Train::Transports
class Docker < Train.plugin(1)
name 'docker'

include_options Train::Extras::CommandWrapper
option :host, required: true

def connection(state = {}, &block)
Expand Down Expand Up @@ -60,6 +61,8 @@ def initialize(conf)
@container = ::Docker::Container.get(@id) ||
fail("Can't find Docker container #{@id}")
@files = {}
@cmd_wrapper = nil
@cmd_wrapper = CommandWrapper.load(self, @options)
self
end

Expand All @@ -76,6 +79,7 @@ def file(path)
end

def run_command(cmd)
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
stdout, stderr, exit_status = @container.exec([
'/bin/sh', '-c', cmd
])
Expand Down
11 changes: 8 additions & 3 deletions lib/train/transports/local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,25 @@ module Train::Transports
class Local < Train.plugin(1)
name 'local'

include_options Train::Extras::CommandWrapper

autoload :File, 'train/transports/local_file'
autoload :OS, 'train/transports/local_os'

def connection(_ = nil)
@connection ||= Connection.new
@connection ||= Connection.new(@options)
end

class Connection < BaseConnection
def initialize
super
def initialize(options)
super(options)
@files = {}
@cmd_wrapper = nil
@cmd_wrapper = CommandWrapper.load(self, options)
end

def run_command(cmd)
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
res = Mixlib::ShellOut.new(cmd)
res.run_command
CommandResult.new(res.stdout, res.stderr, res.exitstatus)
Expand Down
4 changes: 4 additions & 0 deletions lib/train/transports/ssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class SSH < Train.plugin(1)

autoload :Connection, 'train/transports/ssh_connection'

# add options for submodules
include_options Train::Extras::CommandWrapper

# common target configuration
option :host, required: true
option :port, default: 22, required: true
Expand Down Expand Up @@ -126,6 +129,7 @@ def connection_options(opts)
keys: opts[:key_files],
password: opts[:password],
forward_agent: opts[:forward_agent],
transport_options: opts,
}
end

Expand Down
6 changes: 6 additions & 0 deletions lib/train/transports/ssh_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def initialize(options)
@max_wait_until_ready = @options.delete(:max_wait_until_ready)
@files = {}
@session = nil
@transport_options = @options.delete(:transport_options)
@cmd_wrapper = nil
@cmd_wrapper = CommandWrapper.load(self, @transport_options)
end

# (see Base::Connection#close)
Expand Down Expand Up @@ -66,6 +69,9 @@ def run_command(cmd)
logger.debug("[SSH] #{self} (#{cmd})")

session.open_channel do |channel|
# wrap commands if that is configured
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?

channel.exec(cmd) do |_, success|
abort 'Couldn\'t execute command on SSH.' unless success

Expand Down
4 changes: 4 additions & 0 deletions test/integration/.kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,9 @@ platforms:
suites:
- name: default
run_list:
- recipe[sudo]
- recipe[test]
attributes:
authorization:
sudo:
include_sudoers_d: true
3 changes: 3 additions & 0 deletions test/integration/Berksfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source 'https://supermarket.chef.io'
cookbook 'sudo', '~> 2.7.2'
cookbook 'test', path: 'cookbooks/test'
1 change: 1 addition & 0 deletions test/integration/cookbooks/test/metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name 'test'
28 changes: 28 additions & 0 deletions test/integration/cookbooks/test/recipes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@
command 'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa vagrant@localhost "echo 1"'
end

# prepare a few users
%w{ nopasswd passwd nosudo }.each do |name|
user name do
password '$1$7MCNTXPI$r./jqCEoVlLlByYKSL3sZ.'
manage_home true
end
end

%w{nopasswd vagrant}.each do |name|
sudo name do
user '%'+name
nopasswd true
end
end

sudo 'passwd' do
user 'passwd'
nopasswd false
end

# execute tests
execute 'bundle install' do
command '/opt/chef/embedded/bin/bundle install'
Expand All @@ -71,3 +91,11 @@
command '/opt/chef/embedded/bin/ruby -I lib test/integration/test_ssh.rb test/integration/tests/*_test.rb'
cwd '/tmp/kitchen/data'
end

%w{passwd nopasswd}.each do |name|
execute "run local sudo tests as #{name}" do
command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/#{name}.rb"
cwd '/tmp/kitchen/data'
user name
end
end
4 changes: 2 additions & 2 deletions test/integration/docker_test_container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
puts ''

backends = {}
backends[:docker] = proc {
backends[:docker] = proc { |*args|
opt = Train.target_config({ host: container_id })
Train.create('docker', opt).connection
Train.create('docker', opt).connection(args[0])
}

backends.each do |type, get_backend|
Expand Down
16 changes: 16 additions & 0 deletions test/integration/sudo/nopasswd.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann

require_relative 'run_as'

describe 'run_command' do
it 'is running as non-root without sudo' do
run_as('whoami').stdout.wont_match /root/i
end

it 'is running nopasswd sudo' do
run_as('whoami', { sudo: true })
.stdout.must_match /root/i
end
end
21 changes: 21 additions & 0 deletions test/integration/sudo/passwd.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann

require_relative 'run_as'

describe 'run_command' do
it 'is running as non-root without sudo' do
run_as('whoami').stdout.wont_match /root/i
end

it 'is not running sudo without password' do
run_as('whoami', { sudo: true })
.exit_status.wont_equal 0
end

it 'is running passwd sudo' do
run_as('whoami', { sudo: true, sudo_password: 'password' })
.stdout.must_match /root/i
end
end
12 changes: 12 additions & 0 deletions test/integration/sudo/run_as.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann

require_relative '../helper'
require 'train'

def run_as(cmd, opts = {})
Train.create('local', opts)
.connection
.run_command(cmd)
end
4 changes: 2 additions & 2 deletions test/integration/test_local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

backends = {}

backends[:local] = proc {
Train.create('local', {}).connection
backends[:local] = proc { |*opts|
Train.create('local', {}).connection(opts[0])
}

tests = ARGV
Expand Down
4 changes: 2 additions & 2 deletions test/integration/test_ssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
'key_files' => '/root/.ssh/id_rsa',
}

backends[:ssh] = proc {
backends[:ssh] = proc { |*args|
conf = Train.target_config(backend_conf)
Train.create('ssh', conf).connection
Train.create('ssh', conf).connection(args[0])
}

tests = ARGV
Expand Down
41 changes: 41 additions & 0 deletions test/unit/extras/command_wrapper_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann

require 'helper'
require 'train/transports/mock'
require 'train/extras'
require 'base64'

describe 'linux command' do
let(:cls) { Train::Extras::LinuxCommand }
let(:cmd) { rand.to_s }
let(:backend) {
backend = Train::Transports::Mock.new.connection
backend.mock_os({ family: 'linux' })
backend
}

it 'wraps commands in sudo' do
lc = cls.new(backend, { sudo: true })
lc.run(cmd).must_equal "sudo #{cmd}"
end

it 'doesnt wrap commands in sudo if user == root' do
lc = cls.new(backend, { sudo: true, user: 'root' })
lc.run(cmd).must_equal cmd
end

it 'wraps commands in sudo with all options' do
opts = rand.to_s
lc = cls.new(backend, { sudo: true, sudo_options: opts })
lc.run(cmd).must_equal "sudo #{opts} #{cmd}"
end

it 'runs commands in sudo with password' do
pw = rand.to_s
lc = cls.new(backend, { sudo: true, sudo_password: pw })
bpw = Base64.strict_encode64(pw + "\n")
lc.run(cmd).must_equal "echo #{bpw} | base64 -d | sudo -S #{cmd}"
end
end