diff options
author | adamedx <adamedx@gmail.com> | 2016-04-02 23:22:14 -0700 |
---|---|---|
committer | Bryan McLellan <btm@loftninjas.org> | 2017-09-05 20:09:30 -0400 |
commit | d8fa21b9851b2a6d3c1ee370c8d0cfee8be278b4 (patch) | |
tree | ef63f7b277c30c4d1d48d944c3be3e219c56a7ef | |
parent | 2e9c29b0658136f0536d423ca71899a825f3920d (diff) | |
download | chef-d8fa21b9851b2a6d3c1ee370c8d0cfee8be278b4.tar.gz |
Windows thread alternate user impersonation support
-rw-r--r-- | lib/chef/mixin/user_context.rb | 62 | ||||
-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/unit/mixin/user_context_spec.rb | 124 | ||||
-rw-r--r-- | spec/unit/util/windows/logon_session_spec.rb | 284 |
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 a2cfe35dad..a6f79f5d7d 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 |