Visual CSharp .NET Developer's Handbook (2002) [eng]
.pdfanother city or country? Because Active Directory replicates all of the data for the entire database on each domain controller, access is faster, but replication can work against you when it comes to recording changes in the schema or object attributes.
One of the most important tools for working with Active Directory is the ADSI Viewer. This utility enables you to find data elements within the database. In addition, many developers use it to obtain the correct syntax for accessing database elements within an application.
We'll discuss three important programming considerations in this chapter. The following list tells you about each concern:
Security Security is always a concern, especially when you're talking about the configuration data for a large organization.
Binding Microsoft calls the process of gaining access to Active Directory objects binding. You're creating a connection to the object to manipulate the data that it contains.
Managing Users and Groups One of the main tasks that you'll likely perform when working with directory objects is modifying the attributes of groups and users. Even if your application doesn't modify users or groups, you'll interact with them to determine user or group rights and potentially change those rights.
ADSI Viewer
The Active Directory Services Interface (ADSI) Viewer enables you to see the schema for Active Directory. The schema controls the structure of the database. Knowing the schema helps you to work with Active Directory, change its contents, and even add new schema elements. In order to control the kinds of data stored for the applications you create, you must know the Active Directory schema. Otherwise, you could damage the database (given sufficient rights) or, at least, prevent your application from working correctly.
When you first start ADSI Viewer, you'll see a New dialog box that allows you to choose between browsing the current objects in the database or making a specific query. You'll use the browse mode when performing research on Active Directory schema structure. The query approach provides precise information fast when you already know what you need to find.
In most cases, you'll begin your work with Active Directory by browsing through it. This means you'll select the Object Viewer at the New object dialog box. Once you do that, you'll see a New Object dialog box like the one shown in Figure 8.3. Notice that this dialog already has the LDAP path for my server entered into it. If you're using Windows 2000, you can also use a WinNT path.
Figure 8.3: The New object dialog box enables you to create a connection to the server.
This figure shows a sample ADs Path entry. You'll need to supply Active Directory path information, which usually means typing LDAP:// followed by the name of your server (WinServer in my case). If you're using Windows 2000 to access Active Directory, you'll want to clear the Use OpenObject option when working with an LDAP path and check it when using a WinNT path.
Once you've filled in the required information in the New Object dialog box, click OK. If you've entered all of the right information and have the proper rights to access Active Directory, then you'll see a dialog box like the one shown in Figure 8.4. (Note that I've expanded the hierarchical display in this figure.)
Figure 8.4: Opening a new object browser allows you to see the Active Directory schema for your server.
This is where you'll begin learning about Active Directory. On the left side of the display is the hierarchical database structure. Each of these elements is an Active Directory object. Clicking the plus signs next to each object will show the layers of objects beneath. Highlighting an object displays detailed information about it in the right pane. For example, in Figure 8.4 you're seeing the details about the domain object for the server. The heading for this display includes object class information, help file location, and shows whether the object is a container used to hold other objects.
Below the header are the properties for the object. You can choose one of the properties from the Properties list box and see its value in the Property Value field. Active Directory is extensible, which means that you can add new properties to an existing object, change an existing property, or delete properties that you no longer need. If you want to add a new property, all you need to do is type its name in the Properties list box and assign it a value in the Property Value field, then click Append. This doesn't make the change final; however, you still need to click Apply at the bottom of the dialog box. Deleting a property is equally easy. Just select it in the Properties list box, then click Delete. Clicking Apply will make the change final.
Leaf properties often have additional features that you can change. For example, the user object shown in Figure 8.5 helps you to change the user password and determine user group affiliation. When working with a computer object, you'll can determine the computer status and even shut it down if you'd like.
Figure 8.5: Some containers and leaf objects provide special buttons that help you to perform tasks associated with that object.
The method you use to access Active Directory affects the ADSI Viewer display. For example, Figure 8.6 shows the information for the same server using WinNT instead of LDAP for access. Notice that you garner less information in the left pane using WinNT. You'll also find that the WinNT method produces fewer property entries. The advantage of using the WinNT path is that more of the information appears in human-readable form. For example, if you want to check the date the user last logged in under LDAP, you'd better be prepared to convert a 64-bit timer tick value to the time and date. The WinNT version provides this value in human-readable form.
Figure 8.6: Using the WinNT path can make some information easier to read.
Active Directory versus the Registry
With all of the functionality that Active Directory provides, it's tempting to think that it will replace the Registry. In some respects, Active Directory does in fact replace the Registry. You should consider using it wherever an application has global importance, more than one user will require access to a single set of settings, or the information is so critical that you want to ensure that it remains safe. However, placing all your data within Active Directory also presents some problems that you need to consider.
Active Directory is a poor choice for local settings for applications that only matter to the user. For example, no one else is really concerned with a user's settings for a word processor. You gain nothing in the way of shared resource management or data security by storing these settings on the server. In fact, using Active Directory could mean a performance hit in this case because the application will need to access the server every time it needs to change a stored setting.
The performance hit for server access is relatively small for a LAN. However, you have to consider the global nature of networks today. A user on the road is going to be out of communication for some time, which means Active Directory setting changes will languish on the local machine. In short, the Registry is a better choice in many situations where the user data is non-critical.
A secondary performance consideration is one of managed versus unmanaged code and data. When you work with Active Directory, you'll often need to work with unmanaged data and code. Active Directory applications will require some access to the native COM components provided as part of the ADSI interface. Every time the application makes a transition between managed and unmanaged environments, the application suffers a performance penalty.
Using the Registry is also easier than using Active Directory. The example application will demonstrate that working with Active Directory is akin to working with a complex database. The Registry is smaller and easier to understand. It's less likely that you'll experience major bugs when working with the Registry because all of the Registry manipulation functionality you need is contained within the .NET Framework. Because C# performs extensive checks on managed code, you'll find that it also catches more potential problems with your code.
Active Directory Programming Example
The example application performs some essential Active Directory tasks. It will accept a general query for user names that you'll use to select an individual user. The application will use this information to create a specific user query, then display certain information about that user including their department and job title. Active Directory also provides a note field for each user entry that you can use to make comments. The application will enable you to view the current comment and modify it as needed.
The first task is to gain access to Active Directory generally. Listing 8.1 shows the code you'll need to create a general query. Note that this code will work with either a WinNT or an LDAP path. (It could also work with other path types, but hasn't been tested to use them.) Make sure you check the application code found in the \Chapter 08\Monitor folder on the CD.
Listing 8.1: Accessing Active Directory
private void btnQuery_Click(object sender, System.EventArgs e)
{
//Clear the previous query (if any). lvUsers.Items.Clear();
//Add the path information to the DirectoryEntry object. ADSIEntry.Path = txtQuery.Text;
//The query might fail, so add some error checking.
try
{
//Process each DirectoryEntry child of the root
//DirectoryEntry object.
foreach (DirectoryEntry Child in ADSIEntry.Children)
{
// Look for user objects, versus group or service objects. if (Child.SchemaClassName.ToUpper() == "USER")
{
//Fill in the ListView object columns. Note that the
//username is available as part of the DirectoryEntry
//Name property, but that we need to obtain the
//Description using another technique.
ListViewItem lvItem = new ListViewItem(Child.Name); lvItem.SubItems.Add(
Child.Properties["Description"].Value.ToString());
lvUsers.Items.Add(lvItem);
}
}
}
catch (System.Runtime.InteropServices.COMException eQuery)
{
MessageBox.Show("Invalid Query\r\nMessage: " + eQuery.Message +
"\r\nSource: " + eQuery.Source, "Query Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void lvUsers_DoubleClick(object sender, System.EventArgs e)
{
// Create a new copy of the Detail Form. DetailForm ViewDetails =
new DetailForm(lvUsers.SelectedItems[0].Text, lvUsers.SelectedItems[0].SubItems[1].Text, txtQuery.Text);
// Display it on screen. ViewDetails.ShowDialog(this);
}
The application begins with the btnQuery_Click() method. It uses a ListView control to display the output of the query, so the first task is to clear the items in the ListView control. Notice that we specifically clear the items, not the entire control. This prevents corruption of settings such as the list headings.
You can configure all elements of the ADSIEntry (DirectoryEntry) control as part of the design process except the path. The application provides an example path in the txtQuery textbox that you'll need to change to meet your specific server configuration. You can obtain the correct path from the ADsPath field for the ADSI Viewer application—the application will allow you to copy the path to the clipboard using the Ctrl+C key combination. See the "ADSI Viewer" section for details.
The ADSIEntry.Children property is a collection of DirectoryEntry objects. The application won't fail with a bad path until you try to access these DirectoryEntry objects, which is why you want to place the portion of the code in a try…catch block. Notice how the code uses a property string as an index into each DirectoryEntry object. Even if the property is a string, you must use the ToString() method or the compiler will complain. This is because C# views each DirectoryEntry value as an object, regardless of object type.
The output of this portion of the code can vary depending on the path string you supply. Figure 8.7 shows the output for a WinNT path while Figure 8.8 shows the output for a LDAP path. Notice that the actual DirectoryEntry value changes to match the path type. This means you can't depend on specific DirectoryEntry values within your code, even if you're working with the same Active Directory entry. For example, notice how the entry for George changes from WinNT to LDAP. The WinNT entry is simple, while the LDAP entry contains the user's full name and the CN qualifier required for the path.
Figure 8.7: The WinNT path tends to produce easy to read DirectoryEntry values.
Figure 8.8: LDAP paths tend to produce complex DirectoryEntry values that you'll need to clean up for user displays
Once you have access to the user names, it's possible to gain details about a specific user. The sample application performs this task using a secondary form. When a user double-clicks on one of the names, the lvUsers_DoubleClick() method creates a new copy of the secondary form and passes it everything needed to create a detailed query. Notice that the code uses the ShowDialog() method, rather than the Show() method. This ensures that one query completes before the user creates another one.
Most of the activity for the details form occurs in the constructor. The constructor accepts the user name, description, and path as inputs so it can create a detailed query for specific user information. Listing 8.2 shows the constructor code for this part of the example.
Listing 8.2: The Details Form Displays Individual User Information
public DetailForm(string UserName, string Description, string Path)
{
string |
UserPath; |
// |
Path |
to the user object. |
bool |
IsLDAP; |
// |
LDAP |
provides more information. |
//Required for Windows Form Designer support InitializeComponent();
//Set the username and description. lblUserName.Text = "User Name: " + UserName; lblDescription.Text = "Description: " + Description;
// Determine the path type and create a path variable. if (Path.Substring(0, 4) == "LDAP")
{
IsLDAP = true;
//LDAP requires some work to manipulate the path
//string.
int CNPosit = Path.IndexOf("CN"); UserPath = Path.Substring(0, CNPosit) +
UserName + "," +
Path.Substring(CNPosit, Path.Length - CNPosit);
}
else
{
IsLDAP = false;
// A WinNT path requires simple concatenation. UserPath = Path + "/" + UserName;
}
//Set the ADSIUserEntry Path and get user details. ADSIUserEntry.Path = UserPath; ADSIUserEntry.RefreshCache();
//This information is only available using LDAP if (IsLDAP)
{
//Get the user's title.
if (ADSIUserEntry.Properties["Title"].Value == null) lblTitleDept.Text = "Title (Department): No Title";
else
lblTitleDept.Text = "Title (Department): " + ADSIUserEntry.Properties["Title"].Value.ToString();
// Get the user's department.
if (ADSIUserEntry.Properties["Department"].Value == null) lblTitleDept.Text = lblTitleDept.Text + " (No Department)";
else
lblTitleDept.Text = lblTitleDept.Text + " (" + ADSIUserEntry.Properties["Department"].Value.ToString() + ")";
}
//This information is common to both WinNT and LDAP, but uses
//slightly different names.
if (IsLDAP)
{
if (ADSIUserEntry.Properties["lastLogon"].Value == null) lblLogOn.Text = "Last Logon: Never Logged On";
else
{
LargeInteger |
Ticks; |
// COM Time in Ticks. |
long |
ConvTicks; |
// Converted Time in Ticks. |
PropertyCollection |
LogOnTime; |
// Logon Property Collection. |
//Create a property collection. LogOnTime = ADSIUserEntry.Properties;
//Obtain the LastLogon property value.
Ticks = (LargeInteger)LogOnTime["lastLogon"][0];
//Convert the System.__ComObject value to a managed
//value.
ConvTicks = (((long)(Ticks.HighPart) << 32) + (long) Ticks.LowPart);
//Release the COM ticks value. Marshal.ReleaseComObject(Ticks);
//Display the value. lblLogOn.Text = "Last Logon: " +
DateTime.FromFileTime(ConvTicks).ToString();
}
}
else
{
if (ADSIUserEntry.Properties["LastLogin"].Value == null) lblLogOn.Text = "Last Logon: Never Logged On";
else
lblLogOn.Text = "Last Logon: " + ADSIUserEntry.Properties["LastLogin"].Value.ToString();
}
//In a few cases, WinNT and LDAP use the same property names. if (ADSIUserEntry.Properties["HomeDirectory"].Value == null)
lblHomeDirectory.Text = "Home Directory: None"; else
lblHomeDirectory.Text = "Home Directory: " + ADSIUserEntry.Properties["HomeDirectory"].Value.ToString();
//Get the text for the user notes. Works only for LDAP.
if (IsLDAP)
{
if (ADSIUserEntry.Properties["Info"].Value != null) txtNotes.Text =
ADSIUserEntry.Properties["Info"].Value.ToString();
// Enable the Update button. btnUpdate.Visible = true;
}
else
{
txtNotes.Text = "Note Feature Not Available with WinNT";
}
}
The application requires two methods for creating the path to the user directory entry. The WinNT path is easy—just add the UserName to the existing Path. The LDAP path requires a little more work in that the user name must appear as the first "CN=" value in the path string. Here's an example of an LDAP formatting user directory entry path.
LDAP://WinServer/CN=George W. Smith,CN=Users,DC=DataCon,DC=domain
Notice that the server name appears first, then the user name, followed by the group, and finally the domain. You must include the full directory entry name as presented in the ADSI
Viewer utility. This differs from the presentation for a WinNT path, which includes only the user's logon name.
The process for adding the path to the DirectoryEntry control, ADSIUserEntry, is the same as before. In this case, the control is activated using the RefreshCache() method. Calling RefreshCache() ensures that the local control contains the property values for the user in question.
LDAP does provide access to a lot more properties than WinNT. The example shows just two of the additional properties in the form of the user's title and department name. While WinNT provides access to a mere 25 properties, you'll find that LDAP provides access to 56 or more. Notice that each property access relies on checks for null values. Active Directory uses null values when a property doesn't have a value, rather than set it to a default value such as 0 or an empty string.
WinNT and LDAP do have some overlap in the property values they provide. In some cases, the properties don't have precisely the same name, so you need to extract the property value depending on the type of path used to access the directory entry. Both WinNT and LDAP provide access to the user's last logon, but WinNT uses LastLogin, while LDAP uses lastLogon.
WinNT normally provides an easy method for accessing data values that CLR can understand. In the case of the lastLogon property, LDAP presents some challenges. This is one case where you need to use the COM access method. Notice that the lastLogon property requires use of a LargeInteger (defined in the ACTIVEDS.TLB file). If you view the property value returned by the lastLogon property, you'll see that it's of the System.__ComObject type. This type always indicates that CLR couldn't understand the value returned by COM. Notice that the code converts the COM value to a managed type, then releases the COM object using Marshal.ReleaseComObject(). If you don't release the object, your application will have a memory leak—so memory allocation problems aren't quite solved in .NET, they just don't occur when using managed types. The final part of the conversion process is to change the number of ticks into a formatted string using the DateTime.FromFileTime() method.
As previously mentioned, the sample application shows how to present and edit one of the user properties. The Info property is only available when working with LDAP, so the code only accesses the property if you're using an LDAP path. The code also enables an Update button when using an LDAP path, so you can update the value in Active Directory. Here's the simple code for sending a change to Active Directory.
private void btnUpdate_Click(object sender, System.EventArgs e)
{
//Place the new value in the correct property. ADSIUserEntry.Properties["info"][0] = txtNotes.Text;
//Update the property. ADSIUserEntry.CommitChanges();
}
The application uses a double index when accessing the property to ensure that the updated text from txtNotes appears in the right place. All you need to do to make the change permanent is call CommitChanges(). Note that the change will only take place if the user has sufficient rights to make it. In most cases, COM will ignore any update errors, so you won't
