diff --git a/lib/puppet/provider/macauthorization/macauthorization.rb b/lib/puppet/provider/macauthorization/macauthorization.rb index 40051bae5..d29c1d294 100644 --- a/lib/puppet/provider/macauthorization/macauthorization.rb +++ b/lib/puppet/provider/macauthorization/macauthorization.rb @@ -1,334 +1,310 @@ +require 'facter' require 'facter/util/plist' require 'puppet' require 'tempfile' Puppet::Type.type(:macauthorization).provide :macauthorization, :parent => Puppet::Provider do -# Puppet::Type.type(:macauthorization).provide :macauth do - desc "Manage Mac OS X authorization database." + + desc "Manage Mac OS X authorization database rules and rights." commands :security => "/usr/bin/security" + commands :sw_vers => "/usr/bin/sw_vers" confine :operatingsystem => :darwin + + product_version = sw_vers "-productVersion" + + confine :true => if /^10.5/.match(product_version) or /^10.6/.match(product_version) + true + end + defaultfor :operatingsystem => :darwin - AuthorizationDB = "/etc/authorization" + AuthDB = "/etc/authorization" @rights = {} @rules = {} @parsed_auth_db = {} @comment = "" # Not implemented yet. Is there any real need to? # This map exists due to the use of hyphens and reserved words in # the authorization schema. PuppetToNativeAttributeMap = { :allow_root => "allow-root", :authenticate_user => "authenticate-user", :auth_class => "class", :k_of_n => "k-of-n", - } + :session_owner => "session-owner", } mk_resource_methods class << self attr_accessor :parsed_auth_db attr_accessor :rights attr_accessor :rules - attr_accessor :comments # Not implemented yet. Is there any real need to? - end + attr_accessor :comments # Not implemented yet. - def self.prefetch(resources) - self.populate_rules_rights - end + def prefetch(resources) + self.populate_rules_rights + end - def self.instances - self.populate_rules_rights - self.parsed_auth_db.collect do |k,v| - new(:name => k) + def instances + if self.parsed_auth_db == {} + self.prefetch(nil) + end + self.parsed_auth_db.collect do |k,v| + new(:name => k) + end end - end - def self.populate_rules_rights - auth_plist = Plist::parse_xml(AuthorizationDB) - if not auth_plist - raise Puppet::Error.new("Unable to parse authorization db at #{AuthorizationDB}") + def populate_rules_rights + auth_plist = Plist::parse_xml(AuthDB) + if not auth_plist + raise Puppet::Error.new("Cannot parse: #{AuthDB}") + end + self.rights = auth_plist["rights"].dup + self.rules = auth_plist["rules"].dup + self.parsed_auth_db = self.rights.dup + self.parsed_auth_db.merge!(self.rules.dup) end - self.rights = auth_plist["rights"].dup - self.rules = auth_plist["rules"].dup - self.parsed_auth_db = self.rights.dup - self.parsed_auth_db.merge!(self.rules.dup) + end + # standard required provider instance methods + def initialize(resource) - if self.class.parsed_auth_db.nil? - self.class.prefetch + if self.class.parsed_auth_db == {} + self.class.prefetch(resource) end super end def create - # we just fill the @property_hash in here and let the flush method deal with it + # we just fill the @property_hash in here and let the flush method + # deal with it rather than repeating code. new_values = {} - Puppet::Type.type(resource.class.name).validproperties.each do |property| - next if property == :ensure - if value = resource.should(property) and value != "" - new_values[property] = value + validprops = Puppet::Type.type(resource.class.name).validproperties + validprops.each do |prop| + next if prop == :ensure + if value = resource.should(prop) and value != "" + new_values[prop] = value end end @property_hash = new_values.dup end def destroy # We explicitly delete here rather than in the flush method. case resource[:auth_type] when :right destroy_right when :rule destroy_rule else - raise Puppet::Error("You must specify the auth_type when removing macauthorization resources.") - end - end - - def destroy_right - security :authorizationdb, :remove, resource[:name] - end - - def destroy_rule - authdb = Plist::parse_xml(AuthorizationDB) - authdb_rules = authdb["rules"].dup - if authdb_rules[resource[:name]] - authdb["rules"].delete(resource[:name]) - Plist::Emit.save_plist(authdb, AuthorizationDB) + raise Puppet::Error.new("Must specify auth_type when destroying.") end end def exists? if self.class.parsed_auth_db.has_key?(resource[:name]) - # return :present return true else return false end end def flush - if resource[:ensure] != :absent # deletion happens in the destroy methods + # deletion happens in the destroy methods + if resource[:ensure] != :absent case resource[:auth_type] when :right flush_right when :rule flush_rule else - raise Puppet::Error.new("flushing something that isn't a right or a rule.") + raise Puppet::Error.new("flush requested for unknown type.") end @property_hash.clear end end + + # utility methods below + + def destroy_right + security "authorizationdb", :remove, resource[:name] + end + + def destroy_rule + authdb = Plist::parse_xml(AuthDB) + authdb_rules = authdb["rules"].dup + if authdb_rules[resource[:name]] + begin + authdb["rules"].delete(resource[:name]) + Plist::Emit.save_plist(authdb, AuthDB) + rescue Errno::EACCES => e + raise Puppet::Error.new("Error saving #{AuthDB}: #{e}") + end + end + end + def flush_right - # first we re-read the right just to make sure we're in sync for values - # that weren't specified in the manifest. As we're supplying the whole - # plist when specifying the right it seems safest to be paranoid. - cmds = [] << :security << "authorizationdb" << "read" << resource[:name] + # first we re-read the right just to make sure we're in sync for + # values that weren't specified in the manifest. As we're supplying + # the whole plist when specifying the right it seems safest to be + # paranoid given the low cost of quering the db once more. + cmds = [] + cmds << :security << "authorizationdb" << "read" << resource[:name] output = execute(cmds, :combine => false) current_values = Plist::parse_xml(output) if current_values.nil? current_values = {} end specified_values = convert_plist_to_native_attributes(@property_hash) - # take the current values, merge the specified values to obtain a complete - # description of the new values. + # take the current values, merge the specified values to obtain a + # complete description of the new values. new_values = current_values.merge(specified_values) set_right(resource[:name], new_values) end def flush_rule - authdb = Plist::parse_xml(AuthorizationDB) + authdb = Plist::parse_xml(AuthDB) authdb_rules = authdb["rules"].dup current_values = {} if authdb_rules[resource[:name]] current_values = authdb_rules[resource[:name]] end specified_values = convert_plist_to_native_attributes(@property_hash) new_values = current_values.merge(specified_values) set_rule(resource[:name], new_values) end def set_right(name, values) # Both creates and modifies rights as it simply overwrites them. # The security binary only allows for writes using stdin, so we # dump the values to a tempfile. values = convert_plist_to_native_attributes(values) tmp = Tempfile.new('puppet_macauthorization') begin - # tmp.flush Plist::Emit.save_plist(values, tmp.path) - # tmp.flush - cmds = [] << :security << "authorizationdb" << "write" << name - output = execute(cmds, :combine => false, :stdinfile => tmp.path.to_s) + cmds = [] + cmds << :security << "authorizationdb" << "write" << name + output = execute(cmds, :combine => false, + :stdinfile => tmp.path.to_s) + rescue Errno::EACCES => e + raise Puppet::Error.new("Cannot save right to #{tmp.path}: #{e}") ensure tmp.close tmp.unlink end end def set_rule(name, values) - # Both creates and modifies rules as it overwrites the entry in the rules - # dictionary. - # Unfortunately the security binary doesn't support modifying rules at all - # so we have to twiddle the whole plist... :( See Apple Bug #6386000 + # Both creates and modifies rules as it overwrites the entry in the + # rules dictionary. Unfortunately the security binary doesn't + # support modifying rules at all so we have to twiddle the whole + # plist... :( See Apple Bug #6386000 values = convert_plist_to_native_attributes(values) - authdb = Plist::parse_xml(AuthorizationDB) + authdb = Plist::parse_xml(AuthDB) authdb["rules"][name] = values begin - Plist::Emit.save_plist(authdb, AuthorizationDB) + Plist::Emit.save_plist(authdb, AuthDB) rescue - raise Puppet::Error.new("Couldn't write to authorization db at #{AuthorizationDB}") + raise Puppet::Error.new("Error writing to: #{AuthDB}") end end def convert_plist_to_native_attributes(propertylist) - # This mainly converts the keys from the puppet attributes to the 'native' - # ones, but also enforces that the keys are all Strings rather than Symbols - # so that any merges of the resultant Hash are sane. + # This mainly converts the keys from the puppet attributes to the + # 'native' ones, but also enforces that the keys are all Strings + # rather than Symbols so that any merges of the resultant Hash are + # sane. newplist = {} propertylist.each_pair do |key, value| - next if key == :ensure - next if key == :auth_type + next if key == :ensure # not part of the auth db schema. + next if key == :auth_type # not part of the auth db schema. new_key = key if PuppetToNativeAttributeMap.has_key?(key) new_key = PuppetToNativeAttributeMap[key].to_s elsif not key.is_a?(String) new_key = key.to_s end newplist[new_key] = value end newplist end def retrieve_value(resource_name, attribute) if not self.class.parsed_auth_db.has_key?(resource_name) - raise Puppet::Error.new("Unable to find resource #{resource_name} in authorization db.") + raise Puppet::Error.new("Cannot find #{resource_name} in auth db") end if PuppetToNativeAttributeMap.has_key?(attribute) native_attribute = PuppetToNativeAttributeMap[attribute] else native_attribute = attribute.to_s end if self.class.parsed_auth_db[resource_name].has_key?(native_attribute) value = self.class.parsed_auth_db[resource_name][native_attribute] case value when true, "true", :true value = :true when false, "false", :false value = :false end @property_hash[attribute] = value return value else @property_hash.delete(attribute) - return "" + return "" # so ralsh doesn't display it. end end - def allow_root - retrieve_value(resource[:name], :allow_root) - end - def allow_root=(value) - @property_hash[:allow_root] = value - end - - def authenticate_user - retrieve_value(resource[:name], :authenticate_user) - end - - def authenticate_user= (dosync) - @property_hash[:authenticate_user] = value - end - - def auth_class - retrieve_value(resource[:name], :auth_class) - end - - def auth_class=(value) - @property_hash[:auth_class] = value - end - - def comment - retrieve_value(resource[:name], :comment) - end - - def comment=(value) - @property_hash[:comment] = value - end - - def group - retrieve_value(resource[:name], :group) - end + # property methods below + # + # We define them all dynamically apart from auth_type which is a special + # case due to not being in the actual authorization db schema. - def group=(value) - @property_hash[:group] = value - end - - def k_of_n - retrieve_value(resource[:name], :k_of_n) - end - - def k_of_n=(value) - @property_hash[:k_of_n] = value - end - - def mechanisms - retrieve_value(resource[:name], :mechanisms) - end + properties = [ :allow_root, :authenticate_user, :auth_class, :comment, + :group, :k_of_n, :mechanisms, :rule, :session_owner, + :shared, :timeout, :tries ] - def mechanisms=(value) - @property_hash[:mechanisms] = value - end - - def rule - retrieve_value(resource[:name], :rule) - end + properties.each do |field| + define_method(field.to_s) do + retrieve_value(resource[:name], field) + end - def rule=(value) - @property_hash[:rule] = value - end - - def shared - retrieve_value(resource[:name], :shared) - end - - def shared=(value) - @property_hash[:shared] = value + define_method(field.to_s + "=") do |value| + @property_hash[field] = value + end end def auth_type if resource.should(:auth_type) != nil return resource.should(:auth_type) elsif self.exists? # this is here just for ralsh, so it can work out what type it is. if self.class.rights.has_key?(resource[:name]) return :right elsif self.class.rules.has_key?(resource[:name]) return :rule else - raise Puppet::Error.new("Unable to determine if macauthorization type: #{resource[:name]} is a right or a rule.") + raise Puppet::Error.new("#{resource[:name]} is unknown type.") end else - raise Puppet::Error.new("You must specify the auth_type for new macauthorization resources.") + raise Puppet::Error.new("auth_type required for new resources.") end end def auth_type=(value) @property_hash[:auth_type] = value end end \ No newline at end of file diff --git a/lib/puppet/type/macauthorization.rb b/lib/puppet/type/macauthorization.rb index 0fae8aba0..46e02ddae 100644 --- a/lib/puppet/type/macauthorization.rb +++ b/lib/puppet/type/macauthorization.rb @@ -1,95 +1,142 @@ -require 'ruby-debug' - Puppet::Type.newtype(:macauthorization) do - @doc = "Manage authorization databases" + @doc = "Manage the Mac OS X authorization database. + + See: http://developer.apple.com/documentation/Security/Conceptual/Security_Overview/Security_Services/chapter_4_section_5.html + for more information." ensurable autorequire(:file) do ["/etc/authorization"] end - # This probably shouldn't be necessary for properties that have declared - # themselves to be booleans already. def munge_boolean(value) case value when true, "true", :true: :true when false, "false", :false :false else raise Puppet::Error("munge_boolean only takes booleans") end end newparam(:name) do - desc "The name of the right or rule to be managed." + desc "The name of the right or rule to be managed. + Corresponds to 'key' in Authorization Services. The key is the name + of a rule. A key uses the same naming conventions as a right. The + Security Server uses a rule’s key to match the rule with a right. + Wildcard keys end with a ‘.’. The generic rule has an empty key value. + Any rights that do not match a specific rule use the generic rule." + isnamevar end newproperty(:auth_type) do - desc "type - can be a right a rule or a comment" + desc "type - can be a 'right' or a 'rule'. 'comment' has not yet been + implemented." + newvalue(:right) newvalue(:rule) - newvalue(:comment) + # newvalue(:comment) # not yet implemented. end newproperty(:allow_root, :boolean => true) do - desc "Corresponds to 'allow-root' in the authorization store. hyphens not allowed..." + desc "Corresponds to 'allow-root' in the authorization store, renamed + due to hyphens being problematic. Specifies whether a right should be + allowed automatically if the requesting process is running with + uid == 0. AuthorizationServices defaults this attribute to false if + not specified" + newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:authenticate_user, :boolean => true) do - desc "authenticate-user" + desc "Corresponds to 'authenticate-user' in the authorization store, + renamed due to hyphens being problematic." + newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:auth_class) do - desc "Corresponds to 'class' in the authorization store. class is - a reserved word in Puppet syntax, so we use 'authclass'." - # newvalue(:user) - # newvalue(:'evaluate-mechanisms') + desc "Corresponds to 'class' in the authorization store, renamed due + to 'class' being a reserved word." + + newvalue(:user) + newvalue(:'evaluate-mechanisms') end newproperty(:comment) do - desc "Comment. simple enough eh?" + desc "The 'comment' attribute for authorization resources." end newproperty(:group) do - desc "group" + desc "The user must authenticate as a member of this group. This + attribute can be set to any one group." end newproperty(:k_of_n) do - desc "k-of-n. odd." + desc "k-of-n. Built-in rights only show a value of '1' or absent, + other values may be acceptable. Undocumented." end newproperty(:mechanisms, :array_matching => :all) do - desc "mechanisms" + desc "an array of suitable mechanisms." end newproperty(:rule, :array_match => :all) do - desc "rule" - end + desc "The rule(s) that this right refers to." + end + + newproperty(:session_owner, :boolean => true) do + desc "Corresponds to 'session-owner' in the authorization store, + renamed due to hyphens being problematic. Whether the session owner + automatically matches this rule or right." + + newvalue(:true) + newvalue(:false) + + munge do |value| + @resource.munge_boolean(value) + end + end newproperty(:shared, :boolean => true) do - desc "shared" + desc "If this is set to true, then the Security Server marks the + credentials used to gain this right as shared. The Security Server + may use any shared credentials to authorize this right. For maximum + security, set sharing to false so credentials stored by the Security + Server for one application may not be used by another application." + newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end + newproperty(:timeout) do + desc "The credential used by this rule expires in the specified + number of seconds. For maximum security where the user must + authenticate every time, set the timeout to 0. For minimum security, + remove the timeout attribute so the user authenticates only once per + session." + end + + newproperty(:tries) do + desc "The number of tries allowed." + end + end diff --git a/spec/unit/provider/macauthorization.rb b/spec/unit/provider/macauthorization.rb new file mode 100644 index 000000000..4754b11c7 --- /dev/null +++ b/spec/unit/provider/macauthorization.rb @@ -0,0 +1,153 @@ +#!/usr/bin/env ruby +# +# Unit testing for the macauthorization provider +# + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet' +require 'facter/util/plist' + +provider_class = Puppet::Type.type(:macauthorization).provider(:macauthorization) + +describe provider_class do + + before :each do + # Create a mock resource + @resource = stub 'resource' + + @provider = provider_class.new(@resource) + + @authname = "foo.spam.eggs.puppettest" + @authplist = {} + + @rules = {@authname => @authplist} + @authdb = {} + @authdb["rules"] = @rules + + # A catch all; no parameters set + @resource.stubs(:[]).returns(nil) + + # But set name, ensure + @resource.stubs(:[]).with(:name).returns @authname + + @resource.stubs(:[]).with(:ensure).returns :present + + @resource.stubs(:ref).returns "MacAuthorization[#{@authname}]" + + # stub out the provider methods that actually touch the filesystem + # or execute commands + @provider.stubs(:populate_rules_rights).returns("") + + # Stub out Plist::parse_xml + Plist.stubs("parse_xml").returns(@authdb) + end + + it "should have a create method" do + @provider.should respond_to(:create) + end + + it "should have a destroy method" do + @provider.should respond_to(:destroy) + end + + it "should have an exists? method" do + @provider.should respond_to(:exists?) + end + + it "should have a flush method" do + @provider.should respond_to(:flush) + end + + properties = [ :allow_root, :authenticate_user, :auth_class, :comment, + :group, :k_of_n, :mechanisms, :rule, :session_owner, + :shared, :timeout, :tries, :auth_type ] + + properties.each do |prop| + it "should have a #{prop.to_s} method" do + @provider.should respond_to(prop.to_s) + end + + it "should have a #{prop.to_s}= method" do + @provider.should respond_to(prop.to_s + "=") + end + end + + describe "when destroying a right" do + before :each do + @resource.stubs(:[]).with(:auth_type).returns(:right) + end + + it "should call the internal method destroy_right" do + @provider.expects("destroy_right") + @provider.destroy + end + it "should call the external command 'security authorizationdb remove @authname" do + @provider.expects(:security).with("authorizationdb", :remove, @authname) + @provider.destroy + end + end + + describe "when destroying a rule" do + before :each do + @resource.stubs(:[]).with(:auth_type).returns(:rule) + end + + it "should call the internal method destroy_rule" do + @provider.expects("destroy_rule") + @provider.destroy + end + end + + describe "when flushing a right" do + before :each do + @resource.stubs(:[]).with(:auth_type).returns(:right) + end + + it "should call the internal method flush_right" do + @provider.expects("flush_right") + @provider.flush + end + + it "should call the internal method set_right" do + @provider.expects("set_right") + @provider.flush + end + + it "should read and write to the auth database with the right arguments" do + @provider.expects(:execute).with() { |cmds, args| + cmds.include?("read") and + cmds.include?(@authname) and + args[:combine] == false + }.once + + @provider.expects(:execute).with() { |cmds, args| + cmds.include?("write") and + cmds.include?(@authname) and + args[:combine] == false and + args[:stdinfile] != nil + }.once + @provider.flush + end + + end + + describe "when flushing a rule" do + before :each do + @resource.stubs(:[]).with(:auth_type).returns(:rule) + end + + it "should call the internal method flush_rule" do + @provider.expects("flush_rule") + @provider.flush + end + + it "should call the internal method set_rule" do + @provider.expects("set_rule") + @provider.flush + end + end + + + +end \ No newline at end of file diff --git a/spec/unit/type/macauthorization.rb b/spec/unit/type/macauthorization.rb new file mode 100644 index 000000000..a27841c82 --- /dev/null +++ b/spec/unit/type/macauthorization.rb @@ -0,0 +1,80 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +macauth_type = Puppet::Type.type(:macauthorization) + + +describe macauth_type, "when validating attributes" do + + parameters = [:name,] + properties = [:auth_type, :allow_root, :authenticate_user, :auth_class, + :comment, :group, :k_of_n, :mechanisms, :rule, + :session_owner, :shared, :timeout, :tries] + + parameters.each do |parameter| + it "should have a %s parameter" % parameter do + macauth_type.attrclass(parameter).ancestors.should be_include(Puppet::Parameter) + end + + it "should have documentation for its %s parameter" % parameter do + macauth_type.attrclass(parameter).doc.should be_instance_of(String) + end + end + + properties.each do |property| + it "should have a %s property" % property do + macauth_type.attrclass(property).ancestors.should be_include(Puppet::Property) + end + + it "should have documentation for its %s property" % property do + macauth_type.attrclass(property).doc.should be_instance_of(String) + end + end + +end + +describe macauth_type, "when validating properties" do + + before do + @provider = stub 'provider' + @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil + end + + after do + macauth_type.clear + end + + it "should have a default provider inheriting from Puppet::Provider" do + macauth_type.defaultprovider.ancestors.should be_include(Puppet::Provider) + end + + it "should be able to create a instance" do + macauth_type.create(:name => "foo").should_not be_nil + end + + it "should be able to create an instance" do + lambda { + macauth_type.create(:name => 'foo') + }.should_not raise_error + end + + it "should support :present as a value to :ensure" do + lambda { + macauth_type.create(:name => "foo", :ensure => :present) + }.should_not raise_error + end + + it "should support :absent as a value to :ensure" do + lambda { + macauth_type.create(:name => "foo", :ensure => :absent) + }.should_not raise_error + end + +end + +describe "instances" do + it "should have a valid provider" do + macauth_type.create(:name => "foo").provider.class.ancestors.should be_include(Puppet::Provider) + end +end \ No newline at end of file