class Puppet::Provider::NameService::DirectoryService

Attributes

ds_path[W]

JJM: This allows us to pass information when calling

    Puppet::Type.type
e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users"
This is referenced in the get_ds_path class method

Public Class Methods

convert_binary_to_hash(plist_data) click to toggle source

This method will accept a binary plist (as a string) and convert it to a hash.

    # File lib/puppet/provider/nameservice/directoryservice.rb
273 def self.convert_binary_to_hash(plist_data)
274   Puppet.debug('Converting binary plist to hash')
275   Puppet::Util::Plist.parse_plist(plist_data)
276 end
convert_hash_to_binary(plist_data) click to toggle source

This method will accept a hash and convert it to a binary plist (string value).

    # File lib/puppet/provider/nameservice/directoryservice.rb
267 def self.convert_hash_to_binary(plist_data)
268   Puppet.debug('Converting plist hash to binary')
269   Puppet::Util::Plist.dump_plist(plist_data, :binary)
270 end
ds_to_ns_attribute_map() click to toggle source
   # File lib/puppet/provider/nameservice/directoryservice.rb
40 def self.ds_to_ns_attribute_map
41   {
42     'RecordName' => :name,
43     'PrimaryGroupID' => :gid,
44     'NFSHomeDirectory' => :home,
45     'UserShell' => :shell,
46     'UniqueID' => :uid,
47     'RealName' => :comment,
48     'Password' => :password,
49     'GeneratedUID' => :guid,
50     'IPAddress'    => :ip_address,
51     'ENetAddress'  => :en_address,
52     'GroupMembership' => :members,
53   }
54 end
generate_attribute_hash(input_hash, *type_properties) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
112 def self.generate_attribute_hash(input_hash, *type_properties)
113   attribute_hash = {}
114   input_hash.each_key do |key|
115     ds_attribute = key.sub("dsAttrTypeStandard:", "")
116     next unless (ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? ds_to_ns_attribute_map[ds_attribute])
117     ds_value = input_hash[key]
118     case ds_to_ns_attribute_map[ds_attribute]
119       when :members
120         ds_value = ds_value # only members uses arrays so far
121       when :gid, :uid
122         # OS X stores objects like uid/gid as strings.
123         # Try casting to an integer for these cases to be
124         # consistent with the other providers and the group type
125         # validation
126         begin
127           ds_value = Integer(ds_value[0])
128         rescue ArgumentError
129           ds_value = ds_value[0]
130         end
131       else ds_value = ds_value[0]
132     end
133     attribute_hash[ds_to_ns_attribute_map[ds_attribute]] = ds_value
134   end
135 
136   # NBK: need to read the existing password here as it's not actually
137   # stored in the user record. It is stored at a path that involves the
138   # UUID of the user record for non-Mobile local accounts.
139   # Mobile Accounts are out of scope for this provider for now
140   attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name]) if @resource_type.validproperties.include?(:password) and Puppet.features.root?
141   attribute_hash
142 end
get_ds_path() click to toggle source
   # File lib/puppet/provider/nameservice/directoryservice.rb
