summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJay Mundrawala <jdmundrawala@gmail.com>2015-05-15 11:40:41 -0500
committerJay Mundrawala <jdmundrawala@gmail.com>2015-05-15 11:40:41 -0500
commiteaffc0a99e962cb7e055b5fd960a9e586df4a184 (patch)
treef47755b4628f41440b581de7df3cab12983dea1c
parent81e4fb0c1f10b46e92226c60b71a79c1c931cd3b (diff)
parent5251d6c8d8fc48ddb1279294b259a39ae9eddab6 (diff)
downloadchef-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.rb31
-rw-r--r--lib/chef/util/windows/net_user.rb191
-rw-r--r--lib/chef/win32/api.rb3
-rw-r--r--lib/chef/win32/api/net.rb117
-rw-r--r--lib/chef/win32/api/security.rb16
-rw-r--r--lib/chef/win32/api/unicode.rb2
-rw-r--r--lib/chef/win32/net.rb214
-rw-r--r--lib/chef/win32/security.rb15
-rw-r--r--spec/functional/resource/user/windows_spec.rb125
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