class Puppet::Provider::NameService::DirectoryService
Attributes
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
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
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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
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
# 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
# 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
# File lib/puppet/provider/nameservice/directoryservice.rb 62 def self.password_hash_dir 63 '/var/db/shadow/hash' 64 end
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
# 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
# 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
# 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
# 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
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
# 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
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
# 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
# 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
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
# 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
# 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
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