require 'facter' require 'facter/util/plist' require 'puppet' require 'tempfile' Puppet::Type.type(:macauthorization).provide :macauthorization, :parent => Puppet::Provider do 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 # This should be confined based on macosx_productversion once # http://projects.reductivelabs.com/issues/show/1796 # is resolved. if FileTest.exists?("/usr/bin/sw_vers") product_version = sw_vers "-productVersion" confine :true => if /^10.5/.match(product_version) or /^10.6/.match(product_version) true end end defaultfor :operatingsystem => :darwin 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", } class << self attr_accessor :parsed_auth_db attr_accessor :rights attr_accessor :rules attr_accessor :comments # Not implemented yet. def prefetch(resources) self.populate_rules_rights end def instances if self.parsed_auth_db == {} self.prefetch(nil) end self.parsed_auth_db.collect do |k,v| new(:name => k) end end 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 end # standard required provider instance methods def initialize(resource) 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 rather than repeating code. new_values = {} 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.new("Must specify auth_type when destroying.") end end def exists? if self.class.parsed_auth_db.has_key?(resource[:name]) return true else return false end end def flush # 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("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 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. new_values = current_values.merge(specified_values) set_right(resource[:name], new_values) end def flush_rule 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 Plist::Emit.save_plist(values, tmp.path) 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 values = convert_plist_to_native_attributes(values) authdb = Plist::parse_xml(AuthDB) authdb["rules"][name] = values begin Plist::Emit.save_plist(authdb, AuthDB) rescue 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. newplist = {} propertylist.each_pair do |key, value| 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("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 "" # so ralsh doesn't display it. end 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. properties = [ :allow_root, :authenticate_user, :auth_class, :comment, :group, :k_of_n, :mechanisms, :rule, :session_owner, :shared, :timeout, :tries ] properties.each do |field| define_method(field.to_s) do retrieve_value(resource[:name], field) end 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("#{resource[:name]} is unknown type.") end else raise Puppet::Error.new("auth_type required for new resources.") end end def auth_type=(value) @property_hash[:auth_type] = value end end