diff options
-rw-r--r-- | lib/mixlib/shellout.rb | 20 | ||||
-rw-r--r-- | lib/mixlib/shellout/unix.rb | 51 | ||||
-rw-r--r-- | spec/mixlib/shellout_spec.rb | 84 |
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 |