Use NetAPI32 calls to power a GUI utility for adding users to a domain en masse

Suppose you're getting ready to leave work at 4:30 p.m. and your boss stops by your office and says, "I need you to create 50 user accounts by tomorrow at 8:00 a.m." To create large groups of users on a tight deadline, you can use various Perl scripts or Microsoft Windows NT Server 4.0 Resource Kit utilities. (For information about these scripts and utilities, see Mark Minasi, "Netdom's Member Option," May 1999, and "AddUsers," May 1998, and Michael D. Reilly, "NET Commands," November 1997.)

Unlike Windows Scripting Host (WSH) scripts, Perl scripts, and resource kit utilities, Visual Basic (VB) lets you quickly and easily create a GUI with the same look and feel as NT's User Manager. I used VB to write a user-creation utility that I call Bulk User Create (BUC). I designed BUC to add user accounts to a PDC's accounts database. The utility doesn't recognize group memberships; user accounts you add through BUC appear in only the Domain Users group. You can run BUC from any NT machine in the same domain as the PDC you want to add user accounts to. However, the utility passes to the PDC in clear text the user account information you supply. If you run the utility on a remote system, the username and password travel across the network, and a network sniffer might intercept them. You must determine the risk of this occurrence and take the appropriate action to prevent it (e.g., run BUC on the PDC).

BUC uses netapi32.dll functions to perform the core of its work. These functions let you obtain template account information, determine the name of the domain's PDC, and add users. For information about other components of BUC's code, see the sidebar "Additional BUC Components."

Using BUC
BUC demonstrates how you can use NetAPI32 calls in a VB application. The utility lets you enter in text boxes information for user accounts you want to add, then builds a list of the user accounts. When you start BUC, the WinNT Magazine Bulk User Create dialog box, which Screen 1 shows, opens. In this main window, you enter values for the User Name, Full Name, and User Password parameters for each account. After you enter information in a field, you can tab to the next field. After you enter the necessary information for one account, tab to Add and press Enter. Then, tab to Add Users and press Enter to add the user account to the accounts database, or tab past Add Users to User Name and enter another user account. I set the User Name, Full Name, User Password, and Confirm Password fields and the Add and Add Users buttons as tab stops so that you can easily add users. Tab stops don't work for other controls in the main window, but you can left-click to select any of the options. If you enter only the User Name, Full Name, and User Password parameters for an account, BUC sets the account's other parameters to the defaults in Table 1.

You can specify additional account parameters (i.e., Description, User Profile, Logon Script, Home Drive, and Home Path) and select password options in BUC's Advanced User Create Options dialog box, which Screen 2, page 118, shows. To reach this dialog box, select the Use Advanced User Options check box in the WinNT Magazine Bulk User Create dialog box to enable the Set User Options button. Click Set User Options to open the Advanced User Create Options dialog box.

If you want to specify account parameters that the Advanced User Create Options dialog box doesn't offer, you can specify a template account for BUC to copy. If you use a template, BUC ensures that new users' profiles match the template, except for User Name, Full Name, and User Password. To use a template, select the Use Template Account check box in the advanced options screen. Specify the account you want to use in the Use Template Account text box. Then, click Get Info to fill in the new account with values from the template account. You can change the template's parameters by entering values in the advanced options screen's text boxes.

The advanced and template options let you save settings as the default for new users. The Save Settings As Default check box on the advanced options screen lets you specify whether BUC saves the settings you configure or uses them for only the current session. If you select this check box, the utility saves the values in the HKEY_LOCAL_MACHINE\ SOFTWARE\WinNTMag\BUC Registry key. When you finish configuring options, click Done to return to the main screen. If you click Cancel, you'll return to the main screen, but the utility won't retain the settings you configured.

After you specify the values for all the user accounts you want to add, click Add Users on the main screen to add all the user accounts to the PDC's accounts database using the values you entered. If an error occurs during the user-addition process, the program pauses and asks you whether you want to continue adding user accounts or stop the process. The utility sends a pop-up window that tells you it successfully added all the user accounts.

You must be a member of the Account Operators or Administrators group on the PDC to run BUC or use its underlying functions. BUC requires no additional software beyond the VB 5.0 runtime modules.

NetAPI Calls
You can use NetAPI32 calls to build a utility such as BUC. NetAPI functions use data structures to manipulate information. Microsoft defines the types of data structures VB can use (e.g., user, server, group). Many data types have multiple levels. You can think of a data structure's level as the amount of detail the data structure contains. BUC uses the level 3 user data structure (TUser3), which is the most detailed user data structure. To use data structures in VB, you must declare them as user data types (UDTs) so that VB is aware of the proper structure. BUC's TUser3 UDT consists entirely of long integer values. The structure is simply a collection of pointers to the real data, which resides in memory buffers. This method prevents you from having to pass large amounts of data into and out of API functions, and thus reduces application overhead.

Listing 1 shows BUC's declarations of the NetAPI functions that the program uses. The first BUC function that I wrote to call a NetAPI function is GetPrimaryDCName, which Listing 2 shows. GetPrimaryDCName discovers the name of the PDC you want to add accounts to. The NetAPI function that GetPrimaryDCName calls, NetGetDCName, has parameters for the name of the machine that will run BUC, the domain name, and a buffer for storing the pointer to the returned PDC name. GetPrimaryDCName accepts string values for the machine name and domain name and returns a string value for the PDC name. BUC leaves the machine name and domain name parameters empty when it calls GetPrimaryDCName, so the function returns the PDC name for the local machine's domain. To tell BUC to add user accounts to a domain other than the domain you're working in or to run GetPrimaryDCName on a machine other than your local system, you must add a domain name or machine name for BUC to pass to the GetPrimaryDCName calls. GetPrimaryDCName uses the PtrToStr function to dereference the pointer that the function returns. The PtrToStr function accesses the string data that the pointer lgDCNPtr refers to.

