diff options
-rw-r--r-- | lib/chef/provider/execute.rb | 7 | ||||
-rw-r--r-- | lib/chef/provider/powershell_script.rb | 8 | ||||
-rw-r--r-- | lib/chef/provider/script.rb | 44 | ||||
-rw-r--r-- | lib/chef/resource/execute.rb | 76 | ||||
-rw-r--r-- | spec/functional/resource/batch_spec.rb | 6 | ||||
-rw-r--r-- | spec/functional/resource/execute_spec.rb | 17 | ||||
-rw-r--r-- | spec/support/shared/functional/execute_resource.rb | 150 | ||||
-rw-r--r-- | spec/support/shared/functional/windows_script.rb | 78 | ||||
-rw-r--r-- | spec/support/shared/unit/execute_resource.rb | 37 | ||||
-rw-r--r-- | spec/unit/provider/execute_spec.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/script_spec.rb | 55 | ||||
-rw-r--r-- | spec/unit/resource/execute_spec.rb | 214 | ||||
-rw-r--r-- | spec/unit/resource_reporter_spec.rb | 2 |
13 files changed, 670 insertions, 25 deletions
diff --git a/lib/chef/provider/execute.rb b/lib/chef/provider/execute.rb index 45f0ad5488..a605d9f7ec 100644 --- a/lib/chef/provider/execute.rb +++ b/lib/chef/provider/execute.rb @@ -27,7 +27,7 @@ class Chef provides :execute - def_delegators :@new_resource, :command, :returns, :environment, :user, :group, :cwd, :umask, :creates + def_delegators :@new_resource, :command, :returns, :environment, :user, :domain, :password, :group, :cwd, :umask, :creates def load_current_resource current_resource = Chef::Resource::Execute.new(new_resource.name) @@ -39,7 +39,7 @@ class Chef end def define_resource_requirements - # @todo: this should change to raise in some appropriate major version bump. + # @todo: this should change to raise in some appropriate major version bump. if creates && creates_relative? && !cwd Chef::Log.warn "Providing a relative path for the creates attribute without the cwd is deprecated and will be changed to fail in the future (CHEF-3819)" end @@ -92,6 +92,8 @@ class Chef opts[:returns] = returns if returns opts[:environment] = environment if environment opts[:user] = user if user + opts[:domain] = domain if domain + opts[:password] = password if password opts[:group] = group if group opts[:cwd] = cwd if cwd opts[:umask] = umask if umask @@ -120,6 +122,7 @@ class Chef ( cwd && creates_relative? ) ? ::File.join(cwd, creates) : creates )) end + end end end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index ab85ec35ac..0cace362df 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -149,6 +149,14 @@ EOH <<-EOH # Chef Client wrapper for powershell_script resources +# In rare cases, such as when PowerShell is executed +# as an alternate user, the new-variable cmdlet is not +# available, so import it just in case +if ( get-module -ListAvailable Microsoft.PowerShell.Utility ) +{ + Import-Module Microsoft.PowerShell.Utility +} + # LASTEXITCODE can be uninitialized -- make it explictly 0 # to avoid incorrect detection of failure (non-zero) codes $global:LASTEXITCODE = 0 diff --git a/lib/chef/provider/script.rb b/lib/chef/provider/script.rb index 6ca4e9f6f3..fca9cea467 100644 --- a/lib/chef/provider/script.rb +++ b/lib/chef/provider/script.rb @@ -18,6 +18,7 @@ require "tempfile" require "chef/provider/execute" +require "chef/win32/security" if Chef::Platform.windows? require "forwardable" class Chef @@ -66,10 +67,45 @@ class Chef end def set_owner_and_group - # FileUtils itself implements a no-op if +user+ or +group+ are nil - # You can prove this by running FileUtils.chown(nil,nil,'/tmp/file') - # as an unprivileged user. - FileUtils.chown(new_resource.user, new_resource.group, script_file.path) + if Chef::Platform.windows? + # And on Windows also this is a no-op if there is no user specified. + grant_alternate_user_read_access + else + # FileUtils itself implements a no-op if +user+ or +group+ are nil + # You can prove this by running FileUtils.chown(nil,nil,'/tmp/file') + # as an unprivileged user. + FileUtils.chown(new_resource.user, new_resource.group, script_file.path) + end + end + + def grant_alternate_user_read_access + # Do nothing if an alternate user isn't specified -- the file + # will already have the correct permissions for the user as part + # of the default ACL behavior on Windows. + return if new_resource.user.nil? + + # Duplicate the script file's existing DACL + # so we can add an ACE later + securable_object = Chef::ReservedNames::Win32::Security::SecurableObject.new(script_file.path) + aces = securable_object.security_descriptor.dacl.reduce([]) { |result, current| result.push(current) } + + username = new_resource.user + + if new_resource.domain + username = new_resource.domain + '\\' + new_resource.user + end + + # Create an ACE that allows the alternate user read access to the script + # file so it can be read and executed. + user_sid = Chef::ReservedNames::Win32::Security::SID.from_account(username) + read_ace = Chef::ReservedNames::Win32::Security::ACE.access_allowed(user_sid, Chef::ReservedNames::Win32::API::Security::GENERIC_READ | Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE, 0) + aces.push(read_ace) + acl = Chef::ReservedNames::Win32::Security::ACL.create(aces) + + # This actually applies the modified DACL to the file + # Use parentheses to bypass RuboCop / ChefStyle warning + # about useless setter + (securable_object.dacl = acl) end def script_file diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb index 1a56607267..677c4608b3 100644 --- a/lib/chef/resource/execute.rb +++ b/lib/chef/resource/execute.rb @@ -135,12 +135,18 @@ class Chef ) end - def user(arg = nil) - set_or_return( - :user, - arg, - :kind_of => [ String, Integer ] - ) + property :user, [ String, Integer ] + + property :domain, String + + property :password, String, sensitive: true + + def sensitive(args = nil) + if password + true + else + super + end end def self.set_guard_inherited_attributes(*inherited_attributes) @@ -159,6 +165,64 @@ class Chef ancestor_attributes.concat(@class_inherited_attributes ? @class_inherited_attributes : []).uniq end + def after_created + validate_identity_platform(user, password, domain) + identity = qualify_user(user, password, domain) + domain(identity[:domain]) + user(identity[:user]) + end + + def validate_identity_platform(specified_user, password = nil, specified_domain = nil) + if node[:platform_family] == "windows" + if specified_user && password.nil? + raise ArgumentError, "A value for `password` must be specified when a value for `user` is specified on the Windows platform" + end + else + if password || specified_domain + raise Exceptions::UnsupportedPlatform, "Values for `domain` and `password` are only supported on the Windows platform" + end + end + end + + def qualify_user(specified_user, password = nil, specified_domain = nil) + domain = specified_domain + user = specified_user + + if specified_user.nil? && ! specified_domain.nil? + raise ArgumentError, "The domain `#{specified_domain}` was specified, but no user name was given" + end + + # if domain is provided in both username and domain + if specified_user && ((specified_user.include? '\\') || (specified_user.include? "@")) && specified_domain + raise ArgumentError, "The domain is provided twice. Username: `#{specified_user}`, Domain: `#{specified_domain}`. Please specify domain only once." + end + + if ! specified_user.nil? && specified_domain.nil? + # Splitting username of format: Domain\Username + domain_and_user = user.split('\\') + + if domain_and_user.length == 2 + domain = domain_and_user[0] + user = domain_and_user[1] + elsif domain_and_user.length == 1 + # Splitting username of format: Username@Domain + domain_and_user = user.split("@") + if domain_and_user.length == 2 + domain = domain_and_user[1] + user = domain_and_user[0] + elsif domain_and_user.length != 1 + raise ArgumentError, "The specified user name `#{user}` is not a syntactically valid user name" + end + end + end + + if ( password || domain ) && user.nil? + raise ArgumentError, "A value for `password` or `domain` was specified without specification of a value for `user`" + end + + { domain: domain, user: user } + end + set_guard_inherited_attributes( :cwd, :environment, diff --git a/spec/functional/resource/batch_spec.rb b/spec/functional/resource/batch_spec.rb index e4fc6420c7..2b176ed06c 100644 --- a/spec/functional/resource/batch_spec.rb +++ b/spec/functional/resource/batch_spec.rb @@ -23,7 +23,11 @@ describe Chef::Resource::WindowsScript::Batch, :windows_only do let(:output_command) { " > " } - let (:architecture_command) { "@echo %PROCESSOR_ARCHITECTURE%" } + let(:architecture_command) { "@echo %PROCESSOR_ARCHITECTURE%" } + + let(:resource) do + Chef::Resource::WindowsScript::Batch.new("Batch resource functional test", @run_context) + end it_behaves_like "a Windows script running on Windows" diff --git a/spec/functional/resource/execute_spec.rb b/spec/functional/resource/execute_spec.rb index 3c31537ebe..09b7286854 100644 --- a/spec/functional/resource/execute_spec.rb +++ b/spec/functional/resource/execute_spec.rb @@ -137,6 +137,18 @@ describe Chef::Resource::Execute do end end + describe "when a guard is specified" do + describe "when using the default guard interpreter" do + let(:guard_interpreter_resource) { nil } + it_behaves_like "a resource with a guard specifying an alternate user identity" + end + + describe "when using the execute resource as the guard interpreter" do + let(:guard_interpreter_resource) { :execute } + it_behaves_like "a resource with a guard specifying an alternate user identity" + end + end + # Ensure that CommandTimeout is raised, and is caused by resource.timeout really expiring. # https://github.com/chef/chef/issues/2985 # @@ -151,4 +163,9 @@ describe Chef::Resource::Execute do expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::CommandTimeout) end end + + describe "when running with an alternate user identity" do + let(:resource_command_property) { :command } + it_behaves_like "an execute resource that supports alternate user identity" + end end diff --git a/spec/support/shared/functional/execute_resource.rb b/spec/support/shared/functional/execute_resource.rb new file mode 100644 index 0000000000..3f9dd8af5c --- /dev/null +++ b/spec/support/shared/functional/execute_resource.rb @@ -0,0 +1,150 @@ +# +# Author:: Adam Edwards (<adamed@chef.io>) +# Copyright:: Copyright (c) 2015 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. +# + +shared_context "a non-admin Windows user" do + include Chef::Mixin::ShellOut + + let(:windows_nonadmin_user_domain) { ENV["COMPUTERNAME"] } + let(:windows_nonadmin_user_qualified) { "#{windows_nonadmin_user_domain}\\#{windows_nonadmin_user}" } + let(:temp_profile_path) { "#{ENV['USERPROFILE']}\\..\\cheftesttempuser" } + before do + shell_out!("net.exe user /delete #{windows_nonadmin_user}", returns: [0, 2]) + + # Supply a profile path when creating a user to avoid an apparent Windows bug where deleting + # the user actually creates the profile when it did not immediately exist before executing + # net user /delete! For some reason, specifying an explicit path ensures that the path + # profile doesn't get created at deletion. + shell_out!("net.exe user /add #{windows_nonadmin_user} \"#{windows_nonadmin_user_password}\" /profilepath:#{temp_profile_path}") + end + + after do + shell_out!("net.exe user /delete #{windows_nonadmin_user}", returns: [0, 2]) + end +end + +shared_context "alternate user identity" do + let(:windows_alternate_user) { "chef%02d%02d%02d" % [Time.now.year % 100, Time.now.month, Time.now.day] } + let(:windows_alternate_user_password) { "lj28;fx3T!x,2" } + let(:windows_alternate_user_qualified) { "#{ENV['COMPUTERNAME']}\\#{windows_alternate_user}" } + + let(:windows_nonadmin_user) { windows_alternate_user } + let(:windows_nonadmin_user_password) { windows_alternate_user_password } + + include_context "a non-admin Windows user" +end + +shared_context "a command that can be executed as an alternate user" do + include_context "alternate user identity" + + let(:script_output_dir) { Dir.mktmpdir } + let(:script_output_path) { File.join(script_output_dir, make_tmpname("chef_execute_identity_test")) } + let(:script_output) { File.read(script_output_path) } + + include Chef::Mixin::ShellOut + + before do + shell_out!("icacls \"#{script_output_dir.gsub(/\//, '\\')}\" /grant \"authenticated users:(F)\"") + end + + after do + File.delete(script_output_path) if File.exists?(script_output_path) + Dir.rmdir(script_output_dir) if Dir.exists?(script_output_dir) + end +end + +shared_examples_for "an execute resource that supports alternate user identity" do + context "when running on Windows", :windows_only do + + include_context "a command that can be executed as an alternate user" + + let(:windows_current_user) { ENV["USERNAME"] } + let(:windows_current_user_qualified) { "#{ENV['USERDOMAIN'] || ENV['COMPUTERNAME']}\\#{windows_current_user}" } + let(:resource_identity_command) { "powershell.exe -noprofile -command \"import-module microsoft.powershell.utility;([Security.Principal.WindowsPrincipal]([Security.Principal.WindowsIdentity]::GetCurrent())).identity.name | out-file -encoding ASCII '#{script_output_path}'\"" } + + let(:execute_resource) do + resource.user(windows_alternate_user) + resource.password(windows_alternate_user_password) + resource.send(resource_command_property, resource_identity_command) + resource + end + + it "executes the process as an alternate user" do + expect(windows_current_user.length).to be > 0 + expect { execute_resource.run_action(:run) }.not_to raise_error + expect(script_output.chomp.length).to be > 0 + expect(script_output.chomp.downcase).to eq(windows_alternate_user_qualified.downcase) + expect(script_output.chomp.downcase).not_to eq(windows_current_user.downcase) + expect(script_output.chomp.downcase).not_to eq(windows_current_user_qualified.downcase) + end + + let(:windows_alternate_user_password_invalid) { "#{windows_alternate_user_password}x" } + + it "raises an exception if the user's password is invalid" do + execute_resource.password(windows_alternate_user_password_invalid) + expect { execute_resource.run_action(:run) }.to raise_error(SystemCallError) + end + end +end + +shared_examples_for "a resource with a guard specifying an alternate user identity" do + context "when running on Windows", :windows_only do + include_context "alternate user identity" + + let(:resource_command_property) { :command } + + let(:powershell_equal_to_alternate_user) { "-eq" } + let(:powershell_not_equal_to_alternate_user) { "-ne" } + let(:guard_identity_command) { "powershell.exe -noprofile -command \"import-module microsoft.powershell.utility;exit @(392,0)[[int32](([Security.Principal.WindowsPrincipal]([Security.Principal.WindowsIdentity]::GetCurrent())).Identity.Name #{comparison_to_alternate_user} '#{windows_alternate_user_qualified}')]\"" } + + before do + resource.guard_interpreter(guard_interpreter_resource) + end + + context "when the guard expression is true if the user is alternate and false otherwise" do + let(:comparison_to_alternate_user) { powershell_equal_to_alternate_user } + + it "causes the resource to be updated for only_if" do + resource.only_if(guard_identity_command, { user: windows_alternate_user, password: windows_alternate_user_password }) + resource.run_action(:run) + expect(resource).to be_updated_by_last_action + end + + it "causes the resource to not be updated for not_if" do + resource.not_if(guard_identity_command, { user: windows_alternate_user, password: windows_alternate_user_password }) + resource.run_action(:run) + expect(resource).not_to be_updated_by_last_action + end + end + + context "when the guard expression is false if the user is alternate and true otherwise" do + let(:comparison_to_alternate_user) { powershell_not_equal_to_alternate_user } + + it "causes the resource not to be updated for only_if" do + resource.only_if(guard_identity_command, { user: windows_alternate_user, password: windows_alternate_user_password }) + resource.run_action(:run) + expect(resource).not_to be_updated_by_last_action + end + + it "causes the resource to be updated for not_if" do + resource.not_if(guard_identity_command, { user: windows_alternate_user, password: windows_alternate_user_password }) + resource.run_action(:run) + expect(resource).to be_updated_by_last_action + end + end + end +end diff --git a/spec/support/shared/functional/windows_script.rb b/spec/support/shared/functional/windows_script.rb index 908198add4..8a9a19d4ad 100644 --- a/spec/support/shared/functional/windows_script.rb +++ b/spec/support/shared/functional/windows_script.rb @@ -46,10 +46,6 @@ shared_context Chef::Resource::WindowsScript do File.delete(script_output_path) if File.exists?(script_output_path) end - let!(:resource) do - Chef::Resource::WindowsScript::Batch.new("Batch resource functional test", @run_context) - end - shared_examples_for "a script resource with architecture attribute" do context "with the given architecture attribute value" do let(:expected_architecture) do @@ -125,6 +121,55 @@ shared_context Chef::Resource::WindowsScript do end shared_examples_for "a Windows script running on Windows" do + shared_examples_for "a script that cannot be accessed by other users if they are not administrators" do + include Chef::Mixin::ShellOut + + let(:script_provider) { resource.provider_for_action(:run) } + let(:script_file) { script_provider.script_file } + let(:script_file_path) { script_file.to_path } + + let(:read_access_denied_command) { "::File.read('#{script_file_path}')" } + let(:modify_access_denied_command) { "::File.write('#{script_file_path}', 'stuff')" } + let(:delete_access_denied_command) { "::File.delete('#{script_file_path}')" } + let(:access_denied_sentinel) { 7334 } + let(:access_allowed_sentinel) { 1586 } + let(:access_command_invalid) { 0 } + + let(:ruby_interpreter_path) { RbConfig.ruby } + let(:ruby_command_template) { "require 'FileUtils';status = 0;begin; #{ruby_access_command};rescue Exception => e; puts e; status = e.class == Errno::EACCES ? #{access_denied_sentinel} : #{access_allowed_sentinel};end;exit status" } + let(:command_template) { "set BUNDLE_GEMFILE=&#{ruby_interpreter_path} -e \"#{ruby_command_template}\"" } + let(:access_command) { command_template } + + before do + expect(script_provider).to receive(:unlink_script_file) + resource.code("echo hi") + script_provider.action_run + end + + after do + script_file.close! if script_file + ::File.delete(script_file.to_path) if script_file && ::File.exists?(script_file.to_path) + end + + include_context "alternate user identity" + + shared_examples_for "a script whose file system location cannot be accessed by other non-admin users" do + let(:ruby_access_command) { file_access_command } + it "generates a script in the local file system that prevents read access to other non-admin users" do + shell_out!(access_command, { user: windows_nonadmin_user, password: windows_nonadmin_user_password, returns: [access_denied_sentinel] }) + end + end + + context "when a different non-admin user attempts write (modify) to access the script" do + let(:file_access_command) { modify_access_denied_command } + it_behaves_like "a script whose file system location cannot be accessed by other non-admin users" + end + + context "when a different non-admin user attempts write (delete) to access the script" do + let(:file_access_command) { delete_access_denied_command } + it_behaves_like "a script whose file system location cannot be accessed by other non-admin users" + end + end describe "when the run action is invoked on Windows" do it "executes the script code" do @@ -132,6 +177,21 @@ shared_context Chef::Resource::WindowsScript do resource.returns(0) resource.run_action(:run) end + + context "the script is executed with the identity of the current user" do + it_behaves_like "a script that cannot be accessed by other users if they are not administrators" + end + + context "the script is executed with an alternate non-admin identity" do + include_context "alternate user identity" + + before do + resource.user(windows_alternate_user) + resource.password(windows_alternate_user_password) + end + + it_behaves_like "a script that cannot be accessed by other users if they are not administrators" + end end context "when $env:TMP has a space" do @@ -165,6 +225,11 @@ shared_context Chef::Resource::WindowsScript do expect(resource.class).to receive(:new).and_call_original expect(resource.should_skip?(:run)).to be_falsey end + + context "when this resource is used as a guard and it is specified with an alternate user identity" do + let(:guard_interpreter_resource) { resource.resource_name } + it_behaves_like "a resource with a guard specifying an alternate user identity" + end end context "when the architecture attribute is not set" do @@ -181,6 +246,11 @@ shared_context Chef::Resource::WindowsScript do let(:resource_architecture) { :x86_64 } it_behaves_like "a script resource with architecture attribute" end + + describe "when running with an alternate user identity" do + let(:resource_command_property) { :code } + it_behaves_like "an execute resource that supports alternate user identity" + end end def get_windows_script_output(suffix = "") diff --git a/spec/support/shared/unit/execute_resource.rb b/spec/support/shared/unit/execute_resource.rb index e33247da42..ab6ed2b86b 100644 --- a/spec/support/shared/unit/execute_resource.rb +++ b/spec/support/shared/unit/execute_resource.rb @@ -106,6 +106,16 @@ shared_examples_for "an execute resource" do expect(@resource.user).to eql(1) end + it "should accept a string for the domain" do + @resource.domain "mothership" + expect(@resource.domain).to eql("mothership") + end + + it "should accept a string for the password" do + @resource.password "we.funk!" + expect(@resource.password).to eql("we.funk!") + end + it "should accept a string for creates" do @resource.creates "something" expect(@resource.creates).to eql("something") @@ -116,6 +126,33 @@ shared_examples_for "an execute resource" do expect(@resource.live_stream).to be true end + describe "the resource's sensitive attribute" do + it "should be false by default" do + expect(@resource.sensitive).to eq(false) + end + + it "should be true if set to true" do + expect(@resource.sensitive).to eq(false) + @resource.sensitive true + expect(@resource.sensitive).to eq(true) + end + + it "should be true if the password is non-nil" do + expect(@resource.sensitive).to eq(false) + @resource.password("we.funk!") + expect(@resource.sensitive).to eq(true) + end + + it "should be true if the password is non-nil but the value is explicitly set to false" do + expect(@resource.sensitive).to eq(false) + @resource.password("we.funk!") + expect(@resource.sensitive).to eq(true) + @resource.sensitive false + expect(@resource.sensitive).to eq(true) + end + + end + describe "when it has cwd, environment, group, path, return value, and a user" do before do @resource.command("grep") diff --git a/spec/unit/provider/execute_spec.rb b/spec/unit/provider/execute_spec.rb index 4b0afcb928..1901e2ea03 100644 --- a/spec/unit/provider/execute_spec.rb +++ b/spec/unit/provider/execute_spec.rb @@ -238,6 +238,5 @@ describe Chef::Provider::Execute do end end - end end diff --git a/spec/unit/provider/script_spec.rb b/spec/unit/provider/script_spec.rb index 7e34a8f083..2f024c4c29 100644 --- a/spec/unit/provider/script_spec.rb +++ b/spec/unit/provider/script_spec.rb @@ -56,12 +56,55 @@ describe Chef::Provider::Script, "action_run" do end end - context "#set_owner_and_group" do - it "sets the owner and group for the script file" do - new_resource.user "toor" - new_resource.group "wheel" - expect(FileUtils).to receive(:chown).with("toor", "wheel", tempfile.path) - provider.set_owner_and_group + context "when configuring the script file's security" do + context "when not running on Windows" do + before do + allow(::Chef::Platform).to receive(:windows?).and_return(false) + end + context "#set_owner_and_group" do + it "sets the owner and group for the script file" do + new_resource.user "toor" + new_resource.group "wheel" + expect(FileUtils).to receive(:chown).with("toor", "wheel", tempfile.path) + provider.set_owner_and_group + end + end + end + + context "when running on Windows" do + before do + allow(::Chef::Platform).to receive(:windows?).and_return(true) + expect(new_resource.user).to eq(nil) + stub_const("Chef::ReservedNames::Win32::API::Security::GENERIC_READ", 1) + stub_const("Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE", 4) + stub_const("Chef::ReservedNames::Win32::Security", Class.new) + stub_const("Chef::ReservedNames::Win32::Security::SecurableObject", Class.new) + stub_const("Chef::ReservedNames::Win32::Security::SID", Class.new) + stub_const("Chef::ReservedNames::Win32::Security::ACE", Class.new) + stub_const("Chef::ReservedNames::Win32::Security::ACL", Class.new) + end + + context "when an alternate user is not specified" do + it "does not attempt to set the script file's security descriptor" do + expect(provider).to receive(:grant_alternate_user_read_access) + expect(Chef::ReservedNames::Win32::Security::SecurableObject).not_to receive(:new) + provider.set_owner_and_group + end + end + + context "when an alternate user is specified" do + let(:security_descriptor) { instance_double("Chef::ReservedNames::Win32::Security::SecurityDescriptor", :dacl => []) } + let(:securable_object) { instance_double("Chef::ReservedNames::Win32::Security::SecurableObject", :security_descriptor => security_descriptor, :dacl= => nil) } + it "sets the script file's security descriptor" do + new_resource.user("toor") + expect(Chef::ReservedNames::Win32::Security::SecurableObject).to receive(:new).and_return(securable_object) + expect(Chef::ReservedNames::Win32::Security::SID).to receive(:from_account).and_return(nil) + expect(Chef::ReservedNames::Win32::Security::ACE).to receive(:access_allowed).and_return(nil) + expect(Chef::ReservedNames::Win32::Security::ACL).to receive(:create).and_return(nil) + expect(securable_object).to receive(:dacl=) + provider.set_owner_and_group + end + end end end diff --git a/spec/unit/resource/execute_spec.rb b/spec/unit/resource/execute_spec.rb index 70824e9f5b..4c0ee694c3 100644 --- a/spec/unit/resource/execute_spec.rb +++ b/spec/unit/resource/execute_spec.rb @@ -32,4 +32,218 @@ describe Chef::Resource::Execute do expect(execute_resource.is_guard_interpreter).to eq(false) end + describe "#qualify_user" do + let(:password) { "password" } + let(:domain) { nil } + + context "when username is passed as user@domain" do + let(:username) { "user@domain" } + + it "correctly parses the user and domain" do + identity = execute_resource.qualify_user(username, password, domain) + expect(identity[:domain]).to eq("domain") + expect(identity[:user]).to eq("user") + end + end + + context "when username is passed as domain\\user" do + let(:username) { "domain\\user" } + + it "correctly parses the user and domain" do + identity = execute_resource.qualify_user(username, password, domain) + expect(identity[:domain]).to eq("domain") + expect(identity[:user]).to eq("user") + end + end + end + + shared_examples_for "it received valid credentials" do + describe "the validation method" do + it "should not raise an error" do + expect { execute_resource.validate_identity_platform(username, password, domain) }.not_to raise_error + end + end + + describe "the name qualification method" do + it "should correctly translate the user and domain" do + identity = nil + expect { identity = execute_resource.qualify_user(username, password, domain) }.not_to raise_error + expect(identity[:domain]).to eq(domain) + expect(identity[:user]).to eq(username) + end + end + end + + shared_examples_for "it received invalid credentials" do + describe "the validation method" do + it "should raise an error" do + expect { execute_resource.validate_identity_platform(username, password, domain) }.to raise_error(ArgumentError) + end + end + end + + shared_examples_for "it received invalid username and domain" do + describe "the validation method" do + it "should raise an error" do + expect { execute_resource.qualify_user(username, password, domain) }.to raise_error(ArgumentError) + end + end + end + + shared_examples_for "it received credentials that are not valid on the platform" do + describe "the validation method" do + it "should raise an error" do + expect { execute_resource.validate_identity_platform(username, password, domain) }.to raise_error(Chef::Exceptions::UnsupportedPlatform) + end + end + end + + shared_examples_for "a consumer of the Execute resource" do + context "when running on Windows" do + before do + allow(execute_resource).to receive(:node).and_return({ :platform_family => "windows" }) + end + + context "when no user, domain, or password is specified" do + let(:username) { nil } + let(:domain) { nil } + let(:password) { nil } + it_behaves_like "it received valid credentials" + end + + context "when a valid username is specified" do + let(:username) { "starchild" } + context "when a valid domain is specified" do + let(:domain) { "mothership" } + + context "when the password is not specified" do + let(:password) { nil } + it_behaves_like "it received invalid credentials" + end + + context "when the password is specified" do + let(:password) { "we.funk!" } + it_behaves_like "it received valid credentials" + end + end + + context "when the domain is not specified" do + let(:domain) { nil } + + context "when the password is not specified" do + let(:password) { nil } + it_behaves_like "it received invalid credentials" + end + + context "when the password is specified" do + let(:password) { "we.funk!" } + it_behaves_like "it received valid credentials" + end + end + + context "when username is not specified" do + let(:username) { nil } + + context "when domain is specified" do + let(:domain) { "mothership" } + let(:password) { nil } + it_behaves_like "it received invalid username and domain" + end + + context "when password is specified" do + let(:domain) { nil } + let(:password) { "we.funk!" } + it_behaves_like "it received invalid username and domain" + end + end + end + + context "when invalid username is specified" do + let(:username) { "user@domain@domain" } + let(:domain) { nil } + let(:password) { "we.funk!" } + it_behaves_like "it received invalid username and domain" + end + + context "when the domain is provided in both username and domain" do + let(:domain) { "some_domain" } + let(:password) { "we.funk!" } + + context "when username is in the form domain\\user" do + let(:username) { "mothership\\starchild" } + it_behaves_like "it received invalid username and domain" + end + + context "when username is in the form user@domain" do + let(:username) { "starchild@mothership" } + it_behaves_like "it received invalid username and domain" + end + end + end + + context "when not running on Windows" do + before do + allow(execute_resource).to receive(:node).and_return({ :platform_family => "ubuntu" }) + end + + context "when no user, domain, or password is specified" do + let(:username) { nil } + let(:domain) { nil } + let(:password) { nil } + it_behaves_like "it received valid credentials" + end + + context "when the user is specified and the domain and password are not" do + let(:username) { "starchild" } + let(:domain) { nil } + let(:password) { nil } + it_behaves_like "it received valid credentials" + + context "when the password is specified and the domain is not" do + let(:password) { "we.funk!" } + let(:domain) { nil } + it_behaves_like "it received credentials that are not valid on the platform" + end + + context "when the domain is specified and the password is not" do + let(:domain) { "mothership" } + let(:password) { nil } + it_behaves_like "it received credentials that are not valid on the platform" + end + + context "when the domain and password are specified" do + let(:domain) { "mothership" } + let(:password) { "we.funk!" } + it_behaves_like "it received credentials that are not valid on the platform" + end + end + + context "when the user is not specified" do + let(:username) { nil } + context "when the domain is specified" do + let(:domain) { "mothership" } + context "when the password is specified" do + let(:password) { "we.funk!" } + it_behaves_like "it received credentials that are not valid on the platform" + end + + context "when password is not specified" do + let(:password) { nil } + it_behaves_like "it received credentials that are not valid on the platform" + end + end + + context "when the domain is not specified" do + let(:domain) { nil } + context "when the password is specified" do + let(:password) { "we.funk!" } + it_behaves_like "it received credentials that are not valid on the platform" + end + end + end + end + end + + it_behaves_like "a consumer of the Execute resource" + end diff --git a/spec/unit/resource_reporter_spec.rb b/spec/unit/resource_reporter_spec.rb index ccd7087c0b..84cfb52418 100644 --- a/spec/unit/resource_reporter_spec.rb +++ b/spec/unit/resource_reporter_spec.rb @@ -285,7 +285,7 @@ describe Chef::ResourceReporter do end it "resource_command in prepared_run_data should be blank" do - expect(@first_update_report["after"]).to eq({ :command => "sensitive-resource" }) + expect(@first_update_report["after"]).to eq({ :command => "sensitive-resource", :user => nil }) end end |