From ebe9a7f262f23ff7bd9f94afa9b0c1a07cbd2a73 Mon Sep 17 00:00:00 2001 From: Serdar Sutay Date: Wed, 30 Jul 2014 16:03:42 -0700 Subject: * Dscl user provider changes to support Mac 10.7, 10.8 & 10.9. * Make the dscl user provider password handling idempotent. * Refactor / modernize dscl user provider unit tests. * Functional tests for dscl user provider. --- .gitignore | 4 + chef.gemspec | 3 +- lib/chef/exceptions.rb | 1 + lib/chef/provider/user/dscl.rb | 684 ++++++++++++++----- lib/chef/resource/user.rb | 18 + spec/data/mac_users/10.7-8.plist.xml | 559 ++++++++++++++++ spec/data/mac_users/10.7-8.shadow.xml | 11 + spec/data/mac_users/10.7.plist.xml | 559 ++++++++++++++++ spec/data/mac_users/10.7.shadow.xml | 11 + spec/data/mac_users/10.8.plist.xml | 559 ++++++++++++++++ spec/data/mac_users/10.8.shadow.xml | 21 + spec/data/mac_users/10.9.plist.xml | 560 ++++++++++++++++ spec/data/mac_users/10.9.shadow.xml | 21 + spec/functional/resource/user/dscl_spec.rb | 198 ++++++ spec/functional/resource/user/useradd_spec.rb | 685 +++++++++++++++++++ spec/functional/resource/user_spec.rb | 685 ------------------- spec/unit/provider/user/dscl_spec.rb | 926 ++++++++++++++++++-------- spec/unit/provider/user/useradd_spec.rb | 1 + 18 files changed, 4400 insertions(+), 1106 deletions(-) create mode 100644 spec/data/mac_users/10.7-8.plist.xml create mode 100644 spec/data/mac_users/10.7-8.shadow.xml create mode 100644 spec/data/mac_users/10.7.plist.xml create mode 100644 spec/data/mac_users/10.7.shadow.xml create mode 100644 spec/data/mac_users/10.8.plist.xml create mode 100644 spec/data/mac_users/10.8.shadow.xml create mode 100644 spec/data/mac_users/10.9.plist.xml create mode 100644 spec/data/mac_users/10.9.shadow.xml create mode 100644 spec/functional/resource/user/dscl_spec.rb create mode 100644 spec/functional/resource/user/useradd_spec.rb delete mode 100644 spec/functional/resource/user_spec.rb diff --git a/.gitignore b/.gitignore index 9ce3eecbe3..a9e4338e2a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ doc/ # Test Kitchen .kitchen/ Berksfile.lock + +# Vagrant +Vagrantfile +.vagrant/ diff --git a/chef.gemspec b/chef.gemspec index dc40690c33..f8e71a43e1 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -30,9 +30,10 @@ Gem::Specification.new do |s| s.add_dependency "diff-lcs", "~> 1.2", ">= 1.2.4" s.add_dependency "chef-zero", "~> 2.2" - s.add_dependency "pry", "~> 0.9" + s.add_dependency 'plist', '~> 3.1.0' + s.add_development_dependency "rack" # Rake 10.2 drops Ruby 1.8 support diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index 194c758f37..0d86b08558 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -83,6 +83,7 @@ class Chef class RequestedUIDUnavailable < RuntimeError; end class InvalidHomeDirectory < ArgumentError; end class DsclCommandFailed < RuntimeError; end + class PlutilCommandFailed < RuntimeError; end class UserIDNotFound < ArgumentError; end class GroupIDNotFound < ArgumentError; end class ConflictingMembersInGroup < ArgumentError; end 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 diff --git a/lib/chef/resource/user.rb b/lib/chef/resource/user.rb index 05c076229f..9d6e857de7 100644 --- a/lib/chef/resource/user.rb +++ b/lib/chef/resource/user.rb @@ -45,6 +45,8 @@ class Chef :manage_home => false, :non_unique => false } + @iterations = 27855 + @salt = nil @allowed_actions.push(:create, :remove, :modify, :manage, :lock, :unlock) end @@ -106,6 +108,22 @@ class Chef ) end + def salt(arg=nil) + set_or_return( + :salt, + arg, + :kind_of => [ String ] + ) + end + + def iterations(arg=nil) + set_or_return( + :iterations, + arg, + :kind_of => [ Integer ] + ) + end + def system(arg=nil) set_or_return( :system, diff --git a/spec/data/mac_users/10.7-8.plist.xml b/spec/data/mac_users/10.7-8.plist.xml new file mode 100644 index 0000000000..4ed294eb38 --- /dev/null +++ b/spec/data/mac_users/10.7-8.plist.xml @@ -0,0 +1,559 @@ + + + + + KerberosKeys + + + MIIBVKEDAgEBoIIBSzCCAUcwc6ErMCmgAwIBEqEiBCBxHUxawMNiov49kfZn + M38ddgXFivE9SNpYgPamy+6prKJEMEKgAwIBA6E7BDlMS0RDOlNIQTEuNEVB + OUJFMDZCQzExQjAxODdEMzQ1MkI3QTA5NjE3QjBCOTI2OTY4RXZhZ3JhbnQw + Y6EbMBmgAwIBEaESBBBD9mGbvFTNIUKAvAbnjh8ookQwQqADAgEDoTsEOUxL + REM6U0hBMS40RUE5QkUwNkJDMTFCMDE4N0QzNDUyQjdBMDk2MTdCMEI5MjY5 + NjhFdmFncmFudDBroSMwIaADAgEQoRoEGG4TEFIf416UH7MvFW7sAXC8ArC6 + AhbCraJEMEKgAwIBA6E7BDlMS0RDOlNIQTEuNEVBOUJFMDZCQzExQjAxODdE + MzQ1MkI3QTA5NjE3QjBCOTI2OTY4RXZhZ3JhbnQ= + + + ShadowHashData + + + YnBsaXN0MDDRAQJfEBRTQUxURUQtU0hBNTEyLVBCS0RGMtMDBAUGBwhXZW50 + cm9weVRzYWx0Wml0ZXJhdGlvbnNPEIDqTC0mXYAboOwN/M0lPfwd6Ry+CAa0 + rMHtf+Iq689r61NE0PRC5ZD/oE1nkHXaOvsRnkG3K16vCO5KpUaTciZG1Rnu + BIQ964o+l3Qo0z9iXoOIeRPlwTtwA1lhXgCte8PnoMmK/D4Z0TYCckVPjTOp + IU0vvovmjR+YIbJmiTEjZk8QIPmU7y9zt8VZTr0VUzAJdrIHM84OJNZZeD2H + 89gcu7apEZugCAsiKTE2QcTnAAAAAAAAAQEAAAAAAAAACQAAAAAAAAAAAAAA + AAAAAOo= + + + _writers_hint + + vagrant + + _writers_jpegphoto + + vagrant + + _writers_passwd + + vagrant + + _writers_picture + + vagrant + + authentication_authority + + ;ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2> + ;Kerberosv5;;vagrant@LKDC:SHA1.4EA9BE06BC11B0187D3452B7A09617B0B926968E;LKDC:SHA1.4EA9BE06BC11B0187D3452B7A09617B0B926968E + + generateduid + + 11112222-3333-4444-AAAA-BBBBCCCCDDDD + + gid + + 80 + + home + + /Users/vagrant + + jpegphoto + + + /9j/4AAQSkZJRgABAQAAAQABAAD/4ge4SUNDX1BST0ZJTEUAAQEAAAeoYXBw + bAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBs + AAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAA + AG9kc2NtAAABeAAABWxjcHJ0AAAG5AAAADh3dHB0AAAHHAAAABRyWFlaAAAH + MAAAABRnWFlaAAAHRAAAABRiWFlaAAAHWAAAABRyVFJDAAAHbAAAAA5jaGFk + AAAHfAAAACxiVFJDAAAHbAAAAA5nVFJDAAAHbAAAAA5kZXNjAAAAAAAAABRH + ZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAUR2VuZXJpYyBSR0IgUHJv + ZmlsZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAbWx1YwAAAAAAAAAeAAAADHNrU0sAAAAoAAABeGhySFIAAAAo + AAABoGNhRVMAAAAkAAAByHB0QlIAAAAmAAAB7HVrVUEAAAAqAAACEmZyRlUA + AAAoAAACPHpoVFcAAAAWAAACZGl0SVQAAAAoAAACem5iTk8AAAAmAAAComtv + S1IAAAAWAAACyGNzQ1oAAAAiAAAC3mhlSUwAAAAeAAADAGRlREUAAAAsAAAD + Hmh1SFUAAAAoAAADSnN2U0UAAAAmAAAConpoQ04AAAAWAAADcmphSlAAAAAa + AAADiHJvUk8AAAAkAAADomVsR1IAAAAiAAADxnB0UE8AAAAmAAAD6G5sTkwA + AAAoAAAEDmVzRVMAAAAmAAAD6HRoVEgAAAAkAAAENnRyVFIAAAAiAAAEWmZp + RkkAAAAoAAAEfHBsUEwAAAAsAAAEpHJ1UlUAAAAiAAAE0GFyRUcAAAAmAAAE + 8mVuVVMAAAAmAAAFGGRhREsAAAAuAAAFPgBWAWEAZQBvAGIAZQBjAG4A/QAg + AFIARwBCACAAcAByAG8AZgBpAGwARwBlAG4AZQByAGkBDQBrAGkAIABSAEcA + QgAgAHAAcgBvAGYAaQBsAFAAZQByAGYAaQBsACAAUgBHAEIAIABnAGUAbgDo + AHIAaQBjAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8E + FwQwBDMEMAQ7BEwEPQQ4BDkAIAQ/BEAEPgREBDAEOQQ7ACAAUgBHAEIAUABy + AG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCkBp1KAAgAFIA + RwBCACCCcl9pY8+P8ABQAHIAbwBmAGkAbABvACAAUgBHAEIAIABnAGUAbgBl + AHIAaQBjAG8ARwBlAG4AZQByAGkAcwBrACAAUgBHAEIALQBwAHIAbwBmAGkA + bMd8vBgAIABSAEcAQgAg1QS4XNMMx3wATwBiAGUAYwBuAP0AIABSAEcAQgAg + AHAAcgBvAGYAaQBsBeQF6AXVBeQF2QXcACAAUgBHAEIAIAXbBdwF3AXZAEEA + bABsAGcAZQBtAGUAaQBuAGUAcwAgAFIARwBCAC0AUAByAG8AZgBpAGwAwQBs + AHQAYQBsAOEAbgBvAHMAIABSAEcAQgAgAHAAcgBvAGYAaQBsZm6QGgAgAFIA + RwBCACBjz4/wZYdO9k4AgiwAIABSAEcAQgAgMNcw7TDVMKEwpDDrAFAAcgBv + AGYAaQBsACAAUgBHAEIAIABnAGUAbgBlAHIAaQBjA5MDtQO9A7kDugPMACAD + wAPBA78DxgOvA7sAIABSAEcAQgBQAGUAcgBmAGkAbAAgAFIARwBCACAAZwBl + AG4A6QByAGkAYwBvAEEAbABnAGUAbQBlAGUAbgAgAFIARwBCAC0AcAByAG8A + ZgBpAGUAbA5CDhsOIw5EDh8OJQ5MACAAUgBHAEIAIA4XDjEOSA4nDkQOGwBH + AGUAbgBlAGwAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUA + bgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAFUAbgBpAHcAZQByAHMAYQBs + AG4AeQAgAHAAcgBvAGYAaQBsACAAUgBHAEIEHgQxBEkEOAQ5ACAEPwRABD4E + RAQ4BDsETAAgAFIARwBCBkUGRAZBACAGKgY5BjEGSgZBACAAUgBHAEIAIAYn + BkQGOQYnBkUARwBlAG4AZQByAGkAYwAgAFIARwBCACAAUAByAG8AZgBpAGwA + ZQBHAGUAbgBlAHIAZQBsACAAUgBHAEIALQBiAGUAcwBrAHIAaQB2AGUAbABz + AGV0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdo + dHMgcmVzZXJ2ZWQuAFhZWiAAAAAAAADzUgABAAAAARbPWFlaIAAAAAAAAHRN + AAA97gAAA9BYWVogAAAAAAAAWnUAAKxzAAAXNFhZWiAAAAAAAAAoGgAAFZ8A + ALg2Y3VydgAAAAAAAAABAc0AAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA + /ZH///ui///9owAAA9wAAMBs/9sAQwACAgICAgECAgICAgICAwMGBAMDAwMH + BQUEBggHCAgIBwgICQoNCwkJDAoICAsPCwwNDg4ODgkLEBEPDhENDg4O/9sA + QwECAgIDAwMGBAQGDgkICQ4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O + Dg4ODg4ODg4ODg4ODg4ODg4ODg4O/8AAEQgBMQEuAwEiAAIRAQMRAf/EAB8A + AAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE + AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYX + GBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3 + eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfI + ycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEB + AQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMR + BAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJico + KSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG + h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW + 19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A/fyiis6/1Wz0 + 6P8AfyAyY+WJeWP4dvxoA0aw9R1+zsN0at9puR/Ah4H1NcjqHiG9vQ0cZ+yQ + H+FD8x+p/wAK5+gDWu9a1C8u1kad4gpyiRkqF/z71u6d4pI2xaiu4dPOQc/i + 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 + By602uefd9PRdPxfmFFFFfJH1oUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU + UUAFFFFABRRRQAUUUUAFFFFABRRRQAV5L8Ufgj8O/i7obW/i7RI21JY9ttq9 + piK9t/TbJj5h/suGX2r1qit8Niq2HqKpSk4yXVaHPisLRxNJ060VKL6PVH4s + fGP9kX4ifDIXWsaJHJ448IxksbywhP2m2X/ptCMnAHV13L3O3pXydX9LFfK/ + xj/ZL+HXxSF1q2mQp4K8Xvlv7R0+EeTct/03h4Dc9WXa3qT0r9a4f8TNqWYL + /t9fqv1X3H5HxD4Yb1cvf/bj/R/o/vPxl0H/AJHvRf8Ar9j/AJ19B5YPlSQc + 8EVzvjX4FfEb4QfFTR4vFWjPJpLagi22s2OZbOf5uPnx8jH+64VvYjmvrD4b + /s1+MPGbQalr6yeE/D74YSXEf+lTr/sRHoCP4mx6gNX3+YZ5gKdCOJlVXI1o + 73v6d35HwGW5FmFSvLDKk+dbq1reb7LzPlG4+Hsfi3W4rXRLG5TX7l9sSWMB + k89vRo16/VcGvtv4M/si6jB4O0ib4o3S2fkR86Pp8253+Yn95KPujn7q5P8A + tA19keB/hp4O+Hmj/ZfDOkx287qFnvpv3lzP/vyHnH+yML6Cu8r8oz3xDxOI + TpYT3I938Xy7fi/Q/Wci8OsNh5Kti/fl2Xw/Pv8AgvJmPoegaL4Z8Ow6ToGm + Wek6dF9yC2jCrnuT3LHuTknvWxRRX5xOcpycpO7Z+kQhGEVGKskFFFFSUFFF + FABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU + AFFFFABRRRQAUUUUAYHib/kUZP8Ar5t//R8db9FFbS/hL1f6GS/iv0X6hRRR + WJqFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//2Q== + + + name + + vagrant + + passwd + + ******** + + passwordpolicyoptions + + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU + WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO + IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w + LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+ZmFp + bGVkTG9naW5Db3VudDwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4KCTxr + ZXk+ZmFpbGVkTG9naW5UaW1lc3RhbXA8L2tleT4KCTxkYXRlPjIwMDEtMDEt + MDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtleT5sYXN0TG9naW5UaW1lc3RhbXA8 + L2tleT4KCTxkYXRlPjIwMDEtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgo8L2Rp + Y3Q+CjwvcGxpc3Q+Cg== + + + realname + + vagrant + + shell + + /bin/bash + + uid + + 501 + + + diff --git a/spec/data/mac_users/10.7-8.shadow.xml b/spec/data/mac_users/10.7-8.shadow.xml new file mode 100644 index 0000000000..8c3b6dd3d7 --- /dev/null +++ b/spec/data/mac_users/10.7-8.shadow.xml @@ -0,0 +1,11 @@ + + + + + SALTED-SHA512 + + b3XXGQRB+sw0KR676h/HVrJC1P6bz/FBvMuE8ZeeJ+U5U5qjH599zJLAzqlZ6hjhi3IO + NY5/vjz76qVhRW9roAiTejA= + + + diff --git a/spec/data/mac_users/10.7.plist.xml b/spec/data/mac_users/10.7.plist.xml new file mode 100644 index 0000000000..5c7a98fada --- /dev/null +++ b/spec/data/mac_users/10.7.plist.xml @@ -0,0 +1,559 @@ + + + + + KerberosKeys + + + MIIBVKEDAgEBoIIBSzCCAUcwc6ErMCmgAwIBEqEiBCCBdluECwTg7Fe5bsZ+ + kxWTdvLPPtNGBCZOK2+aEFrkBaJEMEKgAwIBA6E7BDlMS0RDOlNIQTEuREZG + QTkxRjM1QjUxNjMzMDNDMDc5RTk5ODc2NDAzMEQwOTU2QUYyNnZhZ3JhbnQw + Y6EbMBmgAwIBEaESBBAHZXv8koch6fiOdgRkDXyjokQwQqADAgEDoTsEOUxL + REM6U0hBMS5ERkZBOTFGMzVCNTE2MzMwM0MwNzlFOTk4NzY0MDMwRDA5NTZB + RjI2dmFncmFudDBroSMwIaADAgEQoRoEGKs+5dPs07zLf/0Vhu+YWCXZ6iwg + NLpkqKJEMEKgAwIBA6E7BDlMS0RDOlNIQTEuREZGQTkxRjM1QjUxNjMzMDND + MDc5RTk5ODc2NDAzMEQwOTU2QUYyNnZhZ3JhbnQ= + + + ShadowHashData + + + YnBsaXN0MDDRAQJdU0FMVEVELVNIQTUxMk8QRG911xkEQfrMNCkeu+ofx1ay + QtT+m8/xQbzLhPGXniflOVOaox+ffcySwM6pWeoY4YtyDjWOf748++qlYUVv + a6AIk3owCAsZAAAAAAAAAQEAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAGA= + + + _writers_LinkedIdentity + + vagrant + + _writers_hint + + vagrant + + _writers_jpegphoto + + vagrant + + _writers_passwd + + vagrant + + _writers_picture + + vagrant + + authentication_authority + + ;ShadowHash;HASHLIST:<SALTED-SHA512> + ;Kerberosv5;;vagrant@LKDC:SHA1.DFFA91F35B5163303C079E998764030D0956AF26;LKDC:SHA1.DFFA91F35B5163303C079E998764030D0956AF26 + + generateduid + + 11112222-3333-4444-AAAA-BBBBCCCCDDDD + + gid + + 80 + + home + + /Users/vagrant + + jpegphoto + + + /9j/4AAQSkZJRgABAQAAAQABAAD/4ge4SUNDX1BST0ZJTEUAAQEAAAeoYXBw + bAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBs + AAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAA + AG9kc2NtAAABeAAABWxjcHJ0AAAG5AAAADh3dHB0AAAHHAAAABRyWFlaAAAH + MAAAABRnWFlaAAAHRAAAABRiWFlaAAAHWAAAABRyVFJDAAAHbAAAAA5jaGFk + AAAHfAAAACxiVFJDAAAHbAAAAA5nVFJDAAAHbAAAAA5kZXNjAAAAAAAAABRH + ZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAUR2VuZXJpYyBSR0IgUHJv + ZmlsZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAbWx1YwAAAAAAAAAeAAAADHNrU0sAAAAoAAABeGhySFIAAAAo + AAABoGNhRVMAAAAkAAAByHB0QlIAAAAmAAAB7HVrVUEAAAAqAAACEmZyRlUA + AAAoAAACPHpoVFcAAAAWAAACZGl0SVQAAAAoAAACem5iTk8AAAAmAAAComtv + S1IAAAAWAAACyGNzQ1oAAAAiAAAC3mhlSUwAAAAeAAADAGRlREUAAAAsAAAD + Hmh1SFUAAAAoAAADSnN2U0UAAAAmAAAConpoQ04AAAAWAAADcmphSlAAAAAa + AAADiHJvUk8AAAAkAAADomVsR1IAAAAiAAADxnB0UE8AAAAmAAAD6G5sTkwA + AAAoAAAEDmVzRVMAAAAmAAAD6HRoVEgAAAAkAAAENnRyVFIAAAAiAAAEWmZp + RkkAAAAoAAAEfHBsUEwAAAAsAAAEpHJ1UlUAAAAiAAAE0GFyRUcAAAAmAAAE + 8mVuVVMAAAAmAAAFGGRhREsAAAAuAAAFPgBWAWEAZQBvAGIAZQBjAG4A/QAg + AFIARwBCACAAcAByAG8AZgBpAGwARwBlAG4AZQByAGkBDQBrAGkAIABSAEcA + QgAgAHAAcgBvAGYAaQBsAFAAZQByAGYAaQBsACAAUgBHAEIAIABnAGUAbgDo + AHIAaQBjAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8E + FwQwBDMEMAQ7BEwEPQQ4BDkAIAQ/BEAEPgREBDAEOQQ7ACAAUgBHAEIAUABy + AG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCkBp1KAAgAFIA + RwBCACCCcl9pY8+P8ABQAHIAbwBmAGkAbABvACAAUgBHAEIAIABnAGUAbgBl + AHIAaQBjAG8ARwBlAG4AZQByAGkAcwBrACAAUgBHAEIALQBwAHIAbwBmAGkA + bMd8vBgAIABSAEcAQgAg1QS4XNMMx3wATwBiAGUAYwBuAP0AIABSAEcAQgAg + AHAAcgBvAGYAaQBsBeQF6AXVBeQF2QXcACAAUgBHAEIAIAXbBdwF3AXZAEEA + bABsAGcAZQBtAGUAaQBuAGUAcwAgAFIARwBCAC0AUAByAG8AZgBpAGwAwQBs + AHQAYQBsAOEAbgBvAHMAIABSAEcAQgAgAHAAcgBvAGYAaQBsZm6QGgAgAFIA + RwBCACBjz4/wZYdO9k4AgiwAIABSAEcAQgAgMNcw7TDVMKEwpDDrAFAAcgBv + AGYAaQBsACAAUgBHAEIAIABnAGUAbgBlAHIAaQBjA5MDtQO9A7kDugPMACAD + wAPBA78DxgOvA7sAIABSAEcAQgBQAGUAcgBmAGkAbAAgAFIARwBCACAAZwBl + AG4A6QByAGkAYwBvAEEAbABnAGUAbQBlAGUAbgAgAFIARwBCAC0AcAByAG8A + ZgBpAGUAbA5CDhsOIw5EDh8OJQ5MACAAUgBHAEIAIA4XDjEOSA4nDkQOGwBH + AGUAbgBlAGwAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUA + bgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAFUAbgBpAHcAZQByAHMAYQBs + AG4AeQAgAHAAcgBvAGYAaQBsACAAUgBHAEIEHgQxBEkEOAQ5ACAEPwRABD4E + RAQ4BDsETAAgAFIARwBCBkUGRAZBACAGKgY5BjEGSgZBACAAUgBHAEIAIAYn + BkQGOQYnBkUARwBlAG4AZQByAGkAYwAgAFIARwBCACAAUAByAG8AZgBpAGwA + ZQBHAGUAbgBlAHIAZQBsACAAUgBHAEIALQBiAGUAcwBrAHIAaQB2AGUAbABz + AGV0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdo + dHMgcmVzZXJ2ZWQuAFhZWiAAAAAAAADzUgABAAAAARbPWFlaIAAAAAAAAHRN + AAA97gAAA9BYWVogAAAAAAAAWnUAAKxzAAAXNFhZWiAAAAAAAAAoGgAAFZ8A + ALg2Y3VydgAAAAAAAAABAc0AAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA + /ZH///ui///9owAAA9wAAMBs/9sAQwACAgICAgECAgICAgICAwMGBAMDAwMH + BQUEBggHCAgIBwgICQoNCwkJDAoICAsPCwwNDg4ODgkLEBEPDhENDg4O/9sA + QwECAgIDAwMGBAQGDgkICQ4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O + Dg4ODg4ODg4ODg4ODg4ODg4ODg4O/8AAEQgBMQEuAwEiAAIRAQMRAf/EAB8A + AAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE + AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYX + GBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3 + eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfI + ycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEB + AQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMR + BAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJico + KSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG + h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW + 19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A/fyiis6/1Wz0 + 6P8AfyAyY+WJeWP4dvxoA0aw9R1+zsN0at9puR/Ah4H1NcjqHiG9vQ0cZ+yQ + H+FD8x+p/wAK5+gDWu9a1C8u1kad4gpyiRkqF/z71u6d4pI2xaiu4dPOQc/i + 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 + By602uefd9PRdPxfmFFFFfJH1oUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU + UUAFFFFABRRRQAUUUUAFFFFABRRRQAV5L8Ufgj8O/i7obW/i7RI21JY9ttq9 + piK9t/TbJj5h/suGX2r1qit8Niq2HqKpSk4yXVaHPisLRxNJ060VKL6PVH4s + fGP9kX4ifDIXWsaJHJ448IxksbywhP2m2X/ptCMnAHV13L3O3pXydX9LFfK/ + xj/ZL+HXxSF1q2mQp4K8Xvlv7R0+EeTct/03h4Dc9WXa3qT0r9a4f8TNqWYL + /t9fqv1X3H5HxD4Yb1cvf/bj/R/o/vPxl0H/AJHvRf8Ar9j/AJ19B5YPlSQc + 8EVzvjX4FfEb4QfFTR4vFWjPJpLagi22s2OZbOf5uPnx8jH+64VvYjmvrD4b + /s1+MPGbQalr6yeE/D74YSXEf+lTr/sRHoCP4mx6gNX3+YZ5gKdCOJlVXI1o + 73v6d35HwGW5FmFSvLDKk+dbq1reb7LzPlG4+Hsfi3W4rXRLG5TX7l9sSWMB + k89vRo16/VcGvtv4M/si6jB4O0ib4o3S2fkR86Pp8253+Yn95KPujn7q5P8A + tA19keB/hp4O+Hmj/ZfDOkx287qFnvpv3lzP/vyHnH+yML6Cu8r8oz3xDxOI + TpYT3I938Xy7fi/Q/Wci8OsNh5Kti/fl2Xw/Pv8AgvJmPoegaL4Z8Ow6ToGm + Wek6dF9yC2jCrnuT3LHuTknvWxRRX5xOcpycpO7Z+kQhGEVGKskFFFFSUFFF + FABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU + AFFFFABRRRQAUUUUAYHib/kUZP8Ar5t//R8db9FFbS/hL1f6GS/iv0X6hRRR + WJqFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//2Q== + + + name + + vagrant + + passwd + + ******** + + passwordpolicyoptions + + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU + WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO + IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w + LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+ZmFp + bGVkTG9naW5Db3VudDwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4KCTxr + ZXk+ZmFpbGVkTG9naW5UaW1lc3RhbXA8L2tleT4KCTxkYXRlPjIwMDEtMDEt + MDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtleT5sYXN0TG9naW5UaW1lc3RhbXA8 + L2tleT4KCTxkYXRlPjIwMDEtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgo8L2Rp + Y3Q+CjwvcGxpc3Q+Cg== + + + realname + + vagrant + + shell + + /bin/bash + + uid + + 501 + + + diff --git a/spec/data/mac_users/10.7.shadow.xml b/spec/data/mac_users/10.7.shadow.xml new file mode 100644 index 0000000000..8c3b6dd3d7 --- /dev/null +++ b/spec/data/mac_users/10.7.shadow.xml @@ -0,0 +1,11 @@ + + + + + SALTED-SHA512 + + b3XXGQRB+sw0KR676h/HVrJC1P6bz/FBvMuE8ZeeJ+U5U5qjH599zJLAzqlZ6hjhi3IO + NY5/vjz76qVhRW9roAiTejA= + + + diff --git a/spec/data/mac_users/10.8.plist.xml b/spec/data/mac_users/10.8.plist.xml new file mode 100644 index 0000000000..4ed294eb38 --- /dev/null +++ b/spec/data/mac_users/10.8.plist.xml @@ -0,0 +1,559 @@ + + + + + KerberosKeys + + + MIIBVKEDAgEBoIIBSzCCAUcwc6ErMCmgAwIBEqEiBCBxHUxawMNiov49kfZn + M38ddgXFivE9SNpYgPamy+6prKJEMEKgAwIBA6E7BDlMS0RDOlNIQTEuNEVB + OUJFMDZCQzExQjAxODdEMzQ1MkI3QTA5NjE3QjBCOTI2OTY4RXZhZ3JhbnQw + Y6EbMBmgAwIBEaESBBBD9mGbvFTNIUKAvAbnjh8ookQwQqADAgEDoTsEOUxL + REM6U0hBMS40RUE5QkUwNkJDMTFCMDE4N0QzNDUyQjdBMDk2MTdCMEI5MjY5 + NjhFdmFncmFudDBroSMwIaADAgEQoRoEGG4TEFIf416UH7MvFW7sAXC8ArC6 + AhbCraJEMEKgAwIBA6E7BDlMS0RDOlNIQTEuNEVBOUJFMDZCQzExQjAxODdE + MzQ1MkI3QTA5NjE3QjBCOTI2OTY4RXZhZ3JhbnQ= + + + ShadowHashData + + + YnBsaXN0MDDRAQJfEBRTQUxURUQtU0hBNTEyLVBCS0RGMtMDBAUGBwhXZW50 + cm9weVRzYWx0Wml0ZXJhdGlvbnNPEIDqTC0mXYAboOwN/M0lPfwd6Ry+CAa0 + rMHtf+Iq689r61NE0PRC5ZD/oE1nkHXaOvsRnkG3K16vCO5KpUaTciZG1Rnu + BIQ964o+l3Qo0z9iXoOIeRPlwTtwA1lhXgCte8PnoMmK/D4Z0TYCckVPjTOp + IU0vvovmjR+YIbJmiTEjZk8QIPmU7y9zt8VZTr0VUzAJdrIHM84OJNZZeD2H + 89gcu7apEZugCAsiKTE2QcTnAAAAAAAAAQEAAAAAAAAACQAAAAAAAAAAAAAA + AAAAAOo= + + + _writers_hint + + vagrant + + _writers_jpegphoto + + vagrant + + _writers_passwd + + vagrant + + _writers_picture + + vagrant + + authentication_authority + + ;ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2> + ;Kerberosv5;;vagrant@LKDC:SHA1.4EA9BE06BC11B0187D3452B7A09617B0B926968E;LKDC:SHA1.4EA9BE06BC11B0187D3452B7A09617B0B926968E + + generateduid + + 11112222-3333-4444-AAAA-BBBBCCCCDDDD + + gid + + 80 + + home + + /Users/vagrant + + jpegphoto + + + /9j/4AAQSkZJRgABAQAAAQABAAD/4ge4SUNDX1BST0ZJTEUAAQEAAAeoYXBw + bAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBs + AAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAA + AG9kc2NtAAABeAAABWxjcHJ0AAAG5AAAADh3dHB0AAAHHAAAABRyWFlaAAAH + MAAAABRnWFlaAAAHRAAAABRiWFlaAAAHWAAAABRyVFJDAAAHbAAAAA5jaGFk + AAAHfAAAACxiVFJDAAAHbAAAAA5nVFJDAAAHbAAAAA5kZXNjAAAAAAAAABRH + ZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAUR2VuZXJpYyBSR0IgUHJv + ZmlsZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAbWx1YwAAAAAAAAAeAAAADHNrU0sAAAAoAAABeGhySFIAAAAo + AAABoGNhRVMAAAAkAAAByHB0QlIAAAAmAAAB7HVrVUEAAAAqAAACEmZyRlUA + AAAoAAACPHpoVFcAAAAWAAACZGl0SVQAAAAoAAACem5iTk8AAAAmAAAComtv + S1IAAAAWAAACyGNzQ1oAAAAiAAAC3mhlSUwAAAAeAAADAGRlREUAAAAsAAAD + Hmh1SFUAAAAoAAADSnN2U0UAAAAmAAAConpoQ04AAAAWAAADcmphSlAAAAAa + AAADiHJvUk8AAAAkAAADomVsR1IAAAAiAAADxnB0UE8AAAAmAAAD6G5sTkwA + AAAoAAAEDmVzRVMAAAAmAAAD6HRoVEgAAAAkAAAENnRyVFIAAAAiAAAEWmZp + RkkAAAAoAAAEfHBsUEwAAAAsAAAEpHJ1UlUAAAAiAAAE0GFyRUcAAAAmAAAE + 8mVuVVMAAAAmAAAFGGRhREsAAAAuAAAFPgBWAWEAZQBvAGIAZQBjAG4A/QAg + AFIARwBCACAAcAByAG8AZgBpAGwARwBlAG4AZQByAGkBDQBrAGkAIABSAEcA + QgAgAHAAcgBvAGYAaQBsAFAAZQByAGYAaQBsACAAUgBHAEIAIABnAGUAbgDo + AHIAaQBjAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8E + FwQwBDMEMAQ7BEwEPQQ4BDkAIAQ/BEAEPgREBDAEOQQ7ACAAUgBHAEIAUABy + AG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCkBp1KAAgAFIA + RwBCACCCcl9pY8+P8ABQAHIAbwBmAGkAbABvACAAUgBHAEIAIABnAGUAbgBl + AHIAaQBjAG8ARwBlAG4AZQByAGkAcwBrACAAUgBHAEIALQBwAHIAbwBmAGkA + bMd8vBgAIABSAEcAQgAg1QS4XNMMx3wATwBiAGUAYwBuAP0AIABSAEcAQgAg + AHAAcgBvAGYAaQBsBeQF6AXVBeQF2QXcACAAUgBHAEIAIAXbBdwF3AXZAEEA + bABsAGcAZQBtAGUAaQBuAGUAcwAgAFIARwBCAC0AUAByAG8AZgBpAGwAwQBs + AHQAYQBsAOEAbgBvAHMAIABSAEcAQgAgAHAAcgBvAGYAaQBsZm6QGgAgAFIA + RwBCACBjz4/wZYdO9k4AgiwAIABSAEcAQgAgMNcw7TDVMKEwpDDrAFAAcgBv + AGYAaQBsACAAUgBHAEIAIABnAGUAbgBlAHIAaQBjA5MDtQO9A7kDugPMACAD + wAPBA78DxgOvA7sAIABSAEcAQgBQAGUAcgBmAGkAbAAgAFIARwBCACAAZwBl + AG4A6QByAGkAYwBvAEEAbABnAGUAbQBlAGUAbgAgAFIARwBCAC0AcAByAG8A + ZgBpAGUAbA5CDhsOIw5EDh8OJQ5MACAAUgBHAEIAIA4XDjEOSA4nDkQOGwBH + AGUAbgBlAGwAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUA + bgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAFUAbgBpAHcAZQByAHMAYQBs + AG4AeQAgAHAAcgBvAGYAaQBsACAAUgBHAEIEHgQxBEkEOAQ5ACAEPwRABD4E + RAQ4BDsETAAgAFIARwBCBkUGRAZBACAGKgY5BjEGSgZBACAAUgBHAEIAIAYn + BkQGOQYnBkUARwBlAG4AZQByAGkAYwAgAFIARwBCACAAUAByAG8AZgBpAGwA + ZQBHAGUAbgBlAHIAZQBsACAAUgBHAEIALQBiAGUAcwBrAHIAaQB2AGUAbABz + AGV0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdo + dHMgcmVzZXJ2ZWQuAFhZWiAAAAAAAADzUgABAAAAARbPWFlaIAAAAAAAAHRN + AAA97gAAA9BYWVogAAAAAAAAWnUAAKxzAAAXNFhZWiAAAAAAAAAoGgAAFZ8A + ALg2Y3VydgAAAAAAAAABAc0AAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA + /ZH///ui///9owAAA9wAAMBs/9sAQwACAgICAgECAgICAgICAwMGBAMDAwMH + BQUEBggHCAgIBwgICQoNCwkJDAoICAsPCwwNDg4ODgkLEBEPDhENDg4O/9sA + QwECAgIDAwMGBAQGDgkICQ4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O + Dg4ODg4ODg4ODg4ODg4ODg4ODg4O/8AAEQgBMQEuAwEiAAIRAQMRAf/EAB8A + AAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE + AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYX + GBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3 + eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfI + ycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEB + AQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMR + BAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJico + KSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG + h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW + 19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A/fyiis6/1Wz0 + 6P8AfyAyY+WJeWP4dvxoA0aw9R1+zsN0at9puR/Ah4H1NcjqHiG9vQ0cZ+yQ + H+FD8x+p/wAK5+gDWu9a1C8u1kad4gpyiRkqF/z71u6d4pI2xaiu4dPOQc/i + P8K43v0P5UlMZ7LDPDcW6ywSJLGejKcipa8gtL65sLjzLaV4z3HVT9RXb6d4 + mt7grFegW0398fcP+H40hHUUUgIZQykEEZBHeloAKKKKACiiigAooooAKKKK + ACiiigAooooAKKKKACiiigAooooAKKKguLmC1tzLcSpDGO7H/OaAJ6K4y48W + gXyi2tvMtwfmLnDN9PSuisNVs9RjzBJiQfeibhh+Hf8ACgC3cgHT5wQCDG2Q + fpX4u/CD9rf4h/C+aHRtXkfxt4PjbaLG/mP2i2XP/LGc5YADorbl7AL1r9o7 + j/jxn/65n+VfzYS/8fMn+8f51+qeG+W4XHUcXSxEFKPub/8Ab2z3T9D8p8S8 + zxWBq4Srh5uMvf2/7d3WzXkz99/hb8cPh38XtCW48J6yn9ppGGutHvMRXtv6 + 5TJ3L/tIWX3zxXrtfgD8JPhh8TviF4ut9Q+Hlvd6ZDZTgv4nluHtLSxYd1mX + 5nkGfuRbm+lftD4C1HXfDvw+0vRfGHiW48batBFtudbeyS2aZvaNf4QOAxO4 + 9W5r57jHh7BZXX5cPXUr/Z3lH1a0++z8nufQ8G8R43NaHNiKDjb7X2Zeiev3 + XXpseu0VBb3MF1biW3lSaM91NT18Yfankuk/FDQfHPgxNb8B6zYa1ojna17a + ybnjb+5Ihw0T/wCy4Bqkzs8hd2Z3JyWJyTX4a+HPF3ijwL4/bxB4O13UPDus + K5DTWr/LMufuSocrKn+y4Ir76+Fv7YXhzXfs+j/FO1tvB+sHCLrtorNplwfW + VOXtifX5o/da/Rc/8O8bg71cL+9h2+0vl19Vr5I/OOHvEbBYxqli/wB1Pv8A + Zfz6fPTzPs6jt61HFLDPY293bTwXVpcIJLe4t5VlimQ9GR1JVgfUE0/mvzux + +jppq6F70lL6elHagBPyo7elHejvQM1tP1m905wI3MkPeJ+V/D0ru9O1yy1D + CK3k3B/5ZOeT9D3ry/tR0YH360CPaaK8607xJd2m2K6zdQDjJPzj6Hv+NdzZ + 39rfweZbSq/95Twy/UUgLlFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS + MyohZ2CqBkknAFYmo69Z2AZFb7Rcj/lmh6fU9q4S/wBWvNRc+dJiLPyxJwo/ + x/GgDrNR8TwQho7BRcydPMP3B/jXE3V3c3lwZrmV5X9+g+g7VXzj2PrS8EZP + FMBvtT1ZkkDozI68gqcEU0gjP86Tv9aBnWWPiWXyWttQ/eIyFRMo+YccZHQ/ + pXwD8MP2PdC0iSDWvivd2/inVc+Yvh+xkZdOgPXE0gw9wR3Vdqf71fadHQ5r + 1MvzvG4GlUp4efKp2u1vpfZ7rfpqeRmGR4LHVadTEQ5nC9k9tbbrrt10IoIY + LTTbaxs7e2srC2Ty7a1toVihgX+6iKAqj2AqXtSVla7r2ieFvDjav4j1S00f + TRwstw3Mp/uxqPmkb2UH8K86EJ1JqMVeT6LVtnpznClByk0orvokv0N62u7i + yuRLbTPE4646H6jvV3U/i74Q8M3MVh4p1SGw1Z03rZwRvNMU/vtGgLIuSOWx + nPFfEXjj9ojWNT87TvAcE/hzTzlW1S4UG+lHqi8rAD6/M/uK8d8JPJL4xvbi + eSWe5lgZ5ZpXLySMWXJZjyT7k1+g5XwBXnTdbGPkX8q+L5vZfi/Q/PM08QKE + aipYKPO/5n8PyWjfrovU+P5/+P2b/fP86irqfG/hfUvBXxe8SeE9XXGoaVqE + ttK23Ak2sdrj/ZZcMPYiuWr97pVI1IRlF3TV0fz/AFacqc5QkrNOzPpr4GfE + bxb4B8GyDQdQEmlm/czaReZks5OFzhc5jb/aQg/WvvvwJ8ZvCPjeSGwkk/4R + rxG/A02/lGyZv+mE3Cv/ALp2t7GvzM8Af8iHL/1+yf8AoK128dvJeXcFlBbT + XtzO4WG2hiMkkrdgqgEk/SviOIuE8BmUpTkuSf8AMv1Wz+evmj7vhzizH5bC + MIPmh/K/0e6+Wnkz9V2VkdlYMrDhlIwRTfzr5Ut/E3xQ+CX7Nlz4x8foniHQ + bW7tbW38O3FyDqduk0mzd9p5CbRyIn389Ste2fDz4peBPipob3ngvW0vbmJN + 15pNyvk6hZ/9dISclf8AbQsnvX4hj8jr4eMqsGqlJO3PHWN9N+268uibP3DA + Z/h8TONKadOq1fklpK3l32fn3SO/780d6Xr3/Wkrxj3Be1JS/likoAU9KfFL + LBOssMjxSA5DKcGmUn50Adrp3in7sOornt5yD+Y/w/KuximingWWGRJY26Mp + yDXjXOenWuL8V/Fzw58MWYahqMlzrRXcmiWJElxJ6GQH5Yl/2nwfQGt8LhK2 + JqKnRi5SfRf1+OxzYrF0cNSdWtNRiur/AK/A+naK5vwdr0nij4VeHfEktqtl + JqenxXTW6ybxEXUNt3YGcZxnFdJWVWnKnNwlutDWlUjUgpx2auvmFFFFQWFF + FFABRRRQAUUV4D8evjfbfBHQ/Cmpajo17qmk6tqD2l3NYyL9otFEe4SJG3yy + e67lPoc104PB1sVWVGjHmk9l30ucuMxlHCUXWrStFbv52PcL3UbTT4d1zKFY + jKoOWb6CuG1LxHd3ZaODNrbnsp+Zvqf6CvOvCnjfwv8AETwqfEfhHxDZ+JNP + JHnyRMfOt2P8E8bfPE3swx6E10fFZVaU6U3CpFqS3T0a9UbUa1OtBVKclKL2 + a1T+Yd6KPyrk/F3jjwv4F0lbnxLqaWs0i7rawhXzbu5/3Igc4/2mwvvVUKFW + tUVOlFyk9ktWLEYilQpupVkoxW7bsjq6O9fK+hftaeBrj4n3nhvxlpl34It9 + yHT9Wkm+02xVlyFudq5ibP8AGoZPp1r6lhlgudOt7y0uLe8sbhBJbXVtKssM + yf3kdSVYe4NduZZPjcvmo4mm4327P0aun566HDlmdYHMIuWGqKVt+69U7P8A + Akyce1LwTx1pM0qqzyBEVmduiqMk15h6YnQ1HPNBa6bc315cW1lY2yeZc3Vz + MsUMCf3ndiFUe5NeXeOvjH4R8DtLY+b/AMJJ4hQY/s2wlG2E/wDTaYZVP90Z + b2Ffm58ZfGXxR+IOpvdeKdU/tDwvFIXtNJ0pDFY2Y7F4cku4/wCejlifUV9n + w9wTjczanP8Ad0+73f8AhXX1dl2bPiuIuOMFlicIfvKnZbL1fT0V33sfUfxR + /bE0LR/tOjfCmzt/FWqDKN4gv42GnQHpmGM4a4I7M21P96vmXR/FPiTxra3f + iLxdrmoeItblvJAbq8kzsXC4SNRhY0HZVAFfP4IIyCCD0INexeAf+RCk/wCv + 2T+S1+1Zbw1gMqo2w8Pe6yesn8+i8lZeR+JZjxNmGbV74ifu9IrSK+X6u78z + ta6zwd/yM0//AF6t/wChLXJnpXt3wO+HWp/EHx1rENldR2FtZWAae5kjLKGe + RQicdyFcj/dNGZ4inQws6lR2iupeWYepXxUKdNXk+g79vb4c/wBm/Erw78Tr + CDbaaxENP1RlXgXMS5iYn1eIFfpBX58V+/nx0+HafFH9lzxX4TWJZNTktTca + UxxlbqL54sHtuI2E/wB12r8BXR45njkVkkUkMrDBBHUEV4/h1m/1vLPYyfvU + tPl9n9V8j1vEfJ/qmae2ivdq6/P7X6P5n1L+zt8Orj4lWd/pltr+j6StndNL + eRvKJL7yyF+aK3zlx1+c4UY5r9FfBnw/8KeArBk8OacFvZE2z6pdESXk/wBX + x8i/7KAD61+JVpd3mna1aalpt5eabqdrIJLW8tJmimgYfxI6kFT9DX218Lv2 + x9V082+jfFuym16yGEXxJpsCi9jHTNxAMLOPV02v3IauDjnh/OcYnPDVOan1 + gtH/APbej26Js7+A+IMmwbUMTT5anSb1X/2vqvm0j6A/a0/5MP8AEIH/AEGN + N/8AR5r8p7G+vtK16z1XSr+90rVbSQSWt7ZTtDPA3qjqQR/XvX6f/tJ+I/D3 + i3/gnHrOv+Fdc0zxFos+sad5d5Yzb1z5/KsPvIwyMqwBHpX5b11eGlJxympC + as+eSaf+GOjX6HL4l1oyzenUpyuuSLTT85apo+5/hd+2TqFobfR/i9Yy6va8 + KnibS7cC6QetxbjCy+7x7W7lWr700TW9F8TeErTX/Der6dr+h3IHkX1jMJIm + OM7T3Vh3VgGHpX4SV2Hgjx/4y+HHis6z4L1670S7fi5iXD212v8Admhb5JB9 + Rn0IrHiDw4wmKvVwbVOfb7L+X2flp5G3DviTi8JaljF7SHf7S+f2vnr5n7fY + 70vavkv4XftbeDfF722j+PobbwB4kchFvDIW0q7Y8cOfmtyf7r5XPRhX1hcT + QWelSajeXVraaakQle8mmVYAhHD+ZnaVPYg89s1+MZnk+My+t7LE03F9Oz9H + s/kfteV51gsxo+1w9RSS37r1XQf+VYviHxLoHhLw7/aviXVrXSLM5ERlOZJz + /djjHzOfoMepFfP3jj9ouxtPO074fW0eqXPKnWr6I/Zk94Yjgyf7z4X2NfKm + r6vq3iDxFNq+u6le6xqkv37m7k3vj+6Oyr/sqAPavrcj4BxWKtUxX7uHb7T+ + XT56+R8jnnH+Fw16eEXtJ9/sr5/a+Wnme7eOP2hde1kzad4Khn8LaU2Va/kw + dQnHqpGVgB/2ct/tCvng5aaSRmeSWRi8kkjFndj1LMeSfc0UV+t5blOEy+n7 + PDw5V17v1e7/AE6H5JmWbYvH1faYibk+nZei2X9XP1/+E3/JsXgD/sAWv/op + a9Crz34S/wDJsPgD/sAWv/opa9Cr+ccx/wB7q/4n+Z/R+W/7pS/wr8kFFFFc + R2hRRRQAUUUUAFfn7/wUE/5Ij4B/7Dkv/og1+gVfn7/wUE/5Ij4B/wCw5L/6 + INfVcEf8jzD+r/JnynHH/IixHovzR+YfhzxJ4h8H+MbfxD4U1vUvDutw8JeW + MuxyP7rjo6HurAg+lffXws/bF0rVWtdD+LFjFoOpMRHH4i0yBmspm6Dz4Blo + ST/Em5OfurX511Ys/wDkNWP/AF8x/wDoYr98zvhvAZrC2Ih7y2ktJL5/o7ry + P5+yPiTMMqqXw89OsXrF/L9VZ+Z+mPjr9oq7lln0z4fWr2EQJR9bv4QZ294Y + TkJ7M+W7gCvmO6ubq/1a41C/urm/1Cdt091cymSWU+rMeT/Smy/8fMn+8f51 + H+PNcGVZLg8up8mHhbu+r9X+m3ZHqZrnWMzGpz4id+y6L0X6792eJ+N/+SmX + n/XCH/0Cul+Gnxj8ffCfUifCmrB9Hkk33WhX4M1hcep2ZzG3+3GVP1rmvG// + ACUy8/64Q/8AoFcpX0VXCUcTh/ZVoKUWtU1dHzFLF1sNiPa0ZuMk9GnZn6se + B/2q/hr4s8I3Fxq0eqeFfEdrDvn0UxG5+084JtpVADrnGQ+1lByc9a8s8d/H + bxT4shuNO0USeEvD7gq0NtNm7uF/6azDoD/dTA9zXxj4A/5Hub/ryk/mtewZ + z/8AWr43DcFZVgsS6sIXe6UndR9P83d+Z9riONs2x2FVKpUstnZWcvX/ACVl + 3QgAVcAADPQU4Eq2VJU+opMcUV9KfOHJa14O0vVWknt9ulX7cmSJP3ch/wBt + P6rg/WrHhTS7zR/DM1jfJGswu3ZTG+5XUhcMD+B6810tFW6knHlZkqUVLmW4 + vb0r9RP2cPBX/CI/s3afd3MIj1XXD/aFwSOQjDEK/TZhsdi7V+fXwu8HP48+ + Ovh7w3tY2c1wJL5hn5bdPnk57EqCoPqwr9f0RIoUjjRY40UKiKMBQOgA7Cvy + zxHzTlp08JF7+8/Tp+N/uP1Xw3yvmqVMZJbe6vV7/hp8x1fiL+1z8Of+Ff8A + 7YmtXFpB5WieIh/a1jtHyq0jHz09OJQ5wOiulft1Xx7+2r8OP+Ez/ZQk8S2U + Hm6z4TmN8hVcs1q+FuF+gASQn0iNfL8B5v8AUc1gpP3anuv57P7/AMGz6jj7 + J/r2UzcV71P3l8t1934pH400UUV/SB/NRPDdXVtZXtrb3VzBaXmz7Zbxyssd + zsO5PMUHDFTyCRkHpUPB6cH0pKKVkF29wopc8YPIpKYAQCCCAQRgg969m8Ba + tq138N7jRLvVtTutFsb4NZafNdO9vblkydiE4XJ54HHbFeM16v8ADv8A5F3V + /wDr8T/0CufFRThdrY6sJKSqaPc9AzRTXdY03SMqr6k/kK9Av/hh400j9nPx + H8Utb0ibRvDOk2iXEcV4DHd3wZ0QeXGeVX5wdz4z2B615FfE0qPL7SSXM0lf + q3sl3PZo4atW5uSLfKm3bolq2+xwPOKWs/TdV0/V7Iz6ddJcIo/eJjbJH/vK + eR9envV/vW7TWjME76o/UT9m7xcnij9mTS7OR1/tDQ2OnTqODsUAxNj02FVz + 3KtXvtfmv+yz4y/4R/8AaAfw/cS7NP8AEFv5GCcKLiPLxH8R5ifVxX6UV/Pf + GOWfU80qJL3Ze8vnv+Nz+heDsz+u5XTbfvR91/Lb8LBRRRXy59SFFFFABRRR + QAV+Vv7fPj9NS+KHhf4c2bo8Wj2xv9QK8kTzDEaH0Kxru9xKPSv1D1TU7LRf + DOo6xqU62unWNrJc3UzdI441Lux9gATX883xA8YXvj/42eJ/GWob1uNX1CS5 + EbHPlITiOPPoiBVHsor9K8M8q9vmEsTJaU1p/ien5X/A/M/E/NvYZfHCxetR + 6/4Vr+dvxOPqzZ/8hqx/6+Y//QxVapYJBDfwTEFhHKrlQcZwQcfpX70z8BTP + paX/AI+JiSFVSSzMcADPUnsK4HWfHVjZl4NIVNTuhwZmyIEPt3c/TA964TXf + EOq69LIbiZUsN2VtYMrGv+8OrH3PFc7XHSwyteR21cW3pEtXt7d6jqct7fTG + 4upMbnIA4HAAA6AdhVWiiutHG3c7fwB/yPM3/Xk/81r2DvXj/gD/AJHqb/ry + f+a17BXn4n+IelhP4YdqPzoorA6gooxV/StNvNa8Tado+nxGe/vrlLe3jz95 + 3YKo/M0pSUU29kOMXJqK3Puj9kbwV9k8J6347u4cT37/AGHT2Yc+ShBkYezO + FX6xGvsquf8ACnh2z8JfDbRPDVgB9l060SBWC48wgfM5HqzZY+5NdBX81Z7m + Tx+OqV+jenotF+B/SuRZasBgadDqlr6vV/iFVb6ytNS0W806/gjurG6geC4g + kGVkjdSrKR6EEirVFeUm07o9ZpNWZ/PN8U/A118Nv2g/Ffgq68xhpl8yW0j9 + ZYGw8Mh92jZCfQkiuAr9Lv2+vhxlPC3xTsIOR/xKNXKj/ekt3OP+2qlj/wBM + x6V+aNf1LwzmyzHLaVe+trP1Wj/z+Z/KvE+UvLcyq0Oid1/heq+7b1QUUUV7 + x4AUUUUAFfSn7PXw18V/E2fXdM8MQ2IW3u4mvry8uAkVqrIcMVB3ueOig+5F + fNdd18OdfvvD3xQtLnTtQvNKvJh5cF3aTGKWKQcoQw9TkEHg55BrjzCFaeHm + qMkpdG1dfddf132O3LqlGGJg6ybjfVJ2f32f9dtz9oPhn+zv4I+H0ttql3Gf + FXimPkalfxjZA3/TCLlY+3zctx96qH7XH/KOb4pf9g6L/wBKYa8Z+G37WV7Z + m30j4nWbX1uMIuvafBiRfeeAdf8Aej9PuV6f+05r2i+Jv+CYPxK1nw/qljrG + lz6ZEYrm0mEiH/SITjI6EZ5B5HevwSrgM1o55hp468r1I2lvH4lt0Xpp6H9A + UsflNbI8TDA2janO8dpfC9+r9bv1PxQgnntb1Lm1nltrlD8ksTbWX8f6V6Ro + /j4ErBr0WD0F7AnH/A0H81/KvMu9B6Gv6EnTjPc/nanVlB3R9Q6Tqk2n6xpm + t6VcgT280d1Z3EZyNysGRh+IBr9lvCfiG08WfDXQ/Ellj7PqNmk4UHPlsR8y + H3VsqfcV+Jehf8iLov8A15J/Kv0U/ZH8Y/bvh9rXgm6lzcaXN9rslY8mCU/O + oHosnJ/661+U+ImWe2wUcRFa03r6PT87fifrPhzmnscY8PJ6VFp6rX8r/gfY + NFFFfih+2hRRRQAUUUUAfHP7bPxC/wCER/ZNPhmzn8rVvFdz9jUKcMLWPDzs + PY/u4z7Smvxtr6r/AGxfiF/wnH7Y+q6daT+bo/hmMaVbBW+UyqSbhsevmEp7 + iNa+VK/pXgfKfqOU00170/efz2+5W+Z/MvHWbfX83qNP3Ye6vlv+N/kFFFFf + Xnx4qkq4ZSQw6EU5ni2M0pWHAyX6KPc+n1Fdx4A+Gfjj4oeJH03wVoU2prEw + F5qEreTY2XvLO3yqf9kZY9hX6I/Cz9lPwL4Ee11jxa0HxC8WR4dGurfbplm/ + X91bt/rGB/jlz7IK+Yz7i3L8qTjVlzT/AJVq/n2Xr8kz6jh/hHMc2d6UbQ/m + e3y7v0+dj8vLuyvrAWRv7K9sVvIBPZNcW7xi6iPSSMsBvXjquRVav3R8WeFv + DXjzwlJoHjPQ7DxJpDcrBdpzAf70LjDRMOxQj8RxX5//ABR/Y88Q6J9o1j4V + 3dz4v0kZZtCvGVdTgHUiJ+EuQPT5ZPZq8PIvEbAY2Xs8SvZSe13eL/7e0s/V + JefQ93PvDjH4GPtMO/ax62VpL/t3W/ybfkfMvgD/AJHqb/ryf+a17Aa8i8Cx + TW3xKvrS5hntLy3tZI7i3njaOWFgVyrowBU+xFeu19liPjPjsIrQDsaKD60d + qwOgK+qf2UvBX9t/Ge88WXcW6x0GD9wSOGuZQVX67U3n2JU18rd6/WL4GeCv + +EH/AGcNDsJ4fJ1S9X7fqIIwwllAIU+6oEQ+6mvjeOc0+qZbKEX71T3V6dfw + 0+Z9nwNlf1vMozkvdp+8/Xp+OvyPX6KKK/BT97CiiigDg/if4Gs/iV8A/FPg + i9aONdUsWjgmdciCcYeGTHfbIqNjvjFfhh8Svg/8QPhN4jFh4z0Kezhkcra6 + jD+8s7rv+7lAwTjnacMO4Ff0F1maxoukeIfDd1o+u6ZY6xpVymy4tLyBZYpB + 6FWBFfY8K8YV8mbhy81OTu1s790/6+R8ZxXwbQzlKfNy1Iqye6a7Nf1bzP5u + qK/QP9qT9lbwp8PPhzqHxJ8D6jPpmlRXMcd1oVzmVVMsgQGGQncACfuvnvhu + gr8/K/fsmznDZnhlXoPTbXRp9v8Ahj+fs6yXE5XiXh8Qtd9HdNd/w66hRRRX + qnkhSqzpIskTFJUYMjDswOQfzpKKAPovTr9NU0Cy1GPgXEQdh/dbow/76BrQ + M9z/AMItrehpeX1vo+sweRqtpBOUjukDBhuXpuBAIbGRjrivM/h7qO63v9Hk + blD9ptwfQ4Dj88H8TXpFeTVppSaa/rdHtUKjlFST/rZnj2s+CNRsA9xppfVb + MclQuJ4x7qPvD3X8q4jOQfbg+1fTIODkZFc7rXhjStbDSTRm0vyOLuAAMf8A + fHR/x5966aeJe0jkq4PrEu6F/wAiLov/AF5R/wAq9n+DHjL/AIQf9ovw9rE0 + vladLN9j1DJwvky/KzH2U7X/AOACvIdPtmsvD9hZSOsr28CxF1GA2O4Bq53r + zcZhoYmjOlPaSa+89TA4mphqsKsN4tP7j9wKK8l+B/jH/hNv2bvD+pTS+bqV + rH9hvyTlvNiAXcfdl2P/AMCr1qv5jxmFnhq86M94tr7j+nsHioYmhCtDaST+ + 8KKKK5jpCvPPix45g+G37Oni3xpMY/M02wZrRH6SXDYSFD7GRkB9ia9Dr82/ + 2/PiF5en+EvhhZT/ADSsdX1VVPO0bo4FPsT5zEH+6hr3eGsq/tHMqVBrRu79 + Fq/8jweJs2WXZZVxF9UrL1ei/wAz80rm5nvNRuLu6mkuLqeRpJpXOWdmOSxP + ckkmoKKK/qZK2h/KbberCj8jz0PIPsfUe1FFMD9DPg5+1n4Qj8NaX4N8e6Fp + Pw+itVEVnqWh2nl6Qe2ZYFy1ux7uNynvivt6Ce3u9Mtr6yuba+sLmPzLW6tZ + llhnX+8jqSrD3Br8Fa9M+Gvxf8ffCfVTJ4Q1cDSZJN93oV8pm0+59SY8/u2/ + 24yrfWvy3iLw2o4hyrYGXLJ6uL+F+j3T+9eh+qcN+JVbDKNHGx54LRSW6Xps + 19z9T9ozXJ+LvG/hbwNpK3XifVFtJJF3W1jCvm3dz/uRDnH+02F96+Urr9qb + WvFHw4sZ/Cfh2PwlfzqyXt3c3Au2hkU4YWwwAF7h3BYenevBrq5ur/VrjUL+ + 6utQ1C4bdPdXUpkllPqzHk/yFfLZN4d4mpLmxr5Euis5P56pL735Lc+szjxG + w1OHLgVzt9Wmkvlo2/uXqeg/E/4gxfEnxxbawfC2j6LJao0UF75YfUp4zgbZ + 5xjevAwmCF7GvOPeijvX6zg8HRwlGNGirRjstf1PyXGYytiq0q1V3lLd7fkF + FBo4rqOY9a+CHgr/AITr9ozQ9Mni87S7R/t2ogjKmGIg7T7MxRP+BV+s9fKX + 7KHgr+xvg9feL7qLbfa5Pttyw5W2iJUY9Nz7z7hVNfVtfg3HOafW8ycIv3af + u/Pr+OnyP3rgXK/qmWqpJe9U975dPw1+YUUUV8YfZhRRRQAUUUUAfKX7af8A + yYH4j/7CFl/6UJX4qV+1f7af/JgfiP8A7CFl/wClCV+Klfv3hh/yKJf43+UT + +ffFL/kbx/wL85BRRRX6Mfm4UUUUAaWj6idI8U2OojJSKT96B/FGeGH5HP4V + 9DHGcq25DyrDowPQ/lXzP1HPIr27wbqX9o+BYI5G3XFk32eTPUgcofxXj8K5 + MVDRSO3Bz1cTqaDRRXEegA6UUfrQaAPrn9knxl/ZvxQ1fwZdTYttXt/tFmrH + pPECSAP9qPcT/wBcxX6EV+LXhnXrzwv8QdF8R2B/0rTryO4QZwH2tkqfYjIP + sTX7K6TqdnrfhfTtY0+TzrC+to7i3f8AvI6hlP5EV+LeImWexxkcTFaVFr6r + /gW/E/afDrM/bYKWGk9YPT0f/Bv+BoUUUV+eH6IFfhh+1hd3N1+3/wDEP7TP + JP5NzBFFuP3EW2iwo9AP6n1r9z6/Cn9qj/k//wCJP/X9D/6TQ1+meFiX9p1f + 8D/9KifmHiq2sspf41/6TI+faKKK/eD8DCiiigYUV3Ph7whHrXhCe9nuZrOd + 5ito4XcuF4YsvcE8ZB7VzuraFqeiThb+3xCxxHcxndE/0bsfY4NQqkW7X1NH + SkoqVtD1XwR/yTS0/wCvib/0KurrlfBH/JNLTr/x8Tf+hV1Wa82p8bPVo/Av + QKKKKgsO9b3hjw/eeK/iHo3hzTwfteo3aQIduQgY8ufZRlj7A1g9q+xv2R/B + X23xrrPjq7izBpsf2OwYjgzyDMjD3WMgfSWvJzzMlgMDUrvdLT1ei/E9bI8t + ePx1Ogtm9fRav8D7q0jSrPQ/Cmm6Lp0fk2Fjapb26eiIoUZ98DrWjRRX81yk + 5NtvVn9LRiopJLRBRRRUlBRRRQAUUUUAfKf7aKO37AXiUqjMFvrIsQM7R9oQ + ZPoMkD8a/FOv6MvGfhbTvG/wp8Q+EdWXOn6tYSWsrbcmPcpAcf7SnDD3Ar+e + XxDoWo+F/Hms+G9Xi8jVNLvZbS6TsJI3Ktj1GRwe4r9x8LMdTlg6uG+1GXN8 + mkv0/I/CvFXA1I42lifsyjy/NNv8b/mY9FISFUsegGTV+/02/wBMliS/tpLc + SoHhc8pICMgqw4PB6da/Urn5VZlGjtRR2pgFdj4H1H7F42W0dsQX6eScngOO + UP55H41x1KrOkiyRMUlRgyMOzA5B/OpnHmi0VCXLJM+lqOtU9Ov01TQLPUY+ + BcRB2Ufwt0YfgQaufyryWrHtJpq6DvR2oooGH41+kP7KvjH+3fgRceGrmXdf + aBc+WgJ5NvKS8Z/BvMX2AWvzer3P9nfxj/wiP7TOkJPL5em6wP7NusngGQjy + 2/CQIM9gWr5ji/LPruWVIpe9H3l8v81dH0/CGZ/Uszpyb92Xuv5/5OzP1Ooo + or+ej+hgr8Kf2qP+T/8A4k/9f0P/AKTQ1+4Wta5o/hzw1dazr+qWGjaVbJvn + u7ydYo4x7sTj8O9fg58f/FOheNf2wvHPifw1eNqGh314htLkxNH5oWGNCwVg + CBuU4yASMHAr9R8LKNT6/Vqcr5eW1+l7rS/c/K/FatT+oUqfMubnvbraz1t2 + PHqKKK/cz8JYU5I3lnjhjx5kjhEz0yTgfzptWbL/AJDlj/19R/8AoYoGj6Gt + LOPTtJtdPhGIraIRD3x1P4nJ/Gp2VZIHjkRJYnGHjdQysPcHrUsv/H1J/vn+ + dR9ulePe57dkirZWNppuni0sYRb2wkZ1jBJCljkgZ7Z7dqtUUAEsAAST0Aob + BK2gUfyqkupWL682lx3Mct+sZkeKM7vLAx94jgHnp1q7RYE09hyqzyqkas7s + cKqjJJPYV+vfwp8Gr4D+A3h/w6yKt9Hbia/I/iuJPmk574J2g+iivz6/Z08F + /wDCYftJ6ZPcReZpWij+0LrI+VmQjyl/GQqcdwrV+pNfkniPmnNUp4OL295+ + vT8Lv5o/XPDfK7U6mMkt/dXp1/Gy+TCiiivy4/UgooooAKKKKACiiigAr8kv + 27Phz/YHx90r4gWMGzTvEtt5V4VXhbuBQpJ7DfH5ZHqUc1+tteFftIfDj/hZ + 37I3ifQ7aDz9as4v7S0gBcsbiEFgi+7oXj/4HX1HB+b/ANnZpTqN2jL3Zej/ + AMnZ/I+W4yyf+0cqqU0ryj70fVf5q6+Z+DMn/HvJ/umvo6OGG58NWttdQRXN + s9rFvilXcrfIO39etfOMn/HvJ/umvpWwjlntdKtbeGa5upoIkhghjLySsUXh + VGST9K/pHFuyTP5rwSu2jznWfAJ+e40GTPc2U78/8Ac/yb8682mjkt7+S0uY + 3t7uP/WQyLtdfqP61+lHgf8AZ11XURFqXj65m0GxOGXSLVwb2Uekj8rCPYbn + +le8eIPg38LfE/w4h8Kar4L0oaTbg/Y5bRfJvLRj1kjuBmTeep3Fge4NfDY3 + xFy/CVlSV6ndxtZfPaXy08+h91gvDjMcXRdXSn2Ur3fy6fPXy6n4uUV9V/FP + 9k7xt4KS61nwVJcfELwtGC7xwwhdUs0HUyQLxKo/vxZPHKivlJWVgdpzglWH + QqR1BHYj0PNfZ5Zm2EzCl7XDVFJfivVbr5nxOZ5Ti8ureyxNNxf4P0ezXoep + fD3Ud1tf6PI3KH7Tbj2PDgfjg/ia9Hr560bUW0nxTY6iM7IpP3o/vRnhx+R/ + SvoY4yNrBlIyrDuD0P5U8TC0r9ysJO8LdhO/vR60Ud65zqD605HaOdZEdkdS + GVlOCpHQg+tNo7UAfsL8MvFyeOfgX4c8S71a5ubULeAfwzp8kox2G5SR7EV5 + J+0T+0ZpnwM0PTLODST4g8WarFJJZWjS+XDAikL5sxHzYJOFVR821uVxmvL/ + ANkLxjsvvEfgS6l+WRRqNgpP8Q2pMo+o8sgf7LGvgb9pT4hf8LJ/bC8V6zbz + +fo9lN/ZmlMGypggJXcp/uu/mSD/AH6/Isp4Np1eIKtCrG9KHveqfwr+uzP1 + zN+M6lHh+lXpStVn7vo18T/rujkviT8XvH/xZ8SjUfGmuz30UblrXT4v3dpa + 9v3cQ4BxxuOWPcmvNKKK/b8Ph6VCmqdKKjFbJaI/DMRiateo6lWTlJ7tu7Ci + iitjEKs2X/Icsf8Ar5j/APQxVarNl/yHLH/r6j/9DFDBbn0lL/x9Sf75/nUf + 50+X/j5k/wB8/wA6ZXjo90zdT1jTNItRLqF0kRI+SJfmlf6KOfxOB715ZrXj + XUtSV7exDaVYtwQjZmkH+0/b6L+del6v4f0rW483sBS5C4S7h+WVB6Z/iHsf + 0rynWvCWqaMHnCjUNPH/AC8wKcoP9teq/Xke9dWHVPrucWJdXpsaHw+AHjmY + AAD7E5/8eWvYO9eP/D8g+OJiCCPsL4IPutfSHgDwpceOPjJ4f8LwbwL66VZ3 + XrHCvzSv+CBj9cVhj60KSlUm7JK79Eb5bRnV5acFdt2Xqz9Af2YfBX/CM/s9 + x63cw+XqfiGQXbEjDCBcrCv0ILOP+ulfSFQWttb2Wm29naRJb2sESxQxIMKi + KMKo9gABU9fy9meOnjcXUry3k7/5L5LQ/qTLMBDBYSnQjtFW+fV/N6hRRRXC + d4UUUUAFFFFABRRRQAUUUUAfhP8AtQfDf/hW/wC154n0u1g+z6Jqrf2ppW1c + KsM5YsgHYJIJEA9FHrX0L+zp8cPg/pumWOgazpVv8PvGjRJbtr99OZ7bUSAF + AFw3Nrn/AJ5sAn+0a91/bm+HP/CTfs42Pjmxg36p4Xuc3BUcvZzFUfp12uI2 + 9l3n1r8hCAVIIBBGCCODX9BZTTo8S5DCnXm04+62nbVd+jurOzXpZ6n885vU + rcNZ/OpQgmn7yur6Pt1VndXT/wAj98dpAQ8FXQPGykFXU9GUjgg9iODTec1+ + Pfws+PvxC+FDQ2GlXqa/4TD5k8O6s7PbqO5gcfPbt/ufL6qa+w9T/bR+HkPw + wh1PR/DniW/8WzAr/YF2FhitXA+9LdLlXjz08tdzei1+b5p4fZtha6hSh7WL + 2a/9uT+H1vbzP0rKvEXKcVQc60vZSS1T1/8AAWt/S1/I+vLi6t9P0u51O9vL + XTbC0Xzbi9uZ1hht1H8TyMQFHuT9K/MX9pX4lfBfx3r7f8IP4X/tPxYsg+1+ + NbcmyguADynlYzd57SuFx1BavGfiR8W/HvxX1dZvGGsmXTYpC9nolmph0+1/ + 3Ys/O3+25Zj6ivNq/QeE+Anl1WOJxFRuoukW0l5N6OXpovJn53xbx9/aVN4a + hTSp95JNv06R9dX5oOo55Fe3eDdS/tDwLBG7brmzb7PKSeSByh/FePwrxGux + 8Daj9i8araO2IL9PJOTwHHKH88j8a/Q8RDmh6H55hp8tT1PZqKKO1eaeqFFH + tRQMv2HivWvBMl54m8PXItNXs7KcQSldwXfG0Z4+jHHocGvlkdBX0Hr3/Ii6 + 1/15P/KvnwdK6sHTinKaWrsr+S2/N/ecONqTajBvRXdvN2v+S+4KKKK7jgCi + ijnsGY9gqkk/gKACrVgM6/p49bqL/wBDFVFZXjV0ZWVhkMDkEVd07/kYtO/6 + +4v/AEMUnsNbn0fL/wAfMh/2j/Oo6fJ/x8yH/aNMrx0e4B9e9KCQcg4PsaTt + RTEZcOi6bbeJH1W1tltbt42jlEXyxuCQclegbjqK+9/2RPBWItf8fXkXLf8A + Eu04sO3DzMP/ACGoI9HFfEtnaXN/q1rY2cL3F5czLDBEg5d2IVVHuSQK/Yvw + J4VtvBPwh0DwvbbCLC0VJXXpJKfmkf8A4E5Y/jXwfiDmroYBUE/eqaf9urf9 + F95974e5Sq+Pddr3aev/AG89v1f3HW0UUV+Hn7iFFFFABRRRQAUUUUAFFFFA + BRRRQBl63o+n+IfBuraBq0AudL1KzktLuI/xxyIUYfkTX89HjzwjqHgL4yeJ + fBuqBjeaTfyWxcrjzVB+SQD0dCrj2YV/RXX5cft8fDj7D438NfFCwt9tvqUf + 9masyrx58aloXPqWjDr9IRX6V4aZv9Xx8sNJ+7UWn+Jbfer/AIH5n4nZP9Yw + EcVFe9Tev+F7/c7fifndRRRX70fgNgooooAKVWdJFkjYpKjBkb0YHIP50lFA + H0Xp1+mq6BZ6lHwLiIOw/ut0YfmDVyvN/h7qO62v9HkblD9otx7HhwPxwfxN + ekV5VSHLJo9ilPngmHeijtRUGpl67/yImtf9eUn8q+fB0FfQeu/8iNrX/XlJ + /KvnwdBXbhPhZ52N+JBRRRXWcYV9p/sNfDr/AISr9qa+8bXsHmaR4Rs90JYA + q19cKyRjB/uRea3sWSvip3WOF5HO1EUsx9AOTX7kfsofDhvhx+xf4bt7238j + X9bB1nVg33lknAKRnv8AJEIkx2KmvieP82+pZTKMX71T3V6P4vw0+aPuPD7K + PrubRnJe7T95+q2/HX5M89+Mv7FfgPx415rvgJ4Ph54ukJkdLeHOmXj8n97A + MeWxOPnjx3yrV+Y/jT4WePvhP8TNM0nx54eudIaW+jWzv0Pm2V786/6qcDaT + gg7Dhxnla/dXxv8AEXwf8O/D39o+K9ZgsN4P2e1X95c3JH8McQ+Zj74wO5Ff + n78Wf2ifEHxH0q98O6bplpoHg2f5ZYLmJLi7u1BBBdiCsXIBAT5h/fr5HgbO + c9qWhKPPR7ydrej1b9LP1R9jx1kuRQvOMuSt2jrf1WiXrdejPnqX/j6k/wB4 + /wA6Z29aPxor9MPzMPWiiigD6V/Zd8Ff8JJ+0B/b11D5mm+HohckkZU3DZWE + fUYdx7xiv0srwz9njwV/wh37NmlNcReXqmsf8TG7yPmUOB5aeoxGF47MWr3O + v574wzT69mc2n7sfdXy3+93P6F4Pyv6jlkE170vefz2+5WCiiivlz6gKKKKA + CiiigAooooAKKKKACiiigAryz41/D6L4ofsyeLPB5RGvrm0Mumu2Bsuo/nhO + ewLKFJ/us3rXqdFb4bETw9aNWm7Si016owxWGp4ijOlUV4yTT9GfzVyxSwXM + kM0bwzRsVkjdcMrA4IIPQio6+qP2wfhz/wAIH+2DqmoWcBi0TxMn9q2hA+VZ + WJFwmfXzMvjsJFr5Xr+r8sx8MbhKeIhtJJ/8D5PQ/krNMBUwWLqYee8G1/k/ + mtQoooruOEKKKKANLRtROk+KbHUeSkUn70f3ozw4/I5/CvoY4zlSGU4Kt/eB + 5Br5n6jnkV7b4N1L+0PAsCO265sm+zyknkgDKH8V4/CuTFQ0Ujtwc7PlOqoo + oriPQMzXP+RH1r/ryk/lXz2Ogr6E1z/kR9a/68pP5V89joK7cJ8LPOxnxIKK + KK6zjPT/AINeENO8bftJeGdJ165tbHwrazf2n4hurqQRxR2VuRJIrMem9tkY + 9fMr9IPiT+1nI5n0j4W2iiMZQ+INQg4+sEDdfZpMDn7pr85/AGmiLQbvVJky + 91II4cj+BDkn8W7/AOzXoHU18pm+SYbH4uNXELmUFZR6X6t93su2mzPrsnzr + FYDBypYd8rm7uXW3RJ9Fv567lzUtS1LWfENxq+s6je6xq1wf315eTGSV/bJ6 + D0UYA7CqdHr2or0oxUUklZI89tyd3uw9qO1FB6GmIK9I+EvgxvHnx98P+H3j + aSwM/n6gewt4/mcE9t2AgPqwrzev0A/ZI8Ff2f4A1jxzdw7bnVJPslixHIgj + b52B9Gk4P/XIV4HE+afUMuqVU/eei9X/AJb/ACPf4Yyv6/mNOk17q1fov89v + mfYCqqoFUBVAwABgAUtFFfzkf0cFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAF + FFFAHyP+2d8OP+E2/ZHutes4PN1rwpKdRhKrlmtiNtyvsNmJD/1xFfi/X9KN + 1a299plzZXkMdzaXETRTxSLlZEYEMpHcEEiv59fi54CuPhn+0b4s8FzCQw6f + fN9ikfrLbPh4XJ9TGy59Dkdq/bPC7N+ehUwUnrH3l6Pf7nr8z8Q8U8n5K9PG + wWkvdfqtvvWnyPOKKKK/WD8kCiiigArsfA2o/YvGq2jtiC/TyTk8CQcofzyP + xrjqVWdJFkiYpKjBkb0YHIP51M480WioT5ZJn0tQKp6dfpqnh+z1GPhbiIOw + /ut0YfgQa6zwz4U8R+MtfbTfDGlT6rcJjz5AQkFsP70sp+VB9efQGvFrVYUY + OdSSilu3ol8z3qFKdaajTTbeyWrfojjdbOPBOs/9eUn8q+eh0FfrN4W/Zz8J + 6foNx/wnDjxjfXMDRTWsbPBZQhhg7MEO7Y6OSMHkLXzj8T/2ONc0o3Gr/Ce/ + m8UaaMu3h/UJVXUIh1xDKcJcD0Vtrn/arwMv47yepiHQ9pbtJq0X8+nq0l5n + u5lwJnFPDqv7O/eKd5L5dfk2+58T1LDBLdXsNrbqWnmkEcYHqTgUt1bXVhrF + 1p2oWl3p+o2rmO5tLqFopoGH8LowBU/UV2vgHTvtPiifUnXMVlH+79DK4IH4 + hcn8RX3EppQ5j4aFNufKz1W1tYbDS7axgA8m2iEaY74HX8Tk/jU9H40nO8fS + vKPYQUveiigYe+KKKKBGnoukXuv+MNM0PTo/Nv7+6jtoF7bnYKM+3PJ7Cv2T + 8N6DZeF/AOj+HdOGLPTrRLeIkYLbRgsfcnJPuTXwX+yb4L/tb4saj4xu4t1l + osHlWpYcNcSgjI9dse/PoXU1+h9fjPiJmntcXHCxekNX6v8AyX5s/aPDrK/Z + YSWKktZ6L0X+b/JBRRRX5yfowUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUU + UAFfm/8At9fDnztI8LfFKwgy9uf7J1cqP4GLPA59AG8xST/fQV+kFcR8SPBV + l8RfgV4o8FX5RYdWsHhjkcZEMo+aKTH+xIqN/wABr3OG82eXZjSxHROz9Ho/ + 8/U8LiXKVmWW1cP1auvVar/L0P53qu6dYy6nrltp8DxRzzsVjaQ4XOCeT26U + up6be6P4k1DSNSt3tNRsbmS2uoH+9FIjFXU+4IIrT8J/8lK0b/rv/wCytX9S + ua5OZH8rKHv8sjKvtPvdMv8A7LqFtLaz9g44ceqnow+lU6+kLq1tb/T2tL63 + hu7dv+Wcq5APqO4PuK801jwBcRu02gu96h5+xyn96P8Acbo/0OD9axp4lPSW + htVwso6x1R51RSsrJIyOpV1JDKeoI6ikrpOVnvfwL1f4ax+Ln0f4q6vqmkaC + 0olsprcbbcyH7yXEgy8cZwDuUeuSK/WTR7LRtP8ABun2/hqDSrfw4ybrH+y9 + ptZBj7ysvDH1JJb1r8IK9U+Gnxm8ffCnU93hfVt+kO4a60W+BmsrjnJzGT8j + dfmTBycnNfn/ABjwfXzX95RrNNfZfw+q7Pzd/kfoPBnGVDKX7OtRTT+0viXk + +68tPmfsy33OxpSPXkV88/Df9pr4a/EDTlttVvYPAPiVY90tjqtx/o0uBljB + cHhuhOxsNgVgeOv2i0iefTPh7aebICVbXNQh+Ue8EB6+zycdwtfj1HhPNqmJ + eH9i1Jbt6Jed9n8r36H7LW4uymnhViPbKSeyWsn5W3Xzsu53/wAbfC3wf8Qe + CEuvi1DbW90Iium6laNs1gccCAqC0i/7Lho/pX5/6bo+n6FaXFhpU97d2P2q + SSG4vIkjuJUJ+UyKhKhgoAwpIra1HUNQ1jXrjVNYv7zVdTnOZru7lMkj+2T0 + HsMAdhVTvX7Rw3kdTK8N7KVZzv0+yv8ACunn33sj8W4kzunmmJ9rGioW6/af + +J7P7tNrsPek/wCWg+lL3qhqOo2Wk6Y1/qMxtrRCFaTYW+Y/dXjucHGcZr6F + anzzdi/39axdT8Q6XpN3FazzGa+kkVFtYcM67iBluyjnvz7V53rXjm/vd9vp + avpVoeDJnM8g+vRB7Dn3rj7H/kOWR6k3UZJPJJ3jk+tdUMM95HHUxa2ifSDA + rIynkg4yBTakl/4+ZP8AeP8AOvYfgN4K/wCE2/aR0W0uIvN0rT2/tC/BGQUi + IKqfUM5RSPQn0ry8Zi4YbDzrT2imz18FhJ4rEQow3k0vvP0H+C3gr/hBP2dt + B0iaLytTnj+2aiCMN58oBKn3Vdqf8Ar1Wiiv5lxeJnia86095Nt/M/pvCYWG + GoQow2ikl8gooornOgKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig + D8df23fhx/wiX7UUXi6xg8vSPFdubhiowq3ce1Jx/wACBjkz3Lt6V8peE/8A + kpWjf9d//ZWr9pf2rfhz/wALF/Y71+O0g87W9DH9r6dtHzMYlbzUHc7ojIAv + dtvpX4g2l3cWOoxXdpKYLqI5jkABKnBGRnvzX9GcB5t9fyiMJP3qfuv06P7t + Pkz+buPco+oZxKcV7tT3l6/aX36+jR71q2t6ZokAbULjZKwzHbxjdLJ9F7D3 + OBXlOteMdU1ZXt4CdM09uDFE/wC8cf7b9fwGB9a5V3eSd5ZXeWVzl5HYszH1 + JPWm19jToRjq9WfHVcTKe2iCiiitznCiiigAIDAhgGB6gius0XxhqmkqkE5O + qaeOBDM/zoP9h+o+hyK5QdaSplFSVmVGbi7pn0FpOt6ZrcG7T590yjMltINs + qf8AAe49xkVroju+1FLt1OOw9T6V80I7xzpLE7xSoco6MVZT6gjpX0B4fv7u + /wDh9pM15N5sskO6VtoBkbcRubHU4A59q4K9Hk1R6WHxHO7M1yAOjBj3K9Pz + 719Ffs3WlnqPjLx9p2pWVlqenXOhQx3NneQLNDOv2j7rowIYfX8K+c6+k/2Y + /wDkpfjT/sCw/wDpQK+U4vbWTV2uy/8ASon1vCEVLOaCa3b/APSWYvxR/Y40 + rURc6z8JL2HQb7ln8N6lOTZSnri3nOWhPokm5P8AaWvhHVPDniDwh8TIPD3i + rRdS8O65BcxmSzvoTG5G8fMp6Oh7MpIPrX7h319Y6VodxqeqX1npem24zPd3 + cojiT6se/sMk9ga+OvjJ8XPCnjrww3hTS/C+n+ItPik3Qa3rNsQ9s4Od9mvD + xngfOxAP9w18twVxVnFaaoVIOrBbyejj6yekvR+8+59Pxvwnk1CDr05qlUe0 + VqpeiWsfX4fI+epf+PmT/fP86/Rr9lXwV/YPwQufFF3Ft1DX590RYcrbRkqn + 03MXb3BWvgXwh4cvPGPxR0Pw1ZFvtOo3iwmTbny1Jy8h9lUMx9ga/ZDTNOtN + I8O2GlafEILGyt0t7eIdERFCqPwAFaeIuaeyw0MJF6z1fov83+RPhzlftcTP + FyWkNF6vf7l+Zdooor8cP2QKKKKACiiigAooooAKKKKACiiigAooooAKKKKA + CiiigAooooAQgMpDAEEYIPevwP8A2gfh0fhd+1d4q8MwwGHSGn+2aRxhTazZ + dFHqEO6PPrGa/fGvgX9vL4cf2z8HtC+JFhb7r7QJ/smpMq8taTMAjE+iS4AH + /TZjX3vh3m/1PNFSk/dq+78/s/5fM+A8Rsn+uZW6sV71L3vl9r/P5H5Q0UUV + /Q5/OgUUUUAFFdT4P8D+MPiB4wj0DwT4c1PxLqzYLxWsfyQKTgPNIcJEmeNz + kD0zX6U/Bz9hbw9ootdd+MF3B4t1cYdNAtGZdNtz1xK3DXDDjrtTI+6w5rwM + 74mwGVQvXn73SK1k/l09XZH0GR8MZhmsrUIe71k9Ir59fRXZ+dfhX4Y+OfGX + gDxF4u0PQZ28I6HYT3uo63dHybQJChd44nb/AF0uBwiZ5IyRXAA5UH1r98Pj + rZ2em/sE/FGx0+0trCxt/CN5HBb28QjjiUQMAqquAAPQV+B6/wCrX6Vw8J8R + 1M5p1asoKKjKyW+lur7/ACR3cXcN08mqUaUZuTlG7e2t7aLsLXu/hT/km2i+ + 8B/9DavCK938Kf8AJNdG/wCuB/8AQ2r6TFfCj5vB/G/Q6DHNekfDX4ht8N9U + 8SalBpK6xf3+nx2tpHLKY4Y2WTeXkI+YjHQLyT3Feb96K8fGYOjiqMqNZXi9 + 16O/T0PcweLq4WtGtRdpR2fyt1Ol8VeMPE3jbXFv/E+qy6i8Zzb2yr5dtbD0 + iiHyr9eWPc1zVH+TUkUUk9zFBBG8s0jBI0RSWZicAAdya0o0adGmoU4qMVsl + okZ1q1StUc6knKT3b1bPsv8AZF8FfaPEeu+PLuEGKzT7Bp7MP+WrANKw9Cqb + V+kjV95Vwnwz8Hx+BPgf4e8NBUFzbWwa8Zed87/PIc9xuJA9gK7uv514kzT6 + /mFSsn7uy9Ft9+/zP6L4byv+z8up0Wvetd+r3+7b5BRRRXhHuhRRRQAUUUUA + FFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABUNzbW95p89peW8N1azIUmh + mQOkikYKsp4II7GpqKabTuDVz4O+Mf7EPhbxOLrXPhdPb+D9cbLtpU2Tp9wf + RMZaAk+m5ewVetfmX44+HvjL4ceMX0LxpoN9od+MmLzlzHOoON8cgysi+6k1 + /RLXNeK/B3hfxz4Rn0Hxdoen6/pMv3oLuPdtP95W6ow7MpBHrX6Hw/4h43BW + p4n95D/yZfPr8/vR+dcQ+HWCxt6mG/dz/wDJX6rp6r7mfzmUV+hnxi/YZ1fS + vteu/CO8k13Txl30G+kAuohySIpDhZR6K2G93NfAGo6bqOj65daZq1jeaZqV + tIY7i1uoWiliYdVZWAIPsa/acoz3BZnT58NO/ddV6r+kfiWb5DjssqcmJhbs + +j9H/TNTwv4u8VeCfFsGveD/ABDqvhvV4iMXFjMV3gHO2RD8siequCPav0g+ + Dv7dul34tdC+M9jDoF7gIviXTomNlKcYzPFy0BPdl3Jk/wAAr8vqKxzrhvAZ + pC2Ihr0ktJL5/o7ryNsk4lx+VTvh56dYvWL+X6qz8z96/jjqWnax+wF8UNT0 + m/stU0258JXr293aTrLDMphb5ldSQw9wa/BJf9Wv0r0PwL478YeGrLU/B2ia + /eWfhXxLBJp2saS4EttLHMux3SNsiOYA8SJg565rI1nwfqmjo80QOpaev/Le + FTvQf7adR9RkV5vCvD7yWFWjKfMpO6ezta2vn8z0uLOIVnc6VeMHFxVmt1e9 + 9PL5HK17v4T/AOSa6N/1wP8A6G1eDgggEEEHoQa948KD/i2ujf8AXA/+htX0 + uL+FHzWDfvv0Ogorq/CXgjxR468RDTPDGkXWpzjHmug2xQg/xO5+VR9Tz2zX + 3V8Nv2XPDnh0W+qeN5IfFGsDDCzAP2KE+hB5l/4Fhf8AZ718lnPEmBy2P72V + 5fyrf/gfM+xybhvHZlL91G0f5nov+D8j5C+HfwX8b/Ei5jm0ux/s/RN2JNVv + QUgHrs7yH2Xj1Ir7++HHwI8EfDsQ3sVt/bviJME6pfICyN6xJyI/qMt/tGva + Ioo4LaOGGNIYY1CpGihVUAYAAHQCn1+P55xjjsxvBPkh2XX1fX8F5H7DkfBu + By602uefd9PRdPxfmFFFFfJH1oUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU + UUAFFFFABRRRQAUUUUAFFFFABRRRQAV5L8Ufgj8O/i7obW/i7RI21JY9ttq9 + piK9t/TbJj5h/suGX2r1qit8Niq2HqKpSk4yXVaHPisLRxNJ060VKL6PVH4s + fGP9kX4ifDIXWsaJHJ448IxksbywhP2m2X/ptCMnAHV13L3O3pXydX9LFfK/ + xj/ZL+HXxSF1q2mQp4K8Xvlv7R0+EeTct/03h4Dc9WXa3qT0r9a4f8TNqWYL + /t9fqv1X3H5HxD4Yb1cvf/bj/R/o/vPxl0H/AJHvRf8Ar9j/AJ19B5YPlSQc + 8EVzvjX4FfEb4QfFTR4vFWjPJpLagi22s2OZbOf5uPnx8jH+64VvYjmvrD4b + /s1+MPGbQalr6yeE/D74YSXEf+lTr/sRHoCP4mx6gNX3+YZ5gKdCOJlVXI1o + 73v6d35HwGW5FmFSvLDKk+dbq1reb7LzPlG4+Hsfi3W4rXRLG5TX7l9sSWMB + k89vRo16/VcGvtv4M/si6jB4O0ib4o3S2fkR86Pp8253+Yn95KPujn7q5P8A + tA19keB/hp4O+Hmj/ZfDOkx287qFnvpv3lzP/vyHnH+yML6Cu8r8oz3xDxOI + TpYT3I938Xy7fi/Q/Wci8OsNh5Kti/fl2Xw/Pv8AgvJmPoegaL4Z8Ow6ToGm + Wek6dF9yC2jCrnuT3LHuTknvWxRRX5xOcpycpO7Z+kQhGEVGKskFFFFSUFFF + FABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU + AFFFFABRRRQAUUUUAYHib/kUZP8Ar5t//R8db9FFbS/hL1f6GS/iv0X6hRRR + WJqFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//2Q== + + + name + + vagrant + + passwd + + ******** + + passwordpolicyoptions + + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU + WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO + IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w + LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+ZmFp + bGVkTG9naW5Db3VudDwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4KCTxr + ZXk+ZmFpbGVkTG9naW5UaW1lc3RhbXA8L2tleT4KCTxkYXRlPjIwMDEtMDEt + MDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtleT5sYXN0TG9naW5UaW1lc3RhbXA8 + L2tleT4KCTxkYXRlPjIwMDEtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgo8L2Rp + Y3Q+CjwvcGxpc3Q+Cg== + + + realname + + vagrant + + shell + + /bin/bash + + uid + + 501 + + + diff --git a/spec/data/mac_users/10.8.shadow.xml b/spec/data/mac_users/10.8.shadow.xml new file mode 100644 index 0000000000..c4b8ec8363 --- /dev/null +++ b/spec/data/mac_users/10.8.shadow.xml @@ -0,0 +1,21 @@ + + + + + SALTED-SHA512-PBKDF2 + + entropy + + 6kwtJl2AG6DsDfzNJT38HekcvggGtKzB7X/iKuvPa+tTRND0QuWQ/6BNZ5B1 + 2jr7EZ5BtyterwjuSqVGk3ImRtUZ7gSEPeuKPpd0KNM/Yl6DiHkT5cE7cANZ + YV4ArXvD56DJivw+GdE2AnJFT40zqSFNL76L5o0fmCGyZokxI2Y= + + iterations + 39840 + salt + + +ZTvL3O3xVlOvRVTMAl2sgczzg4k1ll4PYfz2By7tqk= + + + + diff --git a/spec/data/mac_users/10.9.plist.xml b/spec/data/mac_users/10.9.plist.xml new file mode 100644 index 0000000000..be3456ea6c --- /dev/null +++ b/spec/data/mac_users/10.9.plist.xml @@ -0,0 +1,560 @@ + + + + + KerberosKeys + + + MIIBVKEDAgEBoIIBSzCCAUcwc6ErMCmgAwIBEqEiBCAJxcIcjX3sMb98++d0 + YvKqc351+CJJTMpyJO5mwWFMCaJEMEKgAwIBA6E7BDlMS0RDOlNIQTEuNEEy + NDA4NDVBMjU0OUZCOEUwRjI4NEU1NkUyODE3NzU2RUU5Q0QyMnZhZ3JhbnQw + Y6EbMBmgAwIBEaESBBDzYvuM3CLsLOGCIX4FJ8vdokQwQqADAgEDoTsEOUxL + REM6U0hBMS40QTI0MDg0NUEyNTQ5RkI4RTBGMjg0RTU2RTI4MTc3NTZFRTlD + RDIydmFncmFudDBroSMwIaADAgEQoRoEGCkvuVvN92vqnm0cy+9GWNBoIEoW + XtUNx6JEMEKgAwIBA6E7BDlMS0RDOlNIQTEuNEEyNDA4NDVBMjU0OUZCOEUw + RjI4NEU1NkUyODE3NzU2RUU5Q0QyMnZhZ3JhbnQ= + + + ShadowHashData + + + YnBsaXN0MDDRAQJfEBRTQUxURUQtU0hBNTEyLVBCS0RGMtMDBAUGBwhXZW50 + cm9weVRzYWx0Wml0ZXJhdGlvbnNPEIASYBqQ2xfL+LpICOY4L7DTudimwaGQ + R3v2gKshr7YGVGcTblXMIIpvdBVuPa8g+xM2nvS3uvoEfYA1n7RqSKStzNVI + 67M4UbCTR8yoQ0Gn+TonFHND+J+4Q/tGwAF9Jkr6SXa6rPlBuRW9HsHKJMML + PnWeAkA+AvWf5/9ZOKdjbE8QIO6VS+Ry/cYN34lIR4FDOZNiXwBq9uyBDAj0 + mn5BOUahEYayCAsiKTE2QcTnAAAAAAAAAQEAAAAAAAAACQAAAAAAAAAAAAAA + AAAAAOo= + + + _writers_hint + + vagrant + + _writers_jpegphoto + + vagrant + + _writers_passwd + + vagrant + + _writers_picture + + vagrant + + authentication_authority + + ;ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2> + ;Kerberosv5;;vagrant@LKDC:SHA1.4A240845A2549FB8E0F284E56E2817756EE9CD22;LKDC:SHA1.4A240845A2549FB8E0F284E56E2817756EE9CD22 + + generateduid + + 11112222-3333-4444-AAAA-BBBBCCCCDDDD + + gid + + 80 + + home + + /Users/vagrant + + jpegphoto + + + /9j/4AAQSkZJRgABAQAAAQABAAD/4ge4SUNDX1BST0ZJTEUAAQEAAAeoYXBw + bAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBs + AAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAA + AG9kc2NtAAABeAAABWxjcHJ0AAAG5AAAADh3dHB0AAAHHAAAABRyWFlaAAAH + MAAAABRnWFlaAAAHRAAAABRiWFlaAAAHWAAAABRyVFJDAAAHbAAAAA5jaGFk + AAAHfAAAACxiVFJDAAAHbAAAAA5nVFJDAAAHbAAAAA5kZXNjAAAAAAAAABRH + ZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAUR2VuZXJpYyBSR0IgUHJv + ZmlsZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAbWx1YwAAAAAAAAAeAAAADHNrU0sAAAAoAAABeGhySFIAAAAo + AAABoGNhRVMAAAAkAAAByHB0QlIAAAAmAAAB7HVrVUEAAAAqAAACEmZyRlUA + AAAoAAACPHpoVFcAAAAWAAACZGl0SVQAAAAoAAACem5iTk8AAAAmAAAComtv + S1IAAAAWAAACyGNzQ1oAAAAiAAAC3mhlSUwAAAAeAAADAGRlREUAAAAsAAAD + Hmh1SFUAAAAoAAADSnN2U0UAAAAmAAAConpoQ04AAAAWAAADcmphSlAAAAAa + AAADiHJvUk8AAAAkAAADomVsR1IAAAAiAAADxnB0UE8AAAAmAAAD6G5sTkwA + AAAoAAAEDmVzRVMAAAAmAAAD6HRoVEgAAAAkAAAENnRyVFIAAAAiAAAEWmZp + RkkAAAAoAAAEfHBsUEwAAAAsAAAEpHJ1UlUAAAAiAAAE0GFyRUcAAAAmAAAE + 8mVuVVMAAAAmAAAFGGRhREsAAAAuAAAFPgBWAWEAZQBvAGIAZQBjAG4A/QAg + AFIARwBCACAAcAByAG8AZgBpAGwARwBlAG4AZQByAGkBDQBrAGkAIABSAEcA + QgAgAHAAcgBvAGYAaQBsAFAAZQByAGYAaQBsACAAUgBHAEIAIABnAGUAbgDo + AHIAaQBjAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8E + FwQwBDMEMAQ7BEwEPQQ4BDkAIAQ/BEAEPgREBDAEOQQ7ACAAUgBHAEIAUABy + AG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCkBp1KAAgAFIA + RwBCACCCcl9pY8+P8ABQAHIAbwBmAGkAbABvACAAUgBHAEIAIABnAGUAbgBl + AHIAaQBjAG8ARwBlAG4AZQByAGkAcwBrACAAUgBHAEIALQBwAHIAbwBmAGkA + bMd8vBgAIABSAEcAQgAg1QS4XNMMx3wATwBiAGUAYwBuAP0AIABSAEcAQgAg + AHAAcgBvAGYAaQBsBeQF6AXVBeQF2QXcACAAUgBHAEIAIAXbBdwF3AXZAEEA + bABsAGcAZQBtAGUAaQBuAGUAcwAgAFIARwBCAC0AUAByAG8AZgBpAGwAwQBs + AHQAYQBsAOEAbgBvAHMAIABSAEcAQgAgAHAAcgBvAGYAaQBsZm6QGgAgAFIA + RwBCACBjz4/wZYdO9k4AgiwAIABSAEcAQgAgMNcw7TDVMKEwpDDrAFAAcgBv + AGYAaQBsACAAUgBHAEIAIABnAGUAbgBlAHIAaQBjA5MDtQO9A7kDugPMACAD + wAPBA78DxgOvA7sAIABSAEcAQgBQAGUAcgBmAGkAbAAgAFIARwBCACAAZwBl + AG4A6QByAGkAYwBvAEEAbABnAGUAbQBlAGUAbgAgAFIARwBCAC0AcAByAG8A + ZgBpAGUAbA5CDhsOIw5EDh8OJQ5MACAAUgBHAEIAIA4XDjEOSA4nDkQOGwBH + AGUAbgBlAGwAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUA + bgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAFUAbgBpAHcAZQByAHMAYQBs + AG4AeQAgAHAAcgBvAGYAaQBsACAAUgBHAEIEHgQxBEkEOAQ5ACAEPwRABD4E + RAQ4BDsETAAgAFIARwBCBkUGRAZBACAGKgY5BjEGSgZBACAAUgBHAEIAIAYn + BkQGOQYnBkUARwBlAG4AZQByAGkAYwAgAFIARwBCACAAUAByAG8AZgBpAGwA + ZQBHAGUAbgBlAHIAZQBsACAAUgBHAEIALQBiAGUAcwBrAHIAaQB2AGUAbABz + AGV0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdo + dHMgcmVzZXJ2ZWQuAFhZWiAAAAAAAADzUgABAAAAARbPWFlaIAAAAAAAAHRN + AAA97gAAA9BYWVogAAAAAAAAWnUAAKxzAAAXNFhZWiAAAAAAAAAoGgAAFZ8A + ALg2Y3VydgAAAAAAAAABAc0AAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA + /ZH///ui///9owAAA9wAAMBs/9sAQwACAgICAgECAgICAgICAwMGBAMDAwMH + BQUEBggHCAgIBwgICQoNCwkJDAoICAsPCwwNDg4ODgkLEBEPDhENDg4O/9sA + QwECAgIDAwMGBAQGDgkICQ4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O + Dg4ODg4ODg4ODg4ODg4ODg4ODg4O/8AAEQgBMQEuAwEiAAIRAQMRAf/EAB8A + AAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE + AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYX + GBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3 + eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfI + ycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEB + AQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMR + BAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJico + KSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG + h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW + 19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A/fyiis6/1Wz0 + 6P8AfyAyY+WJeWP4dvxoA0aw9R1+zsN0at9puR/Ah4H1NcjqHiG9vQ0cZ+yQ + H+FD8x+p/wAK5+gDWu9a1C8u1kad4gpyiRkqF/z71u6d4pI2xaiu4dPOQc/i + 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 + By602uefd9PRdPxfmFFFFfJH1oUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU + UUAFFFFABRRRQAUUUUAFFFFABRRRQAV5L8Ufgj8O/i7obW/i7RI21JY9ttq9 + piK9t/TbJj5h/suGX2r1qit8Niq2HqKpSk4yXVaHPisLRxNJ060VKL6PVH4s + fGP9kX4ifDIXWsaJHJ448IxksbywhP2m2X/ptCMnAHV13L3O3pXydX9LFfK/ + xj/ZL+HXxSF1q2mQp4K8Xvlv7R0+EeTct/03h4Dc9WXa3qT0r9a4f8TNqWYL + /t9fqv1X3H5HxD4Yb1cvf/bj/R/o/vPxl0H/AJHvRf8Ar9j/AJ19B5YPlSQc + 8EVzvjX4FfEb4QfFTR4vFWjPJpLagi22s2OZbOf5uPnx8jH+64VvYjmvrD4b + /s1+MPGbQalr6yeE/D74YSXEf+lTr/sRHoCP4mx6gNX3+YZ5gKdCOJlVXI1o + 73v6d35HwGW5FmFSvLDKk+dbq1reb7LzPlG4+Hsfi3W4rXRLG5TX7l9sSWMB + k89vRo16/VcGvtv4M/si6jB4O0ib4o3S2fkR86Pp8253+Yn95KPujn7q5P8A + tA19keB/hp4O+Hmj/ZfDOkx287qFnvpv3lzP/vyHnH+yML6Cu8r8oz3xDxOI + TpYT3I938Xy7fi/Q/Wci8OsNh5Kti/fl2Xw/Pv8AgvJmPoegaL4Z8Ow6ToGm + Wek6dF9yC2jCrnuT3LHuTknvWxRRX5xOcpycpO7Z+kQhGEVGKskFFFFSUFFF + FABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU + AFFFFABRRRQAUUUUAYHib/kUZP8Ar5t//R8db9FFbS/hL1f6GS/iv0X6hRRR + WJqFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//2Q== + + + name + + vagrant + + passwd + + ******** + + passwordpolicyoptions + + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU + WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO + IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w + LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+ZmFp + bGVkTG9naW5Db3VudDwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4KCTxr + ZXk+ZmFpbGVkTG9naW5UaW1lc3RhbXA8L2tleT4KCTxkYXRlPjIwMDEtMDEt + MDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtleT5sYXN0TG9naW5UaW1lc3RhbXA8 + L2tleT4KCTxkYXRlPjIwMDEtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtl + eT5wYXNzd29yZExhc3RTZXRUaW1lPC9rZXk+Cgk8ZGF0ZT4yMDE0LTAzLTA2 + VDE4OjU0OjQ1WjwvZGF0ZT4KPC9kaWN0Pgo8L3BsaXN0Pgo= + + + realname + + vagrant + + shell + + /bin/bash + + uid + + 501 + + + diff --git a/spec/data/mac_users/10.9.shadow.xml b/spec/data/mac_users/10.9.shadow.xml new file mode 100644 index 0000000000..b8359d080a --- /dev/null +++ b/spec/data/mac_users/10.9.shadow.xml @@ -0,0 +1,21 @@ + + + + + SALTED-SHA512-PBKDF2 + + entropy + + EmAakNsXy/i6SAjmOC+w07nYpsGhkEd79oCrIa+2BlRnE25VzCCKb3QVbj2v + IPsTNp70t7r6BH2ANZ+0akikrczVSOuzOFGwk0fMqENBp/k6JxRzQ/ifuEP7 + RsABfSZK+kl2uqz5QbkVvR7ByiTDCz51ngJAPgL1n+f/WTinY2w= + + iterations + 34482 + salt + + 7pVL5HL9xg3fiUhHgUM5k2JfAGr27IEMCPSafkE5RqE= + + + + diff --git a/spec/functional/resource/user/dscl_spec.rb b/spec/functional/resource/user/dscl_spec.rb new file mode 100644 index 0000000000..5f13bfcb0b --- /dev/null +++ b/spec/functional/resource/user/dscl_spec.rb @@ -0,0 +1,198 @@ +# +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'chef/mixin/shell_out' + +metadata = { + :unix_only => true, + :requires_root => true, + :provider => {:user => Chef::Provider::User::Dscl} +} + +describe "Chef::Resource::User with Chef::Provider::User::Dscl provider", metadata do + include Chef::Mixin::ShellOut + + def clean_user + begin + shell_out!("/usr/bin/dscl . -delete '/Users/#{username}'") + rescue Mixlib::ShellOut::ShellCommandFailed + # Raised when the user is already cleaned + end + end + + def user_should_exist + shell_out("/usr/bin/dscl . -ls /Users").stdout.should include username + end + + def check_password(pass) + # In order to test the password we use dscl passwd command since + # that's the only command that gets the user password from CLI. + shell_out("dscl . -passwd /Users/greatchef #{pass} new_password").exitstatus.should == 0 + # Now reset the password back + shell_out("dscl . -passwd /Users/greatchef new_password #{pass}").exitstatus.should == 0 + end + + let(:node) do + n = Chef::Node.new + n.consume_external_attrs(OHAI_SYSTEM.data.dup, {}) + n + end + + let(:events) do + Chef::EventDispatch::Dispatcher.new + end + + let(:run_context) do + Chef::RunContext.new(node, {}, events) + end + + let(:username) do + "greatchef" + end + + let(:uid) { nil } + let(:gid) { 20 } + let(:home) { nil } + let(:manage_home) { false } + let(:password) { "XXXYYYZZZ" } + let(:comment) { "Great Chef" } + let(:shell) { "/bin/bash" } + let(:salt) { nil } + let(:iterations) { nil } + + let(:user_resource) do + r = Chef::Resource::User.new("TEST USER RESOURCE", run_context) + r.username(username) + r.uid(uid) + r.gid(gid) + r.home(home) + r.shell(shell) + r.comment(comment) + r.manage_home(manage_home) + r.password(password) + r.salt(salt) + r.iterations(iterations) + r + end + + before do + clean_user + end + + after(:each) do + clean_user + end + + describe "action :create" do + it "should create the user" do + user_resource.run_action(:create) + user_should_exist + check_password(password) + end + end + + describe "when user exists" do + before do + existing_resource = user_resource.dup + existing_resource.run_action(:create) + user_should_exist + end + + describe "when password is updated" do + it "should update the password of the user" do + user_resource.password("mykitchen") + user_resource.run_action(:create) + check_password("mykitchen") + end + end + end + + describe "when password is being set via shadow hash" do + let(:password) { + if node[:platform_version].start_with?("10.7.") + # On Mac 10.7 we only need to set the password + "c9b3bd1a0cde797eef0eff16c580dab996ba3a21961cccc\ +d0f5e65c61558243e50b1a490088bd4824e3b35562d383ca02260398\ +ef1979b302212ec1c5383d1d05fc8d843" + else + "c734b6e4787c3727bb35e29fdd92b97c\ +1de12df509577a045728255ec7c6c5f5\ +c18efa05ed02b682ffa7ebc05119900e\ +b1d4880833aa7a190afc13e2bf0936b8\ +20123e8c98f0f9bcac2a629d9163caac\ +9464a8c234f3919082400b4f939bb77b\ +c5adbbac718b7eb99463a7b679571e0f\ +1c9fef2ef08d0b9e9c2bcf644eed2ffc" + end + } + + let(:iterations) { 25000 } + let(:salt) { "9e2e7d5ee473b496fd24cf0bbfcaedfcb291ee21740e570d1e917e874f8788ca" } + + it "action :create should create the user" do + user_resource.run_action(:create) + user_should_exist + check_password("soawesome") + end + + describe "when user exists" do + before do + existing_resource = user_resource.dup + existing_resource.run_action(:create) + user_should_exist + end + + describe "when password is updated" do + it "should update the password of the user" do + user_resource.password("mykitchen") + user_resource.run_action(:create) + check_password("mykitchen") + end + end + end + end + + describe "when a user is member of some groups" do + let(:groups) { ["staff", "operator"] } + + before do + existing_resource = user_resource.dup + existing_resource.run_action(:create) + + groups.each do |group| + shell_out!("/usr/bin/dscl . -append '/Groups/#{group}' GroupMembership #{username}") + end + end + + after do + groups.each do |group| + # Do not raise an error when user is correctly removed + shell_out("/usr/bin/dscl . -delete '/Groups/#{group}' GroupMembership #{username}") + end + end + + it ":remove action removes the user from the groups and deletes the user"do + user_resource.run_action(:remove) + groups.each do |group| + # Do not raise an error when group is empty + shell_out("dscl . read /Groups/staff GroupMembership").stdout.should_not include(group) + end + end + end + +end diff --git a/spec/functional/resource/user/useradd_spec.rb b/spec/functional/resource/user/useradd_spec.rb new file mode 100644 index 0000000000..997276fc3c --- /dev/null +++ b/spec/functional/resource/user/useradd_spec.rb @@ -0,0 +1,685 @@ +# encoding: UTF-8 +# +# Author:: Daniel DeLeo () +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'functional/resource/base' +require 'chef/mixin/shell_out' + +def user_provider_for_platform + case ohai[:platform] + when "aix" + Chef::Provider::User::Aix + else + Chef::Provider::User::Useradd + end +end + +metadata = { :unix_only => true, + :requires_root => true, + :provider => {:user => user_provider_for_platform} +} + +describe Chef::Provider::User::Useradd, metadata do + + include Chef::Mixin::ShellOut + + + # Utility code for /etc/passwd interaction, avoid any caching of user records: + PwEntry = Struct.new(:name, :passwd, :uid, :gid, :gecos, :home, :shell) + + class UserNotFound < StandardError; end + + def pw_entry + passwd_file = File.open("/etc/passwd", "rb") {|f| f.read} + matcher = /^#{Regexp.escape(username)}.+$/ + if passwd_entry = passwd_file.scan(matcher).first + PwEntry.new(*passwd_entry.split(':')) + else + raise UserNotFound, "no entry matching #{matcher.inspect} found in /etc/passwd" + end + end + + def etc_shadow + case ohai[:platform] + when "aix" + File.open("/etc/security/passwd") {|f| f.read } + else + File.open("/etc/shadow") {|f| f.read } + end + end + + def supports_quote_in_username? + OHAI_SYSTEM["platform_family"] == "debian" + end + + def password_should_be_set + if ohai[:platform] == "aix" + pw_entry.passwd.should == "!" + else + pw_entry.passwd.should == "x" + end + end + + before do + # Silence shell_out live stream + Chef::Log.level = :warn + end + + after do + begin + pw_entry # will raise if the user doesn't exist + shell_out!("userdel", "-r", username, :returns => [0,12]) + rescue UserNotFound + # nothing to remove + end + end + + let(:node) do + n = Chef::Node.new + n.consume_external_attrs(OHAI_SYSTEM.data.dup, {}) + n + end + + let(:events) do + Chef::EventDispatch::Dispatcher.new + end + + let(:run_context) do + Chef::RunContext.new(node, {}, events) + end + + let(:username) do + "cf-test" + end + + let(:uid) { nil } + let(:home) { nil } + let(:manage_home) { false } + let(:password) { nil } + let(:system) { false } + let(:comment) { nil } + + let(:user_resource) do + r = Chef::Resource::User.new("TEST USER RESOURCE", run_context) + r.username(username) + r.uid(uid) + r.home(home) + r.comment(comment) + r.manage_home(manage_home) + r.password(password) + r.system(system) + r + end + + let(:expected_shadow) do + if ohai[:platform] == "aix" + expected_shadow = "cf-test" # For aix just check user entry in shadow file + else + expected_shadow = "cf-test:$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + let(:skip) { false } + + describe "action :create" do + + context "when the user does not exist beforehand" do + before do + if reason = skip + pending(reason) + end + user_resource.run_action(:create) + user_resource.should be_updated_by_last_action + end + + + it "ensures the user exists" do + pw_entry.name.should == username + end + + # On Debian, the only constraints are that usernames must neither start + # with a dash ('-') nor plus ('+') nor tilde ('~') nor contain a colon + # (':'), a comma (','), or a whitespace (space: ' ', end of line: '\n', + # tabulation: '\t', etc.). Note that using a slash ('/') may break the + # default algorithm for the definition of the user's home directory. + + context "and the username contains a single quote" do + let(:skip) do + if supports_quote_in_username? + false + else + "Platform #{OHAI_SYSTEM["platform"]} not expected to support username w/ quote" + end + end + + let(:username) { "t'bilisi" } + + it "ensures the user exists" do + pw_entry.name.should == username + end + end + + + context "when uid is set" do + # Should verify uid not in use... + let(:uid) { 1999 } + + it "ensures the user has the given uid" do + pw_entry.uid.should == "1999" + end + end + + context "when comment is set" do + let(:comment) { "hello this is dog" } + + it "ensures the comment is set" do + pw_entry.gecos.should == "hello this is dog" + end + + context "in standard gecos format" do + let(:comment) { "Bobo T. Clown,some building,555-555-5555,@boboclown" } + + it "ensures the comment is set" do + pw_entry.gecos.should == comment + end + end + + context "to a string containing multibyte characters" do + let(:comment) { "(╯°□°)╯︵ ┻━┻" } + + it "ensures the comment is set" do + actual = pw_entry.gecos + actual.force_encoding(Encoding::UTF_8) if "".respond_to?(:force_encoding) + actual.should == comment + end + end + + context "to a string containing an apostrophe `'`" do + let(:comment) { "don't go" } + + it "ensures the comment is set" do + pw_entry.gecos.should == comment + end + end + end + + context "when home is set" do + let(:home) { "/home/#{username}" } + + it "ensures the user's home is set to the given path" do + pw_entry.home.should == "/home/#{username}" + end + + if %w{rhel fedora}.include?(OHAI_SYSTEM["platform_family"]) + # Inconsistent behavior. See: CHEF-2205 + it "creates the home dir when not explicitly asked to on RHEL (XXX)" do + File.should exist("/home/#{username}") + end + else + it "does not create the home dir without `manage_home'" do + File.should_not exist("/home/#{username}") + end + end + + context "and manage_home is enabled" do + let(:manage_home) { true } + + it "ensures the user's home directory exists" do + File.should exist("/home/#{username}") + end + end + end + + context "when a password is specified" do + # openssl passwd -1 "secretpassword" + let(:password) do + case ohai[:platform] + when "aix" + "eL5qfEVznSNss" + else + "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + it "sets the user's shadow password" do + password_should_be_set + etc_shadow.should include(expected_shadow) + end + end + + context "when a system user is specified" do + let(:system) { true } + let(:uid_min) do + case ohai[:platform] + when "aix" + # UIDs and GIDs below 100 are typically reserved for system accounts and services + # http://www.ibm.com/developerworks/aix/library/au-satuidgid/ + 100 + else + # from `man useradd`, login user means uid will be between + # UID_SYS_MIN and UID_SYS_MAX defined in /etc/login.defs. On my + # Ubuntu 13.04 system, these are commented out, so we'll look at + # UID_MIN to find the lower limit of the non-system-user range, and + # use that value in our assertions. + login_defs = File.open("/etc/login.defs", "rb") {|f| f.read } + uid_min_scan = /^UID_MIN\s+(\d+)/ + login_defs.match(uid_min_scan)[1] + end + end + + it "ensures the user has the properties of a system user" do + pw_entry.uid.to_i.should be < uid_min.to_i + end + end + end # when the user does not exist beforehand + + context "when the user already exists" do + + let(:expect_updated?) { true } + + let(:existing_uid) { nil } + let(:existing_home) { nil } + let(:existing_manage_home) { false } + let(:existing_password) { nil } + let(:existing_system) { false } + let(:existing_comment) { nil } + + let(:existing_user) do + r = Chef::Resource::User.new("TEST USER RESOURCE", run_context) + # username is identity attr, must match. + r.username(username) + r.uid(existing_uid) + r.home(existing_home) + r.comment(existing_comment) + r.manage_home(existing_manage_home) + r.password(existing_password) + r.system(existing_system) + r + end + + before do + if reason = skip + pending(reason) + end + existing_user.run_action(:create) + existing_user.should be_updated_by_last_action + user_resource.run_action(:create) + user_resource.updated_by_last_action?.should == expect_updated? + end + + context "and all properties are in the desired state" do + let(:uid) { 1999 } + let(:home) { "/home/bobo" } + let(:manage_home) { true } + # openssl passwd -1 "secretpassword" + let(:password) do + case ohai[:platform] + when "aix" + "eL5qfEVznSNss" + else + "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + let(:system) { false } + let(:comment) { "hello this is dog" } + + let(:existing_uid) { uid } + let(:existing_home) { home } + let(:existing_manage_home) { manage_home } + let(:existing_password) { password } + let(:existing_system) { false } + let(:existing_comment) { comment } + + let(:expect_updated?) { false } + + it "does not update the user" do + user_resource.should_not be_updated + end + end + + context "and the uid is updated" do + let(:uid) { 1999 } + let(:existing_uid) { 1998 } + + it "ensures the uid is set to the desired value" do + pw_entry.uid.should == "1999" + end + end + + context "and the comment is updated" do + let(:comment) { "hello this is dog" } + let(:existing_comment) { "woof" } + + it "ensures the comment field is set to the desired value" do + pw_entry.gecos.should == "hello this is dog" + end + end + + context "and home directory is updated" do + let(:existing_home) { "/home/foo" } + let(:home) { "/home/bar" } + it "ensures the home directory is set to the desired value" do + pw_entry.home.should == "/home/bar" + end + + context "and manage_home is enabled" do + let(:existing_manage_home) { true } + let(:manage_home) { true } + it "moves the home directory to the new location" do + File.should_not exist("/home/foo") + File.should exist("/home/bar") + end + end + + context "and manage_home wasn't enabled but is now" do + let(:existing_manage_home) { false } + let(:manage_home) { true } + + if %w{rhel fedora}.include?(OHAI_SYSTEM["platform_family"]) + # Inconsistent behavior. See: CHEF-2205 + it "created the home dir b/c of CHEF-2205 so it still exists" do + # This behavior seems contrary to expectation and non-convergent. + File.should_not exist("/home/foo") + File.should exist("/home/bar") + end + elsif ohai[:platform] == "aix" + it "creates the home dir in the desired location" do + File.should_not exist("/home/foo") + File.should exist("/home/bar") + end + else + it "does not create the home dir in the desired location (XXX)" do + # This behavior seems contrary to expectation and non-convergent. + File.should_not exist("/home/foo") + File.should_not exist("/home/bar") + end + end + end + + context "and manage_home was enabled but is not now" do + let(:existing_manage_home) { true } + let(:manage_home) { false } + + it "leaves the old home directory around (XXX)" do + # Would it be better to remove the old home? + File.should exist("/home/foo") + File.should_not exist("/home/bar") + end + end + end + + context "and a password is added" do + # openssl passwd -1 "secretpassword" + let(:password) do + case ohai[:platform] + when "aix" + "eL5qfEVznSNss" + else + "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + + it "ensures the password is set" do + password_should_be_set + etc_shadow.should include(expected_shadow) + end + + end + + context "and the password is updated" do + # openssl passwd -1 "OLDpassword" + let(:existing_password) do + case ohai[:platform] + when "aix" + "jkzG6MvUxjk2g" + else + "$1$1dVmwm4z$CftsFn8eBDjDRUytYKkXB." + end + end + + # openssl passwd -1 "secretpassword" + let(:password) do + case ohai[:platform] + when "aix" + "eL5qfEVznSNss" + else + "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + + it "ensures the password is set to the desired value" do + password_should_be_set + etc_shadow.should include(expected_shadow) + end + end + + context "and the user is changed from not-system to system" do + let(:existing_system) { false } + let(:system) { true } + + let(:expect_updated?) { false } + + it "does not modify the user at all" do + end + end + + context "and the user is changed from system to not-system" do + let(:existing_system) { true } + let(:system) { false } + + let(:expect_updated?) { false } + + it "does not modify the user at all" do + end + end + + end # when the user already exists + end # action :create + + shared_context "user exists for lock/unlock" do + let(:user_locked_context?) { false } + + def shadow_entry + etc_shadow.lines.select {|l| l.include?(username) }.first + end + + def shadow_password + shadow_entry.split(':')[1] + end + + def aix_user_lock_status + lock_info = shell_out!("lsuser -a account_locked #{username}") + status = /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] + end + + def user_account_should_be_locked + case ohai[:platform] + when "aix" + aix_user_lock_status.should == "true" + else + shadow_password.should include("!") + end + end + + def user_account_should_be_unlocked + case ohai[:platform] + when "aix" + aix_user_lock_status.should == "false" + else + shadow_password.should_not include("!") + end + end + + def lock_user_account + case ohai[:platform] + when "aix" + shell_out!("chuser account_locked=true #{username}") + else + shell_out!("usermod -L #{username}") + end + end + + before do + # create user and setup locked/unlocked state + user_resource.dup.run_action(:create) + + if user_locked_context? + lock_user_account + user_account_should_be_locked + elsif password + user_account_should_be_unlocked + end + end + end + + describe "action :lock" do + context "when the user does not exist" do + it "raises a sensible error" do + expect { user_resource.run_action(:lock) }.to raise_error(Chef::Exceptions::User) + end + end + + context "when the user exists" do + + include_context "user exists for lock/unlock" + + before do + user_resource.run_action(:lock) + end + + context "and the user is not locked" do + # user will be locked if it has no password + let(:password) do + case ohai[:platform] + when "aix" + "eL5qfEVznSNss" + else + "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + + it "locks the user's password" do + user_account_should_be_locked + end + end + + context "and the user is locked" do + # user will be locked if it has no password + let(:password) do + case ohai[:platform] + when "aix" + "eL5qfEVznSNss" + else + "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + let(:user_locked_context?) { true } + it "does not update the user" do + user_resource.should_not be_updated_by_last_action + end + end + end + end # action :lock + + describe "action :unlock" do + context "when the user does not exist" do + it "raises a sensible error" do + expect { user_resource.run_action(:unlock) }.to raise_error(Chef::Exceptions::User) + end + end + + context "when the user exists" do + + include_context "user exists for lock/unlock" + + before do + begin + user_resource.run_action(:unlock) + @error = nil + rescue Exception => e + @error = e + end + end + + context "and has no password" do + + # TODO: platform_family should be setup in spec_helper w/ tags + if %w[suse opensuse].include?(OHAI_SYSTEM["platform_family"]) + # suse gets this right: + it "errors out trying to unlock the user" do + @error.should be_a(Mixlib::ShellOut::ShellCommandFailed) + @error.message.should include("Cannot unlock the password") + end + else + + # borked on all other platforms: + it "is marked as updated but doesn't modify the user (XXX)" do + # This should be an error instead; note that usermod still exits 0 + # (which is probably why this case silently fails): + # + # DEBUG: ---- Begin output of usermod -U chef-functional-test ---- + # DEBUG: STDOUT: + # DEBUG: STDERR: usermod: unlocking the user's password would result in a passwordless account. + # You should set a password with usermod -p to unlock this user's password. + # DEBUG: ---- End output of usermod -U chef-functional-test ---- + # DEBUG: Ran usermod -U chef-functional-test returned 0 + @error.should be_nil + if ohai[:platform] == "aix" + pw_entry.passwd.should == '*' + else + pw_entry.passwd.should == 'x' + end + user_account_should_be_unlocked + end + end + end + + context "and has a password" do + let(:password) do + case ohai[:platform] + when "aix" + "eL5qfEVznSNss" + else + "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" + end + end + + context "and the user is not locked" do + it "does not update the user" do + user_resource.should_not be_updated_by_last_action + end + end + + context "and the user is locked" do + let(:user_locked_context?) { true } + + it "unlocks the user's password" do + user_account_should_be_unlocked + end + end + end + end + end # action :unlock + +end diff --git a/spec/functional/resource/user_spec.rb b/spec/functional/resource/user_spec.rb deleted file mode 100644 index 1e9f924b34..0000000000 --- a/spec/functional/resource/user_spec.rb +++ /dev/null @@ -1,685 +0,0 @@ -# encoding: UTF-8 -# -# Author:: Daniel DeLeo () -# Copyright:: Copyright (c) 2013 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'spec_helper' -require 'functional/resource/base' -require 'chef/mixin/shell_out' - -def user_provider_for_platform - case ohai[:platform] - when "aix" - Chef::Provider::User::Aix - else - Chef::Provider::User::Useradd - end -end - -metadata = { :unix_only => true, - :requires_root => true, - :provider => {:user => user_provider_for_platform} -} - -describe Chef::Resource::User, metadata do - - include Chef::Mixin::ShellOut - - - # Utility code for /etc/passwd interaction, avoid any caching of user records: - PwEntry = Struct.new(:name, :passwd, :uid, :gid, :gecos, :home, :shell) - - class UserNotFound < StandardError; end - - def pw_entry - passwd_file = File.open("/etc/passwd", "rb") {|f| f.read} - matcher = /^#{Regexp.escape(username)}.+$/ - if passwd_entry = passwd_file.scan(matcher).first - PwEntry.new(*passwd_entry.split(':')) - else - raise UserNotFound, "no entry matching #{matcher.inspect} found in /etc/passwd" - end - end - - def etc_shadow - case ohai[:platform] - when "aix" - File.open("/etc/security/passwd") {|f| f.read } - else - File.open("/etc/shadow") {|f| f.read } - end - end - - def supports_quote_in_username? - OHAI_SYSTEM["platform_family"] == "debian" - end - - def password_should_be_set - if ohai[:platform] == "aix" - pw_entry.passwd.should == "!" - else - pw_entry.passwd.should == "x" - end - end - - before do - # Silence shell_out live stream - Chef::Log.level = :warn - end - - after do - begin - pw_entry # will raise if the user doesn't exist - shell_out!("userdel", "-r", username, :returns => [0,12]) - rescue UserNotFound - # nothing to remove - end - end - - let(:node) do - n = Chef::Node.new - n.consume_external_attrs(OHAI_SYSTEM.data.dup, {}) - n - end - - let(:events) do - Chef::EventDispatch::Dispatcher.new - end - - let(:run_context) do - Chef::RunContext.new(node, {}, events) - end - - let(:username) do - "cf-test" - end - - let(:uid) { nil } - let(:home) { nil } - let(:manage_home) { false } - let(:password) { nil } - let(:system) { false } - let(:comment) { nil } - - let(:user_resource) do - r = Chef::Resource::User.new("TEST USER RESOURCE", run_context) - r.username(username) - r.uid(uid) - r.home(home) - r.comment(comment) - r.manage_home(manage_home) - r.password(password) - r.system(system) - r - end - - let(:expected_shadow) do - if ohai[:platform] == "aix" - expected_shadow = "cf-test" # For aix just check user entry in shadow file - else - expected_shadow = "cf-test:$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - let(:skip) { false } - - describe "action :create" do - - context "when the user does not exist beforehand" do - before do - if reason = skip - pending(reason) - end - user_resource.run_action(:create) - user_resource.should be_updated_by_last_action - end - - - it "ensures the user exists" do - pw_entry.name.should == username - end - - # On Debian, the only constraints are that usernames must neither start - # with a dash ('-') nor plus ('+') nor tilde ('~') nor contain a colon - # (':'), a comma (','), or a whitespace (space: ' ', end of line: '\n', - # tabulation: '\t', etc.). Note that using a slash ('/') may break the - # default algorithm for the definition of the user's home directory. - - context "and the username contains a single quote" do - let(:skip) do - if supports_quote_in_username? - false - else - "Platform #{OHAI_SYSTEM["platform"]} not expected to support username w/ quote" - end - end - - let(:username) { "t'bilisi" } - - it "ensures the user exists" do - pw_entry.name.should == username - end - end - - - context "when uid is set" do - # Should verify uid not in use... - let(:uid) { 1999 } - - it "ensures the user has the given uid" do - pw_entry.uid.should == "1999" - end - end - - context "when comment is set" do - let(:comment) { "hello this is dog" } - - it "ensures the comment is set" do - pw_entry.gecos.should == "hello this is dog" - end - - context "in standard gecos format" do - let(:comment) { "Bobo T. Clown,some building,555-555-5555,@boboclown" } - - it "ensures the comment is set" do - pw_entry.gecos.should == comment - end - end - - context "to a string containing multibyte characters" do - let(:comment) { "(╯°□°)╯︵ ┻━┻" } - - it "ensures the comment is set" do - actual = pw_entry.gecos - actual.force_encoding(Encoding::UTF_8) if "".respond_to?(:force_encoding) - actual.should == comment - end - end - - context "to a string containing an apostrophe `'`" do - let(:comment) { "don't go" } - - it "ensures the comment is set" do - pw_entry.gecos.should == comment - end - end - end - - context "when home is set" do - let(:home) { "/home/#{username}" } - - it "ensures the user's home is set to the given path" do - pw_entry.home.should == "/home/#{username}" - end - - if %w{rhel fedora}.include?(OHAI_SYSTEM["platform_family"]) - # Inconsistent behavior. See: CHEF-2205 - it "creates the home dir when not explicitly asked to on RHEL (XXX)" do - File.should exist("/home/#{username}") - end - else - it "does not create the home dir without `manage_home'" do - File.should_not exist("/home/#{username}") - end - end - - context "and manage_home is enabled" do - let(:manage_home) { true } - - it "ensures the user's home directory exists" do - File.should exist("/home/#{username}") - end - end - end - - context "when a password is specified" do - # openssl passwd -1 "secretpassword" - let(:password) do - case ohai[:platform] - when "aix" - "eL5qfEVznSNss" - else - "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - it "sets the user's shadow password" do - password_should_be_set - etc_shadow.should include(expected_shadow) - end - end - - context "when a system user is specified" do - let(:system) { true } - let(:uid_min) do - case ohai[:platform] - when "aix" - # UIDs and GIDs below 100 are typically reserved for system accounts and services - # http://www.ibm.com/developerworks/aix/library/au-satuidgid/ - 100 - else - # from `man useradd`, login user means uid will be between - # UID_SYS_MIN and UID_SYS_MAX defined in /etc/login.defs. On my - # Ubuntu 13.04 system, these are commented out, so we'll look at - # UID_MIN to find the lower limit of the non-system-user range, and - # use that value in our assertions. - login_defs = File.open("/etc/login.defs", "rb") {|f| f.read } - uid_min_scan = /^UID_MIN\s+(\d+)/ - login_defs.match(uid_min_scan)[1] - end - end - - it "ensures the user has the properties of a system user" do - pw_entry.uid.to_i.should be < uid_min.to_i - end - end - end # when the user does not exist beforehand - - context "when the user already exists" do - - let(:expect_updated?) { true } - - let(:existing_uid) { nil } - let(:existing_home) { nil } - let(:existing_manage_home) { false } - let(:existing_password) { nil } - let(:existing_system) { false } - let(:existing_comment) { nil } - - let(:existing_user) do - r = Chef::Resource::User.new("TEST USER RESOURCE", run_context) - # username is identity attr, must match. - r.username(username) - r.uid(existing_uid) - r.home(existing_home) - r.comment(existing_comment) - r.manage_home(existing_manage_home) - r.password(existing_password) - r.system(existing_system) - r - end - - before do - if reason = skip - pending(reason) - end - existing_user.run_action(:create) - existing_user.should be_updated_by_last_action - user_resource.run_action(:create) - user_resource.updated_by_last_action?.should == expect_updated? - end - - context "and all properties are in the desired state" do - let(:uid) { 1999 } - let(:home) { "/home/bobo" } - let(:manage_home) { true } - # openssl passwd -1 "secretpassword" - let(:password) do - case ohai[:platform] - when "aix" - "eL5qfEVznSNss" - else - "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - let(:system) { false } - let(:comment) { "hello this is dog" } - - let(:existing_uid) { uid } - let(:existing_home) { home } - let(:existing_manage_home) { manage_home } - let(:existing_password) { password } - let(:existing_system) { false } - let(:existing_comment) { comment } - - let(:expect_updated?) { false } - - it "does not update the user" do - user_resource.should_not be_updated - end - end - - context "and the uid is updated" do - let(:uid) { 1999 } - let(:existing_uid) { 1998 } - - it "ensures the uid is set to the desired value" do - pw_entry.uid.should == "1999" - end - end - - context "and the comment is updated" do - let(:comment) { "hello this is dog" } - let(:existing_comment) { "woof" } - - it "ensures the comment field is set to the desired value" do - pw_entry.gecos.should == "hello this is dog" - end - end - - context "and home directory is updated" do - let(:existing_home) { "/home/foo" } - let(:home) { "/home/bar" } - it "ensures the home directory is set to the desired value" do - pw_entry.home.should == "/home/bar" - end - - context "and manage_home is enabled" do - let(:existing_manage_home) { true } - let(:manage_home) { true } - it "moves the home directory to the new location" do - File.should_not exist("/home/foo") - File.should exist("/home/bar") - end - end - - context "and manage_home wasn't enabled but is now" do - let(:existing_manage_home) { false } - let(:manage_home) { true } - - if %w{rhel fedora}.include?(OHAI_SYSTEM["platform_family"]) - # Inconsistent behavior. See: CHEF-2205 - it "created the home dir b/c of CHEF-2205 so it still exists" do - # This behavior seems contrary to expectation and non-convergent. - File.should_not exist("/home/foo") - File.should exist("/home/bar") - end - elsif ohai[:platform] == "aix" - it "creates the home dir in the desired location" do - File.should_not exist("/home/foo") - File.should exist("/home/bar") - end - else - it "does not create the home dir in the desired location (XXX)" do - # This behavior seems contrary to expectation and non-convergent. - File.should_not exist("/home/foo") - File.should_not exist("/home/bar") - end - end - end - - context "and manage_home was enabled but is not now" do - let(:existing_manage_home) { true } - let(:manage_home) { false } - - it "leaves the old home directory around (XXX)" do - # Would it be better to remove the old home? - File.should exist("/home/foo") - File.should_not exist("/home/bar") - end - end - end - - context "and a password is added" do - # openssl passwd -1 "secretpassword" - let(:password) do - case ohai[:platform] - when "aix" - "eL5qfEVznSNss" - else - "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - - it "ensures the password is set" do - password_should_be_set - etc_shadow.should include(expected_shadow) - end - - end - - context "and the password is updated" do - # openssl passwd -1 "OLDpassword" - let(:existing_password) do - case ohai[:platform] - when "aix" - "jkzG6MvUxjk2g" - else - "$1$1dVmwm4z$CftsFn8eBDjDRUytYKkXB." - end - end - - # openssl passwd -1 "secretpassword" - let(:password) do - case ohai[:platform] - when "aix" - "eL5qfEVznSNss" - else - "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - - it "ensures the password is set to the desired value" do - password_should_be_set - etc_shadow.should include(expected_shadow) - end - end - - context "and the user is changed from not-system to system" do - let(:existing_system) { false } - let(:system) { true } - - let(:expect_updated?) { false } - - it "does not modify the user at all" do - end - end - - context "and the user is changed from system to not-system" do - let(:existing_system) { true } - let(:system) { false } - - let(:expect_updated?) { false } - - it "does not modify the user at all" do - end - end - - end # when the user already exists - end # action :create - - shared_context "user exists for lock/unlock" do - let(:user_locked_context?) { false } - - def shadow_entry - etc_shadow.lines.select {|l| l.include?(username) }.first - end - - def shadow_password - shadow_entry.split(':')[1] - end - - def aix_user_lock_status - lock_info = shell_out!("lsuser -a account_locked #{username}") - status = /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] - end - - def user_account_should_be_locked - case ohai[:platform] - when "aix" - aix_user_lock_status.should == "true" - else - shadow_password.should include("!") - end - end - - def user_account_should_be_unlocked - case ohai[:platform] - when "aix" - aix_user_lock_status.should == "false" - else - shadow_password.should_not include("!") - end - end - - def lock_user_account - case ohai[:platform] - when "aix" - shell_out!("chuser account_locked=true #{username}") - else - shell_out!("usermod -L #{username}") - end - end - - before do - # create user and setup locked/unlocked state - user_resource.dup.run_action(:create) - - if user_locked_context? - lock_user_account - user_account_should_be_locked - elsif password - user_account_should_be_unlocked - end - end - end - - describe "action :lock" do - context "when the user does not exist" do - it "raises a sensible error" do - expect { user_resource.run_action(:lock) }.to raise_error(Chef::Exceptions::User) - end - end - - context "when the user exists" do - - include_context "user exists for lock/unlock" - - before do - user_resource.run_action(:lock) - end - - context "and the user is not locked" do - # user will be locked if it has no password - let(:password) do - case ohai[:platform] - when "aix" - "eL5qfEVznSNss" - else - "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - - it "locks the user's password" do - user_account_should_be_locked - end - end - - context "and the user is locked" do - # user will be locked if it has no password - let(:password) do - case ohai[:platform] - when "aix" - "eL5qfEVznSNss" - else - "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - let(:user_locked_context?) { true } - it "does not update the user" do - user_resource.should_not be_updated_by_last_action - end - end - end - end # action :lock - - describe "action :unlock" do - context "when the user does not exist" do - it "raises a sensible error" do - expect { user_resource.run_action(:unlock) }.to raise_error(Chef::Exceptions::User) - end - end - - context "when the user exists" do - - include_context "user exists for lock/unlock" - - before do - begin - user_resource.run_action(:unlock) - @error = nil - rescue Exception => e - @error = e - end - end - - context "and has no password" do - - # TODO: platform_family should be setup in spec_helper w/ tags - if %w[suse opensuse].include?(OHAI_SYSTEM["platform_family"]) - # suse gets this right: - it "errors out trying to unlock the user" do - @error.should be_a(Mixlib::ShellOut::ShellCommandFailed) - @error.message.should include("Cannot unlock the password") - end - else - - # borked on all other platforms: - it "is marked as updated but doesn't modify the user (XXX)" do - # This should be an error instead; note that usermod still exits 0 - # (which is probably why this case silently fails): - # - # DEBUG: ---- Begin output of usermod -U chef-functional-test ---- - # DEBUG: STDOUT: - # DEBUG: STDERR: usermod: unlocking the user's password would result in a passwordless account. - # You should set a password with usermod -p to unlock this user's password. - # DEBUG: ---- End output of usermod -U chef-functional-test ---- - # DEBUG: Ran usermod -U chef-functional-test returned 0 - @error.should be_nil - if ohai[:platform] == "aix" - pw_entry.passwd.should == '*' - else - pw_entry.passwd.should == 'x' - end - user_account_should_be_unlocked - end - end - end - - context "and has a password" do - let(:password) do - case ohai[:platform] - when "aix" - "eL5qfEVznSNss" - else - "$1$RRa/wMM/$XltKfoX5ffnexVF4dHZZf/" - end - end - - context "and the user is not locked" do - it "does not update the user" do - user_resource.should_not be_updated_by_last_action - end - end - - context "and the user is locked" do - let(:user_locked_context?) { true } - - it "unlocks the user's password" do - user_account_should_be_unlocked - end - end - end - end - end # action :unlock - -end diff --git a/spec/unit/provider/user/dscl_spec.rb b/spec/unit/provider/user/dscl_spec.rb index c7657aa6e5..0ff5a26ea6 100644 --- a/spec/unit/provider/user/dscl_spec.rb +++ b/spec/unit/provider/user/dscl_spec.rb @@ -20,142 +20,226 @@ ShellCmdResult = Struct.new(:stdout, :stderr, :exitstatus) require 'spec_helper' require 'ostruct' +require 'mixlib/shellout' describe Chef::Provider::User::Dscl do - before do - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::User.new("toor") - @provider = Chef::Provider::User::Dscl.new(@new_resource, @run_context) - end + let(:node) { + node = Chef::Node.new + node.stub(:[]).with(:platform_version).and_return(mac_version) + node.stub(:[]).with(:platform).and_return("mac_os_x") + node + } + + let(:events) { + Chef::EventDispatch::Dispatcher.new + } + + let(:run_context) { + Chef::RunContext.new(node, {}, events) + } + + let(:new_resource) { + r = Chef::Resource::User.new("toor") + r.password(password) + r.salt(salt) + r.iterations(iterations) + r + } + + let(:provider) { + Chef::Provider::User::Dscl.new(new_resource, run_context) + } + + let(:mac_version) { + "10.9.1" + } + + let(:password) { nil } + let(:salt) { nil } + let(:iterations) { nil } + + let(:salted_sha512_password) { + "0f543f021c63255e64e121a3585601b8ecfedf6d2\ +705ddac69e682a33db5dbcdb9b56a2520bc8fff63a\ +2ba6b7984c0737ff0b7949455071581f7affcd536d\ +402b6cdb097" + } + + let(:salted_sha512_pbkdf2_password) { + "c734b6e4787c3727bb35e29fdd92b97c\ +1de12df509577a045728255ec7c6c5f5\ +c18efa05ed02b682ffa7ebc05119900e\ +b1d4880833aa7a190afc13e2bf0936b8\ +20123e8c98f0f9bcac2a629d9163caac\ +9464a8c234f3919082400b4f939bb77b\ +c5adbbac718b7eb99463a7b679571e0f\ +1c9fef2ef08d0b9e9c2bcf644eed2ffc" + } + + let(:salted_sha512_pbkdf2_salt) { + "2d942d8364a9ccf2b8e5cb7ed1ff58f78\ +e29dbfee7f9db58859144d061fd0058" + } + + let(:salted_sha512_pbkdf2_iterations) { + 25000 + } + + let(:vagrant_sha_512) { + "6f75d7190441facc34291ebbea1fc756b242d4f\ +e9bcff141bccb84f1979e27e539539aa31f9f7dcc92c0cea959\ +ea18e18b720e358e7fbe3cfbeaa561456f6ba008937a30" + } + + let(:vagrant_sha_512_pbkdf2) { + "12601a90db17cbf\ +8ba4808e6382fb0d3b9d8a6c1a190477bf680ab21afb\ +6065467136e55cc208a6f74156e3daf20fb13369ef4b\ +7bafa047d80359fb46a48a4adccd548ebb33851b093\ +47cca84341a7f93a27147343f89fb843fb46c0017d2\ +64afa4976baacf941b915bd1ec1ca24c30b3e759e02\ +403e02f59fe7ff5938a7636c" + } + + let(:vagrant_sha_512_pbkdf2_salt) { + "ee954be472fdc60ddf89484781433993625f006af6ec810c08f49a7e413946a1" + } + + let(:vagrant_sha_512_pbkdf2_iterations) { + 34482 + } describe "when shelling out to dscl" do it "should run dscl with the supplied cmd /Path args" do shell_return = ShellCmdResult.new('stdout', 'err', 0) - @provider.should_receive(:shell_out).with("dscl . -cmd /Path args").and_return(shell_return) - @provider.safe_dscl("cmd /Path args").should == 'stdout' + provider.should_receive(:shell_out).with("dscl . -cmd /Path args").and_return(shell_return) + provider.run_dscl("cmd /Path args").should == 'stdout' end it "returns an empty string from delete commands" do shell_return = ShellCmdResult.new('out', 'err', 23) - @provider.should_receive(:shell_out).with("dscl . -delete /Path args").and_return(shell_return) - @provider.safe_dscl("delete /Path args").should == "" + provider.should_receive(:shell_out).with("dscl . -delete /Path args").and_return(shell_return) + provider.run_dscl("delete /Path args").should == "" end it "should raise an exception for any other command" do shell_return = ShellCmdResult.new('out', 'err', 23) - @provider.should_receive(:shell_out).with('dscl . -cmd /Path arguments').and_return(shell_return) - lambda { @provider.safe_dscl("cmd /Path arguments") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + provider.should_receive(:shell_out).with('dscl . -cmd /Path arguments').and_return(shell_return) + lambda { provider.run_dscl("cmd /Path arguments") }.should raise_error(Chef::Exceptions::DsclCommandFailed) end it "raises an exception when dscl reports 'no such key'" do shell_return = ShellCmdResult.new("No such key: ", 'err', 23) - @provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) - lambda { @provider.safe_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) + lambda { provider.run_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) end it "raises an exception when dscl reports 'eDSRecordNotFound'" do shell_return = ShellCmdResult.new(" DS Error: -14136 (eDSRecordNotFound)", 'err', -14136) - @provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) - lambda { @provider.safe_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) + provider.should_receive(:shell_out).with('dscl . -cmd /Path args').and_return(shell_return) + lambda { provider.run_dscl("cmd /Path args") }.should raise_error(Chef::Exceptions::DsclCommandFailed) end end describe "get_free_uid" do before do - @provider.stub(:safe_dscl).and_return("\nwheel 200\nstaff 201\n") + provider.should_receive(:run_dscl).with("list /Users uid").and_return("\nwheel 200\nstaff 201\nbrahms 500\nchopin 501\n") end - it "should run safe_dscl with list /Users uid" do - @provider.should_receive(:safe_dscl).with("list /Users uid") - @provider.get_free_uid + describe "when resource is configured as system" do + before do + new_resource.system(true) + end + + it "should return the first unused uid number on or above 500" do + provider.get_free_uid.should eq(202) + end end it "should return the first unused uid number on or above 200" do - @provider.get_free_uid.should == 202 + provider.get_free_uid.should eq(502) end it "should raise an exception when the search limit is exhausted" do search_limit = 1 - lambda { @provider.get_free_uid(search_limit) }.should raise_error(RuntimeError) + lambda { provider.get_free_uid(search_limit) }.should raise_error(RuntimeError) end end describe "uid_used?" do - before do - @provider.stub(:safe_dscl).and_return("\naj 500\n") - end - - it "should run safe_dscl with list /Users uid" do - @provider.should_receive(:safe_dscl).with("list /Users uid") - @provider.uid_used?(500) + it "should return false if not given any valid uid number" do + provider.uid_used?(nil).should be_false end - it "should return true for a used uid number" do - @provider.uid_used?(500).should be_true - end + describe "when called with a user id" do + before do + provider.should_receive(:run_dscl).with("list /Users uid").and_return("\naj 500\n") + end - it "should return false for an unused uid number" do - @provider.uid_used?(501).should be_false - end + it "should return true for a used uid number" do + provider.uid_used?(500).should be_true + end - it "should return false if not given any valid uid number" do - @provider.uid_used?(nil).should be_false + it "should return false for an unused uid number" do + provider.uid_used?(501).should be_false + end end end describe "when determining the uid to set" do it "raises RequestedUIDUnavailable if the requested uid is already in use" do - @provider.stub(:uid_used?).and_return(true) - @provider.should_receive(:get_free_uid).and_return(501) - lambda { @provider.set_uid }.should raise_error(Chef::Exceptions::RequestedUIDUnavailable) + provider.stub(:uid_used?).and_return(true) + provider.should_receive(:get_free_uid).and_return(501) + lambda { provider.dscl_set_uid }.should raise_error(Chef::Exceptions::RequestedUIDUnavailable) end it "finds a valid, unused uid when none is specified" do - @provider.should_receive(:safe_dscl).with("list /Users uid").and_return('') - @provider.should_receive(:safe_dscl).with("create /Users/toor UniqueID 501") - @provider.should_receive(:get_free_uid).and_return(501) - @provider.set_uid - @new_resource.uid.should == 501 + provider.should_receive(:run_dscl).with("list /Users uid").and_return('') + provider.should_receive(:run_dscl).with("create /Users/toor UniqueID 501") + provider.should_receive(:get_free_uid).and_return(501) + provider.dscl_set_uid + new_resource.uid.should eq(501) end it "sets the uid specified in the resource" do - @new_resource.uid(1000) - @provider.should_receive(:safe_dscl).with("create /Users/toor UniqueID 1000").and_return(true) - @provider.should_receive(:safe_dscl).with("list /Users uid").and_return('') - @provider.set_uid + new_resource.uid(1000) + provider.should_receive(:run_dscl).with("create /Users/toor UniqueID 1000").and_return(true) + provider.should_receive(:run_dscl).with("list /Users uid").and_return('') + provider.dscl_set_uid end end describe "when modifying the home directory" do + let(:current_resource) { + new_resource.dup + } + before do - @new_resource.supports({ :manage_home => true }) - @new_resource.home('/Users/toor') + new_resource.supports({ :manage_home => true }) + new_resource.home('/Users/toor') - @current_resource = @new_resource.dup - @provider.current_resource = @current_resource + provider.current_resource = current_resource end it "deletes the home directory when resource#home is nil" do - @new_resource.instance_variable_set(:@home, nil) - @provider.should_receive(:safe_dscl).with("delete /Users/toor NFSHomeDirectory").and_return(true) - @provider.modify_home + new_resource.instance_variable_set(:@home, nil) + provider.should_receive(:run_dscl).with("delete /Users/toor NFSHomeDirectory").and_return(true) + provider.dscl_set_home end it "raises InvalidHomeDirectory when the resource's home directory doesn't look right" do - @new_resource.home('epic-fail') - lambda { @provider.modify_home }.should raise_error(Chef::Exceptions::InvalidHomeDirectory) + new_resource.home('epic-fail') + lambda { provider.dscl_set_home }.should raise_error(Chef::Exceptions::InvalidHomeDirectory) end it "moves the users home to the new location if it exists and the target location is different" do - @new_resource.supports(:manage_home => true) + new_resource.supports(:manage_home => true) current_home = CHEF_SPEC_DATA + '/old_home_dir' current_home_files = [current_home + '/my-dot-emacs', current_home + '/my-dot-vim'] - @current_resource.home(current_home) - @new_resource.gid(23) + current_resource.home(current_home) + new_resource.gid(23) ::File.stub(:exists?).with('/old/home/toor').and_return(true) ::File.stub(:exists?).with('/Users/toor').and_return(true) @@ -165,316 +249,630 @@ describe Chef::Provider::User::Dscl do FileUtils.should_receive(:mv).with(current_home_files, "/Users/toor", :force => true) FileUtils.should_receive(:chown_R).with('toor','23','/Users/toor') - @provider.should_receive(:safe_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'") - @provider.modify_home + provider.should_receive(:run_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'") + provider.dscl_set_home end it "should raise an exception when the systems user template dir (skel) cannot be found" do ::File.stub(:exists?).and_return(false,false,false) - lambda { @provider.modify_home }.should raise_error(Chef::Exceptions::User) + lambda { provider.dscl_set_home }.should raise_error(Chef::Exceptions::User) end it "should run ditto to copy any missing files from skel to the new home dir" do ::File.should_receive(:exists?).with("/System/Library/User\ Template/English.lproj").and_return(true) FileUtils.should_receive(:chown_R).with('toor', '', '/Users/toor') - @provider.should_receive(:shell_out!).with("ditto '/System/Library/User Template/English.lproj' '/Users/toor'") - @provider.ditto_home + provider.should_receive(:shell_out!).with("ditto '/System/Library/User Template/English.lproj' '/Users/toor'") + provider.ditto_home end it "creates the user's NFSHomeDirectory and home directory" do - @provider.should_receive(:safe_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'").and_return(true) - @provider.should_receive(:ditto_home) - @provider.modify_home + provider.should_receive(:run_dscl).with("create /Users/toor NFSHomeDirectory '/Users/toor'").and_return(true) + provider.should_receive(:ditto_home) + provider.dscl_set_home end end - describe "osx_shadow_hash?" do - it "should return true when the string is a shadow hash" do - @provider.osx_shadow_hash?("0"*8*155).should eql(true) + describe "resource_requirements" do + let(:dscl_exists) { true } + let(:plutil_exists) { true } + + before do + ::File.stub(:exists?).with("/usr/bin/dscl").and_return(dscl_exists) + ::File.stub(:exists?).with("/usr/bin/plutil").and_return(plutil_exists) end - it "should return false otherwise" do - @provider.osx_shadow_hash?("any other string").should eql(false) + def run_requirements + provider.define_resource_requirements + provider.action = :create + provider.process_resource_requirements end - end - describe "when detecting the format of a password" do - it "detects a OS X salted sha1" do - @provider.osx_salted_sha1?("0"*48).should eql(true) - @provider.osx_salted_sha1?("any other string").should eql(false) + describe "when dscl doesn't exist" do + let(:dscl_exists) { false } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end end - end - describe "guid" do - it "should run safe_dscl with read /Users/user GeneratedUID to get the users GUID" do - expected_uuid = "b398449e-cee0-45e0-80f8-b0b5b1bfdeaa" - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(expected_uuid + "\n") - @provider.guid.should == expected_uuid + describe "when plutil doesn't exist" do + let(:plutil_exists) { false } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when on Mac 10.6" do + let(:mac_version) { + "10.6.5" + } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when on Mac 10.7" do + let(:mac_version) { + "10.7.5" + } + + describe "when password is SALTED-SHA512" do + let(:password) { salted_sha512_password } + + it "should not raise an error" do + lambda { run_requirements }.should_not raise_error + end + end + + describe "when password is SALTED-SHA512-PBKDF2" do + let(:password) { salted_sha512_pbkdf2_password } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + end + + [ "10.9", "10.10"].each do |version| + describe "when on Mac #{version}" do + let(:mac_version) { + "#{version}.2" + } + + describe "when password is SALTED-SHA512" do + let(:password) { salted_sha512_password } + + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when password is SALTED-SHA512-PBKDF2" do + let(:password) { salted_sha512_pbkdf2_password } + + describe "when salt and iteration is not set" do + it "should raise an error" do + lambda { run_requirements }.should raise_error + end + end + + describe "when salt and iteration is set" do + let(:salt) { salted_sha512_pbkdf2_salt } + let(:iterations) { salted_sha512_pbkdf2_iterations } + + it "should not raise an error" do + lambda { run_requirements }.should_not raise_error + end + end + end + end end end - describe "shadow_hash_set?" do + describe "load_current_resource" do + # set this to any of the user plist files under spec/data + let(:user_plist_file) { nil } + + before do + provider.should_receive(:shell_out).with("plutil -convert xml1 -o - /var/db/dslocal/nodes/Default/users/toor.plist") do + if user_plist_file.nil? + ShellCmdResult.new('Can not find the file', 'Sorry!!', 1) + else + ShellCmdResult.new(File.read(File.join(CHEF_SPEC_DATA, "mac_users/#{user_plist_file}.plist.xml")), "", 0) + end + end - it "should run safe_dscl with read /Users/user to see if the AuthenticationAuthority key exists" do - @provider.should_receive(:safe_dscl).with("read /Users/toor") - @provider.shadow_hash_set? + if !user_plist_file.nil? + provider.should_receive(:convert_binary_plist_to_xml).and_return(File.read(File.join(CHEF_SPEC_DATA, "mac_users/#{user_plist_file}.shadow.xml"))) + end end - describe "when the user account has an AuthenticationAuthority key" do - it "uses the shadow hash when there is a ShadowHash field in the AuthenticationAuthority key" do - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.shadow_hash_set?.should be_true + describe "when user is not there" do + it "shouldn't raise an error" do + lambda { provider.load_current_resource }.should_not raise_error end - it "does not use the shadow hash when there is no ShadowHash field in the AuthenticationAuthority key" do - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: \n") - @provider.shadow_hash_set?.should be_false + it "should set @user_exists" do + provider.load_current_resource + provider.instance_variable_get(:@user_exists).should be_false end + it "should set username" do + provider.load_current_resource + provider.current_resource.username.should eq("toor") + end end - describe "with no AuthenticationAuthority key in the user account" do - it "does not use the shadow hash" do - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("") - @provider.shadow_hash_set?.should eql(false) + describe "when user is there" do + let(:password) { "something" } # Load password during load_current_resource + + describe "on 10.7" do + let(:mac_version) { + "10.7.5" + } + + let(:user_plist_file) { "10.7" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq(vagrant_sha_512) + end + + describe "when a plain password is set that is same" do + let(:password) { "vagrant" } + + it "diverged_password? should report false" do + provider.load_current_resource + pending + provider.diverged_password?.should be_false + end + end + + describe "when a plain password is set that is different" do + let(:password) { "not_vagrant" } + + it "diverged_password? should report true" do + provider.load_current_resource + pending + provider.diverged_password?.should be_true + end + end + + describe "when iterations change" do + let(:password) { vagrant_sha_512 } + let(:iterations) { 12345 } + + it "diverged_password? should report false" do + provider.load_current_resource + provider.diverged_password?.should be_false + end + end + + describe "when shadow hash changes" do + let(:password) { salted_sha512_password } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when salt change" do + let(:password) { vagrant_sha_512 } + let(:salt) { "SOMETHINGRANDOM" } + + it "diverged_password? should report false" do + provider.load_current_resource + provider.diverged_password?.should be_false + end + end + end + + describe "on 10.8" do + let(:mac_version) { + "10.8.3" + } + + let(:user_plist_file) { "10.8" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq("ea4c2d265d801ba0ec0dfccd\ +253dfc1de91cbe0806b4acc1ed7fe22aebcf6beb5344d0f442e590\ +ffa04d679075da3afb119e41b72b5eaf08ee4aa54693722646d5\ +19ee04843deb8a3e977428d33f625e83887913e5c13b70035961\ +5e00ad7bc3e7a0c98afc3e19d1360272454f8d33a9214d2fbe8b\ +e68d1f9821b26689312366") + provider.current_resource.salt.should eq("f994ef2f73b7c5594ebd1553300976b20733ce0e24d659783d87f3d81cbbb6a9") + provider.current_resource.iterations.should eq(39840) + end + end + + describe "on 10.7 upgraded to 10.8" do + # In this scenario user password is still in 10.7 format + let(:mac_version) { + "10.8.3" + } + + let(:user_plist_file) { "10.7-8" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq("6f75d7190441facc34291ebbea1fc756b242d4f\ +e9bcff141bccb84f1979e27e539539aa31f9f7dcc92c0cea959\ +ea18e18b720e358e7fbe3cfbeaa561456f6ba008937a30") + end + + describe "when a plain text password is set" do + it "reports password needs to be updated" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when a salted-sha512-pbkdf2 shadow is set" do + let(:password) { salted_sha512_pbkdf2_password } + let(:salt) { salted_sha512_pbkdf2_salt } + let(:iterations) { salted_sha512_pbkdf2_iterations } + + it "reports password needs to be updated" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + end + + describe "on 10.9" do + let(:mac_version) { + "10.9.1" + } + + let(:user_plist_file) { "10.9" } + + it "collects the user data correctly" do + provider.load_current_resource + provider.current_resource.comment.should eq("vagrant") + provider.current_resource.uid.should eq("11112222-3333-4444-AAAA-BBBBCCCCDDDD") + provider.current_resource.gid.should eq("80") + provider.current_resource.home.should eq("/Users/vagrant") + provider.current_resource.shell.should eq("/bin/bash") + provider.current_resource.password.should eq(vagrant_sha_512_pbkdf2) + provider.current_resource.salt.should eq(vagrant_sha_512_pbkdf2_salt) + provider.current_resource.iterations.should eq(vagrant_sha_512_pbkdf2_iterations) + end + + describe "when a plain password is set that is same" do + let(:password) { "vagrant" } + + it "diverged_password? should report false" do + provider.load_current_resource + provider.diverged_password?.should be_false + end + end + + describe "when a plain password is set that is different" do + let(:password) { "not_vagrant" } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when iterations change" do + let(:password) { vagrant_sha_512_pbkdf2 } + let(:salt) { vagrant_sha_512_pbkdf2_salt } + let(:iterations) { 12345 } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when shadow hash changes" do + let(:password) { salted_sha512_pbkdf2_password } + let(:salt) { vagrant_sha_512_pbkdf2_salt } + let(:iterations) { vagrant_sha_512_pbkdf2_iterations } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end + + describe "when salt change" do + let(:password) { vagrant_sha_512_pbkdf2 } + let(:salt) { salted_sha512_pbkdf2_salt } + let(:iterations) { vagrant_sha_512_pbkdf2_iterations } + + it "diverged_password? should report true" do + provider.load_current_resource + provider.diverged_password?.should be_true + end + end end end end - describe "when setting or modifying the user password" do - before do - @new_resource.password("password") - @output = StringIO.new + describe "salted_sha512_pbkdf2?" do + it "should return true when the string is a salted_sha512_pbkdf2 hash" do + provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password).should be_true end - describe "when using a salted sha1 for the password" do - before do - @new_resource.password("F"*48) - end - - it "should write a shadow hash file with the expected salted sha1" do - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - expected_salted_sha1 = @new_resource.password - expected_shadow_hash = "00000000"*155 - expected_shadow_hash[168] = expected_salted_sha1 - @provider.modify_password - @output.string.strip.should == expected_shadow_hash - end - end - - describe "when given a shadow hash file for the password" do - it "should write the shadow hash file directly to /var/db/shadow/hash/GUID" do - shadow_hash = '0123456789ABCDE0123456789ABCDEF' * 40 - raise 'oops' unless shadow_hash.size == 1240 - @new_resource.password shadow_hash - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.modify_password - @output.string.strip.should == shadow_hash - end - end - - describe "when given a string for the password" do - it "should output a salted sha1 and shadow hash file from the specified password" do - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with('/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA', "w", 384).and_yield(@output) - @new_resource.password("password") - OpenSSL::Random.stub(:random_bytes).and_return("\377\377\377\377\377\377\377\377") - expected_salted_sha1 = "F"*8+"SHA1-"*8 - expected_shadow_hash = "00000000"*155 - expected_shadow_hash[168] = expected_salted_sha1 - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.modify_password - @output.string.strip.should match(/^0{168}(FFFFFFFF1C1AA7935D4E1190AFEC92343F31F7671FBF126D)0{1071}$/) - end - end - - it "should write the output directly to the shadow hash file at /var/db/shadow/hash/GUID" do - shadow_file = StringIO.new - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with("/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA",'w',0600).and_yield(shadow_file) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority: ;ShadowHash;\n") - @provider.modify_password - shadow_file.string.should match(/^0{168}[0-9A-F]{48}0{1071}$/) - end - - it "should run safe_dscl append /Users/user AuthenticationAuthority ;ShadowHash; when no shadow hash set" do - shadow_file = StringIO.new - uuid = "B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA" - File.should_receive(:open).with("/var/db/shadow/hash/B398449E-CEE0-45E0-80F8-B0B5B1BFDEAA",'w',0600).and_yield(shadow_file) - @provider.should_receive(:safe_dscl).with("read /Users/toor GeneratedUID").and_return(uuid) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("\nAuthenticationAuthority:\n") - @provider.should_receive(:safe_dscl).with("append /Users/toor AuthenticationAuthority ';ShadowHash;'") - @provider.modify_password - shadow_file.string.should match(/^0{168}[0-9A-F]{48}0{1071}$/) + it "should return false otherwise" do + provider.salted_sha512_pbkdf2?(salted_sha512_password).should be_false + provider.salted_sha512_pbkdf2?("any other string").should be_false end end - describe "load_current_resource" do - it "should raise an error if the required binary /usr/bin/dscl doesn't exist" do - ::File.should_receive(:exists?).with("/usr/bin/dscl").and_return(false) - lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::User) + describe "salted_sha512?" do + it "should return true when the string is a salted_sha512_pbkdf2 hash" do + provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password).should be_true end - it "shouldn't raise an error if /usr/bin/dscl exists" do - ::File.stub(:exists?).and_return(true) - lambda { @provider.load_current_resource }.should_not raise_error + it "should return false otherwise" do + provider.salted_sha512?(salted_sha512_pbkdf2_password).should be_false + provider.salted_sha512?("any other string").should be_false + end + end + + describe "prepare_password_shadow_info" do + describe "when on Mac 10.7" do + let(:mac_version) { + "10.7.1" + } + + describe "when the password is plain text" do + let(:password) { "vagrant" } + + it "password_shadow_info should have salted-sha-512 format" do + pending + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512") + info = shadow_info["SALTED-SHA512"].string.unpack('H*').first + provider.salted_sha512?(info).should be_true + end + end + + describe "when the password is salted-sha-512" do + let(:password) { vagrant_sha_512 } + + it "password_shadow_info should have salted-sha-512 format" do + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512") + info = shadow_info["SALTED-SHA512"].string.unpack('H*').first + provider.salted_sha512?(info).should be_true + info.should eq(vagrant_sha_512) + end + end + end + + ["10.8", "10.9", "10.10"].each do |version| + describe "when on Mac #{version}" do + let(:mac_version) { + "#{version}.1" + } + + describe "when the password is plain text" do + let(:password) { "vagrant" } + + it "password_shadow_info should have salted-sha-512 format" do + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512-PBKDF2") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("entropy") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("salt") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("iterations") + info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first + provider.salted_sha512_pbkdf2?(info).should be_true + end + end + + describe "when the password is salted-sha-512" do + let(:password) { vagrant_sha_512_pbkdf2 } + let(:iterations) { vagrant_sha_512_pbkdf2_iterations } + let(:salt) { vagrant_sha_512_pbkdf2_salt } + + it "password_shadow_info should have salted-sha-512 format" do + shadow_info = provider.prepare_password_shadow_info + shadow_info.should have_key("SALTED-SHA512-PBKDF2") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("entropy") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("salt") + shadow_info["SALTED-SHA512-PBKDF2"].should have_key("iterations") + info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first + provider.salted_sha512_pbkdf2?(info).should be_true + info.should eq(vagrant_sha_512_pbkdf2) + end + end + end + end + end + + describe "set_password" do + before do + new_resource.password("something") + end + + it "should sleep and flush the dscl cache before saving the password" do + provider.should_receive(:prepare_password_shadow_info).and_return({ }) + mock_shellout = double("Mock::Shellout") + mock_shellout.stub(:run_command) + Mixlib::ShellOut.should_receive(:new).and_return(mock_shellout) + provider.should_receive(:read_user_info) + provider.should_receive(:dscl_set) + provider.should_receive(:sleep).with(3) + provider.should_receive(:shell_out).with("dscacheutil '-flushcache'") + provider.should_receive(:save_user_info) + provider.set_password end end describe "when the user does not yet exist and chef is creating it" do context "with a numeric gid" do before do - @new_resource.comment "#mockssuck" - @new_resource.gid 1001 + new_resource.comment "#mockssuck" + new_resource.gid 1001 end it "creates the user, comment field, sets uid, gid, configures the home directory, sets the shell, and sets the password" do - @provider.should_receive :dscl_create_user - @provider.should_receive :dscl_create_comment - @provider.should_receive :set_uid - @provider.should_receive :dscl_set_gid - @provider.should_receive :modify_home - @provider.should_receive :dscl_set_shell - @provider.should_receive :modify_password - @provider.create_user + provider.should_receive :dscl_create_user + provider.should_receive :dscl_create_comment + provider.should_receive :dscl_set_uid + provider.should_receive :dscl_set_gid + provider.should_receive :dscl_set_home + provider.should_receive :dscl_set_shell + provider.should_receive :set_password + provider.create_user end it "creates the user and sets the comment field" do - @provider.should_receive(:safe_dscl).with("create /Users/toor").and_return(true) - @provider.dscl_create_user + provider.should_receive(:run_dscl).with("create /Users/toor").and_return(true) + provider.dscl_create_user end it "sets the comment field" do - @provider.should_receive(:safe_dscl).with("create /Users/toor RealName '#mockssuck'").and_return(true) - @provider.dscl_create_comment + provider.should_receive(:run_dscl).with("create /Users/toor RealName '#mockssuck'").and_return(true) + provider.dscl_create_comment end - it "should run safe_dscl with create /Users/user PrimaryGroupID to set the users primary group" do - @provider.should_receive(:safe_dscl).with("create /Users/toor PrimaryGroupID '1001'").and_return(true) - @provider.dscl_set_gid + it "should run run_dscl with create /Users/user PrimaryGroupID to set the users primary group" do + provider.should_receive(:run_dscl).with("create /Users/toor PrimaryGroupID '1001'").and_return(true) + provider.dscl_set_gid end - it "should run safe_dscl with create /Users/user UserShell to set the users login shell" do - @provider.should_receive(:safe_dscl).with("create /Users/toor UserShell '/usr/bin/false'").and_return(true) - @provider.dscl_set_shell + it "should run run_dscl with create /Users/user UserShell to set the users login shell" do + provider.should_receive(:run_dscl).with("create /Users/toor UserShell '/usr/bin/false'").and_return(true) + provider.dscl_set_shell end end context "with a non-numeric gid" do before do - @new_resource.comment "#mockssuck" - @new_resource.gid "newgroup" + new_resource.comment "#mockssuck" + new_resource.gid "newgroup" end it "should map the group name to a numeric ID when the group exists" do - @provider.should_receive(:safe_dscl).with("read /Groups/newgroup PrimaryGroupID").ordered.and_return("PrimaryGroupID: 1001\n") - @provider.should_receive(:safe_dscl).with("create /Users/toor PrimaryGroupID '1001'").ordered.and_return(true) - @provider.dscl_set_gid + provider.should_receive(:run_dscl).with("read /Groups/newgroup PrimaryGroupID").ordered.and_return("PrimaryGroupID: 1001\n") + provider.should_receive(:run_dscl).with("create /Users/toor PrimaryGroupID '1001'").ordered.and_return(true) + provider.dscl_set_gid end it "should raise an exception when the group does not exist" do shell_return = ShellCmdResult.new(" DS Error: -14136 (eDSRecordNotFound)", 'err', -14136) - @provider.should_receive(:shell_out).with('dscl . -read /Groups/newgroup PrimaryGroupID').and_return(shell_return) - lambda { @provider.dscl_set_gid }.should raise_error(Chef::Exceptions::GroupIDNotFound) + provider.should_receive(:shell_out).with('dscl . -read /Groups/newgroup PrimaryGroupID').and_return(shell_return) + lambda { provider.dscl_set_gid }.should raise_error(Chef::Exceptions::GroupIDNotFound) end end end describe "when the user exists and chef is managing it" do before do - @current_resource = @new_resource.dup - @provider.current_resource = @current_resource + current_resource = new_resource.dup + provider.current_resource = current_resource - # These are all different from @current_resource - @new_resource.username "mud" - @new_resource.uid 2342 - @new_resource.gid 2342 - @new_resource.home '/Users/death' - @new_resource.password 'goaway' + # These are all different from current_resource + new_resource.username "mud" + new_resource.uid 2342 + new_resource.gid 2342 + new_resource.home '/Users/death' + new_resource.password 'goaway' end it "sets the user, comment field, uid, gid, moves the home directory, sets the shell, and sets the password" do - @provider.should_receive :dscl_create_user - @provider.should_receive :dscl_create_comment - @provider.should_receive :set_uid - @provider.should_receive :dscl_set_gid - @provider.should_receive :modify_home - @provider.should_receive :dscl_set_shell - @provider.should_receive :modify_password - @provider.create_user + provider.should_receive :dscl_create_user + provider.should_receive :dscl_create_comment + provider.should_receive :dscl_set_uid + provider.should_receive :dscl_set_gid + provider.should_receive :dscl_set_home + provider.should_receive :dscl_set_shell + provider.should_receive :set_password + provider.create_user end end describe "when changing the gid" do before do - @current_resource = @new_resource.dup - @provider.current_resource = @current_resource + current_resource = new_resource.dup + provider.current_resource = current_resource - # This is different from @current_resource - @new_resource.gid 2342 + # This is different from current_resource + new_resource.gid 2342 end it "sets the gid" do - @provider.should_receive :dscl_set_gid - @provider.manage_user + provider.should_receive :dscl_set_gid + provider.manage_user end end - describe "when the user exists and chef is removing it" do - it "removes the user's home directory when the resource is configured to manage home" do - @new_resource.supports({ :manage_home => true }) - @provider.should_receive(:safe_dscl).with("read /Users/toor").and_return("NFSHomeDirectory: /Users/fuuuuuuuuuuuuu") - @provider.should_receive(:safe_dscl).with("delete /Users/toor") - FileUtils.should_receive(:rm_rf).with("/Users/fuuuuuuuuuuuuu") - @provider.remove_user + describe "when the user exists" do + before do + provider.should_receive(:shell_out).with("plutil -convert xml1 -o - /var/db/dslocal/nodes/Default/users/toor.plist") do + ShellCmdResult.new(File.read(File.join(CHEF_SPEC_DATA, "mac_users/10.9.plist.xml")), "", 0) + end + provider.load_current_resource + end + + describe "when Chef is removing the user" do + it "removes the user from the groups and deletes home directory when the resource is configured to manage home" do + new_resource.supports({ :manage_home => true }) + provider.should_receive(:run_dscl).with("list /Groups").and_return("my_group\nyour_group\nreal_group\n") + provider.should_receive(:run_dscl).with("read /Groups/my_group").and_raise(Chef::Exceptions::DsclCommandFailed) # Empty group + provider.should_receive(:run_dscl).with("read /Groups/your_group").and_return("GroupMembership: not_you") + provider.should_receive(:run_dscl).with("read /Groups/real_group").and_return("GroupMembership: toor") + provider.should_receive(:run_dscl).with("delete /Groups/real_group GroupMembership 'toor'") + provider.should_receive(:run_dscl).with("delete /Users/toor") + FileUtils.should_receive(:rm_rf).with("/Users/vagrant") + provider.remove_user + end end - it "removes the user from any group memberships" do - Etc.stub(:group).and_yield(OpenStruct.new(:name => 'ragefisters', :mem => 'toor')) - @provider.should_receive(:safe_dscl).with("delete /Users/toor") - @provider.should_receive(:safe_dscl).with("delete /Groups/ragefisters GroupMembership 'toor'") - @provider.remove_user + describe "when user is not locked" do + it "determines the user as not locked" do + provider.should_not be_locked + end end - end - - describe "when discovering if a user is locked" do - it "determines the user is not locked when dscl shows an AuthenticationAuthority without a DisabledUser field" do - @provider.should_receive(:safe_dscl).with("read /Users/toor") - @provider.should_not be_locked - end + describe "when user is locked" do + before do + auth_authority = provider.instance_variable_get(:@authentication_authority) + provider.instance_variable_set(:@authentication_authority, auth_authority + ";DisabledUser;") + end - it "determines the user is locked when dscl shows an AuthenticationAuthority with a DisabledUser field" do - @provider.should_receive(:safe_dscl).with('read /Users/toor').and_return("\nAuthenticationAuthority: ;DisabledUser;\n") - @provider.should be_locked - end + it "determines the user as not locked" do + provider.should be_locked + end - it "determines the user is not locked when dscl shows no AuthenticationAuthority" do - @provider.should_receive(:safe_dscl).with('read /Users/toor').and_return("\n") - @provider.should_not be_locked + it "can unlock the user" do + provider.should_receive(:run_dscl).with("create /Users/toor AuthenticationAuthority ';ShadowHash;HASHLIST:'") + provider.unlock_user + end end end describe "when locking the user" do - it "should run safe_dscl with append /Users/user AuthenticationAuthority ;DisabledUser; to lock the user account" do - @provider.should_receive(:safe_dscl).with("append /Users/toor AuthenticationAuthority ';DisabledUser;'") - @provider.lock_user + it "should run run_dscl with append /Users/user AuthenticationAuthority ;DisabledUser; to lock the user account" do + provider.should_receive(:run_dscl).with("append /Users/toor AuthenticationAuthority ';DisabledUser;'") + provider.lock_user end end - describe "when unlocking the user" do - it "removes DisabledUser from the authentication string" do - @provider.should_receive(:safe_dscl).with("read /Users/toor AuthenticationAuthority").and_return("\nAuthenticationAuthority: ;ShadowHash; ;DisabledUser;\n") - @provider.should_receive(:safe_dscl).with("create /Users/toor AuthenticationAuthority ';ShadowHash;'") - @provider.unlock_user - end - end end diff --git a/spec/unit/provider/user/useradd_spec.rb b/spec/unit/provider/user/useradd_spec.rb index 64734c499f..a65da3f9e1 100644 --- a/spec/unit/provider/user/useradd_spec.rb +++ b/spec/unit/provider/user/useradd_spec.rb @@ -19,6 +19,7 @@ # require 'spec_helper' +require 'chef/provider/user/useradd' describe Chef::Provider::User::Useradd do -- cgit v1.2.1