The second BUC function that calls a NetAPI function is GetUserInfo, which Listing 3 shows. BUC calls this function when you select the Use Template Account check box in the Advanced User Create Options dialog box. GetUserInfo gathers information about the template account that BUC later uses to create user accounts. GetUserInfo calls the NetAPI function NetUserGetInfo, which accepts values for the name of the machine that runs the function, the username, the data structure level, and a pointer to the buffer that holds the account data.

I created the GetUserInfo function to simplify the NetAPI function. GetUserInfo accepts two string values (i.e., machine name and username) and returns a long integer that is the function error code. GetUserInfo starts by converting the string data that it accepts as input to byte arrays, which helps control how the string data converts to Unicode from ANSI.

After the function calls NetUserGetInfo, it handles a few common errors. Retrieving the data that NetUserGetInfo returns is a little tricky. At callout A in Listing 3, GetUserInfo dereferences the pointer lgBufferPtr to enable access to the pointer's members. Then, the function can dereference structure members individually to retrieve user data.

Because BUC processes many members in the same way, Listing 3 includes only a few examples of how the GetUserInfo function handles each of the data types NetUserGetInfo returns. Callout B in Listing 3 shows how BUC handles the username value that the BUC user provides. The password value is a null string, because NetUserGetInfo doesn't provide a password for an account.

Callout C in Listing 3 shows how BUC retrieves string data from the buffer that NetUserGetInfo returns. First, BUC resizes a byte array to hold the buffer's entire contents. Second, BUC copies the contents of the buffer (i.e., the UserStruct comment buffer) to the array. BUC then converts the byte array to a string and deletes null characters that the function returned. (TrimUniStr removes null characters from the end of a string.) Finally, BUC assigns the data to an array element for later use.

Callout D in Listing 3 shows BUC's logon-hours function. BUC stores logon-hours data as a 21-byte array that represents each hour of the week (21 bytes of 8 bits each equal 168 bits, or 7 * 24 bits). If the value of an hour's bit is 1, the user has permission to log on at that time. The logon-hours function dereferences the byte-array data and checks the bits' values. If all the bits are set to 1 or an error occurs in reading the bit mask data, the function sets no hour restrictions on the user account. Otherwise, the function puts the bit data together in a large bit string and stores it for later use.

Callout E in Listing 3 shows how BUC handles the long integer data that the NetAPI call returns. Long integer values aren't pointers, so BUC doesn't have to dereference them the way it dereferences string data. BUC just needs to retrieve the values so it can use them later. Thus, BUC converts each to a string and stores it in the UserInfo array. You might need to handle integer data differently if you plan to show the returned data to users.

The third and most important BUC function is AddUser, which Listing 4 shows. AddUser calls the NetUserAdd API. Whereas the GetUserInfo function merely obtains the data that NetUserGetInfo returns, AddUser prepares the data that GetUserInfo returns for NetUserAdd to use. Callout A in Listing 4 shows the AddUser function declaration. This portion of the function converts the data to byte arrays and handles the optional parameters. For example, Listing 4 shows the conversion of the stSName and stUName strings to arrays and shows the AddUser function's handling of the TUser3 comment parameter. BUC sets an optional parameter to a null string if the parameter's string doesn't contain any data. Otherwise, BUC sets the parameter to an array that contains the data in the parameter's string. Again, the logon-hours parameter requires a special case. The 168-bit string becomes a byte array for processing.

Callout B in Listing 4 shows an example of how BUC allocates to individual data members buffers and pointers to the buffers that contain the data. The NetAPIBufferAllocate function sets the buffers to the length of the byte array, plus one, to allow space in the buffer for the trailing null character. Then BUC starts filling up the TUser3 data structure with the new values. For example, Listing 4 shows AddUsers entering data into a new account's name, full_name, logon_hours, password_expired, country_code, and code_page parameters. Table 1 contains information about the TUser3 values and their default settings.

Callout C in Listing 4 shows the code that invokes the NetUserAdd API call. First, you see the byte array that represents the server name string. A double backslash (\\) must precede this value. This requirement isn't a problem for BUC, because the program uses the value that GetPrimaryDCName returns, which contains the \\ by default. NetUserAdd accepts the first element in the btSNArray array as a pointer to the name of the PDC. The second parameter is the data structure level. (This value can be 0, 1, 2, or 3, but I've hard-coded 3 into the function call because BUC requires the TUser3 structure.) The next parameter is the name of the buffer that holds the data BUC needs to add to the new account. The last NetUserAdd parameter is the long integer value for the parameter error code. If an error occurs when BUC calls the function, this value points to the parameter that caused the error. The return code for NetUserAdd is in lgResult. A value of 0 in lgResult means the call completed successfully; other values identify an error.

Following the NetUserAdd call is some code to handle errors. lgResult values range from 0 to 6100; the most common errors have values between 2100 and 2700, which are NetAPI-specific error codes. For more information about return code values and what they mean, see the sidebar "Additional BUC Components."

Expanding BUC
Although BUC is useful as is, you can easily improve the utility to make it a full-blown user-management application. You can add group awareness so that the function copies groups from template accounts or lets you specify the group(s) to add the user list to. In addition, you can change the template account selection from a text box to a drop-down list that enumerates users in the domain to populate the list. You can even configure the list to filter for a specific account name prefix (e.g., tem-) and thus show only the template accounts in the drop-down list. You can enable input from or output to a file or ODBC data source. Finally, you can add logging to BUC so that the utility writes to a log file or writes events to the Application log when you add users. You can add BUC's code to VB-aware programming environments to extend existing applications and macros to include user-management functions.