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
  # but puppet resource doesn't make the facts available and
  # that interface is heavily used with this provider.
  if Puppet::FileSystem.exist?("/usr/bin/sw_vers")
    product_version = sw_vers "-productVersion"

    confine :true => unless /^10\.[0-4]/.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)
      raise Puppet::Error.new("Cannot parse: #{AuthDB}") if not auth_plist
      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?
    !!self.class.parsed_auth_db.has_key?(resource[:name])
  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}", 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, :failonfail => false, :combine => false)
    current_values = Plist::parse_xml(output)
    current_values ||= {}
    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 = {}
    current_values = authdb_rules[resource[:name]] if authdb_rules[resource[:name]]
    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
      execute(cmds, :failonfail => false, :combine => false, :stdinfile => tmp.path.to_s)
    rescue Errno::EACCES => e
      raise Puppet::Error.new("Cannot save right to #{tmp.path}: #{e}", 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. The exception is booleans, where we coerce to a proper bool
    # if they come in as a symbol.
    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.
      case value
      when true, :true
        value = true
      when false, :false
        value = false
      end
      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)
    # We set boolean values to symbols when retrieving values
    raise Puppet::Error.new("Cannot find #{resource_name} in auth db") if not self.class.parsed_auth_db.has_key?(resource_name)

    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
        value = :true
      when 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
