Title: How to get a list of users from a server Author: Derek Lakin Email: derek_lakin@lineone.net Environment: VC++ 5.0-6.0, NT 4.0, Win95/98 Keywords: Network, user, username, NetQueryDisplayInformation, NET_DISPLAY_USER, NET_DISPLAY_MACHINE, NET_DISPLAY_GROUP Level: Intermediate Description: How to get a list of users and their details from a specified server Section General SubSection Internet / Network
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.
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.
So, how do you go about using all this? There are essentially three main parts to the process:
Parameter setup looks like this:
// First we need to convert our string into Unicode wide-character format CString szServer = "\\\\MYSERVER"; // Server to query LPWSTR pWideServer; int nBytesSource = strlen(pString) * 2; // Query the number of WChars required to store the destination string int nWCharNeeded = MultiByteToWideChar (CP_ACP, MB_PRECOMPOSED, pString, nBytesSource, NULL, 0); // Allocate the required amount of space plus 2 more bytes for '\0' pWideServer = (LPWSTR)GlobalAlloc (GPTR, (nWCharNeeded + 1) * 2); // Do the conversion nWCharNeeded = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pString, nBytesSource,(LPWSTR)pWideServer, nWCharNeeded); if (0L == nWCharNeeded) { pWideServer = NULL; } else { *(LPWSTR)(pWideServer + nWCharNeeded) = L'\0'; } nIndex = 0; // Index into the list of user accounts DWORD dwCount; // Returned entry count void* pBuffer; // Buffer to store the results in NET_DISPLAY_USER* ndu; // The actual info we want DWORD dwResult, i; // Function return code and an index
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); // Use %S, not %s for wide strings 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: // Do nothing break; default: { // Some other error, probably RPC related LPVOID lpMsgBuf; // Windows will allocate ::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, 0, dwResult, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language (LPTSTR)&lpMsgBuf, 0, NULL ); TRACE ("%s(%i): %s\n", __FILE__, __LINE__, lpMsgBuf); // Free the buffer that was allocated using LocalAlloc() ::LocalFree (lpMsgBuf); } break; } GlobalFree (pWideServer);
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 classname (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) { // Process the results 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 { // Handle any errors 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.
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!