lib/puppet/provider/nameservice/directoryservice.rb in puppet-0.24.6 vs lib/puppet/provider/nameservice/directoryservice.rb in puppet-0.24.7
- old
+ new
@@ -12,11 +12,13 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston MA 02110-1301 USA
require 'puppet'
require 'puppet/provider/nameservice'
+require 'facter/util/plist'
+
class Puppet::Provider::NameService
class DirectoryService < Puppet::Provider::NameService
# JJM: Dive into the eigenclass
class << self
# JJM: This allows us to pass information when calling
@@ -24,23 +26,24 @@
# e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users"
# This is referenced in the get_ds_path class method
attr_writer :ds_path
end
+
# JJM 2007-07-24: Not yet sure what initvars() does. I saw it in netinfo.rb
# I do know, however, that it makes methods "work" =)
# e.g. addcmd isn't available if this method call isn't present.
#
# JJM: Also, where this method is defined seems to impact the visibility
# of methods. If I put initvars after commands, confine and defaultfor,
# then getinfo is called from the parent class, not this class.
initvars()
commands :dscl => "/usr/bin/dscl"
+ commands :dseditgroup => "/usr/sbin/dseditgroup"
confine :operatingsystem => :darwin
- # JJM FIXME: This will need to be the default around October 2007.
- # defaultfor :operatingsystem => :darwin
+ defaultfor :operatingsystem => :darwin
# 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
@@ -53,30 +56,37 @@
'NFSHomeDirectory' => :home,
'UserShell' => :shell,
'UniqueID' => :uid,
'RealName' => :comment,
'Password' => :password,
+ 'GeneratedUID' => :guid,
+ 'IPAddress' => :ip_address,
+ 'ENetAddress' => :en_address,
+ 'GroupMembership' => :members,
}
# JJM The same table as above, inverted.
@@ns_to_ds_attribute_map = {
:name => 'RecordName',
:gid => 'PrimaryGroupID',
:home => 'NFSHomeDirectory',
:shell => 'UserShell',
:uid => 'UniqueID',
:comment => 'RealName',
:password => 'Password',
+ :guid => 'GeneratedUID',
+ :en_address => 'ENetAddress',
+ :ip_address => 'IPAddress',
+ :members => 'GroupMembership',
}
+ @@password_hash_dir = "/var/db/shadow/hash"
+
def self.instances
# JJM Class method that provides an array of instance objects of this
# type.
-
# JJM: Properties are dependent on the Puppet::Type we're managine.
type_property_array = [:name] + @resource_type.validproperties
- # JJM: No sense reporting the password. It's hashed.
- type_property_array.delete(:password) if type_property_array.include? :password
# Create a new instance of this Puppet::Type for each object present
# on the system.
list_all_present.collect do |name_string|
self.new(single_report(name_string, *type_property_array))
@@ -117,11 +127,11 @@
# This class method returns nil if the object doesn't exist
# Otherwise, it returns a hash of the object properties.
all_present_str_array = list_all_present()
- # JJM: Return nil if the named object isn't present.
+ # NBK: shortcut the process if the resource is missing
return nil unless all_present_str_array.include? resource_name
dscl_vector = get_exec_preamble("-read", resource_name)
begin
dscl_output = execute(dscl_vector)
@@ -130,48 +140,41 @@
end
# JJM: We need a new hash to return back to our caller.
attribute_hash = Hash.new
- # JJM: First, the output string goes into an array.
- # Then, the each array element is split
- # If you want to figure out what this is doing, I suggest
- # ruby-debug, and stepping through it.
- dscl_output.split("\n").each do |line|
- # JJM: Split the attribute name and the list of values.
- ds_attribute, ds_values_string = line.split(':')
-
- # Split sets the values to nil if there's nothing after the :
- ds_values_string ||= ""
-
- # JJM: skip this attribute line if the Puppet::Type doesn't care about it.
+ dscl_plist = Plist.parse_xml(dscl_output)
+ dscl_plist.keys().each do |key|
+ ds_attribute = key.sub("dsAttrTypeStandard:", "")
next unless (@@ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? @@ds_to_ns_attribute_map[ds_attribute])
-
- # JJM: We asked dscl to output url encoded values so we're able
- # to machine parse on whitespace. We need to urldecode:
- # " Jeff%20McCune John%20Doe " => ["Jeff McCune", "John Doe"]
- ds_value_array = ds_values_string.scan(/[^\s]+/).collect do |v|
- url_decoded_value = CGI::unescape v
- if url_decoded_value =~ /^[-0-9]+$/
- url_decoded_value.to_i
- else
- url_decoded_value
- end
+ ds_value = dscl_plist[key]
+ case @@ds_to_ns_attribute_map[ds_attribute]
+ when :members:
+ ds_value = ds_value # only members uses arrays so far
+ when :gid, :uid:
+ # OS X stores objects like uid/gid as strings.
+ # Try casting to an integer for these cases to be
+ # consistent with the other providers and the group type
+ # validation
+ begin
+ ds_value = Integer(ds_value[0])
+ rescue ArgumentError
+ ds_value = ds_value[0]
+ end
+ else ds_value = ds_value[0]
end
-
- # JJM: Finally, we're able to build up our attribute hash.
- # Remember, the hash is keyed by NameService attribute names,
- # not DirectoryService attribute names.
- # NOTE: We're also sort of cheating here... DirectoryService
- # is robust enough to allow multiple values for almost every
- # attribute in the system. Traditional NameService things
- # really don't handle this case, so we'll always pull thet first
- # value returned from DirectoryService.
- # THERE MAY BE AN ORDERING ISSUE HERE, but I think it's ok...
- attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value_array[0]
+ attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value
end
- return attribute_hash
+
+ # NBK: need to read the existing password here as it's not actually
+ # stored in the user record. It is stored at a path that involves the
+ # UUID of the user record for non-Mobile local acccounts.
+ # Mobile Accounts are out of scope for this provider for now
+ if @resource_type.validproperties.include?(:password)
+ attribute_hash[:password] = self.get_password(attribute_hash[:guid])
+ end
+ return attribute_hash
end
def self.get_exec_preamble(ds_action, resource_name = nil)
# JJM 2007-07-24
# DSCL commands are often repetitive and contain the same positional
@@ -179,11 +182,11 @@
# for an example of what I mean.
# This method spits out proper DSCL commands for us.
# We EXPECT name to be @resource[:name] when called from an instance object.
# There are two ways to specify paths in 10.5. See man dscl.
- command_vector = [ command(:dscl), "-url", "." ]
+ command_vector = [ command(:dscl), "-plist", "." ]
# JJM: The actual action to perform. See "man dscl"
# Common actiosn: -create, -delete, -merge, -append, -passwd
command_vector << ds_action
# JJM: get_ds_path will spit back "Users" or "Groups",
# etc... Depending on the Puppet::Type of our self.
@@ -194,21 +197,66 @@
end
# JJM: This returns most of the preamble of the command.
# e.g. 'dscl / -create /Users/mccune'
return command_vector
end
+
+ def self.set_password(resource_name, guid, password_hash)
+ password_hash_file = "#{@@password_hash_dir}/#{guid}"
+ begin
+ File.open(password_hash_file, 'w') { |f| f.write(password_hash)}
+ rescue Errno::EACCES => detail
+ raise Puppet::Error, "Could not write to password hash file: #{detail}"
+ end
+
+ # NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of
+ # ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it
+ # will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if
+ # missing. Thus we make sure we only set ;ShadowHash; if it is missing, and
+ # we can do this with the merge command. This allows people to continue to
+ # use other custom AuthenticationAuthority attributes without stomping on them.
+ #
+ # There is a potential problem here in that we're only doing this when setting
+ # the password, and the attribute could get modified at other times while the
+ # hash doesn't change and so this doesn't get called at all... but
+ # without switching all the other attributes to merge instead of create I can't
+ # see a simple enough solution for this that doesn't modify the user record
+ # every single time. This should be a rather rare edge case. (famous last words)
+
+ dscl_vector = self.get_exec_preamble("-merge", resource_name)
+ dscl_vector << "AuthenticationAuthority" << ";ShadowHash;"
+ begin
+ dscl_output = execute(dscl_vector)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set AuthenticationAuthority."
+ end
+ end
+
+ def self.get_password(guid)
+ password_hash = nil
+ password_hash_file = "#{@@password_hash_dir}/#{guid}"
+ # TODO: sort out error conditions?
+ if File.exists?(password_hash_file)
+ if not File.readable?(password_hash_file)
+ raise Puppet::Error("Could not read password hash file at #{password_hash_file} for #{@resource[:name]}")
+ end
+ f = File.new(password_hash_file)
+ password_hash = f.read
+ f.close
+ end
+ password_hash
+ end
def ensure=(ensure_value)
super
# JJM: Modeled after nameservice/netinfo.rb, we need to
# loop over all valid properties for the type we're managing
# and call the method which sets that property value
# Like netinfo, dscl can't create everything at once, afaik.
if ensure_value == :present
@resource.class.validproperties.each do |name|
next if name == :ensure
-
# LAK: We use property.sync here rather than directly calling
# the settor method because the properties might do some kind
# of conversion. In particular, the user gid property might
# have a string and need to convert it to a number
if @resource.should(name)
@@ -221,85 +269,136 @@
end
end
end
def password=(passphrase)
- # JJM: Setting the password is a special case. We don't just
- # set the attribute because we need to update the password
- # databases.
- # FIRST, make sure the AuthenticationAuthority is ;ShadowHash; If
- # we don't do this, we don't get a shadow hash account. ("Obviously...")
- dscl_vector = self.class.get_exec_preamble("-create", @resource[:name])
- dscl_vector << "AuthenticationAuthority" << ";ShadowHash;"
- begin
- dscl_output = execute(dscl_vector)
- rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set AuthenticationAuthority."
- end
-
- # JJM: Second, we need to actually set the password. dscl does
- # some magic, creating the proper hash for us based on the
- # AuthenticationAuthority attribute, set above.
- dscl_vector = self.class.get_exec_preamble("-passwd", @resource[:name])
- dscl_vector << passphrase
- # JJM: Should we not log the password string? This may be a security
- # risk...
- begin
- dscl_output = execute(dscl_vector)
- rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set password using command vector: %{dscl_vector.inspect}"
- end
+ exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name)
+ exec_arg_vector << @@ns_to_ds_attribute_map[:guid]
+ begin
+ guid_output = execute(exec_arg_vector)
+ guid_plist = Plist.parse_xml(guid_output)
+ # Although GeneratedUID like all DirectoryService values can be multi-valued
+ # according to the schema, in practice user accounts cannot have multiple UUIDs
+ # otherwise Bad Things Happen, so we just deal with the first value.
+ guid = guid_plist["dsAttrTypeStandard:#{@@ns_to_ds_attribute_map[:guid]}"][0]
+ self.class.set_password(@resource.name, guid, passphrase)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail]
+ end
end
- # JJM: nameservice.rb defines methods for each attribute of the type.
- # We implement these methods here, by implementing get() and set()
- # See the resource_type= method defined in nameservice.rb
- # I'm not sure what the implications are of doing things this way.
- # It was a bit difficult to sort out what was happening in my head,
- # but ruby-debug makes this process much more transparent.
- #
- def set(property, value)
- # JJM: As it turns out, the set method defined in our parent class
- # is fine. It just calls the modifycmd() method, which
- # I'll implement here.
- super
- 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.
- def get(param)
- hash = getinfo(false)
- if hash
- return hash[param]
+ def set(param, value)
+ self.class.validate(param, value)
+ current_members = @property_value_cache_hash[:members]
+ if param == :members
+ # If we are meant to be authoritative for the group membership
+ # then remove all existing members who haven't been specified
+ # in the manifest.
+ if @resource[:auth_membership] and not current_members.nil?
+ remove_unwanted_members(current_members, value)
+ end
+
+ # if they're not a member, make them one.
+ add_members(current_members, value)
else
- return :absent
+ exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
+ # JJM: The following line just maps the NS name to the DS name
+ # e.g. { :uid => 'UniqueID' }
+ exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(param)]
+ # JJM: The following line sends the actual value to set the property to
+ exec_arg_vector << value.to_s
+ begin
+ execute(exec_arg_vector)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail]
+ end
end
end
- def modifycmd(property, value)
- # JJM: This method will assemble a exec vector which modifies
- # a single property and it's value using dscl.
- # JJM: With /usr/bin/dscl, the -create option will destroy an
- # existing property record if it exists
+ # 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.
+ def create
+ if exists?
+ info "already exists"
+ # The object already exists
+ return nil
+ end
+
+ # NBK: First we create the object with a known guid so we can set the contents
+ # of the password hash if required
+ # Shelling out sucks, but for a single use case it doesn't seem worth
+ # requiring people install a UUID library that doesn't come with the system.
+ # This should be revisited if Puppet starts managing UUIDs for other platform
+ # user records.
+ guid = %x{/usr/bin/uuidgen}.chomp
+
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
- # JJM: The following line just maps the NS name to the DS name
- # e.g. { :uid => 'UniqueID' }
- exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(property)]
- # JJM: The following line sends the actual value to set the property to
- exec_arg_vector << value.to_s
- return exec_arg_vector
+ exec_arg_vector << @@ns_to_ds_attribute_map[:guid] << guid
+ begin
+ execute(exec_arg_vector)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set GeneratedUID for %s %s: %s" %
+ [@resource.class.name, @resource.name, detail]
+ end
+
+ if value = @resource.should(:password) and value != ""
+ self.class.set_password(@resource[:name], guid, value)
+ end
+
+ # Now we create all the standard properties
+ Puppet::Type.type(@resource.class.name).validproperties.each do |property|
+ next if property == :ensure
+ if value = @resource.should(property) and value != ""
+ if property == :members
+ add_members(nil, value)
+ else
+ exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
+ exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(property)]
+ next if property == :password # skip setting the password here
+ exec_arg_vector << value.to_s
+ begin
+ execute(exec_arg_vector)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not create %s %s: %s" %
+ [@resource.class.name, @resource.name, detail]
+ end
+ end
+ end
+ end
end
- def addcmd
- # JJM 2007-07-24:
- # - addcmd returns an array to be executed to create a new object.
- # - This method is probably being called from the
- # ensure= method in nameservice.rb, or here...
- # - This should only be called if the object doesn't exist.
- # JJM: Blame nameservice.rb for the terse method name. =)
- #
- self.class.get_exec_preamble("-create", @resource[:name])
+ def remove_unwanted_members(current_members, new_members)
+ current_members.each do |member|
+ if not value.include?(member)
+ cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]]
+ begin
+ execute(cmd)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail]
+ end
+ end
+ end
end
+ def add_members(current_members, new_members)
+ new_members.each do |user|
+ if current_members.nil? or not current_members.include?(user)
+ cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", user, @resource[:name]]
+ begin
+ execute(cmd)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail]
+ end
+ end
+ end
+ end
+
def deletecmd
# JJM: Like addcmd, only called when deleting the object itself
# Note, this isn't used to delete properties of the object,
# at least that's how I understand it...
self.class.get_exec_preamble("-delete", @resource[:name])
@@ -339,11 +438,15 @@
#
# Ultimately, we add :name to the list, delete :ensure from the
# list, then report on the remaining list. Pretty whacky, ehh?
type_properties = [:name] + self.class.resource_type.validproperties
type_properties.delete(:ensure) if type_properties.include? :ensure
+ type_properties << :guid # append GeneratedUID so we just get the report here
@property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties)
+ [:uid, :gid].each do |param|
+ @property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param)
+ end
end
return @property_value_cache_hash
end
end
-end
+end
\ No newline at end of file