diff --git a/lib/puppet/provider/nameservice.rb b/lib/puppet/provider/nameservice.rb index 9830fab54..d57052bd9 100644 --- a/lib/puppet/provider/nameservice.rb +++ b/lib/puppet/provider/nameservice.rb @@ -1,274 +1,276 @@ require 'puppet' # This is the parent class of all NSS classes. They're very different in # their backend, but they're pretty similar on the front-end. This class # provides a way for them all to be as similar as possible. class Puppet::Provider::NameService < Puppet::Provider class << self def autogen_default(param) defined?(@autogen_defaults) ? @autogen_defaults[symbolize(param)] : nil end def autogen_defaults(hash) @autogen_defaults ||= {} hash.each do |param, value| @autogen_defaults[symbolize(param)] = value end end def initvars @checks = {} super end def instances objects = [] listbyname do |name| objects << new(:name => name, :ensure => :present) end objects end def option(name, option) name = name.intern if name.is_a? String (defined?(@options) and @options.include? name and @options[name].include? option) ? @options[name][option] : nil end def options(name, hash) raise Puppet::DevError, "#{name} is not a valid attribute for #{resource_type.name}" unless resource_type.valid_parameter?(name) @options ||= {} @options[name] ||= {} # Set options individually, so we can call the options method # multiple times. hash.each do |param, value| @options[name][param] = value end end # List everything out by name. Abstracted a bit so that it works # for both users and groups. def listbyname names = [] Etc.send("set#{section()}ent") begin while ent = Etc.send("get#{section()}ent") names << ent.name yield ent.name if block_given? end ensure Etc.send("end#{section()}ent") end names end def resource_type=(resource_type) super @resource_type.validproperties.each do |prop| next if prop == :ensure define_method(prop) { get(prop) || :absent} unless public_method_defined?(prop) define_method(prop.to_s + "=") { |*vals| set(prop, *vals) } unless public_method_defined?(prop.to_s + "=") end end # This is annoying, but there really aren't that many options, # and this *is* built into Ruby. def section unless defined?(@resource_type) raise Puppet::DevError, "Cannot determine Etc section without a resource type" end if @resource_type.name == :group "gr" else "pw" end end def validate(name, value) name = name.intern if name.is_a? String if @checks.include? name block = @checks[name][:block] raise ArgumentError, "Invalid value #{value}: #{@checks[name][:error]}" unless block.call(value) end end def verify(name, error, &block) name = name.intern if name.is_a? String @checks[name] = {:error => error, :block => block} end private def op(property) @ops[property.name] || ("-#{property.name}") end end # Autogenerate a value. Mostly used for uid/gid, but also used heavily # with DirectoryServices, because DirectoryServices is stupid. def autogen(field) field = symbolize(field) id_generators = {:user => :uid, :group => :gid} if id_generators[@resource.class.name] == field return autogen_id(field) else if value = self.class.autogen_default(field) return value elsif respond_to?("autogen_#{field}") return send("autogen_#{field}") else return nil end end end # Autogenerate either a uid or a gid. This is hard-coded: we can only # generate one field type per class. def autogen_id(field) highest = 0 group = method = nil case @resource.class.name when :user; group = :passwd; method = :uid when :group; group = :group; method = :gid else raise Puppet::DevError, "Invalid resource name #{resource}" end # Make sure we don't use the same value multiple times if defined?(@@prevauto) @@prevauto += 1 else Etc.send(group) { |obj| if obj.gid > highest highest = obj.send(method) unless obj.send(method) > 65000 end } @@prevauto = highest + 1 end @@prevauto end def create if exists? info "already exists" # The object already exists return nil end begin execute(self.addcmd) - execute(self.passcmd) if self.feature? :manages_password_age + if feature?(:manages_password_age) && (cmd = passcmd) + execute(cmd) + end rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not create #{@resource.class.name} #{@resource.name}: #{detail}" end end def delete unless exists? info "already absent" # the object already doesn't exist return nil end begin execute(self.deletecmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not delete #{@resource.class.name} #{@resource.name}: #{detail}" end end def ensure if exists? :present else :absent end end # Does our object exist? def exists? !!getinfo(true) end # Retrieve a specific value by name. def get(param) (hash = getinfo(false)) ? hash[param] : nil end # Retrieve what we can about our object def getinfo(refresh) if @objectinfo.nil? or refresh == true @etcmethod ||= ("get" + self.class.section.to_s + "nam").intern begin @objectinfo = Etc.send(@etcmethod, @resource[:name]) rescue ArgumentError => detail @objectinfo = nil end end # Now convert our Etc struct into a hash. @objectinfo ? info2hash(@objectinfo) : nil end # The list of all groups the user is a member of. Different # user mgmt systems will need to override this method. def groups groups = [] # Reset our group list Etc.setgrent user = @resource[:name] # Now iterate across all of the groups, adding each one our # user is a member of while group = Etc.getgrent members = group.mem groups << group.name if members.include? user end # We have to close the file, so each listing is a separate # reading of the file. Etc.endgrent groups.join(",") end # Convert the Etc struct into a hash. def info2hash(info) hash = {} self.class.resource_type.validproperties.each do |param| method = posixmethod(param) hash[param] = info.send(posixmethod(param)) if info.respond_to? method end hash end def initialize(resource) super @objectinfo = nil end def set(param, value) self.class.validate(param, value) cmd = modifycmd(param, value) raise Puppet::DevError, "Nameservice command must be an array" unless cmd.is_a?(Array) begin execute(cmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}" end end end diff --git a/lib/puppet/provider/user/user_role_add.rb b/lib/puppet/provider/user/user_role_add.rb index c8be75a8f..caca1ef88 100644 --- a/lib/puppet/provider/user/user_role_add.rb +++ b/lib/puppet/provider/user/user_role_add.rb @@ -1,190 +1,192 @@ require 'puppet/util/user_attr' Puppet::Type.type(:user).provide :user_role_add, :parent => :useradd, :source => :useradd do desc "User management inherits `useradd` and adds logic to manage roles on Solaris using roleadd." defaultfor :operatingsystem => :solaris commands :add => "useradd", :delete => "userdel", :modify => "usermod", :password => "passwd", :role_add => "roleadd", :role_delete => "roledel", :role_modify => "rolemod" options :home, :flag => "-d", :method => :dir options :comment, :method => :gecos options :groups, :flag => "-G" options :roles, :flag => "-R" options :auths, :flag => "-A" options :profiles, :flag => "-P" options :password_min_age, :flag => "-n" options :password_max_age, :flag => "-x" 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_solaris_rbac, :manages_passwords, :manages_password_age #must override this to hand the keyvalue pairs def add_properties cmd = [] Puppet::Type.type(:user).validproperties.each do |property| #skip the password because we can't create it with the solaris useradd next if [:ensure, :password, :password_min_age, :password_max_age].include?(property) # 1680 Now you can set the hashed passwords on solaris:lib/puppet/provider/user/user_role_add.rb # the value needs to be quoted, mostly because -c might # have spaces in it if value = @resource.should(property) and value != "" if property == :keys cmd += build_keys_cmd(value) else cmd << flag(property) << value end end end cmd end def user_attributes @user_attributes ||= UserAttr.get_attributes_by_name(@resource[:name]) end def flush @user_attributes = nil end def command(cmd) cmd = ("role_#{cmd}").intern if is_role? or (!exists? and @resource[:ensure] == :role) super(cmd) end def is_role? user_attributes and user_attributes[:type] == "role" end def run(cmd, msg) execute(cmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not #{msg} #{@resource.class.name} #{@resource.name}: #{detail}" end def transition(type) cmd = [command(:modify)] cmd << "-K" << "type=#{type}" cmd += add_properties cmd << @resource[:name] end def create if is_role? run(transition("normal"), "transition role to") else run(addcmd, "create") - run(passcmd, "change password policy for") + if cmd = passcmd + run(cmd, "change password policy for") + end end # added to handle case when password is specified self.password = @resource[:password] if @resource[:password] end def destroy run(deletecmd, "delete "+ (is_role? ? "role" : "user")) end def create_role if exists? and !is_role? run(transition("role"), "transition user to") else run(addcmd, "create role") end end def roles user_attributes[:roles] if user_attributes end def auths user_attributes[:auths] if user_attributes end def profiles user_attributes[:profiles] if user_attributes end def project user_attributes[:project] if user_attributes end def managed_attributes [:name, :type, :roles, :auths, :profiles, :project] end def remove_managed_attributes managed = managed_attributes user_attributes.select { |k,v| !managed.include?(k) }.inject({}) { |hash, array| hash[array[0]] = array[1]; hash } end def keys if user_attributes #we have to get rid of all the keys we are managing another way remove_managed_attributes end end def build_keys_cmd(keys_hash) cmd = [] keys_hash.each do |k,v| cmd << "-K" << "#{k}=#{v}" end cmd end def keys=(keys_hash) run([command(:modify)] + build_keys_cmd(keys_hash) << @resource[:name], "modify attribute key pairs") end #Read in /etc/shadow, find the line for this user (skipping comments, because who knows) and return it #No abstraction, all esoteric knowledge of file formats, yay def shadow_entry return @shadow_entry if defined? @shadow_entry @shadow_entry = File.readlines("/etc/shadow").reject { |r| r =~ /^[^\w]/ }.collect { |l| l.chomp.split(':') }.find { |user, _| user == @resource[:name] } end def password shadow_entry[1] if shadow_entry end def min_age shadow_entry ? shadow_entry[3] : :absent end def max_age shadow_entry ? shadow_entry[4] : :absent end #Read in /etc/shadow, find the line for our used and rewrite it with the new pw #Smooth like 80 grit def password=(cryptopw) begin File.open("/etc/shadow", "r") do |shadow| File.open("/etc/shadow_tmp", "w", 0600) do |shadow_tmp| while line = shadow.gets line_arr = line.split(':') if line_arr[0] == @resource[:name] line_arr[1] = cryptopw line = line_arr.join(':') end shadow_tmp.print line end end end File.rename("/etc/shadow_tmp", "/etc/shadow") rescue => detail fail "Could not write temporary shadow file: #{detail}" ensure # Make sure this *always* gets deleted File.unlink("/etc/shadow_tmp") if File.exist?("/etc/shadow_tmp") end end end diff --git a/lib/puppet/provider/user/useradd.rb b/lib/puppet/provider/user/useradd.rb index 9a62db464..5a163f35a 100644 --- a/lib/puppet/provider/user/useradd.rb +++ b/lib/puppet/provider/user/useradd.rb @@ -1,110 +1,109 @@ require 'puppet/provider/nameservice/objectadd' 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 the `Shadow Password` Ruby library often known as ruby-libshadow 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" options :password_max_age, :flag => "-M" 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 :manages_passwords, :manages_password_age if Puppet.features.libshadow? def check_allow_dup @resource.allowdupe? ? ["-o"] : [] end def check_manage_home cmd = [] if @resource.managehome? cmd << "-m" elsif %w{Fedora RedHat CentOS OEL OVS}.include?(Facter.value("operatingsystem")) cmd << "-M" end cmd end def check_manage_expiry cmd = [] if @resource[:expiry] cmd << "-e #{@resource[:expiry]}" end cmd end def add_properties cmd = [] Puppet::Type.type(:user).validproperties.each do |property| next if property == :ensure next if property.to_s =~ /password_.+_age/ # the value needs to be quoted, mostly because -c might # have spaces in it if value = @resource.should(property) and value != "" cmd << flag(property) << value end end cmd end def addcmd cmd = [command(:add)] cmd += add_properties cmd += check_allow_dup cmd += check_manage_home cmd += check_manage_expiry cmd << @resource[:name] end def passcmd - cmd = [command(:password)] - [:password_min_age, :password_max_age].each do |property| - if value = @resource.should(property) - cmd << flag(property) << value - end + 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 - cmd << @resource[:name] end def min_age if Puppet.features.libshadow? if ent = Shadow::Passwd.getspnam(@resource.name) return ent.sp_min end end :absent end def max_age if Puppet.features.libshadow? if ent = Shadow::Passwd.getspnam(@resource.name) return ent.sp_max end end :absent end # Retrieve the password using the Shadow Password library def password if Puppet.features.libshadow? if ent = Shadow::Passwd.getspnam(@resource.name) return ent.sp_pwdp end end :absent end end diff --git a/spec/unit/provider/user/useradd_spec.rb b/spec/unit/provider/user/useradd_spec.rb index 26367c584..9ebba596c 100755 --- a/spec/unit/provider/user/useradd_spec.rb +++ b/spec/unit/provider/user/useradd_spec.rb @@ -1,134 +1,176 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' provider_class = Puppet::Type.type(:user).provider(:useradd) describe provider_class do before do @resource = stub("resource", :name => "myuser", :managehome? => nil) @resource.stubs(:should).returns "fakeval" @resource.stubs(:[]).returns "fakeval" @provider = provider_class.new(@resource) end # #1360 it "should add -o when allowdupe is enabled and the user is being created" do @resource.expects(:allowdupe?).returns true @provider.stubs(:execute) @provider.expects(:execute).with { |args| args.include?("-o") } @provider.create end it "should add -o when allowdupe is enabled and the uid is being modified" do @resource.expects(:allowdupe?).returns true @provider.expects(:execute).with { |args| args.include?("-o") } @provider.uid = 150 end it "should set password age rules" do provider_class.has_feature :manages_password_age @resource = Puppet::Type.type(:user).new :name => "myuser", :password_min_age => 5, :password_max_age => 10, :provider => :useradd @provider = provider_class.new(@resource) @provider.stubs(:execute) @provider.expects(:execute).with { |cmd, *args| args == ["-m", 5, "-M", 10, "myuser"] } @provider.create end describe "when checking to add allow dup" do it "should check allow dup" do @resource.expects(:allowdupe?) @provider.check_allow_dup end it "should return an array with a flag if dup is allowed" do @resource.stubs(:allowdupe?).returns true @provider.check_allow_dup.must == ["-o"] end it "should return an empty array if no dup is allowed" do @resource.stubs(:allowdupe?).returns false @provider.check_allow_dup.must == [] end end describe "when checking manage home" do it "should check manage home" do @resource.expects(:managehome?) @provider.check_manage_home end it "should return an array with -m flag if home is managed" do @resource.stubs(:managehome?).returns true @provider.check_manage_home.must == ["-m"] end it "should return an array with -M if home is not managed and on Redhat" do Facter.stubs(:value).with("operatingsystem").returns("RedHat") @resource.stubs(:managehome?).returns false @provider.check_manage_home.must == ["-M"] end it "should return an empty array if home is not managed and not on Redhat" do Facter.stubs(:value).with("operatingsystem").returns("some OS") @resource.stubs(:managehome?).returns false @provider.check_manage_home.must == [] end end describe "when adding properties" do it "should get the valid properties" it "should not add the ensure property" it "should add the flag and value to an array" it "should return and array of flags and values" end describe "when calling addcmd" do before do @resource.stubs(:allowdupe?).returns true @resource.stubs(:managehome?).returns true 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([]) @provider.addcmd end it "should check and add if dup allowed" do @provider.expects(:check_allow_dup).returns([]) @provider.addcmd end it "should check and add if home is managed" do @provider.expects(:check_manage_home).returns([]) @provider.addcmd end it "should add the resource :name" do @resource.expects(:[]).with(:name) @provider.addcmd end it "should return an array with full command" do @provider.stubs(:command).with(:add).returns("useradd") @provider.stubs(:add_properties).returns(["-G", "somegroup"]) @resource.stubs(:[]).with(:name).returns("someuser") @resource.stubs(:[]).with(:expiry).returns("somedate") @provider.addcmd.must == ["useradd", "-G", "somegroup", "-o", "-m", '-e somedate', "someuser"] end it "should return an array without -e if expery is undefined full command" do @provider.stubs(:command).with(:add).returns("useradd") @provider.stubs(:add_properties).returns(["-G", "somegroup"]) @resource.stubs(:[]).with(:name).returns("someuser") @resource.stubs(:[]).with(:expiry).returns nil @provider.addcmd.must == ["useradd", "-G", "somegroup", "-o", "-m", "someuser"] end end + + describe "when calling passcmd" do + before do + @resource.stubs(:allowdupe?).returns true + @resource.stubs(:managehome?).returns true + end + + it "should call command with :pass" do + @provider.expects(:command).with(:password) + @provider.passcmd + end + + it "should return nil if neither min nor max is set" do + @resource.stubs(:should).with(:password_min_age).returns nil + @resource.stubs(:should).with(:password_max_age).returns nil + @provider.passcmd.must == nil + end + + it "should return a chage command array with -m and the user name if password_min_age is set" do + @provider.stubs(:command).with(:password).returns("chage") + @resource.stubs(:[]).with(:name).returns("someuser") + @resource.stubs(:should).with(:password_min_age).returns 123 + @resource.stubs(:should).with(:password_max_age).returns nil + @provider.passcmd.must == ['chage','-m',123,'someuser'] + end + + it "should return a chage command array with -M if password_max_age is set" do + @provider.stubs(:command).with(:password).returns("chage") + @resource.stubs(:[]).with(:name).returns("someuser") + @resource.stubs(:should).with(:password_min_age).returns nil + @resource.stubs(:should).with(:password_max_age).returns 999 + @provider.passcmd.must == ['chage','-M',999,'someuser'] + end + + it "should return a chage command array with -M -m if both password_min_age and password_max_age are set" do + @provider.stubs(:command).with(:password).returns("chage") + @resource.stubs(:[]).with(:name).returns("someuser") + @resource.stubs(:should).with(:password_min_age).returns 123 + @resource.stubs(:should).with(:password_max_age).returns 999 + @provider.passcmd.must == ['chage','-m',123,'-M',999,'someuser'] + end + end end