diff --git a/lib/puppet/suidmanager.rb b/lib/puppet/suidmanager.rb index 4d00f59b0..d6bbc90f1 100644 --- a/lib/puppet/suidmanager.rb +++ b/lib/puppet/suidmanager.rb @@ -1,100 +1,97 @@ require 'facter' +require 'puppet/util/warnings' module Puppet module SUIDManager + include Puppet::Util::Warnings + platform = Facter["kernel"].value [:uid=, :gid=, :uid, :gid].each do |method| define_method(method) do |*args| # NOTE: 'method' is closed here. newmethod = method if platform == "Darwin" - if !@darwinwarned - Puppet.warning "Cannot change real UID on Darwin" - @darwinwarned = true - end + warnonce "Cannot change real UID on Darwin" newmethod = ("e" + method.to_s).intern end return Process.send(newmethod, *args) end module_function method end [:euid=, :euid, :egid=, :egid].each do |method| define_method(method) do |*args| Process.send(method, *args) end module_function method end def run_and_capture(command, new_uid=self.euid, new_gid=self.egid) output = nil asuser(new_uid, new_gid) do # capture both stdout and stderr unless we are on ruby < 1.8.4 # NOTE: this would be much better facilitated with a specialized popen() # (see the test suite for more details.) if (Facter['rubyversion'].value <=> "1.8.4") < 0 - unless @@alreadywarned - Puppet.warning "Cannot capture STDERR when running as another user on Ruby < 1.8.4" - @@alreadywarned = true - end + warnonce "Cannot capture STDERR when running as another user on Ruby < 1.8.4" output = %x{#{command}} else output = %x{#{command} 2>&1} end end [output, $?.dup] end module_function :run_and_capture def system(command, new_uid=self.euid, new_gid=self.egid) status = nil asuser(new_uid, new_gid) do Kernel.system(command) status = $?.dup end status end module_function :system def asuser(new_euid=nil, new_egid=nil) # Unless we're root, don't do a damn thing. unless Process.uid == 0 return yield end old_egid = old_euid = nil if new_egid saved_state_egid = new_egid new_egid = Puppet::Util.gid(new_egid) if new_egid == nil raise Puppet::Error, "Invalid group: %s" % saved_state_egid end old_egid = self.egid self.egid = new_egid end if new_euid saved_state_euid = new_euid new_euid = Puppet::Util.uid(new_euid) if new_euid == nil raise Puppet::Error, "Invalid user: %s" % saved_state_euid end old_euid = self.euid self.euid = new_euid end return yield ensure self.egid = old_egid if old_egid self.euid = old_euid if old_euid end module_function :asuser end end # $Id$ diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index 0141d1c3c..d8b8c812f 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,462 +1,467 @@ # A module to collect utility functions. require 'sync' require 'puppet/lock' module Puppet # A command failed to execute. class ExecutionFailure < Puppet::Error end module Util require 'benchmark' # Create a hash to store the different sync objects. @@syncresources = {} # Return the sync object associated with a given resource. def self.sync(resource) @@syncresources[resource] ||= Sync.new return @@syncresources[resource] end # Change the process to a different user def self.chuser if Facter["operatingsystem"].value == "Darwin" $stderr.puts "Ruby on darwin is broken; puppetmaster must run as root" return end if group = Puppet[:group] group = self.gid(group) unless group raise Puppet::Error, "No such group %s" % Puppet[:group] end unless Puppet::SUIDManager.gid == group begin Puppet::SUIDManager.egid = group Puppet::SUIDManager.gid = group rescue => detail Puppet.warning "could not change to group %s: %s" % [group.inspect, detail] $stderr.puts "could not change to group %s" % group.inspect # Don't exit on failed group changes, since it's # not fatal #exit(74) end end end if user = Puppet[:user] user = self.uid(user) unless user raise Puppet::Error, "No such user %s" % Puppet[:user] end unless Puppet::SUIDManager.uid == user begin Puppet::SUIDManager.uid = user Puppet::SUIDManager.euid = user rescue $stderr.puts "could not change to user %s" % user exit(74) end end end end # Create a shared lock for reading def self.readlock(file) self.sync(file).synchronize(Sync::SH) do File.open(file) { |f| f.lock_shared { |lf| yield lf } } end end # Create an exclusive lock for writing, and do the writing in a # tmp file. def self.writelock(file, mode = 0600) tmpfile = file + ".tmp" unless FileTest.directory?(File.dirname(tmpfile)) raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % [file, File.dirname(file)] end self.sync(file).synchronize(Sync::EX) do File.open(file, "w", mode) do |rf| rf.lock_exclusive do |lrf| File.open(tmpfile, "w", mode) do |tf| yield tf end begin File.rename(tmpfile, file) rescue => detail Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail] end end end end end # Get the GID of a given group, provided either a GID or a name def self.gid(group) if group =~ /^\d+$/ group = Integer(group) end unless group raise Puppet::DevError, "Invalid group %s" % group.inspect end gid = nil obj = nil # We want to look the group up either way if group.is_a?(Integer) # If this doesn't find anything obj = Puppet.type(:group).find { |gobj| gobj.should(:gid) == group || gobj.is(:gid) == group } unless obj begin gobj = Etc.getgrgid(group) gid = gobj.gid rescue ArgumentError => detail # ignore it; we couldn't find the group end end else if obj = Puppet.type(:group)[group] obj[:check] = [:gid] else obj = Puppet.type(:group).create( :name => group, :check => [:gid] ) end obj.retrieve end if obj gid = obj.should(:gid) || obj.is(:gid) if gid == :absent gid = nil end end return gid end # Get the UID of a given user, whether a UID or name is provided def self.uid(user) uid = nil # if we don't have any user info, warn and GTFO. if !user Puppet.warning "Username provided for lookup is nil" return nil end + # One of the unit tests was passing a Passwd struct + unless user.is_a?(String) or user.is_a?(Integer) + raise Puppet::DevError, "Invalid value for uid: %s" % user.class + end + if user =~ /^\d+$/ user = Integer(user) end if user.is_a?(Integer) # If this doesn't find anything obj = Puppet.type(:user).find { |uobj| uobj.should(:uid) == user || uobj.is(:uid) == user } unless obj begin uobj = Etc.getpwuid(user) uid = uobj.uid rescue ArgumentError => detail # ignore it; we couldn't find the user end end else unless obj = Puppet.type(:user)[user] obj = Puppet.type(:user).create( :name => user ) end obj[:check] = [:uid, :gid] end if obj obj.retrieve uid = obj.should(:uid) || obj.is(:uid) if uid == :absent uid = nil end end return uid end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| if args.is_a?(Array) args = args.join(" ") end if useself Puppet::Log.create( :level => level, :source => self, :message => args ) else Puppet::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) Dir.mkdir(File.join(path), mode) elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? object = Puppet else object = args.pop end unless level puts caller.join("\n") raise Puppet::DevError, "Failed to provide level" end unless object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to %s" % level end # Only benchmark if our log level is high enough if level != :none and Puppet::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def binary(bin) if bin =~ /^\// if FileTest.exists? bin return true else return nil end else ENV['PATH'].split(":").each do |dir| if FileTest.exists? File.join(dir, bin) return File.join(dir, bin) end end return nil end end module_function :binary # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '%s'" % command else Puppet.debug "Executing '%s'" % command end output = open("| #{command} 2>&1") do |pipe| yield pipe end if failonfail unless $? == 0 raise ExecutionFailure, output end end return output end def execfail(command, exception) begin output = execute(command) return output rescue ExecutionFailure raise exception, output end end # Execute the desired command, and return the status and output. def execute(command, failonfail = true) if respond_to? :debug debug "Executing '%s'" % command else Puppet.debug "Executing '%s'" % command end command += " 2>&1" unless command =~ />/ output = %x{#{command}} if failonfail unless $? == 0 raise ExecutionFailure, "Could not execute '%s': %s" % [command, output] end end return output end module_function :execute # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.sync(resource).synchronize(type) do yield end end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined? @pmap pmap = %x{which pmap 2>/dev/null}.chomp if $? != 0 or pmap =~ /^no/ @pmap = nil else @pmap = pmap end end if @pmap return %x{pmap #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern return value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end return hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } return seconds end module_function :memory end end require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/package' require 'puppet/util/warnings' # $Id$ diff --git a/test/puppet/tc_suidmanager.rb b/test/puppet/tc_suidmanager.rb index 04cbed8e6..c9b843147 100644 --- a/test/puppet/tc_suidmanager.rb +++ b/test/puppet/tc_suidmanager.rb @@ -1,92 +1,92 @@ require 'puppet' require 'puppettest' require 'test/unit' class TestSUIDManager < Test::Unit::TestCase include PuppetTest def setup if Process.uid != 0 warn "Process tests must be run as root" @run = false else @run = true end super end def test_metaprogramming_function_additions # NOTE: the way that we are dynamically generating the methods in SUIDManager for # the UID/GID calls was causing problems due to the modification # of a closure. Should the bug rear itself again, this test # will fail. assert_nothing_raised do Puppet::SUIDManager.uid Puppet::SUIDManager.uid end end def test_id_set if @run user = nonrootuser assert_nothing_raised do Puppet::SUIDManager.egid = user.gid Puppet::SUIDManager.euid = user.uid end assert_equal(Puppet::SUIDManager.euid, Process.euid) assert_equal(Puppet::SUIDManager.egid, Process.egid) assert_nothing_raised do Puppet::SUIDManager.euid = 0 Puppet::SUIDManager.egid = 0 end assert_uid_gid(user.uid, user.gid, tempfile) end end def test_utiluid - user = nonrootuser + user = nonrootuser.name if @run assert_not_equal(nil, Puppet::Util.uid(user)) end end def test_asuser if @run user = nonrootuser uid, gid = [nil, nil] assert_nothing_raised do Puppet::SUIDManager.asuser(user.uid, user.gid) do uid = Puppet::SUIDManager.euid gid = Puppet::SUIDManager.egid end end assert_equal(user.uid, uid) assert_equal(user.gid, gid) end end def test_system # NOTE: not sure what shells this will work on.. if @run user = nonrootuser status = Puppet::SUIDManager.system("exit $EUID", user.uid, user.gid) assert_equal(status.exitstatus, user.uid) end end def test_run_and_capture if (RUBY_VERSION <=> "1.8.4") < 0 warn "Cannot run this test on ruby < 1.8.4" else # NOTE: because of the way that run_and_capture currently # works, we cannot just blindly echo to stderr. This little # hack gets around our problem, but the real problem is the # way that run_and_capture works. output = Puppet::SUIDManager.run_and_capture("ruby -e '$stderr.puts \"foo\"'")[0].chomp assert_equal(output, 'foo') end end end -# $Id:$ \ No newline at end of file +# $Id:$