diff --git a/lib/puppet/provider/user/useradd.rb b/lib/puppet/provider/user/useradd.rb index 10c4060a9..dd2d24b7d 100644 --- a/lib/puppet/provider/user/useradd.rb +++ b/lib/puppet/provider/user/useradd.rb @@ -1,235 +1,231 @@ require 'puppet/provider/nameservice/objectadd' require 'date' require 'puppet/util/libuser' require 'time' require 'puppet/error' Puppet::Type.type(:user).provide :useradd, :parent => Puppet::Provider::NameService::ObjectAdd do desc "User management via `useradd` and its ilk. Note that you will need to install Ruby's shadow password library (often known as `ruby-libshadow`) if you wish to manage user passwords." commands :add => "useradd", :delete => "userdel", :modify => "usermod", :password => "chage" options :home, :flag => "-d", :method => :dir options :comment, :method => :gecos options :groups, :flag => "-G" options :password_min_age, :flag => "-m", :method => :sp_min options :password_max_age, :flag => "-M", :method => :sp_max options :password, :method => :sp_pwdp options :expiry, :method => :sp_expire, :munge => proc { |value| if value == :absent '' else case Facter.value(:operatingsystem) when 'Solaris' # Solaris uses %m/%d/%Y for useradd/usermod expiry_year, expiry_month, expiry_day = value.split('-') [expiry_month, expiry_day, expiry_year].join('/') else value end end }, :unmunge => proc { |value| if value == -1 :absent else # Expiry is days after 1970-01-01 (Date.new(1970,1,1) + value).strftime('%Y-%m-%d') end } optional_commands :localadd => "luseradd" has_feature :libuser if Puppet.features.libuser? def exists? return !!localuid if @resource.forcelocal? super end def uid return localuid if @resource.forcelocal? get(:uid) end def finduser(key, value) passwd_file = "/etc/passwd" passwd_keys = ['account', 'password', 'uid', 'gid', 'gecos', 'directory', 'shell'] index = passwd_keys.index(key) File.open(passwd_file) do |f| f.each_line do |line| user = line.split(":") if user[index] == value f.close return user end end end false end def local_username finduser('uid', @resource.uid) end def localuid user = finduser('account', resource[:name]) return user[2] if user false end def shell=(value) check_valid_shell set("shell", value) end verify :gid, "GID must be an integer" do |value| value.is_a? Integer end verify :groups, "Groups must be comma-separated" do |value| value !~ /\s/ end has_features :manages_homedir, :allows_duplicates, :manages_expiry has_features :system_users unless %w{HP-UX Solaris}.include? Facter.value(:operatingsystem) has_features :manages_passwords, :manages_password_age if Puppet.features.libshadow? has_features :manages_shell def check_allow_dup # We have to manually check for duplicates when using libuser # because by default duplicates are allowed. This check is # to ensure consistent behaviour of the useradd provider when # using both useradd and luseradd if not @resource.allowdupe? and @resource.forcelocal? if @resource.should(:uid) and finduser('uid', @resource.should(:uid).to_s) raise(Puppet::Error, "UID #{@resource.should(:uid).to_s} already exists, use allowdupe to force user creation") end elsif @resource.allowdupe? and not @resource.forcelocal? return ["-o"] end [] end def check_valid_shell unless File.exists?(@resource.should(:shell)) raise(Puppet::Error, "Shell #{@resource.should(:shell)} must exist") end unless File.executable?(@resource.should(:shell).to_s) raise(Puppet::Error, "Shell #{@resource.should(:shell)} must be executable") end end def check_manage_home cmd = [] if @resource.managehome? and not @resource.forcelocal? cmd << "-m" elsif not @resource.managehome? and Facter.value(:osfamily) == 'RedHat' cmd << "-M" end cmd end def check_manage_expiry cmd = [] if @resource[:expiry] and not @resource.forcelocal? cmd << "-e #{@resource[:expiry]}" end cmd end def check_system_users if self.class.system_users? and resource.system? ["-r"] else [] end end def add_properties cmd = [] # validproperties is a list of properties in undefined order # sort them to have a predictable command line in tests Puppet::Type.type(:user).validproperties.sort.each do |property| next if property == :ensure next if property.to_s =~ /password_.+_age/ next if property == :groups and @resource.forcelocal? next if property == :expiry and @resource.forcelocal? # the value needs to be quoted, mostly because -c might # have spaces in it if value = @resource.should(property) and value != "" cmd << flag(property) << munge(property, value) end end cmd end def addcmd if @resource.forcelocal? cmd = [command(:localadd)] @custom_environment = Puppet::Util::Libuser.getenv else cmd = [command(:add)] end if not @resource.should(:gid) and Puppet::Util.gid(@resource[:name]) cmd += ["-g", @resource[:name]] end cmd += add_properties cmd += check_allow_dup cmd += check_manage_home cmd += check_system_users cmd << @resource[:name] end def deletecmd - if @resource.forcelocal? - cmd = [command(:localdelete)] - else - cmd = [command(:delete)] - end + cmd = [command(:delete)] cmd += @resource.managehome? ? ['-r'] : [] cmd << @resource[:name] end def passcmd age_limits = [:password_min_age, :password_max_age].select { |property| @resource.should(property) } if age_limits.empty? nil else [command(:password),age_limits.collect { |property| [flag(property), @resource.should(property)]}, @resource[:name]].flatten end end [:expiry, :password_min_age, :password_max_age, :password].each do |shadow_property| define_method(shadow_property) do if Puppet.features.libshadow? if ent = Shadow::Passwd.getspnam(@resource.name) method = self.class.option(shadow_property, :method) return unmunge(shadow_property, ent.send(method)) end end :absent end end def create if @resource[:shell] check_valid_shell end super if @resource.forcelocal? and self.groups? set(:groups, @resource[:groups]) end if @resource.forcelocal? and @resource[:expiry] set(:expiry, @resource[:expiry]) end end def groups? !!@resource[:groups] end end diff --git a/spec/unit/provider/user/useradd_spec.rb b/spec/unit/provider/user/useradd_spec.rb index 9d42017a7..6188dca84 100755 --- a/spec/unit/provider/user/useradd_spec.rb +++ b/spec/unit/provider/user/useradd_spec.rb @@ -1,424 +1,431 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:user).provider(:useradd) do before :each do described_class.stubs(:command).with(:password).returns '/usr/bin/chage' described_class.stubs(:command).with(:add).returns '/usr/sbin/useradd' described_class.stubs(:command).with(:localadd).returns '/usr/sbin/luseradd' described_class.stubs(:command).with(:modify).returns '/usr/sbin/usermod' described_class.stubs(:command).with(:delete).returns '/usr/sbin/userdel' end let(:resource) do Puppet::Type.type(:user).new( :name => 'myuser', :managehome => :false, :system => :false, :provider => provider ) end let(:provider) { described_class.new(:name => 'myuser') } let(:shadow_entry) { return unless Puppet.features.libshadow? Struct::PasswdEntry.new( 'myuser', # login name '$6$FvW8Ib8h$qQMI/CR9m.QzIicZKutLpBgCBBdrch1IX0rTnxuI32K1pD9.RXZrmeKQlaC.RzODNuoUtPPIyQDufunvLOQWF0', # encrypted password 15573, # date of last password change 10, # minimum password age 20, # maximum password age 7, # password warning period -1, # password inactivity period 15706, # account expiration date -1 # reserved field ) } describe "#create" do before do provider.stubs(:exists?).returns(false) end it "should add -g when no gid is specified and group already exists" do Puppet::Util.stubs(:gid).returns(true) resource[:ensure] = :present provider.expects(:execute).with(includes('-g'), kind_of(Hash)) provider.create end it "should add -o when allowdupe is enabled and the user is being created" do resource[:allowdupe] = true provider.expects(:execute).with(includes('-o'), kind_of(Hash)) provider.create end describe "on systems that support has_system", :if => described_class.system_users? do it "should add -r when system is enabled" do resource[:system] = :true provider.should be_system_users provider.expects(:execute).with(includes('-r'), kind_of(Hash)) provider.create end end describe "on systems that do not support has_system", :unless => described_class.system_users? do it "should not add -r when system is enabled" do resource[:system] = :true provider.should_not be_system_users provider.expects(:execute).with(['/usr/sbin/useradd', 'myuser'], kind_of(Hash)) provider.create end end it "should set password age rules" do described_class.has_feature :manages_password_age resource[:password_min_age] = 5 resource[:password_max_age] = 10 provider.expects(:execute).with(includes('/usr/sbin/useradd'), kind_of(Hash)) provider.expects(:execute).with(['/usr/bin/chage', '-m', 5, '-M', 10, 'myuser']) provider.create end describe "on systems with the libuser and forcelocal=true" do before do described_class.has_feature :libuser resource[:forcelocal] = true end it "should use luseradd instead of useradd" do provider.expects(:execute).with(includes('/usr/sbin/luseradd'), has_entry(:custom_environment, has_key('LIBUSER_CONF'))) provider.create end it "should NOT use -o when allowdupe=true" do resource[:allowdupe] = :true provider.expects(:execute).with(Not(includes('-o')), has_entry(:custom_environment, has_key('LIBUSER_CONF'))) provider.create end it "should raise an exception for duplicate UIDs" do resource[:uid] = 505 provider.stubs(:finduser).returns(true) lambda { provider.create }.should raise_error(Puppet::Error, "UID 505 already exists, use allowdupe to force user creation") end it "should not use -G for luseradd and should call usermod with -G after luseradd when groups property is set" do resource[:groups] = ['group1', 'group2'] provider.expects(:execute).with(Not(includes("-G")), has_entry(:custom_environment, has_key('LIBUSER_CONF'))) provider.expects(:execute).with(includes('/usr/sbin/usermod')) provider.create end it "should not use -m when managehome set" do resource[:managehome] = :true provider.expects(:execute).with(Not(includes('-m')), has_entry(:custom_environment, has_key('LIBUSER_CONF'))) provider.create end it "should not use -e with luseradd, should call usermod with -e after luseradd when expiry is set" do resource[:expiry] = '2038-01-24' provider.expects(:execute).with(all_of(includes('/usr/sbin/luseradd'), Not(includes('-e'))), has_entry(:custom_environment, has_key('LIBUSER_CONF'))) provider.expects(:execute).with(all_of(includes('/usr/sbin/usermod'), includes('-e'))) provider.create end + + it "should use userdel to delete users" do + resource[:ensure] = :absent + provider.stubs(:exists?).returns(true) + provider.expects(:execute).with(includes('/usr/sbin/userdel')) + provider.delete + end end describe "on systems that allow to set shell" do it "should trigger shell validation" do resource[:shell] = '/bin/bash' provider.expects(:check_valid_shell) provider.expects(:execute).with(includes('-s'), kind_of(Hash)) provider.create end end end describe "#uid=" do it "should add -o when allowdupe is enabled and the uid is being modified" do resource[:allowdupe] = :true provider.expects(:execute).with(['/usr/sbin/usermod', '-u', 150, '-o', 'myuser']) provider.uid = 150 end end describe "#expiry=" do it "should pass expiry to usermod as MM/DD/YY when on Solaris" do Facter.expects(:value).with(:operatingsystem).returns 'Solaris' resource[:expiry] = '2012-10-31' provider.expects(:execute).with(['/usr/sbin/usermod', '-e', '10/31/2012', 'myuser']) provider.expiry = '2012-10-31' end it "should pass expiry to usermod as YYYY-MM-DD when not on Solaris" do Facter.expects(:value).with(:operatingsystem).returns 'not_solaris' resource[:expiry] = '2012-10-31' provider.expects(:execute).with(['/usr/sbin/usermod', '-e', '2012-10-31', 'myuser']) provider.expiry = '2012-10-31' end it "should use -e with an empty string when the expiry property is removed" do resource[:expiry] = :absent provider.expects(:execute).with(['/usr/sbin/usermod', '-e', '', 'myuser']) provider.expiry = :absent end end describe "#check_allow_dup" do it "should return an array with a flag if dup is allowed" do resource[:allowdupe] = :true provider.check_allow_dup.must == ["-o"] end it "should return an empty array if no dup is allowed" do resource[:allowdupe] = :false provider.check_allow_dup.must == [] end end describe "#check_system_users" do it "should check system users" do described_class.expects(:system_users?).returns true resource.expects(:system?) provider.check_system_users end it "should return an array with a flag if it's a system user" do described_class.expects(:system_users?).returns true resource[:system] = :true provider.check_system_users.must == ["-r"] end it "should return an empty array if it's not a system user" do described_class.expects(:system_users?).returns true resource[:system] = :false provider.check_system_users.must == [] end it "should return an empty array if system user is not featured" do described_class.expects(:system_users?).returns false resource[:system] = :true provider.check_system_users.must == [] end end describe "#check_manage_home" do it "should return an array with -m flag if home is managed" do resource[:managehome] = :true provider.expects(:execute).with(includes('-m'), kind_of(Hash)) provider.create end it "should return an array with -r flag if home is managed" do resource[:managehome] = :true resource[:ensure] = :absent provider.stubs(:exists?).returns(true) provider.expects(:execute).with(includes('-r')) provider.delete end it "should use -M flag if home is not managed and on Redhat" do Facter.stubs(:value).with(:osfamily).returns("RedHat") resource[:managehome] = :false provider.expects(:execute).with(includes('-M'), kind_of(Hash)) provider.create end it "should not use -M flag if home is not managed and not on Redhat" do Facter.stubs(:value).with(:osfamily).returns("not RedHat") resource[:managehome] = :false provider.expects(:execute).with(Not(includes('-M')), kind_of(Hash)) provider.create end end describe "#addcmd" do before do resource[:allowdupe] = :true resource[:managehome] = :true resource[:system] = :true resource[:groups] = [ 'somegroup' ] end it "should call command with :add" do provider.expects(:command).with(:add) provider.addcmd end it "should add properties" do provider.expects(:add_properties).returns(['-foo_add_properties']) provider.addcmd.should include '-foo_add_properties' end it "should check and add if dup allowed" do provider.expects(:check_allow_dup).returns(['-allow_dup_flag']) provider.addcmd.should include '-allow_dup_flag' end it "should check and add if home is managed" do provider.expects(:check_manage_home).returns(['-manage_home_flag']) provider.addcmd.should include '-manage_home_flag' end it "should add the resource :name" do provider.addcmd.should include 'myuser' end describe "on systems featuring system_users", :if => described_class.system_users? do it "should return an array with -r if system? is true" do resource[:system] = :true provider.addcmd.should include("-r") end it "should return an array without -r if system? is false" do resource[:system] = :false provider.addcmd.should_not include("-r") end end describe "on systems not featuring system_users", :unless => described_class.system_users? do [:false, :true].each do |system| it "should return an array without -r if system? is #{system}" do resource[:system] = system provider.addcmd.should_not include("-r") end end end it "should return an array with the full command and expiry as MM/DD/YY when on Solaris" do Facter.stubs(:value).with(:operatingsystem).returns 'Solaris' described_class.expects(:system_users?).returns true resource[:expiry] = "2012-08-18" provider.addcmd.must == ['/usr/sbin/useradd', '-e', '08/18/2012', '-G', 'somegroup', '-o', '-m', '-r', 'myuser'] end it "should return an array with the full command and expiry as YYYY-MM-DD when not on Solaris" do Facter.stubs(:value).with(:operatingsystem).returns 'not_solaris' described_class.expects(:system_users?).returns true resource[:expiry] = "2012-08-18" provider.addcmd.must == ['/usr/sbin/useradd', '-e', '2012-08-18', '-G', 'somegroup', '-o', '-m', '-r', 'myuser'] end it "should return an array without -e if expiry is undefined full command" do described_class.expects(:system_users?).returns true provider.addcmd.must == ["/usr/sbin/useradd", "-G", "somegroup", "-o", "-m", "-r", "myuser"] end it "should pass -e \"\" if the expiry has to be removed" do described_class.expects(:system_users?).returns true resource[:expiry] = :absent provider.addcmd.must == ['/usr/sbin/useradd', '-e', '', '-G', 'somegroup', '-o', '-m', '-r', 'myuser'] end end { :password_min_age => 10, :password_max_age => 20, :password => '$6$FvW8Ib8h$qQMI/CR9m.QzIicZKutLpBgCBBdrch1IX0rTnxuI32K1pD9.RXZrmeKQlaC.RzODNuoUtPPIyQDufunvLOQWF0' }.each_pair do |property, expected_value| describe "##{property}" do before :each do resource # just to link the resource to the provider end it "should return absent if libshadow feature is not present" do Puppet.features.stubs(:libshadow?).returns false # Shadow::Passwd.expects(:getspnam).never # if we really don't have libshadow we dont have Shadow::Passwd either provider.send(property).should == :absent end it "should return absent if user cannot be found", :if => Puppet.features.libshadow? do Shadow::Passwd.expects(:getspnam).with('myuser').returns nil provider.send(property).should == :absent end it "should return the correct value if libshadow is present", :if => Puppet.features.libshadow? do Shadow::Passwd.expects(:getspnam).with('myuser').returns shadow_entry provider.send(property).should == expected_value end end end describe '#expiry' do before :each do resource # just to link the resource to the provider end it "should return absent if libshadow feature is not present" do Puppet.features.stubs(:libshadow?).returns false provider.expiry.should == :absent end it "should return absent if user cannot be found", :if => Puppet.features.libshadow? do Shadow::Passwd.expects(:getspnam).with('myuser').returns nil provider.expiry.should == :absent end it "should return absent if expiry is -1", :if => Puppet.features.libshadow? do shadow_entry.sp_expire = -1 Shadow::Passwd.expects(:getspnam).with('myuser').returns shadow_entry provider.expiry.should == :absent end it "should convert to YYYY-MM-DD", :if => Puppet.features.libshadow? do Shadow::Passwd.expects(:getspnam).with('myuser').returns shadow_entry provider.expiry.should == '2013-01-01' end end describe "#passcmd" do before do resource[:allowdupe] = :true resource[:managehome] = :true resource[:system] = :true described_class.has_feature :manages_password_age end it "should call command with :pass" do # command(:password) is only called inside passcmd if # password_min_age or password_max_age is set resource[:password_min_age] = 123 provider.expects(:command).with(:password) provider.passcmd end it "should return nil if neither min nor max is set" do provider.passcmd.must be_nil end it "should return a chage command array with -m and the user name if password_min_age is set" do resource[:password_min_age] = 123 provider.passcmd.must == ['/usr/bin/chage','-m',123,'myuser'] end it "should return a chage command array with -M if password_max_age is set" do resource[:password_max_age] = 999 provider.passcmd.must == ['/usr/bin/chage','-M',999,'myuser'] end it "should return a chage command array with -M -m if both password_min_age and password_max_age are set" do resource[:password_min_age] = 123 resource[:password_max_age] = 999 provider.passcmd.must == ['/usr/bin/chage','-m',123,'-M',999,'myuser'] end end describe "#check_valid_shell" do it "should raise an error if shell does not exist" do resource[:shell] = 'foo/bin/bash' lambda { provider.check_valid_shell }.should raise_error(Puppet::Error, /Shell foo\/bin\/bash must exist/) end it "should raise an error if the shell is not executable" do resource[:shell] = 'LICENSE' lambda { provider.check_valid_shell }.should raise_error(Puppet::Error, /Shell LICENSE must be executable/) end end end