diff --git a/lib/puppet/util/suidmanager.rb b/lib/puppet/util/suidmanager.rb index 82524d031..c7bab2377 100644 --- a/lib/puppet/util/suidmanager.rb +++ b/lib/puppet/util/suidmanager.rb @@ -1,168 +1,158 @@ require 'puppet/util/warnings' require 'forwardable' module Puppet::Util::SUIDManager include Puppet::Util::Warnings extend Forwardable # Note groups= is handled specially due to a bug in OS X 10.6, 10.7, # and probably upcoming releases... to_delegate_to_process = [ :euid=, :euid, :egid=, :egid, :uid=, :uid, :gid=, :gid, :groups ] to_delegate_to_process.each do |method| def_delegator Process, method module_function method end def osx_maj_ver return @osx_maj_ver unless @osx_maj_ver.nil? require 'facter' # 'kernel' is available without explicitly loading all facts if Facter.value('kernel') != 'Darwin' @osx_maj_ver = false return @osx_maj_ver end # But 'macosx_productversion_major' requires it. Facter.loadfacts @osx_maj_ver = Facter.value('macosx_productversion_major') end module_function :osx_maj_ver def groups=(grouplist) begin return Process.groups = grouplist rescue Errno::EINVAL => e #We catch Errno::EINVAL as some operating systems (OS X in particular) can # cause troubles when using Process#groups= to change *this* user / process # list of supplementary groups membership. This is done via Ruby's function # "static VALUE proc_setgroups(VALUE obj, VALUE ary)" which is effectively # a wrapper for "int setgroups(size_t size, const gid_t *list)" (part of SVr4 # and 4.3BSD but not in POSIX.1-2001) that fails and sets errno to EINVAL. # # This does not appear to be a problem with Ruby but rather an issue on the # operating system side. Therefore we catch the exception and look whether # we run under OS X or not -- if so, then we acknowledge the problem and # re-throw the exception otherwise. if osx_maj_ver and not osx_maj_ver.empty? return true else raise e end end end module_function :groups= def self.root? return Process.uid == 0 unless Puppet.features.microsoft_windows? - require 'sys/admin' - require 'win32/security' - require 'facter' - - majversion = Facter.value(:kernelmajversion) - return false unless majversion - - # if Vista or later, check for unrestricted process token - return Win32::Security.elevated_security? unless majversion.to_f < 6.0 - - group = Sys::Admin.get_group("Administrators", :sid => Win32::Security::SID::BuiltinAdministrators) - group and group.members.index(Sys::Admin.get_login) != nil + require 'puppet/util/windows/user' + Puppet::Util::Windows::User.admin? end # Runs block setting uid and gid if provided then restoring original ids def asuser(new_uid=nil, new_gid=nil) return yield if Puppet.features.microsoft_windows? or !root? old_euid, old_egid = self.euid, self.egid begin change_group(new_gid) if new_gid change_user(new_uid) if new_uid yield ensure change_group(old_egid) change_user(old_euid) end end module_function :asuser def change_group(group, permanently=false) gid = convert_xid(:gid, group) raise Puppet::Error, "No such group #{group}" unless gid if permanently begin Process::GID.change_privilege(gid) rescue NotImplementedError Process.egid = gid Process.gid = gid end else Process.egid = gid end end module_function :change_group def change_user(user, permanently=false) uid = convert_xid(:uid, user) raise Puppet::Error, "No such user #{user}" unless uid if permanently begin Process::UID.change_privilege(uid) rescue NotImplementedError # If changing uid, we must be root. So initgroups first here. initgroups(uid) Process.euid = uid Process.uid = uid end else # If we're already root, initgroups before changing euid. If we're not, # change euid (to root) first. if Process.euid == 0 initgroups(uid) Process.euid = uid else Process.euid = uid initgroups(uid) end end end module_function :change_user # Make sure the passed argument is a number. def convert_xid(type, id) map = {:gid => :group, :uid => :user} raise ArgumentError, "Invalid id type #{type}" unless map.include?(type) ret = Puppet::Util.send(type, id) if ret == nil raise Puppet::Error, "Invalid #{map[type]}: #{id}" end ret end module_function :convert_xid # Initialize supplementary groups def initgroups(user) require 'etc' Process.initgroups(Etc.getpwuid(user).name, Process.gid) end module_function :initgroups def run_and_capture(command, new_uid=nil, new_gid=nil) output = Puppet::Util.execute(command, :failonfail => false, :combine => true, :uid => new_uid, :gid => new_gid) [output, $CHILD_STATUS.dup] end module_function :run_and_capture def system(command, new_uid=nil, new_gid=nil) status = nil asuser(new_uid, new_gid) do Kernel.system(command) status = $CHILD_STATUS.dup end status end module_function :system end diff --git a/lib/puppet/util/windows.rb b/lib/puppet/util/windows.rb index 53b2380aa..a5ff82a0f 100644 --- a/lib/puppet/util/windows.rb +++ b/lib/puppet/util/windows.rb @@ -1,4 +1,5 @@ module Puppet::Util::Windows require 'puppet/util/windows/error' require 'puppet/util/windows/security' + require 'puppet/util/windows/user' end diff --git a/lib/puppet/util/windows/user.rb b/lib/puppet/util/windows/user.rb new file mode 100644 index 000000000..6722a0e2e --- /dev/null +++ b/lib/puppet/util/windows/user.rb @@ -0,0 +1,44 @@ +require 'puppet/util/windows' + +require 'win32/security' + +module Puppet::Util::Windows::User + include Windows::Security + extend Windows::Security + + def admin? + require 'facter' + + majversion = Facter.value(:kernelmajversion) + return false unless majversion + + # if Vista or later, check for unrestricted process token + return Win32::Security.elevated_security? unless majversion.to_f < 6.0 + + # otherwise 2003 or less + check_token_membership + end + module_function :admin? + + def check_token_membership + sid = 0.chr * 80 + size = [80].pack('L') + member = 0.chr * 4 + + unless CreateWellKnownSid(WinBuiltinAdministratorsSid, nil, sid, size) + raise Puppet::Util::Windows::Error.new("Failed to create administrators SID") + end + + unless IsValidSid(sid) + raise Puppet::Util::Windows::Error.new("Invalid SID") + end + + unless CheckTokenMembership(nil, sid, member) + raise Puppet::Util::Windows::Error.new("Failed to check membership") + end + + # Is administrators SID enabled in calling thread's access token? + member.unpack('L')[0] == 1 + end + module_function :check_token_membership +end diff --git a/spec/integration/util/windows/user_spec.rb b/spec/integration/util/windows/user_spec.rb new file mode 100755 index 000000000..82d065d74 --- /dev/null +++ b/spec/integration/util/windows/user_spec.rb @@ -0,0 +1,59 @@ +#!/usr/bin/env ruby -- rspec + +require 'spec_helper' + +describe "Puppet::Util::Windows::User", :if => Puppet.features.microsoft_windows? do + describe "2003 without UAC" do + before :each do + Facter.stubs(:value).with(:kernelmajversion).returns("5.2") + end + + it "should be an admin if user's token contains the Administrators SID" do + Puppet::Util::Windows::User.expects(:check_token_membership).returns(true) + Win32::Security.expects(:elevated_security?).never + + Puppet::Util::Windows::User.should be_admin + end + + it "should not be an admin if user's token doesn't contain the Administrators SID" do + Puppet::Util::Windows::User.expects(:check_token_membership).returns(false) + Win32::Security.expects(:elevated_security?).never + + Puppet::Util::Windows::User.should_not be_admin + end + + it "should raise an exception if we can't check token membership" do + Puppet::Util::Windows::User.expects(:check_token_membership).raises(Win32::Security::Error, "Access denied.") + Win32::Security.expects(:elevated_security?).never + + lambda { Puppet::Util::Windows::User.admin? }.should raise_error(Win32::Security::Error, /Access denied./) + end + end + + describe "2008 with UAC" do + before :each do + Facter.stubs(:value).with(:kernelmajversion).returns("6.0") + end + + it "should be an admin if user is running with elevated privileges" do + Win32::Security.stubs(:elevated_security?).returns(true) + Puppet::Util::Windows::User.expects(:check_token_membership).never + + Puppet::Util::Windows::User.should be_admin + end + + it "should not be an admin if user is not running with elevated privileges" do + Win32::Security.stubs(:elevated_security?).returns(false) + Puppet::Util::Windows::User.expects(:check_token_membership).never + + Puppet::Util::Windows::User.should_not be_admin + end + + it "should raise an exception if the process fails to open the process token" do + Win32::Security.stubs(:elevated_security?).raises(Win32::Security::Error, "Access denied.") + Puppet::Util::Windows::User.expects(:check_token_membership).never + + lambda { Puppet::Util::Windows::User.admin? }.should raise_error(Win32::Security::Error, /Access denied./) + end + end +end diff --git a/spec/unit/util/suidmanager_spec.rb b/spec/unit/util/suidmanager_spec.rb index 575762f3c..7581e10e7 100755 --- a/spec/unit/util/suidmanager_spec.rb +++ b/spec/unit/util/suidmanager_spec.rb @@ -1,329 +1,284 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Util::SUIDManager do let :user do Puppet::Type.type(:user).new(:name => 'name', :uid => 42, :gid => 42) end let :xids do Hash.new {|h,k| 0} end before :each do Puppet::Util::SUIDManager.stubs(:convert_xid).returns(42) Puppet::Util::SUIDManager.stubs(:initgroups) [:euid, :egid, :uid, :gid, :groups].each do |id| Process.stubs("#{id}=").with {|value| xids[id] = value} end end describe "#uid" do it "should allow setting euid/egid" do Puppet::Util::SUIDManager.egid = user[:gid] Puppet::Util::SUIDManager.euid = user[:uid] xids[:egid].should == user[:gid] xids[:euid].should == user[:uid] end end describe "#asuser" do it "should set euid/egid when root" do Process.stubs(:uid).returns(0) Puppet.features.stubs(:microsoft_windows?).returns(false) Process.stubs(:egid).returns(51) Process.stubs(:euid).returns(50) Puppet::Util::SUIDManager.stubs(:convert_xid).with(:gid, 51).returns(51) Puppet::Util::SUIDManager.stubs(:convert_xid).with(:uid, 50).returns(50) yielded = false Puppet::Util::SUIDManager.asuser(user[:uid], user[:gid]) do xids[:egid].should == user[:gid] xids[:euid].should == user[:uid] yielded = true end xids[:egid].should == 51 xids[:euid].should == 50 # It's possible asuser could simply not yield, so the assertions in the # block wouldn't fail. So verify those actually got checked. yielded.should be_true end it "should not get or set euid/egid when not root" do Process.stubs(:uid).returns(1) Process.stubs(:egid).returns(51) Process.stubs(:euid).returns(50) Puppet::Util::SUIDManager.asuser(user[:uid], user[:gid]) {} xids.should be_empty end it "should not get or set euid/egid on Windows" do Puppet.features.stubs(:microsoft_windows?).returns true Puppet::Util::SUIDManager.asuser(user[:uid], user[:gid]) {} xids.should be_empty end end describe "#change_group" do describe "when changing permanently" do it "should try to change_privilege if it is supported" do Process::GID.expects(:change_privilege).with do |gid| Process.gid = gid Process.egid = gid end Puppet::Util::SUIDManager.change_group(42, true) xids[:egid].should == 42 xids[:gid].should == 42 end it "should change both egid and gid if change_privilege isn't supported" do Process::GID.stubs(:change_privilege).raises(NotImplementedError) Puppet::Util::SUIDManager.change_group(42, true) xids[:egid].should == 42 xids[:gid].should == 42 end end describe "when changing temporarily" do it "should change only egid" do Puppet::Util::SUIDManager.change_group(42, false) xids[:egid].should == 42 xids[:gid].should == 0 end end end describe "#change_user" do describe "when changing permanently" do it "should try to change_privilege if it is supported" do Process::UID.expects(:change_privilege).with do |uid| Process.uid = uid Process.euid = uid end Puppet::Util::SUIDManager.change_user(42, true) xids[:euid].should == 42 xids[:uid].should == 42 end it "should change euid and uid and groups if change_privilege isn't supported" do Process::UID.stubs(:change_privilege).raises(NotImplementedError) Puppet::Util::SUIDManager.expects(:initgroups).with(42) Puppet::Util::SUIDManager.change_user(42, true) xids[:euid].should == 42 xids[:uid].should == 42 end end describe "when changing temporarily" do it "should change only euid and groups" do Puppet::Util::SUIDManager.change_user(42, false) xids[:euid].should == 42 xids[:uid].should == 0 end it "should set euid before groups if changing to root" do Process.stubs(:euid).returns 50 when_not_root = sequence 'when_not_root' Process.expects(:euid=).in_sequence(when_not_root) Puppet::Util::SUIDManager.expects(:initgroups).in_sequence(when_not_root) Puppet::Util::SUIDManager.change_user(0, false) end it "should set groups before euid if changing from root" do Process.stubs(:euid).returns 0 when_root = sequence 'when_root' Puppet::Util::SUIDManager.expects(:initgroups).in_sequence(when_root) Process.expects(:euid=).in_sequence(when_root) Puppet::Util::SUIDManager.change_user(50, false) end end end describe "when running commands" do before :each do # We want to make sure $CHILD_STATUS is set Kernel.system '' if $CHILD_STATUS.nil? end describe "with #system" do it "should set euid/egid when root" do Process.stubs(:uid).returns(0) Puppet.features.stubs(:microsoft_windows?).returns(false) Process.stubs(:egid).returns(51) Process.stubs(:euid).returns(50) Puppet::Util::SUIDManager.stubs(:convert_xid).with(:gid, 51).returns(51) Puppet::Util::SUIDManager.stubs(:convert_xid).with(:uid, 50).returns(50) Puppet::Util::SUIDManager.expects(:change_group).with(user[:uid]) Puppet::Util::SUIDManager.expects(:change_user).with(user[:uid]) Puppet::Util::SUIDManager.expects(:change_group).with(51) Puppet::Util::SUIDManager.expects(:change_user).with(50) Kernel.expects(:system).with('blah') Puppet::Util::SUIDManager.system('blah', user[:uid], user[:gid]) end it "should not get or set euid/egid when not root" do Process.stubs(:uid).returns(1) Kernel.expects(:system).with('blah') Puppet::Util::SUIDManager.system('blah', user[:uid], user[:gid]) xids.should be_empty end it "should not get or set euid/egid on Windows" do Puppet.features.stubs(:microsoft_windows?).returns true Kernel.expects(:system).with('blah') Puppet::Util::SUIDManager.system('blah', user[:uid], user[:gid]) xids.should be_empty end end describe "with #run_and_capture" do it "should capture the output and return process status" do Puppet::Util. expects(:execute). with('yay', :combine => true, :failonfail => false, :uid => user[:uid], :gid => user[:gid]). returns('output') output = Puppet::Util::SUIDManager.run_and_capture 'yay', user[:uid], user[:gid] output.first.should == 'output' output.last.should be_a(Process::Status) end end end describe "#root?" do describe "on POSIX systems" do before :each do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) end it "should be root if uid is 0" do Process.stubs(:uid).returns(0) Puppet::Util::SUIDManager.should be_root end it "should not be root if uid is not 0" do Process.stubs(:uid).returns(1) Puppet::Util::SUIDManager.should_not be_root end end describe "on Microsoft Windows", :if => Puppet.features.microsoft_windows? do - describe "2003 without UAC" do - before :each do - Facter.stubs(:value).with(:kernelmajversion).returns("5.2") - end - - it "should be root if user is a member of the Administrators group" do - Sys::Admin.stubs(:get_login).returns("Administrator") - Sys::Group.stubs(:members).returns(%w[Administrator]) - - Win32::Security.expects(:elevated_security?).never - Puppet::Util::SUIDManager.should be_root - end - - it "should not be root if the process is running as Guest" do - Sys::Admin.stubs(:get_login).returns("Guest") - Sys::Group.stubs(:members).returns([]) + it "should be root if user is privileged" do + Puppet::Util::Windows::User.stubs(:admin?).returns true - Win32::Security.expects(:elevated_security?).never - Puppet::Util::SUIDManager.should_not be_root - end - - it "should raise an exception if the process fails to open the process token" do - Win32::Security.stubs(:elevated_security?).raises(Win32::Security::Error, "Access denied.") - Sys::Admin.stubs(:get_login).returns("Administrator") - Sys::Group.expects(:members).never - - lambda { Puppet::Util::SUIDManager.should raise_error(Win32::Security::Error, /Access denied./) } - end + Puppet::Util::SUIDManager.should be_root end - describe "2008 with UAC" do - before :each do - Facter.stubs(:value).with(:kernelmajversion).returns("6.0") - end - - it "should be root if user is running with elevated privileges" do - Win32::Security.stubs(:elevated_security?).returns(true) - Sys::Admin.expects(:get_login).never - - Puppet::Util::SUIDManager.should be_root - end - - it "should not be root if user is not running with elevated privileges" do - Win32::Security.stubs(:elevated_security?).returns(false) - Sys::Admin.expects(:get_login).never + it "should not be root if user is not privileged" do + Puppet::Util::Windows::User.stubs(:admin?).returns false - Puppet::Util::SUIDManager.should_not be_root - end - - it "should raise an exception if the process fails to open the process token" do - Win32::Security.stubs(:elevated_security?).raises(Win32::Security::Error, "Access denied.") - Sys::Admin.expects(:get_login).never - - lambda { Puppet::Util::SUIDManager.should raise_error(Win32::Security::Error, /Access denied./) } - end + Puppet::Util::SUIDManager.should_not be_root end end end end describe 'Puppet::Util::SUIDManager#groups=' do subject do Puppet::Util::SUIDManager end it "(#3419) should rescue Errno::EINVAL on OS X" do Process.expects(:groups=).raises(Errno::EINVAL, 'blew up') subject.expects(:osx_maj_ver).returns('10.7').twice subject.groups = ['list', 'of', 'groups'] end it "(#3419) should fail if an Errno::EINVAL is raised NOT on OS X" do Process.expects(:groups=).raises(Errno::EINVAL, 'blew up') subject.expects(:osx_maj_ver).returns(false) expect { subject.groups = ['list', 'of', 'groups'] }.should raise_error(Errno::EINVAL) end end