diff options
Diffstat (limited to 'lib/chef/provider/user/dscl.rb')
-rw-r--r-- | lib/chef/provider/user/dscl.rb | 684 |
1 files changed, 528 insertions, 156 deletions
diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb index 96b5db24ba..0c4ac27377 100644 --- a/lib/chef/provider/user/dscl.rb +++ b/lib/chef/provider/user/dscl.rb @@ -16,40 +16,193 @@ # limitations under the License. # +require 'mixlib/shellout' require 'chef/provider/user' require 'openssl' +require 'plist' class Chef class Provider class User + # + # The most tricky bit of this provider is the way it deals with user passwords. + # Mac OS X has different password shadow calculations based on the version. + # < 10.7 => password shadow calculation format SALTED-SHA1 + # => stored in: /var/db/shadow/hash/#{guid} + # => shadow binary length 68 bytes + # => First 4 bytes salt / Next 64 bytes shadow value + # = 10.7 => password shadow calculation format SALTED-SHA512 + # => stored in: /var/db/dslocal/nodes/Default/users/#{name}.plist + # => shadow binary length 68 bytes + # => First 4 bytes salt / Next 64 bytes shadow value + # > 10.7 => password shadow calculation format SALTED-SHA512-PBKDF2 + # => stored in: /var/db/dslocal/nodes/Default/users/#{name}.plist + # => shadow binary length 128 bytes + # => Salt / Iterations are stored seperately in the same file + # + # This provider only supports Mac OSX versions 10.7 and above class Dscl < Chef::Provider::User - NFS_HOME_DIRECTORY = %r{^NFSHomeDirectory: (.*)$} - AUTHENTICATION_AUTHORITY = %r{^AuthenticationAuthority: (.*)$} + def define_resource_requirements + super + + requirements.assert(:all_actions) do |a| + a.assertion { mac_osx_version_less_than_10_7? == false } + a.failure_message(Chef::Exceptions::User, "Chef::Provider::User::Dscl only supports Mac OS X versions 10.7 and above.") + end + + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/bin/dscl") } + a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/dscl' on the system for #{@new_resource}!") + end + + requirements.assert(:all_actions) do |a| + a.assertion { ::File.exists?("/usr/bin/plutil") } + a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/plutil' on the system for #{@new_resource}!") + end - def dscl(*args) - shell_out("dscl . -#{args.join(' ')}") + requirements.assert(:create, :modify, :manage) do |a| + # Password Requirements + a.assertion do + if @new_resource.password + if mac_osx_version_greater_than_10_7? + if salted_sha512?(@new_resource.password) + # SALTED-SHA512 password shadow hashes are not supported + false + elsif salted_sha512_pbkdf2?(@new_resource.password) + # salt and iterations should be specified when + # SALTED-SHA512-PBKDF2 password shadow hash is given + @new_resource.salt && @new_resource.iterations + else + true + end + else + # On 10.7 SALTED-SHA512-PBKDF2 is not supported + !salted_sha512_pbkdf2?(@new_resource.password) + end + else + true + end + end + a.failure_message(Chef::Exceptions::User, "Requirements for password is not achieved. Check \ + http://docs.getchef.com/resource_user.html#attributes for more information!") + end end - def safe_dscl(*args) - result = dscl(*args) - return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 ) - raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") unless result.exitstatus == 0 - raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") if result.stdout =~ /No such key: / - return result.stdout + def load_current_resource + @current_resource = Chef::Resource::User.new(@new_resource.username) + @current_resource.username(@new_resource.username) + + user_info = read_user_info + if user_info + @current_resource.uid(dscl_get(user_info, :uid)) + @current_resource.gid(dscl_get(user_info, :gid)) + @current_resource.home(dscl_get(user_info, :home)) + @current_resource.shell(dscl_get(user_info, :shell)) + @current_resource.comment(dscl_get(user_info, :comment)) + @authentication_authority = dscl_get(user_info, :auth_authority) + + if @new_resource.password && dscl_get(user_info, :password) == "********" + # A password is set. Let's get the password information from shadow file + shadow_hash_binary = dscl_get(user_info, :shadow_hash) + + # Calling shell_out directly since we want to give an input stream + shadow_hash_xml = convert_binary_plist_to_xml(shadow_hash_binary.string) + shadow_hash = Plist::parse_xml(shadow_hash_xml) + + if shadow_hash["SALTED-SHA512"] + # Convert the shadow value from Base64 encoding to hex before consuming them + @password_shadow_conversion_algorithm = "SALTED-SHA512" + @current_resource.password(shadow_hash["SALTED-SHA512"].string.unpack('H*').first) + elsif shadow_hash["SALTED-SHA512-PBKDF2"] + @password_shadow_conversion_algorithm = "SALTED-SHA512-PBKDF2" + # Convert the entropy from Base64 encoding to hex before consuming them + @current_resource.password(shadow_hash["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first) + @current_resource.iterations(shadow_hash["SALTED-SHA512-PBKDF2"]["iterations"]) + # Convert the salt from Base64 encoding to hex before consuming them + @current_resource.salt(shadow_hash["SALTED-SHA512-PBKDF2"]["salt"].string.unpack('H*').first) + else + raise(Chef::Exceptions::User,"Unknown shadow_hash format: #{shadow_hash.keys.join(' ')}") + end + end + + convert_group_name if @new_resource.gid + else + @user_exists = false + Chef::Log.debug("#{@new_resource} user does not exist") + end + + @current_resource + end + + # + # Provider Actions + # + + def create_user + dscl_create_user + dscl_create_comment + dscl_set_uid + dscl_set_gid + dscl_set_home + dscl_set_shell + set_password end - # This is handled in providers/group.rb by Etc.getgrnam() - # def user_exists?(user) - # users = safe_dscl("list /Users") - # !! ( users =~ Regexp.new("\n#{user}\n") ) - # end + def manage_user + dscl_create_user if diverged?(:username) + dscl_create_comment if diverged?(:comment) + dscl_set_uid if diverged?(:uid) + dscl_set_gid if diverged?(:gid) + dscl_set_home if diverged?(:home) + dscl_set_shell if diverged?(:shell) + set_password if diverged_password? + end + + # + # Action Helpers + # + + # + # Create a user using dscl + # + def dscl_create_user + run_dscl("create /Users/#{@new_resource.username}") + end + + # + # Saves the specified Chef user `comment` into RealName attribute + # of Mac user. + # + def dscl_create_comment + run_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'") + end + + # + # Sets the user id for the user using dscl. + # If a `uid` is not specified, it finds the next available one starting + # from 200 if `system` is set, 500 otherwise. + # + def dscl_set_uid + @new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '') + + if uid_used?(@new_resource.uid) + raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use") + end + + run_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}") + end - # get a free UID greater than 200 + # + # Find the next available uid on the system. starting with 200 if `system` is set, + # 500 otherwise. + # def get_free_uid(search_limit=1000) - uid = nil; next_uid_guess = 200 - users_uids = safe_dscl("list /Users uid") - while(next_uid_guess < search_limit + 200) + uid = nil + base_uid = @new_resource.system ? 200 : 500 + next_uid_guess = base_uid + users_uids = run_dscl("list /Users uid") + while(next_uid_guess < search_limit + base_uid) if users_uids =~ Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n") next_uid_guess += 1 else @@ -60,22 +213,41 @@ class Chef return uid || raise("uid not found. Exhausted. Searched #{search_limit} times") end + # + # Returns true if uid is in use by a different account, false otherwise. + # def uid_used?(uid) return false unless uid - users_uids = safe_dscl("list /Users uid") + users_uids = run_dscl("list /Users uid") !! ( users_uids =~ Regexp.new("#{Regexp.escape(uid.to_s)}\n") ) end - def set_uid - @new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '') - if uid_used?(@new_resource.uid) - raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use") + # + # Sets the group id for the user using dscl. Fails if a group doesn't + # exist on the system with given group id. + # + def dscl_set_gid + unless @new_resource.gid && @new_resource.gid.to_s.match(/^\d+$/) + begin + possible_gid = run_dscl("read /Groups/#{@new_resource.gid} PrimaryGroupID").split(" ").last + rescue Chef::Exceptions::DsclCommandFailed => e + raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{@new_resource.gid} when creating user #{@new_resource.username}") + end + @new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) end - safe_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}") + run_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'") end - def modify_home - return safe_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") if (@new_resource.home.nil? || @new_resource.home.empty?) + # + # Sets the home directory for the user. If `:manage_home` is set home + # directory is managed (moved / created) for the user. + # + def dscl_set_home + if @new_resource.home.nil? || @new_resource.home.empty? + run_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") + return + end + if @new_resource.supports[:manage_home] validate_home_dir_specification! @@ -87,199 +259,399 @@ class Chef move_home end end - safe_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'") + run_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'") end - def osx_shadow_hash?(string) - return !! ( string =~ /^[[:xdigit:]]{1240}$/ ) + def validate_home_dir_specification! + unless @new_resource.home =~ /^\// + raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'") + end + end + + def current_home_exists? + ::File.exist?("#{@current_resource.home}") + end + + def new_home_exists? + ::File.exist?("#{@new_resource.home}") end - def osx_salted_sha1?(string) - return !! ( string =~ /^[[:xdigit:]]{48}$/ ) + def ditto_home + skel = "/System/Library/User Template/English.lproj" + raise(Chef::Exceptions::User,"can't find skel at: #{skel}") unless ::File.exists?(skel) + shell_out! "ditto '#{skel}' '#{@new_resource.home}'" + ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) end - def guid - safe_dscl("read /Users/#{@new_resource.username} GeneratedUID").gsub(/GeneratedUID: /,"").strip + def move_home + Chef::Log.debug("#{@new_resource} moving #{self} home from #{@current_resource.home} to #{@new_resource.home}") + + src = @current_resource.home + FileUtils.mkdir_p(@new_resource.home) + files = ::Dir.glob("#{src}/*", ::File::FNM_DOTMATCH) - ["#{src}/.","#{src}/.."] + ::FileUtils.mv(files,@new_resource.home, :force => true) + ::FileUtils.rmdir(src) + ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) end - def shadow_hash_set? - user_data = safe_dscl("read /Users/#{@new_resource.username}") - if user_data =~ /AuthenticationAuthority: / && user_data =~ /ShadowHash/ - true + # + # Sets the shell for the user using dscl. + # + def dscl_set_shell + if @new_resource.shell || ::File.exists?("#{@new_resource.shell}") + run_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'") else - false + run_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'") end end - def modify_password - if @new_resource.password - shadow_hash = nil + # + # Sets the password for the user based on given password parameters. + # Chef supports specifying plain-text passwords and password shadow + # hash data. + # + def set_password + # Return if there is no password to set + return if @new_resource.password.nil? + + shadow_info = prepare_password_shadow_info + + # Shadow info is saved as binary plist. Convert the info to binary plist. + shadow_info_binary = StringIO.new + command = Mixlib::ShellOut.new("plutil -convert binary1 -o - -", + :input => shadow_info.to_plist, :live_stream => shadow_info_binary) + command.run_command + + # Replace the shadow info in user's plist + user_info = read_user_info + dscl_set(user_info, :shadow_hash, shadow_info_binary) + + # + # Before saving the user's plist file we need to wait for dscl to + # update its caches and flush them to disk. In order to achieve this + # we need to wait first for our changes to get into the dscl cache + # and then flush the cache to disk before saving password into the + # plist file. 3 seconds is the minimum experimental value for dscl + # cache to be updated. We can get rid of this sleep when we find a + # trigger to update dscl cache. + # + sleep 3 + shell_out("dscacheutil '-flushcache'") + save_user_info(user_info) + end - Chef::Log.debug("#{new_resource} updating password") - if osx_shadow_hash?(@new_resource.password) - shadow_hash = @new_resource.password.upcase + # + # Prepares the password shadow info based on the platform version. + # + def prepare_password_shadow_info + shadow_info = { } + entropy = nil + salt = nil + iterations = nil + + if mac_osx_version_10_7? + hash_value = if salted_sha512?(@new_resource.password) + @new_resource.password else - if osx_salted_sha1?(@new_resource.password) - salted_sha1 = @new_resource.password.upcase - else - hex_salt = "" - OpenSSL::Random.random_bytes(10).each_byte { |b| hex_salt << b.to_i.to_s(16) } - hex_salt = hex_salt.slice(0...8) - salt = [hex_salt].pack("H*") - sha1 = ::OpenSSL::Digest::SHA1.hexdigest(salt+@new_resource.password) - salted_sha1 = (hex_salt+sha1).upcase - end - shadow_hash = String.new("00000000"*155) - shadow_hash[168] = salted_sha1 + # Create a random 4 byte salt + salt = OpenSSL::Random.random_bytes(4) + encoded_password = OpenSSL::Digest::SHA512.hexdigest(salt + @new_resource.password) + hash_value = salt.unpack('H*').first + encoded_password end - ::File.open("/var/db/shadow/hash/#{guid}",'w',0600) do |output| - output.puts shadow_hash + shadow_info["SALTED-SHA512"] = StringIO.new + shadow_info["SALTED-SHA512"].string = convert_to_binary(hash_value) + shadow_info + else + if salted_sha512_pbkdf2?(@new_resource.password) + entropy = convert_to_binary(@new_resource.password) + salt = convert_to_binary(@new_resource.salt) + iterations = @new_resource.iterations + else + salt = OpenSSL::Random.random_bytes(32) + iterations = @new_resource.iterations # Use the default if not specified by the user + + entropy = OpenSSL::PKCS5::pbkdf2_hmac( + @new_resource.password, + salt, + iterations, + 128, + OpenSSL::Digest::SHA512.new + ) end - unless shadow_hash_set? - safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';ShadowHash;'") + pbkdf_info = { } + pbkdf_info["entropy"] = StringIO.new + pbkdf_info["entropy"].string = entropy + pbkdf_info["salt"] = StringIO.new + pbkdf_info["salt"].string = salt + pbkdf_info["iterations"] = iterations + + shadow_info["SALTED-SHA512-PBKDF2"] = pbkdf_info + end + + shadow_info + end + + # + # Removes the user from the system after removing user from his groups + # and deleting home directory if needed. + # + def remove_user + if @new_resource.supports[:manage_home] + # Remove home directory + FileUtils.rm_rf(@current_resource.home) + end + + # Remove the user from its groups + run_dscl("list /Groups").each_line do |group| + if member_of_group?(group.chomp) + run_dscl("delete /Groups/#{group.chomp} GroupMembership '#{@new_resource.username}'") end end + + # Remove user account + run_dscl("delete /Users/#{@new_resource.username}") end - def load_current_resource - super - raise Chef::Exceptions::User, "Could not find binary /usr/bin/dscl for #{@new_resource}" unless ::File.exists?("/usr/bin/dscl") + # + # Locks the user. + # + def lock_user + run_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'") end - def create_user - dscl_create_user - dscl_create_comment - set_uid - dscl_set_gid - modify_home - dscl_set_shell - modify_password + # + # Unlocks the user + # + def unlock_user + auth_string = @authentication_authority.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip + run_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'") end - def manage_user - dscl_create_user if diverged?(:username) - dscl_create_comment if diverged?(:comment) - set_uid if diverged?(:uid) - dscl_set_gid if diverged?(:gid) - modify_home if diverged?(:home) - dscl_set_shell if diverged?(:shell) - modify_password if diverged?(:password) + # + # Returns true if the user is locked, false otherwise. + # + def locked? + if @authentication_authority + !!(@authentication_authority =~ /DisabledUser/ ) + else + false + end end - def dscl_create_user - safe_dscl("create /Users/#{@new_resource.username}") + # + # This is the interface base User provider requires to provide idempotency. + # + def check_lock + return @locked = locked? end - def dscl_create_comment - safe_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'") + # + # Helper functions + # + + # + # Returns true if the system state and desired state is different for + # given attribute. + # + def diverged?(parameter) + parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?) end - def dscl_set_gid - unless @new_resource.gid && @new_resource.gid.to_s.match(/^\d+$/) - begin - possible_gid = safe_dscl("read /Groups/#{@new_resource.gid} PrimaryGroupID").split(" ").last - rescue Chef::Exceptions::DsclCommandFailed => e - raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{@new_resource.gid} when creating user #{@new_resource.username}") - end - @new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) - end - safe_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'") + def parameter_updated?(parameter) + not (@new_resource.send(parameter) == @current_resource.send(parameter)) end - def dscl_set_shell - if @new_resource.password || ::File.exists?("#{@new_resource.shell}") - safe_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'") + # + # We need a special check function for password since we support both + # plain text and shadow hash data. + # + # Checks if password needs update based on platform version and the + # type of the password specified. + # + def diverged_password? + return false if @new_resource.password.nil? + + # Dscl provider supports both plain text passwords and shadow hashes. + if mac_osx_version_10_7? + if salted_sha512?(@new_resource.password) + diverged?(:password) + else + !salted_sha512_password_match? + end else - safe_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'") + # When a system is upgraded to a version 10.7+ shadow hashes of the users + # will be updated when the user logs in. So it's possible that we will have + # SALTED-SHA512 password in the current_resource. In that case we will force + # password to be updated. + return true if salted_sha512?(@current_resource.password) + + if salted_sha512_pbkdf2?(@new_resource.password) + diverged?(:password) || diverged?(:salt) || diverged?(:iterations) + else + !salted_sha512_pbkdf2_password_match? + end end end - def remove_user - if @new_resource.supports[:manage_home] - user_info = safe_dscl("read /Users/#{@new_resource.username}") - if nfs_home_match = user_info.match(NFS_HOME_DIRECTORY) - #nfs_home = safe_dscl("read /Users/#{@new_resource.username} NFSHomeDirectory") - #nfs_home.gsub!(/NFSHomeDirectory: /,"").gsub!(/\n$/,"") - nfs_home = nfs_home_match[1] - FileUtils.rm_rf(nfs_home) - end - end - # remove the user from its groups - groups = [] - Etc.group do |group| - groups << group.name if group.mem.include?(@new_resource.username) + # + # Returns true if user is member of the specified group, false otherwise. + # + def member_of_group?(group_name) + membership_info = "" + begin + membership_info = run_dscl("read /Groups/#{group_name}") + rescue Chef::Exceptions::DsclCommandFailed + # Raised if the group doesn't contain any members end - groups.each do |group_name| - safe_dscl("delete /Groups/#{group_name} GroupMembership '#{@new_resource.username}'") - end - # remove user account - safe_dscl("delete /Users/#{@new_resource.username}") + # Output is something like: + # GroupMembership: root admin etc + members = membership_info.split(" ") + members.shift # Get rid of GroupMembership: string + members.include?(@new_resource.username) end - def locked? - user_info = safe_dscl("read /Users/#{@new_resource.username}") - if auth_authority_md = AUTHENTICATION_AUTHORITY.match(user_info) - !!(auth_authority_md[1] =~ /DisabledUser/ ) - else - false + # + # DSCL Helper functions + # + + # A simple map of Chef's terms to DSCL's terms. + DSCL_PROPERTY_MAP = { + :uid => "generateduid", + :gid => "gid", + :home => "home", + :shell => "shell", + :comment => "realname", + :password => "passwd", + :auth_authority => "authentication_authority", + :shadow_hash => "ShadowHashData" + }.freeze + + # Directory where the user plist files are stored for versions 10.7 and above + USER_PLIST_DIRECTORY = "/var/db/dslocal/nodes/Default/users".freeze + + # + # Reads the user plist and returns a hash keyed with DSCL properties specified + # in DSCL_PROPERTY_MAP. Return nil if the user is not found. + # + def read_user_info + user_info = nil + + begin + user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist" + user_plist_info = run_plutil("convert xml1 -o - #{user_plist_file}") + user_info = Plist::parse_xml(user_plist_info) + rescue Chef::Exceptions::PlutilCommandFailed end + + user_info end - def check_lock - return @locked = locked? + # + # Saves the given hash keyed with DSCL properties specified + # in DSCL_PROPERTY_MAP to the disk. + # + def save_user_info(user_info) + user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist" + Plist::Emit.save_plist(user_info, user_plist_file) + run_plutil("convert binary1 #{user_plist_file}") end - def lock_user - safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'") + # + # Sets a value in user information hash using Chef attributes as keys. + # + def dscl_set(user_hash, key, value) + raise "Unknown dscl key #{key}" unless DSCL_PROPERTY_MAP.keys.include?(key) + user_hash[DSCL_PROPERTY_MAP[key]] = [ value ] + user_hash end - def unlock_user - auth_info = safe_dscl("read /Users/#{@new_resource.username} AuthenticationAuthority") - auth_string = auth_info.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip#.gsub!(/[; ]*$/,"") - safe_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'") + # + # Gets a value from user information hash using Chef attributes as keys. + # + def dscl_get(user_hash, key) + raise "Unknown dscl key #{key}" unless DSCL_PROPERTY_MAP.keys.include?(key) + # DSCL values are set as arrays + value = user_hash[DSCL_PROPERTY_MAP[key]] + value.nil? ? value : value.first end - def validate_home_dir_specification! - unless @new_resource.home =~ /^\// - raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'") - end + # + # System Helpets + # + + def mac_osx_version + # This provider will only be invoked on node[:platform] == "mac_os_x" + # We do not check or assert that here. + node[:platform_version] end - def current_home_exists? - ::File.exist?("#{@current_resource.home}") + def mac_osx_version_10_7? + mac_osx_version.start_with?("10.7.") end - def new_home_exists? - ::File.exist?("#{@new_resource.home}") + def mac_osx_version_less_than_10_7? + versions = mac_osx_version.split(".") + # Make integer comparison in order not to report 10.10 less than 10.7 + (versions[0].to_i <= 10 && versions[1].to_i < 7) end - def ditto_home - skel = "/System/Library/User Template/English.lproj" - raise(Chef::Exceptions::User,"can't find skel at: #{skel}") unless ::File.exists?(skel) - shell_out! "ditto '#{skel}' '#{@new_resource.home}'" - ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) + def mac_osx_version_greater_than_10_7? + versions = mac_osx_version.split(".") + # Make integer comparison in order not to report 10.10 less than 10.7 + (versions[0].to_i >= 10 && versions[1].to_i > 7) end - def move_home - Chef::Log.debug("#{@new_resource} moving #{self} home from #{@current_resource.home} to #{@new_resource.home}") + def run_dscl(*args) + result = shell_out("dscl . -#{args.join(' ')}") + return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 ) + raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") unless result.exitstatus == 0 + raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") if result.stdout =~ /No such key: / + result.stdout + end - src = @current_resource.home - FileUtils.mkdir_p(@new_resource.home) - files = ::Dir.glob("#{src}/*", ::File::FNM_DOTMATCH) - ["#{src}/.","#{src}/.."] - ::FileUtils.mv(files,@new_resource.home, :force => true) - ::FileUtils.rmdir(src) - ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) + def run_plutil(*args) + result = shell_out("plutil -#{args.join(' ')}") + raise(Chef::Exceptions::PlutilCommandFailed,"plutil error: #{result.inspect}") unless result.exitstatus == 0 + result.stdout end - def diverged?(parameter) - parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?) + def convert_binary_plist_to_xml(binary_plist_string) + Mixlib::ShellOut.new("plutil -convert xml1 -o - -", :input => binary_plist_string).run_command.stdout end - def parameter_updated?(parameter) - not (@new_resource.send(parameter) == @current_resource.send(parameter)) + def convert_to_binary(string) + string.unpack('a2'*(string.size/2)).collect { |i| i.hex.chr }.join + end + + def salted_sha512?(string) + !!(string =~ /^[[:xdigit:]]{136}$/) + end + + def salted_sha512_password_match? + # Salt is included in the first 4 bytes of shadow data + salt = @current_resource.password.slice(0,8) + shadow = OpenSSL::Digest::SHA512.hexdigest(convert_to_binary(salt) + @new_resource.password) + @current_resource.password == salt + shadow end + + def salted_sha512_pbkdf2?(string) + !!(string =~ /^[[:xdigit:]]{256}$/) + end + + def salted_sha512_pbkdf2_password_match? + salt = convert_to_binary(@current_resource.salt) + + OpenSSL::PKCS5::pbkdf2_hmac( + @new_resource.password, + salt, + @current_resource.iterations, + 128, + OpenSSL::Digest::SHA512.new + ).unpack('H*').first == @current_resource.password + end + end end end |