From d1abb86565b63f98d8fc1395a78c840c45e47238 Mon Sep 17 00:00:00 2001 From: Andrew Shafer Date: Wed, 1 Oct 2008 18:58:09 -0600 Subject: [PATCH] Add role support to user type and an implemention modify user type: add ensure = role logic add roles property add manages_solaris_rbac feature refactored 'list' property to reuse logic for groups in roles --- lib/puppet/property/list.rb | 78 ++++++++++++ lib/puppet/provider/user/user_role_add.rb | 89 +++++++++++++ lib/puppet/type/user.rb | 98 +++++++-------- spec/unit/property/list.rb | 147 ++++++++++++++++++++++ spec/unit/provider/user/user_role_add.rb | 131 +++++++++++++++++++ 5 files changed, 489 insertions(+), 54 deletions(-) create mode 100644 lib/puppet/property/list.rb create mode 100644 lib/puppet/provider/user/user_role_add.rb create mode 100644 spec/unit/property/list.rb create mode 100644 spec/unit/provider/user/user_role_add.rb diff --git a/lib/puppet/property/list.rb b/lib/puppet/property/list.rb new file mode 100644 index 000000000..4e7f6ec90 --- /dev/null +++ b/lib/puppet/property/list.rb @@ -0,0 +1,78 @@ +require 'puppet/property' + +module Puppet + class Property + class List < Property + + def should_to_s(should_value) + #just return the should value + should_value + end + + def is_to_s(currentvalue) + currentvalue.join(delimiter) + end + + def membership + :membership + end + + def add_should_with_current(should, current) + if current.is_a?(Array) + should += current + end + should.uniq + end + + def inclusive? + @resource[membership] == :inclusive + end + + def should + unless defined? @should and @should + return nil + end + + members = @should + #inclusive means we are managing everything so if it isn't in should, its gone + if ! inclusive? + members = add_should_with_current(members, retrieve) + end + + members.sort.join(delimiter) + end + + def delimiter + "," + end + + def retrieve + #ok, some 'convention' if the list property is named groups, provider should implement a groups method + if tmp = provider.send(name) and tmp != :absent + return tmp.split(delimiter) + else + return :absent + end + end + + def prepare_is_for_comparison(is) + if is.is_a? Array + is = is.sort.join(delimiter) + end + is + end + + def insync?(is) + unless defined? @should and @should + return true + end + + unless is + return true + end + + return (prepare_is_for_comparison(is) == self.should) + end + end + end +end diff --git a/lib/puppet/provider/user/user_role_add.rb b/lib/puppet/provider/user/user_role_add.rb new file mode 100644 index 000000000..23581abbb --- /dev/null +++ b/lib/puppet/provider/user/user_role_add.rb @@ -0,0 +1,89 @@ +require 'puppet/util/user_attr' + +Puppet::Type.type(:user).provide :user_role_add, :parent => Puppet::Type::User::ProviderUseradd 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", :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" + + 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 + + if Puppet.features.libshadow? + has_feature :manages_passwords + end + + def user_attributes + @user_attributes ||= UserAttr.get_attributes_by_name(@resource[:name]) + end + + def flush + @user_attributes = nil + end + + def command(cmd) + if is_role? or (!exists? and @resource[:ensure] == :role) + cmd = ("role_" + cmd.to_s).intern + end + super(cmd) + end + + def is_role? + user_attributes and user_attributes[:type] == "role" + end + + def run(cmd, msg) + begin + execute(cmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not %s %s %s: %s" % + [msg, @resource.class.name, @resource.name, detail] + end + end + + def transition(type) + cmd = [command(:modify)] + cmd << "-K" << "type=#{type}" + cmd << @resource[:name] + end + + def create + if is_role? + run(transition("normal"), "transition role to") + else + run(addcmd, "create") + end + 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 + if user_attributes + user_attributes[:roles] + end + end +end + diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index bb0a86fd0..74c4c6a33 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,5 +1,6 @@ require 'etc' require 'facter' +require 'puppet/property/list' module Puppet newtype(:user) do @@ -21,6 +22,9 @@ module Puppet "The provider can modify user passwords, by accepting a password hash." + feature :manages_solaris_rbac, + "The provider can manage roles and normal users" + newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create @@ -30,6 +34,10 @@ module Puppet provider.delete end + newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do + provider.create_role + end + desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to @@ -44,7 +52,11 @@ module Puppet def retrieve if provider.exists? - return :present + if provider.respond_to?(:is_role?) and provider.is_role? + return :role + else + return :present + end else return :absent end @@ -125,72 +137,40 @@ module Puppet end end - newproperty(:groups) do + newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups of which the user is a member. The primary group should not be listed. Multiple groups should be specified as an array." - def should_to_s(newvalue) - self.should - end - - def is_to_s(currentvalue) - currentvalue.join(",") - end - - # We need to override this because the groups need to - # be joined with commas - def should - current_value = retrieve - - unless defined? @should and @should - return nil - end - - if @resource[:membership] == :inclusive - return @should.sort.join(",") - else - members = @should - if current_value.is_a?(Array) - members += current_value - end - return members.uniq.sort.join(",") - end - end - - def retrieve - if tmp = provider.groups and tmp != :absent - return tmp.split(",") - else - return :absent - end - end - - def insync?(is) - unless defined? @should and @should - return true - end - unless defined? is and is - return true - end - tmp = is - if is.is_a? Array - tmp = is.sort.join(",") - end - - return tmp == self.should - end - validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not numbers" end if value.include?(",") + puts value raise ArgumentError, "Group names must be provided as an array, not a comma-separated list" end end end + newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do + desc "The roles of which the user the user has. The roles should be + specified as an array." + + def membership + :role_membership + end + + validate do |value| + if value =~ /^\d+$/ + raise ArgumentError, "Role names must be provided, not numbers" + end + if value.include?(",") + raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" + end + end + end + newparam(:name) do desc "User name. While limitations are determined for each operating system, it is generally a good idea to keep to @@ -202,7 +182,17 @@ module Puppet desc "Whether specified groups should be treated as the only groups of which the user is a member or whether they should merely be treated as the minimum membership list." - + + newvalues(:inclusive, :minimum) + + defaultto :minimum + end + + newparam(:role_membership) do + desc "Whether specified roles should be treated as the only roles + of which the user is a member or whether they should merely + be treated as the minimum membership list." + newvalues(:inclusive, :minimum) defaultto :minimum diff --git a/spec/unit/property/list.rb b/spec/unit/property/list.rb new file mode 100644 index 000000000..9c832c0cd --- /dev/null +++ b/spec/unit/property/list.rb @@ -0,0 +1,147 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/property/list' + +list_class = Puppet::Property::List + +describe list_class do + + it "should be a subclass of Property" do + list_class.superclass.must == Puppet::Property + end + + describe "as an instance" do + before do + # Wow that's a messy interface to the resource. + list_class.initvars + @resource = stub 'resource', :[]= => nil, :property => nil + @property = list_class.new(:resource => @resource) + end + + it "should have a , as default delimiter" do + @property.delimiter.should == "," + end + + it "should have a :membership as default membership" do + @property.membership.should == :membership + end + + it "should return the same value passed into should_to_s" do + @property.should_to_s("foo") == "foo" + end + + it "should return the passed in array values joined with the delimiter from is_to_s" do + @property.is_to_s(["foo","bar"]).should == "foo,bar" + end + + describe "when adding should to current" do + it "should add the arrays when current is an array" do + @property.add_should_with_current(["foo"], ["bar"]).should == ["foo", "bar"] + end + + it "should return should if current is not a array" do + @property.add_should_with_current(["foo"], :absent).should == ["foo"] + end + + it "should return only the uniq elements" do + @property.add_should_with_current(["foo", "bar"], ["foo", "baz"]).should == ["foo", "bar", "baz"] + end + end + + describe "when calling inclusive?" do + it "should use the membership method to look up on the @resource" do + @property.expects(:membership).returns(:membership) + @resource.expects(:[]).with(:membership) + @property.inclusive? + end + + it "should return true when @resource[membership] == inclusive" do + @property.stubs(:membership).returns(:membership) + @resource.stubs(:[]).with(:membership).returns(:inclusive) + @property.inclusive?.must == true + end + + it "should return false when @resource[membership] != inclusive" do + @property.stubs(:membership).returns(:membership) + @resource.stubs(:[]).with(:membership).returns(:minimum) + @property.inclusive?.must == false + end + end + + describe "when calling should" do + it "should return nil if @should is nil" do + @property.should.must == nil + end + + it "should return the sorted values of @should as a string if inclusive" do + @property.should = ["foo", "bar"] + @property.expects(:inclusive?).returns(true) + @property.should.must == "bar,foo" + end + + it "should return the uniq sorted values of @should + retrieve as a string if !inclusive" do + @property.should = ["foo", "bar"] + @property.expects(:inclusive?).returns(false) + @property.expects(:retrieve).returns(["foo","baz"]) + @property.should.must == "bar,baz,foo" + end + end + + describe "when calling retrieve" do + before do + @provider = mock("provider") + @property.stubs(:provider).returns(@provider) + end + + it "should send 'name' to the provider" do + @provider.expects(:send).with(:group) + @property.expects(:name).returns(:group) + @property.retrieve + end + + it "should return an array with the provider returned info" do + @provider.stubs(:send).with(:group).returns("foo,bar,baz") + @property.stubs(:name).returns(:group) + @property.retrieve == ["foo", "bar", "baz"] + end + + it "should return :absent when the provider returns :absent" do + @provider.stubs(:send).with(:group).returns(:absent) + @property.stubs(:name).returns(:group) + @property.retrieve == :absent + end + end + + describe "when calling insync?" do + it "should return true unless @should is defined and not nil" do + @property.insync?("foo") == true + end + + it "should return true unless the passed in values is not nil" do + @property.should = "foo" + @property.insync?(nil) == true + end + + it "should call prepare_is_for_comparison with value passed in and should" do + @property.should = "foo" + @property.expects(:prepare_is_for_comparison).with("bar") + @property.expects(:should) + @property.insync?("bar") + end + + it "should return true if prepared value == should value" do + @property.should = "bar,foo" + @property.expects(:inclusive?).returns(true) + @property.insync?(["bar","foo"]).must == true + end + + it "should return false if prepared value != should value" do + @property.should = "bar,baz,foo" + @property.expects(:inclusive?).returns(true) + @property.insync?(["bar","foo"]).must == false + end + end + end +end diff --git a/spec/unit/provider/user/user_role_add.rb b/spec/unit/provider/user/user_role_add.rb new file mode 100644 index 000000000..87ad92049 --- /dev/null +++ b/spec/unit/provider/user/user_role_add.rb @@ -0,0 +1,131 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +provider_class = Puppet::Type.type(:user).provider(:user_role_add) + +describe provider_class do + before do + @resource = stub("resource", :name => "myuser", :managehome? => nil) + @resource.stubs(:should).returns "fakeval" + @resource.stubs(:[]).returns "fakeval" + @resource.stubs(:allowdupe?).returns false + @provider = provider_class.new(@resource) + end + + describe "when calling 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.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 "when calling transition" do + it "should return foomod setting the type to bar" do + @provider.expects(:command).with(:modify).returns("foomod") + @provider.transition("bar").should == ["foomod", "-K", "type=bar", "fakeval"] + end + end + + describe "when calling create" do + it "should use the add command when the user doesn't exist" do + @provider.stubs(:exists?).returns(false) + @provider.expects(:addcmd).returns("useradd") + @provider.expects(:run) + @provider.create + end + + it "should use transition(normal) when the user is a role" do + @provider.stubs(:exists?).returns(true) + @provider.stubs(:is_role?).returns(true) + @provider.expects(:transition).with("normal") + @provider.expects(:run) + @provider.create + end + end + + describe "when calling 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 "when calling 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 "when allow duplicate is enabled" do + before do + @resource.expects(:allowdupe?).returns true + @provider.expects(:execute).with { |args| args.include?("-o") } + end + + it "should add -o when the user is being created" do + @provider.create + end + + it "should add -o when the uid is being modified" do + @provider.uid = 150 + end + end + + describe "when getting roles" do + it "should get the user_attributes" do + @provider.expects(:user_attributes) + @provider.roles + end + + it "should get the :roles attribute" do + attributes = mock("attributes") + attributes.expects(:[]).with(:roles) + @provider.stubs(:user_attributes).returns(attributes) + @provider.roles + end + end +end