83 def self.get_ds_path
84   # JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with.
85   #      For example, if we're working with an user type, this will be /Users
86   #      with a group type, this will be /Groups.
87   #   @ds_path is an attribute of the class itself.
88   return @ds_path if defined?(@ds_path)
89   # JJM: "Users" or "Groups" etc ...  (Based on the Puppet::Type)
90   #       Remember this is a class method, so self.class is Class
91   #       Also, @resource_type seems to be the reference to the
92   #       Puppet::Type this class object is providing for.
93   @resource_type.name.to_s.capitalize + "s"
94 end
get_exec_preamble(ds_action, resource_name = nil) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
169 def self.get_exec_preamble(ds_action, resource_name = nil)
170   # JJM 2007-07-24
171   #     DSCL commands are often repetitive and contain the same positional
172   #     arguments over and over. See https://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html
173   #     for an example of what I mean.
174   #     This method spits out proper DSCL commands for us.
175   #     We EXPECT name to be @resource[:name] when called from an instance object.
176 
177   command_vector = [ command(:dscl), "-plist", "." ]
178 
179   # JJM: The actual action to perform. See "man dscl".
180   #      Common actions: -create, -delete, -merge, -append, -passwd
181   command_vector << ds_action
182   # JJM: get_ds_path will spit back "Users" or "Groups",
183   # etc...  Depending on the Puppet::Type of our self.
184   if resource_name
185     command_vector << "/#{get_ds_path}/#{resource_name}"
186   else
187     command_vector << "/#{get_ds_path}"
188   end
189   # JJM:  This returns most of the preamble of the command.
190   #       e.g. 'dscl / -create /Users/mccune'
191   command_vector
192 end
get_password(guid, username) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
241 def self.get_password(guid, username)
242   plist_file = "#{users_plist_dir}/#{username}.plist"
243   if Puppet::FileSystem.exist?(plist_file)
244     # If a plist exists in /var/db/dslocal/nodes/Default/users, we will
245     # extract the binary plist from the 'ShadowHashData' key, decode the
246     # salted-SHA512 password hash, and then return it.
247     users_plist = Puppet::Util::Plist.read_plist_file(plist_file)
248 
249     if users_plist['ShadowHashData']
250       # users_plist['ShadowHashData'][0] is actually a binary plist
251       # that's nested INSIDE the user's plist (which itself is a binary
252       # plist).
253       password_hash_plist = users_plist['ShadowHashData'][0]
254       converted_hash_plist = convert_binary_to_hash(password_hash_plist)
255 
256       # converted_hash_plist['SALTED-SHA512'] is a Base64 encoded
257       # string. The password_hash provided as a resource attribute is a
258       # hex value. We need to convert the Base64 encoded string to a
259       # hex value and provide it back to Puppet.
260       password_hash = converted_hash_plist['SALTED-SHA512'].unpack("H*")[0]
261       password_hash
262     end
263   end
264 end
instances() click to toggle source
   # File lib/puppet/provider/nameservice/directoryservice.rb
70 def self.instances
71   # JJM Class method that provides an array of instance objects of this
72   #     type.
73   # JJM: Properties are dependent on the Puppet::Type we're managing.
74   type_property_array = [:name] + @resource_type.validproperties
75 
76   # Create a new instance of this Puppet::Type for each object present
77   #    on the system.
78   list_all_present.collect do |name_string|
79     self.new(single_report(name_string, *type_property_array))
80   end
81 end
list_all_present() click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
 96 def self.list_all_present
 97   @all_present ||= begin
 98     # JJM: List all objects of this Puppet::Type already present on the system.
 99     begin
100       dscl_output = execute(get_exec_preamble("-list"))
101     rescue Puppet::ExecutionFailure
102       fail(_("Could not get %{resource} list from DirectoryService") % { resource: @resource_type.name })
103     end
104     dscl_output.split("\n")
105   end
106 end
next_system_id(id_type, min_id=20) click to toggle source

Unlike most other *nixes, OS X doesn't provide built in functionality for automatically assigning uids and gids to accounts, so we set up these methods for consumption by functionality like –mkusers By default we restrict to a reasonably sane range for system accounts

    # File lib/puppet/provider/nameservice/directoryservice.rb
282 def self.next_system_id(id_type, min_id=20)
283   dscl_args = ['.', '-list']
284   if id_type == 'uid'
285     dscl_args << '/Users' << 'uid'
286   elsif id_type == 'gid'
287     dscl_args << '/Groups' << 'gid'
288   else
289     fail(_("Invalid id_type %{id_type}. Only 'uid' and 'gid' supported") % { id_type: id_type })
290   end
291   dscl_out = dscl(dscl_args)
292   # We're ok with throwing away negative uids here.
293   ids = dscl_out.split.compact.collect { |l| l.to_i if l =~ /^\d+$/ }
294   ids.compact!.sort! { |a,b| a.to_f <=> b.to_f }
295   # We're just looking for an unused id in our sorted array.
296   ids.each_index do |i|
297     next_id = ids[i] + 1
298     return next_id if ids[i+1] != next_id and next_id >= min_id
299   end
300 end
ns_to_ds_attribute_map() click to toggle source
   # File lib/puppet/provider/nameservice/directoryservice.rb
58 def self.ns_to_ds_attribute_map
59   @ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert
60 end
parse_dscl_plist_data(dscl_output) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
108 def self.parse_dscl_plist_data(dscl_output)
109   Puppet::Util::Plist.parse_plist(dscl_output)
110 end
password_hash_dir() click to toggle source
   # File lib/puppet/provider/nameservice/directoryservice.rb
62 def self.password_hash_dir
63   '/var/db/shadow/hash'
64 end
post_resource_eval() click to toggle source

