summaryrefslogtreecommitdiffstats
path: root/lib/puppet/provider/macauthorization/macauthorization.rb
blob: fdf9fd18c8fa22f5d7289c35c834a6f61bc66b35 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
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)
      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}")
      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)
    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

        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. 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