summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradamedx <adamedx@gmail.com>2016-04-02 23:22:14 -0700
committeradamedx <adamedx@gmail.com>2016-07-07 16:45:32 -0700
commit4bf4b1e7a3322fcfa5d0d73ce45e3cb265a9cc30 (patch)
tree0fcddbd1886560e6713acacffbfc34d5cac06c5c
parentfd2fedf4f49a05bec355d5c569e17a02ca6ff28f (diff)
downloadchef-4bf4b1e7a3322fcfa5d0d73ce45e3cb265a9cc30.tar.gz
Windows thread alternate user impersonation support
-rw-r--r--lib/chef/mixin/user_context.rb62
-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/unit/mixin/user_context_spec.rb124
-rw-r--r--spec/unit/util/windows/logon_session_spec.rb284
6 files changed, 708 insertions, 0 deletions
diff --git a/lib/chef/mixin/user_context.rb b/lib/chef/mixin/user_context.rb
new file mode 100644
index 0000000000..5305678f2b
--- /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/util/windows/logon_session.rb b/lib/chef/util/windows/logon_session.rb
new file mode 100644
index 0000000000..c557ce0ff1
--- /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 64df077686..ea3f7ec83a 100644
--- a/lib/chef/win32/api/security.rb
+++ b/lib/chef/win32/api/security.rb
@@ -453,6 +453,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..e157f959af
--- /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/unit/mixin/user_context_spec.rb b/spec/unit/mixin/user_context_spec.rb
new file mode 100644
index 0000000000..00306592ec
--- /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/util/windows/logon_session_spec.rb b/spec/unit/util/windows/logon_session_spec.rb
new file mode 100644
index 0000000000..492c204298
--- /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) { "fairyland" }
+ it_behaves_like "the session is not open"
+ end
+
+ context "when the session was opened" do
+ let(:password) { "ponies" }
+ let(:session_domain) { "fairyland" }
+
+ 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