puppet 3.0.2.rc1 → 3.0.2.rc2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puppet might be problematic. Click here for more details.

@@ -51,13 +51,26 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do
51
51
  has_feature :enableable
52
52
  mk_resource_methods
53
53
 
54
- Launchd_Paths = [ "/Library/LaunchAgents",
55
- "/Library/LaunchDaemons",
56
- "/System/Library/LaunchAgents",
57
- "/System/Library/LaunchDaemons"]
54
+ # These are the paths in OS X where a launchd service plist could
55
+ # exist. This is a helper method, versus a constant, for easy testing
56
+ # and mocking
57
+ def self.launchd_paths
58
+ [
59
+ "/Library/LaunchAgents",
60
+ "/Library/LaunchDaemons",
61
+ "/System/Library/LaunchAgents",
62
+ "/System/Library/LaunchDaemons"
63
+ ]
64
+ end
65
+ private_class_method :launchd_paths
66
+
67
+ # This is the path to the overrides plist file where service enabling
68
+ # behavior is defined in 10.6 and greater
69
+ def self.launchd_overrides
70
+ "/var/db/launchd.db/com.apple.launchd/overrides.plist"
71
+ end
72
+ private_class_method :launchd_overrides
58
73
 
59
- Launchd_Overrides = "/var/db/launchd.db/com.apple.launchd/overrides.plist"
60
-
61
74
  # Caching is enabled through the following three methods. Self.prefetch will
62
75
  # call self.instances to create an instance for each service. Self.flush will
63
76
  # clear out our cache when we're done.
@@ -81,6 +94,22 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do
81
94
  end
82
95
  end
83
96
 
97
+ # This method will return a list of files in the passed directory. This method
98
+ # does not go recursively down the tree and does not return directories
99
+ #
100
+ # @param path [String] The directory to glob
101
+ #
102
+ # @api private
103
+ #
104
+ # @return [Array] of String instances modeling file paths
105
+ def self.return_globbed_list_of_file_paths(path)
106
+ array_of_files = Dir.glob(File.join(path, '*')).collect do |filepath|
107
+ File.file?(filepath) ? filepath : nil
108
+ end
109
+ array_of_files.compact
110
+ end
111
+ private_class_method :return_globbed_list_of_file_paths
112
+
84
113
  # Sets a class instance variable with a hash of all launchd plist files that
85
114
  # are found on the system. The key of the hash is the job id and the value
86
115
  # is the path to the file. If a label is passed, we return the job id and
@@ -88,14 +117,20 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do
88
117
  def self.jobsearch(label=nil)
89
118
  @label_to_path_map ||= {}
90
119
  if @label_to_path_map.empty?
91
- Launchd_Paths.each do |path|
92
- Dir.glob(File.join(path,'*')).each do |filepath|
93
- next if ! File.file?(filepath)
120
+ launchd_paths.each do |path|
121
+ return_globbed_list_of_file_paths(path).each do |filepath|
94
122
  job = read_plist(filepath)
95
- if job.has_key?("Label") and job["Label"] == label
96
- return { label => filepath }
123
+ next if job.nil?
124
+ if job.has_key?("Label")
125
+ if job["Label"] == label
126
+ return { label => filepath }
127
+ else
128
+ @label_to_path_map[job["Label"]] = filepath
129
+ end
97
130
  else
98
- @label_to_path_map[job["Label"]] = filepath
131
+ Puppet.warning("The #{filepath} plist does not contain a 'label' key; " +
132
+ "Puppet is skipping it")
133
+ next
99
134
  end
100
135
  end
101
136
  end
@@ -143,7 +178,13 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do
143
178
  # Read a plist, whether its format is XML or in Apple's "binary1"
144
179
  # format.
145
180
  def self.read_plist(path)
146
- Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', path))
181
+ begin
182
+ Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', path))
183
+ rescue Puppet::ExecutionFailure => detail
184
+ Puppet.warning("Cannot read file #{path}; Puppet is skipping it. \n" +
185
+ "Details: #{detail}")
186
+ return nil
187
+ end
147
188
  end
148
189
 
149
190
  # Clean out the @property_hash variable containing the cached list of services
@@ -245,7 +286,7 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do
245
286
  job_plist_disabled = job_plist["Disabled"] if job_plist.has_key?("Disabled")
