Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to get a list of users from a server

0.00/5 (No votes)
27 Nov 2001 6  
How to get a list of users and their details from a specified server.

Screenshot

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) {
    // 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;
// **** The New Bit **** //

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:

  1. Set-up the parameters to pass to the function.
  2. Call the function and process the results.
  3. Handle any errors.

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;
            // Use %S, not %s for wide strings

            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: 
        //    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);

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) {
    // 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.

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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here