There is no generalized mechanism for provider cache management, but we can use post_resource_eval, which will be run for each suitable provider at the end of each transaction. Use this to clear @all_present after each run.

   # File lib/puppet/provider/nameservice/directoryservice.rb
29 def self.post_resource_eval
30   @all_present = nil
31 end
set_password(resource_name, guid, password_hash) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
194 def self.set_password(resource_name, guid, password_hash)
195   # 10.7 uses salted SHA512 password hashes which are 128 characters plus
196   # an 8 character salt. Previous versions used a SHA1 hash padded with
197   # zeroes. If someone attempts to use a password hash that worked with
198   # a previous version of OS X, we will fail early and warn them.
199   if password_hash.length != 136
200     #TRANSLATORS 'OS X 10.7' is an operating system and should not be translated, 'Salted SHA512' is the name of a hashing algorithm
201     fail(_("OS X 10.7 requires a Salted SHA512 hash password of 136 characters.") +
202          ' ' + _("Please check your password and try again."))
203   end
204 
205   plist_file = "#{users_plist_dir}/#{resource_name}.plist"
206   if Puppet::FileSystem.exist?(plist_file)
207     # If a plist already exists in /var/db/dslocal/nodes/Default/users, then
208     # we will need to extract the binary plist from the 'ShadowHashData'
209     # key, log the new password into the resultant plist's 'SALTED-SHA512'
210     # key, and then save the entire structure back.
211     users_plist = Puppet::Util::Plist.read_plist_file(plist_file)
212 
213     # users_plist['ShadowHashData'][0] is actually a binary plist
214     # that's nested INSIDE the user's plist (which itself is a binary
215     # plist). If we encounter a user plist that DOESN'T have a
216     # ShadowHashData field, create one.
217     if users_plist['ShadowHashData']
218       password_hash_plist = users_plist['ShadowHashData'][0]
219       converted_hash_plist = convert_binary_to_hash(password_hash_plist)
220     else
221       users_plist['ShadowHashData'] = String.new
222       converted_hash_plist = {'SALTED-SHA512' => String.new}
223     end
224 
225     # converted_hash_plist['SALTED-SHA512'] expects a Base64 encoded
226     # string. The password_hash provided as a resource attribute is a
227     # hex value. We need to convert the provided hex value to a Base64
228     # encoded string to nest it in the converted hash plist.
229     converted_hash_plist['SALTED-SHA512'] = \
230       password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join
231 
232     # Finally, we can convert the nested plist back to binary, embed it
233     # into the user's plist, and convert the resultant plist back to
234     # a binary plist.
235     changed_plist = convert_hash_to_binary(converted_hash_plist)
236     users_plist['ShadowHashData'][0] = changed_plist
237     Puppet::Util::Plist.write_plist_file(users_plist, plist_file, :binary)
238   end
239 end
single_report(resource_name, *type_properties) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
144 def self.single_report(resource_name, *type_properties)
145   # JJM 2007-07-24:
146   #     Given a the name of an object and a list of properties of that
147   #     object, return all property values in a hash.
148   #
149   #     This class method returns nil if the object doesn't exist
150   #     Otherwise, it returns a hash of the object properties.
151 
152   all_present_str_array = list_all_present
153 
154   # NBK: shortcut the process if the resource is missing
155   return nil unless all_present_str_array.include? resource_name
156 
157   dscl_vector = get_exec_preamble("-read", resource_name)
158   begin
159     dscl_output = execute(dscl_vector)
160   rescue Puppet::ExecutionFailure
161     fail(_("Could not get report.  command execution failed."))
162   end
163 
164   dscl_plist = self.parse_dscl_plist_data(dscl_output)
165 
166   self.generate_attribute_hash(dscl_plist, *type_properties)
167 end
users_plist_dir() click to toggle source
   # File lib/puppet/provider/nameservice/directoryservice.rb
66 def self.users_plist_dir
67   '/var/db/dslocal/nodes/Default/users'
68 end

Public Instance Methods

add_members(current_members, new_members) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
453 def add_members(current_members, new_members)
454   new_members.flatten.each do |new_member|
455     if current_members.nil? or not current_members.include?(new_member)
456       cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]]
457       begin
458         execute(cmd)
459       rescue Puppet::ExecutionFailure => detail
460         fail(_("Could not add %{new_member} to group: %{name}, %{detail}") % { new_member: new_member, name: @resource.name, detail: detail })
461       end
462     end
463   end
464 end
create() click to toggle source