246
287
 
247
288
  if has_macosx_plist_overrides?
248
- if FileTest.file?(Launchd_Overrides) and overrides = self.class.read_plist(Launchd_Overrides)
289
+ if FileTest.file?(self.class.launchd_overrides) and overrides = self.class.read_plist(self.class.launchd_overrides)
249
290
  if overrides.has_key?(resource[:name])
250
291
  overrides_disabled = overrides[resource[:name]]["Disabled"] if overrides[resource[:name]].has_key?("Disabled")
251
292
  end
@@ -270,9 +311,9 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do
270
311
  # overrides plist, in earlier versions this is stored in the job plist itself.
271
312
  def enable
272
313
  if has_macosx_plist_overrides?
273
- overrides = self.class.read_plist(Launchd_Overrides)
314
+ overrides = self.class.read_plist(self.class.launchd_overrides)
274
315
  overrides[resource[:name]] = { "Disabled" => false }
275
- Plist::Emit.save_plist(overrides, Launchd_Overrides)
316
+ Plist::Emit.save_plist(overrides, self.class.launchd_overrides)
276
317
  else
277
318
  job_path, job_plist = plist_from_label(resource[:name])
278
319
  if self.enabled? == :false
@@ -285,9 +326,9 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do
285
326
 
286
327
  def disable
287
328
  if has_macosx_plist_overrides?
288
- overrides = self.class.read_plist(Launchd_Overrides)
329
+ overrides = self.class.read_plist(self.class.launchd_overrides)
289
330
  overrides[resource[:name]] = { "Disabled" => true }
290
- Plist::Emit.save_plist(overrides, Launchd_Overrides)
331
+ Plist::Emit.save_plist(overrides, self.class.launchd_overrides)
291
332
  else
292
333
  job_path, job_plist = plist_from_label(resource[:name])
293
334
  job_plist["Disabled"] = true
@@ -34,10 +34,10 @@ Puppet::Type.type(:user).provide :directoryservice do
34
34
  ## Class Methods ##
35
35
  ## ##
36
36
 
37
+ # This method exists to map the dscl values to the correct Puppet
38
+ # properties. This stays relatively consistent, but who knows what
39
+ # Apple will do next year...
37
40
  def self.ds_to_ns_attribute_map
