diff options
author | Jay Mundrawala <jdmundrawala@gmail.com> | 2015-05-15 11:40:41 -0500 |
---|---|---|
committer | Jay Mundrawala <jdmundrawala@gmail.com> | 2015-05-15 11:40:41 -0500 |
commit | eaffc0a99e962cb7e055b5fd960a9e586df4a184 (patch) | |
tree | f47755b4628f41440b581de7df3cab12983dea1c | |
parent | 81e4fb0c1f10b46e92226c60b71a79c1c931cd3b (diff) | |
parent | 5251d6c8d8fc48ddb1279294b259a39ae9eddab6 (diff) | |
download | chef-eaffc0a99e962cb7e055b5fd960a9e586df4a184.tar.gz |
Merge pull request #3344 from chef/jdm/netuser
Changing Net User things to use ffi instead of win32-api
-rw-r--r-- | lib/chef/mixin/wstring.rb | 31 | ||||
-rw-r--r-- | lib/chef/util/windows/net_user.rb | 191 | ||||
-rw-r--r-- | lib/chef/win32/api.rb | 3 | ||||
-rw-r--r-- | lib/chef/win32/api/net.rb | 117 | ||||
-rw-r--r-- | lib/chef/win32/api/security.rb | 16 | ||||
-rw-r--r-- | lib/chef/win32/api/unicode.rb | 2 | ||||
-rw-r--r-- | lib/chef/win32/net.rb | 214 | ||||
-rw-r--r-- | lib/chef/win32/security.rb | 15 | ||||
-rw-r--r-- | spec/functional/resource/user/windows_spec.rb | 125 |
9 files changed, 592 insertions, 122 deletions
diff --git a/lib/chef/mixin/wstring.rb b/lib/chef/mixin/wstring.rb new file mode 100644 index 0000000000..bb6fdf4884 --- /dev/null +++ b/lib/chef/mixin/wstring.rb @@ -0,0 +1,31 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright 2015 Chef Software +# 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. +# + +class Chef + module Mixin + module WideString + def wstring(str) + if str.nil? || str.encoding == Encoding::UTF_16LE + str + else + str.to_wstring + end + end + end + end +end diff --git a/lib/chef/util/windows/net_user.rb b/lib/chef/util/windows/net_user.rb index 5df1a8aaa4..26fbe53db6 100644 --- a/lib/chef/util/windows/net_user.rb +++ b/lib/chef/util/windows/net_user.rb @@ -18,98 +18,69 @@ require 'chef/util/windows' require 'chef/exceptions' +require 'chef/win32/net' +require 'chef/win32/security' #wrapper around a subset of the NetUser* APIs. #nothing Chef specific, but not complete enough to be its own gem, so util for now. class Chef::Util::Windows::NetUser < Chef::Util::Windows private - - LogonUser = Windows::API.new('LogonUser', 'SSSLLP', 'I', 'advapi32') - - DOMAIN_GROUP_RID_USERS = 0x00000201 - - UF_SCRIPT = 0x000001 - UF_ACCOUNTDISABLE = 0x000002 - UF_PASSWD_CANT_CHANGE = 0x000040 - UF_NORMAL_ACCOUNT = 0x000200 - UF_DONT_EXPIRE_PASSWD = 0x010000 - - #[:symbol_name, default_val] - #default_val duals as field type - #array index duals as structure offset - - #OC-8391 - #Changing [:password, nil], to [:password, ""], - #if :password is set to nil, windows user creation api ignores the password policy applied - #thus initializing it with empty string value. - USER_INFO_3 = [ - [:name, nil], - [:password, ""], - [:password_age, 0], - [:priv, 0], #"The NetUserAdd and NetUserSetInfo functions ignore this member" - [:home_dir, nil], - [:comment, nil], - [:flags, UF_SCRIPT|UF_DONT_EXPIRE_PASSWD|UF_NORMAL_ACCOUNT], - [:script_path, nil], - [:auth_flags, 0], - [:full_name, nil], - [:user_comment, nil], - [:parms, nil], - [:workstations, nil], - [:last_logon, 0], - [:last_logoff, 0], - [:acct_expires, -1], - [:max_storage, -1], - [:units_per_week, 0], - [:logon_hours, nil], - [:bad_pw_count, 0], - [:num_logons, 0], - [:logon_server, nil], - [:country_code, 0], - [:code_page, 0], - [:user_id, 0], - [:primary_group_id, DOMAIN_GROUP_RID_USERS], - [:profile, nil], - [:home_dir_drive, nil], - [:password_expired, 0] - ] - - USER_INFO_3_TEMPLATE = - USER_INFO_3.collect { |field| field[1].class == Fixnum ? 'i' : 'L' }.join - - SIZEOF_USER_INFO_3 = #sizeof(USER_INFO_3) - USER_INFO_3.inject(0){|sum,item| - sum + (item[1].class == Fixnum ? 4 : PTR_SIZE) - } - - def user_info_3(args) - USER_INFO_3.collect { |field| - args.include?(field[0]) ? args[field[0]] : field[1] - } - end - - def user_info_3_pack(user) - user.collect { |v| - v.class == Fixnum ? v : str_to_ptr(multi_to_wide(v)) - }.pack(USER_INFO_3_TEMPLATE) + NetUser = Chef::ReservedNames::Win32::NetUser + Security = Chef::ReservedNames::Win32::Security + + USER_INFO_3_TRANSFORM = { + name: :usri3_name, + password: :usri3_password, + password_age: :usri3_password_age, + priv: :usri3_priv, + home_dir: :usri3_home_dir, + comment: :usri3_comment, + flags: :usri3_flags, + script_path: :usri3_script_path, + auth_flags: :usri3_auth_flags, + full_name: :usri3_full_name, + user_comment: :usri3_usr_comment, + parms: :usri3_parms, + workstations: :usri3_workstations, + last_logon: :usri3_last_logon, + last_logoff: :usri3_last_logoff, + acct_expires: :usri3_acct_expires, + max_storage: :usri3_max_storage, + units_per_week: :usri3_units_per_week, + logon_hours: :usri3_logon_hours, + bad_pw_count: :usri3_bad_pw_count, + num_logons: :usri3_num_logons, + logon_server: :usri3_logon_server, + country_code: :usri3_country_code, + code_page: :usri3_code_page, + user_id: :usri3_user_id, + primary_group_id: :usri3_primary_group_id, + profile: :usri3_profile, + home_dir_drive: :usri3_home_dir_drive, + password_expired: :usri3_password_expired, + } + + def transform_usri3(args) + args.inject({}) do |memo, (k,v)| + memo[USER_INFO_3_TRANSFORM[k]] = v + memo + end end - def user_info_3_unpack(buffer) - user = Hash.new - USER_INFO_3.each_with_index do |field,offset| - user[field[0]] = field[1].class == Fixnum ? - dword_to_i(buffer, offset) : lpwstr_to_s(buffer, offset) + def usri3_to_hash(usri3) + t = USER_INFO_3_TRANSFORM.invert + usri3.inject({}) do |memo, (k,v)| + memo[t[k]] = v + memo end - user end def set_info(args) - user = user_info_3(args) - buffer = user_info_3_pack(user) - rc = NetUserSetInfo.call(nil, @name, 3, buffer, nil) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + rc = NetUser::net_user_set_info_l3(nil, @username, transform_usri3(args)) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end @@ -120,49 +91,32 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows @name = multi_to_wide(username) end - LOGON32_PROVIDER_DEFAULT = 0 - LOGON32_LOGON_NETWORK = 3 + LOGON32_PROVIDER_DEFAULT = Security::LOGON32_PROVIDER_DEFAULT + LOGON32_LOGON_NETWORK = Security::LOGON32_LOGON_NETWORK #XXX for an extra painful alternative, see: http://support.microsoft.com/kb/180548 def validate_credentials(passwd) - token = 0.chr * PTR_SIZE - res = LogonUser.call(@username, nil, passwd, - LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, token) - if res == 0 + begin + token = Security::logon_user(@username, nil, passwd, + LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT) + return true + rescue Chef::Exceptions::Win32APIError return false end - ::Windows::Handle::CloseHandle.call(token.unpack('L')[0]) - return true end def get_info - ptr = 0.chr * PTR_SIZE - rc = NetUserGetInfo.call(nil, @name, 3, ptr) - - if rc == NERR_UserNotFound - raise Chef::Exceptions::UserIDNotFound, get_last_error(rc) - elsif rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + ui3 = NetUser::net_user_get_info_l3(nil, @username) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end - - ptr = ptr.unpack('L')[0] - buffer = 0.chr * SIZEOF_USER_INFO_3 - memcpy(buffer, ptr, buffer.size) - NetApiBufferFree(ptr) - user_info_3_unpack(buffer) + usri3_to_hash(ui3) end def add(args) - user = user_info_3(args) - buffer = user_info_3_pack(user) - - rc = NetUserAdd.call(nil, 3, buffer, rc) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - - #usri3_primary_group_id: - #"When you call the NetUserAdd function, this member must be DOMAIN_GROUP_RID_USERS" - NetLocalGroupAddMembers(nil, multi_to_wide("Users"), 3, buffer[0,PTR_SIZE], 1) + transformed_args = transform_usri3(args) + NetUser::net_user_add_l3(nil, transformed_args) + NetUser::net_local_group_add_member(nil, "Users", args[:name]) end def user_modify(&proc) @@ -182,15 +136,16 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows end def delete - rc = NetUserDel.call(nil, @name) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + NetUser::net_user_del(nil, @username) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end def disable_account user_modify do |user| - user[:flags] |= UF_ACCOUNTDISABLE + user[:flags] |= NetUser::UF_ACCOUNTDISABLE #This does not set the password to nil. It (for some reason) means to ignore updating the field. #See similar behavior for the logon_hours field documented at #http://msdn.microsoft.com/en-us/library/windows/desktop/aa371338%28v=vs.85%29.aspx @@ -200,7 +155,7 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows def enable_account user_modify do |user| - user[:flags] &= ~UF_ACCOUNTDISABLE + user[:flags] &= ~NetUser::UF_ACCOUNTDISABLE #This does not set the password to nil. It (for some reason) means to ignore updating the field. #See similar behavior for the logon_hours field documented at #http://msdn.microsoft.com/en-us/library/windows/desktop/aa371338%28v=vs.85%29.aspx @@ -209,6 +164,6 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows end def check_enabled - (get_info()[:flags] & UF_ACCOUNTDISABLE) != 0 + (get_info()[:flags] & NetUser::UF_ACCOUNTDISABLE) != 0 end end diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb index efa632f454..e9d273808a 100644 --- a/lib/chef/win32/api.rb +++ b/lib/chef/win32/api.rb @@ -67,7 +67,7 @@ class Chef # BaseTsd.h: #ifdef (_WIN64) host.typedef int HALF_PTR; #else host.typedef short HALF_PTR; host.typedef :ulong, :HACCEL # (L) Handle to an accelerator table. WinDef.h: #host.typedef HANDLE HACCEL; # See http://msdn.microsoft.com/en-us/library/ms645526%28VS.85%29.aspx - host.typedef :ulong, :HANDLE # (L) Handle to an object. WinNT.h: #host.typedef PVOID HANDLE; + host.typedef :size_t, :HANDLE # (L) Handle to an object. WinNT.h: #host.typedef PVOID HANDLE; # todo: Platform-dependent! Need to change to :uint64 for Win64 host.typedef :ulong, :HBITMAP # (L) Handle to a bitmap: http://msdn.microsoft.com/en-us/library/dd183377%28VS.85%29.aspx host.typedef :ulong, :HBRUSH # (L) Handle to a brush. http://msdn.microsoft.com/en-us/library/dd183394%28VS.85%29.aspx @@ -117,6 +117,7 @@ class Chef host.typedef :uint32, :LCID # Locale identifier. For more information, see Locales. host.typedef :uint32, :LCTYPE # Locale information type. For a list, see Locale Information Constants. host.typedef :uint32, :LGRPID # Language group identifier. For a list, see EnumLanguageGroupLocales. + host.typedef :pointer, :LMSTR # Pointer to null termiated string of unicode characters host.typedef :long, :LONG # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal. host.typedef :int32, :LONG32 # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal. host.typedef :int64, :LONG64 # 64-bit signed integer. The range is –9,223,372,036,854,775,808 through +...807 diff --git a/lib/chef/win32/api/net.rb b/lib/chef/win32/api/net.rb index eeb2b078a4..72caf46628 100644 --- a/lib/chef/win32/api/net.rb +++ b/lib/chef/win32/api/net.rb @@ -32,8 +32,24 @@ class Chef MAX_PREFERRED_LENGTH = 0xFFFF - NERR_Success = 0 - NERR_UserNotFound = 2221 + DOMAIN_GROUP_RID_USERS = 0x00000201 + + UF_SCRIPT = 0x000001 + UF_ACCOUNTDISABLE = 0x000002 + UF_PASSWD_CANT_CHANGE = 0x000040 + UF_NORMAL_ACCOUNT = 0x000200 + UF_DONT_EXPIRE_PASSWD = 0x010000 + + NERR_Success = 0 + NERR_InvalidComputer = 2351 + NERR_NotPrimary = 2226 + NERR_SpeGroupOp = 2234 + NERR_LastAdmin = 2452 + NERR_BadUsername = 2202 + NERR_BadPassword = 2203 + NERR_PasswordTooShort = 2245 + NERR_UserNotFound = 2221 + ERROR_ACCESS_DENIED = 5 ffi_lib "netapi32" @@ -67,6 +83,57 @@ class Chef :usri3_profile, :LPWSTR, :usri3_home_dir_drive, :LPWSTR, :usri3_password_expired, :DWORD + + def set(key, val) + val = if val.is_a? String + encoded = if val.encoding == Encoding::UTF_16LE + val + else + val.to_wstring + end + FFI::MemoryPointer.from_string(encoded) + else + val + end + self[key] = val + end + + def get(key) + if respond_to? key + send(key) + else + val = self[key] + if val.is_a? FFI::Pointer + if val.null? + nil + else + val.read_wstring + end + else + val + end + end + end + + def usri3_logon_hours + val = self[:usri3_logon_hours] + if !val.nil? && !val.null? + val.read_bytes(21) + else + nil + end + end + + def as_ruby + members.inject({}) do |memo, key| + memo[key] = get(key) + memo + end + end + end + + class LOCALGROUP_MEMBERS_INFO_3 < FFI::Struct + layout :lgrmi3_domainandname, :LPWSTR end # NET_API_STATUS NetUserEnum( @@ -85,6 +152,52 @@ class Chef # _In_ LPVOID Buffer # ); safe_attach_function :NetApiBufferFree, [ :LPVOID ], :DWORD + +#NET_API_STATUS NetUserAdd( + #_In_ LMSTR servername, + #_In_ DWORD level, + #_In_ LPBYTE buf, + #_Out_ LPDWORD parm_err +#); + safe_attach_function :NetUserAdd, [:LMSTR, :DWORD, :LPBYTE, :LPDWORD ], :DWORD + +#NET_API_STATUS NetLocalGroupAddMembers( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR groupname, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _In_ DWORD totalentries +#); + safe_attach_function :NetLocalGroupAddMembers, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD ], :DWORD + +#NET_API_STATUS NetUserGetInfo( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username, +# _In_ DWORD level, +# _Out_ LPBYTE *bufptr +#); + safe_attach_function :NetUserGetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE], :DWORD + +#NET_API_STATUS NetApiBufferFree( +# _In_ LPVOID Buffer +#); + safe_attach_function :NetApiBufferFree, [:LPVOID], :DWORD + +#NET_API_STATUS NetUserSetInfo( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _Out_ LPDWORD parm_err +#); + safe_attach_function :NetUserSetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD], :DWORD + +#NET_API_STATUS NetUserDel( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username +#); + safe_attach_function :NetUserDel, [:LPCWSTR, :LPCWSTR], :DWORD + end end end diff --git a/lib/chef/win32/api/security.rb b/lib/chef/win32/api/security.rb index 229f2ace10..95e7f4aab7 100644 --- a/lib/chef/win32/api/security.rb +++ b/lib/chef/win32/api/security.rb @@ -193,6 +193,20 @@ class Chef MAXDWORD = 0xffffffff + # LOGON32 constants for LogonUser + LOGON32_LOGON_INTERACTIVE = 2; + LOGON32_LOGON_NETWORK = 3; + LOGON32_LOGON_BATCH = 4; + LOGON32_LOGON_SERVICE = 5; + LOGON32_LOGON_UNLOCK = 7; + LOGON32_LOGON_NETWORK_CLEARTEXT = 8; + LOGON32_LOGON_NEW_CREDENTIALS = 9; + + LOGON32_PROVIDER_DEFAULT = 0; + LOGON32_PROVIDER_WINNT35 = 1; + LOGON32_PROVIDER_WINNT40 = 2; + LOGON32_PROVIDER_WINNT50 = 3; + ############################################### # Win32 API Bindings ############################################### @@ -405,6 +419,8 @@ class Chef safe_attach_function :SetSecurityDescriptorOwner, [ :pointer, :pointer, :BOOL ], :BOOL 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 + end end end diff --git a/lib/chef/win32/api/unicode.rb b/lib/chef/win32/api/unicode.rb index 0b2cb09a6b..2e3a599f0a 100644 --- a/lib/chef/win32/api/unicode.rb +++ b/lib/chef/win32/api/unicode.rb @@ -139,7 +139,7 @@ int WideCharToMultiByte( ustring = (ustring + "").force_encoding('UTF-8') if ustring.respond_to?(:force_encoding) && ustring.encoding.name != "UTF-8" # ensure we have the double-null termination Windows Wide likes - ustring = ustring + "\000\000" if ustring[-1].chr != "\000" + ustring = ustring + "\000\000" if ustring.length == 0 or ustring[-1].chr != "\000" # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode ustring = begin diff --git a/lib/chef/win32/net.rb b/lib/chef/win32/net.rb new file mode 100644 index 0000000000..0bb4d12bd4 --- /dev/null +++ b/lib/chef/win32/net.rb @@ -0,0 +1,214 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright 2015 Chef Software +# 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/win32/api/net' +require 'chef/win32/error' +require 'chef/mixin/wstring' + +class Chef + module ReservedNames::Win32 + class NetUser + include Chef::ReservedNames::Win32::API::Error + extend Chef::ReservedNames::Win32::API::Error + + include Chef::ReservedNames::Win32::API::Net + extend Chef::ReservedNames::Win32::API::Net + + include Chef::Mixin::WideString + extend Chef::Mixin::WideString + + def self.default_user_info_3 + ui3 = USER_INFO_3.new.tap do |s| + { usri3_name: nil, + usri3_password: nil, + usri3_password_age: 0, + usri3_priv: 0, + usri3_home_dir: nil, + usri3_comment: nil, + usri3_flags: UF_SCRIPT|UF_DONT_EXPIRE_PASSWD|UF_NORMAL_ACCOUNT, + usri3_script_path: nil, + usri3_auth_flags: 0, + usri3_full_name: nil, + usri3_usr_comment: nil, + usri3_parms: nil, + usri3_workstations: nil, + usri3_last_logon: 0, + usri3_last_logoff: 0, + usri3_acct_expires: -1, + usri3_max_storage: -1, + usri3_units_per_week: 0, + usri3_logon_hours: nil, + usri3_bad_pw_count: 0, + usri3_num_logons: 0, + usri3_logon_server: nil, + usri3_country_code: 0, + usri3_code_page: 0, + usri3_user_id: 0, + usri3_primary_group_id: DOMAIN_GROUP_RID_USERS, + usri3_profile: nil, + usri3_home_dir_drive: nil, + usri3_password_expired: 0 + }.each do |(k,v)| + s.set(k, v) + end + end + end + + def self.net_api_error!(code) + msg = case code + when NERR_InvalidComputer + "The user does not have access to the requested information." + when NERR_NotPrimary + "The operation is allowed only on the primary domain controller of the domain." + when NERR_SpeGroupOp + "This operation is not allowed on this special group." + when NERR_LastAdmin + "This operation is not allowed on the last administrative account." + when NERR_BadUsername + "The user name or group name parameter is invalid." + when NERR_BadPassword + "The password parameter is invalid." + when NERR_UserNotFound + raise Chef::Exceptions::UserIDNotFound, code + when NERR_PasswordTooShort + <<END +The password is shorter than required. (The password could also be too +long, be too recent in its change history, not have enough unique characters, +or not meet another password policy requirement.) +END + when ERROR_ACCESS_DENIED + "The user does not have access to the requested information." + else + "Received unknown error code (#{code})" + end + + formatted_message = "" + formatted_message << "---- Begin Win32 API output ----\n" + formatted_message << "Net Api Error Code: #{code}\n" + formatted_message << "Net Api Error Message: #{msg}\n" + formatted_message << "---- End Win32 API output ----\n" + + raise Chef::Exceptions::Win32APIError, msg + "\n" + formatted_message + end + + def self.net_user_add_l3(server_name, args) + buf = default_user_info_3 + + args.each do |k, v| + buf.set(k, v) + end + + server_name = wstring(server_name) + + rc = NetUserAdd(server_name, 3, buf, nil) + if rc != NERR_Success + if Chef::ReservedNames::Win32::Error.get_last_error != 0 + Chef::ReservedNames::Win32::Error.raise! + else + net_api_error!(rc) + end + end + end + + def self.net_user_get_info_l3(server_name, user_name) + server_name = wstring(server_name) + user_name = wstring(user_name) + + ui3_p = FFI::MemoryPointer.new(:pointer) + + rc = NetUserGetInfo(server_name, user_name, 3, ui3_p) + + if rc != NERR_Success + if Chef::ReservedNames::Win32::Error.get_last_error != 0 + Chef::ReservedNames::Win32::Error.raise! + else + net_api_error!(rc) + end + end + + ui3 = USER_INFO_3.new(ui3_p.read_pointer).as_ruby + + rc = NetApiBufferFree(ui3_p.read_pointer) + + if rc != NERR_Success + if Chef::ReservedNames::Win32::Error.get_last_error != 0 + Chef::ReservedNames::Win32::Error.raise! + else + net_api_error!(rc) + end + end + + ui3 + end + + def self.net_user_set_info_l3(server_name, user_name, info) + buf = default_user_info_3 + + info.each do |k, v| + buf.set(k, v) + end + + server_name = wstring(server_name) + user_name = wstring(user_name) + + rc = NetUserSetInfo(server_name, user_name, 3, buf, nil) + if rc != NERR_Success + if Chef::ReservedNames::Win32::Error.get_last_error != 0 + Chef::ReservedNames::Win32::Error.raise! + else + net_api_error!(rc) + end + end + end + + def self.net_user_del(server_name, user_name) + server_name = wstring(server_name) + user_name = wstring(user_name) + + rc = NetUserDel(server_name, user_name) + if rc != NERR_Success + if Chef::ReservedNames::Win32::Error.get_last_error != 0 + Chef::ReservedNames::Win32::Error.raise! + else + net_api_error!(rc) + end + end + end + + def self.net_local_group_add_member(server_name, group_name, domain_user) + server_name = wstring(server_name) + group_name = wstring(group_name) + domain_user = wstring(domain_user) + + buf = LOCALGROUP_MEMBERS_INFO_3.new + buf[:lgrmi3_domainandname] = FFI::MemoryPointer.from_string(domain_user) + + rc = NetLocalGroupAddMembers(server_name, group_name, 3, buf, 1) + + if rc != NERR_Success + if Chef::ReservedNames::Win32::Error.get_last_error != 0 + Chef::ReservedNames::Win32::Error.raise! + else + net_api_error!(rc) + end + end + end + + end + end +end diff --git a/lib/chef/win32/security.rb b/lib/chef/win32/security.rb index 3902d8caaf..a5a845e55c 100644 --- a/lib/chef/win32/security.rb +++ b/lib/chef/win32/security.rb @@ -22,6 +22,7 @@ require 'chef/win32/memory' require 'chef/win32/process' require 'chef/win32/unicode' require 'chef/win32/security/token' +require 'chef/mixin/wstring' class Chef module ReservedNames::Win32 @@ -31,6 +32,8 @@ class Chef include Chef::ReservedNames::Win32::API::Security extend Chef::ReservedNames::Win32::API::Security extend Chef::ReservedNames::Win32::API::Macros + include Chef::Mixin::WideString + extend Chef::Mixin::WideString def self.access_check(security_descriptor, token, desired_access, generic_mapping) token_handle = token.handle.handle @@ -543,6 +546,18 @@ class Chef success && (elevation_result.read_ulong != 0) end end + + def self.logon_user(username, domain, password, logon_type, logon_provider) + username = wstring(username) + domain = wstring(domain) + password = wstring(password) + + token = FFI::Buffer.new(:pointer) + unless LogonUserW(username, domain, password, logon_type, logon_provider, token) + Chef::ReservedNames::Win32::Error.raise! + end + Token.new(Handle.new(token.read_pointer)) + end end end end diff --git a/spec/functional/resource/user/windows_spec.rb b/spec/functional/resource/user/windows_spec.rb new file mode 100644 index 0000000000..5e68478b34 --- /dev/null +++ b/spec/functional/resource/user/windows_spec.rb @@ -0,0 +1,125 @@ +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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/shell_out' + +describe Chef::Provider::User::Windows, :windows_only do + include Chef::Mixin::ShellOut + + let(:username) { 'ChefFunctionalTest' } + let(:password) { SecureRandom.uuid } + + let(:node) do + n = Chef::Node.new + n.consume_external_attrs(OHAI_SYSTEM.data.dup, {}) + n + end + + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:new_resource) do + Chef::Resource::User.new(username, run_context).tap do |r| + r.provider(Chef::Provider::User::Windows) + r.password(password) + end + end + + def delete_user(u) + shell_out("net user #{u} /delete") + end + + before do + delete_user(username) + end + + describe 'action :create' do + it 'creates a user when a username and password are given' do + new_resource.run_action(:create) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").exitstatus).to eq(0) + end + + it 'reports no changes if there are no changes needed' do + new_resource.run_action(:create) + new_resource.run_action(:create) + expect(new_resource).not_to be_updated_by_last_action + end + + it 'allows chaning the password' do + new_resource.run_action(:create) + new_resource.password(SecureRandom.uuid) + new_resource.run_action(:create) + expect(new_resource).to be_updated_by_last_action + end + end + + describe 'action :remove' do + before do + new_resource.run_action(:create) + end + + it 'deletes the user' do + new_resource.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").exitstatus).to eq(2) + end + + it 'is idempotent' do + new_resource.run_action(:remove) + new_resource.run_action(:remove) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe 'action :lock' do + before do + new_resource.run_action(:create) + end + + it 'locks the user account' do + new_resource.run_action(:lock) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").stdout).to match(/Account active\s*No/) + end + + it 'is idempotent' do + new_resource.run_action(:lock) + new_resource.run_action(:lock) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe 'action :unlock' do + before do + new_resource.run_action(:create) + new_resource.run_action(:lock) + end + + it 'unlocks the user account' do + new_resource.run_action(:unlock) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").stdout).to match(/Account active\s*Yes/) + end + + it 'is idempotent' do + new_resource.run_action(:unlock) + new_resource.run_action(:unlock) + expect(new_resource).not_to be_updated_by_last_action + end + end +end |