diff options
author | Lamont Granquist <lamont@chef.io> | 2020-08-03 15:54:47 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-03 15:54:47 -0700 |
commit | 8ed7a20faaa13c50fd69da35a41a7633951c655d (patch) | |
tree | 5c50eb4e555af460572c18bf23cf388105ddf0b1 | |
parent | e066e76fd41b0e90c6610bcf9cb0a024226f0c3a (diff) | |
parent | 6f93233791693b4ae193312628cb615c0fabe172 (diff) | |
download | chef-8ed7a20faaa13c50fd69da35a41a7633951c655d.tar.gz |
Merge pull request #10239 from chef/lcg/osx_profile
Convert osx_profile to custom resource
-rw-r--r-- | lib/chef/provider/osx_profile.rb | 255 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/osx_profile.rb | 232 | ||||
-rw-r--r-- | spec/unit/provider/osx_profile_spec.rb | 255 | ||||
-rw-r--r-- | spec/unit/provider_resolver_spec.rb | 2 | ||||
-rw-r--r-- | spec/unit/resource/osx_profile_spec.rb | 233 |
6 files changed, 461 insertions, 517 deletions
diff --git a/lib/chef/provider/osx_profile.rb b/lib/chef/provider/osx_profile.rb deleted file mode 100644 index 07d35e633c..0000000000 --- a/lib/chef/provider/osx_profile.rb +++ /dev/null @@ -1,255 +0,0 @@ -# -# Author:: Nate Walck (<nate.walck@gmail.com>) -# Copyright:: Copyright 2015-2016, Facebook, 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 "../log" -require_relative "../provider" -require_relative "../resource" -require_relative "../resource/file" -require "uuidtools" -require "plist" - -class Chef - class Provider - class OsxProfile < Chef::Provider - provides :osx_profile - provides :osx_config_profile - - def load_current_resource - @current_resource = Chef::Resource::OsxProfile.new(new_resource.name) - current_resource.profile_name(new_resource.profile_name) - - all_profiles = get_installed_profiles - # FIXME: stop mutating the desired state - new_resource.profile( - new_resource.profile || - new_resource.profile_name - ) - - @new_profile_hash = get_profile_hash(new_resource.profile) - if @new_profile_hash - @new_profile_hash["PayloadUUID"] = - config_uuid(@new_profile_hash) - end - - if @new_profile_hash - @new_profile_identifier = @new_profile_hash["PayloadIdentifier"] - else - @new_profile_identifier = new_resource.identifier || - new_resource.profile_name - end - - current_profile = nil - if all_profiles && all_profiles.key?("_computerlevel") - current_profile = all_profiles["_computerlevel"].find do |item| - item["ProfileIdentifier"] == @new_profile_identifier - end - end - current_resource.profile(current_profile) - end - - def define_resource_requirements - requirements.assert(:remove) do |a| - if @new_profile_identifier - a.assertion do - !@new_profile_identifier.nil? && - !@new_profile_identifier.end_with?(".mobileconfig") && - /^\w+(?:(\.| )\w+)+$/.match(@new_profile_identifier) - end - a.failure_message RuntimeError, "when removing using the identifier property, it must match the profile identifier" - else - new_profile_name = new_resource.profile_name - a.assertion do - !new_profile_name.end_with?(".mobileconfig") && - /^\w+(?:(\.| )\w+)+$/.match(new_profile_name) - end - a.failure_message RuntimeError, "When removing by resource name, it must match the profile identifier " - end - end - - requirements.assert(:install) do |a| - if @new_profile_hash.is_a?(Hash) - a.assertion do - @new_profile_hash.include?("PayloadIdentifier") - end - a.failure_message RuntimeError, "The specified profile does not seem to be valid" - end - if @new_profile_hash.is_a?(String) - a.assertion do - @new_profile_hash.end_with?(".mobileconfig") - end - a.failure_message RuntimeError, "#{new_profile_hash}' is not a valid profile" - end - end - end - - action :install do - unless profile_installed? - converge_by("install profile #{@new_profile_identifier}") do - profile_path = write_profile_to_disk - install_profile(profile_path) - get_installed_profiles(true) - end - end - end - - action :remove do - # Clean up profile after removing it - if profile_installed? - converge_by("remove profile #{@new_profile_identifier}") do - remove_profile - get_installed_profiles(true) - end - end - end - - def load_profile_hash(new_profile) - # file must exist in cookbook - if new_profile.end_with?(".mobileconfig") - unless cookbook_file_available?(new_profile) - error_string = "#{self}: '#{new_profile}' not found in cookbook" - raise Chef::Exceptions::FileNotFound, error_string - end - cookbook_profile = cache_cookbook_profile(new_profile) - read_plist(cookbook_profile) - else - nil - end - end - - def cookbook_file_available?(cookbook_file) - run_context.has_cookbook_file_in_cookbook?( - new_resource.cookbook_name, cookbook_file - ) - end - - def get_cache_dir - cache_dir = Chef::FileCache.create_cache_path( - "profiles/#{new_resource.cookbook_name}" - ) - end - - def cache_cookbook_profile(cookbook_file) - Chef::FileCache.create_cache_path( - ::File.join( - "profiles", - new_resource.cookbook_name, - ::File.dirname(cookbook_file) - ) - ) - # FIXME: should use a real cookbook file, or document what this craziness is - remote_file = Chef::Resource::CookbookFile.new( - ::File.join( - get_cache_dir, - "#{cookbook_file}.remote" - ), - run_context - ) - remote_file.cookbook_name = new_resource.cookbook_name - remote_file.source(cookbook_file) - remote_file.backup(false) - remote_file.run_action(:create) - remote_file.path - end - - def get_profile_hash(new_profile) - if new_profile.is_a?(Hash) - new_profile - elsif new_profile.is_a?(String) - load_profile_hash(new_profile) - end - end - - def config_uuid(profile) - # Make a UUID of the profile contents and return as string - UUIDTools::UUID.sha1_create( - UUIDTools::UUID_DNS_NAMESPACE, - profile.to_s - ).to_s - end - - def write_profile_to_disk - # FIXME: use a real chef file resource and stop hacking up tempfiles directly - new_resource.path(Chef::FileCache.create_cache_path("profiles")) - tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile - tempfile.write(@new_profile_hash.to_plist) - tempfile.close - tempfile.path - end - - def install_profile(profile_path) - cmd = [ "/usr/bin/profiles", "-I", "-F", profile_path ] - logger.trace("cmd: #{cmd.join(" ")}") - shellout_results = shell_out(*cmd) - shellout_results.exitstatus - end - - def remove_profile - cmd = [ "/usr/bin/profiles", "-R", "-p", @new_profile_identifier ] - logger.trace("cmd: #{cmd.join(" ")}") - shellout_results = shell_out(*cmd) - shellout_results.exitstatus - end - - def get_installed_profiles(update = nil) - if update - node.run_state[:config_profiles] = query_installed_profiles - else - node.run_state[:config_profiles] ||= query_installed_profiles - end - end - - def query_installed_profiles - # Dump all profile metadata to a tempfile - tempfile = generate_tempfile - write_installed_profiles(tempfile) - installed_profiles = read_plist(tempfile) - logger.trace("Saved profiles to run_state") - # Clean up the temp file as we do not need it anymore - ::File.unlink(tempfile) - installed_profiles - end - - def generate_tempfile - tempfile = ::Dir::Tmpname.create("allprofiles.plist") {} - end - - def write_installed_profiles(tempfile) - shell_out!( "/usr/bin/profiles", "-P", "-o", tempfile ) - end - - def read_plist(xml_file) - ::Plist.parse_xml(xml_file) - end - - def profile_installed? - # Profile Identifier and UUID must match a currently installed profile - if current_resource.profile.nil? || current_resource.profile.empty? - false - else - if new_resource.action.include?(:remove) - true - else - current_resource.profile["ProfileUUID"] == - @new_profile_hash["PayloadUUID"] - end - end - end - - end - end -end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index b085160c0e..d2e9f1991b 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -36,7 +36,6 @@ require_relative "provider/mount" require_relative "provider/noop" require_relative "provider/package" require_relative "provider/powershell_script" -require_relative "provider/osx_profile" require_relative "provider/remote_directory" require_relative "provider/remote_file" require_relative "provider/route" diff --git a/lib/chef/resource/osx_profile.rb b/lib/chef/resource/osx_profile.rb index 40be334678..985d708008 100644 --- a/lib/chef/resource/osx_profile.rb +++ b/lib/chef/resource/osx_profile.rb @@ -17,6 +17,10 @@ # require_relative "../resource" +require_relative "../log" +require_relative "../resource/file" +require "uuidtools" +require "plist" class Chef class Resource @@ -29,9 +33,6 @@ class Chef description "Use the **osx_profile** resource to manage configuration profiles (.mobileconfig files) on the macOS platform. The osx_profile resource installs profiles by using the uuidgen library to generate a unique ProfileUUID, and then using the profiles command to install the profile on the system." introduced "12.7" - default_action :install - allowed_actions :install, :remove - property :profile_name, String, description: "Use to specify the name of the profile, if different from the name of the resource block.", name_property: true @@ -42,8 +43,229 @@ class Chef property :identifier, String, description: "Use to specify the identifier for the profile, such as com.company.screensaver." - property :path, String, - description: "The path to write the profile to disk before loading it." + # this is not a property it is necessary for the tempfile this resource uses to work (FIXME: this is terrible) + # + # @api private + # + def path(path = nil) + @path ||= path + @path + end + + action_class do + def load_current_resource + @current_resource = Chef::Resource::OsxProfile.new(new_resource.name) + current_resource.profile_name(new_resource.profile_name) + + if new_profile_hash + new_profile_hash["PayloadUUID"] = config_uuid(new_profile_hash) + end + + current_resource.profile(current_profile) + end + + def current_profile + all_profiles = get_installed_profiles + + if all_profiles && all_profiles.key?("_computerlevel") + return all_profiles["_computerlevel"].find do |item| + item["ProfileIdentifier"] == new_profile_identifier + end + end + nil + end + + def invalid_profile_name?(name_or_identifier) + name_or_identifier.end_with?(".mobileconfig") || !/^\w+(?:(\.| )\w+)+$/.match(name_or_identifier) + end + + def check_resource_semantics! + if mac? && node["platform_version"] =~ "> 10.15" + raise "The osx_profile resource is not available on macOS Bug Sur or above due to the removal of apple support for CLI installation of profiles" + end + + if action == :remove + if new_profile_identifier + if invalid_profile_name?(new_profile_identifier) + raise "when removing using the identifier property, it must match the profile identifier" + end + else + if invalid_profile_name?(new_resource.profile_name) + raise "When removing by resource name, it must match the profile identifier" + end + end + end + + if action == :install + if new_profile_hash.is_a?(Hash) && !new_profile_hash.include?("PayloadIdentifier") + raise "The specified profile does not seem to be valid" + end + if new_profile_hash.is_a?(String) && !new_profile_hash.end_with?(".mobileconfig") + raise "#{new_profile_hash}' is not a valid profile" + end + end + end + end + + action :install do + unless profile_installed? + converge_by("install profile #{new_profile_identifier}") do + profile_path = write_profile_to_disk + install_profile(profile_path) + get_installed_profiles(true) + end + end + end + + action :remove do + # Clean up profile after removing it + if profile_installed? + converge_by("remove profile #{new_profile_identifier}") do + remove_profile + get_installed_profiles(true) + end + end + end + + action_class do + private + + def profile + @profile ||= new_resource.profile || new_resource.profile_name + end + + def new_profile_hash + @new_profile_hash ||= get_profile_hash(profile) + end + + def new_profile_identifier + @new_profile_identifier ||= if new_profile_hash + new_profile_hash["PayloadIdentifier"] + else + new_resource.identifier || new_resource.profile_name + end + end + + def load_profile_hash(new_profile) + # file must exist in cookbook + return nil unless new_profile.end_with?(".mobileconfig") + + unless cookbook_file_available?(new_profile) + raise Chef::Exceptions::FileNotFound, "#{self}: '#{new_profile}' not found in cookbook" + end + + cookbook_profile = cache_cookbook_profile(new_profile) + ::Plist.parse_xml(cookbook_profile) + end + + def cookbook_file_available?(cookbook_file) + run_context.has_cookbook_file_in_cookbook?( + new_resource.cookbook_name, cookbook_file + ) + end + + def get_cache_dir + Chef::FileCache.create_cache_path( + "profiles/#{new_resource.cookbook_name}" + ) + end + + def cache_cookbook_profile(cookbook_file) + Chef::FileCache.create_cache_path( + ::File.join( + "profiles", + new_resource.cookbook_name, + ::File.dirname(cookbook_file) + ) + ) + + path = ::File.join( get_cache_dir, "#{cookbook_file}.remote") + + cookbook_file path do + cookbook_name = new_resource.cookbook_name + source(cookbook_file) + backup(false) + run_action(:create) + end + + path + end + + def get_profile_hash(new_profile) + if new_profile.is_a?(Hash) + new_profile + elsif new_profile.is_a?(String) + load_profile_hash(new_profile) + end + end + + def config_uuid(profile) + # Make a UUID of the profile contents and return as string + UUIDTools::UUID.sha1_create( + UUIDTools::UUID_DNS_NAMESPACE, + profile.to_s + ).to_s + end + + def write_profile_to_disk + # FIXME: this is kind of terrible, the resource needs a tempfile to use and + # wants it created similarly to the file providers (with all the magic necessary + # for determining if it should go in the cwd or into a tmpdir), but it abuses + # the Chef::FileContentManagement::Tempfile API to do that, which requires setting + # a `path` method on the resource because of tight-coupling to the file provider + # pattern. We don't just want to use a file here because the point is to get + # at the tempfile pattern from the file provider, but to feed that into a shell + # command rather than deploying the file to somewhere on disk. There's some + # better API that needs extracting here. + new_resource.path(Chef::FileCache.create_cache_path("profiles")) + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile + tempfile.write(new_profile_hash.to_plist) + tempfile.close + tempfile.path + end + + def install_profile(profile_path) + cmd = [ "/usr/bin/profiles", "-I", "-F", profile_path ] + logger.trace("cmd: #{cmd.join(" ")}") + shell_out!(*cmd) + end + + def remove_profile + cmd = [ "/usr/bin/profiles", "-R", "-p", new_profile_identifier ] + logger.trace("cmd: #{cmd.join(" ")}") + shell_out!(*cmd) + end + + # + # FIXME FIXME FIXME + # The node object should not be used for caching state like this and this is not a public API and may break. + # FIXME FIXME FIXME + # + + def get_installed_profiles(update = nil) + if update + node.run_state[:config_profiles] = query_installed_profiles + else + node.run_state[:config_profiles] ||= query_installed_profiles + end + logger.trace("Saved profiles to run_state") + end + + def query_installed_profiles + Tempfile.open("allprofiles.plist") do |tempfile| + shell_out!( "/usr/bin/profiles", "-P", "-o", tempfile.path ) + ::Plist.parse_xml(tempfile) + end + end + + def profile_installed? + # Profile Identifier and UUID must match a currently installed profile + return false if current_resource.profile.nil? || current_resource.profile.empty? + return true if action == :remove + + current_resource.profile["ProfileUUID"] == new_profile_hash["PayloadUUID"] + end + end end end end diff --git a/spec/unit/provider/osx_profile_spec.rb b/spec/unit/provider/osx_profile_spec.rb deleted file mode 100644 index 9554f235f3..0000000000 --- a/spec/unit/provider/osx_profile_spec.rb +++ /dev/null @@ -1,255 +0,0 @@ -# -# Author:: Nate Walck (<nate.walck@gmail.com>) -# Copyright:: Copyright 2015-2016, Chef, 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::OsxProfile do - let(:shell_out_success) do - double("shell_out", exitstatus: 0, error?: false) - end - describe "action_create" do - let(:node) { Chef::Node.new } - let(:events) { Chef::EventDispatch::Dispatcher.new } - let(:run_context) { Chef::RunContext.new(node, {}, events) } - let(:new_resource) { Chef::Resource::OsxProfile.new("Profile Test", run_context) } - let(:provider) { Chef::Provider::OsxProfile.new(new_resource, run_context) } - let(:all_profiles) do - { "_computerlevel" => [{ "ProfileDisplayName" => "Finder Settings", - "ProfileIdentifier" => "com.apple.finder", - "ProfileInstallDate" => "2015-11-08 23:15:21 +0000", - "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.finder" => { "Forced" => [{ "mcx_preference_settings" => { "ShowExternalHardDrivesOnDesktop" => false } }] } } }, - "PayloadDisplayName" => "Custom: (com.apple.finder)", - "PayloadIdentifier" => "com.apple.finder", - "PayloadType" => "com.apple.ManagedClient.preferences", - "PayloadUUID" => "a017048f-684b-4e81-baa3-43afe316d739", - "PayloadVersion" => 1 }], - "ProfileOrganization" => "Chef", - "ProfileRemovalDisallowed" => "false", - "ProfileType" => "Configuration", - "ProfileUUID" => "e2e09bef-e673-44a6-bcbe-ecb5f1c1b740", - "ProfileVerificationState" => "unsigned", - "ProfileVersion" => 1 }, - { "ProfileDisplayName" => "ScreenSaver Settings", - "ProfileIdentifier" => "com.testprofile.screensaver", - "ProfileInstallDate" => "2015-10-05 23:15:21 +0000", - "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.screensaver" => { "Forced" => [{ "mcx_preference_settings" => { "idleTime" => 0 } }] } } }, - "PayloadDisplayName" => "Custom: (com.apple.screensaver)", - "PayloadIdentifier" => "com.apple.screensaver", - "PayloadType" => "com.apple.ManagedClient.preferences", - "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c110", - "PayloadVersion" => 1 }], - "ProfileOrganization" => "Chef", - "ProfileRemovalDisallowed" => "false", - "ProfileType" => "Configuration", - "ProfileUUID" => "6e95927c-f200-54b4-85c7-52ab99b61c47", - "ProfileVerificationState" => "unsigned", - "ProfileVersion" => 1 }], - } - end - # If anything is changed within this profile, be sure to update the - # ProfileUUID in all_profiles to match the new config specific UUID - let(:test_profile) do - { - "PayloadIdentifier" => "com.testprofile.screensaver", - "PayloadRemovalDisallowed" => false, - "PayloadScope" => "System", - "PayloadType" => "Configuration", - "PayloadUUID" => "1781fbec-3325-565f-9022-8aa28135c3cc", - "PayloadOrganization" => "Chef", - "PayloadVersion" => 1, - "PayloadDisplayName" => "Screensaver Settings", - "PayloadContent" => [ - { - "PayloadType" => "com.apple.ManagedClient.preferences", - "PayloadVersion" => 1, - "PayloadIdentifier" => "com.testprofile.screensaver", - "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c108", - "PayloadEnabled" => true, - "PayloadDisplayName" => "com.apple.screensaver", - "PayloadContent" => { - "com.apple.screensaver" => { - "Forced" => [ - { - "mcx_preference_settings" => { - "idleTime" => 0, - }, - }, - ], - }, - }, - }, - ], - } - end - let(:no_profiles) do - {} - end - - before(:each) do - allow(provider).to receive(:cookbook_file_available?).and_return(true) - allow(provider).to receive(:cache_cookbook_profile).and_return("/tmp/test.mobileconfig.remote") - allow(provider).to receive(:get_new_profile_hash).and_return(test_profile) - allow(provider).to receive(:get_installed_profiles).and_return(all_profiles) - allow(provider).to receive(:read_plist).and_return(all_profiles) - allow(::File).to receive(:unlink).and_return(true) - end - - it "should build the get all profiles shellout command correctly" do - profile_name = "com.testprofile.screensaver.mobileconfig" - tempfile = "/tmp/allprofiles.plist" - new_resource.profile_name profile_name - allow(provider).to receive(:generate_tempfile).and_return(tempfile) - allow(provider).to receive(:get_installed_profiles).and_call_original - allow(provider).to receive(:read_plist).and_return(all_profiles) - expect(provider).to receive(:shell_out_compacted!).with("/usr/bin/profiles", "-P", "-o", "/tmp/allprofiles.plist") - provider.load_current_resource - end - - it "should use profile name as profile when no profile is set" do - profile_name = "com.testprofile.screensaver.mobileconfig" - new_resource.profile_name profile_name - provider.load_current_resource - expect(new_resource.profile_name).to eql(profile_name) - end - - it "should use identifier from specified profile" do - new_resource.profile test_profile - provider.load_current_resource - expect( - provider.instance_variable_get(:@new_profile_identifier) - ).to eql(test_profile["PayloadIdentifier"]) - end - - it "should install when not installed" do - new_resource.profile test_profile - allow(provider).to receive(:get_installed_profiles).and_return(no_profiles) - provider.load_current_resource - expect(provider).to receive(:install_profile) - expect { provider.run_action(:install) }.to_not raise_error - end - - it "does not install if the profile is already installed" do - new_resource.profile test_profile - allow(provider).to receive(:get_installed_profiles).and_return(all_profiles) - provider.load_current_resource - expect(provider).to_not receive(:install_profile) - expect { provider.action_install }.to_not raise_error - end - - it "should install when installed but uuid differs" do - new_resource.profile test_profile - all_profiles["_computerlevel"][1]["ProfileUUID"] = "1781fbec-3325-565f-9022-9bb39245d4dd" - provider.load_current_resource - expect(provider).to receive(:install_profile) - expect { provider.run_action(:install) }.to_not raise_error - end - - it "should build the shellout install command correctly" do - profile_path = "/tmp/test.mobileconfig" - new_resource.profile test_profile - # Change the profile so it triggers an install - all_profiles["_computerlevel"][1]["ProfileUUID"] = "1781fbec-3325-565f-9022-9bb39245d4dd" - provider.load_current_resource - allow(provider).to receive(:write_profile_to_disk).and_return(profile_path) - expect(provider).to receive(:shell_out_compacted).with("/usr/bin/profiles", "-I", "-F", profile_path).and_return(shell_out_success) - provider.action_install - end - - it "should fail if there is no identifier inside the profile" do - test_profile.delete("PayloadIdentifier") - new_resource.profile test_profile - error_message = "The specified profile does not seem to be valid" - expect { provider.run_action(:install) }.to raise_error(RuntimeError, error_message) - end - - end - - describe "action_remove" do - let(:node) { Chef::Node.new } - let(:events) { Chef::EventDispatch::Dispatcher.new } - let(:run_context) { Chef::RunContext.new(node, {}, events) } - let(:new_resource) { Chef::Resource::OsxProfile.new("Profile Test", run_context) } - let(:provider) { Chef::Provider::OsxProfile.new(new_resource, run_context) } - let(:current_resource) { Chef::Resource::OsxProfile.new("Profile Test") } - let(:all_profiles) do - { "_computerlevel" => [{ "ProfileDisplayName" => "ScreenSaver Settings", - "ProfileIdentifier" => "com.apple.screensaver", - "ProfileInstallDate" => "2015-10-05 23:15:21 +0000", - "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.screensaver" => { "Forced" => [{ "mcx_preference_settings" => { "idleTime" => 0 } }] } } }, - "PayloadDisplayName" => "Custom: (com.apple.screensaver)", - "PayloadIdentifier" => "com.apple.screensaver", - "PayloadType" => "com.apple.ManagedClient.preferences", - "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c108", - "PayloadVersion" => 1 }], - "ProfileOrganization" => "Chef", - "ProfileRemovalDisallowed" => "false", - "ProfileType" => "Configuration", - "ProfileUUID" => "1781fbec-3325-565f-9022-8aa28135c3cc", - "ProfileVerificationState" => "unsigned", - "ProfileVersion" => 1 }, - { "ProfileDisplayName" => "ScreenSaver Settings", - "ProfileIdentifier" => "com.testprofile.screensaver", - "ProfileInstallDate" => "2015-10-05 23:15:21 +0000", - "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.screensaver" => { "Forced" => [{ "mcx_preference_settings" => { "idleTime" => 0 } }] } } }, - "PayloadDisplayName" => "Custom: (com.apple.screensaver)", - "PayloadIdentifier" => "com.apple.screensaver", - "PayloadType" => "com.apple.ManagedClient.preferences", - "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c110", - "PayloadVersion" => 1 }], - "ProfileOrganization" => "Chef", - "ProfileRemovalDisallowed" => "false", - "ProfileType" => "Configuration", - "ProfileUUID" => "1781fbec-3325-565f-9022-8aa28135c3cc", - "ProfileVerificationState" => "unsigned", - "ProfileVersion" => 1 }], - } - end - before(:each) do - provider.current_resource = current_resource - allow(provider).to receive(:get_installed_profiles).and_return(all_profiles) - end - - it "should use resource name for identifier when not specified" do - new_resource.profile_name "com.testprofile.screensaver" - new_resource.action(:remove) - provider.load_current_resource - expect(provider.instance_variable_get(:@new_profile_identifier)).to eql(new_resource.profile_name) - end - - it "should use specified identifier" do - new_resource.identifier "com.testprofile.screensaver" - new_resource.action(:remove) - provider.load_current_resource - expect(provider.instance_variable_get(:@new_profile_identifier)).to eql(new_resource.identifier) - end - - it "should work with spaces in the identifier" do - provider.action = :remove - provider.define_resource_requirements - expect { provider.process_resource_requirements }.not_to raise_error - end - - it "should build the shellout remove command correctly" do - new_resource.identifier "com.testprofile.screensaver" - new_resource.action(:remove) - provider.load_current_resource - expect(provider).to receive(:shell_out_compacted).with("/usr/bin/profiles", "-R", "-p", new_resource.identifier).and_return(shell_out_success) - provider.action_remove - end - end -end diff --git a/spec/unit/provider_resolver_spec.rb b/spec/unit/provider_resolver_spec.rb index dc8619a663..f4212e3a8d 100644 --- a/spec/unit/provider_resolver_spec.rb +++ b/spec/unit/provider_resolver_spec.rb @@ -722,7 +722,7 @@ describe Chef::ProviderResolver do %w{mac_os_x mac_os_x_server} => { group: [ Chef::Resource::Group, Chef::Provider::Group::Dscl ], package: [ Chef::Resource::HomebrewPackage, Chef::Provider::Package::Homebrew ], - osx_profile: [ Chef::Resource::OsxProfile, Chef::Provider::OsxProfile], + osx_profile: [ Chef::Resource::OsxProfile], user: [ Chef::Resource::User::DsclUser, Chef::Provider::User::Dscl ], "mac_os_x" => { diff --git a/spec/unit/resource/osx_profile_spec.rb b/spec/unit/resource/osx_profile_spec.rb index 5653044358..a9acadced8 100644 --- a/spec/unit/resource/osx_profile_spec.rb +++ b/spec/unit/resource/osx_profile_spec.rb @@ -58,4 +58,237 @@ describe Chef::Resource::OsxProfile do resource.profile test_profile expect(resource.profile).to be_a(Hash) end + + let(:shell_out_success) do + double("shell_out", exitstatus: 0, error?: false) + end + + describe "action_create" do + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:resource) { Chef::Resource::OsxProfile.new("Profile Test", run_context) } + let(:provider) { resource.provider_for_action(:create) } + let(:all_profiles) do + { "_computerlevel" => [{ "ProfileDisplayName" => "Finder Settings", + "ProfileIdentifier" => "com.apple.finder", + "ProfileInstallDate" => "2015-11-08 23:15:21 +0000", + "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.finder" => { "Forced" => [{ "mcx_preference_settings" => { "ShowExternalHardDrivesOnDesktop" => false } }] } } }, + "PayloadDisplayName" => "Custom: (com.apple.finder)", + "PayloadIdentifier" => "com.apple.finder", + "PayloadType" => "com.apple.ManagedClient.preferences", + "PayloadUUID" => "a017048f-684b-4e81-baa3-43afe316d739", + "PayloadVersion" => 1 }], + "ProfileOrganization" => "Chef", + "ProfileRemovalDisallowed" => "false", + "ProfileType" => "Configuration", + "ProfileUUID" => "e2e09bef-e673-44a6-bcbe-ecb5f1c1b740", + "ProfileVerificationState" => "unsigned", + "ProfileVersion" => 1 }, + { "ProfileDisplayName" => "ScreenSaver Settings", + "ProfileIdentifier" => "com.testprofile.screensaver", + "ProfileInstallDate" => "2015-10-05 23:15:21 +0000", + "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.screensaver" => { "Forced" => [{ "mcx_preference_settings" => { "idleTime" => 0 } }] } } }, + "PayloadDisplayName" => "Custom: (com.apple.screensaver)", + "PayloadIdentifier" => "com.apple.screensaver", + "PayloadType" => "com.apple.ManagedClient.preferences", + "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c110", + "PayloadVersion" => 1 }], + "ProfileOrganization" => "Chef", + "ProfileRemovalDisallowed" => "false", + "ProfileType" => "Configuration", + "ProfileUUID" => "6e95927c-f200-54b4-85c7-52ab99b61c47", + "ProfileVerificationState" => "unsigned", + "ProfileVersion" => 1 }], + } + end + # If anything is changed within this profile, be sure to update the + # ProfileUUID in all_profiles to match the new config specific UUID + let(:test_profile) do + { + "PayloadIdentifier" => "com.testprofile.screensaver", + "PayloadRemovalDisallowed" => false, + "PayloadScope" => "System", + "PayloadType" => "Configuration", + "PayloadUUID" => "1781fbec-3325-565f-9022-8aa28135c3cc", + "PayloadOrganization" => "Chef", + "PayloadVersion" => 1, + "PayloadDisplayName" => "Screensaver Settings", + "PayloadContent" => [ + { + "PayloadType" => "com.apple.ManagedClient.preferences", + "PayloadVersion" => 1, + "PayloadIdentifier" => "com.testprofile.screensaver", + "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c108", + "PayloadEnabled" => true, + "PayloadDisplayName" => "com.apple.screensaver", + "PayloadContent" => { + "com.apple.screensaver" => { + "Forced" => [ + { + "mcx_preference_settings" => { + "idleTime" => 0, + }, + }, + ], + }, + }, + }, + ], + } + end + let(:no_profiles) do + {} + end + + before(:each) do + allow(provider).to receive(:cookbook_file_available?).and_return(true) + allow(provider).to receive(:cache_cookbook_profile).and_return("/tmp/test.mobileconfig.remote") + allow(provider).to receive(:get_new_profile_hash).and_return(test_profile) + allow(provider).to receive(:get_installed_profiles).and_return(all_profiles) + allow(provider).to receive(:read_plist).and_return(all_profiles) + allow(::File).to receive(:unlink).and_return(true) + end + + it "should build the get all profiles shellout command correctly" do + profile_name = "com.testprofile.screensaver.mobileconfig" + resource.profile_name profile_name + allow(provider).to receive(:get_installed_profiles).and_call_original + allow(provider).to receive(:read_plist).and_return(all_profiles) + expect(provider).to receive(:shell_out_compacted!).with("/usr/bin/profiles", "-P", "-o", kind_of(String)) + provider.load_current_resource + end + + it "should use profile name as profile when no profile is set" do + profile_name = "com.testprofile.screensaver.mobileconfig" + resource.profile_name profile_name + provider.load_current_resource + expect(resource.profile_name).to eql(profile_name) + end + + it "should use identifier from specified profile" do + resource.profile test_profile + provider.load_current_resource + expect( + provider.instance_variable_get(:@new_profile_identifier) + ).to eql(test_profile["PayloadIdentifier"]) + end + + it "should install when not installed" do + resource.profile test_profile + allow(provider).to receive(:get_installed_profiles).and_return(no_profiles) + provider.load_current_resource + expect(provider).to receive(:install_profile) + expect { provider.run_action(:install) }.to_not raise_error + end + + it "does not install if the profile is already installed" do + resource.profile test_profile + allow(provider).to receive(:get_installed_profiles).and_return(all_profiles) + provider.load_current_resource + expect(provider).to_not receive(:install_profile) + expect { provider.action_install }.to_not raise_error + end + + it "should install when installed but uuid differs" do + resource.profile test_profile + all_profiles["_computerlevel"][1]["ProfileUUID"] = "1781fbec-3325-565f-9022-9bb39245d4dd" + provider.load_current_resource + expect(provider).to receive(:install_profile) + expect { provider.run_action(:install) }.to_not raise_error + end + + it "should build the shellout install command correctly" do + profile_path = "/tmp/test.mobileconfig" + resource.profile test_profile + # Change the profile so it triggers an install + all_profiles["_computerlevel"][1]["ProfileUUID"] = "1781fbec-3325-565f-9022-9bb39245d4dd" + provider.load_current_resource + allow(provider).to receive(:write_profile_to_disk).and_return(profile_path) + expect(provider).to receive(:shell_out_compacted!).with("/usr/bin/profiles", "-I", "-F", profile_path).and_return(shell_out_success) + provider.action_install + end + + it "should fail if there is no identifier inside the profile" do + test_profile.delete("PayloadIdentifier") + resource.profile test_profile + error_message = "The specified profile does not seem to be valid" + expect { provider.run_action(:install) }.to raise_error(RuntimeError, error_message) + end + end + + describe "action_remove" do + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:resource) { Chef::Resource::OsxProfile.new("Profile Test", run_context) } + let(:provider) { resource.provider_for_action(:remove) } + let(:current_resource) { Chef::Resource::OsxProfile.new("Profile Test") } + let(:all_profiles) do + { "_computerlevel" => [{ "ProfileDisplayName" => "ScreenSaver Settings", + "ProfileIdentifier" => "com.apple.screensaver", + "ProfileInstallDate" => "2015-10-05 23:15:21 +0000", + "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.screensaver" => { "Forced" => [{ "mcx_preference_settings" => { "idleTime" => 0 } }] } } }, + "PayloadDisplayName" => "Custom: (com.apple.screensaver)", + "PayloadIdentifier" => "com.apple.screensaver", + "PayloadType" => "com.apple.ManagedClient.preferences", + "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c108", + "PayloadVersion" => 1 }], + "ProfileOrganization" => "Chef", + "ProfileRemovalDisallowed" => "false", + "ProfileType" => "Configuration", + "ProfileUUID" => "1781fbec-3325-565f-9022-8aa28135c3cc", + "ProfileVerificationState" => "unsigned", + "ProfileVersion" => 1 }, + { "ProfileDisplayName" => "ScreenSaver Settings", + "ProfileIdentifier" => "com.testprofile.screensaver", + "ProfileInstallDate" => "2015-10-05 23:15:21 +0000", + "ProfileItems" => [{ "PayloadContent" => { "PayloadContentManagedPreferences" => { "com.apple.screensaver" => { "Forced" => [{ "mcx_preference_settings" => { "idleTime" => 0 } }] } } }, + "PayloadDisplayName" => "Custom: (com.apple.screensaver)", + "PayloadIdentifier" => "com.apple.screensaver", + "PayloadType" => "com.apple.ManagedClient.preferences", + "PayloadUUID" => "73fc30e0-1e57-0131-c32d-000c2944c110", + "PayloadVersion" => 1 }], + "ProfileOrganization" => "Chef", + "ProfileRemovalDisallowed" => "false", + "ProfileType" => "Configuration", + "ProfileUUID" => "1781fbec-3325-565f-9022-8aa28135c3cc", + "ProfileVerificationState" => "unsigned", + "ProfileVersion" => 1 }], + } + end + + before(:each) do + provider.current_resource = current_resource + allow(provider).to receive(:get_installed_profiles).and_return(all_profiles) + end + + it "should use resource name for identifier when not specified" do + resource.profile_name "com.testprofile.screensaver" + resource.action(:remove) + provider.load_current_resource + expect(provider.instance_variable_get(:@new_profile_identifier)).to eql(resource.profile_name) + end + + it "should use specified identifier" do + resource.identifier "com.testprofile.screensaver" + resource.action(:remove) + provider.load_current_resource + expect(provider.instance_variable_get(:@new_profile_identifier)).to eql(resource.identifier) + end + + it "should work with spaces in the identifier" do + provider.action = :remove + provider.define_resource_requirements + expect { provider.process_resource_requirements }.not_to raise_error + end + + it "should build the shellout remove command correctly" do + resource.identifier "com.testprofile.screensaver" + resource.action(:remove) + provider.load_current_resource + expect(provider).to receive(:shell_out_compacted!).with("/usr/bin/profiles", "-R", "-p", resource.identifier).and_return(shell_out_success) + provider.action_remove + end + end end |