38
- # This method exists to map the dscl values to the correct Puppet
39
- # properties. This stays relatively consistent, but who knows what
40
- # Apple will do next year...
41
41
  {
42
42
  'RecordName' => :name,
43
43
  'PrimaryGroupID' => :gid,
@@ -57,16 +57,16 @@ Puppet::Type.type(:user).provide :directoryservice do
57
57
  @ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert
58
58
  end
59
59
 
60
+ # Prefetching is necessary to use @property_hash inside any setter methods.
61
+ # self.prefetch uses self.instances to gather an array of user instances
62
+ # on the system, and then populates the @property_hash instance variable
63
+ # with attribute data for the specific instance in question (i.e. it
64
+ # gathers the 'is' values of the resource into the @property_hash instance
65
+ # variable so you don't have to read from the system every time you need
66
+ # to gather the 'is' values for a resource. The downside here is that
67
+ # populating this instance variable for every resource on the system
68
+ # takes time and front-loads your Puppet run.
60
69
  def self.prefetch(resources)
61
- # Prefetching is necessary to use @property_hash inside any setter methods.
62
- # self.prefetch uses self.instances to gather an array of user instances
63
- # on the system, and then populates the @property_hash instance variable
64
- # with attribute data for the specific instance in question (i.e. it
65
- # gathers the 'is' values of the resource into the @property_hash instance
66
- # variable so you don't have to read from the system every time you need
67
- # to gather the 'is' values for a resource. The downside here is that
68
- # populating this instance variable for every resource on the system
69
- # takes time and front-loads your Puppet run.
70
70
  instances.each do |prov|
71
71
  if resource = resources[prov.name]
72
72
  resource.provider = prov
@@ -74,28 +74,28 @@ Puppet::Type.type(:user).provide :directoryservice do
74
74
  end
75
75
  end
76
76
 
77
+ # This method assembles an array of provider instances containing
78
+ # information about every instance of the user type on the system (i.e.
79
+ # every user and its attributes). The `puppet resource` command relies
80
+ # on self.instances to gather an array of user instances in order to
81
+ # display its output.
77
82
  def self.instances
78
- # This method assembles an array of provider instances containing
79
- # information about every instance of the user type on the system (i.e.
80
- # every user and its attributes). The `puppet resource` command relies
81
- # on self.instances to gather an array of user instances in order to
82
- # display its output.
83
83
  get_all_users.collect do |user|
84
84
  self.new(generate_attribute_hash(user))
85
85
  end
86
86
  end
87
87
 
88
+ # Return an array of hashes containing information about every user on
89
+ # the system.
88
90
  def self.get_all_users
89
- # Return an array of hashes containing information about every user on
90
- # the system.
91
91
  Plist.parse_xml(dscl '-plist', '.', 'readall', '/Users')
92
92
  end
93
93
 
94
+ # This method accepts an individual user plist, passed as a hash, and
95
+ # strips the dsAttrTypeStandard: prefix that dscl adds for each key.
96
+ # An attribute hash is assembled and returned from the properties
97
+ # supported by the user type.
94
98
  def self.generate_attribute_hash(input_hash)
95
- # This method accepts an individual user plist, passed as a hash, and
96
- # strips the dsAttrTypeStandard: prefix that dscl adds for each key.
97
- # An attribute hash is assembled and returned from the properties
98
- # supported by the user type.
99
99
  attribute_hash = {}
100
100
  input_hash.keys.each do |key|
101
101
  ds_attribute = key.sub("dsAttrTypeStandard:", "")
@@ -138,7 +138,7 @@ Puppet::Type.type(:user).provide :directoryservice do
138
138
  ################################
139
139
  # Get Password/Salt/Iterations #
140
140
  ################################
141
- if (Puppet::Util::Package.versioncmp(Facter.value(:macosx_productversion_major), '10.7') == -1)
141
+ if (Puppet::Util::Package.versioncmp(get_os_version, '10.7') == -1)
142
142
  attribute_hash[:password] = get_sha1(attribute_hash[:guid])
143
143
  else
144
144
  if attribute_hash[:shadowhashdata].empty?
@@ -158,30 +158,34 @@ Puppet::Type.type(:user).provide :directoryservice do
158
158
  attribute_hash
159
159
  end
160
160
 
161
+ def self.get_os_version
162
+ @os_version ||= Facter.value(:macosx_productversion_major)
163
+ end
164
+
165
+ # Use dscl to retrieve an array of hashes containing attributes about all
166
+ # of the local groups on the machine.
161
167
  def self.get_list_of_groups
162
- # Use dscl to retrieve an array of hashes containing attributes about all
163
- # of the local groups on the machine.
164
168
  @groups ||= Plist.parse_xml(dscl '-plist', '.', 'readall', '/Groups')
165
169
  end
166
170
 
171
+ # Perform a dscl lookup at the path specified for the specific keyname
172
+ # value. The value returned is the first item within the array returned
173
+ # from dscl
167
174
  def self.get_attribute_from_dscl(path, username, keyname)
168
- # Perform a dscl lookup at the path specified for the specific keyname
169
- # value. The value returned is the first item within the array returned
170
- # from dscl
171
175
  Plist.parse_xml(dscl '-plist', '.', 'read', "/#{path}/#{username}", keyname)
172
176
  end
173
177
 
178
+ # The plist embedded in the ShadowHashData key is a binary plist. The
179
+ # facter/util/plist library doesn't read binary plists, so we need to
180
+ # extract the binary plist, convert it to XML, and return it.
174
181
  def self.get_embedded_binary_plist(shadow_hash_data)
175
- # The plist embedded in the ShadowHashData key is a binary plist. The
176
- # facter/util/plist library doesn't read binary plists, so we need to
177
- # extract the binary plist, convert it to XML, and return it.
178
182
  embedded_binary_plist = Array(shadow_hash_data['dsAttrTypeNative:ShadowHashData'][0].delete(' ')).pack('H*')
179
183
  convert_binary_to_xml(embedded_binary_plist)
180
184
  end
181
185
 
186
+ # This method will accept a hash that has been returned from Plist::parse_xml
187
+ # and convert it to a binary plist (string value).
182
188
  def self.convert_xml_to_binary(plist_data)
183
- # This method will accept a hash that has been returned from Plist::parse_xml
184
- # and convert it to a binary plist (string value).
185
189
  Puppet.debug('Converting XML plist to binary')
186
190
  Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'')
