summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md10
-rw-r--r--Gemfile.lock16
-rw-r--r--VERSION2
-rw-r--r--chef-bin/lib/chef-bin/version.rb2
-rw-r--r--chef-config/lib/chef-config/version.rb2
-rw-r--r--lib/chef/cookbook/gem_installer.rb9
-rw-r--r--lib/chef/knife/bootstrap.rb9
-rw-r--r--lib/chef/provider/user/dscl.rb4
-rw-r--r--lib/chef/provider/user/mac.rb628
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/user.rb1
-rw-r--r--lib/chef/resource/user/dscl_user.rb2
-rw-r--r--lib/chef/resource/user/mac_user.rb119
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/version.rb2
-rw-r--r--omnibus/Gemfile.lock16
-rw-r--r--omnibus_overrides.rb4
-rw-r--r--spec/functional/resource/user/mac_user_spec.rb188
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/platform_helpers.rb10
-rw-r--r--spec/unit/cookbook/gem_installer_spec.rb23
-rw-r--r--spec/unit/knife/bootstrap_spec.rb18
-rw-r--r--spec/unit/provider/user/dscl_spec.rb1
-rw-r--r--spec/unit/provider/user/mac_spec.rb38
24 files changed, 1070 insertions, 37 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9d6c1435e..495d207594 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,15 +1,19 @@
<!-- usage documentation: http://expeditor-docs.es.chef.io/configuration/changelog/ -->
-<!-- latest_release 15.3.6 -->
-## [v15.3.6](https://github.com/chef/chef/tree/v15.3.6) (2019-09-06)
+<!-- latest_release 15.4.0 -->
+## [v15.4.0](https://github.com/chef/chef/tree/v15.4.0) (2019-09-10)
#### Merged Pull Requests
-- Add Chef 15.3 release notes [#8860](https://github.com/chef/chef/pull/8860) ([tas50](https://github.com/tas50))
+- Update Ruby to 2.6.4 and nokogori to 10.10.4 to resolve CVEs [#8851](https://github.com/chef/chef/pull/8851) ([tas50](https://github.com/tas50))
<!-- latest_release -->
<!-- release_rollup since=15.2.20 -->
### Changes not yet released to stable
#### Merged Pull Requests
+- Update Ruby to 2.6.4 and nokogori to 10.10.4 to resolve CVEs [#8851](https://github.com/chef/chef/pull/8851) ([tas50](https://github.com/tas50)) <!-- 15.4.0 -->
+- Add mac_user resource that is compatible with macOS &gt;= 10.14 [#8775](https://github.com/chef/chef/pull/8775) ([ryancragun](https://github.com/ryancragun)) <!-- 15.3.9 -->
+- Support to provide --local flag to gem installer. [#8847](https://github.com/chef/chef/pull/8847) ([samshinde](https://github.com/samshinde)) <!-- 15.3.8 -->
+- Bootstrap: Set pty true only if required [#8816](https://github.com/chef/chef/pull/8816) ([vsingh-msys](https://github.com/vsingh-msys)) <!-- 15.3.7 -->
- Add Chef 15.3 release notes [#8860](https://github.com/chef/chef/pull/8860) ([tas50](https://github.com/tas50)) <!-- 15.3.6 -->
- Remove duplicate policy_path config [#8864](https://github.com/chef/chef/pull/8864) ([tas50](https://github.com/tas50)) <!-- 15.3.5 -->
- convert chocolatey resource to use shell_out splat args [#8861](https://github.com/chef/chef/pull/8861) ([lamont-granquist](https://github.com/lamont-granquist)) <!-- 15.3.4 -->
diff --git a/Gemfile.lock b/Gemfile.lock
index fad5746c74..09cedf52b1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -27,11 +27,11 @@ GIT
PATH
remote: .
specs:
- chef (15.3.6)
+ chef (15.3.10)
addressable
bcrypt_pbkdf (~> 1.0)
bundler (>= 1.10)
- chef-config (= 15.3.6)
+ chef-config (= 15.3.10)
chef-zero (>= 14.0.11)
diff-lcs (~> 1.2, >= 1.2.4)
ed25519 (~> 1.2)
@@ -58,11 +58,11 @@ PATH
train-winrm
tty-screen (~> 0.6)
uuidtools (~> 2.1.5)
- chef (15.3.6-universal-mingw32)
+ chef (15.3.10-universal-mingw32)
addressable
bcrypt_pbkdf (~> 1.0)
bundler (>= 1.10)
- chef-config (= 15.3.6)
+ chef-config (= 15.3.10)
chef-zero (>= 14.0.11)
diff-lcs (~> 1.2, >= 1.2.4)
ed25519 (~> 1.2)
@@ -105,13 +105,13 @@ PATH
PATH
remote: chef-bin
specs:
- chef-bin (15.3.6)
- chef (= 15.3.6)
+ chef-bin (15.3.10)
+ chef (= 15.3.10)
PATH
remote: chef-config
specs:
- chef-config (15.3.6)
+ chef-config (15.3.10)
addressable
fuzzyurl
mixlib-config (>= 2.2.12, < 4.0)
@@ -304,7 +304,7 @@ GEM
ruby-progressbar (1.10.1)
ruby-shadow (2.5.0)
rubyntlm (0.6.2)
- rubyzip (1.2.3)
+ rubyzip (1.2.4)
safe_yaml (1.0.5)
semverse (3.0.0)
simplecov (0.17.0)
diff --git a/VERSION b/VERSION
index ec60fbc209..73773a8f0e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-15.3.6 \ No newline at end of file
+15.3.10 \ No newline at end of file
diff --git a/chef-bin/lib/chef-bin/version.rb b/chef-bin/lib/chef-bin/version.rb
index 3957009dd4..fe3963d94c 100644
--- a/chef-bin/lib/chef-bin/version.rb
+++ b/chef-bin/lib/chef-bin/version.rb
@@ -21,7 +21,7 @@
module ChefBin
CHEFBIN_ROOT = File.expand_path("../..", __FILE__)
- VERSION = "15.3.6".freeze
+ VERSION = "15.3.10".freeze
end
#
diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb
index 7e1faf0d38..9aa3d8c27d 100644
--- a/chef-config/lib/chef-config/version.rb
+++ b/chef-config/lib/chef-config/version.rb
@@ -21,7 +21,7 @@
module ChefConfig
CHEFCONFIG_ROOT = File.expand_path("../..", __FILE__)
- VERSION = "15.3.6".freeze
+ VERSION = "15.3.10".freeze
end
#
diff --git a/lib/chef/cookbook/gem_installer.rb b/lib/chef/cookbook/gem_installer.rb
index eab4b47241..cf0177d1d5 100644
--- a/lib/chef/cookbook/gem_installer.rb
+++ b/lib/chef/cookbook/gem_installer.rb
@@ -66,8 +66,13 @@ class Chef
tf.close
Chef::Log.trace("generated Gemfile contents:")
Chef::Log.trace(IO.read(tf.path))
- so = shell_out!("bundle install", cwd: dir, env: { "PATH" => path_with_prepended_ruby_bin })
- Chef::Log.info(so.stdout)
+ # Skip installation only if Chef::Config[:skip_gem_metadata_installation] option is true
+ unless Chef::Config[:skip_gem_metadata_installation]
+ # Add additional options to bundle install
+ cmd = [ "bundle", "install", Chef::Config[:gem_installer_bundler_options] ]
+ so = shell_out!(cmd, cwd: dir, env: { "PATH" => path_with_prepended_ruby_bin })
+ Chef::Log.info(so.stdout)
+ end
end
end
Gem.clear_paths
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
index e892f1f2c9..efa1515858 100644
--- a/lib/chef/knife/bootstrap.rb
+++ b/lib/chef/knife/bootstrap.rb
@@ -672,6 +672,14 @@ class Chef
def do_connect(conn_options)
@connection = TrainConnector.new(host_descriptor, connection_protocol, conn_options)
connection.connect!
+ rescue Train::UserError => e
+ if !conn_options.key?(:pty) && e.reason == :sudo_no_tty
+ ui.warn("#{e.message} - trying with pty request")
+ conn_options[:pty] = true # ensure we can talk to systems with requiretty set true in sshd config
+ retry
+ else
+ raise
+ end
end
# Fail if both first_boot_attributes and first_boot_attributes_from_file
@@ -895,7 +903,6 @@ class Chef
opts = {}
return opts if winrm?
- opts[:pty] = true # ensure we can talk to systems with requiretty set true in sshd config
opts[:non_interactive] = true # Prevent password prompts from underlying net/ssh
opts[:forward_agent] = (config_value(:ssh_forward_agent) === true)
opts[:connection_timeout] = session_timeout
diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb
index b8f85618da..027f9eba38 100644
--- a/lib/chef/provider/user/dscl.rb
+++ b/lib/chef/provider/user/dscl.rb
@@ -42,7 +42,7 @@ class Chef
# => shadow binary length 128 bytes
# => Salt / Iterations are stored separately in the same file
#
- # This provider only supports Mac OSX versions 10.7 and above
+ # This provider only supports macOS versions 10.7 to 10.13
class Dscl < Chef::Provider::User
attr_accessor :user_info
@@ -50,7 +50,7 @@ class Chef
attr_accessor :password_shadow_conversion_algorithm
provides :dscl_user
- provides :user, os: "darwin"
+ 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
diff --git a/lib/chef/provider/user/mac.rb b/lib/chef/provider/user/mac.rb
new file mode 100644
index 0000000000..414445cfa1
--- /dev/null
+++ b/lib/chef/provider/user/mac.rb
@@ -0,0 +1,628 @@
+#
+# Author:: Ryan Cragun (<ryan@chef.io>)
+# Copyright:: Copyright (c) 2019, 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 "../../resource"
+require_relative "../../dsl/declare_resource"
+require_relative "../../mixin/shell_out"
+require_relative "../../mixin/which"
+require_relative "../user"
+require_relative "../../resource/user/mac_user"
+
+class Chef
+ class Provider
+ 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.
+ class MacUser < Chef::Provider::User
+ include Chef::Mixin::Which
+
+ provides :mac_user
+ provides :user, os: "darwin", platform_version: ">= 10.14"
+
+ attr_reader :user_plist, :admin_group_plist
+
+ def load_current_resource
+ @current_resource = Chef::Resource::User::MacUser.new(new_resource.username)
+ current_resource.username(new_resource.username)
+
+ reload_admin_group_plist
+ reload_user_plist
+
+ if user_plist
+ current_resource.uid(user_plist[:uid][0])
+ current_resource.gid(user_plist[:gid][0])
+ current_resource.home(user_plist[:home][0])
+ current_resource.shell(user_plist[:shell][0])
+ current_resource.comment(user_plist[:comment][0])
+
+ shadow_hash = user_plist[:shadow_hash]
+ if shadow_hash
+ current_resource.password(shadow_hash[0]["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack("H*")[0])
+ current_resource.salt(shadow_hash[0]["SALTED-SHA512-PBKDF2"]["salt"].string.unpack("H*")[0])
+ current_resource.iterations(shadow_hash[0]["SALTED-SHA512-PBKDF2"]["iterations"].to_i)
+ end
+
+ current_resource.secure_token(secure_token_enabled?)
+ current_resource.admin(admin_user?)
+ else
+ @user_exists = false
+ logger.trace("#{new_resource} user does not exist")
+ end
+
+ current_resource
+ end
+
+ def reload_admin_group_plist
+ @admin_group_plist = nil
+
+ admin_group_xml = run_dscl("read", "/Groups/admin")
+ return nil unless admin_group_xml && admin_group_xml != ""
+
+ @admin_group_plist = Plist.new(::Plist.parse_xml(admin_group_xml))
+ end
+
+ def reload_user_plist
+ @user_plist = nil
+
+ # Load the user information.
+ begin
+ user_xml = run_dscl("read", "/Users/#{new_resource.username}")
+ rescue Chef::Exceptions::DsclCommandFailed
+ return nil
+ end
+
+ return nil if user_xml.nil? || user_xml == ""
+
+ @user_plist = Plist.new(::Plist.parse_xml(user_xml))
+
+ shadow_hash_hex = user_plist[:shadow_hash][0]
+ return unless shadow_hash_hex && shadow_hash_hex != ""
+
+ # The password infomation is stored in the ShadowHashData key in the
+ # plist. However, parsing it is a bit tricky as the value is itself
+ # another encoded binary plist. We have to extract the encoded plist,
+ # decode it from hex to a binary plist and then convert the binary
+ # into XML plist. From there we can extract the hash data.
+ #
+ # NOTE: `dscl -read` and `plutil -convert` return different values for
+ # ShadowHashData.
+ #
+ # `dscl` returns the value encoded as a hex string and stored as a <string>
+ # `plutil` returns the value encoded as a base64 string stored as <data>
+ #
+ # eg:
+ #
+ # <array>
+ # <string>77687920 63616e27 74206170 706c6520 6275696c 6420636f 6e736973 74656e74 20746f6f 6c696e67</string>
+ # </array>
+ #
+ # vs
+ #
+ # <array>
+ # <data>AADKAAAKAA4LAA0MAAAAAAAAAAA=</data>
+ # </array>
+ #
+ begin
+ shadow_binary_plist = [shadow_hash_hex.delete(" ")].pack("H*")
+ shadow_xml_plist = shell_out("plutil", "-convert", "xml1", "-o", "-", "-", input: shadow_binary_plist).stdout
+ user_plist[:shadow_hash] = ::Plist.parse_xml(shadow_xml_plist)
+ rescue Chef::Exceptions::PlistUtilCommandFailed, Chef::Exceptions::DsclCommandFailed
+ nil
+ end
+ end
+
+ #
+ # User Provider Callbacks
+ #
+
+ def create_user
+ cmd = [-"-addUser", new_resource.username]
+ cmd += ["-fullName", new_resource.comment] if new_resource.property_is_set?(:comment)
+ cmd += ["-UID", new_resource.uid] if new_resource.property_is_set?(:uid)
+ cmd += ["-shell", new_resource.shell]
+ cmd += ["-home", new_resource.home]
+ cmd += ["-admin"] if new_resource.admin
+
+ # We can technically create a new user without the admin credentials
+ # but without them the user cannot enable SecureToken, thus they cannot
+ # create other secure users or enable FileVault full disk encryption.
+ if new_resource.property_is_set?(:admin_username) && new_resource.property_is_set?(:admin_password)
+ cmd += ["-adminUser", new_resource.admin_username]
+ cmd += ["-adminPassword", new_resource.admin_password]
+ end
+
+ converge_by "create user" do
+ # sysadminctl doesn't exit with a non-zero exit code if it encounters
+ # a problem. We'll check stderr and make sure we see that it finished
+ # correctly.
+ res = run_sysadminctl(cmd)
+ unless res.downcase =~ /creating user/
+ raise Chef::Exceptions::User, "error when creating user: #{res}"
+ end
+ end
+
+ # Wait for the user to show up in the ds cache
+ wait_for_user
+
+ # Reload with up-to-date user information
+ reload_user_plist
+ reload_admin_group_plist
+
+ if new_resource.property_is_set?(:password)
+ converge_by("set password") { set_password }
+ end
+
+ if new_resource.manage_home
+ # "sydadminctl -addUser" will create the home directory if it's
+ # the default /Users/<username>, otherwise it sets it in plist
+ # but does not create it. Here we'll ensure that it gets created
+ # if we've been given a directory that is not the default.
+ unless ::File.directory?(new_resource.home) && ::File.exist?(new_resource.home)
+ converge_by("create home directory") do
+ shell_out!("createhomedir -c -u #{new_resource.username}")
+ end
+ end
+ end
+
+ if new_resource.property_is_set?(:gid)
+ # NOTE: Here we're managing the primary group of the user which is
+ # a departure from previous behavior. We could just set the
+ # PrimaryGroupID for the user and move on if we decide that actual
+ # group magement should be done outside of the core resource.
+ group_name, group_id, group_action = user_group_info
+
+ declare_resource(:group, group_name) do
+ members new_resource.username
+ gid group_id if group_id
+ action :nothing
+ append true
+ end.run_action(group_action)
+
+ converge_by("create primary group ID") do
+ run_dscl("create", "/Users/#{new_resource.username}", "PrimaryGroupID", new_resource.gid)
+ end
+ end
+
+ if diverged?(:secure_token)
+ converge_by("alter SecureToken") { toggle_secure_token }
+ end
+
+ reload_user_plist
+ end
+
+ def compare_user
+ %i{comment shell uid gid salt password admin secure_token}.any? { |m| diverged?(m) }
+ end
+
+ def manage_user
+ %i{uid home}.each do |prop|
+ raise Chef::Exceptions::User, "cannot modify #{prop} on macOS >= 10.14" if diverged?(prop)
+ end
+
+ if diverged?(:password)
+ converge_by("alter password") { set_password }
+ end
+
+ if diverged?(:comment)
+ converge_by("alter comment") do
+ run_dscl("create", "/Users/#{new_resource.username}", "RealName", new_resource.comment)
+ end
+ end
+
+ if diverged?(:shell)
+ converge_by("alter shell") do
+ run_dscl("create", "/Users/#{new_resource.username}", "UserShell", new_resource.shell)
+ end
+ end
+
+ if diverged?(:secure_token)
+ converge_by("alter SecureToken") { toggle_secure_token }
+ end
+
+ if diverged?(:admin)
+ converge_by("alter admin group membership") do
+ declare_resource(:group, "admin") do
+ if new_resource.admin
+ members new_resource.username
+ else
+ excluded_members new_resource.username
+ end
+
+ action :nothing
+ append true
+ end.run_action(:create)
+
+ admins = admin_group_plist[:group_members]
+ if new_resource.admin
+ admins << user_plist[:guid][0]
+ else
+ admins.reject! { |m| m == user_plist[:guid][0] }
+ end
+
+ run_dscl("create", "/Groups/admin", "GroupMembers", admins)
+ end
+
+ reload_admin_group_plist
+ end
+
+ group_name, group_id, group_action = user_group_info
+ declare_resource(:group, group_name) do
+ gid group_id if group_id
+ members new_resource.username
+ action :nothing
+ append true
+ end.run_action(group_action)
+
+ if diverged?(:gid)
+ converge_by("alter group membership") do
+ run_dscl("create", "/Users/#{new_resource.username}", "PrimaryGroupID", new_resource.gid)
+ end
+ end
+
+ reload_user_plist
+ end
+
+ def remove_user
+ cmd = ["-deleteUser", new_resource.username]
+ cmd << new_resource.manage_home ? "-secure" : "-keepHome"
+ if new_resource.property_is_set?(:admin_username) && new_resource.property_is_set?(:admin_password)
+ cmd += ["-adminUser", new_resource.admin_username]
+ cmd += ["-adminPassword", new_resource.admin_password]
+ end
+
+ # sysadminctl doesn't exit with a non-zero exit code if it encounters
+ # a problem. We'll check stderr and make sure we see that it finished
+ converge_by "remove user" do
+ res = run_sysadminctl(cmd)
+ unless res.downcase =~ /deleting record|not found/
+ raise Chef::Exceptions::User, "error deleting user: #{res}"
+ end
+ end
+
+ reload_user_plist
+ @user_exists = false
+ end
+
+ def lock_user
+ converge_by "lock user" do
+ run_dscl("append", "/Users/#{new_resource.username}", "AuthenticationAuthority", ";DisabledUser;")
+ end
+
+ reload_user_plist
+ end
+
+ def unlock_user
+ auth_string = user_plist[:auth_authority].reject! { |tag| tag == ";DisabledUser;" }.join.strip
+ converge_by "unlock user" do
+ run_dscl("create", "/Users/#{new_resource.username}", "AuthenticationAuthority", auth_string)
+ end
+
+ reload_user_plist
+ end
+
+ def locked?
+ user_plist[:auth_authority].any? { |tag| tag == ";DisabledUser;" }
+ rescue
+ false
+ end
+
+ def check_lock
+ @locked = locked?
+ end
+
+ #
+ # Methods
+ #
+
+ def diverged?(prop)
+ prop = prop.to_sym
+
+ case prop
+ when :password
+ password_diverged?
+ when :gid
+ user_group_diverged?
+ when :secure_token
+ secure_token_diverged?
+ else
+ # Other fields are have been set on current resource so just compare
+ # them.
+ new_resource.property_is_set?(prop) && (new_resource.send(prop) != current_resource.send(prop))
+ end
+ end
+
+ # Attempt to resolve the group name, gid, and the action required for
+ # associated group resource. If a group exists we'll modify it, otherwise
+ # create it.
+ def user_group_info
+ @user_group_info ||= begin
+ if new_resource.gid.is_a?(String)
+ begin
+ g = Etc.getgrnam(new_resource.gid)
+ [g.name, g.gid.to_s, :modify]
+ rescue
+ [new_resource.gid, nil, :create]
+ end
+ else
+ begin
+ g = Etc.getgrgid(new_resource.gid)
+ [g.name, g.gid.to_s, :modify]
+ rescue
+ [g.username, nil, :create]
+ end
+ end
+ end
+ end
+
+ def secure_token_enabled?
+ user_plist[:auth_authority].any? { |tag| tag == ";SecureToken;" }
+ rescue
+ false
+ end
+
+ def secure_token_diverged?
+ new_resource.secure_token ? !secure_token_enabled? : secure_token_enabled?
+ end
+
+ def toggle_secure_token
+ # Check for this lazily as we only need to validate for these credentials
+ # if we're toggling secure token.
+ unless new_resource.property_is_set?(:admin_username) &&
+ new_resource.property_is_set?(:admin_password) &&
+ # property_is_set? can't handle a default inherited from password
+ # when not using shadow hash data. Hence, we'll just have to
+ # make sure some valid string is there.
+ new_resource.secure_token_password &&
+ new_resource.secure_token_password != ""
+ raise Chef::Exceptions::User, "secure_token_password, admin_user and admin_password properties are required to modify SecureToken"
+ end
+
+ cmd = (new_resource.secure_token ? %w{-secureTokenOn} : %w{-secureTokenOff})
+ cmd += [new_resource.username, "-password", new_resource.secure_token_password]
+ cmd += ["-adminUser", new_resource.admin_username]
+ cmd += ["-adminPassword", new_resource.admin_password]
+
+ # sysadminctl doesn't exit with a non-zero exit code if it encounters
+ # a problem. We'll check stderr and make sure we see that it finished
+ res = run_sysadminctl(cmd)
+ unless res.downcase =~ /done/
+ raise Chef::Exceptions::User, "error when modifying SecureToken: #{res}"
+ end
+
+ # HACK: When SecureToken is enabled or disabled it requires the user
+ # password in plaintext, which it verifies and uses as a key. It also
+ # takes the liberty of _rehashing_ the password with a random salt and
+ # iterations count and saves it back into the user ShadowHashData.
+ #
+ # Therefore, if we're configuring a user based upon existing shadow
+ # hash data we'll have to set the password again so that future runs
+ # of the client don't show password drift.
+ set_password if new_resource.property_is_set?(:salt)
+ end
+
+ def user_group_diverged?
+ return false unless new_resource.property_is_set?(:gid)
+
+ group_name, group_id = user_group_info
+
+ if current_resource.gid.is_a?(String)
+ current_resource.gid != group_name
+ else
+ current_resource.gid != group_id.to_i
+ end
+ end
+
+ def password_diverged?
+ # There are three options for configuring the password:
+ # * ShadowHashData which includes the hash data as:
+ # * hashed entropy as the "password"
+ # * salt
+ # * iterations
+ # * Plaintext password
+ # * Not configuring it
+
+ # Check for no desired password configuration
+ return false unless new_resource.property_is_set?(:password)
+
+ # Check for ShadowHashData divergence by comparing the entropy,
+ # salt, and iterations.
+ if new_resource.property_is_set?(:salt)
+ return true if %i{salt iterations}.any? { |prop| diverged?(prop) }
+
+ return new_resource.password != current_resource.password
+ end
+
+ # Check for plaintext password divergence. We don't actually know
+ # what the stored password is but we can hash the given password with
+ # stored salt and iterations, and compare the resulting entropy with
+ # the saved entropy.
+ OpenSSL::PKCS5.pbkdf2_hmac(
+ new_resource.password,
+ convert_to_binary(current_resource.salt),
+ current_resource.iterations.to_i,
+ 128,
+ OpenSSL::Digest::SHA512.new
+ ).unpack("H*")[0] != current_resource.password
+ end
+
+ def admin_user?
+ admin_group_plist[:group_members].any? { |mem| mem == user_plist[:guid][0] }
+ rescue
+ false
+ end
+
+ def convert_to_binary(string)
+ string.unpack("a2" * (string.size / 2)).collect { |i| i.hex.chr }.join
+ end
+
+ def set_password
+ if new_resource.property_is_set?(:salt)
+ entropy = StringIO.new(convert_to_binary(new_resource.password))
+ salt = StringIO.new(convert_to_binary(new_resource.salt))
+ else
+ salt = StringIO.new(OpenSSL::Random.random_bytes(32))
+ entropy = StringIO.new(
+ OpenSSL::PKCS5.pbkdf2_hmac(
+ new_resource.password,
+ salt.string,
+ new_resource.iterations,
+ 128,
+ OpenSSL::Digest::SHA512.new
+ )
+ )
+ end
+
+ shadow_hash = user_plist[:shadow_hash][0]
+ shadow_hash["SALTED-SHA512-PBKDF2"] = {
+ "entropy" => entropy,
+ "salt" => salt,
+ "iterations" => new_resource.iterations,
+ }
+
+ shadow_hash_binary = StringIO.new
+ shell_out("plutil", "-convert", "binary1", "-o", "-", "-",
+ input: shadow_hash.to_plist,
+ live_stream: shadow_hash_binary)
+
+ # Apple seem to have killed their dsimport documentation about the
+ # dsimport record format. Perhaps that means our days of being able to
+ # use dsimport without an admin password or perhaps at all could be
+ # numbered. Here is the record format for posterity:
+ #
+ # End of record character
+ # Escape character
+ # Field separator
+ # Value separator
+ # Record type (Users, Groups, Computers, ComputerGroups, ComputerLists)
+ # Number of properties
+ # Property 1
+ # ...
+ # Property N
+ #
+ # The user password shadow data format breaks down as:
+ #
+ # 0x0A End of record denoted by \n
+ # 0x5C Escaping is denoted by \
+ # 0x3A Fields are separated by :
+ # 0x2C Values are seperated by ,
+ # dsRecTypeStandard:Users The record type we're configuring
+ # 2 How many properties we're going to set
+ # dsAttrTypeStandard:RecordName Property 1: our users record name
+ # base64:dsAttrTypeNative:ShadowHashData Property 2: our shadow hash data
+
+ import_file = ::File.join(Chef::Config["file_cache_path"], "#{new_resource.username}_password_dsimport")
+ ::File.open(import_file, "w+", 0600) do |f|
+ f.write <<~DSIMPORT
+ 0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName base64:dsAttrTypeNative:ShadowHashData
+ #{new_resource.username}:#{::Base64.strict_encode64(shadow_hash_binary.string)}
+ DSIMPORT
+ end
+
+ run_dscl("delete", "/Users/#{new_resource.username}", "ShadowHashData")
+ run_dsimport(import_file, "/Local/Default", "M")
+ run_dscl("create", "/Users/#{new_resource.username}", "Password", "********")
+ ensure
+ ::File.delete(import_file) if defined?(import_file) && ::File.exist?(import_file)
+ end
+
+ def wait_for_user
+ timeout = Time.now + 5
+
+ loop do
+ begin
+ run_dscl("read", "/Users/#{new_resource.username}", "ShadowHashData")
+ break
+ rescue Chef::Exceptions::DsclCommandFailed => e
+ if Time.now < timeout
+ sleep 0.1
+ else
+ raise Chef::Exceptions::User, e.message
+ end
+ end
+ end
+ end
+
+ def run_dsimport(*args)
+ shell_out!("dsimport", args)
+ end
+
+ def run_sysadminctl(args)
+ # sysadminctl doesn't exit with a non-zero code when errors are encountered
+ # and ouputs everything to STDERR instead of STDOUT and STDERR. Therefore we'll
+ # return the STDERR and let the caller handle it.
+ shell_out!("sysadminctl", args).stderr
+ end
+
+ def run_dscl(*args)
+ result = shell_out("dscl", "-plist", ".", "-#{args[0]}", args[1..-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 =~ /No such key: /
+
+ result.stdout
+ end
+
+ def run_plutil(*args)
+ result = shell_out("plutil", "-#{args[0]}", args[1..-1])
+ raise(Chef::Exceptions::PlistUtilCommandFailed, "plutil error: #{result.inspect}") unless result.exitstatus == 0
+
+ result.stdout
+ end
+
+ class Plist
+ DSCL_PROPERTY_MAP = {
+ uid: "dsAttrTypeStandard:UniqueID",
+ guid: "dsAttrTypeStandard:GeneratedUID",
+ gid: "dsAttrTypeStandard:PrimaryGroupID",
+ home: "dsAttrTypeStandard:NFSHomeDirectory",
+ shell: "dsAttrTypeStandard:UserShell",
+ comment: "dsAttrTypeStandard:RealName",
+ password: "dsAttrTypeStandard:Password",
+ auth_authority: "dsAttrTypeStandard:AuthenticationAuthority",
+ shadow_hash: "dsAttrTypeNative:ShadowHashData",
+ group_members: "dsAttrTypeStandard:GroupMembers",
+ }.freeze
+
+ attr_accessor :plist_hash, :property_map
+
+ def initialize(plist_hash = {}, property_map = DSCL_PROPERTY_MAP)
+ @plist_hash = plist_hash
+ @property_map = property_map
+ end
+
+ def get(key)
+ return nil unless property_map.key?(key)
+
+ plist_hash[property_map[key]]
+ end
+ alias_method :[], :get
+
+ def set(key, value)
+ return nil unless property_map.key?(key)
+
+ plist_hash[property_map[key]] = [ value ]
+ end
+ alias_method :[]=, :set
+
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
index 85f2cddaf9..1e45ca13ed 100644
--- a/lib/chef/providers.rb
+++ b/lib/chef/providers.rb
@@ -107,6 +107,7 @@ 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"
require_relative "provider/user/solaris"
require_relative "provider/user/windows"
diff --git a/lib/chef/resource/user.rb b/lib/chef/resource/user.rb
index 19ae2d8dc8..6ade20082f 100644
--- a/lib/chef/resource/user.rb
+++ b/lib/chef/resource/user.rb
@@ -47,6 +47,7 @@ class Chef
property :password, String,
description: "The password shadow hash",
+ sensitive: true,
desired_state: false
property :non_unique, [ TrueClass, FalseClass ],
diff --git a/lib/chef/resource/user/dscl_user.rb b/lib/chef/resource/user/dscl_user.rb
index 54d78d20a0..a3f6661c22 100644
--- a/lib/chef/resource/user/dscl_user.rb
+++ b/lib/chef/resource/user/dscl_user.rb
@@ -24,7 +24,7 @@ class Chef
resource_name :dscl_user
provides :dscl_user
- provides :user, os: "darwin"
+ provides :user, os: "darwin", platform_version: "<= 10.13"
property :iterations, Integer,
description: "macOS platform only. The number of iterations for a password with a SALTED-SHA512-PBKDF2 shadow hash.",
diff --git a/lib/chef/resource/user/mac_user.rb b/lib/chef/resource/user/mac_user.rb
new file mode 100644
index 0000000000..ab5cc12947
--- /dev/null
+++ b/lib/chef/resource/user/mac_user.rb
@@ -0,0 +1,119 @@
+#
+# Author:: Ryan Cragun (<ryan@chef.io>)
+# Copyright:: Copyright 2019, 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
+ # Provide a user resource that is compatible with default TCC restrictions
+ # that were introduced in macOS 10.14.
+ #
+ # Changes:
+ #
+ # * This resource and the corresponding provider have been modified to
+ # work with default macOS TCC policies. Direct access to user binary
+ # plists are no longer permitted by default, thus we've chosen to use
+ # a combination of newer utilities for managing user lifecycles and older
+ # utilities for managing passwords.
+ #
+ # * Due to tooling changes that were necessitated by the new policy
+ # restrictions the mac_user resource is only suitable for use on macOS
+ # >= 10.14. Support for older platforms has been removed.
+ #
+ # New Features:
+ #
+ # * Primary group management is now included.
+ #
+ # * 'admin' is now a boolean property that configures a user to an admin.
+ #
+ # * 'admin_username' and 'admin_password' are new properties that define the
+ # admin user credentials required for toggling SecureToken for a user.
+ #
+ # The value of 'admin_username' must correspond to a system user that
+ # is part of the 'admin' with SecureToken enabled in order to toggle
+ # SecureToken.
+ #
+ # * 'secure_token' is a boolean property that sets the desired state
+ # for SecureToken. SecureToken token is required for FileVault full
+ # disk encryption.
+ #
+ # * 'secure_token_password' is the plaintext password required to enable
+ # or disable secure_token for a user. If no salt is specified we assume
+ # the 'password' property corresponds to a plaintext password and will
+ # attempt to use it in place of secure_token_password if it not set.
+ class MacUser < Chef::Resource::User
+ resource_name :mac_user
+
+ provides :mac_user
+ provides :user, os: "darwin", platform_version: ">= 10.14"
+
+ introduced "15.3"
+
+ property :iterations, Integer,
+ description: "The number of iterations for a password with a SALTED-SHA512-PBKDF2 shadow hash.",
+ default: 57803, desired_state: false
+
+ # Overload gid to set our default gid to 20, the macOS "staff" group.
+ # We also allow a string group name here which we'll attempt to resolve
+ # or create in the provider.
+ property :gid, [Integer, String], description: "The numeric group identifier.", default: 20, coerce: ->(gid) do
+ begin
+ Integer(gid) # Try and coerce a group id string into an integer
+ rescue
+ gid # assume we have a group name
+ end
+ end
+
+ # Overload the password so we can set a length requirements and update the
+ # description.
+ property :password, String, description: "The plain text user password", sensitive: true, coerce: ->(password) {
+ # It would be nice if this could be in callbacks but we need the context
+ # of the resource to get the salt property so we have to do it in coerce.
+ if salt && password !~ /^[[:xdigit:]]{256}$/
+ raise Chef::Exceptions::User, "Password must be a SALTED-SHA512-PBKDF2 shadow hash entropy when a shadow hash salt is given"
+ end
+
+ password
+ },
+ callbacks: {
+ "Password length must be >= 4" => ->(password) { password.size >= 4 },
+ }
+
+ # Overload home so we set our default.
+ property :home, String, description: "The user home directory", default: lazy { "/Users/#{name}" }
+
+ property :admin, [TrueClass, FalseClass], description: "Create the user as an admin", default: false
+
+ # TCC on macOS >= 10.14 requires admin credentials of an Admin user that
+ # has SecureToken enabled in order to toggle SecureToken.
+ property :admin_username, String, description: "Admin username for superuser actions"
+ property :admin_password, String, description: "Admin password for superuser actions", sensitive: true
+
+ property :secure_token, [TrueClass, FalseClass], description: "Enable SecureToken for the user", default: false
+ # In order to enable SecureToken for a user we require the plaintext password.
+ property :secure_token_password, String, description: "The plaintext password for enabling SecureToken", sensitive: true, default: lazy {
+ # In some cases the user can pass the plaintext value to "password" instead of
+ # SALTED-SHA512-PBKDF2 entropy. In those cases we'll default to the
+ # same value.
+ (salt.nil? && password) ? password : nil
+ }
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index d76f1bbf06..235c1e41a1 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -116,6 +116,7 @@ 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"
require_relative "resource/user/solaris_user"
require_relative "resource/user/windows_user"
diff --git a/lib/chef/version.rb b/lib/chef/version.rb
index 50cc73ae79..4d83abd4ed 100644
--- a/lib/chef/version.rb
+++ b/lib/chef/version.rb
@@ -23,7 +23,7 @@ require_relative "version_string"
class Chef
CHEF_ROOT = File.expand_path("../..", __FILE__)
- VERSION = Chef::VersionString.new("15.3.6")
+ VERSION = Chef::VersionString.new("15.3.10")
end
#
diff --git a/omnibus/Gemfile.lock b/omnibus/Gemfile.lock
index 09d7e4f6f6..700031007b 100644
--- a/omnibus/Gemfile.lock
+++ b/omnibus/Gemfile.lock
@@ -18,7 +18,7 @@ GIT
GIT
remote: https://github.com/chef/omnibus-software
- revision: b851a32e09a384cad768a6d44df267ffe920cfc2
+ revision: 82ff3963cf4624afd77dfd283a187e25d21325b9
branch: master
specs:
omnibus-software (4.0.0)
@@ -33,8 +33,8 @@ GEM
artifactory (3.0.5)
awesome_print (1.8.0)
aws-eventstream (1.0.3)
- aws-partitions (1.208.0)
- aws-sdk-core (3.66.0)
+ aws-partitions (1.211.0)
+ aws-sdk-core (3.67.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.1)
@@ -198,7 +198,7 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
- minitar (0.8)
+ minitar (0.9)
mixlib-archive (1.0.1)
mixlib-log
mixlib-archive (1.0.1-universal-mingw32)
@@ -234,7 +234,7 @@ GEM
nori (2.6.0)
octokit (4.14.0)
sawyer (~> 0.8.0, >= 0.5.3)
- ohai (15.2.5)
+ ohai (15.3.1)
chef-config (>= 12.8, < 16)
ffi (~> 1.9)
ffi-yajl (~> 2.2)
@@ -263,7 +263,7 @@ GEM
retryable (3.0.4)
ruby-progressbar (1.10.1)
rubyntlm (0.6.2)
- rubyzip (1.2.3)
+ rubyzip (1.2.4)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
@@ -279,12 +279,12 @@ GEM
structured_warnings (0.4.0)
syslog-logger (1.6.8)
systemu (2.6.5)
- test-kitchen (2.3.1)
+ test-kitchen (2.3.2)
bcrypt_pbkdf (~> 1.0)
ed25519 (~> 1.2)
license-acceptance (~> 1.0, >= 1.0.11)
mixlib-install (~> 3.6)
- mixlib-shellout (>= 1.2, < 3.0)
+ mixlib-shellout (>= 1.2, < 4.0)
net-scp (>= 1.1, < 3.0)
net-ssh (>= 2.9, < 6.0)
net-ssh-gateway (>= 1.2, < 3.0)
diff --git a/omnibus_overrides.rb b/omnibus_overrides.rb
index 1f33f9be1f..cd8d0f1cd8 100644
--- a/omnibus_overrides.rb
+++ b/omnibus_overrides.rb
@@ -16,10 +16,10 @@ override "libxslt", version: "1.1.30"
override "libyaml", version: "0.1.7"
override "makedepend", version: "1.0.5"
override "ncurses", version: "5.9"
-override "nokogiri", version: "1.10.2"
+override "nokogiri", version: "1.10.4"
override "openssl", version: "1.0.2s"
override "pkg-config-lite", version: "0.28-1"
-override "ruby", version: "2.6.3"
+override "ruby", version: "2.6.4"
override "ruby-windows-devkit-bash", version: "3.1.23-4-msys-1.0.18"
override "util-macros", version: "1.19.0"
override "xproto", version: "7.0.28"
diff --git a/spec/functional/resource/user/mac_user_spec.rb b/spec/functional/resource/user/mac_user_spec.rb
new file mode 100644
index 0000000000..d01b08616e
--- /dev/null
+++ b/spec/functional/resource/user/mac_user_spec.rb
@@ -0,0 +1,188 @@
+#
+# Copyright:: Copyright 2019, 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_1014: true,
+ requires_root: true,
+}
+
+describe "Chef::Resource::User with Chef::Provider::User::MacUser 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 . -read /Users/#{username}").error?).to be(false)
+ 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::MacUser.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/spec_helper.rb b/spec/spec_helper.rb
index 5ab97a8320..e19a06f3d7 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -145,6 +145,7 @@ 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 mac_osx?
config.filter_run_excluding macos_only: true unless mac_osx?
+ config.filter_run_excluding macos_1014: true unless mac_osx_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 0b13169ac1..3ac657d84e 100644
--- a/spec/support/platform_helpers.rb
+++ b/spec/support/platform_helpers.rb
@@ -3,6 +3,7 @@ require "chef/mixin/shell_out"
require "ohai/mixin/http_helper"
require "ohai/mixin/gce_metadata"
require "chef/mixin/powershell_out"
+require "chef/version_class"
class ShellHelpers
extend Chef::Mixin::ShellOut
@@ -110,6 +111,15 @@ def mac_osx_106?
false
end
+def mac_osx_1014?
+ if mac_osx?
+ ver = Chef::Version.new(ohai[:platform_version])
+ return ver.major == 10 && ver.minor == 14
+ end
+
+ false
+end
+
def mac_osx?
if File.exists? "/usr/bin/sw_vers"
result = ShellHelpers.shell_out("/usr/bin/sw_vers")
diff --git a/spec/unit/cookbook/gem_installer_spec.rb b/spec/unit/cookbook/gem_installer_spec.rb
index b7c8db514a..807e801aaf 100644
--- a/spec/unit/cookbook/gem_installer_spec.rb
+++ b/spec/unit/cookbook/gem_installer_spec.rb
@@ -59,23 +59,25 @@ describe Chef::Cookbook::GemInstaller do
expect(File).to receive(:open).and_yield(gemfile)
expect(gemfile).to receive(:path).and_return("")
expect(IO).to receive(:read).and_return("")
- expect(gem_installer).to receive(:shell_out!).and_return(shell_out)
end
it "generates a valid Gemfile" do
+ expect(gem_installer).to receive(:shell_out!).and_return(shell_out)
expect { gem_installer.install }.to_not raise_error
expect { bundler_dsl }.to_not raise_error
end
it "generate a Gemfile with all constraints" do
+ expect(gem_installer).to receive(:shell_out!).and_return(shell_out)
expect { gem_installer.install }.to_not raise_error
expect(bundler_dsl.dependencies.find { |d| d.name == "httpclient" }.requirements_list.length).to eql(2)
end
it "generates a valid Gemfile when Chef::Config[:rubygems_url] is set to a String" do
+ expect(gem_installer).to receive(:shell_out!).and_return(shell_out)
Chef::Config[:rubygems_url] = "https://www.rubygems.org"
expect { gem_installer.install }.to_not raise_error
@@ -83,10 +85,29 @@ describe Chef::Cookbook::GemInstaller do
end
it "generates a valid Gemfile when Chef::Config[:rubygems_url] is set to an Array" do
+ expect(gem_installer).to receive(:shell_out!).and_return(shell_out)
Chef::Config[:rubygems_url] = [ "https://www.rubygems.org" ]
expect { gem_installer.install }.to_not raise_error
expect(bundler_dsl.dependencies.find { |d| d.name == "httpclient" }.requirements_list.length).to eql(2)
end
+
+ it "skip metadata installation when Chef::Config[:skip_gem_metadata_installation] is set to true" do
+ Chef::Config[:skip_gem_metadata_installation] = true
+ expect(gem_installer.install).to_not receive(:shell_out!)
+ end
+
+ it "install metadata when Chef::Config[:skip_gem_metadata_installation] is not true" do
+ expect(gem_installer).to receive(:shell_out!).and_return(shell_out)
+ expect(Chef::Log).to receive(:info).and_return("")
+ expect(gem_installer.install).to be_nil
+ end
+
+ it "install from local cache when Chef::Config[:gem_installer_bundler_options] is set to local" do
+ Chef::Config[:gem_installer_bundler_options] = "--local"
+ expect(gem_installer).to receive(:shell_out!).with(["bundle", "install", "--local"], any_args).and_return(shell_out)
+ expect(Chef::Log).to receive(:info).and_return("")
+ expect(gem_installer.install).to be_nil
+ end
end
diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb
index 5f4be8dfa2..752fcff1e3 100644
--- a/spec/unit/knife/bootstrap_spec.rb
+++ b/spec/unit/knife/bootstrap_spec.rb
@@ -1021,7 +1021,6 @@ describe Chef::Knife::Bootstrap do
verify_host_key: nil,
port: 9999,
non_interactive: true,
- pty: true,
}
end
@@ -1076,7 +1075,6 @@ describe Chef::Knife::Bootstrap do
verify_host_key: nil, # Config
port: 12, # cli
non_interactive: true,
- pty: true,
}
end
@@ -1128,7 +1126,6 @@ describe Chef::Knife::Bootstrap do
sudo_password: "blah",
verify_host_key: true,
non_interactive: true,
- pty: true,
}
end
it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do
@@ -1152,7 +1149,6 @@ describe Chef::Knife::Bootstrap do
sudo: false,
verify_host_key: "always",
non_interactive: true,
- pty: true,
connection_timeout: 60,
}
end
@@ -1504,7 +1500,6 @@ describe Chef::Knife::Bootstrap do
let(:default_opts) do
{
non_interactive: true,
- pty: true,
forward_agent: false,
connection_timeout: 60,
}
@@ -2003,6 +1998,19 @@ describe Chef::Knife::Bootstrap do
expect(connection).to receive(:connect!)
knife.do_connect({})
end
+
+ context "when sshd confgiured with requiretty" do
+ let(:pty_err_msg) { "Sudo requires a TTY. Please see the README on how to configure sudo to allow for non-interactive usage." }
+ let(:expected_error) { Train::UserError.new(pty_err_msg, :sudo_no_tty) }
+ before do
+ allow(connection).to receive(:connect!).and_raise(expected_error)
+ end
+ it "retry with pty true request option" do
+ expect(Chef::Knife::Bootstrap::TrainConnector).to receive(:new).and_return(connection).exactly(2).times
+ expect(knife.ui).to receive(:warn).with("#{pty_err_msg} - trying with pty request")
+ expect { knife.do_connect({}) }.to raise_error(expected_error)
+ end
+ end
end
describe "validate_winrm_transport_opts!" do
diff --git a/spec/unit/provider/user/dscl_spec.rb b/spec/unit/provider/user/dscl_spec.rb
index b12ea78977..e20873dc92 100644
--- a/spec/unit/provider/user/dscl_spec.rb
+++ b/spec/unit/provider/user/dscl_spec.rb
@@ -35,6 +35,7 @@ describe Chef::Provider::User::Dscl do
let(:node) do
Chef::Node.new.tap do |node|
node.automatic["os"] = "darwin"
+ node.automatic["platform_version"] = "10.13.0"
end
end
diff --git a/spec/unit/provider/user/mac_spec.rb b/spec/unit/provider/user/mac_spec.rb
new file mode 100644
index 0000000000..f7024f45c1
--- /dev/null
+++ b/spec/unit/provider/user/mac_spec.rb
@@ -0,0 +1,38 @@
+#
+# Author:: Ryan Cragun (<ryan@chef.io>)
+# Copyright:: Copyright (c) 2019 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"
+
+describe Chef::Provider::User::MacUser do
+ before do
+ allow(ChefConfig).to receive(:windows?) { false }
+ end
+
+ let(:new_resource) { Chef::Resource::User::MacUser.new("jane") }
+
+ let(:provider) do
+ node = Chef::Node.new
+ events = Chef::EventDispatch::Dispatcher.new
+ run_context = Chef::RunContext.new(node, {}, events)
+ described_class.new(new_resource, run_context)
+ end
+
+ it "responds to load_current_resource" do
+ expect(provider).to respond_to(:load_current_resource)
+ end
+end