diff --git a/lib/puppet/type/resources.rb b/lib/puppet/type/resources.rb index b75179489..757b3491c 100644 --- a/lib/puppet/type/resources.rb +++ b/lib/puppet/type/resources.rb @@ -1,171 +1,187 @@ require 'puppet' require 'puppet/parameter/boolean' Puppet::Type.newtype(:resources) do @doc = "This is a metatype that can manage other resource types. Any metaparams specified here will be passed on to any generated resources, so you can purge umanaged resources but set `noop` to true so the purging is only logged and does not actually happen." newparam(:name) do desc "The name of the type to be managed." validate do |name| raise ArgumentError, "Could not find resource type '#{name}'" unless Puppet::Type.type(name) end munge { |v| v.to_s } end newparam(:purge, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Purge unmanaged resources. This will delete any resource that is not specified in your configuration and is not required by any specified resources. Purging ssh_authorized_keys this way is deprecated; see the purge_ssh_keys parameter of the user type for a better alternative." defaultto :false validate do |value| if munge(value) unless @resource.resource_type.respond_to?(:instances) raise ArgumentError, "Purging resources of type #{@resource[:name]} is not supported, since they cannot be queried from the system" end raise ArgumentError, "Purging is only supported on types that accept 'ensure'" unless @resource.resource_type.validproperty?(:ensure) end end end newparam(:unless_system_user) do desc "This keeps system users from being purged. By default, it - does not purge users whose UIDs are less than 500 or 1000, depending on operating system, but you can specify + does not purge users whose UIDs are less than the minimum UID for the system (typically 500 or 1000), but you can specify a different UID as the inclusive limit." newvalues(:true, :false, /^\d+$/) munge do |value| case value when /^\d+/ Integer(value) when :true, true - @resource.system_users_max_uid + @resource.class.system_users_max_uid when :false, false false when Integer; value else raise ArgumentError, "Invalid value #{value.inspect}" end end defaultto { if @resource[:name] == "user" - @resource.system_users_max_uid + @resource.class.system_users_max_uid else nil end } end newparam(:unless_uid) do desc 'This keeps specific uids or ranges of uids from being purged when purge is true. Accepts integers, integer strings, and arrays of integers or integer strings. To specify a range of uids, consider using the range() function from stdlib.' munge do |value| value = [value] unless value.is_a? Array value.flatten.collect do |v| case v when Integer v when String Integer(v) else raise ArgumentError, "Invalid value #{v.inspect}." end end end end def check(resource) @checkmethod ||= "#{self[:name]}_check" @hascheck ||= respond_to?(@checkmethod) if @hascheck return send(@checkmethod, resource) else return true end end def able_to_ensure_absent?(resource) resource[:ensure] = :absent rescue ArgumentError, Puppet::Error err "The 'ensure' attribute on #{self[:name]} resources does not accept 'absent' as a value" false end # Generate any new resources we need to manage. This is pretty hackish # right now, because it only supports purging. def generate return [] unless self.purge? resource_type.instances. reject { |r| catalog.resource_refs.include? r.ref }. select { |r| check(r) }. select { |r| r.class.validproperty?(:ensure) }. select { |r| able_to_ensure_absent?(r) }. each { |resource| @parameters.each do |name, param| resource[name] = param.value if param.metaparam? end # Mark that we're purging, so transactions can handle relationships # correctly resource.purging } end def resource_type unless defined?(@resource_type) unless type = Puppet::Type.type(self[:name]) raise Puppet::DevError, "Could not find resource type" end @resource_type = type end @resource_type end def self.deprecate_params(title,params) return unless params if title == 'cron' and ! params.select { |param| param.name.intern == :purge and param.value == true }.empty? Puppet.deprecation_warning("Change notice: purging cron entries will be more aggressive in future versions, take care when updating your agents. See http://links.puppetlabs.com/puppet-aggressive-cron-purge") end end # Make sure we don't purge users with specific uids def user_check(resource) return true unless self[:name] == "user" return true unless self[:unless_system_user] resource[:audit] = :uid current_values = resource.retrieve_resource current_uid = current_values[resource.property(:uid)] unless_uids = self[:unless_uid] return false if system_users.include?(resource[:name]) return false if unless_uids && unless_uids.include?(current_uid) current_uid > self[:unless_system_user] end def system_users %w{root nobody bin noaccess daemon sys} end - def system_users_max_uid - case Facter.value(:osfamily) - when 'Debian', 'OpenBSD', 'FreeBSD' - 999 - else - 499 + def self.system_users_max_uid + return @system_users_max_uid if @system_users_max_uid + + # First try to read the minimum user id from login.defs + if Puppet::FileSystem.exist?('/etc/login.defs') + @system_users_max_uid = Puppet::FileSystem.each_line '/etc/login.defs' do |line| + break $1.to_i - 1 if line =~ /^\s*UID_MIN\s+(\d+)(\s*#.*)?$/ + end end + + # Otherwise, use a sensible default based on the OS family + @system_users_max_uid ||= case Facter.value(:osfamily) + when 'OpenBSD', 'FreeBSD' + 999 + else + 499 + end + + @system_users_max_uid + end + + def self.reset_system_users_max_uid! + @system_users_max_uid = nil end end diff --git a/spec/unit/type/resources_spec.rb b/spec/unit/type/resources_spec.rb index fe48f3189..d2e259042 100755 --- a/spec/unit/type/resources_spec.rb +++ b/spec/unit/type/resources_spec.rb @@ -1,290 +1,318 @@ #! /usr/bin/env ruby require 'spec_helper' resources = Puppet::Type.type(:resources) # There are still plenty of tests to port over from test/. describe resources do + + before :each do + described_class.reset_system_users_max_uid! + end + describe "when initializing" do it "should fail if the specified resource type does not exist" do Puppet::Type.stubs(:type).with { |x| x.to_s.downcase == "resources"}.returns resources Puppet::Type.expects(:type).with("nosuchtype").returns nil lambda { resources.new :name => "nosuchtype" }.should raise_error(Puppet::Error) end it "should not fail when the specified resource type exists" do lambda { resources.new :name => "file" }.should_not raise_error end it "should set its :resource_type attribute" do resources.new(:name => "file").resource_type.should == Puppet::Type.type(:file) end end describe :purge do let (:instance) { described_class.new(:name => 'file') } it "defaults to false" do instance[:purge].should be_false end it "can be set to false" do instance[:purge] = 'false' end it "cannot be set to true for a resource type that does not accept ensure" do instance.resource_type.stubs(:respond_to?).returns true instance.resource_type.stubs(:validproperty?).returns false expect { instance[:purge] = 'yes' }.to raise_error Puppet::Error end it "cannot be set to true for a resource type that does not have instances" do instance.resource_type.stubs(:respond_to?).returns false instance.resource_type.stubs(:validproperty?).returns true expect { instance[:purge] = 'yes' }.to raise_error Puppet::Error end it "can be set to true for a resource type that has instances and can accept ensure" do instance.resource_type.stubs(:respond_to?).returns true instance.resource_type.stubs(:validproperty?).returns true expect { instance[:purge] = 'yes' }.not_to raise_error Puppet::Error end end describe "#check_user purge behaviour" do describe "with unless_system_user => true" do before do @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true @res.catalog = Puppet::Resource::Catalog.new + Puppet::FileSystem.stubs(:exist?).with('/etc/login.defs').returns false end it "should never purge hardcoded system users" do %w{root nobody bin noaccess daemon sys}.each do |sys_user| @res.user_check(Puppet::Type.type(:user).new(:name => sys_user)).should be_false end end it "should not purge system users if unless_system_user => true" do user_hash = {:name => 'system_user', :uid => 125, :system => true} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_false end - it "should purge manual users if unless_system_user => true" do - user_hash = {:name => 'system_user', :uid => 500, :system => true} - os = Facter.value(:osfamily) - user_hash[:uid] = 1000 if os == 'Debian' || os =='OpenBSD' || os == 'FreeBSD' + it "should purge non-system users if unless_system_user => true" do + user_hash = {:name => 'system_user', :uid => described_class.system_users_max_uid + 1, :system => true} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_true end it "should not purge system users under 600 if unless_system_user => 600" do res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => 600 res.catalog = Puppet::Resource::Catalog.new user_hash = {:name => 'system_user', :uid => 500, :system => true} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) res.user_check(user).should be_false end + end - ['Debian', 'FreeBSD', 'OpenBSD'].each do |os| - describe "on #{os}" do - before :each do - Facter.stubs(:value).with(:kernel).returns(os) - Facter.stubs(:value).with(:operatingsystem).returns(os) - Facter.stubs(:value).with(:osfamily).returns(os) - @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true - @res.catalog = Puppet::Resource::Catalog.new - end + %w(FreeBSD OpenBSD).each do |os| + describe "on #{os}" do + before :each do + Facter.stubs(:value).with(:kernel).returns(os) + Facter.stubs(:value).with(:operatingsystem).returns(os) + Facter.stubs(:value).with(:osfamily).returns(os) + Puppet::FileSystem.stubs(:exist?).with('/etc/login.defs').returns false + @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true + @res.catalog = Puppet::Resource::Catalog.new + end - it "should not purge system users under 1000" do - user_hash = {:name => 'system_user', :uid => 999} - user = Puppet::Type.type(:user).new(user_hash) - user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) - @res.user_check(user).should be_false - end + it "should not purge system users under 1000" do + user_hash = {:name => 'system_user', :uid => 999} + user = Puppet::Type.type(:user).new(user_hash) + user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) + @res.user_check(user).should be_false + end - it "should purge users over 999" do - user_hash = {:name => 'system_user', :uid => 1000} - user = Puppet::Type.type(:user).new(user_hash) - user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) - @res.user_check(user).should be_true - end + it "should purge users over 999" do + user_hash = {:name => 'system_user', :uid => 1000} + user = Puppet::Type.type(:user).new(user_hash) + user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) + @res.user_check(user).should be_true end end end + describe 'with login.defs present' do + before :each do + Puppet::FileSystem.expects(:exist?).with('/etc/login.defs').returns true + Puppet::FileSystem.expects(:each_line).with('/etc/login.defs').yields(' UID_MIN 1234 # UID_MIN comment ') + @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true + @res.catalog = Puppet::Resource::Catalog.new + end + + it 'should not purge a system user' do + user_hash = {:name => 'system_user', :uid => 1233} + user = Puppet::Type.type(:user).new(user_hash) + user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) + @res.user_check(user).should be_false + end + + it 'should purge a non-system user' do + user_hash = {:name => 'system_user', :uid => 1234} + user = Puppet::Type.type(:user).new(user_hash) + user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) + @res.user_check(user).should be_true + end + end + describe "with unless_uid" do describe "with a uid array" do before do @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => [15_000, 15_001, 15_002] @res.catalog = Puppet::Resource::Catalog.new end it "should purge uids that are not in a specified array" do user_hash = {:name => 'special_user', :uid => 25_000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_true end it "should not purge uids that are in a specified array" do user_hash = {:name => 'special_user', :uid => 15000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_false end end describe "with a single integer uid" do before do @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => 15_000 @res.catalog = Puppet::Resource::Catalog.new end it "should purge uids that are not specified" do user_hash = {:name => 'special_user', :uid => 25_000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_true end it "should not purge uids that are specified" do user_hash = {:name => 'special_user', :uid => 15_000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_false end end describe "with a single string uid" do before do @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => '15000' @res.catalog = Puppet::Resource::Catalog.new end it "should purge uids that are not specified" do user_hash = {:name => 'special_user', :uid => 25_000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_true end it "should not purge uids that are specified" do user_hash = {:name => 'special_user', :uid => 15_000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_false end end describe "with a mixed uid array" do before do @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => ['15000', 16_666] @res.catalog = Puppet::Resource::Catalog.new end it "should not purge ids in the range" do user_hash = {:name => 'special_user', :uid => 15_000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_false end it "should not purge specified ids" do user_hash = {:name => 'special_user', :uid => 16_666} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_false end it "should purge unspecified ids" do user_hash = {:name => 'special_user', :uid => 17_000} user = Puppet::Type.type(:user).new(user_hash) user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash) @res.user_check(user).should be_true end end end end describe "#generate" do before do @host1 = Puppet::Type.type(:host).new(:name => 'localhost', :ip => '127.0.0.1') @catalog = Puppet::Resource::Catalog.new end describe "when dealing with non-purging resources" do before do @resources = Puppet::Type.type(:resources).new(:name => 'host') end it "should not generate any resource" do @resources.generate.should be_empty end end describe "when the catalog contains a purging resource" do before do @resources = Puppet::Type.type(:resources).new(:name => 'host', :purge => true) @purgeable_resource = Puppet::Type.type(:host).new(:name => 'localhost', :ip => '127.0.0.1') @catalog.add_resource @resources end it "should not generate a duplicate of that resource" do Puppet::Type.type(:host).stubs(:instances).returns [@host1] @catalog.add_resource @host1 @resources.generate.collect { |r| r.ref }.should_not include(@host1.ref) end it "should not include the skipped system users" do res = Puppet::Type.type(:resources).new :name => :user, :purge => true res.catalog = Puppet::Resource::Catalog.new root = Puppet::Type.type(:user).new(:name => "root") Puppet::Type.type(:user).expects(:instances).returns [ root ] list = res.generate names = list.collect { |r| r[:name] } names.should_not be_include("root") end describe "when generating a purgeable resource" do it "should be included in the generated resources" do Puppet::Type.type(:host).stubs(:instances).returns [@purgeable_resource] @resources.generate.collect { |r| r.ref }.should include(@purgeable_resource.ref) end end describe "when the instance's do not have an ensure property" do it "should not be included in the generated resources" do @no_ensure_resource = Puppet::Type.type(:exec).new(:name => "#{File.expand_path('/usr/bin/env')} echo") Puppet::Type.type(:host).stubs(:instances).returns [@no_ensure_resource] @resources.generate.collect { |r| r.ref }.should_not include(@no_ensure_resource.ref) end end describe "when the instance's ensure property does not accept absent" do it "should not be included in the generated resources" do @no_absent_resource = Puppet::Type.type(:service).new(:name => 'foobar') Puppet::Type.type(:host).stubs(:instances).returns [@no_absent_resource] @resources.generate.collect { |r| r.ref }.should_not include(@no_absent_resource.ref) end end describe "when checking the instance fails" do it "should not be included in the generated resources" do @purgeable_resource = Puppet::Type.type(:host).new(:name => 'foobar') Puppet::Type.type(:host).stubs(:instances).returns [@purgeable_resource] @resources.expects(:check).with(@purgeable_resource).returns(false) @resources.generate.collect { |r| r.ref }.should_not include(@purgeable_resource.ref) end end end end end