187
191
  IO.popen('plutil -convert binary1 -o - -', mode='r+') do |io|
@@ -192,9 +196,9 @@ Puppet::Type.type(:user).provide :directoryservice do
192
196
  @converted_plist
193
197
  end
194
198
 
199
+ # This method will accept a binary plist (as a string) and convert it to a
200
+ # hash via Plist::parse_xml.
195
201
  def self.convert_binary_to_xml(plist_data)
196
- # This method will accept a binary plist (as a string) and convert it to a
197
- # hash via Plist::parse_xml.
198
202
  Puppet.debug('Converting binary plist to XML')
199
203
  Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'')
200
204
  IO.popen('plutil -convert xml1 -o - -', mode='r+') do |io|
@@ -206,17 +210,17 @@ Puppet::Type.type(:user).provide :directoryservice do
206
210
  Plist::parse_xml(@converted_plist)
207
211
  end
208
212
 
213
+ # The salted-SHA512 password hash in 10.7 is stored in the 'SALTED-SHA512'
214
+ # key as binary data. That data is extracted and converted to a hex string.
209
215
  def self.get_salted_sha512(embedded_binary_plist)
210
- # The salted-SHA512 password hash in 10.7 is stored in the 'SALTED-SHA512'
211
- # key as binary data. That data is extracted and converted to a hex string.
212
216
  embedded_binary_plist['SALTED-SHA512'].string.unpack("H*")[0]
213
217
  end
214
218
 
219
+ # This method reads the passed embedded_binary_plist hash and returns values
220
+ # according to which field is passed. Arguments passed are the hash
221
+ # containing the value read from the 'ShadowHashData' key in the User's
222
+ # plist, and the field to be read (one of 'entropy', 'salt', or 'iterations')
215
223
  def self.get_salted_sha512_pbkdf2(field, embedded_binary_plist)
216
- # This method reads the passed embedded_binary_plist hash and returns values
217
- # according to which field is passed. Arguments passed are the hash
218
- # containing the value read from the 'ShadowHashData' key in the User's
219
- # plist, and the field to be read (one of 'entropy', 'salt', or 'iterations')
220
224
  case field
221
225
  when 'salt', 'entropy'
222
226
  embedded_binary_plist['SALTED-SHA512-PBKDF2'][field].string.unpack('H*').first
@@ -229,9 +233,9 @@ Puppet::Type.type(:user).provide :directoryservice do
229
233
  end
230
234
  end
231
235
 
236
+ # In versions 10.5 and 10.6 of OS X, the password hash is stored in a file
237
+ # in the /var/db/shadow/hash directory that matches the GUID of the user.
232
238
  def self.get_sha1(guid)
233
- # In versions 10.5 and 10.6 of OS X, the password hash is stored in a file
234
- # in the /var/db/shadow/hash directory that matches the GUID of the user.
235
239
  password_hash = nil
236
240
  password_hash_file = "#{password_hash_dir}/#{guid}"
237
241
  if File.exists?(password_hash_file) and File.file?(password_hash_file)
@@ -258,10 +262,10 @@ Puppet::Type.type(:user).provide :directoryservice do
258
262
  true
259
263
  end
260
264
 
265
+ # This method is called if ensure => present is passed and the exists?
266
+ # method returns false. Dscl will directly set most values, but the
267
+ # setter methods will be used for any exceptions.
261
268
  def create
262
- # This method is called if ensure => present is passed and the exists?
263
- # method returns false. Dscl will directly set most values, but the
264
- # setter methods will be used for any exceptions.
265
269
  create_new_user(@resource.name)
266
270
 
267
271
  # Retrieve the user's GUID
@@ -323,9 +327,9 @@ Puppet::Type.type(:user).provide :directoryservice do
323
327
  end
324
328
  end
325
329
 
330
+ # This method is called when ensure => absent has been set.
331
+ # Deleting a user is handled by dscl
326
332
  def delete
