diff --git a/lib/puppet/provider/user/user_role_add.rb b/lib/puppet/provider/user/user_role_add.rb index 2abec5f45..f3100e04f 100644 --- a/lib/puppet/provider/user/user_role_add.rb +++ b/lib/puppet/provider/user/user_role_add.rb @@ -1,209 +1,210 @@ require 'puppet/util' require 'puppet/util/user_attr' require 'date' Puppet::Type.type(:user).provide :user_role_add, :parent => :useradd, :source => :useradd do desc "User and role management on Solaris, via `useradd` and `roleadd`." defaultfor :osfamily => :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, :manages_shell #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}", detail.backtrace 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") 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 # This helper makes it possible to test this on stub data without having to # do too many crazy things! def target_file_path "/etc/shadow" end private :target_file_path #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(target_file_path). reject { |r| r =~ /^[^\w]/ }. - collect { |l| l.chomp.split(':') }. + # PUP-229 dont suppress the empty fields + collect { |l| l.chomp.split(':', -1) }. find { |user, _| user == @resource[:name] } end def password shadow_entry[1] if shadow_entry end def password_min_age - shadow_entry ? shadow_entry[3] : :absent + shadow_entry[3].empty? ? -1 : shadow_entry[3] end def password_max_age return :absent unless shadow_entry - shadow_entry[4] || -1 + shadow_entry[4].empty? ? -1 : shadow_entry[4] end # Read in /etc/shadow, find the line for our used and rewrite it with the # new pw. Smooth like 80 grit sandpaper. # # Now uses the `replace_file` mechanism to minimize the chance that we lose # data, but it is still terrible. We still skip platform locking, so a # concurrent `vipw -s` session will have no idea we risk data loss. def password=(cryptopw) begin shadow = File.read(target_file_path) # Go Mifune loves the race here where we can lose data because # /etc/shadow changed between reading it and writing it. # --daniel 2012-02-05 Puppet::Util.replace_file(target_file_path, 0640) do |fh| shadow.each_line do |line| line_arr = line.split(':') if line_arr[0] == @resource[:name] line_arr[1] = cryptopw line_arr[2] = (Date.today - Date.new(1970,1,1)).to_i.to_s line = line_arr.join(':') end fh.print line end end rescue => detail self.fail Puppet::Error, "Could not write replace #{target_file_path}: #{detail}", detail end end end diff --git a/spec/unit/provider/user/user_role_add_spec.rb b/spec/unit/provider/user/user_role_add_spec.rb index 8dd48e767..42cc4995e 100755 --- a/spec/unit/provider/user/user_role_add_spec.rb +++ b/spec/unit/provider/user/user_role_add_spec.rb @@ -1,335 +1,357 @@ require 'spec_helper' require 'puppet_spec/files' require 'tempfile' describe Puppet::Type.type(:user).provider(:user_role_add), :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files let(:resource) { Puppet::Type.type(:user).new(:name => 'myuser', :managehome => false, :allowdupe => false) } let(:provider) { described_class.new(resource) } before do resource.stubs(:should).returns "fakeval" resource.stubs(:should).with(:keys).returns Hash.new resource.stubs(:[]).returns "fakeval" end describe "#command" do before do klass = stub("provider") klass.stubs(:command).with(:foo).returns("userfoo") klass.stubs(:command).with(:role_foo).returns("rolefoo") provider.stubs(:class).returns(klass) end it "should use the command if not a role and ensure!=role" do provider.stubs(:is_role?).returns(false) provider.stubs(:exists?).returns(false) resource.stubs(:[]).with(:ensure).returns(:present) provider.class.stubs(:foo) provider.command(:foo).should == "userfoo" end it "should use the role command when a role" do provider.stubs(:is_role?).returns(true) provider.command(:foo).should == "rolefoo" end it "should use the role command when !exists and ensure=role" do provider.stubs(:is_role?).returns(false) provider.stubs(:exists?).returns(false) resource.stubs(:[]).with(:ensure).returns(:role) provider.command(:foo).should == "rolefoo" end end describe "#transition" do it "should return the type set to whatever is passed in" do provider.expects(:command).with(:modify).returns("foomod") provider.transition("bar").include?("type=bar") end end describe "#create" do before do provider.stubs(:password=) end it "should use the add command when the user is not a role" do provider.stubs(:is_role?).returns(false) provider.expects(:addcmd).returns("useradd") provider.expects(:run).at_least_once provider.create end it "should use transition(normal) when the user is a role" do provider.stubs(:is_role?).returns(true) provider.expects(:transition).with("normal") provider.expects(:run) provider.create end it "should set password age rules" do resource = Puppet::Type.type(:user).new :name => "myuser", :password_min_age => 5, :password_max_age => 10, :provider => :user_role_add provider = described_class.new(resource) provider.stubs(:user_attributes) provider.stubs(:execute) provider.expects(:execute).with { |cmd, *args| args == ["-n", 5, "-x", 10, "myuser"] } provider.create end end describe "#destroy" do it "should use the delete command if the user exists and is not a role" do provider.stubs(:exists?).returns(true) provider.stubs(:is_role?).returns(false) provider.expects(:deletecmd) provider.expects(:run) provider.destroy end it "should use the delete command if the user is a role" do provider.stubs(:exists?).returns(true) provider.stubs(:is_role?).returns(true) provider.expects(:deletecmd) provider.expects(:run) provider.destroy end end describe "#create_role" do it "should use the transition(role) if the user exists" do provider.stubs(:exists?).returns(true) provider.stubs(:is_role?).returns(false) provider.expects(:transition).with("role") provider.expects(:run) provider.create_role end it "should use the add command when role doesn't exists" do provider.stubs(:exists?).returns(false) provider.expects(:addcmd) provider.expects(:run) provider.create_role end end describe "with :allow_duplicates" do before do resource.stubs(:allowdupe?).returns true provider.stubs(:is_role?).returns(false) provider.stubs(:execute) resource.stubs(:system?).returns false provider.expects(:execute).with { |args| args.include?("-o") } end it "should add -o when the user is being created" do provider.stubs(:password=) provider.create end it "should add -o when the uid is being modified" do provider.uid = 150 end end [:roles, :auths, :profiles].each do |val| context "#send" do describe "when getting #{val}" do it "should get the user_attributes" do provider.expects(:user_attributes) provider.send(val) end it "should get the #{val} attribute" do attributes = mock("attributes") attributes.expects(:[]).with(val) provider.stubs(:user_attributes).returns(attributes) provider.send(val) end end end end describe "#keys" do it "should get the user_attributes" do provider.expects(:user_attributes) provider.keys end it "should call removed_managed_attributes" do provider.stubs(:user_attributes).returns({ :type => "normal", :foo => "something" }) provider.expects(:remove_managed_attributes) provider.keys end it "should removed managed attribute (type, auths, roles, etc)" do provider.stubs(:user_attributes).returns({ :type => "normal", :foo => "something" }) provider.keys.should == { :foo => "something" } end end describe "#add_properties" do it "should call build_keys_cmd" do resource.stubs(:should).returns "" resource.expects(:should).with(:keys).returns({ :foo => "bar" }) provider.expects(:build_keys_cmd).returns([]) provider.add_properties end it "should add the elements of the keys hash to an array" do resource.stubs(:should).returns "" resource.expects(:should).with(:keys).returns({ :foo => "bar"}) provider.add_properties.must == ["-K", "foo=bar"] end end describe "#build_keys_cmd" do it "should build cmd array with keypairs seperated by -K ending with user" do provider.build_keys_cmd({"foo" => "bar", "baz" => "boo"}).should.eql? ["-K", "foo=bar", "-K", "baz=boo"] end end describe "#keys=" do before do provider.stubs(:is_role?).returns(false) end it "should run a command" do provider.expects(:run) provider.keys=({}) end it "should build the command" do resource.stubs(:[]).with(:name).returns("someuser") provider.stubs(:command).returns("usermod") provider.expects(:build_keys_cmd).returns(["-K", "foo=bar"]) provider.expects(:run).with(["usermod", "-K", "foo=bar", "someuser"], "modify attribute key pairs") provider.keys=({}) end end describe "#password" do before do @array = mock "array" end it "should readlines of /etc/shadow" do File.expects(:readlines).with("/etc/shadow").returns([]) provider.password end it "should reject anything that doesn't start with alpha numerics" do @array.expects(:reject).returns([]) File.stubs(:readlines).with("/etc/shadow").returns(@array) provider.password end it "should collect splitting on ':'" do @array.stubs(:reject).returns(@array) @array.expects(:collect).returns([]) File.stubs(:readlines).with("/etc/shadow").returns(@array) provider.password end it "should find the matching user" do resource.stubs(:[]).with(:name).returns("username") @array.stubs(:reject).returns(@array) @array.stubs(:collect).returns([["username", "hashedpassword"], ["someoneelse", "theirpassword"]]) File.stubs(:readlines).with("/etc/shadow").returns(@array) provider.password.must == "hashedpassword" end it "should get the right password" do resource.stubs(:[]).with(:name).returns("username") File.stubs(:readlines).with("/etc/shadow").returns(["#comment", " nonsense", " ", "username:hashedpassword:stuff:foo:bar:::", "other:pword:yay:::"]) provider.password.must == "hashedpassword" end end describe "#password=" do let(:path) { tmpfile('etc-shadow') } before :each do provider.stubs(:target_file_path).returns(path) end def write_fixture(content) File.open(path, 'w') { |f| f.print(content) } end it "should update the target user" do write_fixture <