|
|
#include "stdafx.h"
#include "userinfo.h"
#pragma hdrstop
/*******************************************************************
CUserInfo implementation *******************************************************************/
CUserInfo::CUserInfo() { m_fHaveExtraUserInfo = FALSE; m_psid = NULL; }
CUserInfo::~CUserInfo() { if (m_psid != NULL) LocalFree(m_psid);
ZeroPassword(); }
HRESULT CUserInfo::Reload(BOOL fLoadExtraInfo /* = NULL */) { // Initialize the structure and add it to the head of the list
DWORD cchUsername = ARRAYSIZE(m_szUsername); DWORD cchDomain = ARRAYSIZE(m_szDomain);
if (LookupAccountSid(NULL, m_psid, m_szUsername, &cchUsername, m_szDomain, &cchDomain, &m_sUse)) { m_fHaveExtraUserInfo = FALSE; if (fLoadExtraInfo) GetExtraUserInfo();
SetUserType(); SetAccountDisabled(); return SetLocalGroups(); } return E_FAIL; }
HRESULT CUserInfo::SetLocalGroups() { TCHAR szDomainUser[MAX_DOMAIN + MAX_USER + 2]; ::MakeDomainUserString(m_szDomain, m_szUsername, szDomainUser, ARRAYSIZE(szDomainUser));
DWORD dwEntriesRead; DWORD dwTotalEntries; BOOL fMore = TRUE; DWORD iNextGroupName = 0; BOOL fAddElipses = FALSE;
HRESULT hr = S_OK; while (fMore) { LOCALGROUP_USERS_INFO_0* prglgrui0; NET_API_STATUS status = NetUserGetLocalGroups(NULL, szDomainUser, 0, 0, (BYTE**) &prglgrui0, 2048, &dwEntriesRead, &dwTotalEntries);
if ((status == NERR_Success) || (status == ERROR_MORE_DATA)) { for (DWORD i = 0; i < dwEntriesRead; i++) { DWORD iThisGroupName = iNextGroupName; iNextGroupName += lstrlen(prglgrui0[i].lgrui0_name) + 2;
if (iNextGroupName < (ARRAYSIZE(m_szGroups) - 1)) { lstrcpy(&m_szGroups[iThisGroupName], prglgrui0[i].lgrui0_name); lstrcpy(&m_szGroups[iNextGroupName - 2], TEXT("; ")); } else { fAddElipses = TRUE; if (iThisGroupName + 3 >= (ARRAYSIZE(m_szGroups))) iThisGroupName -= 3;
lstrcpy(&m_szGroups[iThisGroupName], TEXT("..."));
// No need to read more; we're out o' buffer
fMore = FALSE; } } NetApiBufferFree((void*) prglgrui0); } else { hr = E_FAIL; }
if (status != ERROR_MORE_DATA) { fMore = FALSE; } }
// There is an extra ';' at the end. Nuke it
if (!fAddElipses && ((iNextGroupName - 2) < (ARRAYSIZE(m_szGroups)))) { m_szGroups[iNextGroupName - 2] = TEXT('\0'); }
// Absolutely guarantee the string ends in a null
m_szGroups[ARRAYSIZE(m_szGroups) - 1] = TEXT('\0');
return hr; }
HRESULT CUserInfo::Load(PSID psid, BOOL fLoadExtraInfo /* = NULL */) { CUserInfo(); // Nuke the record first
// Make a copy of the SID
DWORD cbSid = GetLengthSid(psid); m_psid = (PSID) LocalAlloc(NULL, cbSid); if (!m_psid) return E_OUTOFMEMORY;
CopySid(cbSid, m_psid, psid); return Reload(fLoadExtraInfo); }
HRESULT CUserInfo::Create(HWND hwndError, GROUPPSEUDONYM grouppseudonym) { NET_API_STATUS status = NERR_Success;
CWaitCursor cur;
HRESULT hr = E_FAIL; if (m_userType == CUserInfo::LOCALUSER) { // Fill in the big, ugly structure containing information about our new user
USER_INFO_2 usri2 = {0}; usri2.usri2_name = T2W(m_szUsername);
// Reveal the password
RevealPassword();
usri2.usri2_password = T2W(m_szPasswordBuffer); usri2.usri2_priv = USER_PRIV_USER; usri2.usri2_comment = T2W(m_szComment);
if (m_szPasswordBuffer[0] == TEXT('\0')) usri2.usri2_flags = UF_NORMAL_ACCOUNT | UF_SCRIPT | UF_PASSWD_NOTREQD; else usri2.usri2_flags = UF_NORMAL_ACCOUNT | UF_SCRIPT;
usri2.usri2_full_name = T2W(m_szFullName); usri2.usri2_acct_expires = TIMEQ_FOREVER; usri2.usri2_max_storage = USER_MAXSTORAGE_UNLIMITED;
TCHAR szCountryCode[7]; if (0 < GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_ICOUNTRY, szCountryCode, ARRAYSIZE(szCountryCode))) { usri2.usri2_country_code = (DWORD) StrToLong(szCountryCode); }
usri2.usri2_code_page = GetACP();
// Create the user
status = NetUserAdd(NULL, 2, (BYTE*) &usri2, NULL);
// Hide the password
HidePassword();
switch (status) { case NERR_Success: hr = S_OK; break; case NERR_PasswordTooShort: ::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_USR_CREATE_PASSWORDTOOSHORT_ERROR, MB_ICONERROR | MB_OK);
break; case NERR_GroupExists: ::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_USR_CREATE_GROUPEXISTS_ERROR, MB_ICONERROR | MB_OK); break;
case NERR_UserExists: ::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_USR_CREATE_USEREXISTS_ERROR, MB_ICONERROR | MB_OK, m_szUsername); break;
default: { TCHAR szMessage[512];
if (!FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, (DWORD) status, 0, szMessage, ARRAYSIZE(szMessage), NULL)) LoadString(g_hinst, IDS_ERR_UNEXPECTED, szMessage, ARRAYSIZE(szMessage));
::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_USERCREATE_GENERICERROR, MB_ICONERROR | MB_OK, szMessage); break; } } } else { hr = S_OK; // m_userType == DOMAINUSER or GROUP
}
if (SUCCEEDED(hr)) { hr = ChangeLocalGroups(hwndError, grouppseudonym); if (SUCCEEDED(hr)) { // User type may have been updated by ChangeLocalGroups - // relect that!
SetUserType(); } }
return hr; }
HRESULT CUserInfo::Remove() { CWaitCursor cur; if (m_userType == CUserInfo::LOCALUSER) { // Try to actually remove this local user (this may fail!)
NET_API_STATUS status = NetUserDel(NULL, m_szUsername); if (status != NERR_Success) { return E_FAIL; } } else { // We can only delete local users. For all others the best we can do is
// remove them from all local groups
return RemoveFromLocalGroups(); } return S_OK; }
HRESULT CUserInfo::InitializeForNewUser() { CUserInfo(); // Nuke the record first
m_fHaveExtraUserInfo = TRUE; m_sUse = SidTypeUser; m_userType = LOCALUSER;
return S_OK; }
HRESULT CUserInfo::RemoveFromLocalGroups() { // Create a data structure we'll need to pass to NetLocalGroupxxx functions
TCHAR szDomainUser[MAX_USER + MAX_DOMAIN + 2]; ::MakeDomainUserString(m_szDomain, m_szUsername, szDomainUser, ARRAYSIZE(szDomainUser)); LOCALGROUP_MEMBERS_INFO_3 rglgrmi3[] = {{szDomainUser}};
// Try and remove the user/group from ALL local groups. The reason
// for this is the NetUserGetLocalGroups won't work for groups, even
// well-known ones. For instance, it will fail for "Everyone" even
// though "Everyone" may very well belong to local groups.
DWORD_PTR dwResumeHandle = 0;
BOOL fMoreData = TRUE; while (fMoreData) { DWORD dwEntriesRead; DWORD dwTotalEntries; LOCALGROUP_INFO_0* plgrpi0 = NULL;
NET_API_STATUS status = NetLocalGroupEnum(NULL, 0, (BYTE**)&plgrpi0, 8192, &dwEntriesRead, &dwTotalEntries, &dwResumeHandle); if ((status == NERR_Success) || (status == ERROR_MORE_DATA)) { for (DWORD i = 0; i < dwEntriesRead; i ++) { status = NetLocalGroupDelMembers(NULL, plgrpi0[i].lgrpi0_name, 3, (BYTE*) rglgrmi3, ARRAYSIZE(rglgrmi3)); }
if (dwEntriesRead == dwTotalEntries) { fMoreData = FALSE; }
NetApiBufferFree(plgrpi0); } else { fMoreData = FALSE; } } return S_OK; }
HRESULT CUserInfo::SetUserType() { TCHAR szComputerName[MAX_COMPUTERNAME + 1]; DWORD cchComputerName = ARRAYSIZE(szComputerName); ::GetComputerName(szComputerName, &cchComputerName);
// Figure out what type of user we're talking about
if ((m_sUse == SidTypeWellKnownGroup) || (m_sUse == SidTypeGroup)) { m_userType = GROUP; } else { // User type - see if this user is a local one
if ((m_szDomain[0] == TEXT('\0')) || (StrCmpI(m_szDomain, szComputerName) == 0)) { m_userType = LOCALUSER; // Local user
} else { m_userType = DOMAINUSER; // User is a network one
} }
return S_OK; }
HRESULT CUserInfo::SetAccountDisabled() { m_fAccountDisabled = FALSE;
USER_INFO_1* pusri1 = NULL; NET_API_STATUS status = NetUserGetInfo(NULL, T2W(m_szUsername), 1, (BYTE**)&pusri1); if (NERR_Success == status) { if (pusri1->usri1_flags & UF_ACCOUNTDISABLE) { m_fAccountDisabled = TRUE; }
NetApiBufferFree(pusri1); }
return S_OK; }
HRESULT CUserInfo::GetExtraUserInfo() { USES_CONVERSION;
CWaitCursor cur;
if (!m_fHaveExtraUserInfo) { NET_API_STATUS status; USER_INFO_11* pusri11 = NULL;
// Even if we fail to the info, we only want to try once since it may take a long time
m_fHaveExtraUserInfo = TRUE;
// Get the name of the domain's DC if we aren't talking about a local user
#ifdef _0 // Turns out this is REALLY slow to fail if the DsGetDcName call fails
if (m_userType != LOCALUSER) {
DOMAIN_CONTROLLER_INFO* pDCInfo; DWORD dwErr = DsGetDcName(NULL, m_szDomain, NULL, NULL, DS_IS_FLAT_NAME, &pDCInfo); if (dwErr != NO_ERROR) return E_FAIL;
// Get the user's detailed information (we really need full name and comment)
// Need to use level 11 here since this allows a domain user to query their
// information
status = NetUserGetInfo(T2W(pDCInfo->DomainControllerName), T2W(m_szUsername), 11, (BYTE**)&pusri11); NetApiBufferFree(pDCInfo); } else #endif //0
{ status = NetUserGetInfo(NULL, T2W(m_szUsername), 11, (BYTE**)&pusri11); }
if (status != NERR_Success) return E_FAIL;
lstrcpyn(m_szComment, W2T(pusri11->usri11_comment), ARRAYSIZE(m_szComment)); lstrcpyn(m_szFullName, W2T(pusri11->usri11_full_name), ARRAYSIZE(m_szFullName));
NetApiBufferFree(pusri11); } return S_OK; }
// ChangeLocalGroups
// Removes the specified user from all current local groups and adds them to the
// SINGLE local group specified in pUserInfo->szGroups
HRESULT CUserInfo::ChangeLocalGroups(HWND hwndError, GROUPPSEUDONYM grouppseudonym) { // First, remove the user from all existing local groups
HRESULT hr = RemoveFromLocalGroups(); if (SUCCEEDED(hr)) { TCHAR szDomainAndUser[MAX_USER + MAX_DOMAIN + 2]; ::MakeDomainUserString(m_szDomain, m_szUsername, szDomainAndUser, ARRAYSIZE(szDomainAndUser));
// Create a data structure we'll need to pass to NetLocalGroupxxx functions
LOCALGROUP_MEMBERS_INFO_3 rglgrmi3[] = {{szDomainAndUser}}; // Now add the user to the SINGLE localgroup that should be specified in
// m_szGroups; Assert this is the case!
NET_API_STATUS status = NetLocalGroupAddMembers(NULL, T2W(m_szGroups), 3, (BYTE*) rglgrmi3, ARRAYSIZE(rglgrmi3)); if (status == NERR_Success) { // We may now need to get the user's SID. This happens if we are
// changing local groups for a domain user and we couldn't read their
// SID since they weren't in the local SAM.
DWORD cchDomain = ARRAYSIZE(m_szDomain); hr = ::AttemptLookupAccountName(szDomainAndUser, &m_psid, m_szDomain, &cchDomain, &m_sUse); if (FAILED(hr)) { ::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_USR_CREATE_MISC_ERROR, MB_ICONERROR | MB_OK); } } else { switch(status) { case ERROR_NO_SUCH_MEMBER: { switch (grouppseudonym) { case RESTRICTED: ::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_BADRESTRICTEDUSER, MB_ICONERROR | MB_OK, szDomainAndUser); break; case STANDARD: ::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_BADSTANDARDUSER, MB_ICONERROR | MB_OK, szDomainAndUser); break; case USEGROUPNAME: default: ::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_USR_CHANGEGROUP_ERR, MB_ICONERROR | MB_OK, szDomainAndUser, m_szGroups); break; } break; }
default: { TCHAR szMessage[512];
if (!FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, (DWORD) status, 0, szMessage, ARRAYSIZE(szMessage), NULL)) LoadString(g_hinst, IDS_ERR_UNEXPECTED, szMessage, ARRAYSIZE(szMessage));
::DisplayFormatMessage(hwndError, IDS_USR_APPLET_CAPTION, IDS_ERR_ADDUSER, MB_ICONERROR | MB_OK, szMessage); } } hr = E_FAIL; } } return hr;; }
HRESULT CUserInfo::UpdateUsername(LPTSTR pszNewUsername) { CWaitCursor cur;
USER_INFO_0 usri0; usri0.usri0_name = T2W(pszNewUsername);
DWORD dwErr; NET_API_STATUS status = NetUserSetInfo(NULL, T2W(m_szUsername), 0, (BYTE*) &usri0, &dwErr); if (status != NERR_Success) return E_FAIL;
lstrcpyn(m_szUsername, pszNewUsername, ARRAYSIZE(m_szUsername)); return S_OK; }
HRESULT CUserInfo::UpdateFullName(LPTSTR pszFullName) { CWaitCursor cur;
USER_INFO_1011 usri1011; usri1011.usri1011_full_name = T2W(pszFullName); DWORD dwErr;
NET_API_STATUS status = NetUserSetInfo(NULL, T2W(m_szUsername), 1011, (BYTE*) &usri1011, &dwErr); if (status != NERR_Success) return E_FAIL;
lstrcpyn(m_szFullName, pszFullName, ARRAYSIZE(m_szFullName)); return S_OK; }
HRESULT CUserInfo::UpdatePassword(BOOL* pfBadPWFormat) { CWaitCursor cur;
RevealPassword();
USER_INFO_1003 usri1003; usri1003.usri1003_password = T2W(m_szPasswordBuffer);
DWORD dwErr; NET_API_STATUS status = NetUserSetInfo(NULL, T2W(m_szUsername), 1003, (BYTE*)&usri1003, &dwErr);
ZeroPassword(); // Kill the password
if (pfBadPWFormat != NULL) *pfBadPWFormat = (status == NERR_PasswordTooShort);
return (status == NERR_Success) ? S_OK:E_FAIL; }
HRESULT CUserInfo::UpdateGroup(HWND hwndError, LPTSTR pszGroup, GROUPPSEUDONYM grouppseudonym) { CWaitCursor cur;
// Save the old group before we change it
TCHAR szOldGroups[MAX_GROUP * 2 + 3]; lstrcpyn(szOldGroups, m_szGroups, ARRAYSIZE(szOldGroups));
// Try to change the local group
lstrcpyn(m_szGroups, pszGroup, ARRAYSIZE(m_szGroups)); HRESULT hr = ChangeLocalGroups(hwndError, grouppseudonym);
if (FAILED(hr)) lstrcpyn(m_szGroups, szOldGroups, ARRAYSIZE(m_szGroups)); // Restore the old group in case of failure
return hr; }
HRESULT CUserInfo::UpdateDescription(LPTSTR pszDescription) { CWaitCursor cur;
USER_INFO_1007 usri1007; usri1007.usri1007_comment = T2W(pszDescription);
DWORD dwErr; NET_API_STATUS status = NetUserSetInfo(NULL, T2W(m_szUsername), 1007, (BYTE*) &usri1007, &dwErr);
if (status != NERR_Success) return E_FAIL;
lstrcpyn(m_szComment, pszDescription, ARRAYSIZE(m_szComment)); return S_OK; }
void CUserInfo::HidePassword() { m_Seed = 0; RtlInitUnicodeString(&m_Password, m_szPasswordBuffer); RtlRunEncodeUnicodeString(&m_Seed, &m_Password); }
void CUserInfo::RevealPassword() { RtlRunDecodeUnicodeString(m_Seed, &m_Password); }
void CUserInfo::ZeroPassword() { ZeroMemory(m_szPasswordBuffer, ARRAYSIZE(m_szPasswordBuffer)); }
/*******************************************************************
CUserListLoader implementation *******************************************************************/
CUserListLoader::CUserListLoader() { m_hInitDoneEvent = CreateEvent(NULL, TRUE, TRUE, NULL); }
CUserListLoader::~CUserListLoader() { EndInitNow(); WaitForSingleObject(m_hInitDoneEvent, INFINITE); }
BOOL CUserListLoader::HasUserBeenAdded(PSID psid) { // Walk the user list looking for a given username and domain
CUserInfo* pUserInfo = NULL; BOOL fFound = FALSE; for (int i = 0; i < m_dpaAddedUsers.GetPtrCount(); i ++) { pUserInfo = m_dpaAddedUsers.GetPtr(i); if (pUserInfo->m_psid && psid && EqualSid(pUserInfo->m_psid, psid)) { fFound = TRUE; break; } } return fFound; }
HRESULT CUserListLoader::Initialize(HWND hwndUserListPage) { USES_CONVERSION; // Tell any existing init thread to exit and wait for it to do so
m_fEndInitNow = TRUE; WaitForSingleObject(m_hInitDoneEvent, INFINITE); ResetEvent(m_hInitDoneEvent);
m_fEndInitNow = FALSE; m_hwndUserListPage = hwndUserListPage;
// Launch the initialize thread
DWORD InitThreadId; HANDLE hInitThread = CreateThread(NULL, 0, CUserListLoader::InitializeThread, (LPVOID) this, 0, &InitThreadId); if (hInitThread == NULL) return E_FAIL;
CloseHandle(hInitThread); // Let this thread go about his/her merry way
return S_OK; }
HRESULT CUserListLoader::UpdateFromLocalGroup(LPWSTR szLocalGroup) { USES_CONVERSION; DWORD_PTR dwResumeHandle = 0;
HRESULT hr = S_OK; BOOL fBreakLoop = FALSE; while(!fBreakLoop) { LOCALGROUP_MEMBERS_INFO_0* prgMembersInfo; DWORD dwEntriesRead = 0; DWORD dwTotalEntries = 0;
NET_API_STATUS status = NetLocalGroupGetMembers(NULL, szLocalGroup, 0, (BYTE**) &prgMembersInfo, 8192, &dwEntriesRead, &dwTotalEntries, &dwResumeHandle); if ((status == NERR_Success) || (status == ERROR_MORE_DATA)) { // for all the members in the structure, lets add them
DWORD iMember; for (iMember = 0; ((iMember < dwEntriesRead) && (!m_fEndInitNow)); iMember ++) { hr = AddUserInformation(prgMembersInfo[iMember].lgrmi0_sid); }
NetApiBufferFree((BYTE*) prgMembersInfo);
// See if we can avoid calling NetLocalGroupGetMembers again
fBreakLoop = ((dwEntriesRead == dwTotalEntries) || m_fEndInitNow); } else { fBreakLoop = TRUE; hr = E_FAIL; } } return hr; }
HRESULT CUserListLoader::AddUserInformation(PSID psid) { // Only add this user if we haven't already
if (!HasUserBeenAdded(psid)) { CUserInfo *pUserInfo = new CUserInfo; if (!pUserInfo) return E_OUTOFMEMORY;
if (SUCCEEDED(pUserInfo->Load(psid, FALSE))) { PostMessage(m_hwndUserListPage, WM_ADDUSERTOLIST, (WPARAM) FALSE, (LPARAM)pUserInfo); m_dpaAddedUsers.AppendPtr(pUserInfo); // Remember we've added this user
} } return S_OK; }
DWORD CUserListLoader::InitializeThread(LPVOID pvoid) { CUserListLoader *pthis = (CUserListLoader*)pvoid;
// First delete any old list
PostMessage(GetDlgItem(pthis->m_hwndUserListPage, IDC_USER_LIST), LVM_DELETEALLITEMS, 0, 0);
// Create a list of adready-added users so we don't add a user twice
// if they're in multiple local groups
if (pthis->m_dpaAddedUsers.Create(8)) { // Read each local group
DWORD_PTR dwResumeHandle = 0;
BOOL fBreakLoop = FALSE; while (!fBreakLoop) { DWORD dwEntriesRead = 0; DWORD dwTotalEntries = 0; LOCALGROUP_INFO_1* prgGroupInfo; NET_API_STATUS status = NetLocalGroupEnum(NULL, 1, (BYTE**) &prgGroupInfo, 8192, &dwEntriesRead, &dwTotalEntries, &dwResumeHandle);
if ((status == NERR_Success) || (status == ERROR_MORE_DATA)) { // We got some local groups - add information for all users in these local groups to our list
DWORD iGroup; for (iGroup = 0; ((iGroup < dwEntriesRead) && (!pthis->m_fEndInitNow)); iGroup ++) { pthis->UpdateFromLocalGroup(prgGroupInfo[iGroup].lgrpi1_name); }
NetApiBufferFree((BYTE*) prgGroupInfo); // Maybe we don't have to try NetLocalGroupEnum again (if we got all the groups)
fBreakLoop = ((dwEntriesRead == dwTotalEntries) || pthis->m_fEndInitNow); } else { fBreakLoop = TRUE; } }
// Its okay to orphan any CUserInfo pointers stored here; they'll be
// released when the ulistpg exits or reinits.
pthis->m_dpaAddedUsers.Destroy(); }
SetEvent(pthis->m_hInitDoneEvent); SetCursor(LoadCursor(NULL, IDC_ARROW)); return 0; }
// User utility functions
BOOL UserAlreadyHasPermission(CUserInfo* pUserInfo, HWND hwndMsgParent) { TCHAR szDomainUser[MAX_DOMAIN + MAX_USER + 2]; ::MakeDomainUserString(pUserInfo->m_szDomain, pUserInfo->m_szUsername, szDomainUser, ARRAYSIZE(szDomainUser));
BOOL fHasPermission = FALSE;
// See if this user is already in local groups on this machine
DWORD dwEntriesRead, dwIgnore2; LOCALGROUP_USERS_INFO_0* plgrui0 = NULL; if (NERR_Success == NetUserGetLocalGroups(NULL, szDomainUser, 0, 0, (LPBYTE*)&plgrui0, 8192, &dwEntriesRead, &dwIgnore2)) { fHasPermission = (0 != dwEntriesRead); NetApiBufferFree((LPVOID) plgrui0); }
if ((NULL != hwndMsgParent) && (fHasPermission)) { // Display an error; the user doesn't have permission
TCHAR szDomainUser[MAX_DOMAIN + MAX_USER + 2]; MakeDomainUserString(pUserInfo->m_szDomain, pUserInfo->m_szUsername, szDomainUser, ARRAYSIZE(szDomainUser));
DisplayFormatMessage(hwndMsgParent, IDS_USR_NEWUSERWIZARD_CAPTION, IDS_USR_CREATE_USEREXISTS_ERROR, MB_OK | MB_ICONINFORMATION, szDomainUser); }
return fHasPermission; }
|