327
- # This method is called when ensure => absent has been set.
328
- # Deleting a user is handled by dscl
329
333
  dscl '.', '-delete', "/Users/#{@resource.name}"
330
334
  end
331
335
 
@@ -333,9 +337,9 @@ Puppet::Type.type(:user).provide :directoryservice do
333
337
  ## Getter/Setter Methods ##
334
338
  ## ##
335
339
 
340
+ # In the setter method we're only going to take action on groups for which
341
+ # the user is not currently a member.
336
342
  def groups=(value)
337
- # In the setter method we're only going to take action on groups for which
338
- # the user is not currently a member.
339
343
  guid = self.class.get_attribute_from_dscl('Users', @resource.name, 'GeneratedUID')['dsAttrTypeStandard:GeneratedUID'][0]
340
344
  groups_to_add = value.split(',') - groups.split(',')
341
345
  groups_to_add.each do |group|
@@ -344,21 +348,21 @@ Puppet::Type.type(:user).provide :directoryservice do
344
348
  end
345
349
  end
346
350
 
351
+ # If you thought GETTING a password was bad, try SETTING it. This method
352
+ # makes me want to cry. A thousand tears...
353
+ #
354
+ # I've been unsuccessful in tracking down a way to set the password for
355
+ # a user using dscl that DOESN'T require passing it as plaintext. We were
356
+ # also unable to get dsimport to work like this. Due to these downfalls,
357
+ # the sanest method requires opening the user's plist, dropping in the
358
+ # password hash, and serializing it back to disk. The problems with THIS
359
+ # method revolve around dscl. Any time you directly modify a user's plist,
360
+ # you need to flush the cache that dscl maintains.
347
361
  def password=(value)
348
- # If you thought GETTING a password was bad, try SETTING it. This method
349
- # makes me want to cry. A thousand tears...
350
- #
351
- # I've been unsuccessful in tracking down a way to set the password for
352
- # a user using dscl that DOESN'T require passing it as plaintext. We were
353
- # also unable to get dsimport to work like this. Due to these downfalls,
354
- # the sanest method requires opening the user's plist, dropping in the
355
- # password hash, and serializing it back to disk. The problems with THIS
356
- # method revolve around dscl. Any time you directly modify a user's plist,
357
- # you need to flush the cache that dscl maintains.
358
- if (Puppet::Util::Package.versioncmp(Facter.value(:macosx_productversion_major), '10.7') == -1)
362
+ if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') == -1)
359
363
  write_sha1_hash(value)
360
364
  else
361
- if Facter.value(:macosx_productversion_major) == '10.7'
365
+ if self.class.get_os_version == '10.7'
362
366
  if value.length != 136
363
367
  raise Puppet::Error, "OS X 10.7 requires a Salted SHA512 hash password of 136 characters. Please check your password and try again."
364
368
  end
@@ -395,30 +399,30 @@ Puppet::Type.type(:user).provide :directoryservice do
395
399
  end
396
400
  end
397
401
 
402
+ # The iterations and salt properties, like the password property, can only
403
+ # be modified by directly changing the user's plist. Because of this fact,
404
+ # we have to treat the ds cache just like you would in the password=
405
+ # method.
398
406
  def iterations=(value)
399
- # The iterations and salt properties, like the password property, can only
400
- # be modified by directly changing the user's plist. Because of this fact,
401
- # we have to treat the ds cache just like you would in the password=
402
- # method.
403
- if (Puppet::Util::Package.versioncmp(Facter.value(:macosx_productversion_major), '10.7') > 0)
407
+ if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') > 0)
404
408
  sleep 2
405
409
  flush_dscl_cache