NBK: we override @parent.create as we need to execute a series of commands to create objects with dscl, rather than the single command nameservice.rb expects to be returned by addcmd. Thus we don't bother defining addcmd.

    # File lib/puppet/provider/nameservice/directoryservice.rb
378 def create
379   if exists?
380     info _("already exists")
381     return nil
382   end
383 
384   # NBK: First we create the object with a known guid so we can set the contents
385   # of the password hash if required
386   # Shelling out sucks, but for a single use case it doesn't seem worth
387   # requiring people install a UUID library that doesn't come with the system.
388   # This should be revisited if Puppet starts managing UUIDs for other platform
389   # user records.
390   guid = %x{/usr/bin/uuidgen}.chomp
391 
392   exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
393   exec_arg_vector << ns_to_ds_attribute_map[:guid] << guid
394   begin
395     execute(exec_arg_vector)
396   rescue Puppet::ExecutionFailure => detail
397     fail(_("Could not set GeneratedUID for %{resource} %{name}: %{detail}") % { resource: @resource.class.name, name: @resource.name, detail: detail })
398   end
399 
400   value = @resource.should(:password)
401   if value && value != ""
402     self.class.set_password(@resource[:name], guid, value)
403   end
404 
405   # Now we create all the standard properties
406   Puppet::Type.type(@resource.class.name).validproperties.each do |property|
407     next if property == :ensure
408     value = @resource.should(property)
409     if property == :gid and value.nil?
410       value = self.class.next_system_id('gid')
411     end
412     if property == :uid and value.nil?
413       value = self.class.next_system_id('uid')
414     end
415     if value != "" and not value.nil?
416       if property == :members
417         add_members(nil, value)
418       else
419         exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
420         exec_arg_vector << ns_to_ds_attribute_map[property.intern]
421         next if property == :password  # skip setting the password here
422         exec_arg_vector << value.to_s
423         begin
424           execute(exec_arg_vector)
425         rescue Puppet::ExecutionFailure => detail
426           fail(_("Could not create %{resource} %{name}: %{detail}") % { resource: @resource.class.name, name: @resource.name, detail: detail })
427         end
428       end
429     end
430   end
431 end
deletecmd() click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
466 def deletecmd
467   # JJM: Like addcmd, only called when deleting the object itself
468   #    Note, this isn't used to delete properties of the object,
469   #    at least that's how I understand it...
470   self.class.get_exec_preamble("-delete", @resource[:name])
471 end
ds_to_ns_attribute_map() click to toggle source

JJM 2007-07-25: This map is used to map NameService attributes to their

corresponding DirectoryService attribute names.
See: http://images.apple.com/server/docs.Open_Directory_v10.4.pdf

JJM: Note, this is de-coupled from the Puppet::Type, and must

be actively maintained.  There may also be collisions with different
types (Users, Groups, Mounts, Hosts, etc...)
   # File lib/puppet/provider/nameservice/directoryservice.rb
39 def ds_to_ns_attribute_map; self.class.ds_to_ns_attribute_map; end
ensure=(ensure_value) click to toggle source
Calls superclass method
    # File lib/puppet/provider/nameservice/directoryservice.rb
303 def ensure=(ensure_value)
304   super
305   # We need to loop over all valid properties for the type we're
306   # managing and call the method which sets that property value
307   # dscl can't create everything at once unfortunately.
308   if ensure_value == :present
309     @resource.class.validproperties.each do |name|
310       next if name == :ensure
311       # LAK: We use property.sync here rather than directly calling
312       # the settor method because the properties might do some kind
313       # of conversion.  In particular, the user gid property might
314       # have a string and need to convert it to a number
315       if @resource.should(name)
316         @resource.property(name).sync
317       else
318         value = autogen(name)
319         if value
320           self.send(name.to_s + "=", value)
321         else
322           next
323         end
324       end
325     end
326   end
327 end
getinfo(refresh = false) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
473 def getinfo(refresh = false)
474   # JJM 2007-07-24:
475   #      Override the getinfo method, which is also defined in nameservice.rb
476   #      This method returns and sets @infohash
477   # I'm not re-factoring the name "getinfo" because this method will be
478   # most likely called by nameservice.rb, which I didn't write.
479   if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash)
480     # JJM 2007-07-24: OK, there's a bit of magic that's about to
481     # happen... Let's see how strong my grip has become... =)
482     #
483     # self is a provider instance of some Puppet::Type, like
484     # Puppet::Type::User::ProviderDirectoryservice for the case of the
485     # user type and this provider.
486     #
487     # self.class looks like "user provider directoryservice", if that
488     # helps you ...
489     #
490     # self.class.resource_type is a reference to the Puppet::Type class,
491     # probably Puppet::Type::User or Puppet::Type::Group, etc...
492     #
493     # self.class.resource_type.validproperties is a class method,
494     # returning an Array of the valid properties of that specific
495     # Puppet::Type.
496     #
497     # So... something like [:comment, :home, :password, :shell, :uid,
498     # :groups, :ensure, :gid]
499     #
500     # Ultimately, we add :name to the list, delete :ensure from the
501     # list, then report on the remaining list. Pretty whacky, ehh?
502     type_properties = [:name] + self.class.resource_type.validproperties
503     type_properties.delete(:ensure) if type_properties.include? :ensure
504     type_properties << :guid  # append GeneratedUID so we just get the report here
505     @property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties)
506     [:uid, :gid].each do |param|
507       @property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param)
508     end
509   end
510   @property_value_cache_hash
511 end
ns_to_ds_attribute_map() click to toggle source

