diff options
author | Tim Smith <tsmith84@gmail.com> | 2020-11-24 12:28:38 -0800 |
---|---|---|
committer | Tim Smith <tsmith84@gmail.com> | 2020-11-24 12:45:07 -0800 |
commit | f2523cb55f63c94dfd393adb490863e48a383e0b (patch) | |
tree | 0cd5534f397fe80ddffb17a3a6f6ebcf968ebc56 | |
parent | 3d7728a6ae3f7baed8c3d6bd4f1612607f6bce74 (diff) | |
download | chef-remove_dscl.tar.gz |
Fully remove user resource support for macOS < 10.14remove_dscl
This builds on the removal of macOS 10.13 from the build matrix in https://github.com/chef/chef/pull/10680. With 10.14+ required for Chef packages we can safely remove the dscl_user resource and just assume 10.14+ when creating users.
Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r-- | lib/chef/provider/user/dscl.rb | 637 | ||||
-rw-r--r-- | lib/chef/provider/user/mac.rb | 5 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/user/dscl_user.rb | 35 | ||||
-rw-r--r-- | lib/chef/resource/user/mac_user.rb | 2 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/functional/resource/user/dscl_spec.rb | 188 | ||||
-rw-r--r-- | spec/functional/resource/user/mac_user_spec.rb | 4 | ||||
-rw-r--r-- | spec/spec_helper.rb | 2 | ||||
-rw-r--r-- | spec/support/platform_helpers.rb | 8 | ||||
-rw-r--r-- | spec/unit/provider/user/dscl_spec.rb | 699 | ||||
-rwxr-xr-x | tasks/docs.rb | 2 |
12 files changed, 6 insertions, 1578 deletions
diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb deleted file mode 100644 index 7b266b8d62..0000000000 --- a/lib/chef/provider/user/dscl.rb +++ /dev/null @@ -1,637 +0,0 @@ -# -# Author:: Dreamcat4 (<dreamcat4@gmail.com>) -# Copyright:: Copyright (c) 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_relative "../../mixin/shell_out" -require_relative "../user" -require_relative "../../resource/user/dscl_user" -autoload :OpenSSL, "openssl" -autoload :Plist, "plist" -require_relative "../../util/path_helper" - -class Chef - class Provider - class User - # - # The most tricky bit of this provider is the way it deals with user passwords. - # macOS 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 separately in the same file - # - # This provider only supports macOS versions 10.7 to 10.13 - class Dscl < Chef::Provider::User - - attr_accessor :user_info - attr_accessor :authentication_authority - attr_accessor :password_shadow_conversion_algorithm - - provides :dscl_user - provides :user, os: "darwin", platform_version: "<= 10.13" - - # Just-in-case a recipe calls the user dscl provider without specifying - # a gid property. Avoids chown issues in move_home when the manage_home - # property is in use. #5393 - STAFF_GROUP_ID = 20 - - def define_resource_requirements - super - - requirements.assert(:all_actions) do |a| - a.assertion { ::File.exist?("/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.exist?("/usr/bin/plutil") } - a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/plutil' on the system for #{new_resource}!") - end - - requirements.assert(:create, :modify, :manage) do |a| - a.assertion do - if new_resource.password - # SALTED-SHA512 password shadow hashes are not supported on 10.8 and above. - !salted_sha512?(new_resource.password) - else - true - end - end - a.failure_message(Chef::Exceptions::User, "SALTED-SHA512 passwords are not supported on Mac 10.8 and above. \ -If you want to set the user password using shadow info make sure you specify a SALTED-SHA512-PBKDF2 shadow hash \ -in 'password', with the associated 'salt' and 'iterations'.") - end - - requirements.assert(:create, :modify, :manage) do |a| - a.assertion do - if new_resource.password && salted_sha512_pbkdf2?(new_resource.password) - # salt and iterations should be specified when - # SALTED-SHA512-PBKDF2 password shadow hash is given - !new_resource.salt.nil? && !new_resource.iterations.nil? - else - true - end - end - a.failure_message(Chef::Exceptions::User, "SALTED-SHA512-PBKDF2 shadow hash is given without associated \ -'salt' and 'iterations'. Please specify 'salt' and 'iterations' in order to set the user password using shadow hash.") - end - end - - def load_current_resource - @current_resource = Chef::Resource::User::DsclUser.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-PBKDF2"] # 10.7+ contains this, but we retain the check in case it goes away in the future - @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 - logger.trace("#{new_resource} user does not exist") - end - - current_resource - end - - # - # Provider Actions - # - - def create_user - dscl_create_user - # set_password modifies the plist file of the user directly. So update - # the password first before making any modifications to the user. - set_password - dscl_create_comment - dscl_set_uid - dscl_set_gid - dscl_set_home - dscl_set_shell - end - - def manage_user - # set_password modifies the plist file of the user directly. So update - # the password first before making any modifications to the user. - set_password if diverged_password? - 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) - 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. If `comment` is not specified, it takes `username` value. - # - def dscl_create_comment - comment = new_resource.comment || new_resource.username - run_dscl("create", "/Users/#{new_resource.username}", "RealName", 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, 501 otherwise. - # - def dscl_set_uid - # XXX: mutates the new resource - 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 - - # - # Find the next available uid on the system. starting with 200 if `system` is set, - # 501 otherwise. - # - def get_free_uid(search_limit = 1000) - uid = nil - base_uid = new_resource.system ? 200 : 501 - next_uid_guess = base_uid - users_uids = run_dscl("list", "/Users", "uid") - while next_uid_guess < search_limit + base_uid - if users_uids&.match?(Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n")) - next_uid_guess += 1 - else - uid = next_uid_guess - break - end - end - 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 = run_dscl("list", "/Users", "uid").split("\n") - uid_map = users_uids.each_with_object({}) do |tuid, tmap| - x = tuid.split - tmap[x[1]] = x[0] - tmap - end - if uid_map[uid.to_s] - unless uid_map[uid.to_s] == new_resource.username - return true - end - end - false - end - - # - # Sets the group id for the user using dscl. Fails if a group doesn't - # exist on the system with given group id. If `gid` is not specified, it - # sets a default Mac user group "staff", with id 20 using the CONSTANT - # - def dscl_set_gid - if new_resource.gid.nil? - # XXX: mutates the new resource - new_resource.gid(STAFF_GROUP_ID) - elsif !new_resource.gid.to_s.match(/^\d+$/) - begin - possible_gid = run_dscl("read", "/Groups/#{new_resource.gid}", "PrimaryGroupID").split(" ").last - rescue Chef::Exceptions::DsclCommandFailed - raise Chef::Exceptions::GroupIDNotFound, "Group not found for #{new_resource.gid} when creating user #{new_resource.username}" - end - # XXX: mutates the new resource - new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) - end - run_dscl("create", "/Users/#{new_resource.username}", "PrimaryGroupID", new_resource.gid) - end - - # - # 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.manage_home - validate_home_dir_specification! - - if (current_resource.home == new_resource.home) && !new_home_exists? - ditto_home - elsif !current_home_exists? && !new_home_exists? - ditto_home - elsif current_home_exists? - move_home - end - end - run_dscl("create", "/Users/#{new_resource.username}", "NFSHomeDirectory", new_resource.home) - end - - def validate_home_dir_specification! - unless %r{^/}.match?(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? - !!current_resource.home && ::File.exist?(current_resource.home) - end - - def new_home_exists? - ::File.exist?(new_resource.home) - end - - def ditto_home - shell_out!("/usr/sbin/createhomedir", "-c", "-u", (new_resource.username).to_s) - end - - def move_home - logger.trace("#{new_resource} moving #{self} home from #{current_resource.home} to #{new_resource.home}") - new_resource.gid(STAFF_GROUP_ID) if new_resource.gid.nil? - src = current_resource.home - FileUtils.mkdir_p(new_resource.home) - files = ::Dir.glob("#{Chef::Util::PathHelper.escape_glob_dir(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 - - # - # Sets the shell for the user using dscl. - # - def dscl_set_shell - if new_resource.shell - run_dscl("create", "/Users/#{new_resource.username}", "UserShell", new_resource.shell) - else - run_dscl("create", "/Users/#{new_resource.username}", "UserShell", "/usr/bin/false") - end - end - - # - # 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 - shell_out("plutil", "-convert", "binary1", "-o", "-", "-", - input: shadow_info.to_plist, live_stream: shadow_info_binary) - - if user_info.nil? - # User is just created. read_user_info() will read the fresh information - # for the user with a cache flush. However with experimentation we've seen - # that dscl cache is not immediately updated after the creation of the user - # This is odd and needs to be investigated further. - sleep 3 - @user_info = read_user_info - end - - # Replace the shadow info in user's plist - dscl_set(user_info, :shadow_hash, shadow_info_binary) - save_user_info(user_info) - end - - # - # Prepares the password shadow info based on the platform version. - # - def prepare_password_shadow_info - shadow_info = {} - entropy = nil - salt = nil - iterations = nil - - 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.new("SHA512") - ) - end - - 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 - 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.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 - - # - # Locks the user. - # - def lock_user - run_dscl("append", "/Users/#{new_resource.username}", "AuthenticationAuthority", ";DisabledUser;") - end - - # - # 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 - - # - # Returns true if the user is locked, false otherwise. - # - def locked? - if authentication_authority - !!(authentication_authority =~ /DisabledUser/ ) - else - false - end - end - - # - # This is the interface base User provider requires to provide idempotency. - # - def check_lock - @locked = locked? - end - - # - # Helper functions - # - - # - # Returns true if the system state and desired state is different for - # given attribute. - # - def diverged?(parameter) - parameter_updated?(parameter) && !new_resource.send(parameter).nil? - end - - def parameter_updated?(parameter) - !(new_resource.send(parameter) == current_resource.send(parameter)) - end - - # - # 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. - # - # Some system users don't have salts; this can happen if the system is - # upgraded and the user hasn't logged in yet. In this case, we will force - # the password to be updated. - return true if current_resource.salt.nil? - - if salted_sha512_pbkdf2?(new_resource.password) - diverged?(:password) || diverged?(:salt) || diverged?(:iterations) - else - !salted_sha512_pbkdf2_password_match? - end - end - - # - # 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 - # 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 - - # - # DSCL Helper functions - # - - # A simple map of Chef's terms to DSCL's terms. - DSCL_PROPERTY_MAP = { - uid: "uid", - 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 - - # We flush the cache here in order to make sure that we read fresh information - # for the user. - shell_out("dscacheutil", "-flushcache") # FIXME: this is macOS version dependent - - 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::PlistUtilCommandFailed - end - - user_info - end - - # - # 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 - - # - # 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.key?(key) - - user_hash[DSCL_PROPERTY_MAP[key]] = [ value ] - user_hash - end - - # - # 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.key?(key) - - # DSCL values are set as arrays - value = user_hash[DSCL_PROPERTY_MAP[key]] - value.nil? ? value : value.first - end - - # - # System Helpers - # - - def run_dscl(*args) - result = shell_out("dscl", ".", "-#{args[0]}", args[1..]) - 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.include?("No such key: ") - - result.stdout - end - - def run_plutil(*args) - result = shell_out("plutil", "-#{args[0]}", args[1..]) - raise(Chef::Exceptions::PlistUtilCommandFailed, "plutil error: #{result.inspect}") unless result.exitstatus == 0 - - if result.stdout.encoding == Encoding::ASCII_8BIT - result.stdout.encode("utf-8", "binary", undef: :replace, invalid: :replace, replace: "?") - else - result.stdout - end - end - - def convert_binary_plist_to_xml(binary_plist_string) - shell_out("plutil", "-convert", "xml1", "-o", "-", "-", input: binary_plist_string).stdout - end - - 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_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.new("SHA512") - ).unpack("H*").first == current_resource.password - end - - end - end - end -end diff --git a/lib/chef/provider/user/mac.rb b/lib/chef/provider/user/mac.rb index aef831907a..74d9748d5b 100644 --- a/lib/chef/provider/user/mac.rb +++ b/lib/chef/provider/user/mac.rb @@ -29,13 +29,12 @@ class Chef class User # A macOS user provider that is compatible with default TCC restrictions # in macOS 10.14. See resource/user/mac_user.rb for complete description - # of the mac_user resource and how it differs from the dscl resource used - # on previous platforms. + # of the mac_user resource class MacUser < Chef::Provider::User include Chef::Mixin::Which provides :mac_user - provides :user, os: "darwin", platform_version: ">= 10.14" + provides :user, os: "darwin" attr_reader :user_plist, :admin_group_plist diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 7652d60896..331f224855 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -94,7 +94,6 @@ require_relative "provider/service/aixinit" require_relative "provider/service/aix" require_relative "provider/user/aix" -require_relative "provider/user/dscl" require_relative "provider/user/linux" require_relative "provider/user/mac" require_relative "provider/user/pw" diff --git a/lib/chef/resource/user/dscl_user.rb b/lib/chef/resource/user/dscl_user.rb deleted file mode 100644 index 91efd657de..0000000000 --- a/lib/chef/resource/user/dscl_user.rb +++ /dev/null @@ -1,35 +0,0 @@ -# -# Copyright:: Copyright (c) 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_relative "../user" - -class Chef - class Resource - class User - class DsclUser < Chef::Resource::User - unified_mode true - - provides :dscl_user - provides :user, platform: "mac_os_x", platform_version: "< 10.14" - - property :iterations, Integer, - description: "macOS platform only. The number of iterations for a password with a SALTED-SHA512-PBKDF2 shadow hash.", - default: 27855, desired_state: false - end - end - end -end diff --git a/lib/chef/resource/user/mac_user.rb b/lib/chef/resource/user/mac_user.rb index 2331283bbd..93eef5e893 100644 --- a/lib/chef/resource/user/mac_user.rb +++ b/lib/chef/resource/user/mac_user.rb @@ -61,7 +61,7 @@ class Chef unified_mode true provides :mac_user - provides :user, platform: "mac_os_x", platform_version: ">= 10.14" + provides :user, platform: "mac_os_x" introduced "15.3" diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 843d5610b8..8ae922a28b 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -125,7 +125,6 @@ require_relative "resource/smartos_package" require_relative "resource/template" require_relative "resource/user" require_relative "resource/user/aix_user" -require_relative "resource/user/dscl_user" require_relative "resource/user/linux_user" require_relative "resource/user/mac_user" require_relative "resource/user/pw_user" diff --git a/spec/functional/resource/user/dscl_spec.rb b/spec/functional/resource/user/dscl_spec.rb deleted file mode 100644 index 50da812b0f..0000000000 --- a/spec/functional/resource/user/dscl_spec.rb +++ /dev/null @@ -1,188 +0,0 @@ -# -# Copyright:: Copyright (c) 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 = { - macos_1013: true, - requires_root: true, -} - -describe "Chef::Resource::User with Chef::Provider::User::Dscl provider", metadata do - include Chef::Mixin::ShellOut - - def clean_user - shell_out!("/usr/bin/dscl . -delete '/Users/#{username}'") - rescue Mixlib::ShellOut::ShellCommandFailed - # Raised when the user is already cleaned - end - - def user_should_exist - expect(shell_out("/usr/bin/dscl . -ls /Users").stdout).to 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. - expect(shell_out("dscl . -passwd /Users/greatchef #{pass} new_password").exitstatus).to eq(0) - # Now reset the password back - expect(shell_out("dscl . -passwd /Users/greatchef new_password #{pass}").exitstatus).to eq(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::DsclUser.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) do - "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) { %w{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 - expect(shell_out("dscl . read /Groups/staff GroupMembership").stdout).not_to include(group) - end - end - end - -end diff --git a/spec/functional/resource/user/mac_user_spec.rb b/spec/functional/resource/user/mac_user_spec.rb index dabc303afb..32701da644 100644 --- a/spec/functional/resource/user/mac_user_spec.rb +++ b/spec/functional/resource/user/mac_user_spec.rb @@ -19,9 +19,9 @@ require "spec_helper" require "chef/mixin/shell_out" metadata = { - macos_gte_1014: true, requires_root: true, -} + macos_only: true, + } describe "Chef::Resource::User with Chef::Provider::User::MacUser provider", metadata do include Chef::Mixin::ShellOut diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4c925bace3..f439ccb54c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -143,8 +143,6 @@ RSpec.configure do |config| config.filter_run_excluding not_supported_on_windows: true if windows? config.filter_run_excluding not_supported_on_macos: true if macos? config.filter_run_excluding macos_only: true unless macos? - config.filter_run_excluding macos_1013: true unless macos_1013? - config.filter_run_excluding macos_gte_1014: true unless macos_gte_1014? config.filter_run_excluding not_supported_on_aix: true if aix? config.filter_run_excluding not_supported_on_solaris: true if solaris? config.filter_run_excluding not_supported_on_gce: true if gce? diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index b29c860f30..83f2fcf172 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -92,14 +92,6 @@ def windows_user_right?(right) Chef::ReservedNames::Win32::Security.get_account_right(ENV["USERNAME"]).include?(right) end -def macos_1013? - macos? && Gem::Requirement.new("~> 10.13.0").satisfied_by?(Gem::Version.new(ohai[:platform_version])) -end - -def macos_gte_1014? - macos? && Gem::Requirement.new(">= 10.14").satisfied_by?(Gem::Version.new(ohai[:platform_version])) -end - # detects if the hardware is 64-bit (evaluates to true in "WOW64" mode in a 32-bit app on a 64-bit system) def windows64? windows? && ( ENV["PROCESSOR_ARCHITECTURE"] == "AMD64" || ENV["PROCESSOR_ARCHITEW6432"] == "AMD64" ) diff --git a/spec/unit/provider/user/dscl_spec.rb b/spec/unit/provider/user/dscl_spec.rb deleted file mode 100644 index 5652ae6868..0000000000 --- a/spec/unit/provider/user/dscl_spec.rb +++ /dev/null @@ -1,699 +0,0 @@ -# -# Author:: Dreamcat4 (<dreamcat4@gmail.com>) -# Copyright:: Copyright (c) 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 "ostruct" - -describe Chef::Provider::User::Dscl do - before do - allow(ChefUtils).to receive(:windows?) { false } - end - - let(:shellcmdresult) { Struct.new(:stdout, :stderr, :exitstatus) } - - let(:password) { nil } - let(:salt) { nil } - let(:iterations) { nil } - - let(:events) { Chef::EventDispatch::Dispatcher.new } - - let(:node) do - Chef::Node.new.tap do |node| - node.automatic["os"] = "darwin" - node.automatic["platform_version"] = "10.13.0" - end - end - - let(:run_context) { Chef::RunContext.new(node, {}, events) } - - let(:new_resource) do - r = Chef::Resource::User::DsclUser.new("toor", run_context) - r.password(password) - r.salt(salt) - r.iterations(iterations) - r - end - - let(:provider) do - Chef::Provider::User::Dscl.new(new_resource, run_context) - end - - let(:salted_sha512_password) do - "0f543f021c63255e64e121a3585601b8ecfedf6d2\ -705ddac69e682a33db5dbcdb9b56a2520bc8fff63a\ -2ba6b7984c0737ff0b7949455071581f7affcd536d\ -402b6cdb097" - end - - let(:salted_sha512_pbkdf2_password) do - "c734b6e4787c3727bb35e29fdd92b97c\ -1de12df509577a045728255ec7c6c5f5\ -c18efa05ed02b682ffa7ebc05119900e\ -b1d4880833aa7a190afc13e2bf0936b8\ -20123e8c98f0f9bcac2a629d9163caac\ -9464a8c234f3919082400b4f939bb77b\ -c5adbbac718b7eb99463a7b679571e0f\ -1c9fef2ef08d0b9e9c2bcf644eed2ffc" - end - - let(:salted_sha512_pbkdf2_salt) do - "2d942d8364a9ccf2b8e5cb7ed1ff58f78\ -e29dbfee7f9db58859144d061fd0058" - end - - let(:salted_sha512_pbkdf2_iterations) do - 25000 - end - - let(:vagrant_sha_512) do - "6f75d7190441facc34291ebbea1fc756b242d4f\ -e9bcff141bccb84f1979e27e539539aa31f9f7dcc92c0cea959\ -ea18e18b720e358e7fbe3cfbeaa561456f6ba008937a30" - end - - let(:vagrant_sha_512_pbkdf2) do - "12601a90db17cbf\ -8ba4808e6382fb0d3b9d8a6c1a190477bf680ab21afb\ -6065467136e55cc208a6f74156e3daf20fb13369ef4b\ -7bafa047d80359fb46a48a4adccd548ebb33851b093\ -47cca84341a7f93a27147343f89fb843fb46c0017d2\ -64afa4976baacf941b915bd1ec1ca24c30b3e759e02\ -403e02f59fe7ff5938a7636c" - end - - let(:vagrant_sha_512_pbkdf2_salt) do - "ee954be472fdc60ddf89484781433993625f006af6ec810c08f49a7e413946a1" - end - - let(:vagrant_sha_512_pbkdf2_iterations) do - 34482 - end - - 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) - expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "args").and_return(shell_return) - expect(provider.run_dscl("cmd", "/Path", "args")).to eq("stdout") - end - - it "returns an empty string from delete commands" do - shell_return = shellcmdresult.new("out", "err", 23) - expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-delete", "/Path", "args").and_return(shell_return) - expect(provider.run_dscl("delete", "/Path", "args")).to eq("") - end - - it "should raise an exception for any other command" do - shell_return = shellcmdresult.new("out", "err", 23) - expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "arguments").and_return(shell_return) - expect { provider.run_dscl("cmd", "/Path", "arguments") }.to 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) - expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "args").and_return(shell_return) - expect { provider.run_dscl("cmd", "/Path", "args") }.to raise_error(Chef::Exceptions::DsclCommandFailed) - end - - it "raises an exception when dscl reports 'eDSRecordNotFound'" do - shell_return = shellcmdresult.new("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", "err", -14136) - expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "args").and_return(shell_return) - expect { provider.run_dscl("cmd", "/Path", "args") }.to raise_error(Chef::Exceptions::DsclCommandFailed) - end - end - - describe "get_free_uid" do - before do - expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("\nwheel 200\nstaff 201\nbrahms 500\nchopin 501\n") - end - - describe "when the system property is set to true" do - before do - new_resource.system(true) - end - - it "should return the first unused uid number on or above 200" do - expect(provider.get_free_uid).to eq(202) - end - end - - it "should return the first unused uid number on or above 500" do - expect(provider.get_free_uid).to eq(502) - end - - it "should raise an exception when the search limit is exhausted" do - search_limit = 1 - expect { provider.get_free_uid(search_limit) }.to raise_error(RuntimeError) - end - end - - describe "uid_used?" do - it "should return false if not given any valid uid number" do - expect(provider.uid_used?(nil)).to be_falsey - end - - describe "when called with a user id" do - before do - expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("\naj 500\n") - end - - it "should return true for a used uid number" do - expect(provider.uid_used?(500)).to be_truthy - end - - it "should return false for an unused uid number" do - expect(provider.uid_used?(501)).to be_falsey - end - end - end - - describe "when determining the uid to set" do - it "raises RequestedUIDUnavailable if the requested uid is already in use" do - allow(provider).to receive(:uid_used?).and_return(true) - expect(provider).to receive(:get_free_uid).and_return(501) - expect { provider.dscl_set_uid }.to raise_error(Chef::Exceptions::RequestedUIDUnavailable) - end - - it "finds a valid, unused uid when none is specified" do - expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("") - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "UniqueID", 501) - expect(provider).to receive(:get_free_uid).and_return(501) - provider.dscl_set_uid - expect(new_resource.uid).to eq(501) - end - - it "sets the uid specified in the resource" do - new_resource.uid(1000) - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "UniqueID", 1000).and_return(true) - expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("") - provider.dscl_set_uid - end - end - - describe "current_home_exists?" do - let(:current_resource) do - new_resource.dup - end - - before do - provider.current_resource = current_resource - end - - it "returns false for nil home dir" do - current_resource.home nil - expect(provider.current_home_exists?).to be_falsey - end - - it "is false for empty string" do - current_resource.home "" - expect(provider.current_home_exists?).to be_falsey - end - - it "is true for existing directory" do - current_resource.home "/Users/blah" - allow(::File).to receive(:exist?).with("/Users/blah").and_return(true) - expect(provider.current_home_exists?).to be_truthy - end - end - - describe "when modifying the home directory" do - let(:current_resource) do - new_resource.dup - end - - before do - new_resource.manage_home true - new_resource.home("/Users/toor") - - provider.current_resource = current_resource - end - - it "deletes the home directory when resource#home is nil" do - new_resource.instance_variable_set(:@home, nil) - expect(provider).to 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") - expect { provider.dscl_set_home }.to 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.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) - allow(::File).to receive(:exist?).with("/old/home/toor").and_return(true) - allow(::File).to receive(:exist?).with("/Users/toor").and_return(true) - allow(::File).to receive(:exist?).with(current_home).and_return(true) - - expect(FileUtils).to receive(:mkdir_p).with("/Users/toor").and_return(true) - expect(FileUtils).to receive(:rmdir).with(current_home) - expect(::Dir).to receive(:glob).with("#{CHEF_SPEC_DATA}/old_home_dir/*", ::File::FNM_DOTMATCH).and_return(current_home_files) - expect(FileUtils).to receive(:mv).with(current_home_files, "/Users/toor", force: true) - expect(FileUtils).to receive(:chown_R).with("toor", "23", "/Users/toor") - - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "NFSHomeDirectory", "/Users/toor") - provider.dscl_set_home - end - - it "should run createhomedir to create the user's new home folder" do - expect(provider).to receive(:shell_out_compacted!).with("/usr/sbin/createhomedir", "-c", "-u", "toor") - provider.ditto_home - end - - it "creates the user's NFSHomeDirectory and home directory" do - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "NFSHomeDirectory", "/Users/toor").and_return(true) - expect(provider).to receive(:ditto_home) - provider.dscl_set_home - end - end - - describe "resource_requirements" do - let(:dscl_exists) { true } - let(:plutil_exists) { true } - - before do - allow(::File).to receive(:exist?).with("/usr/bin/dscl").and_return(dscl_exists) - allow(::File).to receive(:exist?).with("/usr/bin/plutil").and_return(plutil_exists) - end - - def run_requirements - provider.define_resource_requirements - provider.action = :create - provider.process_resource_requirements - end - - describe "when dscl doesn't exist" do - let(:dscl_exists) { false } - - it "should raise an error" do - expect { run_requirements }.to raise_error(Chef::Exceptions::User) - end - end - - describe "when plutil doesn't exist" do - let(:plutil_exists) { false } - - it "should raise an error" do - expect { run_requirements }.to raise_error(Chef::Exceptions::User) - end - end - - describe "when password is SALTED-SHA512" do - let(:password) { salted_sha512_password } - - it "should raise an error" do - expect { run_requirements }.to raise_error(Chef::Exceptions::User) - 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 - expect { run_requirements }.to raise_error(Chef::Exceptions::User) - 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 - expect { run_requirements }.not_to raise_error - end - end - end - end - - describe "load_current_resource" do - # set this to any of the user plist files under spec/data - let(:user_plist_file) { nil } - - before do - expect(provider).to receive(:shell_out_compacted).with("dscacheutil", "-flushcache") - expect(provider).to receive(:shell_out_compacted).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 - - unless user_plist_file.nil? - expect(provider).to 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 user is not there" do - it "shouldn't raise an error" do - expect { provider.load_current_resource }.not_to raise_error - end - - it "should set @user_exists" do - provider.load_current_resource - expect(provider.instance_variable_get(:@user_exists)).to be_falsey - end - - it "should set username" do - provider.load_current_resource - expect(provider.current_resource.username).to eq("toor") - end - end - - describe "when user is there" do - let(:password) { "something" } # Load password during load_current_resource - - let(:user_plist_file) { "10.9" } - - it "collects the user data correctly" do - provider.load_current_resource - expect(provider.current_resource.comment).to eq("vagrant") - expect(provider.current_resource.uid).to eq("501") - expect(provider.current_resource.gid).to eq("80") - expect(provider.current_resource.home).to eq("/Users/vagrant") - expect(provider.current_resource.shell).to eq("/bin/bash") - expect(provider.current_resource.password).to eq(vagrant_sha_512_pbkdf2) - expect(provider.current_resource.salt).to eq(vagrant_sha_512_pbkdf2_salt) - expect(provider.current_resource.iterations).to 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 - expect(provider.diverged_password?).to be_falsey - 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 - expect(provider.diverged_password?).to be_truthy - 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 - expect(provider.diverged_password?).to be_truthy - 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 - expect(provider.diverged_password?).to be_truthy - 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 - expect(provider.diverged_password?).to be_truthy - end - end - - describe "when salt isn't found" do - it "diverged_password? should report true" do - provider.load_current_resource - provider.current_resource.salt(nil) - expect(provider.diverged_password?).to be_truthy - end - end - end - end - - describe "salted_sha512_pbkdf2?" do - it "should return true when the string is a salted_sha512_pbkdf2 hash" do - expect(provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password)).to be_truthy - end - - it "should return false otherwise" do - expect(provider.salted_sha512_pbkdf2?(salted_sha512_password)).to be_falsey - expect(provider.salted_sha512_pbkdf2?("any other string")).to be_falsey - end - end - - describe "salted_sha512?" do - it "should return true when the string is a salted_sha512_pbkdf2 hash" do - expect(provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password)).to be_truthy - end - - it "should return false otherwise" do - expect(provider.salted_sha512?(salted_sha512_pbkdf2_password)).to be_falsey - expect(provider.salted_sha512?("any other string")).to be_falsey - end - end - - describe "prepare_password_shadow_info" do - 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 - expect(shadow_info).to have_key("SALTED-SHA512-PBKDF2") - expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("entropy") - expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("salt") - expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("iterations") - info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack("H*").first - expect(provider.salted_sha512_pbkdf2?(info)).to be_truthy - 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 - expect(shadow_info).to have_key("SALTED-SHA512-PBKDF2") - expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("entropy") - expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("salt") - expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("iterations") - info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack("H*").first - expect(provider.salted_sha512_pbkdf2?(info)).to be_truthy - expect(info).to eq(vagrant_sha_512_pbkdf2) - 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 - expect(provider).to receive(:prepare_password_shadow_info).and_return({}) - mock_shellout = double("Mock::Shellout") - allow(mock_shellout).to receive(:run_command) - expect(provider).to receive(:shell_out_compacted).and_return(mock_shellout) - expect(provider).to receive(:read_user_info) - expect(provider).to receive(:dscl_set) - expect(provider).to receive(:sleep).with(3) - expect(provider).to 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 - end - - it "creates the user, comment field, sets uid, gid, configures the home directory, sets the shell, and sets the password" do - expect(provider).to receive :dscl_create_user - expect(provider).to receive :dscl_create_comment - expect(provider).to receive :dscl_set_uid - expect(provider).to receive :dscl_set_gid - expect(provider).to receive :dscl_set_home - expect(provider).to receive :dscl_set_shell - expect(provider).to receive :set_password - provider.create_user - end - - it "creates the user and sets the comment field" do - expect(provider).to receive(:run_dscl).with("create", "/Users/toor").and_return(true) - provider.dscl_create_user - end - - it "sets the comment field" do - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "RealName", "#mockssuck").and_return(true) - provider.dscl_create_comment - end - - it "sets the comment field to username" do - new_resource.comment nil - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "RealName", "toor").and_return(true) - provider.dscl_create_comment - end - - it "should run run_dscl with create /Users/user PrimaryGroupID to set the users primary group" do - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "PrimaryGroupID", 1001).and_return(true) - provider.dscl_set_gid - end - - it "should run run_dscl with create /Users/user UserShell to set the users login shell" do - expect(provider).to 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" - end - - it "should map the group name to a numeric ID when the group exists" do - expect(provider).to receive(:run_dscl).with("read", "/Groups/newgroup", "PrimaryGroupID").ordered.and_return("PrimaryGroupID: 1001\n") - expect(provider).to 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("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", "err", -14136) - expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-read", "/Groups/newgroup", "PrimaryGroupID").and_return(shell_return) - expect { provider.dscl_set_gid }.to raise_error(Chef::Exceptions::GroupIDNotFound) - end - end - - it "should set group ID to 20 if it's not specified" do - new_resource.gid nil - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "PrimaryGroupID", 20).ordered.and_return(true) - provider.dscl_set_gid - expect(new_resource.gid).to eq(20) - 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 - - # 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 - expect(provider).to receive :dscl_create_user - expect(provider).to receive :dscl_create_comment - expect(provider).to receive :dscl_set_uid - expect(provider).to receive :dscl_set_gid - expect(provider).to receive :dscl_set_home - expect(provider).to receive :dscl_set_shell - expect(provider).to 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 - - # This is different from current_resource - new_resource.gid 2342 - end - - it "sets the gid" do - expect(provider).to receive :dscl_set_gid - provider.manage_user - end - end - - describe "when the user exists" do - before do - expect(provider).to receive(:shell_out_compacted).with("dscacheutil", "-flushcache") - expect(provider).to receive(:shell_out_compacted).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.manage_home true - expect(provider).to receive(:run_dscl).with("list", "/Groups").and_return("my_group\nyour_group\nreal_group\n") - expect(provider).to receive(:run_dscl).with("read", "/Groups/my_group").and_raise(Chef::Exceptions::DsclCommandFailed) # Empty group - expect(provider).to receive(:run_dscl).with("read", "/Groups/your_group").and_return("GroupMembership: not_you") - expect(provider).to receive(:run_dscl).with("read", "/Groups/real_group").and_return("GroupMembership: toor") - expect(provider).to receive(:run_dscl).with("delete", "/Groups/real_group", "GroupMembership", "toor") - expect(provider).to receive(:run_dscl).with("delete", "/Users/toor") - expect(FileUtils).to receive(:rm_rf).with("/Users/vagrant") - provider.remove_user - end - end - - describe "when user is not locked" do - it "determines the user as not locked" do - expect(provider).not_to be_locked - end - 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 as not locked" do - expect(provider).to be_locked - end - - it "can unlock the user" do - expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "AuthenticationAuthority", ";ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2>") - provider.unlock_user - end - end - end - - describe "when locking the user" do - it "should run run_dscl with append /Users/user AuthenticationAuthority ;DisabledUser; to lock the user account" do - expect(provider).to receive(:run_dscl).with("append", "/Users/toor", "AuthenticationAuthority", ";DisabledUser;") - provider.lock_user - end - end - -end diff --git a/tasks/docs.rb b/tasks/docs.rb index efb0d5420f..98261a7402 100755 --- a/tasks/docs.rb +++ b/tasks/docs.rb @@ -1,4 +1,4 @@ -RESOURCES_TO_SKIP = ["whyrun_safe_ruby_block", "l_w_r_p_base", "user_resource_abstract_base_class", "linux_user", "pw_user", "aix_user", "dscl_user", "solaris_user", "windows_user", "mac_user", ""].freeze +RESOURCES_TO_SKIP = ["whyrun_safe_ruby_block", "l_w_r_p_base", "user_resource_abstract_base_class", "linux_user", "pw_user", "aix_user", "solaris_user", "windows_user", "mac_user", ""].freeze namespace :docs_site do |