406
- users_plist = Plist::parse_xml(plutil '-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{@resource.name}.plist")
410
+ users_plist = get_users_plist(@resource.name)
407
411
  shadow_hash_data = get_shadow_hash_data(users_plist)
408
412
  set_salted_pbkdf2(users_plist, shadow_hash_data, 'iterations', value)
409
413
  flush_dscl_cache
410
414
  end
411
415
  end
412
416
 
417
+ # The iterations and salt properties, like the password property, can only
418
+ # be modified by directly changing the user's plist. Because of this fact,
419
+ # we have to treat the ds cache just like you would in the password=
420
+ # method.
413
421
  def salt=(value)
414
- # The iterations and salt properties, like the password property, can only
415
- # be modified by directly changing the user's plist. Because of this fact,
416
- # we have to treat the ds cache just like you would in the password=
417
- # method.
418
- if (Puppet::Util::Package.versioncmp(Facter.value(:macosx_productversion_major), '10.7') > 0)
422
+ if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') > 0)
419
423
  sleep 2
420
424
  flush_dscl_cache
421
- users_plist = Plist::parse_xml(plutil '-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{@resource.name}.plist")
425
+ users_plist = get_users_plist(@resource.name)
422
426
  shadow_hash_data = get_shadow_hash_data(users_plist)
423
427
  set_salted_pbkdf2(users_plist, shadow_hash_data, 'salt', value)
424
428
  flush_dscl_cache
@@ -477,8 +481,8 @@ Puppet::Type.type(:user).provide :directoryservice do
477
481
  '/var/db/shadow/hash'
478
482
  end
479
483
 
484
+ # This method will merge in a given value using dscl
480
485
  def merge_attribute_with_dscl(path, username, keyname, value)
481
- # This method will merge in a given value using dscl
482
486
  begin
483
487
  dscl '.', '-merge', "/#{path}/#{username}", keyname, value
484
488
  rescue Puppet::ExecutionFailure => detail
@@ -486,14 +490,14 @@ Puppet::Type.type(:user).provide :directoryservice do
486
490
  end
487
491
  end
488
492
 
493
+ # Create the new user with dscl
489
494
  def create_new_user(username)
490
- # Create the new user with dscl
491
495
  dscl '.', '-create', "/Users/#{username}"
492
496
  end
493
497
 
498
+ # Get the next available uid on the system by getting a list of user ids,
499
+ # sorting them, grabbing the last one, and adding a 1. Scientific stuff here.
494
500
  def next_system_id(min_id=20)
495
- # Get the next available uid on the system by getting a list of user ids,
496
- # sorting them, grabbing the last one, and adding a 1. Scientific stuff here.
497
501
  dscl_output = dscl '.', '-list', '/Users', 'uid'
498
502
  # We're ok with throwing away negative uids here. Also, remove nil values.
499
503
  user_ids = dscl_output.split.compact.collect { |l| l.to_i if l.match(/^\d+$/) }
@@ -505,19 +509,29 @@ Puppet::Type.type(:user).provide :directoryservice do
505
509
  end
506
510
  end
507
511
 
512
+ # This method is only called on version 10.7 or greater. On 10.7 machines,
513
+ # passwords are set using a salted-SHA512 hash, and on 10.8 machines,
514
+ # passwords are set using PBKDF2. It's possible to have users on 10.8
515
+ # who have upgraded from 10.7 and thus have a salted-SHA512 password hash.
516
+ # If we encounter this, do what 10.8 does - remove that key and give them
517
+ # a 10.8-style PBKDF2 password.
508
518
  def write_password_to_users_plist(value)
509
- # # This method is only called on version 10.7 or greater. On 10.7 machines,
510
- # # passwords are set using a salted-SHA512 hash, and on 10.8 machines,
511
- # # passwords are set using PBKDF2. It's possible to have users on 10.8
512
- # # who have upgraded from 10.7 and thus have a salted-SHA512 password hash.
513
- # # If we encounter this, do what 10.8 does - remove that key and give them
514
- # # a 10.8-style PBKDF2 password.
515
- users_plist = Plist::parse_xml(plutil '-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{@resource.name}.plist")
519
+ users_plist = get_users_plist(@resource.name)
516
520
  shadow_hash_data = get_shadow_hash_data(users_plist)
517
- if Facter.value(:macosx_productversion_major) == '10.7'
521
+ if self.class.get_os_version == '10.7'
518
522
  set_salted_sha512(users_plist, shadow_hash_data, value)
519
523
  else
520
- shadow_hash_data.delete('SALTED-SHA512') if shadow_hash_data['SALTED-SHA512']
524
+ # It's possible that a user could exist on the system and NOT have
525
+ # a ShadowHashData key (especially if the system was upgraded from 10.6).
526
+ # In this case, a conditional check is needed to determine if the
527
+ # shadow_hash_data variable is a Hash (it would be false if the key
528
+ # didn't exist for this user on the system). If the shadow_hash_data
529
+ # variable IS a Hash and contains the 'SALTED-SHA512' key (indicating an
530
+ # older 10.7-style password hash), it will be deleted and a newer
531
+ # 10.8-style (PBKDF2) password hash will be generated.
532
+ if (shadow_hash_data.class == Hash) && (shadow_hash_data.has_key?('SALTED-SHA512'))
533
+ shadow_hash_data.delete('SALTED-SHA512')
534
+ end
521
535
  set_salted_pbkdf2(users_plist, shadow_hash_data, 'entropy', value)
522
536
  end
523
537
  end
@@ -526,9 +540,15 @@ Puppet::Type.type(:user).provide :directoryservice do
526
540
  dscacheutil '-flushcache'
527
541
  end
528
542
 
543
+ def get_users_plist(username)
544
+ # This method will retrieve the data stored in a user's plist and
545
+ # return it as a native Ruby hash.
546
+ Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist"))
547
+ end
548
+
549
+ # This method will return the binary plist that's embedded in the
550
+ # ShadowHashData key of a user's plist, or false if it doesn't exist.
529
551
  def get_shadow_hash_data(users_plist)
530
- # This method will return the binary plist that's embedded in the
531
- # ShadowHashData key of a user's plist, or false if it doesn't exist.
532
552
  if users_plist['ShadowHashData']
533
553
  password_hash_plist = users_plist['ShadowHashData'][0].string
534
554
  self.class.convert_binary_to_xml(password_hash_plist)
@@ -537,36 +557,63 @@ Puppet::Type.type(:user).provide :directoryservice do
537
557
  end
538
558
  end
539
559
 
560
+ # This method will embed the binary plist data comprising the user's
561
+ # password hash (and Salt/Iterations value if the OS is 10.8 or greater)
562
+ # into the ShadowHashData key of the user's plist.
563
+ def set_shadow_hash_data(users_plist, binary_plist)
564
+ if users_plist.has_key?('ShadowHashData')
565
+ users_plist['ShadowHashData'][0].string = binary_plist
566
+ else
567
+ users_plist['ShadowHashData'] = [new_stringio_object(binary_plist)]
568
+ end
569
+ write_users_plist_to_disk(users_plist)
570
+ end
571
+
572
+ # This method returns a new StringIO object. Why does it exist?
573
+ # Well, StringIO objects have their own 'serial number', so when
574
+ # writing rspec tests it's difficult to compare StringIO objects
575
+ # due to this serial number. If this action is wrapped in its own
576
+ # method, it can be mocked for easier testing.
577
+ def new_stringio_object(value = '')
578
+ StringIO.new(value)
579
+ end
580
+
581
+ # This method accepts an argument of a hex password hash, and base64
582
+ # decodes it into a format that OS X 10.7 and 10.8 will store
583
+ # in the user's plist.
584
+ def base64_decode_string(value)
585
+ Base64.decode64([[value].pack("H*")].pack("m").strip)
586
+ end
587
+
588
+ # Puppet requires a salted-sha512 password hash for 10.7 users to be passed
589
+ # in Hex, but the embedded plist stores that value as a Base64 encoded
590
+ # string. This method converts the string and calls the
591
+ # set_shadow_hash_data method to serialize and write the plist to disk.
540
592
  def set_salted_sha512(users_plist, shadow_hash_data, value)
541
- # Puppet requires a salted-sha512 password hash for 10.7 users to be passed
542
- # in Hex, but the embedded plist stores that value as a Base64 encoded
543
- # string. This method converts the string and calls the
544
- # write_users_plist_to_disk method to serialize and write the plist to disk.
545
593
  unless shadow_hash_data
546
594
  shadow_hash_data = Hash.new
547
- shadow_hash_data['SALTED-SHA512'] = StringIO.new
595
+ shadow_hash_data['SALTED-SHA512'] = new_stringio_object
548
596
  end
549
- shadow_hash_data['SALTED-SHA512'].string = Base64.decode64([[value].pack("H*")].pack("m").strip)
597
+ shadow_hash_data['SALTED-SHA512'].string = base64_decode_string(value)
550
598
  binary_plist = self.class.convert_xml_to_binary(shadow_hash_data)
551
- users_plist['ShadowHashData'][0].string = binary_plist
552
- write_users_plist_to_disk(users_plist)
599
+ set_shadow_hash_data(users_plist, binary_plist)
553
600
  end
554
601
 
602
+ # This method accepts a passed value and one of three fields: 'salt',
603
+ # 'entropy', or 'iterations'. These fields correspond with the fields
604
+ # utilized in a PBKDF2 password hashing system
605
+ # (see http://en.wikipedia.org/wiki/PBKDF2 ) where 'entropy' is the
606
+ # password hash, 'salt' is the password hash salt value, and 'iterations'
607
+ # is an integer recommended to be > 10,000. The remaining arguments are
608
+ # the user's plist itself, and the shadow_hash_data hash containing the
609
+ # existing PBKDF2 values.
555
610
  def set_salted_pbkdf2(users_plist, shadow_hash_data, field, value)
556
- # This method accepts a passed value and one of three fields: 'salt',
557
- # 'entropy', or 'iterations'. These fields correspond with the fields
558
- # utilized in a PBKDF2 password hashing system
559
- # (see http://en.wikipedia.org/wiki/PBKDF2 ) where 'entropy' is the
560
- # password hash, 'salt' is the password hash salt value, and 'iterations'
561
- # is an integer recommended to be > 10,000. The remaining arguments are
562
- # the user's plist itself, and the shadow_hash_data hash containing the
563
- # existing PBKDF2 values.
564
611
  shadow_hash_data = Hash.new unless shadow_hash_data
565
612
  shadow_hash_data['SALTED-SHA512-PBKDF2'] = Hash.new unless shadow_hash_data['SALTED-SHA512-PBKDF2']
566
613
  case field
567
614
  when 'salt', 'entropy'
568
- shadow_hash_data['SALTED-SHA512-PBKDF2'][field] = StringIO.new unless shadow_hash_data['SALTED-SHA512-PBKDF2'][field]
569
- shadow_hash_data['SALTED-SHA512-PBKDF2'][field].string = Base64.decode64([[value].pack("H*")].pack("m").strip)
615
+ shadow_hash_data['SALTED-SHA512-PBKDF2'][field] = new_stringio_object unless shadow_hash_data['SALTED-SHA512-PBKDF2'][field]
616
+ shadow_hash_data['SALTED-SHA512-PBKDF2'][field].string = base64_decode_string(value)
570
617
  when 'iterations'
571
618
  shadow_hash_data['SALTED-SHA512-PBKDF2'][field] = Integer(value)
572
619
  else
@@ -577,22 +624,22 @@ Puppet::Type.type(:user).provide :directoryservice do
577
624
  # fail.
578
625
  users_plist['passwd'] = ('*' * 8)
579
626
 
580
- # Convert shadow_hash_data to a binary plist, write that value to the
581
- # users_plist hash, and write the users_plist back to disk.
627
+ # Convert shadow_hash_data to a binary plist, and call the
628
+ # set_shadow_hash_data method to serialize and write the data
629
+ # back to the user's plist.
582
630
  binary_plist = self.class.convert_xml_to_binary(shadow_hash_data)
583
- users_plist['ShadowHashData'][0].string = binary_plist
584
- write_users_plist_to_disk(users_plist)
631
+ set_shadow_hash_data(users_plist, binary_plist)
585
632
  end
586
633
 
634
+ # This method will accept a plist in XML format, save it to disk, convert
635
+ # the plist to a binary format, and flush the dscl cache.
587
636
  def write_users_plist_to_disk(users_plist)
588
- # This method will accept a plist in XML format, save it to disk, convert
589
- # the plist to a binary format, and flush the dscl cache.
590
637
  Plist::Emit.save_plist(users_plist, "#{users_plist_dir}/#{@resource.name}.plist")
591
638
  plutil'-convert', 'binary1', "#{users_plist_dir}/#{@resource.name}.plist"
592
639
  end
593
640
 
641
+ # This is a simple wrapper method for writing values to a file.
594
642
  def write_to_file(filename, value)
595
- # This is a simple wrapper method for writing values to a file.
596
643
  begin
597
644
  File.open(filename, 'w') { |f| f.write(value)}
598
645
  rescue Errno::EACCES => detail