JJM The same table as above, inverted.

   # File lib/puppet/provider/nameservice/directoryservice.rb
57 def ns_to_ds_attribute_map; self.class.ns_to_ds_attribute_map end
password=(passphrase) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
329 def password=(passphrase)
330   exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name)
331   exec_arg_vector << ns_to_ds_attribute_map[:guid]
332   begin
333     guid_output = execute(exec_arg_vector)
334     guid_plist = Puppet::Util::Plist.parse_plist(guid_output)
335     # Although GeneratedUID like all DirectoryService values can be multi-valued
336     # according to the schema, in practice user accounts cannot have multiple UUIDs
337     # otherwise Bad Things Happen, so we just deal with the first value.
338     guid = guid_plist["dsAttrTypeStandard:#{ns_to_ds_attribute_map[:guid]}"][0]
339     self.class.set_password(@resource.name, guid, passphrase)
340   rescue Puppet::ExecutionFailure => detail
341     fail(_("Could not set %{param} on %{resource}[%{name}]: %{detail}") % { param: param, resource: @resource.class.name, name: @resource.name, detail: detail })
342   end
343 end
remove_unwanted_members(current_members, new_members) click to toggle source
    # File lib/puppet/provider/nameservice/directoryservice.rb
433 def remove_unwanted_members(current_members, new_members)
434   current_members.each do |member|
435     if not new_members.flatten.include?(member)
436       cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]]
437       begin
438         execute(cmd)
439       rescue Puppet::ExecutionFailure
440         # TODO: We're falling back to removing the member using dscl due to rdar://8481241
441         # This bug causes dseditgroup to fail to remove a member if that member doesn't exist
442         cmd = [:dscl, ".", "-delete", "/Groups/#{@resource.name}", "GroupMembership", member]
443         begin
444           execute(cmd)
445         rescue Puppet::ExecutionFailure => detail
446           fail(_("Could not remove %{member} from group: %{resource}, %{detail}") % { member: member, resource: @resource.name, detail: detail })
447         end
448       end
449     end
450   end
451 end
set(param, value) click to toggle source

NBK: we override @parent.set as we need to execute a series of commands to deal with array values, rather than the single command nameservice.rb expects to be returned by modifycmd. Thus we don't bother defining modifycmd.

    # File lib/puppet/provider/nameservice/directoryservice.rb
349 def set(param, value)
350   self.class.validate(param, value)
351   current_members = @property_value_cache_hash[:members]
352   if param == :members
353     # If we are meant to be authoritative for the group membership
354     # then remove all existing members who haven't been specified
355     # in the manifest.
356     remove_unwanted_members(current_members, value) if @resource[:auth_membership] and not current_members.nil?
357 
358     # if they're not a member, make them one.
359     add_members(current_members, value)
360   else
361     exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
362     # JJM: The following line just maps the NS name to the DS name
363     #      e.g. { :uid => 'UniqueID' }
364     exec_arg_vector << ns_to_ds_attribute_map[param.intern]
365     # JJM: The following line sends the actual value to set the property to
366     exec_arg_vector << value.to_s
367     begin
368       execute(exec_arg_vector)
369     rescue Puppet::ExecutionFailure => detail
370       fail(_("Could not set %{param} on %{resource}[%{name}]: %{detail}") % { param: param, resource: @resource.class.name, name: @resource.name, detail: detail })
371     end
372   end
373 end