summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/mixlib/shellout.rb20
-rw-r--r--lib/mixlib/shellout/unix.rb51
-rw-r--r--spec/mixlib/shellout_spec.rb84
3 files changed, 150 insertions, 5 deletions
diff --git a/lib/mixlib/shellout.rb b/lib/mixlib/shellout.rb
index e0fb3cc..92660c8 100644
--- a/lib/mixlib/shellout.rb
+++ b/lib/mixlib/shellout.rb
@@ -40,8 +40,13 @@ module Mixlib
attr_accessor :user
attr_accessor :domain
attr_accessor :password
+ # TODO remove
attr_accessor :with_logon
+ # Whether to simulate logon as the user. Normally set via options passed to new
+ # Always enabled on windows
+ attr_accessor :login
+
# Group the command will run as. Normally set via options passed to new
attr_accessor :group
@@ -141,6 +146,8 @@ module Mixlib
# child process. Generally this is used to copy data from the child to
# the parent's stdout so that users may observe the progress of
# long-running commands.
+ # * +login+: Whether to simulate a login (set secondary groups, primary group, environment
+ # variables etc) as done by the OS in an actual login
# === Examples:
# Invoke find(1) to search for .rb files:
# find = Mixlib::ShellOut.new("find . -name '*.rb'")
@@ -192,6 +199,7 @@ module Mixlib
# The uid that the subprocess will switch to. If the user attribute was
# given as a username, it is converted to a uid by Etc.getpwnam
+ # TODO migrate to shellout/unix.rb
def uid
return nil unless user
user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid
@@ -199,9 +207,11 @@ module Mixlib
# The gid that the subprocess will switch to. If the group attribute is
# given as a group name, it is converted to a gid by Etc.getgrnam
+ # TODO migrate to shellout/unix.rb
def gid
- return nil unless group
- group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid
+ return group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid if group
+ return Etc.getpwuid(uid).gid if using_login?
+ return nil
end
def timeout
@@ -322,7 +332,8 @@ module Mixlib
self.log_tag = setting
when 'environment', 'env'
self.environment = setting || {}
-
+ when 'login'
+ self.login = setting
else
raise InvalidCommandOption, "option '#{option.inspect}' is not a valid option for #{self.class.name}"
end
@@ -332,6 +343,9 @@ module Mixlib
end
def validate_options(opts)
+ if login && !user
+ raise InvalidCommandOption, "cannot set login without specifying a user"
+ end
super
end
end
diff --git a/lib/mixlib/shellout/unix.rb b/lib/mixlib/shellout/unix.rb
index 40d7efa..d8063df 100644
--- a/lib/mixlib/shellout/unix.rb
+++ b/lib/mixlib/shellout/unix.rb
@@ -30,6 +30,47 @@ module Mixlib
# No options to validate, raise exceptions here if needed
end
+ # Whether we're simulating a login shell
+ def using_login?
+ return login && user
+ end
+
+ # Helper method for sgids
+ def all_seconderies
+ ret = []
+ Etc.endgrent
+ while ( g = Etc.getgrent ) do
+ ret << g
+ end
+ Etc.endgrent
+ return ret
+ end
+
+ # The secondary groups that the subprocess will switch to.
+ # Currently valid only if login is used, and is set
+ # to the user's secondary groups
+ def sgids
+ return nil unless using_login?
+ user_name = Etc.getpwuid(uid).name
+ all_seconderies.select{|g| g.mem.include?(user_name)}.map{|g|g.gid}
+ end
+
+ # The environment variables that are deduced from simulating logon
+ # Only valid if login is used
+ def logon_environment
+ return {} unless using_login?
+ entry = Etc.getpwuid(uid)
+ # According to `man su`, the set fields are:
+ # $HOME, $SHELL, $USER, $LOGNAME, $PATH, and $IFS
+ # Values are copied from "shadow" package in Ubuntu 14.10
+ {'HOME'=>entry.dir, 'SHELL'=>entry.shell, 'USER'=>entry.name, 'LOGNAME'=>entry.name, 'PATH'=>'/sbin:/bin:/usr/sbin:/usr/bin', 'IFS'=>"\t\n"}
+ end
+
+ # Merges the two environments for the process
+ def process_environment
+ logon_environment.merge(self.environment)
+ end
+
# Run the command, writing the command's standard out and standard error
# to +stdout+ and +stderr+, and saving its exit status object to +status+
# === Returns
@@ -133,8 +174,15 @@ module Mixlib
end
end
+ def set_secondarygroups
+ if sgids
+ Process.groups = sgids
+ end
+ end
+
def set_environment
- environment.each do |env_var,value|
+ # user-set variables should override the login ones
+ process_environment.each do |env_var,value|
ENV[env_var] = value
end
end
@@ -295,6 +343,7 @@ module Mixlib
configure_subprocess_file_descriptors
+ set_secondarygroups
set_group
set_user
set_environment
diff --git a/spec/mixlib/shellout_spec.rb b/spec/mixlib/shellout_spec.rb
index b1d7031..1765026 100644
--- a/spec/mixlib/shellout_spec.rb
+++ b/spec/mixlib/shellout_spec.rb
@@ -34,6 +34,7 @@ describe Mixlib::ShellOut do
its(:cwd) { should be_nil }
its(:user) { should be_nil }
its(:with_logon) { should be_nil }
+ its(:login) { should be_nil }
its(:domain) { should be_nil }
its(:password) { should be_nil }
its(:group) { should be_nil }
@@ -63,6 +64,7 @@ describe Mixlib::ShellOut do
should eql(value)
end
+ # TODO add :unix_only
context 'with an integer value for user' do
let(:value) { 0 }
it "should use the user-supplied uid" do
@@ -70,6 +72,7 @@ describe Mixlib::ShellOut do
end
end
+ # TODO add :unix_only
context 'with string value for user' do
let(:value) { username }
@@ -92,6 +95,15 @@ describe Mixlib::ShellOut do
end
end
+ context 'when setting login' do
+ let(:accessor) { :login }
+ let(:value) { true }
+
+ it "should set the login" do
+ should eql(value)
+ end
+ end
+
context 'when setting domain' do
let(:accessor) { :domain }
let(:value) { 'localhost' }
@@ -118,6 +130,7 @@ describe Mixlib::ShellOut do
should eql(value)
end
+ # TODO add :unix_only
context 'with integer value for group' do
let(:value) { 0 }
it "should use the user-supplied gid" do
@@ -263,15 +276,80 @@ describe Mixlib::ShellOut do
end
end
+ context 'testing login', :unix_only do
+ subject {shell_cmd}
+ let (:uid) {1005}
+ let (:gid) {1002}
+ let (:shell) {'/bin/money'}
+ let (:dir) {'/home/castle'}
+ let (:path) {'/sbin:/bin:/usr/sbin:/usr/bin'}
+ before :each do
+ shell_cmd.login=true
+ catbert_user=double("Etc::Passwd", :name=>'catbert', :passwd=>'x', :uid=>1005, :gid=>1002, :gecos=>"Catbert,,,", :dir=>'/home/castle', :shell=>'/bin/money')
+ group_double=[
+ double("Etc::Group", :name=>'catbert', :passwd=>'x', :gid=>1002, :mem=>[]),
+ double("Etc::Group", :name=>'sudo', :passwd=>'x', :gid=>52, :mem=>['catbert']),
+ double("Etc::Group", :name=>'rats', :passwd=>'x', :gid=>43, :mem=>['ratbert']),
+ double("Etc::Group", :name=>'dilbertpets', :passwd=>'x', :gid=>700, :mem=>['catbert', 'ratbert']),
+ ]
+ Etc.stub(:getpwuid).with(1005) {catbert_user}
+ Etc.stub(:getpwnam).with('catbert') {catbert_user}
+ shell_cmd.stub(:all_seconderies) {group_double}
+ end
+
+ # Setting the user by name should change the uid
+ context 'when setting user by name' do
+ before(:each){ shell_cmd.user='catbert' }
+ its(:uid) { should eq(uid) }
+ end
+
+ context 'when setting user by id' do
+ before(:each){shell_cmd.user=uid}
+ # Setting the user by uid should change the uid
+ #it 'should set the uid' do
+ its(:uid) { should eq(uid) }
+ #end
+ # Setting the user without a different gid should change the gid to 1002
+ its(:gid) { should eq(gid) }
+ # Setting the user and the group (to 43) should change the gid to 43
+ context 'when setting the group manually' do
+ before(:each){shell_cmd.group=43}
+ its(:gid) {should eq(43)}
+ end
+ # Setting the user should set the env variables
+ its(:process_environment) { should eq ({'HOME'=>dir, 'SHELL'=>shell, 'USER'=>'catbert', 'LOGNAME'=>'catbert', 'PATH'=>path, 'IFS'=>"\t\n"}) }
+ # Setting the user with overriding env variables should override
+ context 'when adding environment variables' do
+ before(:each){shell_cmd.environment={'PATH'=>'/lord:/of/the/dance', 'CUSTOM'=>'costume'}}
+ it 'should preserve custom variables' do
+ expect(shell_cmd.process_environment['PATH']).to eq('/lord:/of/the/dance')
+ end
+ # Setting the user with additional env variables should have both
+ it 'should allow new variables' do
+ expect(shell_cmd.process_environment['CUSTOM']).to eq('costume')
+ end
+ end
+ # Setting the user should set secondary groups
+ its(:sgids) { should =~ [52,700] }
+ end
+ # Setting login with user should throw errors
+ context 'when not setting a user id' do
+ it 'should fail showing an error' do
+ lambda { Mixlib::ShellOut.new('hostname', {login:true}) }.should raise_error(Mixlib::ShellOut::InvalidCommandOption)
+ end
+ end
+ end
+
context "with options hash" do
let(:cmd) { 'brew install couchdb' }
- let(:options) { { :cwd => cwd, :user => user, :domain => domain, :password => password, :group => group,
+ let(:options) { { :cwd => cwd, :user => user, :login => true, :domain => domain, :password => password, :group => group,
:umask => umask, :timeout => timeout, :environment => environment, :returns => valid_exit_codes,
:live_stream => stream, :input => input } }
let(:cwd) { '/tmp' }
let(:user) { 'toor' }
let(:with_logon) { user }
+ let(:login) { true }
let(:domain) { 'localhost' }
let(:password) { 'vagrant' }
let(:group) { 'wheel' }
@@ -294,6 +372,10 @@ describe Mixlib::ShellOut do
shell_cmd.with_logon.should eql(with_logon)
end
+ it "should set the login" do
+ shell_cmd.login.should eql(login)
+ end
+
it "should set the domain" do
shell_cmd.domain.should eql(domain)
end