diff options
author | Adam Edwards <adamed@opscode.com> | 2015-12-12 23:09:21 -0800 |
---|---|---|
committer | Adam Edwards <adamed@opscode.com> | 2015-12-12 23:09:21 -0800 |
commit | 79c8c059600a59831a7e32521fd25127d475b766 (patch) | |
tree | 7577e32f9d6c3d74711e50ca0512921b5656a372 | |
parent | 1476e48e3c6403f79810cb060c96999018b726ec (diff) | |
download | chef-79c8c059600a59831a7e32521fd25127d475b766.tar.gz |
Access remote_file resource files on Windows as an alternate user identityadamedx/alternate-user-remote-file
-rw-r--r-- | lib/chef/mixin/user_context.rb | 62 | ||||
-rw-r--r-- | lib/chef/provider/remote_file/network_file.rb | 26 | ||||
-rw-r--r-- | lib/chef/resource/remote_file.rb | 32 | ||||
-rw-r--r-- | lib/chef/util/windows/logon_session.rb | 118 | ||||
-rw-r--r-- | lib/chef/win32/api/security.rb | 2 | ||||
-rw-r--r-- | spec/functional/mixin/user_context_spec.rb | 118 | ||||
-rw-r--r-- | spec/functional/resource/remote_file_spec.rb | 158 | ||||
-rw-r--r-- | spec/unit/mixin/user_context_spec.rb | 124 | ||||
-rw-r--r-- | spec/unit/provider/remote_file/network_file_spec.rb | 7 | ||||
-rw-r--r-- | spec/unit/util/windows/logon_session_spec.rb | 284 |
10 files changed, 926 insertions, 5 deletions
diff --git a/lib/chef/mixin/user_context.rb b/lib/chef/mixin/user_context.rb new file mode 100644 index 0000000000..d2458c3bed --- /dev/null +++ b/lib/chef/mixin/user_context.rb @@ -0,0 +1,62 @@ +# +# 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. +# + +require 'chef/util/windows/logon_session' if Chef::Platform.windows? +require 'chef/mixin/user_identity' + +class Chef + module Mixin + module UserContext + + include Chef::Mixin::UserIdentity + + def with_user_context(specified_user, password, specified_domain = nil, &block) + if ! Chef::Platform.windows? + raise Exceptions::UnsupportedPlatform, 'User context impersonation is supported only on the Windows platform' + end + + if ! block_given? + raise Exceptions::ArgumentError, 'You must supply a block to `with_user_context`' + end + + validate_identity(specified_user, password, specified_domain) + + identity = qualify_user(specified_user, specified_domain) + + user = identity[:user] + domain = identity[:domain] + + login_session = nil + + begin + if user + logon_session = Chef::Util::Windows::LogonSession.new(user, password, domain) + logon_session.open + logon_session.set_user_context + end + block.call + ensure + logon_session.close if logon_session + end + end + + protected(:with_user_context) + + end + end +end diff --git a/lib/chef/provider/remote_file/network_file.rb b/lib/chef/provider/remote_file/network_file.rb index 093a388d2a..53dc300161 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,10 +42,21 @@ 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 diff --git a/lib/chef/resource/remote_file.rb b/lib/chef/resource/remote_file.rb index b7a553cbe8..e7837cbafb 100644 --- a/lib/chef/resource/remote_file.rb +++ b/lib/chef/resource/remote_file.rb @@ -122,6 +122,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/lib/chef/util/windows/logon_session.rb b/lib/chef/util/windows/logon_session.rb new file mode 100644 index 0000000000..d98e51ee14 --- /dev/null +++ b/lib/chef/util/windows/logon_session.rb @@ -0,0 +1,118 @@ +# +# Author:: Adam Edwards (<adamed@chef.io>) +# +# Copyright:: Copyright (c) 2015 Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/win32/api/security' if Chef::Platform.windows? +require 'chef/mixin/wide_string' + +class Chef + class Util + class Windows + class LogonSession + include Chef::Mixin::WideString + + def initialize(username, password, domain=nil) + if username.nil? || password.nil? + raise ArgumentError, 'The logon session must be initialize with non-nil user name and password parameters' + end + + @username = username + @password = password + @domain = domain + @token = FFI::Buffer.new(:pointer) + @session_opened = false + @impersonating = false + end + + def open + if @session_opened + raise RuntimeError, 'Attempted to open a logon session that was already open.' + end + + username = wstring(@username) + password = wstring(@password) + domain = wstring(@domain) + + status = Chef::ReservedNames::Win32::API::Security.LogonUserW(username, domain, password, Chef::ReservedNames::Win32::API::Security::LOGON32_LOGON_NETWORK, Chef::ReservedNames::Win32::API::Security::LOGON32_PROVIDER_DEFAULT, @token) + + if status == 0 + last_error = FFI::LastError.error + raise Chef::Exceptions::Win32APIError, "Logon for user `#{@username}` failed with Win32 status #{last_error}." + end + + @session_opened = true + end + + def close + validate_session_open! + + if @impersonating + restore_user_context + end + + Chef::ReservedNames::Win32::API::System.CloseHandle(@token.read_ulong) + @token = nil + @session_opened = false + end + + def set_user_context + validate_session_open! + + if ! @session_opened + raise RuntimeError, 'Attempted to set the user context before opening a session.' + end + + if @impersonating + raise RuntimeError, 'Attempt to set the user context when the user context is already set.' + end + + status = Chef::ReservedNames::Win32::API::Security.ImpersonateLoggedOnUser(@token.read_ulong) + + if status == 0 + last_error = FFI::LastError.error + raise Chef::Exceptions::Win32APIError, "Attempt to impersonate user `#{@username}` failed with Win32 status #{last_error}." + end + + @impersonating = true + end + + def restore_user_context + validate_session_open! + + if @impersonating + status = Chef::ReservedNames::Win32::API::Security.RevertToSelf + + if status == 0 + last_error = FFI::LastError.error + raise Chef::Exceptions::Win32APIError, "Unable to restore user context with Win32 status #{last_error}." + end + end + + @impersonating = false + end + + protected + + def validate_session_open! + if ! @session_opened + raise RuntimeError, 'Attempted to set the user context before opening a session.' + end + end + end + end + end +end diff --git a/lib/chef/win32/api/security.rb b/lib/chef/win32/api/security.rb index 4c352a3554..a8540c3f81 100644 --- a/lib/chef/win32/api/security.rb +++ b/lib/chef/win32/api/security.rb @@ -428,6 +428,8 @@ class Chef safe_attach_function :SetSecurityDescriptorSacl, [ :pointer, :BOOL, :pointer, :BOOL ], :BOOL safe_attach_function :GetTokenInformation, [ :HANDLE, :TOKEN_INFORMATION_CLASS, :pointer, :DWORD, :PDWORD ], :BOOL safe_attach_function :LogonUserW, [:LPTSTR, :LPTSTR, :LPTSTR, :DWORD, :DWORD, :PHANDLE], :BOOL + safe_attach_function :ImpersonateLoggedOnUser, [:HANDLE], :BOOL + safe_attach_function :RevertToSelf, [], :BOOL end end diff --git a/spec/functional/mixin/user_context_spec.rb b/spec/functional/mixin/user_context_spec.rb new file mode 100644 index 0000000000..7055360656 --- /dev/null +++ b/spec/functional/mixin/user_context_spec.rb @@ -0,0 +1,118 @@ +# +# 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. +# + +require 'spec_helper' + +require 'chef/win32/api' if Chef::Platform.windows? +require 'chef/win32/api/error' if Chef::Platform.windows? +require 'chef/mixin/user_context' + +describe Chef::Mixin::UserContext, windows_only: true do + include Chef::Mixin::UserContext + + let(:get_user_name_a) do + FFI::ffi_lib 'advapi32.dll' + FFI::attach_function :GetUserNameA, [ :pointer, :pointer ], :bool + end + + let(:process_username) do + name_size = FFI::Buffer.new(:long).write_long(0) + succeeded = get_user_name_a.call(nil, name_size) + last_error = FFI::LastError.error + if succeeded || last_error != Chef::ReservedNames::Win32::API::Error::ERROR_INSUFFICIENT_BUFFER + raise Chef::Exceptions::Win32APIError, "Expected ERROR_INSUFFICIENT_BUFFER from GetUserNameA but it returned the following error: #{last_error}" + end + user_name = FFI::MemoryPointer.new :char, (name_size.read_long) + succeeded = get_user_name_a.call(user_name, name_size) + last_error = FFI::LastError.error + if succeeded == 0 || last_error != 0 + raise Chef::Exceptions::Win32APIError, "GetUserNameA failed with #{lasterror}" + end + user_name.read_string + end + + let(:test_user) { 'chefuserctx3' } + let(:test_domain) { windows_nonadmin_user_domain } + let(:test_password) { 'j823jfxK3;2Xe1' } + + let(:username_domain_qualification) { nil } + let(:username_with_conditional_domain) { username_domain_qualification.nil? ? username_to_impersonate : "#{username_domain_qualification}\\#{username_to_impersonate}" } + + let(:windows_nonadmin_user) { test_user } + let(:windows_nonadmin_user_password) { test_password } + + let(:username_while_impersonating) do + username = nil + with_user_context(username_with_conditional_domain, username_to_impersonate_password, domain_to_impersonate) do + username = process_username + end + username + end + + shared_examples_for "method that executes the block while impersonating the alternate user" do + it "sets the current thread token to that of the alternate user when the correct password is specified" do + expect(username_while_impersonating.downcase).to eq(username_to_impersonate.downcase) + end + end + + describe "#with_user_context" do + context "when the user and domain are both nil" do + let(:username_to_impersonate) { nil } + let(:domain_to_impersonate) { nil } + let(:username_to_impersonate_password) { nil } + + it "has the same token and username as the process" do + expect(username_while_impersonating.downcase).to eq(ENV['username'].downcase) + end + end + + context "when a non-nil user is specified" do + include_context "a non-admin Windows user" + context "when a username different than the process user is specified" do + let(:username_to_impersonate) { test_user } + let(:username_to_impersonate_password) { test_password } + context "when an explicit domain is given with a valid password" do + let(:domain_to_impersonate) { test_domain } + it "sets the current thread token to that of the alternate user when the correct password is specified" do + expect(username_while_impersonating.downcase).to eq(username_to_impersonate.downcase) + end + end + + context "when a valid password and a non-qualified user is given and no domain is specified" do + let(:domain_to_impersonate) { nil } + it_behaves_like "method that executes the block while impersonating the alternate user" + end + + context "when a valid password and a qualified user is given and no domain is specified" do + let(:domain_to_impersonate) { nil } + let(:username_domain_qualification) { test_domain } + it_behaves_like "method that executes the block while impersonating the alternate user" + end + + it "raises an error user if specified with the wrong password" do + expect { with_user_context(username_to_impersonate, username_to_impersonate_password + '1', nil) }.to raise_error(ArgumentError) + end + end + end + + context "when invalid arguments are passed" do + it "raises an ArgumentError exception if the password is not specified but the user is specified" do + expect { with_user_context(test_user, nil, nil) }.to raise_error(ArgumentError) + end + end + end +end diff --git a/spec/functional/resource/remote_file_spec.rb b/spec/functional/resource/remote_file_spec.rb index 4fbcd2d24b..e0311560a2 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/unit/mixin/user_context_spec.rb b/spec/unit/mixin/user_context_spec.rb new file mode 100644 index 0000000000..0bd16ae869 --- /dev/null +++ b/spec/unit/mixin/user_context_spec.rb @@ -0,0 +1,124 @@ +# +# 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. +# + +require 'spec_helper' +require 'chef/mixin/user_context' +require 'chef/util/windows/logon_session' + +describe "a class that mixes in user_context" do + let(:instance_with_user_context) do + class UserContextConsumer + include ::Chef::Mixin::UserContext + def with_context(user, domain, password, &block) + with_user_context(user, password, domain, &block) + end + end + UserContextConsumer.new + end + + shared_examples_for "a method that requires a block" do + it "raises an ArgumentError exception if a block is not supplied" do + expect { instance_with_user_context.with_context(nil, nil, nil) }.to raise_error(ArgumentError) + end + end + + context "when running on Windows" do + before do + allow(::Chef::Platform).to receive(:windows?).and_return(true) + allow(::Chef::Util::Windows::LogonSession).to receive(:new).and_return(logon_session) + end + + let(:logon_session) { instance_double('::Chef::Util::Windows::LogonSession', :set_user_context => nil, :open => nil, :close => nil) } + + it "does not raise an exception when the user and all parameters are nil" do + expect { instance_with_user_context.with_context(nil, nil, nil) {} }.not_to raise_error + end + + it "raises an exception if the user is supplied but not the domain and password" do + expect { instance_with_user_context.with_context('kamilah', nil, nil) {} }.to raise_error(ArgumentError) + end + + it "raises an exception if the domain is supplied but not the user and password" do + expect { instance_with_user_context.with_context(nil, 'xanadu', nil) {} }.to raise_error(ArgumentError) + end + + it "raises an exception if the password is supplied but not the user and domain" do + expect { instance_with_user_context.with_context(nil, nil, 'chef4life') {} }.to raise_error(ArgumentError) + end + + it "raises an exception if the user and domain is supplied but the password is not" do + expect { instance_with_user_context.with_context('kamilah', 'xanadu', nil) {} }.to raise_error(ArgumentError) + end + + context "when given valid user credentials" do + before do + expect(::Chef::Util::Windows::LogonSession).to receive(:new).and_return(logon_session) + end + + let(:block_object) do + class BlockClass + def block_method + end + end + BlockClass.new + end + + let(:block_parameter) { Proc.new { block_object.block_method } } + + context "when the block doesn't raise an exception" do + before do + expect( block_object ).to receive(:block_method) + end + it "calls the supplied block" do + expect { instance_with_user_context.with_context('kamilah', nil, 'chef4life', &block_parameter) }.not_to raise_error + end + + it "does not raise an exception if the user, password, and domain are specified" do + expect { instance_with_user_context.with_context('kamilah', 'xanadu', 'chef4life', &block_parameter) }.not_to raise_error + end + end + + context 'when the block raises an exception' do + class UserContextTestException < Exception + end + let(:block_parameter) { Proc.new { raise UserContextTextException } } + + it 'raises the exception raised by the block' do + expect { instance_with_user_context.with_context('kamilah', nil, 'chef4life', &block_parameter) }.not_to raise_error(UserContextTestException) + end + + it 'closes the logon session so resources are not leaked' do + expect(logon_session).to receive(:close) + expect { instance_with_user_context.with_context('kamilah', nil, 'chef4life', &block_parameter) }.not_to raise_error(UserContextTestException) + end + end + end + + it_behaves_like "a method that requires a block" + end + + context "when not running on Windows" do + before do + allow(::Chef::Platform).to receive(:windows?).and_return(false) + end + + it "raises a ::Chef::Exceptions::UnsupportedPlatform exception" do + expect { instance_with_user_context.with_context(nil, nil, nil) {} }.to raise_error(::Chef::Exceptions::UnsupportedPlatform) + end + 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 3666a47468..a48a191ab1 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 (c) 2015 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 diff --git a/spec/unit/util/windows/logon_session_spec.rb b/spec/unit/util/windows/logon_session_spec.rb new file mode 100644 index 0000000000..bc9aaefeb1 --- /dev/null +++ b/spec/unit/util/windows/logon_session_spec.rb @@ -0,0 +1,284 @@ +# +# 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. +# + +require 'spec_helper' +require 'chef/util/windows/logon_session' + +describe ::Chef::Util::Windows::LogonSession do + before do + stub_const('Chef::ReservedNames::Win32::API::Security', Class.new) + stub_const('Chef::ReservedNames::Win32::API::Security::LOGON32_LOGON_NETWORK', 314) + stub_const('Chef::ReservedNames::Win32::API::Security::LOGON32_PROVIDER_DEFAULT', 159) + stub_const('Chef::ReservedNames::Win32::API::System', Class.new ) + end + + let(:session) { ::Chef::Util::Windows::LogonSession.new(session_user, password, session_domain) } + + shared_examples_for "it received syntactically invalid credentials" do + it 'does not raisees an exception when it is initialized' do + expect{session}.to raise_error(ArgumentError) + end + end + + shared_examples_for "it received an incorrect username and password combination" do + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:LogonUserW).and_return(0) + end + + it 'raises a Chef::Exceptions::Win32APIError exception when the open method is called' do + expect{session.open}.to raise_error(Chef::Exceptions::Win32APIError) + expect(session).not_to receive(:close) + expect(Chef::ReservedNames::Win32::API::System).not_to receive(:CloseHandle) + end + end + + shared_examples_for "it received valid credentials" do + it 'does not raise an exception when the open method is called' do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:LogonUserW).and_return(1) + expect{session.open}.not_to raise_error + end + end + + shared_examples_for 'the session is not open' do + it "does not raise an exception when #open is called" do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:LogonUserW).and_return(1) + expect{session.open}.not_to raise_error + end + + it "raises an exception if #close is called" do + expect{session.close}.to raise_error(RuntimeError) + end + + it "raises an exception if #restore_user_context is called" do + expect{session.restore_user_context}.to raise_error(RuntimeError) + end + end + + shared_examples_for 'the session is open' do + before do + allow(Chef::ReservedNames::Win32::API::System).to receive(:CloseHandle) + end + it 'does not result in an exception when #restore_user_context is called' do + expect {session.restore_user_context}.not_to raise_error + end + + it 'does not result in an exception when #close is called' do + expect {session.close}.not_to raise_error + end + + it 'does close the operating system handle when #close is called' do + expect(Chef::ReservedNames::Win32::API::System).not_to receive(:CloseHandle) + expect {session.restore_user_context}.not_to raise_error + end + end + + context 'when the session is initialized with a nil user' do + context "when the password, and domain are all nil" do + let(:session_user) { nil } + let(:session_domain) { nil } + let(:password) { nil } + it_behaves_like "it received syntactically invalid credentials" + end + + context "when the password is non-nil password, and the domain is nil" do + let(:session_user) { nil } + let(:password) { 'ponies' } + let(:session_domain) { nil } + it_behaves_like "it received syntactically invalid credentials" + end + + context "when the password is nil and the domain is non-nil" do + let(:session_user) { nil } + let(:password) { nil } + let(:session_domain) { 'fairyland' } + it_behaves_like "it received syntactically invalid credentials" + end + + context "when the password and domain are non-nil" do + let(:session_user) { nil } + let(:password) { 'ponies' } + let(:session_domain) { 'fairyland' } + it_behaves_like "it received syntactically invalid credentials" + end + end + + context 'when the session is initialized with a valid user' do + let(:session_user) { 'chalena' } + + context 'when the password is nil' do + let(:password) { nil } + context 'when the domain is non-nil' do + let(:session_domain) { 'fairyland' } + it_behaves_like "it received syntactically invalid credentials" + end + + context 'when the domain is nil' do + context 'when the domain is non-nil' do + let(:session_domain) { nil } + it_behaves_like "it received syntactically invalid credentials" + end + end + end + + context "when a syntactically valid username and password are supplied" do + context "when the password is non-nil and the domain is nil" do + let(:password) { 'ponies' } + let(:session_domain) { nil } + it "does not raise an exception if it is initialized with a non-nil username, non-nil password, and a nil domain" do + expect{session}.not_to raise_error + end + + it_behaves_like 'it received valid credentials' + it_behaves_like "it received an incorrect username and password combination" + end + + context "when the password and domain are non-nil" do + let(:password) { 'ponies' } + let(:session_domain) { 'fairyland' } + it "does not raise an exception if it is initialized with a non-nil username, non-nil password, and non-nil domain" do + expect{session}.not_to raise_error + end + + it_behaves_like 'it received valid credentials' + it_behaves_like "it received an incorrect username and password combination" + end + + context 'when the #open method has not been called' do + let(:password) { 'ponies' } + let(:session_domain) { 'fairlyand' } + it_behaves_like 'the session is not open' + end + + context 'when the session was opened' do + let(:password) { 'ponies' } + let(:session_domain) { 'fairlyand' } + + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:LogonUserW).and_return(1) + expect{session.open}.not_to raise_error + end + + it 'raises an exception if #open is called' do + expect{session.open}.to raise_error(RuntimeError) + end + + context 'when the session was opened and then closed with the #close method' do + before do + expect(Chef::ReservedNames::Win32::API::System).to receive(:CloseHandle) + expect{session.close}.not_to raise_error + end + it_behaves_like 'the session is not open' + end + + it 'can be closed and close the operating system handle' do + expect(Chef::ReservedNames::Win32::API::System).to receive(:CloseHandle) + expect{session.close}.not_to raise_error + end + + it 'can impersonate the user' do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:ImpersonateLoggedOnUser).and_return(1) + expect{session.set_user_context}.not_to raise_error + end + + context 'when #set_user_context fails due to low resources causing a failure to impersonate' do + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:ImpersonateLoggedOnUser).and_return(0) + end + + it 'raises an exception when #set_user_context fails because impersonation failed' do + expect {session.set_user_context}.to raise_error(Chef::Exceptions::Win32APIError) + end + + context 'when calling subsequent methods' do + before do + expect{session.set_user_context}.to raise_error(Chef::Exceptions::Win32APIError) + expect(Chef::ReservedNames::Win32::API::Security).not_to receive(:RevertToSelf) + end + + it_behaves_like 'the session is open' + end + end + + context 'when #set_user_context successfully impersonates the user' do + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:ImpersonateLoggedOnUser).and_return(1) + expect{session.set_user_context}.not_to raise_error + end + + context 'when attempting to impersonate while already impersonating' do + it 'raises an error if the #set_user_context is called again' do + expect{session.set_user_context}.to raise_error(RuntimeError) + end + end + + describe 'the impersonation will be reverted' do + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:RevertToSelf) + end + it_behaves_like 'the session is open' + end + + context 'when the attempt to revert impersonation fails' do + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:RevertToSelf).and_return(0) + end + + it 'raises an exception when #restore_user_context is called' do + expect{session.restore_user_context}.to raise_error(Chef::Exceptions::Win32APIError) + end + + it 'raises an exception when #close is called and impersonation fails' do + expect{session.close}.to raise_error(Chef::Exceptions::Win32APIError) + end + + context 'when calling methods after revert fails in #restore_user_context' do + before do + expect{session.restore_user_context}.to raise_error(Chef::Exceptions::Win32APIError) + end + + context 'when revert continues to fail' do + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:RevertToSelf).and_return(0) + end + it 'raises an exception when #close is called and impersonation fails' do + expect{session.close}.to raise_error(Chef::Exceptions::Win32APIError) + end + end + + context 'when revert stops failing and succeeds' do + before do + expect(Chef::ReservedNames::Win32::API::Security).to receive(:RevertToSelf).and_return(1) + end + + it 'does not raise an exception when #restore_user_context is called' do + expect{session.restore_user_context}.not_to raise_error + end + + it 'does not raise an exception when #close is called' do + expect(Chef::ReservedNames::Win32::API::System).to receive(:CloseHandle) + expect{session.close}.not_to raise_error + end + end + end + end + end + + end + end + end +end |