summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan McLellan <btm@loftninjas.org>2017-02-06 12:53:49 -0500
committerGitHub <noreply@github.com>2017-02-06 12:53:49 -0500
commit3cb83ee655e28d52a29ce7ef2698d6e097a11d43 (patch)
treed51a24c666f7d19a8486eab0aebf88f7162c3924
parent58f73322224ecbb363468b81e9169a344ee3f5cf (diff)
parentbeb4953af6cc5bec7104616af9f15b0dc3223dcb (diff)
downloadchef-3cb83ee655e28d52a29ce7ef2698d6e097a11d43.tar.gz
Merge pull request #5764 from MsysTechnologiesllc/nim/alternate_user_execute_resources
Windows alternate user support for execute resources
-rw-r--r--lib/chef/provider/execute.rb7
-rw-r--r--lib/chef/provider/powershell_script.rb8
-rw-r--r--lib/chef/provider/script.rb44
-rw-r--r--lib/chef/resource/execute.rb76
-rw-r--r--spec/functional/resource/batch_spec.rb6
-rw-r--r--spec/functional/resource/execute_spec.rb17
-rw-r--r--spec/support/shared/functional/execute_resource.rb150
-rw-r--r--spec/support/shared/functional/windows_script.rb78
-rw-r--r--spec/support/shared/unit/execute_resource.rb37
-rw-r--r--spec/unit/provider/execute_spec.rb1
-rw-r--r--spec/unit/provider/script_spec.rb55
-rw-r--r--spec/unit/resource/execute_spec.rb214
-rw-r--r--spec/unit/resource_reporter_spec.rb2
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