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.
- data/lib/puppet/provider/service/launchd.rb +59 -18
- data/lib/puppet/provider/user/directoryservice.rb +162 -115
- data/lib/puppet/util/run_mode.rb +2 -6
- data/lib/puppet/version.rb +1 -1
- data/spec/unit/provider/service/launchd_spec.rb +32 -1
- data/spec/unit/provider/user/directoryservice_spec.rb +282 -170
- data/spec/unit/util/run_mode_spec.rb +4 -6
- metadata +4 -4
@@ -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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
92
|
-
|
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.
|
96
|
-
|
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
|
-
|
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
|
-
|
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?(
|
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(
|
314
|
+
overrides = self.class.read_plist(self.class.launchd_overrides)
|
274
315
|
overrides[resource[:name]] = { "Disabled" => false }
|
275
|
-
Plist::Emit.save_plist(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(
|
329
|
+
overrides = self.class.read_plist(self.class.launchd_overrides)
|
289
330
|
overrides[resource[:name]] = { "Disabled" => true }
|
290
|
-
Plist::Emit.save_plist(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(
|
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
|
-
|
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
|
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
|
-
|
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 =
|
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
|
-
|
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 =
|
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
|
-
|
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
|
521
|
+
if self.class.get_os_version == '10.7'
|
518
522
|
set_salted_sha512(users_plist, shadow_hash_data, value)
|
519
523
|
else
|
520
|
-
|
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'] =
|
595
|
+
shadow_hash_data['SALTED-SHA512'] = new_stringio_object
|
548
596
|
end
|
549
|
-
shadow_hash_data['SALTED-SHA512'].string =
|
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
|
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] =
|
569
|
-
shadow_hash_data['SALTED-SHA512-PBKDF2'][field].string =
|
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,
|
581
|
-
#
|
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
|
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
|