summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Edwards <adamed@opscode.com>2015-12-12 23:09:21 -0800
committerAdam Edwards <adamed@opscode.com>2015-12-12 23:09:21 -0800
commit79c8c059600a59831a7e32521fd25127d475b766 (patch)
tree7577e32f9d6c3d74711e50ca0512921b5656a372
parent1476e48e3c6403f79810cb060c96999018b726ec (diff)
downloadchef-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.rb62
-rw-r--r--lib/chef/provider/remote_file/network_file.rb26
-rw-r--r--lib/chef/resource/remote_file.rb32
-rw-r--r--lib/chef/util/windows/logon_session.rb118
-rw-r--r--lib/chef/win32/api/security.rb2
-rw-r--r--spec/functional/mixin/user_context_spec.rb118
-rw-r--r--spec/functional/resource/remote_file_spec.rb158
-rw-r--r--spec/unit/mixin/user_context_spec.rb124
-rw-r--r--spec/unit/provider/remote_file/network_file_spec.rb7
-rw-r--r--spec/unit/util/windows/logon_session_spec.rb284
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