diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb index efb9a4caf..ec2242a7a 100644 --- a/lib/puppet/util/windows/api_types.rb +++ b/lib/puppet/util/windows/api_types.rb @@ -1,57 +1,79 @@ require 'ffi' +require 'puppet/util/windows/string' module Puppet::Util::Windows::APITypes module ::FFI::Library # Wrapper method for attach_function + private def attach_function_private(*args) attach_function(*args) private args[0] end end + class ::FFI::MemoryPointer + def self.from_string_to_wide_string(str) + str = Puppet::Util::Windows::String.wide_string(str) + ptr = FFI::MemoryPointer.new(:byte, str.bytesize) + # uchar here is synonymous with byte + ptr.put_array_of_uchar(0, str.bytes.to_a) + + ptr + end + + def read_handle + type_size == 4 ? read_uint32 : read_uint64 + end + + def read_wide_string(char_length) + # char_length is number of wide chars (typically excluding NULLs), *not* bytes + str = get_bytes(0, char_length * 2).force_encoding('UTF-16LE') + str.encode(Encoding.default_external) + end + end + # FFI Types # https://github.com/ffi/ffi/wiki/Types # Windows - Common Data Types # http://msdn.microsoft.com/en-us/library/cc230309.aspx # Windows Data Types # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx FFI.typedef :uint16, :word FFI.typedef :uint32, :dword # uintptr_t is defined in an FFI conf as platform specific, either # ulong_long on x64 or just ulong on x86 FFI.typedef :uintptr_t, :handle # buffer_inout is similar to pointer (platform specific), but optimized for buffers FFI.typedef :buffer_inout, :lpwstr # buffer_in is similar to pointer (platform specific), but optimized for CONST read only buffers FFI.typedef :buffer_in, :lpcwstr # string is also similar to pointer, but should be used for const char * # NOTE that this is not wide, useful only for A suffixed functions FFI.typedef :string, :lpcstr # pointer in FFI is platform specific # NOTE: for API calls with reserved lpvoid parameters, pass a FFI::Pointer::NULL FFI.typedef :pointer, :lpvoid FFI.typedef :pointer, :lpword FFI.typedef :pointer, :lpdword FFI.typedef :pointer, :pdword FFI.typedef :pointer, :phandle FFI.typedef :pointer, :ulong_ptr # any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width # which is what FFI uses by default # instead create new aliases for these very special cases # NOTE: not a good idea to redefine FFI :ulong since other typedefs may rely on it FFI.typedef :uint32, :win32_ulong FFI.typedef :int32, :win32_long # NOTE: FFI already defines ushort as a 16-bit unsigned like this: # FFI.typedef :uint16, :ushort # 8 bits per byte FFI.typedef :uchar, :byte end diff --git a/lib/puppet/util/windows/string.rb b/lib/puppet/util/windows/string.rb index 13d9839d1..147c92915 100644 --- a/lib/puppet/util/windows/string.rb +++ b/lib/puppet/util/windows/string.rb @@ -1,14 +1,16 @@ require 'puppet/util/windows' module Puppet::Util::Windows::String def wide_string(str) + # if given a nil string, assume caller wants to pass a nil pointer to win32 + return nil if str.nil? # ruby (< 2.1) does not respect multibyte terminators, so it is possible # for a string to contain a single trailing null byte, followed by garbage # causing buffer overruns. # # See http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?revision=41920&view=revision newstr = str + "\0".encode(str.encoding) newstr.encode!('UTF-16LE') end module_function :wide_string end diff --git a/lib/puppet/util/windows/user.rb b/lib/puppet/util/windows/user.rb index 51eef779d..2e4e76f3b 100644 --- a/lib/puppet/util/windows/user.rb +++ b/lib/puppet/util/windows/user.rb @@ -1,108 +1,158 @@ require 'puppet/util/windows' require 'win32/security' require 'facter' +require 'ffi' module Puppet::Util::Windows::User include ::Windows::Security extend ::Windows::Security + extend Puppet::Util::Windows::String + extend FFI::Library def admin? majversion = Facter.value(:kernelmajversion) return false unless majversion # if Vista or later, check for unrestricted process token return Win32::Security.elevated_security? unless majversion.to_f < 6.0 # otherwise 2003 or less check_token_membership end module_function :admin? def check_token_membership sid = 0.chr * 80 size = [80].pack('L') member = 0.chr * 4 unless CreateWellKnownSid(WinBuiltinAdministratorsSid, nil, sid, size) raise Puppet::Util::Windows::Error.new("Failed to create administrators SID") end unless IsValidSid(sid) raise Puppet::Util::Windows::Error.new("Invalid SID") end unless CheckTokenMembership(nil, sid, member) raise Puppet::Util::Windows::Error.new("Failed to check membership") end # Is administrators SID enabled in calling thread's access token? member.unpack('L')[0] == 1 end module_function :check_token_membership def password_is?(name, password) logon_user(name, password) true rescue Puppet::Util::Windows::Error false end module_function :password_is? def logon_user(name, password, &block) fLOGON32_LOGON_NETWORK = 3 fLOGON32_PROVIDER_DEFAULT = 0 - logon_user = Win32API.new("advapi32", "LogonUser", ['P', 'P', 'P', 'L', 'L', 'P'], 'L') - close_handle = Win32API.new("kernel32", "CloseHandle", ['L'], 'B') - - token = 0.chr * 4 - if logon_user.call(name, ".", password, fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token) == 0 + token_pointer = FFI::MemoryPointer.new(:handle, 1) + if ! LogonUserW(wide_string(name), wide_string('.'), wide_string(password), + fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token_pointer) raise Puppet::Util::Windows::Error.new("Failed to logon user #{name.inspect}") end - token = token.unpack('L')[0] + token = token_pointer.read_handle begin yield token if block_given? ensure - close_handle.call(token) + CloseHandle(token) end end module_function :logon_user def load_profile(user, password) logon_user(user, password) do |token| - # Set up the PROFILEINFO structure that will be used to load the - # new user's profile - # typedef struct _PROFILEINFO { - # DWORD dwSize; - # DWORD dwFlags; - # LPTSTR lpUserName; - # LPTSTR lpProfilePath; - # LPTSTR lpDefaultPath; - # LPTSTR lpServerName; - # LPTSTR lpPolicyPath; - # HANDLE hProfile; - # } PROFILEINFO, *LPPROFILEINFO; - fPI_NOUI = 1 - profile = 0.chr * 4 - pi = [4 * 8, fPI_NOUI, user, nil, nil, nil, nil, profile].pack('LLPPPPPP') - - load_user_profile = Win32API.new('userenv', 'LoadUserProfile', ['L', 'P'], 'L') - unload_user_profile = Win32API.new('userenv', 'UnloadUserProfile', ['L', 'L'], 'L') + pi = PROFILEINFO.new + pi[:dwSize] = PROFILEINFO.size + pi[:dwFlags] = 1 # PI_NOUI - prevents display of profile error msgs + pi[:lpUserName] = FFI::MemoryPointer.from_string_to_wide_string(user) # Load the profile. Since it doesn't exist, it will be created - if load_user_profile.call(token, pi) == 0 + if ! LoadUserProfileW(token, pi.pointer) raise Puppet::Util::Windows::Error.new("Failed to load user profile #{user.inspect}") end Puppet.debug("Loaded profile for #{user}") - profile = pi.unpack('LLLLLLLL').last - if unload_user_profile.call(token, profile) == 0 + if ! UnloadUserProfile(token, pi[:hProfile]) raise Puppet::Util::Windows::Error.new("Failed to unload user profile #{user.inspect}") end end end module_function :load_profile + + ffi_convention :stdcall + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa378184(v=vs.85).aspx + # BOOL LogonUser( + # _In_ LPTSTR lpszUsername, + # _In_opt_ LPTSTR lpszDomain, + # _In_opt_ LPTSTR lpszPassword, + # _In_ DWORD dwLogonType, + # _In_ DWORD dwLogonProvider, + # _Out_ PHANDLE phToken + # ); + ffi_lib :advapi32 + attach_function_private :LogonUserW, + [:lpwstr, :lpwstr, :lpwstr, :dword, :dword, :phandle], :bool + + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx + # BOOL WINAPI CloseHandle( + # _In_ HANDLE hObject + # ); + ffi_lib 'kernel32' + attach_function_private :CloseHandle, [:handle], :bool + + # http://msdn.microsoft.com/en-us/library/windows/desktop/bb773378(v=vs.85).aspx + # typedef struct _PROFILEINFO { + # DWORD dwSize; + # DWORD dwFlags; + # LPTSTR lpUserName; + # LPTSTR lpProfilePath; + # LPTSTR lpDefaultPath; + # LPTSTR lpServerName; + # LPTSTR lpPolicyPath; + # HANDLE hProfile; + # } PROFILEINFO, *LPPROFILEINFO; + # technically + # NOTE: that for structs, buffer_* (lptstr alias) cannot be used + class PROFILEINFO < FFI::Struct + layout :dwSize, :dword, + :dwFlags, :dword, + :lpUserName, :pointer, + :lpProfilePath, :pointer, + :lpDefaultPath, :pointer, + :lpServerName, :pointer, + :lpPolicyPath, :pointer, + :hProfile, :handle + end + + # http://msdn.microsoft.com/en-us/library/windows/desktop/bb762281(v=vs.85).aspx + # BOOL WINAPI LoadUserProfile( + # _In_ HANDLE hToken, + # _Inout_ LPPROFILEINFO lpProfileInfo + # ); + ffi_lib :userenv + attach_function_private :LoadUserProfileW, + [:handle, :pointer], :bool + + # http://msdn.microsoft.com/en-us/library/windows/desktop/bb762282(v=vs.85).aspx + # BOOL WINAPI UnloadUserProfile( + # _In_ HANDLE hToken, + # _In_ HANDLE hProfile + # ); + ffi_lib :userenv + attach_function_private :UnloadUserProfile, + [:handle, :handle], :bool end diff --git a/spec/integration/util/windows/user_spec.rb b/spec/integration/util/windows/user_spec.rb index 0435b2cdc..67b9cad84 100755 --- a/spec/integration/util/windows/user_spec.rb +++ b/spec/integration/util/windows/user_spec.rb @@ -1,59 +1,92 @@ #! /usr/bin/env ruby require 'spec_helper' describe "Puppet::Util::Windows::User", :if => Puppet.features.microsoft_windows? do describe "2003 without UAC" do before :each do Facter.stubs(:value).with(:kernelmajversion).returns("5.2") end it "should be an admin if user's token contains the Administrators SID" do Puppet::Util::Windows::User.expects(:check_token_membership).returns(true) Win32::Security.expects(:elevated_security?).never Puppet::Util::Windows::User.should be_admin end it "should not be an admin if user's token doesn't contain the Administrators SID" do Puppet::Util::Windows::User.expects(:check_token_membership).returns(false) Win32::Security.expects(:elevated_security?).never Puppet::Util::Windows::User.should_not be_admin end it "should raise an exception if we can't check token membership" do Puppet::Util::Windows::User.expects(:check_token_membership).raises(Win32::Security::Error, "Access denied.") Win32::Security.expects(:elevated_security?).never lambda { Puppet::Util::Windows::User.admin? }.should raise_error(Win32::Security::Error, /Access denied./) end end describe "2008 with UAC" do before :each do Facter.stubs(:value).with(:kernelmajversion).returns("6.0") end it "should be an admin if user is running with elevated privileges" do Win32::Security.stubs(:elevated_security?).returns(true) Puppet::Util::Windows::User.expects(:check_token_membership).never Puppet::Util::Windows::User.should be_admin end it "should not be an admin if user is not running with elevated privileges" do Win32::Security.stubs(:elevated_security?).returns(false) Puppet::Util::Windows::User.expects(:check_token_membership).never Puppet::Util::Windows::User.should_not be_admin end it "should raise an exception if the process fails to open the process token" do Win32::Security.stubs(:elevated_security?).raises(Win32::Security::Error, "Access denied.") Puppet::Util::Windows::User.expects(:check_token_membership).never lambda { Puppet::Util::Windows::User.admin? }.should raise_error(Win32::Security::Error, /Access denied./) end end + + describe "module function" do + let(:username) { 'fabio' } + let(:bad_password) { 'goldilocks' } + let(:logon_fail_msg) { /Failed to logon user "fabio": Logon failure: unknown user name or bad password./ } + + describe "load_profile" do + it "should raise an error when provided with an incorrect username and password" do + lambda { Puppet::Util::Windows::User.load_profile(username, bad_password) }.should raise_error(Puppet::Util::Windows::Error, logon_fail_msg) + end + it "should raise an error when provided with an incorrect username and nil password" do + lambda { Puppet::Util::Windows::User.load_profile(username, nil) }.should raise_error(Puppet::Util::Windows::Error, logon_fail_msg) + end + end + + describe "logon_user" do + it "should raise an error when provided with an incorrect username and password" do + lambda { Puppet::Util::Windows::User.logon_user(username, bad_password) }.should raise_error(Puppet::Util::Windows::Error, logon_fail_msg) + end + it "should raise an error when provided with an incorrect username and nil password" do + lambda { Puppet::Util::Windows::User.logon_user(username, nil) }.should raise_error(Puppet::Util::Windows::Error, logon_fail_msg) + end + end + + describe "password_is?" do + it "should return false given an incorrect username and password" do + Puppet::Util::Windows::User.password_is?(username, bad_password).should be_false + end + it "should return false given an incorrect username and nil password" do + Puppet::Util::Windows::User.password_is?(username, nil).should be_false + end + end + end end diff --git a/spec/unit/util/windows/api_types_spec.rb b/spec/unit/util/windows/api_types_spec.rb new file mode 100644 index 000000000..75d591edd --- /dev/null +++ b/spec/unit/util/windows/api_types_spec.rb @@ -0,0 +1,24 @@ +# encoding: UTF-8 +#!/usr/bin/env ruby + +require 'spec_helper' + +describe "FFI::MemoryPointer", :if => Puppet.features.microsoft_windows? do + context "read_wide_string" do + let (:string) { "foo_bar" } + + it "should properly roundtrip a given string" do + ptr = FFI::MemoryPointer.from_string_to_wide_string(string) + read_string = ptr.read_wide_string(string.length) + + read_string.should == string + end + + it "should return a given string in the default encoding" do + ptr = FFI::MemoryPointer.from_string_to_wide_string(string) + read_string = ptr.read_wide_string(string.length) + + read_string.encoding.should == Encoding.default_external + end + end +end diff --git a/spec/unit/util/windows/string_spec.rb b/spec/unit/util/windows/string_spec.rb index 60f7e6449..5c6473e70 100644 --- a/spec/unit/util/windows/string_spec.rb +++ b/spec/unit/util/windows/string_spec.rb @@ -1,54 +1,58 @@ # encoding: UTF-8 #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/windows' describe "Puppet::Util::Windows::String", :if => Puppet.features.microsoft_windows? do UTF16_NULL = [0, 0] def wide_string(str) Puppet::Util::Windows::String.wide_string(str) end def converts_to_wide_string(string_value) expected = string_value.encode(Encoding::UTF_16LE) expected_bytes = expected.bytes.to_a + UTF16_NULL wide_string(string_value).bytes.to_a.should == expected_bytes end context "wide_string" do it "should return encoding of UTF-16LE" do wide_string("bob").encoding.should == Encoding::UTF_16LE end it "should return valid encoding" do wide_string("bob").valid_encoding?.should be_true end it "should convert an ASCII string" do converts_to_wide_string("bob".encode(Encoding::US_ASCII)) end it "should convert a UTF-8 string" do converts_to_wide_string("bob".encode(Encoding::UTF_8)) end it "should convert a UTF-16LE string" do converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_16LE)) end it "should convert a UTF-16BE string" do converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_16BE)) end it "should convert an UTF-32LE string" do converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_32LE)) end it "should convert an UTF-32BE string" do converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_32BE)) end + + it "should return a nil when given a nil" do + wide_string(nil).should == nil + end end end