diff options
author | adamedx <adamedx@gmail.com> | 2016-04-02 23:23:25 -0700 |
---|---|---|
committer | adamedx <adamedx@gmail.com> | 2016-04-03 18:22:46 -0700 |
commit | eaf69a4ac76515837e3fcefb5dfb1c6567ca6caf (patch) | |
tree | b28c25d6b32a9cbf8af2b327317e63d3e8753c1d | |
parent | ed0ab0180c7d0083597c15f26cdcdbddcaf9e93f (diff) | |
download | chef-adamedx/alternate-user-remote-file-only-rebase.tar.gz |
Access remote_file resource source files as alternate user on Windowsadamedx/alternate-user-remote-file-only-rebase
-rw-r--r-- | lib/chef/mixin/user_context.rb | 2 | ||||
-rw-r--r-- | lib/chef/mixin/user_identity.rb | 2 | ||||
-rw-r--r-- | lib/chef/provider/remote_file/network_file.rb | 27 | ||||
-rw-r--r-- | lib/chef/resource/remote_file.rb | 32 | ||||
-rw-r--r-- | spec/functional/resource/remote_file_spec.rb | 158 | ||||
-rw-r--r-- | spec/support/shared/functional/execute_resource.rb | 150 | ||||
-rw-r--r-- | spec/unit/mixin/user_identity_spec.rb | 42 | ||||
-rw-r--r-- | spec/unit/provider/remote_file/network_file_spec.rb | 7 |
8 files changed, 391 insertions, 29 deletions
diff --git a/lib/chef/mixin/user_context.rb b/lib/chef/mixin/user_context.rb index 5305678f2b..b29afa5087 100644 --- a/lib/chef/mixin/user_context.rb +++ b/lib/chef/mixin/user_context.rb @@ -49,7 +49,7 @@ class Chef logon_session.open logon_session.set_user_context end - block.call + yield ensure logon_session.close if logon_session end diff --git a/lib/chef/mixin/user_identity.rb b/lib/chef/mixin/user_identity.rb index 6218b44ac2..c037adb441 100644 --- a/lib/chef/mixin/user_identity.rb +++ b/lib/chef/mixin/user_identity.rb @@ -57,7 +57,7 @@ class Chef domain_and_user = user.split('\\') if domain_and_user.length == 1 - domain_and_user = user.split('@') + domain_and_user = user.split("@") end if domain_and_user.length == 2 diff --git a/lib/chef/provider/remote_file/network_file.rb b/lib/chef/provider/remote_file/network_file.rb index 44046132a9..2bfcf38dbd 100644 --- a/lib/chef/provider/remote_file/network_file.rb +++ b/lib/chef/provider/remote_file/network_file.rb @@ -19,14 +19,21 @@ require "uri" require "tempfile" require "chef/provider/remote_file" +require "chef/mixin/user_identity" +require "chef/mixin/user_context" class Chef class Provider class RemoteFile class NetworkFile + include Chef::Mixin::UserIdentity + include Chef::Mixin::UserContext + attr_reader :new_resource + TRANSFER_CHUNK_SIZE = 1048576 + def initialize(source, new_resource, current_resource) @new_resource = new_resource @source = source @@ -35,13 +42,23 @@ class Chef # Fetches the file on a network share, returning a Tempfile-like File handle # windows only def fetch - tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile - Chef::Log.debug("#{new_resource} staging #{@source} to #{tempfile.path}") - FileUtils.cp(@source, tempfile.path) - tempfile.close if tempfile + validate_identity(new_resource.remote_user, new_resource.remote_user_password, new_resource.remote_user_domain) + begin + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile + Chef::Log.debug("#{new_resource} staging #{@source} to #{tempfile.path}") + + with_user_context(new_resource.remote_user, new_resource.remote_user_password, new_resource.remote_user_domain) do + ::File.open(@source, "rb") do |remote_file| + while data = remote_file.read(TRANSFER_CHUNK_SIZE) + tempfile.write(data) + end + end + end + ensure + tempfile.close if tempfile + end tempfile end - end end end diff --git a/lib/chef/resource/remote_file.rb b/lib/chef/resource/remote_file.rb index 4a1d1c6cff..f631b4e579 100644 --- a/lib/chef/resource/remote_file.rb +++ b/lib/chef/resource/remote_file.rb @@ -131,6 +131,38 @@ class Chef ) end + def remote_user(args = nil) + set_or_return( + :remote_user, + args, + :kind_of => String + ) + end + + def remote_user_domain(args = nil) + set_or_return( + :remote_user_domain, + args, + :kind_of => String + ) + end + + def remote_user_password(args = nil) + set_or_return( + :remote_user_password, + args, + :kind_of => String + ) + end + + def sensitive(args = nil) + if ! remote_user_password.nil? + true + else + super + end + end + private include Chef::Mixin::Uris diff --git a/spec/functional/resource/remote_file_spec.rb b/spec/functional/resource/remote_file_spec.rb index b394bd0240..1c7cfbb321 100644 --- a/spec/functional/resource/remote_file_spec.rb +++ b/spec/functional/resource/remote_file_spec.rb @@ -123,6 +123,164 @@ describe Chef::Resource::RemoteFile do end + context "when running on Windows", :windows_only do + describe "when fetching files over SMB" do + include Chef::Mixin::ShellOut + let(:smb_share_root_directory) { directory = File.join(Dir.tmpdir, make_tmpname("windows_script_test")); Dir.mkdir(directory); directory } + let(:smb_file_local_file_name) { "smb_file.txt" } + let(:smb_file_local_path) { File.join( smb_share_root_directory, smb_file_local_file_name ) } + let(:smb_share_name) { "chef_smb_test" } + let(:smb_remote_path) { File.join("//#{ENV['COMPUTERNAME']}", smb_share_name, smb_file_local_file_name).gsub(/\//, "\\") } + let(:smb_file_content) { "hellofun" } + let(:local_destination_path) { File.join(Dir.tmpdir, make_tmpname("chef_remote_file")) } + let(:windows_current_user) { ENV["USERNAME"] } + let(:windows_current_user_domain) { ENV["USERDOMAIN"] || ENV["COMPUTERNAME"] } + let(:windows_current_user_qualified) { "#{windows_current_user_domain}\\#{windows_current_user}" } + + let(:remote_domain) { nil } + let(:remote_user) { nil } + let(:remote_password) { nil } + + let(:resource) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + resource = Chef::Resource::RemoteFile.new(path, run_context) + end + + before do + shell_out("net.exe share #{smb_share_name} /delete") + File.write(smb_file_local_path, smb_file_content ) + shell_out!("net.exe share #{smb_share_name}=\"#{smb_share_root_directory.gsub(/\//, '\\')}\" /grant:\"authenticated users\",read") + end + + after do + shell_out("net.exe share #{smb_share_name} /delete") + File.delete(smb_file_local_path) if File.exist?(smb_file_local_path) + File.delete(local_destination_path) if File.exist?(local_destination_path) + Dir.rmdir(smb_share_root_directory) + end + + context "when configuring the Windows identity used to access the remote file" do + before do + resource.path(local_destination_path) + resource.source(smb_remote_path) + resource.remote_user_domain(remote_domain) + resource.remote_user(remote_user) + resource.remote_user_password(remote_password) + end + + shared_examples_for "a remote_file resource accessing a remote file to which the specified user has access" do + it "has the same content as the original file" do + expect { resource.run_action(:create) }.not_to raise_error + expect(::File.read(local_destination_path).chomp).to eq smb_file_content + end + end + + shared_examples_for "a remote_file resource accessing a remote file to which the specified user does not have access" do + it "causes an error to be raised" do + expect { resource.run_action(:create) }.to raise_error(Errno::EACCES) + end + end + + context "when the the file is accessible to non-admin users only as the current identity" do + before do + shell_out!("icacls #{smb_file_local_path} /grant:r \"authenticated users:(W)\" /grant \"#{windows_current_user_qualified}:(R)\" /inheritance:r") + end + + context "when the resource is accessed using the current user's identity" do + let(:remote_user) { nil } + let(:remote_domain) { nil } + let(:remote_password) { nil } + + it_behaves_like "a remote_file resource accessing a remote file to which the specified user has access" + + describe "uses the ::Chef::Provider::RemoteFile::NetworkFile::TRANSFER_CHUNK_SIZE constant to chunk the file" do + let(:invalid_chunk_size) { -1 } + before do + stub_const("::Chef::Provider::RemoteFile::NetworkFile::TRANSFER_CHUNK_SIZE", invalid_chunk_size) + end + + it "raises an ArgumentError when the chunk size is negative" do + expect(::Chef::Provider::RemoteFile::NetworkFile::TRANSFER_CHUNK_SIZE).to eq(invalid_chunk_size) + expect { resource.run_action(:create) }.to raise_error(ArgumentError) + end + end + + context "when the file must be transferred in more than one chunk" do + before do + stub_const("::Chef::Provider::RemoteFile::NetworkFile::TRANSFER_CHUNK_SIZE", 3) + end + it_behaves_like "a remote_file resource accessing a remote file to which the specified user has access" + end + end + + context "when the resource is accessed using an alternate user's identity with no access to the file" do + let (:windows_nonadmin_user) { "chefremfile1" } + let (:windows_nonadmin_user_password) { "j82ajfxK3;2Xe1" } + include_context "a non-admin Windows user" + + let(:remote_user) { windows_nonadmin_user } + let(:remote_domain) { nil } + let(:remote_password) { windows_nonadmin_user_password } + + it_behaves_like "a remote_file resource accessing a remote file to which the specified user does not have access" + end + end + + context "when the the file is only accessible as a specific alternate identity" do + let (:windows_nonadmin_user) { "chefremfile2" } + let (:windows_nonadmin_user_password) { "j82ajfxK3;2Xe2" } + include_context "a non-admin Windows user" + + before do + shell_out!("icacls #{smb_file_local_path} /grant:r \"authenticated users:(W)\" /grant \"#{windows_nonadmin_user_qualified}:(R)\" /deny #{windows_current_user_qualified}:(R) /inheritance:r") + end + + context "when the resource is accessed using the specific non-qualified alternate user identity with access" do + let(:remote_user) { windows_nonadmin_user } + let(:remote_domain) { nil } + let(:remote_password) { windows_nonadmin_user_password } + + it_behaves_like "a remote_file resource accessing a remote file to which the specified user has access" + end + + context "when the resource is accessed using the specific alternate user identity with access and the domain is specified" do + let(:remote_user) { windows_nonadmin_user } + let(:remote_domain) { windows_nonadmin_user_domain } + let(:remote_password) { windows_nonadmin_user_password } + + it_behaves_like "a remote_file resource accessing a remote file to which the specified user has access" + end + + context "when the resource is accessed using the specific qualified alternate user identity with access" do + let(:remote_user) { windows_nonadmin_user_qualified } + let(:remote_domain) { nil } + let(:remote_password) { windows_nonadmin_user_password } + + it_behaves_like "a remote_file resource accessing a remote file to which the specified user has access" + end + + context "when the resource is accessed using the current user's identity" do + it_behaves_like "a remote_file resource accessing a remote file to which the specified user does not have access" + end + + context "when the resource is accessed using an alternate user's identity with no access to the file" do + let (:windows_nonadmin_user) { "chefremfile3" } + let (:windows_nonadmin_user_password) { "j82ajfxK3;2Xe3" } + include_context "a non-admin Windows user" + + let(:remote_user) { windows_nonadmin_user_qualified } + let(:remote_domain) { nil } + let(:remote_password) { windows_nonadmin_user_password } + + it_behaves_like "a remote_file resource accessing a remote file to which the specified user does not have access" + end + end + end + end + end + context "when dealing with content length checking" do before(:all) do start_tiny_server diff --git a/spec/support/shared/functional/execute_resource.rb b/spec/support/shared/functional/execute_resource.rb new file mode 100644 index 0000000000..6561f95ec1 --- /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) { + resource.user(windows_alternate_user) + resource.password(windows_alternate_user_password) + resource.send(resource_command_property, resource_identity_command) + resource + } + + 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/unit/mixin/user_identity_spec.rb b/spec/unit/mixin/user_identity_spec.rb index f75092f660..799260b25a 100644 --- a/spec/unit/mixin/user_identity_spec.rb +++ b/spec/unit/mixin/user_identity_spec.rb @@ -16,13 +16,13 @@ # limitations under the License. # -require 'spec_helper' -require 'chef/mixin/user_identity' +require "spec_helper" +require "chef/mixin/user_identity" shared_examples_for "it received valid credentials" do describe "the validation method" do it "should not raise an error" do - expect {instance_with_identity.validate(username, password, domain)}.not_to raise_error + expect { instance_with_identity.validate(username, password, domain) }.not_to raise_error end end @@ -39,7 +39,7 @@ end shared_examples_for "it received invalid credentials" do describe "the validation method" do it "should raise an error" do - expect { instance_with_identity.validate(username, password, domain)}.to raise_error(ArgumentError) + expect { instance_with_identity.validate(username, password, domain) }.to raise_error(ArgumentError) end end end @@ -47,7 +47,7 @@ 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 { instance_with_identity.validate(username, password, domain)}.to raise_error(Chef::Exceptions::UnsupportedPlatform) + expect { instance_with_identity.validate(username, password, domain) }.to raise_error(Chef::Exceptions::UnsupportedPlatform) end end end @@ -66,9 +66,9 @@ shared_examples_for "a consumer of the ::Chef::Mixin::UserIdentity mixin" do end context "when a valid username is specified" do - let(:username) { 'starchild' } + let(:username) { "starchild" } context "when a valid domain is specified" do - let(:domain) { 'mothership' } + let(:domain) { "mothership" } context "when the password is not specified" do let(:password) { nil } @@ -76,7 +76,7 @@ shared_examples_for "a consumer of the ::Chef::Mixin::UserIdentity mixin" do end context "when the password is specified" do - let(:password) { 'we.funk!' } + let(:password) { "we.funk!" } it_behaves_like "it received valid credentials" end end @@ -90,7 +90,7 @@ shared_examples_for "a consumer of the ::Chef::Mixin::UserIdentity mixin" do end context "when the password is specified" do - let(:password) { 'we.funk!' } + let(:password) { "we.funk!" } it_behaves_like "it received valid credentials" end end @@ -100,20 +100,20 @@ shared_examples_for "a consumer of the ::Chef::Mixin::UserIdentity mixin" do let(:username) { nil } context "when the password is specified and the domain is not" do - let(:password) { 'we.funk!' } + let(:password) { "we.funk!" } let(:domain) { nil } it_behaves_like "it received invalid credentials" end context "when the domain is specified and the password is not" do - let(:domain) { 'mothership' } + let(:domain) { "mothership" } let(:password) { nil } it_behaves_like "it received invalid credentials" end context "when the domain and password are specified" do - let(:domain) { 'mothership' } - let(:password) { 'we.funk!' } + let(:domain) { "mothership" } + let(:password) { "we.funk!" } it_behaves_like "it received invalid credentials" end end @@ -132,26 +132,26 @@ shared_examples_for "a consumer of the ::Chef::Mixin::UserIdentity mixin" do end context "when the user is specified and the domain and password are not" do - let(:username) { 'starchild' } + 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(: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(: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!' } + let(:domain) { "mothership" } + let(:password) { "we.funk!" } it_behaves_like "it received credentials that are not valid on the platform" end end @@ -159,9 +159,9 @@ shared_examples_for "a consumer of the ::Chef::Mixin::UserIdentity mixin" do context "when the user is not specified" do let(:username) { nil } context "when the domain is specified" do - let(:domain) { 'mothership' } + let(:domain) { "mothership" } context "when the password is specified" do - let(:password) { 'we.funk!' } + let(:password) { "we.funk!" } it_behaves_like "it received credentials that are not valid on the platform" end @@ -174,7 +174,7 @@ shared_examples_for "a consumer of the ::Chef::Mixin::UserIdentity mixin" do context "when the domain is not specified" do let(:domain) { nil } context "when the password is specified" do - let(:password) { 'we.funk!' } + let(:password) { "we.funk!" } it_behaves_like "it received credentials that are not valid on the platform" end end diff --git a/spec/unit/provider/remote_file/network_file_spec.rb b/spec/unit/provider/remote_file/network_file_spec.rb index de065c83e2..52743c0830 100644 --- a/spec/unit/provider/remote_file/network_file_spec.rb +++ b/spec/unit/provider/remote_file/network_file_spec.rb @@ -1,3 +1,4 @@ + # # Author:: Jay Mundrawala (<jdm@chef.io>) # Copyright:: Copyright 2015-2016, Chef Software @@ -19,6 +20,9 @@ require "spec_helper" describe Chef::Provider::RemoteFile::NetworkFile do + before do + allow(::Chef::Platform).to receive(:windows?).and_return(true) + end let(:source) { "\\\\foohost\\fooshare\\Foo.tar.gz" } @@ -30,10 +34,11 @@ describe Chef::Provider::RemoteFile::NetworkFile do let(:tempfile) { double("Tempfile", :path => "/tmp/foo/bar/Foo.tar.gz", :close => nil) } let(:chef_tempfile) { double("Chef::FileContentManagement::Tempfile", :tempfile => tempfile) } + let(:source_file) { double("::File", :read => nil) } it "stages the local file to a temporary file" do expect(Chef::FileContentManagement::Tempfile).to receive(:new).with(new_resource).and_return(chef_tempfile) - expect(::FileUtils).to receive(:cp).with(source, tempfile.path) + expect(::File).to receive(:open).with(source, "rb").and_return(source_file) expect(tempfile).to receive(:close) result = fetcher.fetch |