Update
I'm still no closer to finishing the application that spurred me to write
this article, but I have written several more since. As you can see from the
comments below, I was informed of a potential memory leak (quite some time ago I
have to admit), well now I've finally got round to updating the source to fix
this problem.
Basically, the problem is that the buffer used by NetQueryDisplayInformation
can't be freed until the user has finished with the returned information. So I
changed the buffer to be a static member of the CNetInfo
class and used this
buffer instead. The first thing I did then was to put a call to NetApiBufferFree
in the class destructor. Of course, this doesn't work because an instance isn't
created so the destructor never gets called. So I added a CleanUp
function,
which is also static and should be called by the user when they have finished.
Use of CNetInfo
now looks like this (disregard the same example further down
the article):
USER_LIST pUsers = new USER_LIST;
CString szServer = "\\\\MYSERVER"
DWORD dwResult = ssl_net::CNetInfo::GetUserInfo (pUsers, szServer);
if (ERROR_SUCCESS == dwResult) {
POSITION pos = pUsers->GetHeadPosition ();
while (NULL != pos) {
NET_DISPLAY_USER ndu = pUsers->GetNext (pos);
CString szName, szComment, szFlags, szFullName, szUserID;
szName.Format ("%S", ndu.usri1_name);
szComment.Format ("%S", ndu.usri1_comment);
szFlags.Format ("%d", ndu.usri1_flags);
szFullName.Format ("%S",n du.usri1_full_name);
szUserID.Format ("%d", ndu.usri1_user_id);
TRACE ("%S\n%S\n%d\n%S\n%d\n", ndu.usri1_name,
ndu.usri1_comment, ndu.usri1_flags,
ndu.usri1_full_name, ndu.usri1_user_id);
}
}
else {
CString szErrMsg = ssl_net::CNetInfo::FormatMessage (dwResult);
AfxMessageBox (szErrMsg);
}
delete pUsers;
ssl_net::CNetInfo::CleanUp ();
Security
The other thing noticeable from the comments below is that several people
have had trouble accessing information on another machine. To save myself the
effort of typing the same information and to save others from having to look up
the information, here are the security requirements for
NetQueryDisplayInformation
(the function at the heart of this article):
Security Requirements
Windows NT: No special group membership is required to successfully
execute the NetQueryDisplayInformation
function.
Windows 2000: If you call this function on a domain controller that is
running Active Directory, access is allowed or denied based on the
access-control list (ACL) for the securable object. The default ACL permits all
authenticated users and members of the "Pre-Windows 2000 compatible access"
group to view the information. By default, the "Pre-Windows 2000 compatible
access" group includes Everyone as a member. This enables anonymous access to
the information if the system allows anonymous access.
If you call this function on a member server or workstation, all
authenticated users can view the information. Anonymous access is also permitted
if the RestrictAnonymous
policy setting allows anonymous access.
Windows XP: If you call this function on a domain controller that is
running Active Directory, access is allowed or denied based on the ACL for the
securable object. To enable anonymous access, the user Anonymous must be a
member of the "Pre-Windows 2000 compatible access" group. This is because
anonymous tokens do not include the Everyone group SID by default.
If you call this function on a member server or workstation, all
authenticated users can view the information. Anonymous access is also permitted
if the EveryoneIncludesAnonymous
policy setting allows anonymous access.
Introduction
I am in the middle of writing a multi-user application which has an
administrative component to define the users for the system and their security
access. Rather than have the system administrator enter the users manually, I
thought it would be nice to allow them to choose from existing users of their network.
This functionality didn't ring any bells from my usual
work with the MFCs (though I don't usually do much comm.s or network related
stuff), so I instantly reached for the browser and headed off to the
CodeProject. The Internet/Network section at CodeProject is fairly slim and so I didn't find
any joy there. My next port of call was a series of
searches through my MSDN collection. I didn't find much there, but (as I later found
out) I wasn't really looking in the right place. After a bit more digging and
several more searches on the Internet and through the MSDN collection,
I came across the NetUserEnum
function. A little bit more digging
came up trumps with the NetQueryDisplayInformation
(NQDI) function. This
function is used to return user account, computer or group account information.
These are both part of the Network Management area of the Platform SDK. MSDN
says that the NQDI function should be called "to quickly enumerate account information
for display in user interfaces". Perfect! Exactly what I wanted.
The remainder of this article discusses how to use NetQueryDisplayInformation
to
specifically retrieve user account information, and then presents a class to provide more generic
access to the services it provides.
Function details
First things first, what does it look like? Well, the function prototype is as follows:
NET_API_STATUS NetQueryDisplayInformation(
LPCWSTR ServerName,
DWORD Level,
DWORD Index,
DWORD EntriesRequested,
DWORD PreferredMaximumLength,
LPDWORD ReturnedEntryCount,
PVOID *SortedBuffer);
The return type, NET_API_STATUS
was new to me,
but a quick look at the API documentation revealed that it's a DWORD
. If the
function succeeds, then the return value is ERROR_SUCCESS
(Win32 error code 0).
The documentation incorrectly says that if the function fails, it returns one of the following
error codes:
Value |
Meaning |
ERROR_ACCESS_DENIED |
The user does not have access to the requested
information. |
ERROR_INVALID_LEVEL |
The Level parameter specifies an invalid value. |
ERROR_MORE_DATA |
More entries are available. That is, the last entry
returned in the SortedBuffer parameter is not the last entry
available. To retrieve additional entries, call NetQueryDisplayInformation again with the
Index parameter
set to the value returned in the next_index member of the last
entry in SortedBuffer .
|
In actual fact, the function will also return the Win32
error code if none of the above cases is true. For example, whilst debugging I
kept getting a return value of 1722, which is error code RPC_S_SERVER_UNAVAILABLE
.
The server I was querying wasn't actually active while I was trying to query it.
The first parameter, ServerName
, is a Unicode wide-character
string, so we're going to need to use MultiByteToWideChar
to
convert any standard strings to the required format. It may be
NULL
, in which case the function provides information about the
local computer. If it is not NULL
, then the string must begin with \\.
The only other parameter of any interest at the moment is the Level
parameter. This parameter specifies the information level of the data and can be
one of the following values.
Value |
Meaning |
1 |
Return user account information. The SortedBuffer
parameter points to an array of NET_DISPLAY_USER structures. |
2 |
Return individual computer information. The SortedBuffer parameter points to an array of
NET_DISPLAY_MACHINE structures. |
3 |
Return group account information. The SortedBuffer
parameter points to an array of NET_DISPLAY_GROUP
structures. |
I'm interested in user account information so I need to set this parameter to
level 1, but what on earth is the NET_DISPLAY_USER
structure? Time to reach for
the documentation again. The NET_DISPLAY_USER
structure looks like this:
typedef struct _NET_DISPLAY_USER {
LPWSTR usri1_name;
LPWSTR usri1_comment;
DWORD usri1_flags;
LPWSTR usri1_full_name;
DWORD usri1_user_id;
DWORD usri1_next_index;
} NET_DISPLAY_USER, *PNET_DISPLAY_USER;
I'll leave it to you to look up the details of what each field contains,
but basically the user name is provided in usri1_name
and the full name for that account is provided in usri1_full_name
,
both of which are Unicode wide-character strings.
Function use
So, how do you go about using all this? There are essentially
three main parts to the process:
- Set-up the parameters to pass to the function.
- Call the function and process the results.
- Handle any errors.
Parameter setup looks like this:
CString szServer = "\\\\MYSERVER";
LPWSTR pWideServer;
int nBytesSource = strlen(pString) * 2;
int nWCharNeeded = MultiByteToWideChar (CP_ACP,
MB_PRECOMPOSED, pString, nBytesSource, NULL, 0);
pWideServer = (LPWSTR)GlobalAlloc (GPTR, (nWCharNeeded + 1) * 2);
nWCharNeeded = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pString,
nBytesSource,(LPWSTR)pWideServer, nWCharNeeded);
if (0L == nWCharNeeded) {
pWideServer = NULL;
}
else {
*(LPWSTR)(pWideServer + nWCharNeeded) = L'\0';
}
nIndex = 0;
DWORD dwCount;
void* pBuffer;
NET_DISPLAY_USER* ndu;
DWORD dwResult, i;
To process the results, we need this bit of code, too:
do {
dwResult = NetQueryDisplayInformation ((LPCWSTR)pWideServer,
1, nIndex, 10, 24, &dwCount, &pBuffer);
if ((dwResult == ERROR_SUCCESS) || (dwResult == ERROR_MORE_DATA)) {
for (i = 0, ndu = (NET_DISPLAY_USER*)pBuffer;
i < dwCount; ++i, ++ndu) {
CString szName, szFullName, szComment;
szName.Format("%S", ndu->usri1_name);
szFullName.Format("%S", ndu->usri1_full_name);
szComment.Format ("%S", ndu->usri1_comment);
TRACE ("Name:\t\t" + szName + "\n");
TRACE ("Full Name:\t" + szFullName + "\n");
TRACE ("Comment:\t" + szComment + "\n");
TRACE ("--------------------------------\n");
if (dwCount > 0){
nIndex = ((NET_DISPLAY_USER *)pBuffer)[dwCount - 1].usri1_next_index;
}
}
}
} while (dwResult == ERROR_MORE_DATA);
Initially, this calls NQDI asking for index 0. It will keep calling the function,
incrementing the index to ask for, until there are no more results or an error occurs.
The index for the next item is given in the usri1_next_index
field
of the NET_DISPLAY_USER
structure.
switch (dwResult) {
case ERROR_ACCESS_DENIED:
TRACE ("%s(%i): The user does not have"
" access to the requested information.\n", __FILE__, __LINE__);
break;
case ERROR_INVALID_LEVEL:
TRACE ("%s(%i): The Level parameter specifies"
" an invalid value.\n", __FILE__, __LINE__);
break;
case ERROR_MORE_DATA:
TRACE ("%s(%i): More entries are available.\n", __FILE__, __LINE__);
break;
case ERROR_SUCCESS:
break;
default: {
LPVOID lpMsgBuf;
::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
0,
dwResult,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL );
TRACE ("%s(%i): %s\n", __FILE__, __LINE__, lpMsgBuf);
::LocalFree (lpMsgBuf);
}
break;
}
GlobalFree (pWideServer);
The OO Stuff
Like all good developers, I couldn't leave it there, I had to wrap it all up into
a class! It's quite simple and has three main static functions and one extra in a class
called CNetInfo
:
static DWORD GetUserInfo (USER_LIST* pUsers, LPCSTR pServer = NULL);
static DWORD GetMachineInfo (MACHINE_LIST* pMachines, LPCSTR pServer = NULL);
static DWORD GetGroupInfo (GROUP_LIST* pGroups, LPCSTR pServer = NULL);
static CString FormatMessage (DWORD dwError);
The first three are the main functionality and actually
call NQDI. The last one is a generally useful function that receives a Win32
error code and returns a string containing the text description for that error.
The types USER_LIST
, MACHINE_LIST
and GROUP_LIST
are defined as follows:
#define USER_LIST CList<NET_DISPLAY_USER, NET_DISPLAY_USER&>
#define MACHINE_LIST CList<NET_DISPLAY_MACHINE, NET_DISPLAY_MACHINE&>
#define GROUP_LIST CList<NET_DISPLAY_GROUP, NET_DISPLAY_GROUP>
There is no real need to do this, but I'm lazy and it
saves typing! There are also three defines for the different levels of information,
just for convenience:
#define LEVEL_USER 1
#define LEVEL_MACHINE 2
#define LEVEL_GROUP 3
Using it is fairly simple. Just create a list of the
relevant type, pass a pointer to it and a server name to the relevant function
and then iterate through the resulting list. The other thing
to note is that the class is defined in a namespace called ssl_net
,
so you'll either have to add the line using namespace ssl_net;
,
or fully qualify the class name (like the code below):
USER_LIST pUsers = new USER_LIST;
CString szServer = "\\\\MYSERVER"
DWORD dwResult = ssl_net::CNetInfo::GetUserInfo (pUsers, szServer);
if (ERROR_SUCCESS == dwResult) {
POSITION pos = pUsers->GetHeadPosition ();
while (NULL != pos) {
NET_DISPLAY_USER ndu = pUsers->GetNext (pos);
CString szName, szComment, szFlags, szFullName, szUserID;
szName.Format ("%S", ndu.usri1_name);
szComment.Format ("%S", ndu.usri1_comment);
szFlags.Format ("%d", ndu.usri1_flags);
szFullName.Format ("%S",n du.usri1_full_name);
szUserID.Format ("%d", ndu.usri1_user_id);
TRACE ("%S\n%S\n%d\n%S\n%d\n", ndu.usri1_name,
ndu.usri1_comment, ndu.usri1_flags,
ndu.usri1_full_name, ndu.usri1_user_id);
}
}
else {
CString szErrMsg = ssl_net::CNetInfo::FormatMessage (dwResult);
AfxMessageBox (szErrMsg);
}
delete pUsers;
To make it all work, you will need to include NetInfo.h and link to NetApi32.lib.
The code was written using Visual C++ 6 SP4, but I don't think there are any
VC6 or service pack dependant sections to the code.
The code has been tested under Windows NT 4 SP6a only. The documentation
states that the NQDI function is supported on Windows NT 3.1 or later (which
includes Windows 2000), but it is not supported under Windows 95 or 98. No
mention is made of Windows Me and I don't have access to a machine to test on.
Parting Thoughts
Here ends my first article for the CodeProject. I've enjoyed doing this one,
so hopefully there will be more in the future. Now that I have surmounted this
problem, I'll have to get on and write the rest of my application. No doubt I'll
be browsing CodeProject for more help!